# -*- coding: utf-8 -*- module Sablon class Processor def self.process(xml_node, context, properties = {}) processor = new(parser) processor.manipulate xml_node, Sablon::Context.transform(context) processor.write_properties xml_node, properties if properties.any? xml_node end def self.parser @parser ||= Sablon::Parser::MailMerge.new end def initialize(parser) @parser = parser end def manipulate(xml_node, context) operations = build_operations(@parser.parse_fields(xml_node)) operations.each do |step| step.evaluate context end cleanup(xml_node) xml_node end def write_properties(xml_node, properties) if start_page_number = properties[:start_page_number] || properties["start_page_number"] section_properties = SectionProperties.from_document(xml_node) section_properties.start_page_number = start_page_number end end private def build_operations(fields) OperationConstruction.new(fields).operations end def cleanup(xml_node) fill_empty_table_cells xml_node end def fill_empty_table_cells(xml_node) xml_node.xpath("//w:tc[count(*[name() = 'w:p'])=0 or not(*)]").each do |blank_cell| filler = Nokogiri::XML::Node.new("w:p", xml_node.document) blank_cell.add_child filler end end class Block < Struct.new(:start_field, :end_field) def self.enclosed_by(start_field, end_field) @blocks ||= [RowBlock, ParagraphBlock, InlineParagraphBlock] block_class = @blocks.detect { |klass| klass.encloses?(start_field, end_field) } block_class.new start_field, end_field end def process(context) replaced_node = Nokogiri::XML::Node.new("tmp", start_node.document) replaced_node.children = Nokogiri::XML::NodeSet.new(start_node.document, body.map(&:dup)) Processor.process replaced_node, context replaced_node.children end def replace(content) content.each { |n| start_node.add_next_sibling n } remove_control_elements end def remove_control_elements body.each &:remove start_node.remove end_node.remove end def body return @body if defined?(@body) @body = [] node = start_node while (node = node.next_element) && node != end_node @body << node end @body end def start_node @start_node ||= self.class.parent(start_field).first end def end_node @end_node ||= self.class.parent(end_field).first end def self.encloses?(start_field, end_field) parent(start_field).any? && parent(end_field).any? end end class RowBlock < Block def self.parent(node) node.ancestors ".//w:tr" end def self.encloses?(start_field, end_field) super && parent(start_field) != parent(end_field) end end class ParagraphBlock < Block def self.parent(node) node.ancestors ".//w:p" end def self.encloses?(start_field, end_field) super && parent(start_field) != parent(end_field) end end class InlineParagraphBlock < Block def self.parent(node) node.ancestors ".//w:p" end def remove_control_elements body.each &:remove start_field.remove end_field.remove end def start_node @start_node ||= start_field.end_node end def end_node @end_node ||= end_field.start_node end def self.encloses?(start_field, end_field) super && parent(start_field) == parent(end_field) end end class OperationConstruction def initialize(fields) @fields = fields @operations = [] end def operations while @fields.any? @operations << consume(true) end @operations.compact end def consume(allow_insertion) @field = @fields.shift return unless @field case @field.expression when /^=/ if allow_insertion Statement::Insertion.new(Expression.parse(@field.expression[1..-1]), @field) end when /([^ ]+):each\(([^ ]+)\)/ block = consume_block("#{$1}:endEach") Statement::Loop.new(Expression.parse($1), $2, block) when /([^ ]+):if\(([^)]+)\)/ block = consume_block("#{$1}:endIf") Statement::Condition.new(Expression.parse($1), block, $2) when /([^ ]+):if/ block = consume_block("#{$1}:endIf") Statement::Condition.new(Expression.parse($1), block) end end def consume_block(end_expression) start_field = end_field = @field while end_field && end_field.expression != end_expression consume(false) end_field = @field end if end_field Block.enclosed_by start_field, end_field else raise TemplateError, "Could not find end field for «#{start_field.expression}». Was looking for «#{end_expression}»" end end end end end