th-bbcode 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in bbcode.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,78 @@
1
+ A BBcode parser designed to be used with Ruby on Rails
2
+ ======================================================
3
+ A bbcode parser gem you can include in your rails app to parse bbcode-formatted
4
+ strings to HTML or any other format you like.
5
+
6
+ The bbcode gem consists of 4 parts:
7
+
8
+ - The `Tokenizer`-class, which converts the bbcode-formatted string to a stream
9
+ of tokens.
10
+ - The `Parser`-class, which attempts to pair bbcode tags to bbcode elements.
11
+ - The `Handler`-class, which converts bbcode elements anyway you like.
12
+ - The `Helpers`-module, which adds a method to String, allowing you to convert
13
+ bbcode-formatted strings with a registered handler.
14
+
15
+ Additionally, a `HtmlHandler` class is available. This class is a Handler
16
+ designed to convert bbcode elements to HTML more easily.
17
+
18
+ Installation:
19
+ -------------
20
+ Add the gem to the gemfile of your project.
21
+ (todo: add examples)
22
+
23
+ Usage:
24
+ ------
25
+ Create and register a handler. In this example, I'm creating a HtmlHandler and
26
+ I'm going to register it as `:html`.
27
+
28
+ ```ruby
29
+ Bbcode::Base.register_handler :html, Bbcode::HtmlHandler.new(
30
+ :b => :strong,
31
+ :i => :em,
32
+ :url => [ :a, { :href => "%{0}" } ],
33
+ :txt => ->(element){ "#{element.content.source}" },
34
+ :img => ->(element){ %(<img src="#{CGI.escapeHTML(element.content.source)}">) },
35
+ :color => [ :span, { :style => "color: %{0};" } ]
36
+ )
37
+ ```
38
+
39
+ That's it! You can now parse any string as bbcode and convert it to html with
40
+ the `:html`-handler like this:
41
+
42
+ ```ruby
43
+ "[b]Hello, bold world![/]".as_bbcode.to :html
44
+ # => <strong>Hello, bold world!</strong>
45
+ ```
46
+
47
+ If you're using this gem in a rails project, I would recommend registering your
48
+ handlers in an initializer.
49
+
50
+ See examples in `spec/` folder for detailed examples of usage.
51
+
52
+ Features:
53
+ ---------
54
+ * Parsing regular bbcode tags like `[b]` and `[/b]`.
55
+ * Parsing anonymous closing bbcode tags like `[/]`.
56
+ * Parsing bbcode tags with arguments like `[a=foo, bar]`, `[a foo=1 bar:2]`,
57
+ `[a=foo, bar bar:1 foo=2]` and `[a="foo" b='bar']`.
58
+ * Parsing nested bbcode elements like `[b]bold[i]and italic[/]only bold[/]`,
59
+ which might result to `<b>bold<i>and italic</i>only bold</b>`.
60
+ * Parsing incorrectly nested bbcode elements like `[b]bold[i]and italic[/b]only
61
+ italic[/]`, which might result to `<b>bold<i>and italic</i></b><i>only
62
+ italic</i>`.
63
+
64
+ Todo:
65
+ -----
66
+ * An easier way to handle text around bbcode tags to, for example, add smileys
67
+ and wrap hyperlinks to URLs. Currently, the only way to achieve this is by
68
+ adding a `:"#text"`-handler to your handler and adding the functionality
69
+ yourself.
70
+ * An easier way to include the content, source or content-source in the
71
+ `HtmlHandler`-class.
72
+ * Review handleability of element interrupts.
73
+ * Review regular expression matching bbcode tags to allow tags having names
74
+ containing characters other than `A-Z`, `0-9`, `_` and `-`, possibly based on
75
+ the current registered tags.
76
+ * Add CDATA-like feature for bbcode tags to allow tags to be ignored within
77
+ certain elements. Useful for `[code]`-tags.
78
+ * Add a default handler with the most common bbcode tags.
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ # Must... Resist... Deleting... File with unkown use...
2
+
3
+ require 'bundler/gem_tasks'
data/bbcode.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "bbcode/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "th-bbcode"
7
+ s.version = Bbcode::VERSION
8
+ s.authors = ["Toby Hinloopen"]
9
+ s.email = ["toby@kutcomputers.nl"]
10
+ s.homepage = ""
11
+ s.summary = %q{BBCode parser}
12
+ s.description = %q{Gem for parsing bbcode-formatted strings to HTML or any other formatting you like (or don't like).}
13
+
14
+ s.rubyforge_project = "bbcode"
15
+
16
+ s.add_development_dependency "rspec", "~> 2.6"
17
+ s.add_dependency "activesupport", "~> 3.0.9"
18
+ s.add_dependency "actionpack", "~> 3.0.9"
19
+ s.add_dependency "i18n", "~> 0.5.0"
20
+
21
+ s.files = `git ls-files`.split("\n")
22
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
23
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
24
+ s.require_paths = ["lib"]
25
+ end
@@ -0,0 +1,22 @@
1
+ module Bbcode
2
+ class Base
3
+ @@handlers = {}.with_indifferent_access
4
+
5
+ def initialize( string )
6
+ @string = string
7
+ end
8
+
9
+ def to( handler )
10
+ handler = @@handlers[handler]
11
+ raise "Handler #{handler} isn't registered" if handler.blank?
12
+ Parser.new(Tokenizer.new).parse @string, handler
13
+ result = handler.get_document.content.to_s
14
+ handler.clear
15
+ result
16
+ end
17
+
18
+ def self.register_handler( name, handler )
19
+ @@handlers[name] = handler
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ require "bbcode/node_list"
2
+
3
+ module Bbcode
4
+ class Element
5
+ def initialize( handler_element )
6
+ @handler_element = handler_element
7
+ end
8
+
9
+ def tagname
10
+ @handler_element.tagname
11
+ end
12
+
13
+ def attributes
14
+ @handler_element.attributes
15
+ end
16
+
17
+ def []( key )
18
+ @handler_element.attributes[key]
19
+ end
20
+
21
+ def source
22
+ @handler_element.source
23
+ end
24
+
25
+ def source_wraps_content( content = nil )
26
+ "#{@handler_element.start_source}#{content || self.content}#{@handler_element.end_source}"
27
+ end
28
+
29
+ def content
30
+ NodeList.new @handler_element.handler, @handler_element.childs.map{ |child_handler_element| child_handler_element.is_a?(String) ? child_handler_element : child_handler_element.element }
31
+ end
32
+
33
+ def to_s
34
+ @handler_element.handler.get_element_handler(tagname).call(self)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,71 @@
1
+ require "bbcode/handler_element"
2
+
3
+ module Bbcode
4
+ class Handler
5
+ attr_accessor :element_handlers
6
+
7
+ def initialize( element_handlers = nil )
8
+ @element_handlers = {}.with_indifferent_access
9
+ @handler_element_stack = []
10
+ self.clear
11
+ register_element_handlers element_handlers unless element_handlers.blank?
12
+ @interruption_stack = []
13
+ end
14
+
15
+ def register_element_handlers( element_handlers )
16
+ element_handlers.each do |k, v|
17
+ register_element_handler k, v
18
+ end
19
+ end
20
+
21
+ def register_element_handler( name, handler )
22
+ @element_handlers[name] = handler
23
+ end
24
+
25
+ def start_element( tagname, attributes, source )
26
+ handler_element = HandlerElement.new self, tagname, attributes, source
27
+ current_handler_element.childs << handler_element
28
+ @handler_element_stack << handler_element
29
+ end
30
+
31
+ def interrupt_element( tagname )
32
+ # TODO: Add better way to handle interrupts
33
+ @interruption_stack << current_handler_element
34
+ end_element tagname, ""
35
+ end
36
+
37
+ def continue_element( tagname )
38
+ # TODO: Add better way to handle interrupts
39
+ handler_element = @interruption_stack.pop
40
+ start_element handler_element.tagname, handler_element.attributes, ""
41
+ end
42
+
43
+ def end_element( tagname, source )
44
+ raise "Unexpected end of #{tagname.inspect}, expected #{current_handler_element.tagname.inspect}" if tagname != current_handler_element.tagname
45
+ current_handler_element.end_element source
46
+ @handler_element_stack.pop
47
+ end
48
+
49
+ def text( text )
50
+ current_handler_element.childs << text
51
+ end
52
+
53
+ def get_document
54
+ @handler_element_stack.first.element
55
+ end
56
+
57
+ def clear
58
+ @handler_element_stack.clear
59
+ @handler_element_stack << HandlerElement.new( self, :"#document", {}, "" )
60
+ end
61
+
62
+ def get_element_handler( name )
63
+ @element_handlers[name] || ->(element){ element.is_a?(String) ? element : element.source_wraps_content }
64
+ end
65
+
66
+ protected
67
+ def current_handler_element
68
+ @handler_element_stack.last
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,27 @@
1
+ require "bbcode/element"
2
+
3
+ module Bbcode
4
+ # Private data source for an Element updated by the Handler
5
+ class HandlerElement
6
+ attr_accessor :childs
7
+ attr_reader :element, :tagname, :attributes, :handler, :start_source, :end_source
8
+
9
+ def initialize( handler, tagname, attributes, start_source )
10
+ @handler = handler
11
+ @tagname = tagname
12
+ @attributes = attributes
13
+ @start_source = start_source
14
+ @end_source = nil
15
+ @childs = []
16
+ @element = Element.new(self)
17
+ end
18
+
19
+ def end_element( source )
20
+ @end_source = source
21
+ end
22
+
23
+ def source
24
+ "#{@start_source}#{@element.content.source}#{@end_source}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ module Bbcode
2
+ module Helpers
3
+ def as_bbcode
4
+ Base.new self.to_s
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,24 @@
1
+ require "bbcode/handler"
2
+
3
+ module Bbcode
4
+ class HtmlHandler < Handler
5
+ def initialize( element_handlers = nil )
6
+ super :"#text" => ->(text){ CGI.escapeHTML(text) }
7
+ register_element_handlers element_handlers unless element_handlers.nil?
8
+ end
9
+
10
+ def register_element_handler( name, handler )
11
+ unless handler.is_a?(Proc)
12
+ args = *handler
13
+ target_tagname = args.shift
14
+ attributes = args.first
15
+ handler = ->(element){
16
+ content_tag(target_tagname, element.content, !attributes ? {} : Hash[attributes.map{ |k, v|
17
+ [k, v.gsub(/%{[^}]+}/) { |m| CGI.escapeHTML element[m[3] == ":" ? m[3...-1].to_sym : m[2...-1].to_i] }]
18
+ }], false)
19
+ }
20
+ end
21
+ super name, handler
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ module Bbcode
2
+ # An array with Elements and strings
3
+ class NodeList < Array
4
+ def initialize( handler, nodes = [] )
5
+ super nodes
6
+ @handler = handler
7
+ end
8
+
9
+ def source
10
+ self.map{ |element| element.is_a?(String) ? element : element.source }.join
11
+ end
12
+
13
+ def to_s
14
+ self.map{ |element|
15
+ @handler.get_element_handler(element.is_a?(String) ? :"#text" : element.tagname).call(element)
16
+ }.join
17
+ end
18
+
19
+ def with_handler( handler )
20
+ NodeList.new handler, self
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,50 @@
1
+ module Bbcode
2
+ # Attempts to pair a stream of tokens created by a tokenizer
3
+ class Parser
4
+ attr_accessor :tokenizer
5
+
6
+ def initialize( tokenizer = nil )
7
+ @tags_stack = []
8
+ self.tokenizer = tokenizer unless tokenizer.blank?
9
+ end
10
+
11
+ def tokenizer=( tokenizer )
12
+ raise "#{tokenizer.inspect} appears not to be a valid tokenizer for it does not respond to :tokenize" unless tokenizer.respond_to?(:tokenize)
13
+ @tokenizer = tokenizer
14
+ end
15
+
16
+ def text( text )
17
+ @handler.send :text, text
18
+ end
19
+
20
+ def start_element( tagname, attributes, source )
21
+ @tags_stack << tagname
22
+ @handler.send :start_element, tagname, attributes, source
23
+ end
24
+
25
+ def end_element( tagname, source )
26
+ return @tags_stack.last.blank? ? self.text(source) : end_element(@tags_stack.last, source) if tagname.blank?
27
+ return self.text(source) unless @tags_stack.include?(tagname)
28
+
29
+ @interruption_stack = []
30
+ while @tags_stack.last != tagname do
31
+ @interruption_stack << @tags_stack.last
32
+ @handler.send :interrupt_element, @tags_stack.pop
33
+ end
34
+
35
+ @handler.send :end_element, @tags_stack.pop, source
36
+
37
+ while !@interruption_stack.empty? do
38
+ @tags_stack << @interruption_stack.last
39
+ @handler.send :continue_element, @interruption_stack.pop
40
+ end
41
+ end
42
+
43
+ def parse( document, handler )
44
+ @handler = handler
45
+ @tokenizer.tokenize document do |*args|
46
+ self.send *args if [:start_element, :end_element, :text].include?(args.first)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,74 @@
1
+ module Bbcode
2
+ # Scans a string and converts it to a stream of bbcode tokens.
3
+ class Tokenizer
4
+ BBCODE_TAG_PATTERN = /\[(\/?)([a-z0-9_-]*)(\s*=?(?:(?:\s*(?:(?:[a-z0-9_-]+)|(?<=\=))\s*[:=]\s*)?(?:"[^"\\]*(?:\\[\s\S][^"\\]*)*"|'[^'\\]*(?:\\[\s\S][^'\\]*)*'|[^\]\s,]+|(?<=,)(?=\s*,))\s*,?\s*)*)\]/i
5
+ ATTRIBUTE_PATTERN = /(?:\s*(?:([a-z0-9_-]+)|^)\s*[:=]\s*)?("[^"\\]*(?:\\[\s\S][^"\\]*)*"|'[^'\\]*(?:\\[\s\S][^'\\]*)*'|[^\]\s,]+|(?<=,)(?=\s*,))\s*,?/i
6
+ UNESCAPE_PATTERN = /\\(.)/
7
+
8
+ def parse_attributes_string( attributes_string )
9
+ attrs = HashWithIndifferentAccess.new
10
+ return attrs if attributes_string.nil?
11
+
12
+ next_anonymous_key = -1
13
+ attributes_string.scan ATTRIBUTE_PATTERN do |key, value|
14
+ skip_value = key.blank? && value.blank?
15
+ key = next_anonymous_key+=1 if key.blank?
16
+ unless skip_value
17
+ value = value[1...-1].gsub UNESCAPE_PATTERN, "\\1" if value[0] == value[-1] && ["'", '"'].include?(value[0])
18
+ attrs[key] = value
19
+ end
20
+ end
21
+
22
+ return attrs
23
+ end
24
+
25
+ # Parses the document as BBCode-formatted text and calls block with bbcode
26
+ # events.
27
+ #
28
+ # The block is called with the following events:
29
+ # - :text, text
30
+ # A text-event with an additional parameter containing the actual text.
31
+ # - :start_element, element_name, element_arguments
32
+ # An element-event with 2 additional parameters: The element name as a
33
+ # symbol and the element attributes as a hash. This events indicate the
34
+ # start of the element.
35
+ # - :end_element, element_name
36
+ # An element-event indicating the end of an element. Optionally, the
37
+ # element_name is added as a parameter. If no parameter is present, it is
38
+ # assumed to be the last started element.
39
+ #
40
+ # Note that :start_element and :end_element are not guaranteed to be called
41
+ # evenly or in the "correct" order. You must match correct start- and end
42
+ # tags yourself to create the elements.
43
+ #
44
+ # Also note that :text events are not guaranteed to match the whole text.
45
+ # In some cases, the text might be separated to multiple :text events, even
46
+ # though there are no nodes in between.
47
+ def tokenize( document, &handler )
48
+ while !(match = BBCODE_TAG_PATTERN.match(document)).nil?
49
+ offset = match.begin(0)
50
+ elem_source = match[0]
51
+
52
+ handler.call :text, document[0...offset] unless offset == 0
53
+
54
+ elem_is_closing_tag = match[1]=='/'
55
+ elem_name = (match[2].length > 0 && match[2].to_sym) || nil
56
+ elem_attr_string = (match[3].length > 0 && match[3]) || nil
57
+
58
+ if (elem_is_closing_tag && !elem_attr_string) || (!elem_is_closing_tag && elem_name)
59
+ if !elem_is_closing_tag
60
+ handler.call :start_element, elem_name, parse_attributes_string(elem_attr_string), elem_source
61
+ else
62
+ handler.call :end_element, elem_name, elem_source
63
+ end
64
+ else
65
+ handler.call :text, elem_source
66
+ end
67
+
68
+ document = document[(offset+elem_source.length)..-1]
69
+ end
70
+
71
+ handler.call :text, document unless document.length == 0
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,3 @@
1
+ module Bbcode
2
+ VERSION = "0.4.0"
3
+ end
data/lib/bbcode.rb ADDED
@@ -0,0 +1,11 @@
1
+ require "bbcode/version"
2
+ require "bbcode/tokenizer"
3
+ require "bbcode/parser"
4
+ require "bbcode/handler"
5
+ require "bbcode/html_handler"
6
+ require "bbcode/base"
7
+ require "bbcode/helpers"
8
+
9
+ module Bbcode
10
+ String.send :include, Bbcode::Helpers
11
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper.rb'
2
+
3
+ describe Bbcode::Base do
4
+ it "should enable a handler to be registered and used" do
5
+ Bbcode::Base.register_handler :test, Bbcode::Handler.new
6
+ Bbcode::Base.new("test").to(:test).should eql("test")
7
+ end
8
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper.rb'
2
+
3
+ def get_handled_parser_result( string )
4
+ quote_handler = Bbcode::Handler.new()
5
+
6
+ handler = Bbcode::Handler.new({
7
+ :b => ->(element){ "<strong>#{element.content}</strong>" },
8
+ :i => ->(element){ "<em>#{element.content}</em>" },
9
+ :url => ->(element){ %(<a href="#{CGI.escapeHTML(element[0])}">#{element.content}</a>) },
10
+ :txt => ->(element){ "#{CGI.escapeHTML(element.content.source)}" },
11
+ :img => ->(element){ %(<img src="#{CGI.escapeHTML(element.content.source)}">) },
12
+ :quote => ->(element){ %(<blockquote>#{element.content.with_handler(quote_handler)}</blockquote>) },
13
+ :color => ->(element){ %(<span style="color: #{CGI.escapeHTML(element[0])};">#{element.content}</span>) },
14
+ :"#text" => ->(text){ CGI.escapeHTML(text) }
15
+ })
16
+
17
+ quote_handler.register_element_handlers handler.element_handlers.merge({
18
+ :img => ->(element){ %(<a href="#{element.content.source}">image</a>) },
19
+ :quote => ->(element){ "[...]" }
20
+ })
21
+
22
+ parser = Bbcode::Parser.new Bbcode::Tokenizer.new
23
+ parser.parse string, handler
24
+ "#{handler.get_document.content}"
25
+ end
26
+
27
+ describe Bbcode::Handler do
28
+ it "should handle text without bbcode" do
29
+ get_handled_parser_result("Hello, World!").should eql("Hello, World!")
30
+ end
31
+
32
+ it "should handle the text handler" do
33
+ get_handled_parser_result("&").should eql("&amp;")
34
+ end
35
+
36
+ it "should handle a bbcode tag" do
37
+ get_handled_parser_result("[b]bold[/]").should eql("<strong>bold</strong>")
38
+ end
39
+
40
+ it "should ignore unregistered elements" do
41
+ get_handled_parser_result("[foo]text[/foo]").should eql("[foo]text[/foo]")
42
+ end
43
+
44
+ it "should handle bbcode inside an ignored element" do
45
+ get_handled_parser_result("[foo][b]bold[/b][/foo]").should eql("[foo]<strong>bold</strong>[/foo]")
46
+ end
47
+
48
+ it "should handle nested bbcode tags" do
49
+ get_handled_parser_result("[b]bold and [i]italic[/][/]").should \
50
+ eql("<strong>bold and <em>italic</em></strong>")
51
+ end
52
+
53
+ it "should handle attributes in a bbcode tag" do
54
+ get_handled_parser_result("[url=http://google.com/]google[/url]").should \
55
+ eql(%(<a href="http://google.com/">google</a>))
56
+ end
57
+
58
+ it "should handle basic tag interrupts" do
59
+ get_handled_parser_result("[b]bold[i]and italic[/b]only italic[/i]").should \
60
+ eql("<strong>bold<em>and italic</em></strong><em>only italic</em>")
61
+ end
62
+
63
+ it "should resend attributes in tag interrupts" do
64
+ get_handled_parser_result("[b]bold[color=red]and red[/b]but not bold[/]").should \
65
+ eql(%(<strong>bold<span style="color: red;">and red</span></strong><span style="color: red;">but not bold</span>))
66
+ end
67
+
68
+ it "should be able to render the source of an element's contents" do
69
+ get_handled_parser_result("[txt][b]ignored element[/b][/txt]").should \
70
+ eql("[b]ignored element[/b]")
71
+ end
72
+
73
+ it "should be able to switch handlers" do
74
+ get_handled_parser_result("[quote][img]epic.jpg[/img][/quote][img]epic.jpg[/img]").should \
75
+ eql(%(<blockquote><a href="epic.jpg">image</a></blockquote><img src="epic.jpg">))
76
+ end
77
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper.rb'
2
+
3
+ quote_handler = Bbcode::Handler.new()
4
+
5
+ handler = Bbcode::Handler.new({
6
+ :b => ->(element){ "<strong>#{element.content}</strong>" },
7
+ :i => ->(element){ "<em>#{element.content}</em>" },
8
+ :url => ->(element){ %(<a href="#{CGI.escapeHTML(element[0])}">#{element.content}</a>) },
9
+ :txt => ->(element){ "#{element.content.source}" },
10
+ :img => ->(element){ %(<img src="#{element.content.source}">) },
11
+ :quote => ->(element){ %(<blockquote>#{element.content.with_handler(quote_handler)}</blockquote>) },
12
+ :color => ->(element){ %(<span style="color: #{CGI.escapeHTML(element[0])};">#{element.content}</span>) },
13
+ :"#text" => ->(text){ CGI.escapeHTML(text) }
14
+ })
15
+
16
+ quote_handler.register_element_handlers handler.element_handlers.merge({
17
+ :img => ->(element){ %(<a href="#{element.content.source}">image</a>) },
18
+ :quote => ->(element){ "[...]" }
19
+ })
20
+
21
+ Bbcode::Base.register_handler :html, handler
22
+ Bbcode::Base.register_handler :text, Bbcode::Handler.new()
23
+
24
+ describe Bbcode::Helpers do
25
+ it "should enable a string to use a registered helper" do
26
+ "test".as_bbcode.to(:text).should eql("test")
27
+ end
28
+
29
+ it "should be able to parse for a second time without being affected by the first" do
30
+ "test 2".as_bbcode.to(:text).should eql("test 2")
31
+ end
32
+
33
+ it "should enable a string to be parsed as bbcode" do
34
+ "[b]bold[/]".as_bbcode.to(:html).should eql("<strong>bold</strong>")
35
+ end
36
+
37
+ it "should be able to process non-ascii characters" do
38
+ # load UTF-8 content from a file and parse it
39
+ end
40
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper.rb'
2
+
3
+ def get_handled_html_parser_result( string )
4
+ quote_handler = Bbcode::HtmlHandler.new()
5
+
6
+ handler = Bbcode::HtmlHandler.new({
7
+ :b => :strong,
8
+ :i => :em,
9
+ :url => [ :a, { :href => "%{0}" } ],
10
+ :txt => ->(element){ "#{CGI.escapeHTML(element.content.source)}" },
11
+ :img => ->(element){ %(<img src="#{CGI.escapeHTML(element.content.source)}">) },
12
+ :quote => ->(element){ %(<blockquote>#{element.content.with_handler(quote_handler)}</blockquote>) },
13
+ :color => [ :span, { :style => "color: %{0};" } ]
14
+ })
15
+
16
+ quote_handler.register_element_handlers handler.element_handlers.merge({
17
+ :img => ->(element){ %(<a href="#{CGI.escapeHTML(element.content.source)}">image</a>) },
18
+ :quote => ->(element){ "[...]" }
19
+ })
20
+
21
+ parser = Bbcode::Parser.new Bbcode::Tokenizer.new
22
+ parser.parse string, handler
23
+ "#{handler.get_document.content}"
24
+ end
25
+
26
+ describe Bbcode::HtmlHandler do
27
+ it "should handle text without bbcode" do
28
+ get_handled_html_parser_result("Hello, World!").should eql("Hello, World!")
29
+ end
30
+
31
+ it "should escape text" do
32
+ get_handled_html_parser_result("&").should eql("&amp;")
33
+ end
34
+
35
+ it "should handle a bbcode tag" do
36
+ get_handled_html_parser_result("[b]bold[/]").should eql("<strong>bold</strong>")
37
+ end
38
+
39
+ it "should handle nested bbcode tags" do
40
+ get_handled_html_parser_result("[b]bold and [i]italic[/][/]").should \
41
+ eql("<strong>bold and <em>italic</em></strong>")
42
+ end
43
+
44
+ it "should handle attributes in a bbcode tag" do
45
+ get_handled_html_parser_result("[url=http://google.com/]google[/url]").should \
46
+ eql(%(<a href="http://google.com/">google</a>))
47
+ end
48
+
49
+ it "should handle basic tag interrupts" do
50
+ get_handled_html_parser_result("[b]bold[i]and italic[/b]only italic[/i]").should \
51
+ eql("<strong>bold<em>and italic</em></strong><em>only italic</em>")
52
+ end
53
+
54
+ it "should resend attributes in tag interrupts" do
55
+ get_handled_html_parser_result("[b]bold[color=red]and red[/b]but not bold[/]").should \
56
+ eql(%(<strong>bold<span style="color: red;">and red</span></strong><span style="color: red;">but not bold</span>))
57
+ end
58
+
59
+ it "should be able to render the source of an element's contents" do
60
+ get_handled_html_parser_result("[txt][b]ignored element[/b][/txt]").should \
61
+ eql("[b]ignored element[/b]")
62
+ end
63
+
64
+ it "should be able to switch handlers" do
65
+ get_handled_html_parser_result("[quote][img]epic.jpg[/img][/quote][img]epic.jpg[/img]").should \
66
+ eql(%(<blockquote><a href="epic.jpg">image</a></blockquote><img src="epic.jpg">))
67
+ end
68
+
69
+ it "should be able to escape content within tags" do
70
+ get_handled_html_parser_result("[i]&[/i]").should eql("<em>&amp;</em>")
71
+ end
72
+
73
+ it "should be able to escape content within attributes" do
74
+ get_handled_html_parser_result("[url=http://youtube.com/watch?v=FErzTCzR5N4&feature=]epic tune[/]").should \
75
+ eql(%(<a href="http://youtube.com/watch?v=FErzTCzR5N4&amp;feature=">epic tune</a>))
76
+ end
77
+ end
@@ -0,0 +1,82 @@
1
+ require 'spec_helper.rb'
2
+
3
+ class BlockHandler
4
+ def initialize( block )
5
+ @block = block
6
+ end
7
+
8
+ def method_missing(*args)
9
+ @block.call *args
10
+ end
11
+ end
12
+
13
+ def get_parser_results(string, strip_source = true)
14
+ parser = Bbcode::Parser.new Bbcode::Tokenizer.new
15
+ results = []
16
+ parser.parse string, BlockHandler.new(->(*args) {
17
+ args.pop if strip_source && [:end_element, :start_element].include?(args.first) # pop the source
18
+ results.push args
19
+ })
20
+ results
21
+ end
22
+
23
+ describe Bbcode::Parser do
24
+ it "should parse text without bbcode" do
25
+ get_parser_results("Hello, world!")[0].should eql([:text, "Hello, world!"])
26
+ end
27
+
28
+ it "should parse a simple bbcode element" do
29
+ get_parser_results("[b]text[/b]").should \
30
+ eql([ [ :start_element, :b, {} ],
31
+ [ :text, "text" ],
32
+ [ :end_element, :b ] ])
33
+ end
34
+
35
+ it "should parse a simple bbcode element with shorthand closing tag" do
36
+ get_parser_results("[b]text[/]").should \
37
+ eql([ [ :start_element, :b, {} ],
38
+ [ :text, "text" ],
39
+ [ :end_element, :b ] ])
40
+ end
41
+
42
+ it "should provide the actual sourcecode of the elements" do
43
+ get_parser_results("[b a = 1, b:2, c='1'][/][url=http://www.google.com/][/url]", false).should \
44
+ eql([ [ :start_element, :b, { :a => "1", :b => "2", :c => "1" }.with_indifferent_access, "[b a = 1, b:2, c='1']"],
45
+ [ :end_element, :b, "[/]" ],
46
+ [ :start_element, :url, { 0 => "http://www.google.com/" }, "[url=http://www.google.com/]" ],
47
+ [ :end_element, :url, "[/url]" ] ])
48
+ end
49
+
50
+ it "should fire an interrupt for incorrect nested elements" do
51
+ get_parser_results("[b]bold[i]and italic[/b]but not bold[/i]nor italic").should \
52
+ eql([ [ :start_element, :b, {} ],
53
+ [ :text, "bold" ],
54
+ [ :start_element, :i, {} ],
55
+ [ :text, "and italic" ],
56
+ [ :interrupt_element, :i ],
57
+ [ :end_element, :b ],
58
+ [ :continue_element, :i ],
59
+ [ :text, "but not bold" ],
60
+ [ :end_element, :i ],
61
+ [ :text, "nor italic" ] ])
62
+ end
63
+
64
+ it "should fire multiple interrupts for multiple incorrect nested elements" do
65
+ get_parser_results("[u]a[b]b[i]c[/u]d[/i]e[/b]").should \
66
+ eql([ [ :start_element, :u, {} ],
67
+ [ :text, "a" ],
68
+ [ :start_element, :b, {} ],
69
+ [ :text, "b" ],
70
+ [ :start_element, :i, {} ],
71
+ [ :text, "c" ],
72
+ [ :interrupt_element, :i ],
73
+ [ :interrupt_element, :b ],
74
+ [ :end_element, :u ],
75
+ [ :continue_element, :b ],
76
+ [ :continue_element, :i ],
77
+ [ :text, "d" ],
78
+ [ :end_element, :i ],
79
+ [ :text, "e" ],
80
+ [ :end_element, :b ]])
81
+ end
82
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper.rb'
2
+
3
+ def get_tokenizer_results(string, strip_source = true)
4
+ tokenizer = Bbcode::Tokenizer.new
5
+ results = []
6
+ tokenizer.tokenize string do |*args|
7
+ args.pop if strip_source && [:end_element, :start_element].include?(args.first) # pop the source
8
+ results.push args
9
+ end
10
+ results
11
+ end
12
+
13
+ describe Bbcode::Tokenizer do
14
+ it "should parse text without bbcode" do
15
+ get_tokenizer_results("Hello, world!")[0].should eql([:text, "Hello, world!"])
16
+ end
17
+
18
+ it "should parse a simple bbcode tag" do
19
+ get_tokenizer_results("[b]")[0].should eql([:start_element, :b, {}])
20
+ end
21
+
22
+ it "should provide the actual source of the bbcode tag" do
23
+ get_tokenizer_results("[b a = 1, b:2, c='1'][/][url=http://www.google.com/][/url]", false).should \
24
+ eql([ [ :start_element, :b, { :a => "1", :b => "2", :c => "1" }.with_indifferent_access, "[b a = 1, b:2, c='1']"],
25
+ [ :end_element, nil, "[/]" ],
26
+ [ :start_element, :url, { 0 => "http://www.google.com/" }, "[url=http://www.google.com/]" ],
27
+ [ :end_element, :url, "[/url]" ] ])
28
+ end
29
+
30
+ it "should parse 4 simple bbcode tags with text" do
31
+ get_tokenizer_results("[b]bold[i]and italic[/b]but not bold anymore[/i]nor italic")
32
+ end
33
+
34
+ it "should process text before the first bbcode tag" do
35
+ get_tokenizer_results("text[b]")[0].should eql([:text, "text"])
36
+ end
37
+
38
+ it "should process text after the last bbcode tag" do
39
+ get_tokenizer_results("[b]text")[1].should eql([:text, "text"])
40
+ end
41
+
42
+ it "should parse an anonymous closing tag" do
43
+ get_tokenizer_results("[/]")[0].should eql([:end_element, nil])
44
+ end
45
+
46
+ it "should parse a single, regular unnamed argument" do
47
+ get_tokenizer_results("[url=http://www.google.com/]")[0].should \
48
+ eql([:start_element, :url, { 0 => "http://www.google.com/" }])
49
+ end
50
+
51
+ it "should parse a quoted, unnamed argument without the equals-to sign" do
52
+ get_tokenizer_results("[url'http://www.google.nl/']")[0].should \
53
+ eql([:start_element, :url, { 0 => "http://www.google.nl/" }])
54
+ end
55
+
56
+ it "should parse multiple unnamed arguments" do
57
+ get_tokenizer_results("[video=640, 480,,1]")[0].should \
58
+ eql([:start_element, :video, { 0 => "640", 1 => "480", 3 => "1" }]);
59
+ end
60
+
61
+ it "should parse quoted unnamed arguments with escaped characters" do
62
+ get_tokenizer_results(%([abbr='It\\'s a test', "...a \\"test\\"!"]))[0].should \
63
+ eql([:start_element, :abbr, { 0 => "It's a test", 1 => '...a "test"!' }])
64
+ end
65
+
66
+ it "should parse quoted named arguments with escaped characters" do
67
+ get_tokenizer_results(%([abbr a='It\\'s a test', b: "...a \\"test\\"!"]))[0].should \
68
+ eql([:start_element, :abbr, { :a => "It's a test", :b => '...a "test"!' }.with_indifferent_access])
69
+ end
70
+
71
+ it "should parse mixed unnamed and named arguments" do
72
+ get_tokenizer_results(%([a=foo, bar foo=1 bar:2]))[0].should \
73
+ eql([:start_element, :a, { 0 => "foo", 1 => "bar", :foo => "1", :bar => "2" }.with_indifferent_access])
74
+ end
75
+
76
+ it "should ignore the quotes of an attribute value if the quote-pair is incomplete or incorrect" do
77
+ get_tokenizer_results(%([a "test]))[0].should eql([:start_element, :a, { 0 => "\"test" }])
78
+ end
79
+
80
+ it "should parse various key=value attribute pairs" do
81
+ get_tokenizer_results(%([table width = 600 height=300 background-color= \"black\" background-image =url('image.jpg')]))[0].should \
82
+ eql([:start_element, :table, { :width => "600", :height => "300", :"background-color" => "black", :"background-image" => "url('image.jpg')" }.with_indifferent_access])
83
+ end
84
+
85
+ it "should parse key:value attribute pairs separated with optional comma" do
86
+ get_tokenizer_results(%([alt ding: a, banana: b]))[0].should \
87
+ eql([:start_element, :alt, { :ding => "a", :banana => "b" }.with_indifferent_access])
88
+ end
89
+
90
+ it "should ignore the ] in the attribute value" do
91
+ get_tokenizer_results(%([testing "with a ] in my attribute!"])).should \
92
+ eql([[:start_element, :testing, { 0 => "with a ] in my attribute!" }]])
93
+ end
94
+ end
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'active_support/all'
4
+ require 'action_view/helpers/capture_helper'
5
+ require 'action_view/helpers/tag_helper'
6
+
7
+ include ActionView::Helpers::TagHelper
8
+
9
+ require 'bbcode'
10
+
11
+ RSpec.configure do |config|
12
+ # some (optional) config here
13
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: th-bbcode
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Toby Hinloopen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-11-04 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70176012816200 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '2.6'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70176012816200
25
+ - !ruby/object:Gem::Dependency
26
+ name: activesupport
27
+ requirement: &70176012815280 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 3.0.9
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70176012815280
36
+ - !ruby/object:Gem::Dependency
37
+ name: actionpack
38
+ requirement: &70176012814380 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 3.0.9
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70176012814380
47
+ - !ruby/object:Gem::Dependency
48
+ name: i18n
49
+ requirement: &70176012813460 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 0.5.0
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70176012813460
58
+ description: Gem for parsing bbcode-formatted strings to HTML or any other formatting
59
+ you like (or don't like).
60
+ email:
61
+ - toby@kutcomputers.nl
62
+ executables: []
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - .gitignore
67
+ - Gemfile
68
+ - README.md
69
+ - Rakefile
70
+ - bbcode.gemspec
71
+ - lib/bbcode.rb
72
+ - lib/bbcode/base.rb
73
+ - lib/bbcode/element.rb
74
+ - lib/bbcode/handler.rb
75
+ - lib/bbcode/handler_element.rb
76
+ - lib/bbcode/helpers.rb
77
+ - lib/bbcode/html_handler.rb
78
+ - lib/bbcode/node_list.rb
79
+ - lib/bbcode/parser.rb
80
+ - lib/bbcode/tokenizer.rb
81
+ - lib/bbcode/version.rb
82
+ - spec/lib/bbcode/base_spec.rb
83
+ - spec/lib/bbcode/handler_spec.rb
84
+ - spec/lib/bbcode/helpers_spec.rb
85
+ - spec/lib/bbcode/html_handler_spec.rb
86
+ - spec/lib/bbcode/parser_spec.rb
87
+ - spec/lib/bbcode/tokenizer_spec.rb
88
+ - spec/spec_helper.rb
89
+ homepage: ''
90
+ licenses: []
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ! '>='
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ! '>='
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubyforge_project: bbcode
109
+ rubygems_version: 1.8.11
110
+ signing_key:
111
+ specification_version: 3
112
+ summary: BBCode parser
113
+ test_files:
114
+ - spec/lib/bbcode/base_spec.rb
115
+ - spec/lib/bbcode/handler_spec.rb
116
+ - spec/lib/bbcode/helpers_spec.rb
117
+ - spec/lib/bbcode/html_handler_spec.rb
118
+ - spec/lib/bbcode/parser_spec.rb
119
+ - spec/lib/bbcode/tokenizer_spec.rb
120
+ - spec/spec_helper.rb