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.
Files changed (102) hide show
  1. data/CHANGELOG +165 -0
  2. data/README +68 -172
  3. data/app/controllers/admin_controller.rb +94 -0
  4. data/app/controllers/application.rb +131 -0
  5. data/app/controllers/file_controller.rb +129 -0
  6. data/app/controllers/wiki_controller.rb +354 -0
  7. data/{libraries/view_helper.rb → app/helpers/application_helper.rb} +68 -33
  8. data/app/models/author.rb +3 -3
  9. data/app/models/chunks/category.rb +33 -31
  10. data/app/models/chunks/chunk.rb +86 -20
  11. data/app/models/chunks/engines.rb +54 -38
  12. data/app/models/chunks/include.rb +41 -29
  13. data/app/models/chunks/literal.rb +31 -19
  14. data/app/models/chunks/nowiki.rb +28 -31
  15. data/app/models/chunks/test.rb +18 -18
  16. data/app/models/chunks/uri.rb +182 -97
  17. data/app/models/chunks/wiki.rb +141 -82
  18. data/app/models/file_yard.rb +58 -0
  19. data/app/models/page.rb +112 -86
  20. data/app/models/page_lock.rb +22 -23
  21. data/app/models/page_set.rb +89 -64
  22. data/app/models/revision.rb +123 -90
  23. data/app/models/web.rb +176 -89
  24. data/app/models/wiki_content.rb +207 -105
  25. data/app/models/wiki_service.rb +233 -83
  26. data/app/models/wiki_words.rb +23 -25
  27. data/app/views/{wiki/new_system.rhtml → admin/create_system.rhtml} +83 -78
  28. data/app/views/{wiki/new_web.rhtml → admin/create_web.rhtml} +69 -64
  29. data/app/views/admin/edit_web.rhtml +136 -0
  30. data/app/views/file/file.rhtml +19 -0
  31. data/app/views/file/import.rhtml +23 -0
  32. data/app/views/layouts/default.rhtml +85 -0
  33. data/app/views/markdown_help.rhtml +12 -16
  34. data/app/views/mixed_help.rhtml +7 -0
  35. data/app/views/navigation.rhtml +30 -19
  36. data/app/views/rdoc_help.rhtml +12 -16
  37. data/app/views/textile_help.rhtml +24 -28
  38. data/app/views/wiki/authors.rhtml +11 -13
  39. data/app/views/wiki/edit.rhtml +39 -31
  40. data/app/views/wiki/export.rhtml +12 -14
  41. data/app/views/wiki/feeds.rhtml +14 -10
  42. data/app/views/wiki/list.rhtml +64 -57
  43. data/app/views/wiki/locked.rhtml +23 -14
  44. data/app/views/wiki/login.rhtml +14 -11
  45. data/app/views/wiki/new.rhtml +31 -27
  46. data/app/views/wiki/page.rhtml +115 -81
  47. data/app/views/wiki/print.rhtml +14 -16
  48. data/app/views/wiki/published.rhtml +9 -10
  49. data/app/views/wiki/recently_revised.rhtml +27 -30
  50. data/app/views/wiki/revision.rhtml +103 -81
  51. data/app/views/wiki/rollback.rhtml +14 -9
  52. data/app/views/wiki/rss_feed.rhtml +22 -22
  53. data/app/views/wiki/search.rhtml +38 -15
  54. data/app/views/wiki/tex.rhtml +22 -22
  55. data/app/views/wiki/tex_web.rhtml +34 -34
  56. data/app/views/wiki/web_list.rhtml +18 -13
  57. data/app/views/wiki_words_help.rhtml +9 -8
  58. data/config/environment.rb +82 -0
  59. data/config/environments/development.rb +5 -0
  60. data/config/environments/production.rb +4 -0
  61. data/config/environments/test.rb +17 -0
  62. data/config/routes.rb +18 -0
  63. data/instiki +6 -67
  64. data/instiki.rb +3 -0
  65. data/lib/active_record_stub.rb +31 -0
  66. data/{libraries/diff → lib}/diff.rb +444 -475
  67. data/lib/instiki_errors.rb +15 -0
  68. data/{libraries → lib}/rdocsupport.rb +151 -155
  69. data/lib/redcloth_for_tex.rb +736 -0
  70. data/natives/osx/desktop_launcher/AppDelegate.h +18 -0
  71. data/natives/osx/desktop_launcher/AppDelegate.mm +109 -0
  72. data/natives/osx/desktop_launcher/Credits.html +16 -0
  73. data/natives/osx/desktop_launcher/English.lproj/InfoPlist.strings +0 -0
  74. data/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/classes.nib +13 -0
  75. data/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/info.nib +24 -0
  76. data/natives/osx/desktop_launcher/English.lproj/MainMenu.nib/objects.nib +0 -0
  77. data/natives/osx/desktop_launcher/Info.plist +13 -0
  78. data/natives/osx/desktop_launcher/Instiki.xcode/project.pbxproj +592 -0
  79. data/natives/osx/desktop_launcher/Instiki_Prefix.pch +7 -0
  80. data/natives/osx/desktop_launcher/MakeDMG.sh +9 -0
  81. data/natives/osx/desktop_launcher/main.mm +14 -0
  82. data/natives/osx/desktop_launcher/version.plist +16 -0
  83. data/public/404.html +6 -0
  84. data/public/500.html +6 -0
  85. data/public/dispatch.rb +10 -0
  86. data/public/favicon.ico +0 -0
  87. data/public/javascripts/edit_web.js +52 -0
  88. data/public/javascripts/prototype.js +336 -0
  89. data/{app/views/static_style_sheet.rhtml → public/stylesheets/instiki.css} +221 -198
  90. data/script/breakpointer +4 -0
  91. data/script/server +93 -0
  92. metadata +59 -32
  93. data/app/controllers/wiki.rb +0 -389
  94. data/app/models/chunks/match.rb +0 -19
  95. data/app/views/bottom.rhtml +0 -4
  96. data/app/views/top.rhtml +0 -49
  97. data/app/views/wiki/edit_web.rhtml +0 -138
  98. data/libraries/action_controller_servlet.rb +0 -177
  99. data/libraries/erb.rb +0 -490
  100. data/libraries/madeleine_service.rb +0 -68
  101. data/libraries/redcloth_for_tex.rb +0 -869
  102. 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" %><%= 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" %>
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 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>
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,5 @@
1
+ Dependencies.mechanism = :require
2
+ ActionController::Base.consider_all_requests_local = true
3
+ ActionController::Base.perform_caching = false
4
+ BREAKPOINT_SERVER_PORT = 42531
5
+ INSTIKI_DEBUG_LOG = true unless defined? INSTIKI_DEBUG_LOG
@@ -0,0 +1,4 @@
1
+ Dependencies.mechanism = :require
2
+ ActionController::Base.consider_all_requests_local = false
3
+ ActionController::Base.perform_caching = false
4
+
@@ -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
@@ -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/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/")
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'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/ruby
2
+
3
+ load File.dirname(__FILE__) + '/script/server'
@@ -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, 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
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