instiki 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/README +172 -0
  2. data/app/controllers/wiki.rb +389 -0
  3. data/app/models/author.rb +4 -0
  4. data/app/models/chunks/category.rb +31 -0
  5. data/app/models/chunks/chunk.rb +20 -0
  6. data/app/models/chunks/engines.rb +38 -0
  7. data/app/models/chunks/include.rb +29 -0
  8. data/app/models/chunks/literal.rb +19 -0
  9. data/app/models/chunks/match.rb +19 -0
  10. data/app/models/chunks/nowiki.rb +31 -0
  11. data/app/models/chunks/test.rb +18 -0
  12. data/app/models/chunks/uri.rb +97 -0
  13. data/app/models/chunks/wiki.rb +82 -0
  14. data/app/models/page.rb +86 -0
  15. data/app/models/page_lock.rb +24 -0
  16. data/app/models/page_set.rb +64 -0
  17. data/app/models/revision.rb +90 -0
  18. data/app/models/web.rb +89 -0
  19. data/app/models/wiki_content.rb +105 -0
  20. data/app/models/wiki_service.rb +83 -0
  21. data/app/models/wiki_words.rb +25 -0
  22. data/app/views/bottom.rhtml +4 -0
  23. data/app/views/markdown_help.rhtml +16 -0
  24. data/app/views/navigation.rhtml +19 -0
  25. data/app/views/rdoc_help.rhtml +16 -0
  26. data/app/views/static_style_sheet.rhtml +199 -0
  27. data/app/views/textile_help.rhtml +28 -0
  28. data/app/views/top.rhtml +49 -0
  29. data/app/views/wiki/authors.rhtml +13 -0
  30. data/app/views/wiki/edit.rhtml +31 -0
  31. data/app/views/wiki/edit_web.rhtml +138 -0
  32. data/app/views/wiki/export.rhtml +14 -0
  33. data/app/views/wiki/feeds.rhtml +10 -0
  34. data/app/views/wiki/list.rhtml +57 -0
  35. data/app/views/wiki/locked.rhtml +14 -0
  36. data/app/views/wiki/login.rhtml +11 -0
  37. data/app/views/wiki/new.rhtml +27 -0
  38. data/app/views/wiki/new_system.rhtml +78 -0
  39. data/app/views/wiki/new_web.rhtml +64 -0
  40. data/app/views/wiki/page.rhtml +81 -0
  41. data/app/views/wiki/print.rhtml +16 -0
  42. data/app/views/wiki/published.rhtml +10 -0
  43. data/app/views/wiki/recently_revised.rhtml +30 -0
  44. data/app/views/wiki/revision.rhtml +81 -0
  45. data/app/views/wiki/rollback.rhtml +31 -0
  46. data/app/views/wiki/rss_feed.rhtml +22 -0
  47. data/app/views/wiki/search.rhtml +15 -0
  48. data/app/views/wiki/tex.rhtml +23 -0
  49. data/app/views/wiki/tex_web.rhtml +35 -0
  50. data/app/views/wiki/web_list.rhtml +13 -0
  51. data/app/views/wiki_words_help.rhtml +8 -0
  52. data/instiki +67 -0
  53. data/libraries/action_controller_servlet.rb +177 -0
  54. data/libraries/diff/diff.rb +475 -0
  55. data/libraries/erb.rb +490 -0
  56. data/libraries/madeleine_service.rb +68 -0
  57. data/libraries/rdocsupport.rb +156 -0
  58. data/libraries/redcloth_for_tex.rb +869 -0
  59. data/libraries/view_helper.rb +33 -0
  60. data/libraries/web_controller_server.rb +81 -0
  61. metadata +142 -0
@@ -0,0 +1,31 @@
1
+ <%
2
+ @title = "Rollback to #{@page.plain_name} Rev ##{@revision.number}"
3
+ @content_width = 720
4
+ @hide_navigation = true
5
+ %><%= sub_template "top" %>
6
+
7
+ <%= "<p style='color:red'>Please correct the error that caused this error in rendering:<br/><small>#{@params["msg"]}</small></p>" if @params["msg"] %>
8
+
9
+ <form id="editForm" action="../save/<%= @page.name %>" method="post" onSubmit="cleanAuthorName();">
10
+ <p>
11
+ <textarea name="content" style="font-size: 12px; width: 450px; height: 500px"><%= @revision.content %></textarea>
12
+ </p>
13
+ <p>
14
+ <input type="submit" value="Update"> as
15
+ <input type="text" name="author" id="authorName" value="<%= @author %>"
16
+ onClick="this.value == 'AnonymousCoward' ? this.value = '' : true">
17
+ | <a href="../cancel_edit/<%= @page.name %>">Cancel</a> <small>(unlocks page)</small>
18
+ </p>
19
+ </form>
20
+
21
+ <%= render_markup_help %>
22
+
23
+ <script language="JavaScript1.2">
24
+ function cleanAuthorName() {
25
+ if (document.getElementById('authorName').value == "") {
26
+ document.getElementById('authorName').value = 'AnonymousCoward';
27
+ }
28
+ }
29
+ </script>
30
+
31
+ <%= sub_template "bottom" %>
@@ -0,0 +1,22 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
3
+ <channel>
4
+ <title><%= @web.name %></title>
5
+ <link><%= "#{@web_url}/show/HomePage" %></link>
6
+ <description>An Instiki wiki</description>
7
+ <language>en-us</language>
8
+ <ttl>40</ttl>
9
+ <% for page in @pages_by_revision %>
10
+ <item>
11
+ <title><%= page.plain_name %></title>
12
+ <% unless @hide_description %>
13
+ <description><%= CGI.escapeHTML(page.display_content) %></description>
14
+ <% end %>
15
+ <pubDate><%= page.created_at.strftime "%a, %e %b %Y %H:%M:%S %Z" %></pubDate>
16
+ <guid><%= "#{@web_url}/show/#{page.name}" %></guid>
17
+ <link><%= "#{@web_url}/show/#{page.name}" %></link>
18
+ <dc:creator><%= WikiWords.separate(page.author) %></dc:creator>
19
+ </item>
20
+ <% end %>
21
+ </channel>
22
+ </rss>
@@ -0,0 +1,15 @@
1
+ <% @title = @results.length > 0 ? "#{@results.length} pages contains \"#{@params["query"]}\"" : "No pages contains \"#{@query}\"" %><%= sub_template "top" %>
2
+
3
+ <% if @results.length > 0 %>
4
+ <ul>
5
+ <% for page in @results %>
6
+ <li><a href="../show/<%= page.name %>"><%= page.plain_name %></a></li>
7
+ <% end %>
8
+ </ul>
9
+ <% else %>
10
+ <p>Perhaps you should try expanding your query. Remember that Instiki searches for entire phrases, so if you search for "all that jazz" it will not match pages that contain these words in separation&mdash;only as a sentence fragment.</p>
11
+
12
+ <p>If you're a high-tech computer wizard, you might even want try constructing a regular expression. That's actually what Instiki uses, so go right ahead and flex your "[a-z]*Leet?RegExpSkill(s|z)"</p>
13
+ <% end %>
14
+
15
+ <%= sub_template "bottom" %>
@@ -0,0 +1,23 @@
1
+ \documentclass[12pt,titlepage]{article}
2
+
3
+ \usepackage[danish]{babel} %danske tekster
4
+ \usepackage[OT1]{fontenc} %rigtige danske bogstaver...
5
+ \usepackage{a4}
6
+ \usepackage{graphicx}
7
+ \usepackage{ucs}
8
+ \usepackage[utf8]{inputenc}
9
+ \input epsf
10
+
11
+ %-------------------------------------------------------------------
12
+
13
+ \begin{document}
14
+
15
+ \sloppy
16
+
17
+ %-------------------------------------------------------------------
18
+
19
+ \section*{<%= @page.name %>}
20
+
21
+ <%= @tex_content %>
22
+
23
+ \end{document}
@@ -0,0 +1,35 @@
1
+ \documentclass[12pt,titlepage]{article}
2
+
3
+ \usepackage{fancyhdr}
4
+ \pagestyle{fancy}
5
+
6
+ \fancyhead[LE,RO]{}
7
+ \fancyhead[LO,RE]{\nouppercase{\bfseries \leftmark}}
8
+ \fancyfoot[C]{\thepage}
9
+
10
+ \usepackage[danish]{babel} %danske tekster
11
+ \usepackage{a4}
12
+ \usepackage{graphicx}
13
+ \usepackage{ucs}
14
+ \usepackage[utf8]{inputenc}
15
+ \input epsf
16
+
17
+
18
+ %-------------------------------------------------------------------
19
+
20
+ \title{<%= @web_name %>}
21
+
22
+ \begin{document}
23
+
24
+ \maketitle
25
+
26
+ \tableofcontents
27
+ \pagebreak
28
+
29
+ \sloppy
30
+
31
+ %-------------------------------------------------------------------
32
+
33
+ <%= @tex_content %>
34
+
35
+ \end{document}
@@ -0,0 +1,13 @@
1
+ <% @title = "Wiki webs" %><%= sub_template "top" %>
2
+
3
+ <ul>
4
+ <% for web in @webs %>
5
+ <li>
6
+ <a href="/<%= web.address %>/show/HomePage"><%= web.name %></a>
7
+ (<%= web.pages.length %> pages by <%= web.authors.length %> authors)
8
+ <% if web.published then %>(<a href="/<%= web.address %>/published/HomePage">published</a>)<% end %>
9
+ </li>
10
+ <% end %>
11
+ </ul>
12
+
13
+ <%= sub_template "bottom" %>
@@ -0,0 +1,8 @@
1
+ <h3>Wiki words</h3>
2
+ <p style="border-top: 1px dotted #ccc; margin-top: 0px">
3
+ Two or more uppercase words stuck together (camel case) or any phrase surrounded by doubble brackets is a wiki word. A camel-case wiki word can be escaped by putting \ in front of it.
4
+ </p>
5
+ <p>
6
+ Wiki words: <i>HomePage, ThreeWordsTogether, [[C++]], [[Let's play again!]]</i><br/>
7
+ Not wiki words: <i>IBM, School</i>
8
+ </p>
data/instiki ADDED
@@ -0,0 +1,67 @@
1
+ #!/usr/local/bin/ruby
2
+
3
+ if RUBY_VERSION < "1.8.1"
4
+ puts "Instiki requires Ruby 1.8.1+"
5
+ exit
6
+ end
7
+
8
+ require 'optparse'
9
+ require 'fileutils'
10
+
11
+ cdir = File.expand_path(File.dirname(__FILE__))
12
+ %w( /libraries/ /app/models /app/controllers ).each { |dir| $:.unshift(cdir + dir) }
13
+ %w( web_controller_server action_controller_servlet wiki_service wiki ).each { |lib| require lib }
14
+
15
+ fork_available = true
16
+ begin
17
+ exit unless fork
18
+ rescue NotImplementedError
19
+ fork_available = false
20
+ end
21
+
22
+ begin
23
+ pdflatex_available = system "pdflatex -version"
24
+ rescue Errno::ENOENT
25
+ pdflatex_available = false
26
+ end
27
+
28
+ OPTIONS = {
29
+ :server_type => fork_available ? Daemon : SimpleServer,
30
+ :port => 2500,
31
+ :storage => "#{File.expand_path(FileUtils.pwd)}/storage",
32
+ :pdflatex => pdflatex_available
33
+ }
34
+
35
+ ARGV.options do |opts|
36
+ script_name = File.basename($0)
37
+ opts.banner = "Usage: ruby #{script_name} [options]"
38
+
39
+ opts.separator ""
40
+
41
+ opts.on("-p", "--port=port", Integer,
42
+ "Runs Instiki on the specified port.",
43
+ "Default: 2500") { |OPTIONS[:port]| }
44
+ opts.on("-s", "--simple", "--simple-server",
45
+ "Forces Instiki not to run as a Daemon if fork is available."
46
+ ) { OPTIONS[:server_type] = SimpleServer }
47
+ opts.on("-t", "--storage=storage", String,
48
+ "Makes Instiki use the specified directory for storage.",
49
+ "Default: ./storage/[port]") { |OPTIONS[:storage]| }
50
+
51
+ opts.separator ""
52
+
53
+ opts.on("-h", "--help",
54
+ "Show this help message.") { puts opts; exit }
55
+
56
+ opts.parse!
57
+ end
58
+
59
+ Socket.do_not_reverse_lookup = true
60
+
61
+ storage_dir = OPTIONS[:storage] + "/" + OPTIONS[:port].to_s
62
+ FileUtils.mkdir_p(storage_dir)
63
+ WikiService.storage_path = storage_dir
64
+
65
+ WikiController.template_root = "#{cdir}/app/views/"
66
+
67
+ WebControllerServer.new(OPTIONS[:port], OPTIONS[:server_type], "#{cdir}/app/controllers/")
@@ -0,0 +1,177 @@
1
+ require 'erb'
2
+ require 'cgi'
3
+ require 'webrick'
4
+ include WEBrick
5
+
6
+ require 'view_helper'
7
+
8
+ class ActionControllerServlet < HTTPServlet::AbstractServlet
9
+ @@template_root = "./views"
10
+ def self.template_root() @@template_root end
11
+ def self.template_root=(template_root) @@template_root = template_root end
12
+
13
+ include ViewHelper
14
+
15
+ def do_POST(req, res) do_GET(req, res) end
16
+
17
+ def do_GET(req, res)
18
+ @req, @res = req, res
19
+
20
+ @res['Content-Type'] = "text/html; charset=utf-8"
21
+ @res['Pragma'] = "no-cache"
22
+ @res['Cache-Control'] = "no-cache"
23
+
24
+ @params = @req.query
25
+ @assigns = {}
26
+ @performed_render = @performed_redirect = false
27
+
28
+ @logger.info "Performing #{action_name}"
29
+ @logger.info " Parameters: #{@params.inspect}"
30
+ @logger.info " Cookies: #{@req.cookies.collect { |c| "#{c.name} => #{c.value}" }.join(", ") }"
31
+
32
+ perform_action
33
+ @res
34
+ end
35
+
36
+ protected
37
+ def template_root() self.class.template_root end
38
+
39
+ # Issues a HTTP 302 (location) redirect to the specified <tt>path</tt>. Query string
40
+ # parameters can be specified in the <tt>params</tt> hash and are automatically URL escaped.
41
+ # An <tt>anchor</tt> can also be specified (as a string).
42
+ def redirect_path(path, params = nil, anchor = nil)
43
+ path << build_query_string(params) unless params.nil?
44
+ path << "\##{anchor}" unless anchor.nil?
45
+ @res.set_redirect WEBrick::HTTPStatus::MovedPermanently, path
46
+ @performed_redirect = true
47
+ end
48
+
49
+ # Redirects the browser to another action within the current controller, so redirect_path "pages"
50
+ # within a controller called WikiController would take the user to "http://www.example.com/wiki/pages"
51
+ def redirect_action(action, params = nil, anchor = nil)
52
+ redirect_path "/#{controller_name}/#{action}", params, anchor
53
+ end
54
+
55
+
56
+ # Compiles the template response and adds it to the response. If no template_name has been
57
+ # supplied, the action parameter parsed to the controller is used. So invoking
58
+ # render on a object initialized with myController.new("show_person") will compile the template
59
+ # located at "../views/show_person.rhtml". Notice that the ".rhtml" extension is postfixed
60
+ # to the template_name regardless of one was passed or that the action parameter was used.
61
+ def render(template_name = "#{controller_name}/#{action_name}")
62
+ @performed_render = true
63
+ @logger.info "Rendering: #{template_name}"
64
+ add_instance_variables_to_assigns
65
+ @res.body = template_result "#{template_root}/#{template_name}.rhtml"
66
+ end
67
+
68
+ def render_text(text)
69
+ @performed_render = true
70
+ @logger.info "Rendering in text"
71
+ @res.body = text
72
+ end
73
+
74
+ # Wrapper around render that presumes the current controller is used as a base for the action,
75
+ # so render_action("page") in WikiController will be equal to render("wiki/page")
76
+ def render_action(action_name)
77
+ render "#{controller_name}/#{action_name}"
78
+ end
79
+
80
+ def sub_template(template_name)
81
+ template_result "#{template_root}/#{template_name}.rhtml"
82
+ end
83
+
84
+ # Returns the value of the cookie matching +name+ (or nil if none is found).
85
+ def read_cookie(key)
86
+ cookies = @req.cookies.select { |cookie| cookie.name == key }
87
+ cookies.empty? ? nil : cookies.first.value
88
+ end
89
+
90
+ def write_cookie(key, value, permanent = false)
91
+ @logger.info "Writing cookie: #{key} => #{value}"
92
+
93
+ cookie = WEBrick::Cookie.new(key, value)
94
+ cookie.path = "/"
95
+ cookie.expires = Time.local(2030) if permanent
96
+ @res.cookies << cookie
97
+ end
98
+
99
+
100
+ # Returns the last part of the uri path ("page" in "/wiki/page") by default, but
101
+ # can be overwritten to implement mod_rewrite-like behaviour, such as "page" in "/wiki/page/home"
102
+ def action_name
103
+ @req.path.to_s.split(/\//).last
104
+ end
105
+
106
+ # Returns an array with each of the parts in the request as an element. So /something/cool/dude
107
+ # returns ["something", "cool", "dude"]
108
+ def request_path
109
+ request_path_parts = @req.path.to_s.split(/\//)
110
+ request_path_parts.length > 1 ? request_path_parts[1..-1] : []
111
+ end
112
+
113
+ # Can be overwritten by a controller class to implement shared behaviour for all the actions in
114
+ # the class, such as security measures
115
+ def before_action() end
116
+
117
+
118
+ private
119
+ # Wraps around all action calls to ensure the session is properly updated and closed.
120
+ # Figures out whether an action, such as "list", is implemented as show_list or do_list.
121
+ # The show_* type has precedence and automatically calls render after execution.
122
+ def perform_action
123
+ if before_action == false then return end
124
+
125
+ if action_methods.include?(action_name)
126
+ send(action_name)
127
+ render unless @performed_render || @performed_redirect || !@res.body.empty?
128
+ elsif template_exists_for_action
129
+ render
130
+ else
131
+ raise "No action responded to #{action_name}", caller
132
+ end
133
+ end
134
+
135
+ def add_instance_variables_to_assigns
136
+ instance_variables.each { |var|
137
+ next if protected_instance_variables.include?(var)
138
+ @assigns[var[1..-1]] = instance_variable_get(var)
139
+ }
140
+ end
141
+
142
+ def protected_instance_variables
143
+ [ "@assigns", "@performed_redirect", "@performed_render" ]
144
+ end
145
+
146
+ def action_methods
147
+ methods - Object.instance_methods
148
+ end
149
+
150
+ # Returns true if a template exists in the controller directory for the specified <tt>action_name</tt>,
151
+ # which would mean true if called for WikiController#changes and "/views/wiki/changes.rhtml" existed.
152
+ def template_exists_for_action
153
+ File.exist? action_template_path
154
+ end
155
+
156
+ def action_template_path(action = action_name)
157
+ "#{template_root}/#{controller_name}/#{action}.rhtml"
158
+ end
159
+
160
+ def template_result(template_path)
161
+ @assigns.each { |key, value| instance_variable_set "@#{key}", value }
162
+ ERB.new(IO.readlines(template_path).join).result(binding)
163
+ end
164
+
165
+ # Converts the class name from something like "OneModule::TwoModule::NeatController"
166
+ # to "neat".
167
+ def controller_name
168
+ self.class.to_s.split("::").last.sub(/Controller/, "").downcase
169
+ end
170
+
171
+ # Returns a query string with escaped keys and values from the passed hash.
172
+ def build_query_string(hash)
173
+ elements = []
174
+ hash.each { |key, value| elements << "#{CGI.escape(key)}=#{CGI.escape(value.to_s)}" }
175
+ "?" + elements.join("&")
176
+ end
177
+ end
@@ -0,0 +1,475 @@
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, b = a.split(//), b.split(//) if a.kind_of? String and b.kind_of? String
401
+ a, b = html2list(a), 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, b=false)
414
+ mode = 'char'
415
+ cur = ''
416
+ out = []
417
+
418
+ x = x.split(//) if x.kind_of? String
419
+
420
+ x.each do |c|
421
+ if mode == 'tag'
422
+ if c == '>'
423
+ if b
424
+ cur += ']'
425
+ else
426
+ cur += c
427
+ end
428
+ out.push(cur)
429
+ cur = ''
430
+ mode = 'char'
431
+ else
432
+ cur += c
433
+ end
434
+ elsif mode == 'char'
435
+ if c == '<'
436
+ out.push cur
437
+ if b
438
+ cur = '['
439
+ else
440
+ cur = c
441
+ end
442
+ mode = 'tag'
443
+ elsif /\s/.match c
444
+ out.push cur + c
445
+ cur = ''
446
+ else
447
+ cur += c
448
+ end
449
+ end
450
+ end
451
+
452
+ out.push cur
453
+ # TODO: make something better here
454
+ out.each{|x| x.chomp! unless is_newline(x)}
455
+ out.find_all { |x| x != '' }
456
+ end
457
+
458
+
459
+ end
460
+
461
+ if __FILE__ == $0
462
+
463
+ require 'pp'
464
+ # a = "<p>this is the original string</p>" # \n<p>but around the world</p>"
465
+ # b = "<p>this is the original </p><p>other parag</p><p>string</p>"
466
+ a = "<ul>\n\t<li>one</li>\n\t<li>two</li>\n</ul>"
467
+ b = "<ul>\n\t<li>one</li>\n\t<li>two\n\t<ul><li>abc</li></ul></li>\n</ul>"
468
+ puts a
469
+ pp HTMLDiff.html2list(a)
470
+ puts
471
+ puts b
472
+ pp HTMLDiff.html2list(b)
473
+ puts
474
+ puts HTMLDiff.diff(a, b)
475
+ end