tdiff-0.4.0/0000755000004100000410000000000014574713761012656 5ustar www-datawww-datatdiff-0.4.0/.gitignore0000644000004100000410000000002714574713761014645 0ustar www-datawww-dataGemfile.lock doc/ pkg/ tdiff-0.4.0/.document0000644000004100000410000000003414574713761014472 0ustar www-datawww-data- ChangeLog.md LICENSE.txt tdiff-0.4.0/.github/0000755000004100000410000000000014574713761014216 5ustar www-datawww-datatdiff-0.4.0/.github/workflows/0000755000004100000410000000000014574713761016253 5ustar www-datawww-datatdiff-0.4.0/.github/workflows/ruby.yml0000644000004100000410000000112314574713761017754 0ustar www-datawww-dataname: CI on: [ push, pull_request ] jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: ruby: - '3.0' - '3.1' - '3.2' - '3.3' - jruby - truffleruby name: Ruby ${{ matrix.ruby }} steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} - name: Install dependencies run: bundle install --jobs 4 --retry 3 - name: Run tests run: bundle exec rake test tdiff-0.4.0/lib/0000755000004100000410000000000014574713761013424 5ustar www-datawww-datatdiff-0.4.0/lib/tdiff/0000755000004100000410000000000014574713761014520 5ustar www-datawww-datatdiff-0.4.0/lib/tdiff/unordered.rb0000644000004100000410000000534614574713761017044 0ustar www-datawww-data# frozen_string_literal: true require_relative 'tdiff' module TDiff # # Calculates the differences between two trees, without respecting the # order of children nodes. # module Unordered # # Includes {TDiff}. # def self.included(base) base.send :include, TDiff end # # Finds the differences between `self` and another tree, not respecting # the order of the nodes. # # @param [#tdiff_each_child] tree # The other tree. # # @yield [change, node] # The given block will be passed the added or removed nodes. # # @yieldparam [' ', '+', '-'] change # The state-change of the node. # # @yieldparam [Object] node # A node from one of the two trees. # # @return [Enumerator] # If no block is given, an Enumerator object will be returned. # # @since 0.2.0 # def tdiff_unordered(tree,&block) return enum_for(:tdiff_unordered,tree) unless block # check if the nodes differ unless tdiff_equal(tree) yield '-', self yield '+', tree return self end yield ' ', self tdiff_recursive_unordered(tree,&block) return self end protected # # Recursively compares the differences between the children nodes, # without respecting the order of the nodes. # # @param [#tdiff_each_child] tree # The other tree. # # @yield [change, node] # The given block will be passed the added or removed nodes. # # @yieldparam [' ', '+', '-'] change # The state-change of the node. # # @yieldparam [Object] node # A node from one of the two trees. # # @since 0.3.2 # def tdiff_recursive_unordered(tree,&block) x = enum_for(:tdiff_each_child,self) y = enum_for(:tdiff_each_child,tree) unchanged = {} changes = [] x.each_with_index do |xi,i| y.each_with_index do |yj,j| if (!unchanged.has_value?(yj) && xi.tdiff_equal(yj)) unchanged[xi] = yj changes << [i, ' ', xi] break end end unless unchanged.has_key?(xi) changes << [i, '-', xi] end end y.each_with_index do |yj,j| unless unchanged.has_value?(yj) changes << [j, '+', yj] end end # order the changes by index to match the behavior of `tdiff` changes.sort_by { |change| change[0] }.each do |index,change,node| yield change, node end # explicitly release the changes variable changes = nil # recurse down the unchanged nodes unchanged.each do |xi,yj| xi.tdiff_recursive_unordered(yj,&block) end unchanged = nil end end end tdiff-0.4.0/lib/tdiff/version.rb0000644000004100000410000000010414574713761016525 0ustar www-datawww-data# frozen_string_literal: true module TDiff VERSION = '0.4.0' end tdiff-0.4.0/lib/tdiff/tdiff.rb0000644000004100000410000000724614574713761016152 0ustar www-datawww-data# frozen_string_literal: true # # {TDiff} adds the ability to calculate the differences between two tree-like # objects. Simply include {TDiff} into the class which represents the tree # nodes and define the {#tdiff_each_child} and {#tdiff_equal} methods. # module TDiff # # Default method which will enumerate over every child of a parent node. # # @param [Object] node # The parent node. # # @yield [child] # The given block will be passed each child of the parent node. # def tdiff_each_child(node,&block) node.each(&block) if node.kind_of?(Enumerable) end # # Default method which compares nodes. # # @param [Object] node # A node from the new tree. # # @return [Boolean] # Specifies whether the nodes are equal. # def tdiff_equal(node) self == node end # # Finds the differences between `self` and another tree. # # @param [#tdiff_each_child] tree # The other tree. # # @yield [change, node] # The given block will be passed the added or removed nodes. # # @yieldparam [' ', '+', '-'] change # The state-change of the node. # # @yieldparam [Object] node # A node from one of the two trees. # # @return [Enumerator] # If no block is given, an Enumerator object will be returned. # def tdiff(tree,&block) return enum_for(:tdiff,tree) unless block # check if the nodes differ unless tdiff_equal(tree) yield '-', self yield '+', tree return self end yield ' ', self tdiff_recursive(tree,&block) return self end protected # # Recursively compares the differences between the children nodes. # # @param [#tdiff_each_child] tree # The other tree. # # @yield [change, node] # The given block will be passed the added or removed nodes. # # @yieldparam [' ', '+', '-'] change # The state-change of the node. # # @yieldparam [Object] node # A node from one of the two trees. # # @since 0.3.2 # def tdiff_recursive(tree,&block) c = Hash.new { |hash,key| hash[key] = Hash.new(0) } x = enum_for(:tdiff_each_child,self) y = enum_for(:tdiff_each_child,tree) x.each_with_index do |xi,i| y.each_with_index do |yj,j| c[i][j] = if xi.tdiff_equal(yj) c[i-1][j-1] + 1 else if c[i][j-1] > c[i-1][j] c[i][j-1] else c[i-1][j] end end end end unchanged = [] changes = [] x_backtrack = x.each_with_index.reverse_each y_backtrack = y.each_with_index.reverse_each next_child = lambda { |children| begin children.next rescue StopIteration # end of iteration, return a -1 index [nil, -1] end } xi, i = next_child[x_backtrack] yj, j = next_child[y_backtrack] until (i == -1 && j == -1) if (i != -1 && j != -1 && xi.tdiff_equal(yj)) changes.unshift [' ', xi] unchanged.unshift [xi, yj] xi, i = next_child[x_backtrack] yj, j = next_child[y_backtrack] else if (j >= 0 && (i == -1 || c[i][j-1] >= c[i-1][j])) changes.unshift ['+', yj] yj, j = next_child[y_backtrack] elsif (i >= 0 && (j == -1 || c[i][j-1] < c[i-1][j])) changes.unshift ['-', xi] xi, i = next_child[x_backtrack] end end end # explicitly discard the c matrix c = nil # sequentially iterate over the changed nodes changes.each(&block) changes = nil # recurse down through unchanged nodes unchanged.each { |a,b| a.tdiff_recursive(b,&block) } unchanged = nil end end tdiff-0.4.0/lib/tdiff.rb0000644000004100000410000000020214574713761015037 0ustar www-datawww-data# frozen_string_literal: true require_relative 'tdiff/tdiff' require_relative 'tdiff/unordered' require_relative 'tdiff/version' tdiff-0.4.0/LICENSE.txt0000644000004100000410000000204514574713761014502 0ustar www-datawww-dataCopyright (c) 2010-2024 Hal Brodigan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tdiff-0.4.0/spec/0000755000004100000410000000000014574713761013610 5ustar www-datawww-datatdiff-0.4.0/spec/unordered_spec.rb0000644000004100000410000000104214574713761017133 0ustar www-datawww-datarequire 'spec_helper' require 'tdiff_examples' require 'tdiff/unordered' describe TDiff::Unordered do include Helpers::Trees it "should include TDiff when included" do base = Class.new do include TDiff::Unordered end expect(base).to include(TDiff) end it_should_behave_like 'TDiff', :tdiff_unordered it "should not detect when the order of children has changed" do changes = @tree.tdiff_unordered(@changed_order).select do |change,node| change != ' ' end expect(changes).to be_empty end end tdiff-0.4.0/spec/tdiff_examples.rb0000644000004100000410000000216614574713761017134 0ustar www-datawww-datarequire 'spec_helper' require 'helpers/trees' shared_examples_for 'TDiff' do |method| include Helpers::Trees it "should tell if two trees are identical" do expect( @tree.send(method,@tree).all? { |change,node| change == ' ' } ).to be true end it "should stop if the root nodes have changed" do changes = @tree.send(method,@different_root).to_a expect(changes.length).to be 2 expect(changes[0][0]).to be == '-' expect(changes[0][1]).to be == @tree expect(changes[1][0]).to be == '+' expect(changes[1][1]).to be == @different_root end it "should tell when sub-nodes are added" do changes = @tree.send(method,@added).select { |change,node| change == '+' } expect(changes.length).to be 1 expect(changes[0][0]).to be == '+' expect(changes[0][1]).to be == @added.children[0].children[1] end it "should tell when sub-nodes are removed" do changes = @tree.send(method,@removed).select { |change,node| change == '-' } expect(changes.length).to be 1 expect(changes[0][0]).to be == '-' expect(changes[0][1]).to be == @tree.children[0].children[1] end end tdiff-0.4.0/spec/spec_helper.rb0000644000004100000410000000005114574713761016422 0ustar www-datawww-datarequire 'rspec' require 'helpers/trees' tdiff-0.4.0/spec/helpers/0000755000004100000410000000000014574713761015252 5ustar www-datawww-datatdiff-0.4.0/spec/helpers/trees.rb0000644000004100000410000000350214574713761016721 0ustar www-datawww-datarequire 'classes/node' module Helpers module Trees def self.included(base) base.module_eval do before(:all) do @tree = Node.new('root', [ Node.new('leaf1', [ Node.new('subleaf1', []), Node.new('subleaf2', []) ]), Node.new('leaf2', [ Node.new('subleaf1', []), Node.new('subleaf2', []) ]) ]) @different_root = Node.new('wrong', []) @added = Node.new('root', [ Node.new('leaf1', [ Node.new('subleaf1', []), Node.new('subleaf3', []), Node.new('subleaf2', []) ]), Node.new('leaf2', [ Node.new('subleaf1', []), Node.new('subleaf2', []) ]) ]) @removed = Node.new('root', [ Node.new('leaf1', [ Node.new('subleaf1', []) ]), Node.new('leaf2', [ Node.new('subleaf1', []), Node.new('subleaf2', []) ]) ]) @changed_order = Node.new('root', [ Node.new('leaf2', [ Node.new('subleaf1', []), Node.new('subleaf2', []) ]), Node.new('leaf1', [ Node.new('subleaf1', []), Node.new('subleaf2', []) ]) ]) end end end end end tdiff-0.4.0/spec/tdiff_spec.rb0000644000004100000410000000157614574713761016254 0ustar www-datawww-datarequire 'spec_helper' require 'tdiff_examples' require 'tdiff/tdiff' describe TDiff do include Helpers::Trees it_should_behave_like 'TDiff', :tdiff it "should detect when the order of children has changed" do changes = @tree.tdiff(@changed_order).to_a expect(changes.length).to be == 6 expect(changes[0][0]).to be == ' ' expect(changes[0][1]).to be == @tree expect(changes[1][0]).to be == '-' expect(changes[1][1]).to be == @tree.children[0] expect(changes[2][0]).to be == ' ' expect(changes[2][1]).to be == @tree.children[1] expect(changes[3][0]).to be == '+' expect(changes[3][1]).to be == @changed_order.children[1] expect(changes[4][0]).to be == ' ' expect(changes[4][1]).to be == @tree.children[1].children[0] expect(changes[5][0]).to be == ' ' expect(changes[5][1]).to be == @tree.children[1].children[1] end end tdiff-0.4.0/spec/classes/0000755000004100000410000000000014574713761015245 5ustar www-datawww-datatdiff-0.4.0/spec/classes/node.rb0000644000004100000410000000036014574713761016516 0ustar www-datawww-datarequire 'tdiff' class Node < Struct.new(:name, :children) include TDiff include TDiff::Unordered def tdiff_each_child(node,&block) node.children.each(&block) end def tdiff_equal(node) self.name == node.name end end tdiff-0.4.0/.yardopts0000644000004100000410000000007414574713761014525 0ustar www-datawww-data--markup markdown --title "TDiff Documentation" --protected tdiff-0.4.0/gemspec.yml0000644000004100000410000000131114574713761015020 0ustar www-datawww-dataname: tdiff summary: Calculates the differences between two tree-like structures. description: Calculates the differences between two tree-like structures. Similar to Rubys built-in TSort module. license: MIT authors: Postmodern email: postmodern.mod3@gmail.com homepage: https://github.com/postmodern/tdiff#readme has_yard: true metadata: documentation_uri: https://rubydoc.info/gems/tdiff source_code_uri: https://github.com/postmodern/tdiff bug_tracker_uri: https://github.com/postmodern/tdiff/issues changelog_uri: https://github.com/postmodern/tdiff/blob/main/ChangeLog.md rubygems_mfa_required: 'true' required_ruby_version: ">= 2.0.0" development_dependencies: bundler: ~> 2.0 tdiff-0.4.0/.rspec0000644000004100000410000000004014574713761013765 0ustar www-datawww-data--colour --format documentation tdiff-0.4.0/Rakefile0000644000004100000410000000031514574713761014322 0ustar www-datawww-datarequire 'rubygems' require 'rubygems/tasks' Gem::Tasks.new require 'rspec/core/rake_task' RSpec::Core::RakeTask.new task :test => :spec task :default => :spec require 'yard' YARD::Rake::YardocTask.new tdiff-0.4.0/ChangeLog.md0000644000004100000410000000241314574713761015027 0ustar www-datawww-data### 0.4.0 / 2024-01-24 * Switched to using `require_relative` to improve load-times. * Added `# frozen_string_literal: true` to all files. ### 0.3.4 / 2018-06-11 * Fixed shadowed variable warning (@bhollis). ### 0.3.3 / 2012-05-28 * Require ruby >= 1.8.7. * Added {TDiff::VERSION}. * Replaced ore-tasks with [rubygems-tasks](https://github.com/postmodern/rubygems-tasks#readme). ### 0.3.2 / 2010-11-28 * Added {TDiff#tdiff_recursive} to only handle recursively traversing and diffing the children nodes. * Added {TDiff::Unordered#tdiff_recursive_unordered} to only handle recursively traversing and diffing the children nodes, without respecting the order of the nodes. ### 0.3.1 / 2010-11-28 * Fixed a typo in {TDiff::Unordered#tdiff_unordered}, which was causing all nodes to be marked as added. ### 0.3.0 / 2010-11-15 * Changed {TDiff#tdiff_equal} to compare `self` with another node. ### 0.2.0 / 2010-11-14 * Added {TDiff::Unordered}. ### 0.1.0 / 2010-11-13 * Initial release: * Provides the {TDiff} mixin. * Allows custom node equality and traversal logic by overriding the {TDiff#tdiff_equal} and {TDiff#tdiff_each_child} methods. * Implements the [Longest Common Subsequence (LCS)](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem). tdiff-0.4.0/Gemfile0000644000004100000410000000033514574713761014152 0ustar www-datawww-datasource 'https://rubygems.org' gemspec group :development do gem 'rake' gem 'rubygems-tasks', '~> 0.1' gem 'rspec', '~> 3.0' gem 'kramdown' gem 'redcarpet', platform: :mri gem 'yard', '~> 0.9' end tdiff-0.4.0/README.md0000644000004100000410000000376314574713761014146 0ustar www-datawww-data# TDiff * [Source](https://github.com/postmodern/tdiff) * [Issues](https://github.com/postmodern/tdiff/issues) * [Documentation](https://rubydoc.info/gems/tdiff) ## Description Calculates the differences between two tree-like structures. Similar to Rubys built-in [TSort](http://rubydoc.info/docs/ruby-stdlib/1.9.2/TSort) module. ## Features * Provides the {TDiff} mixin. * Provides the {TDiff::Unordered} mixin for unordered diffing. * Allows custom node equality and traversal logic by overriding the {TDiff#tdiff_equal} and {TDiff#tdiff_each_child} methods. * Implements the [Longest Common Subsequence (LCS)](http://en.wikipedia.org/wiki/Longest_common_subsequence_problem) algorithm. ## Examples Diff two HTML documents: ```ruby require 'nokogiri' require 'tdiff' class Nokogiri::XML::Node include TDiff def tdiff_equal(node) if (self.text? && node.text?) self.text == node.text elsif (self.respond_to?(:root) && node.respond_to?(:root)) self.root.tdiff_equal(node.root) elsif (self.respond_to?(:name) && node.respond_to?(:name)) self.name == node.name else false end end def tdiff_each_child(node,&block) node.children.each(&block) end end doc1 = Nokogiri::HTML('

one

three

') doc2 = Nokogiri::HTML('

one

two

three

') doc1.at('div').tdiff(doc2.at('div')) do |change,node| puts "#{change} #{node.to_html}".ljust(30) + node.parent.path end ``` ### Output ``` +

one

/html/body/div + /html/body/div

one

/html/body/div /html/body/div

three

/html/body/div - one /html/body/div/p[1] + two /html/body/div/p[2] three /html/body/div/p[2] ``` ## Requirements * [ruby](http://www.ruby-lang.org/) >= 2.0.0 ## Install ```shell $ gem install tdiff ``` ## Copyright See {file:LICENSE.txt} for details. tdiff-0.4.0/.gemtest0000644000004100000410000000000014574713761014315 0ustar www-datawww-datatdiff-0.4.0/tdiff.gemspec0000644000004100000410000000353614574713761015326 0ustar www-datawww-data# encoding: utf-8 require 'yaml' Gem::Specification.new do |gem| gemspec = YAML.load_file('gemspec.yml') gem.name = gemspec.fetch('name') gem.version = gemspec.fetch('version') do require_relative 'lib/tdiff/version' TDiff::VERSION end gem.summary = gemspec['summary'] gem.description = gemspec['description'] gem.licenses = Array(gemspec['license']) gem.authors = Array(gemspec['authors']) gem.email = gemspec['email'] gem.homepage = gemspec['homepage'] glob = lambda { |patterns| gem.files & Dir[*patterns] } gem.files = `git ls-files`.split($/) gem.files = glob[gemspec['files']] if gemspec['files'] gem.executables = gemspec.fetch('executables') do glob['bin/*'].map { |path| File.basename(path) } end gem.default_executable = gem.executables.first if Gem::VERSION < '1.7.' gem.extensions = glob[gemspec['extensions'] || 'ext/**/extconf.rb'] gem.test_files = glob[gemspec['test_files'] || '{test/{**/}*_test.rb'] gem.extra_rdoc_files = glob[gemspec['extra_doc_files'] || '*.{txt,md}'] gem.require_paths = Array(gemspec.fetch('require_paths') { %w[ext lib].select { |dir| File.directory?(dir) } }) gem.requirements = gemspec['requirements'] gem.required_ruby_version = gemspec['required_ruby_version'] gem.required_rubygems_version = gemspec['required_rubygems_version'] gem.post_install_message = gemspec['post_install_message'] split = lambda { |string| string.split(/,\s*/) } if gemspec['dependencies'] gemspec['dependencies'].each do |name,versions| gem.add_dependency(name,split[versions]) end end if gemspec['development_dependencies'] gemspec['development_dependencies'].each do |name,versions| gem.add_development_dependency(name,split[versions]) end end end