model_formatting 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,3 @@
1
+ This is a plugin for Rails 2.2.x that performs automatic text-to-html formatting.
2
+
3
+ It is extracted from and used by [Tender](http://tenderapp.com/) and [Lighthouse](http://lighthouseapp.com/).
data/Rakefile ADDED
@@ -0,0 +1,34 @@
1
+ require 'rake/gempackagetask'
2
+ require 'rake/testtask'
3
+
4
+ spec = Gem::Specification.new do |s|
5
+ s.name = "model_formatting"
6
+ s.version = "0.2.0"
7
+ s.author = "ENTP"
8
+ s.email = "company@entp.com"
9
+ s.homepage = "http://github.com/entp"
10
+ s.platform = Gem::Platform::RUBY
11
+ s.summary = "Automatically format model attributes using rdiscount and Tender/Lighthouse extensions."
12
+ s.files = FileList['[a-zA-Z]*', 'lib/**/*', 'rails/**/*', 'test/**/*']
13
+ s.require_path = "lib"
14
+ s.has_rdoc = false
15
+ s.extra_rdoc_files = ["README"]
16
+ s.add_dependency("rdiscount", "~>1.5.5")
17
+ s.add_dependency("actionpack", "~>2.2.3")
18
+ s.add_dependency("activerecord", "~>2.2.3")
19
+ s.add_dependency("activesupport", "~>2.2.3")
20
+ s.add_dependency("tidy", "~>1.1.2")
21
+ s.add_development_dependency("jeremymcanally-context", "~>0.5.5")
22
+ s.add_development_dependency("jeremymcanally-matchy", "~>0.1.0")
23
+ end
24
+
25
+
26
+ desc 'Build the gem.'
27
+ Rake::GemPackageTask.new(spec) do |pkg|
28
+ pkg.gem_spec = spec
29
+ end
30
+
31
+ Rake::TestTask.new do |t|
32
+ t.test_files = FileList['test/*_test.rb']
33
+ end
34
+
@@ -0,0 +1,205 @@
1
+ require 'cgi'
2
+ module ModelFormatting
3
+ class Part < Array
4
+ attr_reader :format
5
+ def initialize(format, *args)
6
+ @format = format
7
+ @simple_string = @formatted_string = nil
8
+ super(*args)
9
+ end
10
+
11
+ def <<(s)
12
+ @simple_string = @formatted_string = nil
13
+ super
14
+ end
15
+
16
+ def compact!
17
+ @simple_string = @formatted_string = nil
18
+ super
19
+ end
20
+
21
+ def simple_string
22
+ @simple_string ||= join("\n")
23
+ end
24
+
25
+ def formatted_string
26
+ @formatted_string ||= begin
27
+ if @format == :html
28
+ %(<pre><code#{%( class="#{@class_name}") unless @class_name.blank?}>#{simple_string}</code></pre>)
29
+ else
30
+ simple_string
31
+ end
32
+ end
33
+ end
34
+
35
+ def ==(other)
36
+ simple_string == other
37
+ end
38
+
39
+ def inspect
40
+ "#{self.class.name}: #{simple_string.inspect}"
41
+ end
42
+ end
43
+
44
+ class CodePart < Part
45
+ attr_reader :class_name
46
+
47
+ def initialize(format, class_name, *args)
48
+ @class_name = class_name
49
+ super(format, *args)
50
+ end
51
+
52
+ def <<(s)
53
+ super CGI.escapeHTML(s)
54
+ end
55
+ end
56
+
57
+ class FormattedPart < Part
58
+ attr_reader :options
59
+
60
+ def initialize(format, options = {}, *args)
61
+ @options = options || {}
62
+ super(format, *args)
63
+ end
64
+
65
+ def formatted_string
66
+ @formatted_string ||= begin
67
+ str = simple_string
68
+ str = @options[:before].call(@format, str, @options) if @options[:before]
69
+ str
70
+ end
71
+ end
72
+ end
73
+
74
+ def self.process(format, text, options = {})
75
+ parts = parse_text_parts(format, options, text)
76
+ string = parts.map { |p| p.formatted_string } * "\n"
77
+ string.gsub! /\r/, ''
78
+ if format == :html
79
+ string.gsub! /code><\/pre>/, "code>\n</pre>" # do this so markdown registers ending </pre>'s as a linebreak
80
+ string = gfm(string)
81
+ string = process_markdown(string)
82
+ string.gsub! /\s+<\/code>/, '</code>' # clear linebreak preceding closing <code>
83
+ end
84
+ if options[:after]
85
+ extract_tag(string, :pre, :code) do |str|
86
+ str.replace options[:after].call(format, str, options)
87
+ end
88
+ end
89
+ if format == :html
90
+ if options[:white_list]
91
+ string = options[:white_list].sanitize(string)
92
+ end
93
+ string = process_tidy(string)
94
+ end
95
+ string.strip!
96
+ format == :html ? "<div>#{string}</div>" : string
97
+ end
98
+
99
+ CODEBLOCK_RE = /^@@@( ([a-z]+)\s*)?$/
100
+
101
+ # Parse a string into a given array of [CodeBlcok, FormattedBlock, CodeBlock, FormattedBlock]
102
+ #
103
+ # @@@ my code block
104
+ # @@@ end code block
105
+ #
106
+ # Blah blah formatted block
107
+ #
108
+ # @@@ my code block
109
+ # @@@ end code block
110
+ #
111
+ # Blah blah formatted block
112
+ def self.parse_text_parts(format, options, text)
113
+ parts = []
114
+ current_part = nil
115
+ in_code_block = false
116
+ text.split("\n").each do |line|
117
+ if line.rstrip =~ CODEBLOCK_RE
118
+ line.rstrip!
119
+ if in_code_block
120
+ parts << current_part
121
+ current_part = nil
122
+ else
123
+ if current_part then parts << current_part end
124
+ current_part = CodePart.new(format, $2)
125
+ end
126
+ in_code_block = !in_code_block
127
+ else
128
+ if !in_code_block && current_part.is_a?(CodePart)
129
+ parts << current_part
130
+ current_part = nil
131
+ end
132
+ current_part ||= FormattedPart.new(format, options)
133
+ current_part << line
134
+ end
135
+ end
136
+ parts << current_part if current_part
137
+ parts.compact!
138
+ parts.each { |p| p.compact! }
139
+ end
140
+
141
+ def self.extract_tag(text, *tag_names)
142
+ # Extract pre blocks
143
+ extractions = {}
144
+ tag_names.each do |tag_name|
145
+ text.gsub!(%r{<#{tag_name}[^>]*>.*?</#{tag_name}>}m) do |match|
146
+ md5 = Digest::MD5.hexdigest(match)
147
+ extractions[md5] = match
148
+ "{mkd-extraction-#{md5}}"
149
+ end
150
+ end
151
+ yield text
152
+ # Insert pre block extractions
153
+ text.gsub!(/\{mkd-extraction-([0-9a-f]{32})\}/) do
154
+ extractions[$1]
155
+ end
156
+ text
157
+ end
158
+
159
+ def self.gfm(text)
160
+ extract_tag(text, :pre) do |txt|
161
+ # prevent foo_bar_baz from ending up with an italic word in the middle
162
+ text.gsub!(/(^(?! {4}|\t)\w+_\w+_\w[\w_]*)/) do |x|
163
+ x.gsub('_', '\_') if x.split('').sort.to_s[0..1] == '__'
164
+ end
165
+
166
+ # in very clear cases, let newlines become <br /> tags
167
+ #text.gsub!(/(\A|^$\n)(^\w[^\n]*\n)(^\w[^\n]*$)+/m) do |x|
168
+ # x.gsub(/^(.+)$/, "\\1 ")
169
+ text.gsub!(/^[\w\<][^\n]*\n+/) do |x|
170
+ x =~ /\n{2}/ ? x : (x.strip!; x << " \n")
171
+ end
172
+ end
173
+ end
174
+
175
+ begin
176
+ gem 'rdiscount', '>= 1.2.7.1'
177
+ require 'rdiscount'
178
+ def self.process_markdown(text)
179
+ RDiscount.new(text).to_html
180
+ end
181
+ rescue Gem::LoadError
182
+ puts "No RDiscount gem found. `gem install rdiscount`."
183
+ def self.process_markdown(text)
184
+ text
185
+ end
186
+ end
187
+
188
+ begin
189
+ gem 'tidy'
190
+ require 'tidy'
191
+ Tidy.path = ENV['TIDY_PATH'] unless ENV['TIDY_PATH'].blank?
192
+ def self.process_tidy(text)
193
+ return text unless Tidy.path
194
+ Tidy.open(:show_body_only => true, :input_encoding => :utf8) do |tidy|
195
+ tidy.options.new_inline_tags = "video"
196
+ tidy.clean(text)
197
+ end
198
+ end
199
+ rescue Gem::LoadError
200
+ puts "No Tidy gem found. `gem install tidy`. Don't forget to set Tidy.path."
201
+ def self.process_tidy(text)
202
+ text
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,25 @@
1
+ # Used to configure model formatting for a specific class. See ModelFormatting::Init
2
+ class ModelFormatting::Config
3
+ attr_reader :white_list, :attributes, :context, :before_callback, :after_callback
4
+
5
+ def initialize(white_list, attributes, context)
6
+ @before_callback = nil
7
+ @after_callback = nil
8
+ @white_list = white_list
9
+ @attributes = attributes
10
+ @context = context
11
+ end
12
+
13
+ def before(&block)
14
+ @before_callback = block
15
+ end
16
+
17
+ def after(&block)
18
+ @after_callback = block
19
+ end
20
+
21
+ # replace vars in the string with a symbolized key of the same name in the context.
22
+ def replace_vars(string, context)
23
+ string.gsub!(/:([a-z_]+)/) { |m| $1 && $1.size > 0 && context[$1.to_sym] }; string
24
+ end
25
+ end
@@ -0,0 +1,56 @@
1
+ module ModelFormatting::Init
2
+ def self.setup_on(base)
3
+ base.extend self
4
+ end
5
+
6
+ # class Foo < ActiveRecord::Base
7
+ # formats :body => :formatted_body do
8
+ # # add more attributes
9
+ # attributes[:title] = :full_title
10
+ #
11
+ # # add model methods to add to the processing context
12
+ # context << :project_id
13
+ #
14
+ # # modify the sanitizer
15
+ # white_list.allowed_tags << 'form'
16
+ # white_list.allowed_attributes << 'class'
17
+ #
18
+ # # add a callback for before html/markdown is processed
19
+ # before do |format, text, options|
20
+ #
21
+ # end
22
+ #
23
+ # # add a callback for after html/markdown is processed
24
+ # after do |format, text, options|
25
+ #
26
+ # end
27
+ # end
28
+ # end
29
+ #
30
+ def formats(*args, &block)
31
+ unless respond_to?(:model_formatting_attributes)
32
+ # use all these attributes instead of a single ModelFormatting::Config because
33
+ # it's easier to support subclassing.
34
+ class_inheritable_accessor :model_formatting_attributes,
35
+ :model_formatting_white_list, :model_formatting_context,
36
+ :model_formatting_before_callback, :model_formatting_after_callback
37
+ send :include, ModelFormatting::InstanceMethods
38
+ self.model_formatting_context = []
39
+ self.model_formatting_attributes = {}
40
+ self.model_formatting_white_list = HTML::WhiteListSanitizer.new
41
+ before_save :format_content_with_model_formatting
42
+ end
43
+
44
+ model_formatting_attributes.update args.extract_options!
45
+ args.each do |field|
46
+ model_formatting_attributes[field] = "formatted_#{field}"
47
+ end
48
+
49
+ if block
50
+ config = ModelFormatting::Config.new(model_formatting_white_list, model_formatting_attributes, model_formatting_context)
51
+ config.instance_eval &block
52
+ self.model_formatting_before_callback = config.before_callback if config.before_callback
53
+ self.model_formatting_after_callback = config.after_callback if config.after_callback
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,24 @@
1
+ module ModelFormatting
2
+ module InstanceMethods
3
+ protected
4
+ def format_content_with_model_formatting
5
+ self.class.model_formatting_attributes.each do |original, formatted|
6
+ text = send(original)
7
+ data = \
8
+ if text.blank?
9
+ ''
10
+ else
11
+ options = {:white_list => model_formatting_white_list,
12
+ :before => model_formatting_before_callback,
13
+ :after => model_formatting_after_callback}
14
+ model_formatting_context.inject(options) do |o, attribute|
15
+ o.update attribute => send(attribute)
16
+ end
17
+ ModelFormatting.process(:html, text, options)
18
+ end
19
+
20
+ send("#{formatted}=", data)
21
+ end
22
+ end
23
+ end
24
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ ModelFormatting::Init.setup_on ActiveRecord::Base
data/tender.patch ADDED
@@ -0,0 +1,95 @@
1
+ Only in .: .git
2
+ diff -ur ./lib/model_formatting/instance_methods.rb ../tender/vendor/plugins/model_formatting/lib/model_formatting/instance_methods.rb
3
+ --- ./lib/model_formatting/instance_methods.rb 2010-07-09 10:29:14.000000000 -0700
4
+ +++ ../tender/vendor/plugins/model_formatting/lib/model_formatting/instance_methods.rb 2010-06-14 13:45:10.000000000 -0700
5
+ @@ -2,13 +2,12 @@
6
+ module InstanceMethods
7
+ protected
8
+ def format_content_with_model_formatting
9
+ - model_formatting_attributes.each do |original, formatted|
10
+ + self.class.model_formatting_attributes.each do |original, formatted|
11
+ text = send(original)
12
+ data = \
13
+ if text.blank?
14
+ ''
15
+ else
16
+ - text.strip!
17
+ options = {:white_list => model_formatting_white_list,
18
+ :before => model_formatting_before_callback,
19
+ :after => model_formatting_after_callback}
20
+ diff -ur ./lib/model_formatting.rb ../tender/vendor/plugins/model_formatting/lib/model_formatting.rb
21
+ --- ./lib/model_formatting.rb 2010-07-09 11:29:32.000000000 -0700
22
+ +++ ../tender/vendor/plugins/model_formatting/lib/model_formatting.rb 2010-02-19 14:49:00.000000000 -0800
23
+ @@ -138,11 +138,11 @@
24
+ parts.each { |p| p.compact! }
25
+ end
26
+
27
+ - def self.extract_regex(text, *regexes)
28
+ + def self.extract_tag(text, *tag_names)
29
+ # Extract pre blocks
30
+ extractions = {}
31
+ - regexes.each do |regex|
32
+ - text.gsub!(regex) do |match|
33
+ + tag_names.each do |tag_name|
34
+ + text.gsub!(%r{<#{tag_name}[^>]*>.*?</#{tag_name}>}m) do |match|
35
+ md5 = Digest::MD5.hexdigest(match)
36
+ extractions[md5] = match
37
+ "{mkd-extraction-#{md5}}"
38
+ @@ -156,16 +156,6 @@
39
+ text
40
+ end
41
+
42
+ - def self.tag_name_to_regex(name)
43
+ - %r{<#{name}[^>]*>.*?</#{name}>}m
44
+ - end
45
+ -
46
+ - def self.extract_tag(text, *tag_names)
47
+ - extract_regex text, *tag_names.map { |n| tag_name_to_regex(n) } do |text|
48
+ - yield text
49
+ - end
50
+ - end
51
+ -
52
+ def self.gfm(text)
53
+ extract_tag(text, :pre) do |txt|
54
+ # prevent foo_bar_baz from ending up with an italic word in the middle
55
+ @@ -183,13 +173,13 @@
56
+ end
57
+
58
+ begin
59
+ - gem 'rdiscount', '~> 1.2.7.1'
60
+ + gem 'rdiscount', '>= 1.2.7.1'
61
+ require 'rdiscount'
62
+ def self.process_markdown(text)
63
+ RDiscount.new(text).to_html
64
+ end
65
+ rescue Gem::LoadError
66
+ - puts "No RDiscount gem found. `gem install rdiscount -v '~>1.2.7.1'`."
67
+ + puts "No RDiscount gem found. `gem install rdiscount`."
68
+ def self.process_markdown(text)
69
+ text
70
+ end
71
+ @@ -212,4 +202,4 @@
72
+ text
73
+ end
74
+ end
75
+ -end
76
+ +end
77
+
78
+ Only in .: tender.patch
79
+ diff -ur ./test/formatting_test.rb ../tender/vendor/plugins/model_formatting/test/formatting_test.rb
80
+ --- ./test/formatting_test.rb 2010-07-09 10:29:14.000000000 -0700
81
+ +++ ../tender/vendor/plugins/model_formatting/test/formatting_test.rb 2010-02-19 14:49:00.000000000 -0800
82
+ @@ -38,6 +38,13 @@
83
+ record.save
84
+ record.formatted_body.should == %(<div><p>booya</p></div>)
85
+ end
86
+ +
87
+ + it "preserves leading spaces in code blocks" do
88
+ + record = Simple.new
89
+ + record.body = " code\n more code\n\nnot code\n\n"
90
+ + record.save
91
+ + record.formatted_body.should == %(<div><pre><code>code\nmore code</code></pre>\n\n<p>not code</p></div>)
92
+ + end
93
+ end
94
+
95
+ class Post < Base
@@ -0,0 +1,161 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ module ModelFormatting
4
+ class Test < Test::Unit::TestCase
5
+ class Base < Struct.new(:body, :formatted_body, :title, :title_html, :bio, :full_bio)
6
+ ModelFormatting::Init.setup_on self
7
+ class_inheritable_accessor :before_save_callback
8
+
9
+ def self.before_save(method = nil)
10
+ if method
11
+ self.before_save_callback = method
12
+ else
13
+ before_save_callback
14
+ end
15
+ end
16
+
17
+ def save
18
+ send self.class.before_save
19
+ end
20
+ end
21
+
22
+ class Simple < Base
23
+ formats :body
24
+ end
25
+
26
+ describe "Simple with formatting" do
27
+ it "has attribute from #formats arguments" do
28
+ Simple.model_formatting_attributes[:body].should == "formatted_body"
29
+ end
30
+
31
+ it "sets before_save callback" do
32
+ Simple.before_save.should == :format_content_with_model_formatting
33
+ end
34
+
35
+ it "formats all fields" do
36
+ record = Simple.new
37
+ record.body = 'booya'
38
+ record.save
39
+ record.formatted_body.should == %(<div><p>booya</p></div>)
40
+ end
41
+
42
+ it "preserves leading spaces in code blocks" do
43
+ record = Simple.new
44
+ record.body = " code\n more code\n\nnot code\n\n"
45
+ record.save
46
+ record.formatted_body.should == %(<div><pre><code>code\nmore code</code></pre>\n\n<p>not code</p></div>)
47
+ end
48
+ end
49
+
50
+ class Post < Base
51
+ formats :body, :title => :title_html do
52
+ attributes[:bio] = :full_bio
53
+
54
+ white_list.allowed_tags << 'table'
55
+
56
+ before do |format, text, options|
57
+ text.reverse!
58
+ text
59
+ end
60
+
61
+ after do |format, text, options|
62
+ "(#{text.strip})"
63
+ end
64
+ end
65
+ end
66
+
67
+ describe "Post with formatting" do
68
+ it "has attribute from #formats arguments" do
69
+ Post.model_formatting_attributes[:body].should == "formatted_body"
70
+ end
71
+
72
+ it "has attribute added from #formats options hash" do
73
+ Post.model_formatting_attributes[:title].should == :title_html
74
+ end
75
+
76
+ it "has attribute added from #attributes options hash" do
77
+ Post.model_formatting_attributes[:bio].should == :full_bio
78
+ end
79
+
80
+ it "has white list sanitizer that allows table tags" do
81
+ Post.model_formatting_white_list.allowed_tags.should include('table')
82
+ end
83
+
84
+ it "sets before_save callback" do
85
+ Post.before_save.should == :format_content_with_model_formatting
86
+ end
87
+
88
+ describe "being saved" do
89
+ before :all do
90
+ @record = Post.new
91
+ @record.body = 'booya'
92
+ @record.title = 'wtf'
93
+ @record.save
94
+ end
95
+
96
+ it "formats #body" do
97
+ @record.formatted_body.should == %(<div>(<p>ayoob</p>)</div>)
98
+ end
99
+
100
+ it "formats #title" do
101
+ @record.title_html.should == %(<div>(<p>ftw</p>)</div>)
102
+ end
103
+
104
+ it "formats #bio" do
105
+ @record.full_bio.should == ''
106
+ end
107
+ end
108
+ end
109
+
110
+ class ChildPost < Post
111
+ formats do
112
+ attributes.delete :bio
113
+
114
+ before do |format, text, options|
115
+ text.upcase!
116
+ text
117
+ end
118
+
119
+ after do |format, text, options|
120
+ text
121
+ end
122
+ end
123
+ end
124
+
125
+ describe "ChildPost with formatting" do
126
+ it "has attribute from #formats arguments" do
127
+ ChildPost.model_formatting_attributes[:body].should == "formatted_body"
128
+ end
129
+
130
+ it "has attribute added from #formats options hash" do
131
+ ChildPost.model_formatting_attributes[:title].should == :title_html
132
+ end
133
+
134
+ it "removes attribute added from superclass#attributes options hash" do
135
+ ChildPost.model_formatting_attributes.keys.should_not include(:bio)
136
+ end
137
+
138
+ it "sets before_save callback" do
139
+ ChildPost.before_save.should == :format_content_with_model_formatting
140
+ end
141
+
142
+ describe "being saved" do
143
+ before :all do
144
+ @record = ChildPost.new
145
+ @record.body = 'booya'
146
+ @record.bio = 'wtf'
147
+ @record.save
148
+ end
149
+
150
+ it "formats #body" do
151
+ @record.formatted_body.should == %(<div><p>BOOYA</p></div>)
152
+ end
153
+
154
+ it "skips #bio" do
155
+ @record.full_bio.should == nil
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
161
+
@@ -0,0 +1,117 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ class ModelFormattingTest < Test::Unit::TestCase
4
+ it "parses simple string into array of single FormattedBlock" do
5
+ parts = ModelFormatting.parse_text_parts(nil, nil, 'foo')
6
+ parts.size.should == 1
7
+ parts.first.class.should == ModelFormatting::FormattedPart
8
+ parts.first.should == 'foo'
9
+ end
10
+
11
+ it "parses empty code block into array of single CodeBlock" do
12
+ parts = ModelFormatting.parse_text_parts(nil, nil, "@@@ foo\n@@@")
13
+ parts.size.should == 1
14
+ parts.first.class.should == ModelFormatting::CodePart
15
+ parts.first.should == ''
16
+ end
17
+
18
+ it "parses simple code block into array of single CodeBlock" do
19
+ parts = ModelFormatting.parse_text_parts(nil, nil, "@@@ foo\nbar\n@@@")
20
+ parts.size.should == 1
21
+ parts.first.class.should == ModelFormatting::CodePart
22
+ parts.first.should == 'bar'
23
+ end
24
+
25
+ it "parses formatted block followed by code block into array of parts" do
26
+ parts = ModelFormatting.parse_text_parts(nil, nil, "foo \n@@@ foo\nbar\n@@@")
27
+ parts.size.should == 2
28
+ parts.map { |p| p.class }.should == [ModelFormatting::FormattedPart, ModelFormatting::CodePart]
29
+ parts.map { |p| p.simple_string }.should == ["foo ", "bar"]
30
+ end
31
+
32
+ it "parses code block followed by formatted block into array of parts" do
33
+ parts = ModelFormatting.parse_text_parts(nil, nil, "@@@ foo\nbar\n@@@\n foo \n")
34
+ parts.size.should == 2
35
+ parts.map { |p| p.class }.should == [ModelFormatting::CodePart, ModelFormatting::FormattedPart]
36
+ parts.map { |p| p.simple_string }.should == ["bar", " foo "]
37
+ end
38
+
39
+ it "parses formatted block followed by unfinished code block" do
40
+ parts = ModelFormatting.parse_text_parts(nil, nil, "foo \n@@@ foo\nbar\nbaz")
41
+ parts.size.should == 2
42
+ parts.map { |p| p.class }.should == [ModelFormatting::FormattedPart, ModelFormatting::CodePart]
43
+ parts.map { |p| p.simple_string }.should == ["foo ", "bar\nbaz"]
44
+ end
45
+
46
+ it "parses mixed blocks" do
47
+ parts = ModelFormatting.parse_text_parts(nil, nil, "foo \n@@@ foo\nbar\nbaz\n@@@\n\nblah blah")
48
+ parts.size.should == 3
49
+ parts.map { |p| p.class }.should == [ModelFormatting::FormattedPart, ModelFormatting::CodePart, ModelFormatting::FormattedPart]
50
+ parts.map { |p| p.simple_string }.should == ["foo ", "bar\nbaz", "\nblah blah"]
51
+ end
52
+
53
+ it "#replace_vars replaces variables in a string with a given context" do
54
+ ModelFormatting::Config.new(nil, nil, nil).replace_vars("a:abc/:d_e_f/:foo!", :abc => 'bc', :d_e_f => '-').should == "abc/-/!"
55
+ end
56
+
57
+ it "links and encodes urls correctly" do
58
+ ModelFormatting.process(:html, "a *b* \n[Whoo](http://entp.com?a=1&b=2)").should == %(<div><p>a <em>b</em><br/>\n<a href="http://entp.com?a=1&amp;b=2">Whoo</a></p></div>)
59
+ end
60
+
61
+ it "converts @@@ to code blocks" do
62
+ ModelFormatting.process(:html, "<a>foo</a>\n\n@@@\n<a>bar</a>\n@@@\n\n@@@\nbaz\n@@@\n\n@@@ wah wah \n \n").should == %(<div><p><a>foo</a></p>\n\n<pre><code>&lt;a&gt;bar&lt;/a&gt;</code>\n</pre>\n\n\n\n\n<pre><code>baz</code>\n</pre>\n\n\n<p>@@@ wah wah</p></div>)
63
+ end
64
+
65
+ it "converts @@@ with params to code blocks" do
66
+ ModelFormatting.process(:html, "foo\n@@@ ninja\nbar\n@@@\n@@@\nbaz\n@@@\n@@@ wah wah \n \n").should == %(<div><p>foo<br/>\n</p>\n\n<pre><code class=\"ninja\">bar</code>\n</pre>\n\n\n<pre><code>baz</code>\n</pre>\n\n\n<p>@@@ wah wah</p></div>)
67
+ end
68
+
69
+ it "fixes irregular number of @@@'s" do
70
+ ModelFormatting.process(:html, "foo\n@@@\nbar\n@@@\n@@@\nbaz\n@@@\n@@@ wah wah \n \n@@@").should == %(<div><p>foo<br/>\n</p>\n\n<pre><code>bar</code>\n</pre>\n\n\n<pre><code>baz</code>\n</pre>\n\n\n<p>@@@ wah wah</p>\n\n<pre><code></code>\n</pre></div>)
71
+ end
72
+
73
+ it "converts @@@ with params to code blocks with text format" do
74
+ ModelFormatting.process(:text, "foo\n@@@ ninja\nbar\n@@@\n@@@\nbaz\n@@@\n@@@ wah wah \n \n").should == %(foo\nbar\nbaz\n@@@ wah wah)
75
+ end
76
+
77
+ it "fixes irregular number of @@@'s with text format" do
78
+ ModelFormatting.process(:text, "foo\n@@@\nbar\n@@@\n@@@\nbaz\n@@@\n@@@ wah wah \n \n@@@").should == %(foo\nbar\nbaz\n@@@ wah wah)
79
+ end
80
+
81
+ it "treats linebreaks correctly" do
82
+ ModelFormatting.process(:html, "Line breaks should not be treated as\nnew paragraphs. They are not paragraphs.\n\nHowever, when a line is skipped, that is a paragraph.\nGMail, and basically every comment or submission form on the \nweb work this way.").should == \
83
+ "<div><p>Line breaks should not be treated as<br/>\nnew paragraphs. They are not paragraphs.</p>\n\n<p>However, when a line is skipped, that is a paragraph.<br/>\nGMail, and basically every comment or submission form on the<br/>\nweb work this way.</p></div>"
84
+ end
85
+
86
+ describe "GFM" do
87
+ it "does not touch single underscores inside words" do
88
+ assert_equal "foo_bar", ModelFormatting.gfm("foo_bar")
89
+ end
90
+
91
+ it "does not touch underscores in code blocks" do
92
+ assert_equal " foo_bar_baz", ModelFormatting.gfm(" foo_bar_baz")
93
+ end
94
+
95
+ it "does not touch underscores in pre blocks" do
96
+ assert_equal "<pre>\nfoo_bar_baz\n</pre>", ModelFormatting.gfm("<pre>\nfoo_bar_baz\n</pre>")
97
+ end
98
+
99
+ it "escapes two or more underscores inside words" do
100
+ assert_equal "foo\\_bar\\_baz", ModelFormatting.gfm("foo_bar_baz")
101
+ end
102
+
103
+ it "turns newlines into br tags in simple cases" do
104
+ assert_equal "foo \nbar", ModelFormatting.gfm("foo\nbar")
105
+ end
106
+
107
+ it "converts newlines in all groups" do
108
+ assert_equal "apple \npear \norange \nbanana\n\nruby \npython \nerlang \njavascript",
109
+ ModelFormatting.gfm("apple\npear\norange\nbanana\n\nruby\npython\nerlang\njavascript")
110
+ end
111
+
112
+ it "does not not convert newlines in lists" do
113
+ assert_equal "# foo\n# bar", ModelFormatting.gfm("# foo\n# bar")
114
+ assert_equal "* foo\n* bar", ModelFormatting.gfm("* foo\n* bar")
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,17 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'rubygems'
4
+ require 'action_controller'
5
+ require 'action_view'
6
+ require 'model_formatting'
7
+ require 'model_formatting/init'
8
+ require 'model_formatting/config'
9
+ require 'model_formatting/instance_methods'
10
+ require 'context'
11
+ require 'matchy'
12
+
13
+ begin
14
+ require 'ruby-debug'
15
+ Debugger.start
16
+ rescue LoadError
17
+ end
metadata ADDED
@@ -0,0 +1,188 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: model_formatting
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
+ platform: ruby
12
+ authors:
13
+ - ENTP
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-07-09 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rdiscount
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 9
30
+ segments:
31
+ - 1
32
+ - 5
33
+ - 5
34
+ version: 1.5.5
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: actionpack
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 1
46
+ segments:
47
+ - 2
48
+ - 2
49
+ - 3
50
+ version: 2.2.3
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: activerecord
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ hash: 1
62
+ segments:
63
+ - 2
64
+ - 2
65
+ - 3
66
+ version: 2.2.3
67
+ type: :runtime
68
+ version_requirements: *id003
69
+ - !ruby/object:Gem::Dependency
70
+ name: activesupport
71
+ prerelease: false
72
+ requirement: &id004 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ hash: 1
78
+ segments:
79
+ - 2
80
+ - 2
81
+ - 3
82
+ version: 2.2.3
83
+ type: :runtime
84
+ version_requirements: *id004
85
+ - !ruby/object:Gem::Dependency
86
+ name: tidy
87
+ prerelease: false
88
+ requirement: &id005 !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ hash: 23
94
+ segments:
95
+ - 1
96
+ - 1
97
+ - 2
98
+ version: 1.1.2
99
+ type: :runtime
100
+ version_requirements: *id005
101
+ - !ruby/object:Gem::Dependency
102
+ name: jeremymcanally-context
103
+ prerelease: false
104
+ requirement: &id006 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ hash: 1
110
+ segments:
111
+ - 0
112
+ - 5
113
+ - 5
114
+ version: 0.5.5
115
+ type: :development
116
+ version_requirements: *id006
117
+ - !ruby/object:Gem::Dependency
118
+ name: jeremymcanally-matchy
119
+ prerelease: false
120
+ requirement: &id007 !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ~>
124
+ - !ruby/object:Gem::Version
125
+ hash: 27
126
+ segments:
127
+ - 0
128
+ - 1
129
+ - 0
130
+ version: 0.1.0
131
+ type: :development
132
+ version_requirements: *id007
133
+ description:
134
+ email: company@entp.com
135
+ executables: []
136
+
137
+ extensions: []
138
+
139
+ extra_rdoc_files:
140
+ - README
141
+ files:
142
+ - Rakefile
143
+ - README
144
+ - tender.patch
145
+ - lib/model_formatting/config.rb
146
+ - lib/model_formatting/init.rb
147
+ - lib/model_formatting/instance_methods.rb
148
+ - lib/model_formatting.rb
149
+ - rails/init.rb
150
+ - test/formatting_test.rb
151
+ - test/model_formatting_test.rb
152
+ - test/test_helper.rb
153
+ has_rdoc: true
154
+ homepage: http://github.com/entp
155
+ licenses: []
156
+
157
+ post_install_message:
158
+ rdoc_options: []
159
+
160
+ require_paths:
161
+ - lib
162
+ required_ruby_version: !ruby/object:Gem::Requirement
163
+ none: false
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ hash: 3
168
+ segments:
169
+ - 0
170
+ version: "0"
171
+ required_rubygems_version: !ruby/object:Gem::Requirement
172
+ none: false
173
+ requirements:
174
+ - - ">="
175
+ - !ruby/object:Gem::Version
176
+ hash: 3
177
+ segments:
178
+ - 0
179
+ version: "0"
180
+ requirements: []
181
+
182
+ rubyforge_project:
183
+ rubygems_version: 1.3.7
184
+ signing_key:
185
+ specification_version: 3
186
+ summary: Automatically format model attributes using rdiscount and Tender/Lighthouse extensions.
187
+ test_files: []
188
+