markdiff 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e77befcb13e1946b68b5820403d3a3583836fbe8
4
+ data.tar.gz: 99b131827eece95036986a5f528d96f4ebfb484a
5
+ SHA512:
6
+ metadata.gz: 5201a42d38ed0d77c37b62171abd3163ef0428cc06648a4024082fc22e36d7a001e0720b8389f178e4720bafe123411f3102289c2ced21dc35ad77bab5222491
7
+ data.tar.gz: b0267e2e49d7ca40cc8bd7c82c08652bd0ec1786fcd35e01553fa0d77a2707363aa98f52d82be3302bc99381a90bd2e3c35ed6de8f5318b2d2f1eee01b190415
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.0
4
+ before_install: gem install bundler -v 1.10.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in markdiff.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 r7kamura
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # Markdiff
2
+ Rendered Markdown differ.
3
+
4
+ ## Usage
5
+ ```rb
6
+ require "markdiff"
7
+
8
+ differ = Markdiff::Differ.new
9
+ node = differ.render("<p>a</p>", "<p>b</p>")
10
+ node.to_html #=> "<p><del>a</del><ins>b</ins></p>"
11
+ ```
12
+
13
+ See [spec/markdiff/differ_spec.rb](spec/markdiff/differ_spec.rb) for more examples.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,157 @@
1
+ require "nokogiri"
2
+ require "markdiff/operations/add_child_operation"
3
+ require "markdiff/operations/add_data_before_href_operation"
4
+ require "markdiff/operations/add_data_before_tag_name_operation"
5
+ require "markdiff/operations/add_previous_sibling_operation"
6
+ require "markdiff/operations/remove_operation"
7
+
8
+ module Markdiff
9
+ class Differ
10
+ # Apply a given patch to a given node
11
+ # @param [Array<Markdiff::Operations::Base>] operations
12
+ # @param [Nokogiri::XML::Node] node
13
+ # @return [Nokogiri::XML::Node] Converted node
14
+ def apply_patch(operations, node)
15
+ operations.each do |operation|
16
+ case operation
17
+ when ::Markdiff::Operations::AddChildOperation
18
+ operation.target_node.add_child(operation.inserted_node)
19
+ when ::Markdiff::Operations::AddDataBeforeHrefOperation
20
+ operation.target_node["data-before-href"] = operation.target_node["href"]
21
+ operation.target_node["href"] = operation.after_href
22
+ when ::Markdiff::Operations::AddDataBeforeTagNameOperation
23
+ operation.target_node["data-before-tag-name"] = operation.target_node.name
24
+ operation.target_node.name = operation.after_tag_name
25
+ when ::Markdiff::Operations::AddPreviousSiblingOperation
26
+ operation.target_node.add_previous_sibling(operation.inserted_node)
27
+ when ::Markdiff::Operations::RemoveOperation
28
+ operation.target_node.replace(operation.inserted_node)
29
+ end
30
+ end
31
+ node
32
+ end
33
+
34
+ # Creates a patch from given two nodes
35
+ # @param [Nokogiri::XML::Node] before_node
36
+ # @param [Nokogiri::XML::Node] after_node
37
+ # @return [Array<Markdiff::Operations::Base>] operations
38
+ def create_patch(before_node, after_node)
39
+ if before_node.to_html == after_node.to_html
40
+ []
41
+ else
42
+ create_patch_from_children(before_node, after_node)
43
+ end
44
+ end
45
+
46
+ # Utility method to do both creating and applying a patch
47
+ # @param [String] before_string
48
+ # @param [String] after_string
49
+ # @return [Nokogiri::XML::Node] Converted node
50
+ def render(before_string, after_string)
51
+ before_node = ::Nokogiri::HTML.fragment(before_string)
52
+ after_node = ::Nokogiri::HTML.fragment(after_string)
53
+ patch = create_patch(before_node, after_node)
54
+ apply_patch(patch, before_node)
55
+ end
56
+
57
+ private
58
+
59
+ # 1. Create identity map and collect patches from descendants
60
+ # 1-1. Detect exact-matched nodes
61
+ # 1-2. Detect partial-matched nodes and recursively walk through its children
62
+ # 2. Create remove operations from identity map
63
+ # 3. Create insert operations from identity map
64
+ # 4. Return operations as a patch
65
+ #
66
+ # @param [Nokogiri::XML::Node] before_node
67
+ # @param [Nokogiri::XML::Node] after_node
68
+ # @return [Array<Markdiff::Operations::Base>] operations
69
+ def create_patch_from_children(before_node, after_node)
70
+ operations = []
71
+ identity_map = {}
72
+ inverted_identity_map = {}
73
+
74
+ # Exactly matching
75
+ before_node.children.each do |before_child|
76
+ after_node.children.each do |after_child|
77
+ if inverted_identity_map[after_child]
78
+ next
79
+ end
80
+ if before_child.to_html == after_child.to_html
81
+ identity_map[before_child] = after_child
82
+ inverted_identity_map[after_child] = before_child
83
+ end
84
+ end
85
+ end
86
+
87
+ # Partial matching
88
+ before_node.children.each do |before_child|
89
+ if identity_map[before_child]
90
+ next
91
+ end
92
+ after_node.children.each do |after_child|
93
+ next if inverted_identity_map[after_child]
94
+ next if before_child.text?
95
+ if before_child.name == after_child.name
96
+ if detect_href_difference(before_child, after_child)
97
+ operations << ::Markdiff::Operations::AddDataBeforeHrefOperation.new(after_href: after_child["href"], target_node: before_child)
98
+ end
99
+ identity_map[before_child] = after_child
100
+ inverted_identity_map[after_child] = before_child
101
+ operations += create_patch(before_child, after_child)
102
+ elsif detect_heading_level_difference(before_child, after_child)
103
+ operations << ::Markdiff::Operations::AddDataBeforeTagNameOperation.new(after_tag_name: after_child.name, target_node: before_child)
104
+ identity_map[before_child] = after_child
105
+ inverted_identity_map[after_child] = before_child
106
+ end
107
+ end
108
+ end
109
+
110
+ before_node.children.each do |before_child|
111
+ unless identity_map[before_child]
112
+ operations << ::Markdiff::Operations::RemoveOperation.new(target_node: before_child)
113
+ end
114
+ end
115
+
116
+ after_node.children.each do |after_child|
117
+ unless inverted_identity_map[after_child]
118
+ right_node = after_child.next_sibling
119
+ loop do
120
+ case
121
+ when inverted_identity_map[right_node]
122
+ operations << ::Markdiff::Operations::AddPreviousSiblingOperation.new(inserted_node: after_child, target_node: inverted_identity_map[right_node])
123
+ break
124
+ when right_node.nil?
125
+ operations << ::Markdiff::Operations::AddChildOperation.new(inserted_node: after_child, target_node: before_node)
126
+ break
127
+ else
128
+ right_node = right_node.next_sibling
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ operations
135
+ end
136
+
137
+ # @param [Nokogiri::XML::Node] before_node
138
+ # @param [Nokogiri::XML::Node] after_node
139
+ # @return [false, true] True if given 2 nodes are both hN nodes and have different N (e.g. h1 and h2)
140
+ def detect_heading_level_difference(before_node, after_node)
141
+ before_node.name != after_node.name &&
142
+ %w[h1 h2 h3 h4 h5 h6].include?(before_node.name) &&
143
+ %w[h1 h2 h3 h4 h5 h6].include?(after_node.name) &&
144
+ before_node.inner_html == after_node.inner_html
145
+ end
146
+
147
+ # @param [Nokogiri::XML::Node] before_node
148
+ # @param [Nokogiri::XML::Node] after_node
149
+ # @return [false, true] True if given 2 nodes are both "a" nodes and have different href attributes
150
+ def detect_href_difference(before_node, after_node)
151
+ before_node.name == "a" &&
152
+ after_node.name == "a" &&
153
+ before_node["href"] != after_node["href"] &&
154
+ before_node.inner_html == after_node.inner_html
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,12 @@
1
+ require "markdiff/operations/base"
2
+
3
+ module Markdiff
4
+ module Operations
5
+ class AddChildOperation < Base
6
+ # @return [String]
7
+ def inserted_node
8
+ "<ins>#{@inserted_node}</ins>"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ require "markdiff/operations/base"
2
+
3
+ module Markdiff
4
+ module Operations
5
+ class AddDataBeforeHrefOperation < Base
6
+ attr_reader :after_href
7
+
8
+ def initialize(after_href:, **args)
9
+ super(**args)
10
+ @after_href = after_href
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ require "markdiff/operations/base"
2
+
3
+ module Markdiff
4
+ module Operations
5
+ class AddDataBeforeTagNameOperation < Base
6
+ attr_reader :after_tag_name
7
+
8
+ def initialize(after_tag_name:, **args)
9
+ super(**args)
10
+ @after_tag_name = after_tag_name
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ require "markdiff/operations/base"
2
+
3
+ module Markdiff
4
+ module Operations
5
+ class AddPreviousSiblingOperation < Base
6
+ # @return [String]
7
+ def inserted_node
8
+ "<ins>#{@inserted_node}</ins>"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ module Markdiff
2
+ module Operations
3
+ class Base
4
+ # @return [Nokogiri::XML::Node]
5
+ attr_reader :target_node
6
+
7
+ # @param [Nokogiri::XML::Node, nil] inserted_node
8
+ # @param [Nokogiri::XML::Node] target_node
9
+ def initialize(inserted_node: nil, target_node:)
10
+ @inserted_node = inserted_node
11
+ @target_node = target_node
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ require "markdiff/operations/base"
2
+
3
+ module Markdiff
4
+ module Operations
5
+ class RemoveOperation < Base
6
+ # @return [String]
7
+ def inserted_node
8
+ "<del>#{@target_node}</del>"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,3 @@
1
+ module Markdiff
2
+ VERSION = "0.1.0"
3
+ end
data/lib/markdiff.rb ADDED
@@ -0,0 +1,2 @@
1
+ require "markdiff/differ"
2
+ require "markdiff/version"
data/markdiff.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "markdiff/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "markdiff"
7
+ spec.version = Markdiff::VERSION
8
+ spec.authors = ["Ryo Nakamura"]
9
+ spec.email = ["r7kamura@gmail.com"]
10
+ spec.summary = "Rendered Markdown differ."
11
+ spec.homepage = "https://github.com/r7kamura/markdiff"
12
+ spec.license = "MIT"
13
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.start_with?("spec/") }
14
+ spec.require_paths = ["lib"]
15
+
16
+ spec.add_development_dependency "bundler", "~> 1.10"
17
+ spec.add_development_dependency "pry", "0.10.3"
18
+ spec.add_development_dependency "rake", "~> 10.0"
19
+ spec.add_development_dependency "rspec", "3.4.0"
20
+ spec.add_runtime_dependency "nokogiri"
21
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: markdiff
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ryo Nakamura
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 0.10.3
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 0.10.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 3.4.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 3.4.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: nokogiri
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description:
84
+ email:
85
+ - r7kamura@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".travis.yml"
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - lib/markdiff.rb
97
+ - lib/markdiff/differ.rb
98
+ - lib/markdiff/operations/add_child_operation.rb
99
+ - lib/markdiff/operations/add_data_before_href_operation.rb
100
+ - lib/markdiff/operations/add_data_before_tag_name_operation.rb
101
+ - lib/markdiff/operations/add_previous_sibling_operation.rb
102
+ - lib/markdiff/operations/base.rb
103
+ - lib/markdiff/operations/remove_operation.rb
104
+ - lib/markdiff/version.rb
105
+ - markdiff.gemspec
106
+ homepage: https://github.com/r7kamura/markdiff
107
+ licenses:
108
+ - MIT
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 2.4.5
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Rendered Markdown differ.
130
+ test_files: []
131
+ has_rdoc: