class DocxTools::MailMerge

Constants

REGEXP

Attributes

document[RW]
part_list[RW]

Public Class Methods

new(file_object) click to toggle source
# File lib/docx_tools/mail_merge.rb, line 7
def initialize(file_object)
  self.document  = Document.new(file_object)
  self.part_list = PartList.new(document, %w[document.main header footer settings])
  process_merge_fields
end

Public Instance Methods

fields() click to toggle source
# File lib/docx_tools/mail_merge.rb, line 13
def fields
  fields = Set.new
  part_list.each_part do |part|
    part.xpath('.//w:MergeField').each do |mf|
      fields.add(mf.content)
    end
  end
  fields.to_a
end
merge(replacements = {}) click to toggle source
# File lib/docx_tools/mail_merge.rb, line 23
def merge(replacements = {})
  part_list.each_part do |part|
    replacements.each do |field, text|
      merge_field(part, field, text)
    end
  end
end
write(filename) click to toggle source
# File lib/docx_tools/mail_merge.rb, line 31
def write(filename)
  File.open(filename, 'w') do |file|
    file.write(generate.string)
  end
end

Private Instance Methods

clean_up() click to toggle source
# File lib/docx_tools/mail_merge.rb, line 39
def clean_up
  remaining = fields.map { |field| [field.to_sym, ''] }
  merge(remaining.to_h)
end
generate() click to toggle source
# File lib/docx_tools/mail_merge.rb, line 44
def generate
  clean_up
  buffer = Zip::OutputStream.write_buffer do |out|
    document.entries.each do |entry|
      unless entry.ftype == :directory
        out.put_next_entry(entry.name)
        if self.part_list.has?(entry.name)
          out.write self.part_list.get(entry.name).to_xml(indent: 0).gsub('\n', '')
        else
          out.write entry.get_input_stream.read
        end
      end
    end
  end
  buffer.seek(0)
  buffer
end
merge_field(part, field, text) click to toggle source
# File lib/docx_tools/mail_merge.rb, line 62
def merge_field(part, field, text)
  part.xpath(".//w:MergeField[text()=\"#{field}\"]").each do |merge_field|
    t_elem = Nokogiri::XML::Node.new('t', part)
    t_elem.content = text
    t_elem.parent = merge_field.parent
    merge_field.replace(t_elem)
  end
end
process_merge_fields() click to toggle source

replace the original convoluted tag with a simplified tag for easy searching and processing

# File lib/docx_tools/mail_merge.rb, line 72
def process_merge_fields
  self.part_list.each_part do |part|
    part.root.remove_attribute('Ignorable')
    
    # remove mail merge element from settings
    part.xpath('.//w:mailMerge').each do |mail_merge|
      mail_merge.remove
    end
    

    part.xpath('.//w:fldSimple/..').each do |parent|
      parent.children.each do |child|
        match_data = REGEXP.match(child.attribute('instr'))
        next if (child.node_name != 'fldSimple') || !match_data

        r_elem = Nokogiri::XML::Node.new('r', part)
        new_tag = Nokogiri::XML::Node.new('MergeField', part)
        new_tag.parent = r_elem
        new_tag.content = match_data[1]
        child.replace(r_elem)
      end
    end

    part.xpath('.//w:instrText/../..').each do |parent|
      begin_tags = parent.xpath('w:r/w:fldChar[@w:fldCharType="begin"]/..')
      end_tags = parent.xpath('w:r/w:fldChar[@w:fldCharType="end"]/..')
      instr_tags = parent.xpath('w:r/w:instrText')
      instr_tag_content = instr_tags.map(&:content)

      instr_tag_content.take(begin_tags.length).each_with_index do |instr, idx|
        next unless match_data = REGEXP.match(instr)

        new_tag = Nokogiri::XML::Node.new('MergeField', part)
        new_tag.content = match_data[1]

        children = parent.children
        start_idx = children.index(begin_tags[idx]) + 1
        end_idx = children.index(end_tags[idx])
        children[start_idx..end_idx].each do |child|
          instr_node = child.xpath('w:instrText')
          if instr_node.empty?
            child.remove
          else
            instr_node.first.replace(new_tag)
          end
        end
      end
    end
  end
end