th-bbcode 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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