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.
- data/CHANGELOG +174 -165
- data/README +68 -68
- data/app/controllers/admin_controller.rb +94 -94
- data/app/controllers/application.rb +135 -131
- data/app/controllers/file_controller.rb +129 -129
- data/app/controllers/wiki_controller.rb +354 -354
- data/app/helpers/application_helper.rb +68 -68
- data/app/models/author.rb +3 -3
- data/app/models/chunks/category.rb +33 -33
- data/app/models/chunks/chunk.rb +86 -86
- data/app/models/chunks/engines.rb +61 -54
- data/app/models/chunks/include.rb +41 -41
- data/app/models/chunks/literal.rb +31 -31
- data/app/models/chunks/nowiki.rb +28 -28
- data/app/models/chunks/test.rb +18 -18
- data/app/models/chunks/uri.rb +182 -182
- data/app/models/chunks/wiki.rb +141 -141
- data/app/models/file_yard.rb +58 -58
- data/app/models/page.rb +112 -112
- data/app/models/page_lock.rb +22 -22
- data/app/models/page_set.rb +89 -89
- data/app/models/revision.rb +123 -123
- data/app/models/web.rb +182 -176
- data/app/models/wiki_content.rb +207 -207
- data/app/models/wiki_service.rb +233 -233
- data/app/models/wiki_words.rb +23 -23
- data/app/views/admin/create_system.rhtml +83 -83
- data/app/views/admin/create_web.rhtml +69 -69
- data/app/views/admin/edit_web.rhtml +137 -136
- data/app/views/file/file.rhtml +18 -18
- data/app/views/file/import.rhtml +22 -22
- data/app/views/layouts/default.rhtml +86 -85
- data/app/views/markdown_help.rhtml +12 -12
- data/app/views/mixed_help.rhtml +6 -6
- data/app/views/navigation.rhtml +30 -30
- data/app/views/rdoc_help.rhtml +12 -12
- data/app/views/textile_help.rhtml +24 -24
- data/app/views/wiki/authors.rhtml +11 -11
- data/app/views/wiki/edit.rhtml +39 -39
- data/app/views/wiki/export.rhtml +12 -12
- data/app/views/wiki/feeds.rhtml +14 -14
- data/app/views/wiki/list.rhtml +64 -64
- data/app/views/wiki/locked.rhtml +23 -23
- data/app/views/wiki/login.rhtml +14 -14
- data/app/views/wiki/new.rhtml +31 -31
- data/app/views/wiki/page.rhtml +115 -115
- data/app/views/wiki/print.rhtml +14 -14
- data/app/views/wiki/published.rhtml +9 -9
- data/app/views/wiki/recently_revised.rhtml +26 -26
- data/app/views/wiki/revision.rhtml +103 -103
- data/app/views/wiki/rollback.rhtml +36 -36
- data/app/views/wiki/rss_feed.rhtml +22 -22
- data/app/views/wiki/search.rhtml +38 -38
- data/app/views/wiki/tex.rhtml +22 -22
- data/app/views/wiki/tex_web.rhtml +34 -34
- data/app/views/wiki/web_list.rhtml +18 -18
- data/app/views/wiki_words_help.rhtml +9 -9
- data/config/environment.rb +82 -82
- data/config/environments/development.rb +5 -5
- data/config/environments/production.rb +4 -4
- data/config/environments/test.rb +17 -17
- data/config/routes.rb +18 -18
- data/lib/active_record_stub.rb +31 -31
- data/lib/bluecloth_tweaked.rb +1127 -0
- data/lib/diff.rb +444 -444
- data/lib/instiki_errors.rb +14 -14
- data/lib/rdocsupport.rb +151 -151
- data/lib/redcloth_for_tex.rb +736 -736
- data/natives/osx/desktop_launcher/AppDelegate.h +18 -18
- data/natives/osx/desktop_launcher/AppDelegate.mm +109 -109
- data/natives/osx/desktop_launcher/Credits.html +15 -15
- data/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/classes.nib +12 -12
- data/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/info.nib +24 -24
- data/natives/osx/desktop_launcher/Info.plist +12 -12
- data/natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj +592 -592
- data/natives/osx/desktop_launcher/Instiki_Prefix.pch +7 -7
- data/natives/osx/desktop_launcher/MakeDMG.sh +9 -9
- data/natives/osx/desktop_launcher/main.mm +14 -14
- data/natives/osx/desktop_launcher/version.plist +16 -16
- data/public/404.html +5 -5
- data/public/500.html +5 -5
- data/public/dispatch.rb +9 -9
- data/public/javascripts/edit_web.js +52 -52
- data/public/javascripts/prototype.js +336 -336
- data/public/stylesheets/instiki.css +222 -222
- data/script/breakpointer +4 -4
- data/script/server +93 -93
- 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
|