diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a5713b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +log/* +tmp/**/* +*.swp +*.swo +nbproject +.project +.idea +.idea/** +*~ +**/*~ + diff --git a/README.textile b/README.textile new file mode 100644 index 0000000..b43bd1c --- /dev/null +++ b/README.textile @@ -0,0 +1,45 @@ +h1. DocX Builder + +_DocX Builder_ is a small utility to help you compose a docx (Microsoft Word 2007) based on a template's XML. + +The @slice_template.rb@ can be used separately for non-docx applications. + +The steps: +# Create a docx file with _Microsoft Word_ or _OpenOffice.org_ +# Unzip it (the docx file is actually a ZIP package) +# Create your template from @word/document.xml@ +# Open the template and reformat it (I used RubyMine) +# Find the parts you need to generate programmatically, and mark them up with ... , see @example/plan_report_template.xml@ +# Remove the formatting (compact the XML) +# Create your builder, see @example.rb@ + +h2. The example explained + +pre. + _____head______ _________area_______________________________________ _foot_ + | | | | + | | ________goal________________________| | + | | | | | + | | | _______objective____| | + | | | | | | + | (Plan Name) | (Area Name) | (Goal Name) | (Objective Name) | | + _____________________________________________________________________________ + XML DOCUMENT + +You can assign a text to a placeholder: + +pre. + template['head']['Plan Name'] = @plan.name + +Or you can replace a slice with a string, that can be composed by multiplying that slice: + +pre. + template['area'] = + @plan.areas.map do |area| + + area_slice = template['area'].clone + area_slice['Area Name'] = area.description + area_slice['goal'] = '...' + end + +See @example.rb@ \ No newline at end of file diff --git a/docx_builder.rb b/docx_builder.rb new file mode 100644 index 0000000..a53c68e --- /dev/null +++ b/docx_builder.rb @@ -0,0 +1,38 @@ +require 'slice_template' + +class DocxBuilder + def initialize(template_filename, template_dirname) + @template_filename = template_filename + @template_dirname = template_dirname + end + + def build + template = SliceTemplate.new(@template_filename); + yield template + build_docx(template.render) + end + + private + + def build_docx(content) + docx_content = nil + in_temp_dir do |temp_dir| + system("cp -r #{@template_dirname} #{temp_dir}/plan_report") + open("#{temp_dir}/plan_report/word/document.xml", "w") do |file| + file.write(content) + end + system("cd #{temp_dir}/plan_report; zip -r ../plan_report.docx *") + docx_content = File.read("#{temp_dir}/plan_report.docx") + end + docx_content + end + + def in_temp_dir + temp_dir = "/tmp/docx_#{Time.now.to_f.to_s}" + Dir.mkdir(temp_dir) + yield(temp_dir) + system("rm -Rf #{temp_dir}") + end +end + + diff --git a/example.docx b/example.docx new file mode 100644 index 0000000..ebf8ed8 Binary files /dev/null and b/example.docx differ diff --git a/example.rb b/example.rb new file mode 100644 index 0000000..a02a6fd --- /dev/null +++ b/example.rb @@ -0,0 +1,56 @@ +require 'docx_builder' + + +plan_struct = Struct.new(:name, :areas, :goals_by_area, :objectives_by_goal) +area_struct = Struct.new(:description, :id) +goal_struct = Struct.new(:description, :id) +objective_struct = Struct.new(:description) +@plan = plan_struct.new +@plan.name = 'Business Plan for 2011' +@plan.areas = [area_struct.new('Software Development', 1), area_struct.new('Cooking', 2)] +@plan.goals_by_area = { + 1 => [ goal_struct.new('Create a new app', 1), goal_struct.new('Create another app', 2)], + 2 => [ goal_struct.new('Make a new recipe', 3), goal_struct.new('Open a restaurant', 4)], +} +@plan.objectives_by_goal = { + 1 => [ objective_struct.new('It should be interesting'), objective_struct.new('It should be simple') ], + 2 => [ objective_struct.new('It should be revolutionary'), objective_struct.new('It should be unique') ], + 3 => [ objective_struct.new('Make a unique recipe'), objective_struct.new('Make a tasty recipe') ], + 4 => [ objective_struct.new('Serve high quality food'), objective_struct.new('Make it cheap') ], +} + + +file_path = "#{File.dirname(__FILE__)}/example/plan_report_template.xml" +dir_path = "#{File.dirname(__FILE__)}/example/plan_report_template" + +report = DocxBuilder.new(file_path, dir_path).build do |template| + + template['head']['Plan Name'] = @plan.name + template['area'] = + @plan.areas.map do |area| + + area_slice = template['area'].clone + area_slice['Area Name'] = area.description + area_slice['goal'] = + @plan.goals_by_area[area.id].map do |goal| + + goal_slice = template['goal'].clone + goal_slice['Goal Name'] = goal.description + goal_slice['objective'] = + @plan.objectives_by_goal[goal.id].map do |objective| + objective_slice = template['objective'].clone + objective_slice['Objective Name'] = objective.description + objective_slice + end + goal_slice + end + area_slice + end +end + + +open("example.docx", "w") { |file| file.write(report) } + +# ... or in a Rails controller: +# response.headers['Content-disposition'] = 'attachment; filename=plan_report.docx' +# render :text => report, :content_type => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' diff --git a/example/plan_report_template.xml b/example/plan_report_template.xml new file mode 100644 index 0000000..d82a7dd --- /dev/null +++ b/example/plan_report_template.xml @@ -0,0 +1,2 @@ + +Strategic Plan: (Plan Name)Area: (Area Name)Goal: (Goal Name)Objectives:(Objective Name) \ No newline at end of file diff --git a/example/plan_report_template/[Content_Types].xml b/example/plan_report_template/[Content_Types].xml new file mode 100644 index 0000000..e7ffd57 --- /dev/null +++ b/example/plan_report_template/[Content_Types].xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/example/plan_report_template/_rels/.rels b/example/plan_report_template/_rels/.rels new file mode 100644 index 0000000..f0b72e7 --- /dev/null +++ b/example/plan_report_template/_rels/.rels @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/example/plan_report_template/docProps/app.xml b/example/plan_report_template/docProps/app.xml new file mode 100644 index 0000000..0e51a38 --- /dev/null +++ b/example/plan_report_template/docProps/app.xml @@ -0,0 +1,2 @@ + +0 \ No newline at end of file diff --git a/example/plan_report_template/docProps/core.xml b/example/plan_report_template/docProps/core.xml new file mode 100644 index 0000000..8a7da19 --- /dev/null +++ b/example/plan_report_template/docProps/core.xml @@ -0,0 +1,2 @@ + +2010-05-28T19:47:48.00ZLevente Bagi0 \ No newline at end of file diff --git a/example/plan_report_template/word/_rels/document.xml.rels b/example/plan_report_template/word/_rels/document.xml.rels new file mode 100644 index 0000000..2850fab --- /dev/null +++ b/example/plan_report_template/word/_rels/document.xml.rels @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/example/plan_report_template/word/document.xml b/example/plan_report_template/word/document.xml new file mode 100644 index 0000000..a09fac0 --- /dev/null +++ b/example/plan_report_template/word/document.xml @@ -0,0 +1,2 @@ + +Strategic Plan: (Strategic Plan Name)Area: (Area 1)Goal: (Goal 1)Objectives:(Objective 1)(Objective 2)(Objective 3)Goal: (Goal 2)Objectives:(Objective 1)(Objective 2)(Objective 3)Goal: (Goal 3)Objectives:(Objective 1)(Objective 2)(Objective 3)Area: (Area 2)Goal: (Goal 1)Objectives:(Objective 1)(Objective 2)(Objective 3)Goal: (Goal 2)Objectives:(Objective 1)(Objective 2)(Objective 3)Goal: (Goal 3)Objectives:(Objective 1)(Objective 2)(Objective 3)Area: (Area 3)Goal: (Goal 1)Objectives:(Objective 1)(Objective 2)(Objective 3)Goal: (Goal 2)Objectives:(Objective 1)(Objective 2)(Objective 3)Goal: (Goal 3)Objectives:(Objective 1)(Objective 2)(Objective 3) \ No newline at end of file diff --git a/example/plan_report_template/word/fontTable.xml b/example/plan_report_template/word/fontTable.xml new file mode 100644 index 0000000..773d81f --- /dev/null +++ b/example/plan_report_template/word/fontTable.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/example/plan_report_template/word/numbering.xml b/example/plan_report_template/word/numbering.xml new file mode 100644 index 0000000..55c8260 --- /dev/null +++ b/example/plan_report_template/word/numbering.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/example/plan_report_template/word/styles.xml b/example/plan_report_template/word/styles.xml new file mode 100644 index 0000000..c6f967f --- /dev/null +++ b/example/plan_report_template/word/styles.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/slice_template.rb b/slice_template.rb new file mode 100644 index 0000000..2d83b6c --- /dev/null +++ b/slice_template.rb @@ -0,0 +1,105 @@ +class SliceTemplate + + attr_reader :content + attr_accessor :slices + + + def initialize(filename) + @content = File.read(filename) + @slices = {} + parse + end + + def initialize_copy(other) + @content = other.content.clone + @slices = other.slices.clone + end + + def [](slice_name) + @slices[slice_name] = Slice.new(@slices[slice_name]) unless @slices[slice_name].instance_of?(Slice) + @slices[slice_name] + end + + def []=(slice_name, value) + if value.instance_of?(Array) + @slices[slice_name] = value.map do |item| + item.instance_of?(Slice) ? item.render : item + end + else + @slices[slice_name] = value + end + end + + def render + render_string(@content) + end + + alias_method :to_s, :render + + + private + + + def parse + parse_string(@content) + end + + def parse_string(s) + s.gsub!(/<\!-- BEGIN ([^>]+) -->(.+)<\!-- END \1 -->/m).each do |match| + slice_name, content = [$1.downcase, $2] + parse_string(content) + @slices[slice_name] = content + "(#{slice_name})" + end + end + + def render_string(s) + return if s.nil? + s.gsub(/\(([\w\d _]+)\)/) do |match| + slot_name = $1 + slice = @slices[slot_name] + slice.nil? ? match : render_string(slice.to_s) + end + end + + + class Slice + attr_reader :slots + + def initialize(content) + @content = content + parse + end + + def parse + @slots = Hash[@content.scan(/\(([\w\d _]+)\)/).map{|slot_name| [slot_name, "(#{slot_name})"] }] + end + + def [](slot_name) + @slots[slot_name] + end + + def []=(slot_name, value) + @slots[slot_name] = value + end + + def set(slot_name, value) + @slots[slot_name] = value + self + end + + def render + @content.gsub(/\(([\w\d _]+)\)/) do |match| + slot_name = $1 + @slots[slot_name] + end + end + + alias_method :to_s, :render + + def initialize_copy(other) + @slots = other.slots.clone + end + end + +end \ No newline at end of file