instiki 0.9.2 → 0.10.0
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 +165 -0
- data/README +68 -172
- data/app/controllers/admin_controller.rb +94 -0
- data/app/controllers/application.rb +131 -0
- data/app/controllers/file_controller.rb +129 -0
- data/app/controllers/wiki_controller.rb +354 -0
- data/{libraries/view_helper.rb → app/helpers/application_helper.rb} +68 -33
- data/app/models/author.rb +3 -3
- data/app/models/chunks/category.rb +33 -31
- data/app/models/chunks/chunk.rb +86 -20
- data/app/models/chunks/engines.rb +54 -38
- data/app/models/chunks/include.rb +41 -29
- data/app/models/chunks/literal.rb +31 -19
- data/app/models/chunks/nowiki.rb +28 -31
- data/app/models/chunks/test.rb +18 -18
- data/app/models/chunks/uri.rb +182 -97
- data/app/models/chunks/wiki.rb +141 -82
- data/app/models/file_yard.rb +58 -0
- data/app/models/page.rb +112 -86
- data/app/models/page_lock.rb +22 -23
- data/app/models/page_set.rb +89 -64
- data/app/models/revision.rb +123 -90
- data/app/models/web.rb +176 -89
- data/app/models/wiki_content.rb +207 -105
- data/app/models/wiki_service.rb +233 -83
- data/app/models/wiki_words.rb +23 -25
- data/app/views/{wiki/new_system.rhtml → admin/create_system.rhtml} +83 -78
- data/app/views/{wiki/new_web.rhtml → admin/create_web.rhtml} +69 -64
- data/app/views/admin/edit_web.rhtml +136 -0
- data/app/views/file/file.rhtml +19 -0
- data/app/views/file/import.rhtml +23 -0
- data/app/views/layouts/default.rhtml +85 -0
- data/app/views/markdown_help.rhtml +12 -16
- data/app/views/mixed_help.rhtml +7 -0
- data/app/views/navigation.rhtml +30 -19
- data/app/views/rdoc_help.rhtml +12 -16
- data/app/views/textile_help.rhtml +24 -28
- data/app/views/wiki/authors.rhtml +11 -13
- data/app/views/wiki/edit.rhtml +39 -31
- data/app/views/wiki/export.rhtml +12 -14
- data/app/views/wiki/feeds.rhtml +14 -10
- data/app/views/wiki/list.rhtml +64 -57
- data/app/views/wiki/locked.rhtml +23 -14
- data/app/views/wiki/login.rhtml +14 -11
- data/app/views/wiki/new.rhtml +31 -27
- data/app/views/wiki/page.rhtml +115 -81
- data/app/views/wiki/print.rhtml +14 -16
- data/app/views/wiki/published.rhtml +9 -10
- data/app/views/wiki/recently_revised.rhtml +27 -30
- data/app/views/wiki/revision.rhtml +103 -81
- data/app/views/wiki/rollback.rhtml +14 -9
- data/app/views/wiki/rss_feed.rhtml +22 -22
- data/app/views/wiki/search.rhtml +38 -15
- 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 -13
- data/app/views/wiki_words_help.rhtml +9 -8
- data/config/environment.rb +82 -0
- data/config/environments/development.rb +5 -0
- data/config/environments/production.rb +4 -0
- data/config/environments/test.rb +17 -0
- data/config/routes.rb +18 -0
- data/instiki +6 -67
- data/instiki.rb +3 -0
- data/lib/active_record_stub.rb +31 -0
- data/{libraries/diff → lib}/diff.rb +444 -475
- data/lib/instiki_errors.rb +15 -0
- data/{libraries → lib}/rdocsupport.rb +151 -155
- data/lib/redcloth_for_tex.rb +736 -0
- data/natives/osx/desktop_launcher/AppDelegate.h +18 -0
- data/natives/osx/desktop_launcher/AppDelegate.mm +109 -0
- data/natives/osx/desktop_launcher/Credits.html +16 -0
- data/natives/osx/desktop_launcher/English.lproj/InfoPlist.strings +0 -0
- data/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/classes.nib +13 -0
- data/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/info.nib +24 -0
- data/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/objects.nib +0 -0
- data/natives/osx/desktop_launcher/Info.plist +13 -0
- data/natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj +592 -0
- data/natives/osx/desktop_launcher/Instiki_Prefix.pch +7 -0
- data/natives/osx/desktop_launcher/MakeDMG.sh +9 -0
- data/natives/osx/desktop_launcher/main.mm +14 -0
- data/natives/osx/desktop_launcher/version.plist +16 -0
- data/public/404.html +6 -0
- data/public/500.html +6 -0
- data/public/dispatch.rb +10 -0
- data/public/favicon.ico +0 -0
- data/public/javascripts/edit_web.js +52 -0
- data/public/javascripts/prototype.js +336 -0
- data/{app/views/static_style_sheet.rhtml → public/stylesheets/instiki.css} +221 -198
- data/script/breakpointer +4 -0
- data/script/server +93 -0
- metadata +59 -32
- data/app/controllers/wiki.rb +0 -389
- data/app/models/chunks/match.rb +0 -19
- data/app/views/bottom.rhtml +0 -4
- data/app/views/top.rhtml +0 -49
- data/app/views/wiki/edit_web.rhtml +0 -138
- data/libraries/action_controller_servlet.rb +0 -177
- data/libraries/erb.rb +0 -490
- data/libraries/madeleine_service.rb +0 -68
- data/libraries/redcloth_for_tex.rb +0 -869
- data/libraries/web_controller_server.rb +0 -81
@@ -1,35 +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
|
-
|
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
35
|
\end{document}
|
@@ -1,13 +1,18 @@
|
|
1
|
-
<% @title = "Wiki webs"
|
2
|
-
|
3
|
-
<ul>
|
4
|
-
<%
|
5
|
-
<li>
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
<%
|
11
|
-
|
12
|
-
|
13
|
-
|
1
|
+
<% @title = "Wiki webs" %>
|
2
|
+
|
3
|
+
<ul>
|
4
|
+
<% @webs.each do |web| %>
|
5
|
+
<li>
|
6
|
+
<% if web.published %>
|
7
|
+
<%= link_to_page 'HomePage', web, web.name, :mode => 'publish' %>
|
8
|
+
(read-only) /
|
9
|
+
<%= link_to_page 'HomePage', web, 'editable version', :mode => 'show' %> (requires login)
|
10
|
+
<% else %>
|
11
|
+
<%= link_to_page 'HomePage', web, web.name, :mode => 'show' %>
|
12
|
+
<% end %>
|
13
|
+
<div class="byline" style="margin-bottom: 0px">
|
14
|
+
<%= web.pages.length %> pages by <%= web.authors.length %> authors
|
15
|
+
</div>
|
16
|
+
</li>
|
17
|
+
<% end %>
|
18
|
+
</ul>
|
@@ -1,8 +1,9 @@
|
|
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
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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 double
|
4
|
+
brackets is a wiki word. A camel-case wiki word can be escaped by putting \ in front of it.
|
5
|
+
</p>
|
6
|
+
<p>
|
7
|
+
Wiki words: <i>HomePage, ThreeWordsTogether, [[C++]], [[Let's play again!]]</i><br/>
|
8
|
+
Not wiki words: <i>IBM, School</i>
|
9
|
+
</p>
|
@@ -0,0 +1,82 @@
|
|
1
|
+
if RUBY_VERSION < '1.8.1'
|
2
|
+
puts 'Instiki requires Ruby 1.8.1+'
|
3
|
+
exit
|
4
|
+
end
|
5
|
+
|
6
|
+
# Enable UTF-8 support
|
7
|
+
$KCODE = 'u'
|
8
|
+
require 'jcode'
|
9
|
+
|
10
|
+
RAILS_ROOT = File.expand_path(File.dirname(__FILE__) + '/../') unless defined? RAILS_ROOT
|
11
|
+
RAILS_ENV = ENV['RAILS_ENV'] || 'production' unless defined? RAILS_ENV
|
12
|
+
|
13
|
+
unless defined? ADDITIONAL_LOAD_PATHS
|
14
|
+
# Mocks first.
|
15
|
+
ADDITIONAL_LOAD_PATHS = ["#{RAILS_ROOT}/test/mocks/#{RAILS_ENV}"]
|
16
|
+
|
17
|
+
# Then model subdirectories.
|
18
|
+
ADDITIONAL_LOAD_PATHS.concat(Dir["#{RAILS_ROOT}/app/models/[_a-z]*"])
|
19
|
+
ADDITIONAL_LOAD_PATHS.concat(Dir["#{RAILS_ROOT}/components/[_a-z]*"])
|
20
|
+
|
21
|
+
# Followed by the standard includes.
|
22
|
+
ADDITIONAL_LOAD_PATHS.concat %w(
|
23
|
+
app
|
24
|
+
app/models
|
25
|
+
app/controllers
|
26
|
+
app/helpers
|
27
|
+
app/apis
|
28
|
+
components
|
29
|
+
config
|
30
|
+
lib
|
31
|
+
vendor
|
32
|
+
vendor/rails/railties
|
33
|
+
vendor/rails/railties/lib
|
34
|
+
vendor/rails/actionpack/lib
|
35
|
+
vendor/rails/activesupport/lib
|
36
|
+
vendor/rails/activerecord/lib
|
37
|
+
vendor/rails/actionmailer/lib
|
38
|
+
vendor/rails/actionwebservice/lib
|
39
|
+
vendor/madeleine-0.7.1/lib
|
40
|
+
vendor/RedCloth-3.0.3/lib
|
41
|
+
vendor/rubyzip-0.5.6
|
42
|
+
).map { |dir| "#{File.expand_path(File.join(RAILS_ROOT, dir))}"
|
43
|
+
}.delete_if { |dir| not File.exist?(dir) }
|
44
|
+
|
45
|
+
# Prepend to $LOAD_PATH
|
46
|
+
ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Require Rails libraries.
|
50
|
+
require 'rubygems' unless File.directory?("#{RAILS_ROOT}/vendor/rails")
|
51
|
+
|
52
|
+
require 'active_support'
|
53
|
+
require 'action_controller'
|
54
|
+
|
55
|
+
require_dependency 'instiki_errors'
|
56
|
+
require_dependency 'active_record_stub'
|
57
|
+
|
58
|
+
# Environment specific configuration
|
59
|
+
require_dependency "environments/#{RAILS_ENV}"
|
60
|
+
|
61
|
+
# Configure defaults if the included environment did not.
|
62
|
+
unless defined? RAILS_DEFAULT_LOGGER
|
63
|
+
RAILS_DEFAULT_LOGGER = Logger.new(STDERR)
|
64
|
+
ActionController::Base.logger ||= RAILS_DEFAULT_LOGGER
|
65
|
+
if defined? INSTIKI_DEBUG_LOG and INSTIKI_DEBUG_LOG
|
66
|
+
RAILS_DEFAULT_LOGGER.level = Logger::DEBUG
|
67
|
+
ActionController::Base.logger.level = Logger::DEBUG
|
68
|
+
else
|
69
|
+
RAILS_DEFAULT_LOGGER.level = Logger::INFO
|
70
|
+
ActionController::Base.logger.level = Logger::INFO
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
ActionController::Base.template_root ||= "#{RAILS_ROOT}/app/views/"
|
75
|
+
ActionController::Routing::Routes.reload
|
76
|
+
Controllers = Dependencies::LoadingModule.root(
|
77
|
+
File.join(RAILS_ROOT, 'app', 'controllers'),
|
78
|
+
File.join(RAILS_ROOT, 'components')
|
79
|
+
)
|
80
|
+
|
81
|
+
require 'wiki_service'
|
82
|
+
Socket.do_not_reverse_lookup = true
|
@@ -0,0 +1,17 @@
|
|
1
|
+
Dependencies.mechanism = :require
|
2
|
+
ActionController::Base.consider_all_requests_local = true
|
3
|
+
ActionController::Base.perform_caching = false
|
4
|
+
|
5
|
+
require 'fileutils'
|
6
|
+
FileUtils.mkdir_p(RAILS_ROOT + "/log")
|
7
|
+
|
8
|
+
unless defined? TEST_LOGGER
|
9
|
+
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
|
10
|
+
log_name = RAILS_ROOT + "/log/instiki_test.#{timestamp}.log"
|
11
|
+
$stderr.puts "To see the Rails log:\n less #{log_name}"
|
12
|
+
|
13
|
+
TEST_LOGGER = ActionController::Base.logger = Logger.new(log_name)
|
14
|
+
INSTIKI_DEBUG_LOG = true unless defined? INSTIKI_DEBUG_LOG
|
15
|
+
|
16
|
+
WikiService.storage_path = RAILS_ROOT + '/storage/test/'
|
17
|
+
end
|
data/config/routes.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
ActionController::Routing.draw do |map|
|
2
|
+
map.connect 'create_system', :controller => 'admin', :action => 'create_system'
|
3
|
+
map.connect 'create_web', :controller => 'admin', :action => 'create_web'
|
4
|
+
map.connect ':web/edit_web', :controller => 'admin', :action => 'edit_web'
|
5
|
+
map.connect 'remove_orphaned_pages', :controller => 'admin', :action => 'remove_orphaned_pages'
|
6
|
+
|
7
|
+
map.connect ':web/file/:id', :controller => 'file', :action => 'file'
|
8
|
+
map.connect ':web/pic/:id', :controller => 'file', :action => 'pic'
|
9
|
+
map.connect ':web/import/:id', :controller => 'file', :action => 'import'
|
10
|
+
|
11
|
+
map.connect ':web/login', :controller => 'wiki', :action => 'login'
|
12
|
+
map.connect 'web_list', :controller => 'wiki', :action => 'web_list'
|
13
|
+
map.connect ':web/web_list', :controller => 'wiki', :action => 'web_list'
|
14
|
+
map.connect ':web/:action/:id', :controller => 'wiki'
|
15
|
+
map.connect ':web/:action', :controller => 'wiki'
|
16
|
+
map.connect ':web', :controller => 'wiki', :action => 'index'
|
17
|
+
map.connect '', :controller => 'wiki', :action => 'index'
|
18
|
+
end
|
data/instiki
CHANGED
@@ -1,67 +1,6 @@
|
|
1
|
-
#!/usr/
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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/")
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
# Executable file for a gem
|
4
|
+
# must be same as ./instiki.rb
|
5
|
+
|
6
|
+
load File.dirname(__FILE__) + '/script/server'
|
data/instiki.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# This project uses Railties, which has an external dependency on ActiveRecord
|
2
|
+
# Since ActiveRecord may not be present in Instiki runtime environment, this
|
3
|
+
# file provides a stub replacement for it
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
class Base
|
7
|
+
|
8
|
+
# dependency in railties/lib/dispatcher.rb
|
9
|
+
def self.reset_column_information_and_inheritable_attributes_for_all_subclasses
|
10
|
+
# noop
|
11
|
+
end
|
12
|
+
|
13
|
+
# dependency in actionpack/lib/action_controller/benchmarking.rb
|
14
|
+
def self.connected?
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
# dependency in actionpack/lib/action_controller/benchmarking.rb
|
19
|
+
def self.connection
|
20
|
+
return ConnectionStub
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
module ConnectionStub
|
26
|
+
def self.reset_runtime
|
27
|
+
0
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -1,475 +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
|
401
|
-
|
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 =
|
415
|
-
cur = ''
|
416
|
-
out = []
|
417
|
-
|
418
|
-
x
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
out.push
|
429
|
-
cur =
|
430
|
-
mode =
|
431
|
-
|
432
|
-
cur
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
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
|
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
|