instiki 0.10.0 → 0.10.1

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.
Files changed (88) hide show
  1. data/CHANGELOG +174 -165
  2. data/README +68 -68
  3. data/app/controllers/admin_controller.rb +94 -94
  4. data/app/controllers/application.rb +135 -131
  5. data/app/controllers/file_controller.rb +129 -129
  6. data/app/controllers/wiki_controller.rb +354 -354
  7. data/app/helpers/application_helper.rb +68 -68
  8. data/app/models/author.rb +3 -3
  9. data/app/models/chunks/category.rb +33 -33
  10. data/app/models/chunks/chunk.rb +86 -86
  11. data/app/models/chunks/engines.rb +61 -54
  12. data/app/models/chunks/include.rb +41 -41
  13. data/app/models/chunks/literal.rb +31 -31
  14. data/app/models/chunks/nowiki.rb +28 -28
  15. data/app/models/chunks/test.rb +18 -18
  16. data/app/models/chunks/uri.rb +182 -182
  17. data/app/models/chunks/wiki.rb +141 -141
  18. data/app/models/file_yard.rb +58 -58
  19. data/app/models/page.rb +112 -112
  20. data/app/models/page_lock.rb +22 -22
  21. data/app/models/page_set.rb +89 -89
  22. data/app/models/revision.rb +123 -123
  23. data/app/models/web.rb +182 -176
  24. data/app/models/wiki_content.rb +207 -207
  25. data/app/models/wiki_service.rb +233 -233
  26. data/app/models/wiki_words.rb +23 -23
  27. data/app/views/admin/create_system.rhtml +83 -83
  28. data/app/views/admin/create_web.rhtml +69 -69
  29. data/app/views/admin/edit_web.rhtml +137 -136
  30. data/app/views/file/file.rhtml +18 -18
  31. data/app/views/file/import.rhtml +22 -22
  32. data/app/views/layouts/default.rhtml +86 -85
  33. data/app/views/markdown_help.rhtml +12 -12
  34. data/app/views/mixed_help.rhtml +6 -6
  35. data/app/views/navigation.rhtml +30 -30
  36. data/app/views/rdoc_help.rhtml +12 -12
  37. data/app/views/textile_help.rhtml +24 -24
  38. data/app/views/wiki/authors.rhtml +11 -11
  39. data/app/views/wiki/edit.rhtml +39 -39
  40. data/app/views/wiki/export.rhtml +12 -12
  41. data/app/views/wiki/feeds.rhtml +14 -14
  42. data/app/views/wiki/list.rhtml +64 -64
  43. data/app/views/wiki/locked.rhtml +23 -23
  44. data/app/views/wiki/login.rhtml +14 -14
  45. data/app/views/wiki/new.rhtml +31 -31
  46. data/app/views/wiki/page.rhtml +115 -115
  47. data/app/views/wiki/print.rhtml +14 -14
  48. data/app/views/wiki/published.rhtml +9 -9
  49. data/app/views/wiki/recently_revised.rhtml +26 -26
  50. data/app/views/wiki/revision.rhtml +103 -103
  51. data/app/views/wiki/rollback.rhtml +36 -36
  52. data/app/views/wiki/rss_feed.rhtml +22 -22
  53. data/app/views/wiki/search.rhtml +38 -38
  54. data/app/views/wiki/tex.rhtml +22 -22
  55. data/app/views/wiki/tex_web.rhtml +34 -34
  56. data/app/views/wiki/web_list.rhtml +18 -18
  57. data/app/views/wiki_words_help.rhtml +9 -9
  58. data/config/environment.rb +82 -82
  59. data/config/environments/development.rb +5 -5
  60. data/config/environments/production.rb +4 -4
  61. data/config/environments/test.rb +17 -17
  62. data/config/routes.rb +18 -18
  63. data/lib/active_record_stub.rb +31 -31
  64. data/lib/bluecloth_tweaked.rb +1127 -0
  65. data/lib/diff.rb +444 -444
  66. data/lib/instiki_errors.rb +14 -14
  67. data/lib/rdocsupport.rb +151 -151
  68. data/lib/redcloth_for_tex.rb +736 -736
  69. data/natives/osx/desktop_launcher/AppDelegate.h +18 -18
  70. data/natives/osx/desktop_launcher/AppDelegate.mm +109 -109
  71. data/natives/osx/desktop_launcher/Credits.html +15 -15
  72. data/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/classes.nib +12 -12
  73. data/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/info.nib +24 -24
  74. data/natives/osx/desktop_launcher/Info.plist +12 -12
  75. data/natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj +592 -592
  76. data/natives/osx/desktop_launcher/Instiki_Prefix.pch +7 -7
  77. data/natives/osx/desktop_launcher/MakeDMG.sh +9 -9
  78. data/natives/osx/desktop_launcher/main.mm +14 -14
  79. data/natives/osx/desktop_launcher/version.plist +16 -16
  80. data/public/404.html +5 -5
  81. data/public/500.html +5 -5
  82. data/public/dispatch.rb +9 -9
  83. data/public/javascripts/edit_web.js +52 -52
  84. data/public/javascripts/prototype.js +336 -336
  85. data/public/stylesheets/instiki.css +222 -222
  86. data/script/breakpointer +4 -4
  87. data/script/server +93 -93
  88. metadata +4 -3
data/lib/diff.rb CHANGED
@@ -1,444 +1,444 @@
1
- # heavily based off difflib.py - see that file for documentation
2
- # ported from Python by Bill Atkins
3
-
4
- # This does not support all features offered by difflib; it
5
- # implements only the subset of features necessary
6
- # to support a Ruby version of HTML Differ. You're welcome to finish this off.
7
-
8
- # By default, String#each iterates by line. This isn't really appropriate
9
- # for diff, so often a string will be split by // to get an array of one-
10
- # character strings.
11
-
12
- # Some methods in Diff are untested and are not guaranteed to work. The
13
- # methods in HTMLDiff and any methods it calls should work quite well.
14
-
15
- # changes by DenisMertz
16
- # * main change:
17
- # ** get the tag soup away
18
- # the tag soup problem was first reported with <p> tags, but it appeared also with
19
- # <li>, <ul> etc... tags
20
- # this version should mostly fix these problems
21
- # ** added a Builder class to manage the creation of the final htmldiff
22
- # * minor changes:
23
- # ** use symbols instead of string to represent opcodes
24
- # ** small fix to html2list
25
- #
26
-
27
- module Enumerable
28
- def reduce(init)
29
- result = init
30
- each { |item| result = yield(result, item) }
31
- result
32
- end
33
- end
34
-
35
- module Diff
36
-
37
- class SequenceMatcher
38
- def initialize(a=[''], b=[''], isjunk=nil, byline=false)
39
- a = (!byline and a.kind_of? String) ? a.split(//) : a
40
- b = (!byline and b.kind_of? String) ? b.split(//) : b
41
- @isjunk = isjunk || proc {}
42
- set_seqs a, b
43
- end
44
-
45
- def set_seqs(a, b)
46
- set_seq_a a
47
- set_seq_b b
48
- end
49
-
50
- def set_seq_a(a)
51
- @a = a
52
- @matching_blocks = @opcodes = nil
53
- end
54
-
55
- def set_seq_b(b)
56
- @b = b
57
- @matching_blocks = @opcodes = nil
58
- chain_b
59
- end
60
-
61
- def chain_b
62
- @fullbcount = nil
63
- @b2j = {}
64
- pophash = {}
65
- junkdict = {}
66
-
67
- @b.each_with_index do |elt, i|
68
- if @b2j.has_key? elt
69
- indices = @b2j[elt]
70
- if @b.length >= 200 and indices.length * 100 > @b.length
71
- pophash[elt] = 1
72
- indices.clear
73
- else
74
- indices.push i
75
- end
76
- else
77
- @b2j[elt] = [i]
78
- end
79
- end
80
-
81
- pophash.each_key { |elt| @b2j.delete elt }
82
-
83
- junkdict = {}
84
-
85
- unless @isjunk.nil?
86
- [pophash, @b2j].each do |d|
87
- d.each_key do |elt|
88
- if @isjunk.call(elt)
89
- junkdict[elt] = 1
90
- d.delete elt
91
- end
92
- end
93
- end
94
- end
95
-
96
- @isbjunk = junkdict.method(:has_key?)
97
- @isbpopular = junkdict.method(:has_key?)
98
- end
99
-
100
- def find_longest_match(alo, ahi, blo, bhi)
101
- besti, bestj, bestsize = alo, blo, 0
102
-
103
- j2len = {}
104
-
105
- (alo..ahi).step do |i|
106
- newj2len = {}
107
- (@b2j[@a[i]] || []).each do |j|
108
- if j < blo
109
- next
110
- end
111
- if j >= bhi
112
- break
113
- end
114
-
115
- k = newj2len[j] = (j2len[j - 1] || 0) + 1
116
- if k > bestsize
117
- besti, bestj, bestsize = i - k + 1, j - k + 1, k
118
- end
119
- end
120
- j2len = newj2len
121
- end
122
-
123
- while besti > alo and bestj > blo and
124
- not @isbjunk.call(@b[bestj-1]) and
125
- @a[besti-1] == @b[bestj-1]
126
- besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
127
- end
128
-
129
- while besti+bestsize < ahi and bestj+bestsize < bhi and
130
- not @isbjunk.call(@b[bestj+bestsize]) and
131
- @a[besti+bestsize] == @b[bestj+bestsize]
132
- bestsize += 1
133
- end
134
-
135
- while besti > alo and bestj > blo and
136
- @isbjunk.call(@b[bestj-1]) and
137
- @a[besti-1] == @b[bestj-1]
138
- besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
139
- end
140
-
141
- while besti+bestsize < ahi and bestj+bestsize < bhi and
142
- @isbjunk.call(@b[bestj+bestsize]) and
143
- @a[besti+bestsize] == @b[bestj+bestsize]
144
- bestsize += 1
145
- end
146
-
147
- [besti, bestj, bestsize]
148
- end
149
-
150
- def get_matching_blocks
151
- return @matching_blocks unless @matching_blocks.nil? or
152
- @matching_blocks.empty?
153
-
154
- @matching_blocks = []
155
- la, lb = @a.length, @b.length
156
- match_block_helper(0, la, 0, lb, @matching_blocks)
157
- @matching_blocks.push [la, lb, 0]
158
- end
159
-
160
- def match_block_helper(alo, ahi, blo, bhi, answer)
161
- i, j, k = x = find_longest_match(alo, ahi, blo, bhi)
162
- if not k.zero?
163
- if alo < i and blo < j
164
- match_block_helper(alo, i, blo, j, answer)
165
- end
166
- answer.push x
167
- if i + k < ahi and j + k < bhi
168
- match_block_helper(i + k, ahi, j + k, bhi, answer)
169
- end
170
- end
171
- end
172
-
173
- def get_opcodes
174
- unless @opcodes.nil? or @opcodes.empty?
175
- return @opcodes
176
- end
177
-
178
- i = j = 0
179
- @opcodes = answer = []
180
- get_matching_blocks.each do |ai, bj, size|
181
- tag = if i < ai and j < bj
182
- :replace
183
- elsif i < ai
184
- :delete
185
- elsif j < bj
186
- :insert
187
- end
188
-
189
- answer.push [tag, i, ai, j, bj] if tag
190
-
191
- i, j = ai + size, bj + size
192
-
193
- answer.push [:equal, ai, i, bj, j] unless size.zero?
194
-
195
- end
196
- return answer
197
- end
198
-
199
- # XXX: untested
200
- def get_grouped_opcodes(n=3)
201
- codes = get_opcodes
202
- if codes[0][0] == :equal
203
- tag, i1, i2, j1, j2 = codes[0]
204
- codes[0] = tag, [i1, i2 - n].max, i2, [j1, j2-n].max, j2
205
- end
206
-
207
- if codes[-1][0] == :equal
208
- tag, i1, i2, j1, j2 = codes[-1]
209
- codes[-1] = tag, i1, min(i2, i1+n), j1, min(j2, j1+n)
210
- end
211
- nn = n + n
212
- group = []
213
- codes.each do |tag, i1, i2, j1, j2|
214
- if tag == :equal and i2-i1 > nn
215
- group.push [tag, i1, [i2, i1 + n].min, j1, [j2, j1 + n].min]
216
- yield group
217
- group = []
218
- i1, j1 = [i1, i2-n].max, [j1, j2-n].max
219
- group.push [tag, i1, i2, j1 ,j2]
220
- end
221
- end
222
- if group and group.length != 1 and group[0][0] == :equal
223
- yield group
224
- end
225
- end
226
-
227
- def ratio
228
- matches = get_matching_blocks.reduce(0) do |sum, triple|
229
- sum + triple[-1]
230
- end
231
- Diff.calculate_ratio(matches, @a.length + @b.length)
232
- end
233
-
234
- def quick_ratio
235
- if @fullbcount.nil? or @fullbcount.empty?
236
- @fullbcount = {}
237
- @b.each do |elt|
238
- @fullbcount[elt] = (@fullbcount[elt] || 0) + 1
239
- end
240
- end
241
-
242
- avail = {}
243
- matches = 0
244
- @a.each do |elt|
245
- if avail.has_key? elt
246
- numb = avail[elt]
247
- else
248
- numb = @fullbcount[elt] || 0
249
- end
250
- avail[elt] = numb - 1
251
- if numb > 0
252
- matches += 1
253
- end
254
- end
255
- Diff.calculate_ratio matches, @a.length + @b.length
256
- end
257
-
258
- def real_quick_ratio
259
- la, lb = @a.length, @b.length
260
- Diff.calculate_ratio([la, lb].min, la + lb)
261
- end
262
-
263
- protected :chain_b, :match_block_helper
264
- end # end class SequenceMatcher
265
-
266
- def self.calculate_ratio(matches, length)
267
- return 1.0 if length.zero?
268
- 2.0 * matches / length
269
- end
270
-
271
- # XXX: untested
272
- def self.get_close_matches(word, possibilities, n=3, cutoff=0.6)
273
- unless n > 0
274
- raise "n must be > 0: #{n}"
275
- end
276
- unless 0.0 <= cutoff and cutoff <= 1.0
277
- raise "cutoff must be in (0.0..1.0): #{cutoff}"
278
- end
279
-
280
- result = []
281
- s = SequenceMatcher.new
282
- s.set_seq_b word
283
- possibilities.each do |x|
284
- s.set_seq_a x
285
- if s.real_quick_ratio >= cutoff and
286
- s.quick_ratio >= cutoff and
287
- s.ratio >= cutoff
288
- result.push [s.ratio, x]
289
- end
290
- end
291
-
292
- unless result.nil? or result.empty?
293
- result.sort
294
- result.reverse!
295
- result = result[-n..-1]
296
- end
297
- result.collect { |score, x| x }
298
- end
299
-
300
- def self.count_leading(line, ch)
301
- i, n = 0, line.length
302
- while i < n and line[i].chr == ch
303
- i += 1
304
- end
305
- i
306
- end
307
- end
308
-
309
-
310
- module HTMLDiff
311
- include Diff
312
- class Builder
313
- VALID_METHODS = [:replace, :insert, :delete, :equal]
314
- def initialize(a, b)
315
- @a = a
316
- @b = b
317
- @content = []
318
- end
319
-
320
- def do_op(opcode)
321
- @opcode = opcode
322
- op = @opcode[0]
323
- VALID_METHODS.include?(op) or raise(NameError, "Invalid opcode #{op}")
324
- self.method(op).call
325
- end
326
-
327
- def result
328
- @content.join('')
329
- end
330
-
331
- #this methods have to be called via do_op(opcode) so that @opcode is set properly
332
- private
333
-
334
- def replace
335
- delete("diffmod")
336
- insert("diffmod")
337
- end
338
-
339
- def insert(tagclass="diffins")
340
- op_helper("ins", tagclass, @b[@opcode[3]...@opcode[4]])
341
- end
342
-
343
- def delete(tagclass="diffdel")
344
- op_helper("del", tagclass, @a[@opcode[1]...@opcode[2]])
345
- end
346
-
347
- def equal
348
- @content += @b[@opcode[3]...@opcode[4]]
349
- end
350
-
351
- # using this as op_helper would be equivalent to the first version of diff.rb by Bill Atkins
352
- def op_helper_simple(tagname, tagclass, to_add)
353
- @content << "<#{tagname} class=\"#{tagclass}\">"
354
- @content += to_add
355
- @content << "</#{tagname}>"
356
- end
357
-
358
- # this tries to put <p> tags or newline chars before the opening diff tags (<ins> or <del>)
359
- # or after the ending diff tags
360
- # as a result the diff tags should be the "more inside" possible.
361
- # this seems to work nice with html containing only paragraphs
362
- # but not sure it works if there are other tags (div, span ... ? ) around
363
- def op_helper(tagname, tagclass, to_add)
364
- @content << to_add.shift while ( HTMLDiff.is_newline(to_add.first) or
365
- HTMLDiff.is_p_close_tag(to_add.first) or
366
- HTMLDiff.is_p_open_tag(to_add.first) )
367
- @content << "<#{tagname} class=\"#{tagclass}\">"
368
- @content += to_add
369
- last_tags = []
370
- last_tags.unshift(@content.pop) while ( HTMLDiff.is_newline(@content.last) or
371
- HTMLDiff.is_p_close_tag(@content.last) or
372
- HTMLDiff.is_p_open_tag(@content.last) )
373
- last_tags.unshift "</#{tagname}>"
374
- @content += last_tags
375
- remove_empty_diff(tagname, tagclass)
376
- end
377
-
378
- def remove_empty_diff(tagname, tagclass)
379
- if @content[-2] == "<#{tagname} class=\"#{tagclass}\">" and @content[-1] == "</#{tagname}>" then
380
- @content.pop
381
- @content.pop
382
- end
383
- end
384
-
385
- end
386
-
387
- def self.is_newline(x)
388
- (x == "\n") or (x == "\r") or (x == "\t")
389
- end
390
-
391
- def self.is_p_open_tag(x)
392
- x =~ /\A<(p|li|ul|ol|dir|dt|dl)/
393
- end
394
-
395
- def self.is_p_close_tag(x)
396
- x =~ %r!\A</(p|li|ul|ol|dir|dt|dl)!
397
- end
398
-
399
- def self.diff(a, b)
400
- a = html2list(a)
401
- b = html2list(b)
402
-
403
- out = Builder.new(a, b)
404
- s = SequenceMatcher.new(a, b)
405
-
406
- s.get_opcodes.each do |opcode|
407
- out.do_op(opcode)
408
- end
409
-
410
- out.result
411
- end
412
-
413
- def self.html2list(x)
414
- mode = :char
415
- cur = ''
416
- out = []
417
-
418
- x.split('').each do |c|
419
- if mode == :tag
420
- cur += c
421
- if c == '>'
422
- out.push(cur)
423
- cur = ''
424
- mode = :char
425
- end
426
- elsif mode == :char
427
- if c == '<'
428
- out.push cur
429
- cur = c
430
- mode = :tag
431
- elsif c =~ /\s/
432
- out.push cur + c
433
- cur = ''
434
- else
435
- cur += c
436
- end
437
- end
438
- end
439
-
440
- out.push cur
441
- out.find_all { |x| x != '' }
442
- end
443
-
444
- end
1
+ # heavily based off difflib.py - see that file for documentation
2
+ # ported from Python by Bill Atkins
3
+
4
+ # This does not support all features offered by difflib; it
5
+ # implements only the subset of features necessary
6
+ # to support a Ruby version of HTML Differ. You're welcome to finish this off.
7
+
8
+ # By default, String#each iterates by line. This isn't really appropriate
9
+ # for diff, so often a string will be split by // to get an array of one-
10
+ # character strings.
11
+
12
+ # Some methods in Diff are untested and are not guaranteed to work. The
13
+ # methods in HTMLDiff and any methods it calls should work quite well.
14
+
15
+ # changes by DenisMertz
16
+ # * main change:
17
+ # ** get the tag soup away
18
+ # the tag soup problem was first reported with <p> tags, but it appeared also with
19
+ # <li>, <ul> etc... tags
20
+ # this version should mostly fix these problems
21
+ # ** added a Builder class to manage the creation of the final htmldiff
22
+ # * minor changes:
23
+ # ** use symbols instead of string to represent opcodes
24
+ # ** small fix to html2list
25
+ #
26
+
27
+ module Enumerable
28
+ def reduce(init)
29
+ result = init
30
+ each { |item| result = yield(result, item) }
31
+ result
32
+ end
33
+ end
34
+
35
+ module Diff
36
+
37
+ class SequenceMatcher
38
+ def initialize(a=[''], b=[''], isjunk=nil, byline=false)
39
+ a = (!byline and a.kind_of? String) ? a.split(//) : a
40
+ b = (!byline and b.kind_of? String) ? b.split(//) : b
41
+ @isjunk = isjunk || proc {}
42
+ set_seqs a, b
43
+ end
44
+
45
+ def set_seqs(a, b)
46
+ set_seq_a a
47
+ set_seq_b b
48
+ end
49
+
50
+ def set_seq_a(a)
51
+ @a = a
52
+ @matching_blocks = @opcodes = nil
53
+ end
54
+
55
+ def set_seq_b(b)
56
+ @b = b
57
+ @matching_blocks = @opcodes = nil
58
+ chain_b
59
+ end
60
+
61
+ def chain_b
62
+ @fullbcount = nil
63
+ @b2j = {}
64
+ pophash = {}
65
+ junkdict = {}
66
+
67
+ @b.each_with_index do |elt, i|
68
+ if @b2j.has_key? elt
69
+ indices = @b2j[elt]
70
+ if @b.length >= 200 and indices.length * 100 > @b.length
71
+ pophash[elt] = 1
72
+ indices.clear
73
+ else
74
+ indices.push i
75
+ end
76
+ else
77
+ @b2j[elt] = [i]
78
+ end
79
+ end
80
+
81
+ pophash.each_key { |elt| @b2j.delete elt }
82
+
83
+ junkdict = {}
84
+
85
+ unless @isjunk.nil?
86
+ [pophash, @b2j].each do |d|
87
+ d.each_key do |elt|
88
+ if @isjunk.call(elt)
89
+ junkdict[elt] = 1
90
+ d.delete elt
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ @isbjunk = junkdict.method(:has_key?)
97
+ @isbpopular = junkdict.method(:has_key?)
98
+ end
99
+
100
+ def find_longest_match(alo, ahi, blo, bhi)
101
+ besti, bestj, bestsize = alo, blo, 0
102
+
103
+ j2len = {}
104
+
105
+ (alo..ahi).step do |i|
106
+ newj2len = {}
107
+ (@b2j[@a[i]] || []).each do |j|
108
+ if j < blo
109
+ next
110
+ end
111
+ if j >= bhi
112
+ break
113
+ end
114
+
115
+ k = newj2len[j] = (j2len[j - 1] || 0) + 1
116
+ if k > bestsize
117
+ besti, bestj, bestsize = i - k + 1, j - k + 1, k
118
+ end
119
+ end
120
+ j2len = newj2len
121
+ end
122
+
123
+ while besti > alo and bestj > blo and
124
+ not @isbjunk.call(@b[bestj-1]) and
125
+ @a[besti-1] == @b[bestj-1]
126
+ besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
127
+ end
128
+
129
+ while besti+bestsize < ahi and bestj+bestsize < bhi and
130
+ not @isbjunk.call(@b[bestj+bestsize]) and
131
+ @a[besti+bestsize] == @b[bestj+bestsize]
132
+ bestsize += 1
133
+ end
134
+
135
+ while besti > alo and bestj > blo and
136
+ @isbjunk.call(@b[bestj-1]) and
137
+ @a[besti-1] == @b[bestj-1]
138
+ besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
139
+ end
140
+
141
+ while besti+bestsize < ahi and bestj+bestsize < bhi and
142
+ @isbjunk.call(@b[bestj+bestsize]) and
143
+ @a[besti+bestsize] == @b[bestj+bestsize]
144
+ bestsize += 1
145
+ end
146
+
147
+ [besti, bestj, bestsize]
148
+ end
149
+
150
+ def get_matching_blocks
151
+ return @matching_blocks unless @matching_blocks.nil? or
152
+ @matching_blocks.empty?
153
+
154
+ @matching_blocks = []
155
+ la, lb = @a.length, @b.length
156
+ match_block_helper(0, la, 0, lb, @matching_blocks)
157
+ @matching_blocks.push [la, lb, 0]
158
+ end
159
+
160
+ def match_block_helper(alo, ahi, blo, bhi, answer)
161
+ i, j, k = x = find_longest_match(alo, ahi, blo, bhi)
162
+ if not k.zero?
163
+ if alo < i and blo < j
164
+ match_block_helper(alo, i, blo, j, answer)
165
+ end
166
+ answer.push x
167
+ if i + k < ahi and j + k < bhi
168
+ match_block_helper(i + k, ahi, j + k, bhi, answer)
169
+ end
170
+ end
171
+ end
172
+
173
+ def get_opcodes
174
+ unless @opcodes.nil? or @opcodes.empty?
175
+ return @opcodes
176
+ end
177
+
178
+ i = j = 0
179
+ @opcodes = answer = []
180
+ get_matching_blocks.each do |ai, bj, size|
181
+ tag = if i < ai and j < bj
182
+ :replace
183
+ elsif i < ai
184
+ :delete
185
+ elsif j < bj
186
+ :insert
187
+ end
188
+
189
+ answer.push [tag, i, ai, j, bj] if tag
190
+
191
+ i, j = ai + size, bj + size
192
+
193
+ answer.push [:equal, ai, i, bj, j] unless size.zero?
194
+
195
+ end
196
+ return answer
197
+ end
198
+
199
+ # XXX: untested
200
+ def get_grouped_opcodes(n=3)
201
+ codes = get_opcodes
202
+ if codes[0][0] == :equal
203
+ tag, i1, i2, j1, j2 = codes[0]
204
+ codes[0] = tag, [i1, i2 - n].max, i2, [j1, j2-n].max, j2
205
+ end
206
+
207
+ if codes[-1][0] == :equal
208
+ tag, i1, i2, j1, j2 = codes[-1]
209
+ codes[-1] = tag, i1, min(i2, i1+n), j1, min(j2, j1+n)
210
+ end
211
+ nn = n + n
212
+ group = []
213
+ codes.each do |tag, i1, i2, j1, j2|
214
+ if tag == :equal and i2-i1 > nn
215
+ group.push [tag, i1, [i2, i1 + n].min, j1, [j2, j1 + n].min]
216
+ yield group
217
+ group = []
218
+ i1, j1 = [i1, i2-n].max, [j1, j2-n].max
219
+ group.push [tag, i1, i2, j1 ,j2]
220
+ end
221
+ end
222
+ if group and group.length != 1 and group[0][0] == :equal
223
+ yield group
224
+ end
225
+ end
226
+
227
+ def ratio
228
+ matches = get_matching_blocks.reduce(0) do |sum, triple|
229
+ sum + triple[-1]
230
+ end
231
+ Diff.calculate_ratio(matches, @a.length + @b.length)
232
+ end
233
+
234
+ def quick_ratio
235
+ if @fullbcount.nil? or @fullbcount.empty?
236
+ @fullbcount = {}
237
+ @b.each do |elt|
238
+ @fullbcount[elt] = (@fullbcount[elt] || 0) + 1
239
+ end
240
+ end
241
+
242
+ avail = {}
243
+ matches = 0
244
+ @a.each do |elt|
245
+ if avail.has_key? elt
246
+ numb = avail[elt]
247
+ else
248
+ numb = @fullbcount[elt] || 0
249
+ end
250
+ avail[elt] = numb - 1
251
+ if numb > 0
252
+ matches += 1
253
+ end
254
+ end
255
+ Diff.calculate_ratio matches, @a.length + @b.length
256
+ end
257
+
258
+ def real_quick_ratio
259
+ la, lb = @a.length, @b.length
260
+ Diff.calculate_ratio([la, lb].min, la + lb)
261
+ end
262
+
263
+ protected :chain_b, :match_block_helper
264
+ end # end class SequenceMatcher
265
+
266
+ def self.calculate_ratio(matches, length)
267
+ return 1.0 if length.zero?
268
+ 2.0 * matches / length
269
+ end
270
+
271
+ # XXX: untested
272
+ def self.get_close_matches(word, possibilities, n=3, cutoff=0.6)
273
+ unless n > 0
274
+ raise "n must be > 0: #{n}"
275
+ end
276
+ unless 0.0 <= cutoff and cutoff <= 1.0
277
+ raise "cutoff must be in (0.0..1.0): #{cutoff}"
278
+ end
279
+
280
+ result = []
281
+ s = SequenceMatcher.new
282
+ s.set_seq_b word
283
+ possibilities.each do |x|
284
+ s.set_seq_a x
285
+ if s.real_quick_ratio >= cutoff and
286
+ s.quick_ratio >= cutoff and
287
+ s.ratio >= cutoff
288
+ result.push [s.ratio, x]
289
+ end
290
+ end
291
+
292
+ unless result.nil? or result.empty?
293
+ result.sort
294
+ result.reverse!
295
+ result = result[-n..-1]
296
+ end
297
+ result.collect { |score, x| x }
298
+ end
299
+
300
+ def self.count_leading(line, ch)
301
+ i, n = 0, line.length
302
+ while i < n and line[i].chr == ch
303
+ i += 1
304
+ end
305
+ i
306
+ end
307
+ end
308
+
309
+
310
+ module HTMLDiff
311
+ include Diff
312
+ class Builder
313
+ VALID_METHODS = [:replace, :insert, :delete, :equal]
314
+ def initialize(a, b)
315
+ @a = a
316
+ @b = b
317
+ @content = []
318
+ end
319
+
320
+ def do_op(opcode)
321
+ @opcode = opcode
322
+ op = @opcode[0]
323
+ VALID_METHODS.include?(op) or raise(NameError, "Invalid opcode #{op}")
324
+ self.method(op).call
325
+ end
326
+
327
+ def result
328
+ @content.join('')
329
+ end
330
+
331
+ #this methods have to be called via do_op(opcode) so that @opcode is set properly
332
+ private
333
+
334
+ def replace
335
+ delete("diffmod")
336
+ insert("diffmod")
337
+ end
338
+
339
+ def insert(tagclass="diffins")
340
+ op_helper("ins", tagclass, @b[@opcode[3]...@opcode[4]])
341
+ end
342
+
343
+ def delete(tagclass="diffdel")
344
+ op_helper("del", tagclass, @a[@opcode[1]...@opcode[2]])
345
+ end
346
+
347
+ def equal
348
+ @content += @b[@opcode[3]...@opcode[4]]
349
+ end
350
+
351
+ # using this as op_helper would be equivalent to the first version of diff.rb by Bill Atkins
352
+ def op_helper_simple(tagname, tagclass, to_add)
353
+ @content << "<#{tagname} class=\"#{tagclass}\">"
354
+ @content += to_add
355
+ @content << "</#{tagname}>"
356
+ end
357
+
358
+ # this tries to put <p> tags or newline chars before the opening diff tags (<ins> or <del>)
359
+ # or after the ending diff tags
360
+ # as a result the diff tags should be the "more inside" possible.
361
+ # this seems to work nice with html containing only paragraphs
362
+ # but not sure it works if there are other tags (div, span ... ? ) around
363
+ def op_helper(tagname, tagclass, to_add)
364
+ @content << to_add.shift while ( HTMLDiff.is_newline(to_add.first) or
365
+ HTMLDiff.is_p_close_tag(to_add.first) or
366
+ HTMLDiff.is_p_open_tag(to_add.first) )
367
+ @content << "<#{tagname} class=\"#{tagclass}\">"
368
+ @content += to_add
369
+ last_tags = []
370
+ last_tags.unshift(@content.pop) while ( HTMLDiff.is_newline(@content.last) or
371
+ HTMLDiff.is_p_close_tag(@content.last) or
372
+ HTMLDiff.is_p_open_tag(@content.last) )
373
+ last_tags.unshift "</#{tagname}>"
374
+ @content += last_tags
375
+ remove_empty_diff(tagname, tagclass)
376
+ end
377
+
378
+ def remove_empty_diff(tagname, tagclass)
379
+ if @content[-2] == "<#{tagname} class=\"#{tagclass}\">" and @content[-1] == "</#{tagname}>" then
380
+ @content.pop
381
+ @content.pop
382
+ end
383
+ end
384
+
385
+ end
386
+
387
+ def self.is_newline(x)
388
+ (x == "\n") or (x == "\r") or (x == "\t")
389
+ end
390
+
391
+ def self.is_p_open_tag(x)
392
+ x =~ /\A<(p|li|ul|ol|dir|dt|dl)/
393
+ end
394
+
395
+ def self.is_p_close_tag(x)
396
+ x =~ %r!\A</(p|li|ul|ol|dir|dt|dl)!
397
+ end
398
+
399
+ def self.diff(a, b)
400
+ a = html2list(a)
401
+ b = html2list(b)
402
+
403
+ out = Builder.new(a, b)
404
+ s = SequenceMatcher.new(a, b)
405
+
406
+ s.get_opcodes.each do |opcode|
407
+ out.do_op(opcode)
408
+ end
409
+
410
+ out.result
411
+ end
412
+
413
+ def self.html2list(x)
414
+ mode = :char
415
+ cur = ''
416
+ out = []
417
+
418
+ x.split('').each do |c|
419
+ if mode == :tag
420
+ cur += c
421
+ if c == '>'
422
+ out.push(cur)
423
+ cur = ''
424
+ mode = :char
425
+ end
426
+ elsif mode == :char
427
+ if c == '<'
428
+ out.push cur
429
+ cur = c
430
+ mode = :tag
431
+ elsif c =~ /\s/
432
+ out.push cur + c
433
+ cur = ''
434
+ else
435
+ cur += c
436
+ end
437
+ end
438
+ end
439
+
440
+ out.push cur
441
+ out.find_all { |x| x != '' }
442
+ end
443
+
444
+ end