require 'open-uri' module Sablon module Content class << self def wrap(value) case value when Sablon::Content value else if type = type_wrapping(value) type.new(value) else raise ArgumentError, "Could not find Sablon content type to wrap #{value.inspect}" end end end def make(type_id, *args) if types.key?(type_id) types[type_id].new(*args) else raise ArgumentError, "Could not find Sablon content type with id '#{type_id}'" end end def register(content_type) types[content_type.id] = content_type end def remove(content_type_or_id) types.delete_if {|k,v| k == content_type_or_id || v == content_type_or_id } end private def type_wrapping(value) types.values.reverse.detect { |type| type.wraps?(value) } end def types @types ||= {} end end # Handles simple text replacement of fields in the template class String < Struct.new(:string) include Sablon::Content def self.id; :string end def self.wraps?(value) value.respond_to?(:to_s) end def initialize(value) super value.to_s end def append_to(paragraph, display_node, env) string.scan(/[^\n]+|\n/).reverse.each do |part| if part == "\n" display_node.add_next_sibling Nokogiri::XML::Node.new "w:br", display_node.document else text_part = display_node.dup text_part.content = part display_node.add_next_sibling text_part end end end end # handles direct addition of WordML to the document template class WordML < Struct.new(:xml) include Sablon::Content def self.id; :word_ml end def self.wraps?(value) false end def initialize(value) super Nokogiri::XML.fragment(value) end def append_to(paragraph, display_node, env) # if all nodes are inline then add them to the existing paragraph # otherwise replace the paragraph with the new content. if all_inline? pr_tag = display_node.parent.at_xpath('./w:rPr') add_siblings_to(display_node.parent, pr_tag) display_node.parent.remove else add_siblings_to(paragraph) paragraph.remove end end # This allows proper equality checks with other WordML content objects. # Due to the fact the `xml` attribute is a live Nokogiri object # the default `==` comparison returns false unless it is the exact # same object being compared. This method instead checks if the XML # being added to the document is the same when the `other` object is # an instance of the WordML content class. def ==(other) if other.class == self.class xml.to_s == other.xml.to_s else super end end private # Returns `true` if all of the xml nodes to be inserted are def all_inline? (xml.children.map(&:node_name) - inline_tags).empty? end # Array of tags allowed to be a child of the w:p XML tag as defined # by the Open XML specification def inline_tags %w[w:bdo w:bookmarkEnd w:bookmarkStart w:commentRangeEnd w:commentRangeStart w:customXml w:customXmlDelRangeEnd w:customXmlDelRangeStart w:customXmlInsRangeEnd w:customXmlInsRangeStart w:customXmlMoveFromRangeEnd w:customXmlMoveFromRangeStart w:customXmlMoveToRangeEnd w:customXmlMoveToRangeStart w:del w:dir w:fldSimple w:hyperlink w:ins w:moveFrom w:moveFromRangeEnd w:moveFromRangeStart w:moveTo w:moveToRangeEnd w:moveToRangeStart m:oMath m:oMathPara w:pPr w:proofErr w:r w:sdt w:smartTag] end # Adds the XML to be inserted in the document as siblings to the # node passed in. Run properties are merged here because of namespace # issues when working with a document fragment def add_siblings_to(node, rpr_tag = nil) xml.children.reverse.each do |child| node.add_next_sibling child # merge properties next unless rpr_tag merge_rpr_tags(child, rpr_tag.children) end end # Merges the provided properties into the run proprties of the # node passed in. Properties are only added if they are not already # defined on the node itself. def merge_rpr_tags(node, props) # first assert that all child runs (w:r tags) have a w:rPr tag node.xpath('.//w:r').each do |child| child.prepend_child '' unless child.at_xpath('./w:rPr') end # # merge run props, only adding them if they aren't already defined node.xpath('.//w:rPr').each do |pr_tag| existing = pr_tag.children.map(&:node_name) props.map { |pr| pr_tag << pr unless existing.include? pr.node_name } end end end # Handles conversion of HTML -> WordML and addition into template class HTML < Struct.new(:html_content) include Sablon::Content def self.id; :html end def self.wraps?(value) false end def initialize(value) super value end def append_to(paragraph, display_node, env) converter = HTMLConverter.new word_ml = WordML.new(converter.process(html_content, env)) word_ml.append_to(paragraph, display_node, env) end end # Handles reading image data and inserting it into the document class Image < Struct.new(:name, :data, :local_rid) attr_reader :rid_by_file def self.id; :image end def self.wraps?(value) false end def inspect "#" end def initialize(source, attributes = {}) attributes = Hash[attributes.map { |k, v| [k.to_s, v] }] # If the source object is readable, use it as such otherwise open # and read the content if source.respond_to?(:read) name, img_data = process_readable(source, attributes) else name = File.basename(source) img_data = IO.binread(source) end # super name, img_data @attributes = attributes # rId's are separate for each XML file but I want to be able # to reuse the actual image file itself. @rid_by_file = {} end def append_to(paragraph, display_node, env) end private # Reads the data and attempts to find a filename from either the # attributes hash or a #filename method on the source object itself. # A filename is required inorder for MS Word to know the content type. def process_readable(source, attributes) if attributes['filename'] name = attributes['filename'] elsif source.respond_to?(:filename) name = source.filename else begin name = File.basename(source) rescue TypeError raise ArgumentError, "Error: Could not determine filename from source, try: `Sablon.content(readable_obj, filename: '...')`" end end # [File.basename(name), source.read] end end register Sablon::Content::String register Sablon::Content::WordML register Sablon::Content::HTML register Sablon::Content::Image end end