ruby - Rails Microsoft Word, XML databinding, repeat rows -
those willing jump straight questions can go paragraph "please help with". find there origin of implementation, along short xml samples
the story
the famous problem of inserting repeating content, table rows, word template, using rails framework.
i decided implement 'cleaner' solution replacing variables in word document rails, using xml databinding. solution works non-repetitive content, repetitive content, little dirty work must done , need help it.
no c#, no visual, plain olde ruby on rails & xml
the databinded document
i have word document content controls, tagged "human-readable" text, users know should inside.
i have used word 2007 content command toolkit add together custom xml .docx file. hence in each .docx have customxml/itemsx.xml contains custom xml.
i have manually databinded xml text content command have in word template, using drag & drop word 2007 content command toolkit.
the replacing process nokogiri
basically have code replaces every xml node corresponding value hash. illustration if provide hash function :
variables = { "some_xml-node" => "some_value" }
it replace xml in customxml/itemsx.xml of .docx file :
<root> <some> <xml-node>some_value</xml-node></some> </root>
so taken care of !
the repetitive content
now said, works non-repetitive content. repetitive content (in case want repeat <w:tr>
in document), solution i'd go with,
word/document.xml
of .docx file (this dirty, hell can't think of else) before every <tr>
needs duplicated in rails, parse xml , locate <tr>
needs duplicating using nokogiri copy tr many times need look @ text within <tr>
, find databinding (which looks <w:databinding w:xpath="/root[1]/movies[1]/movie[1]/name[1]"
replace movie[1]
movie[index]
repeat every table needs <tr>
duplication with solution hence ensure 100% compatibility existing scheme ! it's kind of preprocessing...
please help with
finding xml comment containing custom string, , selecting node below (using nokogiri) changing attributes in many sub-nodes of node found in 1.xml/hash samples used (my origin of implementation after that):
sample of .docx word/document.xml
<w:document> <!-- my_custom_tag_id --> <w:tr someparam="something"> <w:td></w:td> <w:td><w:sthelse></w:sthelse><w:databinding w:xpath="/root[1]/movies[1]/movie[1]/name[1]><w:sth>value</w:sth></w:td> <w:td></<:td> </w:tr> </w:document>
sample of input parameter repeat_tag hash
repeat_tags_sample = [ { "tag" => "my_custom_tag_id", "repeatable-content" => "movie" }, { "tag" => "my_custom_tag_id_2", "repeatable-content" => "cartoons" } ]
sample of input parameter contents hash
contents_sample = { "movies" => [{"name" => "x-men", "year" => 1998, "property-xxx" => 42 }, { "name" => "x-men-4", "year" => 2007, "property-xxx" => 42 }], "cartoons" => [{"name" => "tom_jerry", "year" => 1995, "property-yyy" => "cat" }, { "name" => "random_name", "year" => 2008, "property-yyy" => 42 }] }
my origin of implementation :
def dynamic_table_content(zip, repeat_tags, contents) doc = zip.find_entry("word/document.xml") xml = nokogiri::xml.parse(doc.get_input_dtream) # repeat_tags_sample = [ { # "tag" => my_custom_tag_id", # "repeatable-content" => "movie"}, # ...] repeat_tags.each |rpt| content = contents[rpt[:repeatable-content]] # content looks [ # {"name" => "x-men", # "year" => 1998, # "property-xxx" => 42, ...}, # ...] content_name = rpt[:repeateable_content].to_s # 'movie' of '/root[1]/movies[1]/movie[1]/name[1]' (see below) puts "processing #{rpt[:tag]}, adding #{content_name}s" # word document.xml sample code looks : # <!-- my_custom_tag_id_inserted_manually --> # <w:tr ...> # ... # <w:databinding w:xpath="/root[1]/movies[1]/movie[1]/name[1]> # ... # </w:tr>
find comment containing custom string, , select node below
# find starting <w:tr > tag located after <!-- rpt[:tag] --> base_tr_node = find node after # duplicate many times want. content.each_with_index |content, index| puts "adding #{content_name} : #{content}.to_s" new_tr_node = base_tr_node.add_next_sibling(base_tr_node) # within new node there many # <w:databinding w:xpath="/root[1]/movies[1]/movie[1]/name[1]> # <w:databinding w:xpath="/root[1]/movies[1]/movie[1]/year[1]> # ..../movie[1]/property-xxx[1] # goal : replace every movie[1] movie[index]
change attributes in many sub-nodes of node found in 1.
new_tr_node.change_attributes shown in (see goal in previous comments) # maybe, # new_tr_node.gsub("(#{content_name})\[([1-9]+)\]", "\1\[#{index}\]") # ... new_tr_node nokogiri element .gsub doesn't exist end end @replace["word/document.xml"] = xml.serialize :save_zip_with => 0
end
i have looked @ dope extension word documents. looks great ! alas had done lot of work, , (almost) finished building own preprocessor.
what needed more complicated asked. nevertheless, answers :
edit : fixed bad regex/xpath
# 1. find comment containing custom string, , select node below comment_nodes = doc.xpath("//comment()") # loop comment_nodes.each |comment| base_tr_node = comment.next_sibling.next_sibling # reason, need apply next_sibling twice, thought comment indeed above <w:tr> node # 2. alter attributes in many sub-nodes of node found in 1. matches = tr_node.search('.//*[name()='w:databinding']') matches.each |databinding_node| # replace '.*phase[1].*' '.*phase[index].*' databinding_node['w:xpath'].gsub("#{comment.text}\[1\]", "#{comment.text}\[#{index}\]") end
ruby-on-rails ruby xml data-binding ms-word
No comments:
Post a Comment