Skip to content

Instantly share code, notes, and snippets.

@ericmason
Created October 28, 2013 16:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ericmason/7200448 to your computer and use it in GitHub Desktop.
Save ericmason/7200448 to your computer and use it in GitHub Desktop.
word_mail_merge.rb is a simple way to do mail merge in ruby
#!/usr/bin/env ruby
require 'zip/zip' # rubyzip gem
require 'nokogiri'
class WordMailMerge
def self.open(path, &block)
self.new(path, &block)
end
def initialize(path, &block)
@replace = {}
if block_given?
@zip = Zip::ZipFile.open(path)
yield(self)
@zip.close
else
@zip = Zip::ZipFile.open(path)
end
end
def force_settings
@replace["word/settings.xml"] = %{<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:settings xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main"><w:zoom w:percent="100"/></w:settings>}
end
def merge(rec)
xml = @zip.read("word/document.xml")
doc = Nokogiri::XML(xml) {|x| x.noent}
# Word's first way of doing things
(doc/"//w:fldSimple").each do |field|
if field.attributes['instr'].value =~ /MERGEFIELD (\S+)/
text_node = (field/".//w:t").first
puts "#{$1} existing text: #{text_node.inner_html}, Set to #{rec[$1]}"
p text_node
if text_node
puts "Replace node for #{$1}"
text_node.inner_html = rec[$1].to_s
else
puts "No text node for #{$1}"
end
end
end
# Word's second way of doing things (why???)
(doc/"//w:instrText").each do |instr|
if instr.inner_text =~ /MERGEFIELD (\S+)/
#rsid = instr.parent.attributes['rsidR']
text_node = instr.parent.next_sibling.next_sibling.xpath(".//w:t").first
text_node ||= instr.parent.next_sibling.next_sibling.next_sibling.xpath(".//w:t").first
if text_node
#if text_node = instr.parent.parent.xpath(".//w:t").first
#if text_node = (doc/"//w:r[@w:rsidR='#{rsid}']//w:t").first
text_node.inner_html = rec[$1].to_s
end
end
end
@replace["word/document.xml"] = doc.serialize :save_with => 0
end
def save(path)
Zip::ZipFile.open(path, Zip::ZipFile::CREATE) do |out|
@zip.each do |entry|
out.get_output_stream(entry.name) do |o|
if @replace[entry.name]
o.write(@replace[entry.name])
else
o.write(@zip.read(entry.name))
end
end
end
end
end
def close
@zip.close
end
end
if __FILE__ == $0
file = ARGV[0]
out_file = ARGV[1] || file.sub(/\.docx/, ' Merged.docx')
w = WordMailMerge.open(file)
w.force_settings
w.merge('First_Name' => 'Eric', 'Last_Name' => 'Mason')
w.save(out_file)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment