class SkotOS::XMLObject

Attributes

ignore_types[RW]
ignore_whitespace[RW]
merry_only[RW]
noko_doc[R]
pretty[R]

Public Class Methods

diff_between(obj1, obj2, o1_name: "Object 1", o2_name: "Object 2") click to toggle source
# File lib/dgd-tools/skotos_xml_obj.rb, line 39
def self.diff_between(obj1, obj2, o1_name: "Object 1", o2_name: "Object 2")
    of1 = Tempfile.new("skotos_xml_diff1_")
    of2 = Tempfile.new("skotos_xml_diff2_")

    begin
        of1.write(obj1.pretty)
        of2.write(obj2.pretty)
        of1.close
        of2.close

        diff_opts = [ "c" ]
        diff_opts += [ "b", "B" ] if self.ignore_whitespace

        # Diff 'fails' if there's a difference between the two files.
        cmd = "diff -#{diff_opts.join("")} #{of1.path} #{of2.path}"
        #puts "Diff command: #{cmd}"
        diff = system_call(cmd, fail_ok: true)
        diff.sub!(of1.path, o1_name)
        diff.sub!(of2.path, o2_name)
    ensure
        of1.unlink
        of2.unlink
    end
    diff
end
diff_dirs(dir1, dir2) click to toggle source
# File lib/dgd-tools/skotos_xml_obj.rb, line 79
def self.diff_dirs(dir1, dir2)
    entries1 = skip_ignored_files(Dir.glob("*", base: dir1).to_a, dir1)
    entries2 = skip_ignored_files(Dir.glob("*", base: dir2).to_a, dir2)

    only_in_1 = entries1 - entries2
    only_in_2 = entries2 - entries1
    in_both = entries1 & entries2

    diff = []
    diff << "Only in first: #{only_in_1.map { |s| dir1 + "/" + s }.join(", ")}" unless only_in_1.empty?
    diff << "Only in second: #{only_in_2.map { |s| dir2 + "/" + s }.join(", ")}" unless only_in_2.empty?

    in_both.each do |file|
        in_1 = File.join dir1, file
        in_2 = File.join dir2, file
        if File.directory?(in_1) ^ File.directory?(in_2)
            diff << "Only a directory in one, not both: #{dir1}/#{file}"
        elsif File.directory?(in_1)
            d = diff_dirs(in_1, in_2)
            diff.concat(d)
        else
            o1 = from_file(in_1)
            o2 = from_file(in_2)
            this_diff = diff_between(o1, o2, o1_name: in_1, o2_name: in_2)
            diff << this_diff unless this_diff.strip == ""
        end
    end
    diff
end
from_file(filename) click to toggle source
# File lib/dgd-tools/skotos_xml_obj.rb, line 28
def self.from_file(filename)
    # SkotOS files often have references to undefined namespaces,
    # but we can get Nokogiri to parse it.
    doc = Nokogiri::XML(File.read filename)

    remove_undiffed(doc)

    pretty = doc.to_xml(indent:3)
    SkotOS::XMLObject.new pretty, noko_doc: doc
end
new(pretty, noko_doc: nil) click to toggle source
# File lib/dgd-tools/skotos_xml_obj.rb, line 23
def initialize(pretty, noko_doc: nil)
    @pretty = pretty
    @noko_doc = noko_doc
end
noko_non_text(nodes) click to toggle source
# File lib/dgd-tools/skotos_xml_obj.rb, line 171
def self.noko_non_text(nodes)
    nodes.select { |n| !n.is_a? Nokogiri::XML::Text }
end
noko_remove(node) click to toggle source
# File lib/dgd-tools/skotos_xml_obj.rb, line 154
def self.noko_remove(node)
    nn = node.next
    nn.remove if nn.is_a?(Nokogiri::XML::Text)
    node.remove
end
noko_remove_non_merry_nodes(root) click to toggle source
# File lib/dgd-tools/skotos_xml_obj.rb, line 184
def self.noko_remove_non_merry_nodes(root)
    root.children.each do |node|
        if node.name != "Core:PropertyContainer"
            node.remove
            next
        end

        node.children.each do |node2|
            if node2.name != "Core:PCProperties"
                node2.remove
                next
            end

            node2.children.each do |property_node|
                if property_node.name != "Core:Property" || property_node.attribute("property").value[0..5] != "merry:"
                    property_node.remove
                    next
                end
                # Leave the Merry node alone
            end

            if node2.children.size == 0
                node2.remove
            end
        end
        if node.children.size == 0
            node.remove
        end
    end
end
noko_single_node(node, name, attrs: {}) click to toggle source
# File lib/dgd-tools/skotos_xml_obj.rb, line 160
def self.noko_single_node(node, name, attrs: {})
    choices = noko_with_name_and_attrs(node, name, attrs)
    if choices.size < 1
        nil
    elsif choices.size > 1
        raise "Single-node search returned more than one node! #{name.inspect}, #{attrs.inspect}"
    else
        choices[0]
    end
end
noko_with_name_and_attrs(node, name, attrs = {}) click to toggle source
# File lib/dgd-tools/skotos_xml_obj.rb, line 175
def self.noko_with_name_and_attrs(node, name, attrs = {})
    results = node.children.flat_map { |n| noko_with_name_and_attrs(n, name, attrs) }
    if node.name == name &&
        attrs.all? { |k, v| node.attribute(k).value == v }
        results << node
    end
    results
end
remove_undiffed(doc) click to toggle source
# File lib/dgd-tools/skotos_xml_obj.rb, line 109
def self.remove_undiffed(doc)
    if doc.root && doc.root.element?
        ignored_top_elements = ["program", "clone", "owner"]
        ignored_top_elements.each do |attr|
            if doc.root.attribute(attr)
                doc.root.remove_attribute(attr)
            end
        end
    end

    rev = noko_single_node(doc.root, "Core:Property", attrs: { "property" => "revisions" })
    noko_remove(rev) if rev

    list = noko_single_node(doc.root, "Core:Property", attrs: { "property" => "#list#" })
    list.remove if list

    properties = noko_with_name_and_attrs(doc.root, "Core:Property")
    properties.each do |prop_node|
        prop_node.remove if prop_node.attribute("property").value.start_with?("sys:sync")
    end

    if self.merry_only
        # Kill off all the non-Merry nodes
        noko_remove_non_merry_nodes(doc.root)
    end

    if self.ignore_types
        self.ignore_types.each do |ignored_type|
            skipped = noko_with_name_and_attrs(doc.root, ignored_type)
            skipped.each { |n| noko_remove(n) }
        end
    end

    base_combat = noko_single_node(doc.root, "Base:Combat")
    if base_combat
        base_strength = noko_single_node(base_combat, "Base:Strength", attrs: { "value" => "1" })
        base_max_fatigue = noko_single_node(base_combat, "Base:MaxFatigue", attrs: { "value" => "1" })
        if base_strength && base_max_fatigue && noko_non_text(base_combat.children).size == 2
            next_text = base_combat.next
            base_combat.remove
            next_text.remove
        end
    end
end
skip_ignored_files(list, base_dir) click to toggle source
# File lib/dgd-tools/skotos_xml_obj.rb, line 65
def self.skip_ignored_files(list, base_dir)
    if self.merry_only
        list.select { |path| File.directory?(base_dir + "/" + path) ||
            path[/.xml$/] || path[/.XML$/] }
    else
        list.select do |path|
            !path[/,v$/] &&  # Ignore files ending in comma-v
                !path[/-backup-\d+-\d+-\d+\.xml/] && # Ignore files ending in -backup-[DATE].xml
                path != ".git" && # Ignore .git directories
                path != "MOVED" # Ignore MOVED - it's a sort of recycle, waiting to be emptied
        end
    end
end
system_call(cmd, fail_ok: false) click to toggle source
# File lib/dgd-tools/skotos_xml_obj.rb, line 215
def self.system_call(cmd, fail_ok: false)
    f = Tempfile.new("system_call_xml_diff_")
    begin
        system(cmd, out: f)
        unless fail_ok || $?.success?
            f.rewind
            out = f.read
            raise "Error running command: #{cmd.inspect}!\n\nOutput:\n#{out}\n\n"
        end
        f.rewind
        return f.read
    ensure
        f.close
        f.unlink
    end
end