junebug-wiki 0.0.19

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.
@@ -0,0 +1,317 @@
1
+ module HTMLDiff
2
+
3
+ Match = Struct.new(:start_in_old, :start_in_new, :size)
4
+ class Match
5
+ def end_in_old
6
+ self.start_in_old + self.size
7
+ end
8
+
9
+ def end_in_new
10
+ self.start_in_new + self.size
11
+ end
12
+ end
13
+
14
+ Operation = Struct.new(:action, :start_in_old, :end_in_old, :start_in_new, :end_in_new)
15
+
16
+ class DiffBuilder
17
+
18
+ def initialize(old_version, new_version)
19
+ @old_version, @new_version = old_version, new_version
20
+ @content = []
21
+ end
22
+
23
+ def build
24
+ split_inputs_to_words
25
+ index_new_words
26
+ operations.each { |op| perform_operation(op) }
27
+ return @content.join
28
+ end
29
+
30
+ def split_inputs_to_words
31
+ @old_words = convert_html_to_list_of_words(explode(@old_version))
32
+ @new_words = convert_html_to_list_of_words(explode(@new_version))
33
+ end
34
+
35
+ def index_new_words
36
+ @word_indices = Hash.new { |h, word| h[word] = [] }
37
+ @new_words.each_with_index { |word, i| @word_indices[word] << i }
38
+ end
39
+
40
+ def operations
41
+ position_in_old = position_in_new = 0
42
+ operations = []
43
+
44
+ matches = matching_blocks
45
+ # an empty match at the end forces the loop below to handle the unmatched tails
46
+ # I'm sure it can be done more gracefully, but not at 23:52
47
+ matches << Match.new(@old_words.length, @new_words.length, 0)
48
+
49
+ matches.each_with_index do |match, i|
50
+ match_starts_at_current_position_in_old = (position_in_old == match.start_in_old)
51
+ match_starts_at_current_position_in_new = (position_in_new == match.start_in_new)
52
+
53
+ action_upto_match_positions =
54
+ case [match_starts_at_current_position_in_old, match_starts_at_current_position_in_new]
55
+ when [false, false]
56
+ :replace
57
+ when [true, false]
58
+ :insert
59
+ when [false, true]
60
+ :delete
61
+ else
62
+ # this happens if the first few words are same in both versions
63
+ :none
64
+ end
65
+
66
+ if action_upto_match_positions != :none
67
+ operation_upto_match_positions =
68
+ Operation.new(action_upto_match_positions,
69
+ position_in_old, match.start_in_old,
70
+ position_in_new, match.start_in_new)
71
+ operations << operation_upto_match_positions
72
+ end
73
+ if match.size != 0
74
+ match_operation = Operation.new(:equal,
75
+ match.start_in_old, match.end_in_old,
76
+ match.start_in_new, match.end_in_new)
77
+ operations << match_operation
78
+ end
79
+
80
+ position_in_old = match.end_in_old
81
+ position_in_new = match.end_in_new
82
+ end
83
+
84
+ operations
85
+ end
86
+
87
+ def matching_blocks
88
+ matching_blocks = []
89
+ recursively_find_matching_blocks(0, @old_words.size, 0, @new_words.size, matching_blocks)
90
+ matching_blocks
91
+ end
92
+
93
+ def recursively_find_matching_blocks(start_in_old, end_in_old, start_in_new, end_in_new, matching_blocks)
94
+ match = find_match(start_in_old, end_in_old, start_in_new, end_in_new)
95
+ if match
96
+ if start_in_old < match.start_in_old and start_in_new < match.start_in_new
97
+ recursively_find_matching_blocks(
98
+ start_in_old, match.start_in_old, start_in_new, match.start_in_new, matching_blocks)
99
+ end
100
+ matching_blocks << match
101
+ if match.end_in_old < end_in_old and match.end_in_new < end_in_new
102
+ recursively_find_matching_blocks(
103
+ match.end_in_old, end_in_old, match.end_in_new, end_in_new, matching_blocks)
104
+ end
105
+ end
106
+ end
107
+
108
+ def find_match(start_in_old, end_in_old, start_in_new, end_in_new)
109
+
110
+ best_match_in_old = start_in_old
111
+ best_match_in_new = start_in_new
112
+ best_match_size = 0
113
+
114
+ match_length_at = Hash.new { |h, index| h[index] = 0 }
115
+
116
+ start_in_old.upto(end_in_old - 1) do |index_in_old|
117
+
118
+ new_match_length_at = Hash.new { |h, index| h[index] = 0 }
119
+
120
+ @word_indices[@old_words[index_in_old]].each do |index_in_new|
121
+ next if index_in_new < start_in_new
122
+ break if index_in_new >= end_in_new
123
+
124
+ new_match_length = match_length_at[index_in_new - 1] + 1
125
+ new_match_length_at[index_in_new] = new_match_length
126
+
127
+ if new_match_length > best_match_size
128
+ best_match_in_old = index_in_old - new_match_length + 1
129
+ best_match_in_new = index_in_new - new_match_length + 1
130
+ best_match_size = new_match_length
131
+ end
132
+ end
133
+ match_length_at = new_match_length_at
134
+ end
135
+
136
+ # best_match_in_old, best_match_in_new, best_match_size = add_matching_words_left(
137
+ # best_match_in_old, best_match_in_new, best_match_size, start_in_old, start_in_new)
138
+ # best_match_in_old, best_match_in_new, match_size = add_matching_words_right(
139
+ # best_match_in_old, best_match_in_new, best_match_size, end_in_old, end_in_new)
140
+
141
+ return (best_match_size != 0 ? Match.new(best_match_in_old, best_match_in_new, best_match_size) : nil)
142
+ end
143
+
144
+ def add_matching_words_left(match_in_old, match_in_new, match_size, start_in_old, start_in_new)
145
+ while match_in_old > start_in_old and
146
+ match_in_new > start_in_new and
147
+ @old_words[match_in_old - 1] == @new_words[match_in_new - 1]
148
+ match_in_old -= 1
149
+ match_in_new -= 1
150
+ match_size += 1
151
+ end
152
+ [match_in_old, match_in_new, match_size]
153
+ end
154
+
155
+ def add_matching_words_right(match_in_old, match_in_new, match_size, end_in_old, end_in_new)
156
+ while match_in_old + match_size < end_in_old and
157
+ match_in_new + match_size < end_in_new and
158
+ @old_words[match_in_old + match_size] == @new_words[match_in_new + match_size]
159
+ match_size += 1
160
+ end
161
+ [match_in_old, match_in_new, match_size]
162
+ end
163
+
164
+ VALID_METHODS = [:replace, :insert, :delete, :equal]
165
+
166
+ def perform_operation(operation)
167
+ @operation = operation
168
+ self.send operation.action, operation
169
+ end
170
+
171
+ def replace(operation)
172
+ delete(operation, 'diffmod')
173
+ insert(operation, 'diffmod')
174
+ end
175
+
176
+ def insert(operation, tagclass = 'diffins')
177
+ insert_tag('ins', tagclass, @new_words[operation.start_in_new...operation.end_in_new])
178
+ end
179
+
180
+ def delete(operation, tagclass = 'diffdel')
181
+ insert_tag('del', tagclass, @old_words[operation.start_in_old...operation.end_in_old])
182
+ end
183
+
184
+ def equal(operation)
185
+ # no tags to insert, simply copy the matching words from one of the versions
186
+ @content += @new_words[operation.start_in_new...operation.end_in_new]
187
+ end
188
+
189
+ def opening_tag?(item)
190
+ item =~ %r!^\s*<[^>]+>\s*$!
191
+ end
192
+
193
+ def closing_tag?(item)
194
+ item =~ %r!^\s*</[^>]+>\s*$!
195
+ end
196
+
197
+ def tag?(item)
198
+ opening_tag?(item) or closing_tag?(item)
199
+ end
200
+
201
+ def extract_consecutive_words(words, &condition)
202
+ index_of_first_tag = nil
203
+ words.each_with_index do |word, i|
204
+ if !condition.call(word)
205
+ index_of_first_tag = i
206
+ break
207
+ end
208
+ end
209
+ if index_of_first_tag
210
+ return words.slice!(0...index_of_first_tag)
211
+ else
212
+ return words.slice!(0..words.length)
213
+ end
214
+ end
215
+
216
+ # This method encloses words within a specified tag (ins or del), and adds this into @content,
217
+ # with a twist: if there are words contain tags, it actually creates multiple ins or del,
218
+ # so that they don't include any ins or del. This handles cases like
219
+ # old: '<p>a</p>'
220
+ # new: '<p>ab</p><p>c</b>'
221
+ # diff result: '<p>a<ins>b</ins></p><p><ins>c</ins></p>'
222
+ # this still doesn't guarantee valid HTML (hint: think about diffing a text containing ins or
223
+ # del tags), but handles correctly more cases than the earlier version.
224
+ #
225
+ # P.S.: Spare a thought for people who write HTML browsers. They live in this ... every day.
226
+
227
+ def insert_tag(tagname, cssclass, words)
228
+ loop do
229
+ break if words.empty?
230
+ non_tags = extract_consecutive_words(words) { |word| not tag?(word) }
231
+ @content << wrap_text(non_tags.join, tagname, cssclass) unless non_tags.empty?
232
+
233
+ break if words.empty?
234
+ @content += extract_consecutive_words(words) { |word| tag?(word) }
235
+ end
236
+ end
237
+
238
+ def wrap_text(text, tagname, cssclass)
239
+ %(<#{tagname} class="#{cssclass}">#{text}</#{tagname}>)
240
+ end
241
+
242
+ def explode(sequence)
243
+ sequence.is_a?(String) ? sequence.split(//) : sequence
244
+ end
245
+
246
+ def end_of_tag?(char)
247
+ char == '>'
248
+ end
249
+
250
+ def start_of_tag?(char)
251
+ char == '<'
252
+ end
253
+
254
+ def whitespace?(char)
255
+ char =~ /\s/
256
+ end
257
+
258
+ def convert_html_to_list_of_words(x, use_brackets = false)
259
+ mode = :char
260
+ current_word = ''
261
+ words = []
262
+
263
+ explode(x).each do |char|
264
+ case mode
265
+ when :tag
266
+ if end_of_tag? char
267
+ current_word << (use_brackets ? ']' : '>')
268
+ words << current_word
269
+ current_word = ''
270
+ if whitespace?(char)
271
+ mode = :whitespace
272
+ else
273
+ mode = :char
274
+ end
275
+ else
276
+ current_word << char
277
+ end
278
+ when :char
279
+ if start_of_tag? char
280
+ words << current_word unless current_word.empty?
281
+ current_word = (use_brackets ? '[' : '<')
282
+ mode = :tag
283
+ elsif /\s/.match char
284
+ words << current_word unless current_word.empty?
285
+ current_word = char
286
+ mode = :whitespace
287
+ else
288
+ current_word << char
289
+ end
290
+ when :whitespace
291
+ if start_of_tag? char
292
+ words << current_word unless current_word.empty?
293
+ current_word = (use_brackets ? '[' : '<')
294
+ mode = :tag
295
+ elsif /\s/.match char
296
+ current_word << char
297
+ else
298
+ words << current_word unless current_word.empty?
299
+ current_word = char
300
+ mode = :char
301
+ end
302
+ else
303
+ raise "Unknown mode #{mode.inspect}"
304
+ end
305
+ end
306
+ words << current_word unless current_word.empty?
307
+ words
308
+ end
309
+
310
+ end # of class Diff Builder
311
+
312
+ def diff(a, b)
313
+ DiffBuilder.new(a, b).build
314
+ end
315
+
316
+ end
317
+
@@ -0,0 +1,210 @@
1
+ %w(
2
+ rubygems
3
+ test/unit
4
+ active_record
5
+ active_record/fixtures
6
+ active_support/binding_of_caller
7
+ camping
8
+ fileutils
9
+ stringio
10
+ cgi
11
+ ).each { |lib| require lib }
12
+
13
+ ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => ":memory:")
14
+ ActiveRecord::Base.logger = Logger.new("test/test.log")
15
+
16
+ Test::Unit::TestCase.fixture_path = "test/fixtures/"
17
+
18
+ class Test::Unit::TestCase #:nodoc:
19
+ def create_fixtures(*table_names)
20
+ if block_given?
21
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
22
+ else
23
+ Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
24
+ end
25
+ end
26
+
27
+ # Turn off transactional fixtures if you're working with MyISAM tables in MySQL
28
+ self.use_transactional_fixtures = true
29
+ # Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
30
+ self.use_instantiated_fixtures = false
31
+ end
32
+
33
+ class MockRequest
34
+ def initialize
35
+ @headers = {
36
+ 'SERVER_NAME' => 'localhost',
37
+ 'PATH_INFO' => '',
38
+ 'HTTP_ACCEPT_ENCODING' => 'gzip,deflate',
39
+ 'HTTP_USER_AGENT' => 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.8.0.1) Gecko/20060214 Camino/1.0',
40
+ 'SCRIPT_NAME' => '/',
41
+ 'SERVER_PROTOCOL' => 'HTTP/1.1',
42
+ 'HTTP_CACHE_CONTROL' => 'max-age=0',
43
+ 'HTTP_ACCEPT_LANGUAGE' => 'en,ja;q=0.9,fr;q=0.9,de;q=0.8,es;q=0.7,it;q=0.7,nl;q=0.6,sv;q=0.5,nb;q=0.5,da;q=0.4,fi;q=0.3,pt;q=0.3,zh-Hans;q=0.2,zh-Hant;q=0.1,ko;q=0.1',
44
+ 'HTTP_HOST' => 'localhost',
45
+ 'REMOTE_ADDR' => '127.0.0.1',
46
+ 'SERVER_SOFTWARE' => 'Mongrel 0.3.12.4',
47
+ 'HTTP_KEEP_ALIVE' => '300',
48
+ 'HTTP_REFERER' => 'http://localhost/',
49
+ 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
50
+ 'HTTP_VERSION' => 'HTTP/1.1',
51
+ 'REQUEST_URI' => '/',
52
+ 'SERVER_PORT' => '80',
53
+ 'GATEWAY_INTERFACE' => 'CGI/1.2',
54
+ 'HTTP_ACCEPT' => 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
55
+ 'HTTP_CONNECTION' => 'keep-alive',
56
+ 'REQUEST_METHOD' => 'GET',
57
+ }
58
+ end
59
+
60
+ def set(key, val)
61
+ @headers[key] = val
62
+ end
63
+
64
+ def to_hash
65
+ @headers
66
+ end
67
+
68
+ def [](key)
69
+ @headers[key]
70
+ end
71
+
72
+ def []=(key, value)
73
+ @headers[key] = value
74
+ end
75
+
76
+ ##
77
+ # Allow getters like this:
78
+ # o.REQUEST_METHOD
79
+
80
+ def method_missing(method_name, *args)
81
+ if @headers.has_key?(method_name.to_s)
82
+ return @headers[method_name.to_s]
83
+ else
84
+ super(method_name, args)
85
+ end
86
+ end
87
+
88
+ end
89
+
90
+
91
+ module Camping
92
+
93
+ class Test < Test::Unit::TestCase
94
+
95
+ def test_dummy; end
96
+
97
+ def deny(condition, message='')
98
+ assert !condition, message
99
+ end
100
+
101
+ # http://project.ioni.st/post/217#post-217
102
+ #
103
+ # def test_new_publication
104
+ # assert_difference(Publication, :count) do
105
+ # post :create, :publication_title => ...
106
+ # # ...
107
+ # end
108
+ # end
109
+ #
110
+ # Is the number of items different?
111
+ #
112
+ # Can be used for increment and decrement.
113
+ #
114
+ def assert_difference(object, method = :count, difference = 1)
115
+ initial_value = object.send(method)
116
+ yield
117
+ assert_equal initial_value + difference, object.send(method), "#{object}##{method}"
118
+ end
119
+ def assert_no_difference(object, method, &block)
120
+ assert_difference object, method, 0, &block
121
+ end
122
+
123
+ end
124
+
125
+ class FunctionalTest < Test
126
+
127
+ def setup
128
+ @class_name_abbr = self.class.name.gsub(/Test$/, '')
129
+ @request = MockRequest.new
130
+ end
131
+
132
+ def get(url='/')
133
+ send_request url, {}, 'GET'
134
+ end
135
+
136
+ def post(url, post_vars={})
137
+ send_request url, post_vars, 'POST'
138
+ end
139
+
140
+ def send_request(url, post_vars, method)
141
+ @request['REQUEST_METHOD'] = method
142
+ @request['SCRIPT_NAME'] = '/' + @class_name_abbr.downcase
143
+ @request['PATH_INFO'] = '/' + url
144
+ @request['REQUEST_URI'] = [@request.SCRIPT_NAME, @request.PATH_INFO].join('')
145
+
146
+ @request['HTTP_COOKIE'] = @cookies.map {|k,v| "#{k}=#{v}" }.join('; ') if @cookies
147
+
148
+ @response = eval("#{@class_name_abbr}.run StringIO.new('#{qs_build(post_vars)}'), @request")
149
+
150
+ @cookies = @response.headers['Set-Cookie'].inject(@cookies||{}) do |res,header|
151
+ data = header.split(';').first
152
+ name, value = data.split('=')
153
+ res[name] = value
154
+ res
155
+ end
156
+
157
+ if @response.headers['X-Sendfile']
158
+ @response.body = File.read(@response.headers['X-Sendfile'])
159
+ end
160
+ end
161
+
162
+ def assert_response(status_code)
163
+ case status_code
164
+ when :success
165
+ assert_equal 200, @response.status
166
+ when :redirect
167
+ assert_equal 302, @response.status
168
+ when :error
169
+ assert @response.status >= 500
170
+ else
171
+ assert_equal status_code, @response.status
172
+ end
173
+ end
174
+
175
+ def assert_match_body(regex, message=nil)
176
+ assert_match regex, @response.body, message
177
+ end
178
+ def assert_no_match_body(regex, message=nil)
179
+ assert_no_match regex, @response.body, message
180
+ end
181
+
182
+ def assert_redirected_to(url, message=nil)
183
+ assert_equal url,
184
+ @response.headers['Location'].path.gsub(%r!/#{@class_name_abbr.downcase}!, ''),
185
+ message
186
+ end
187
+
188
+ def assert_cookie(name, pat, message=nil)
189
+ assert_match pat, @cookies[name], message
190
+ end
191
+
192
+ def test_dummy; end
193
+
194
+ private
195
+
196
+ def qs_build(var_hash)
197
+ var_hash.map do |k, v|
198
+ [Camping.escape(k.to_s), Camping.escape(v.to_s)].join('=')
199
+ end.join('&')
200
+ end
201
+
202
+ end
203
+
204
+ class UnitTest < Test
205
+
206
+ def test_dummy; end
207
+
208
+ end
209
+
210
+ end