docx_report 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2462f82a4a13faeffaad45e13b5356f0e5c0e693
4
- data.tar.gz: 04753ff74b48efabe96f91d1d2db3712b009c97a
3
+ metadata.gz: df8bc8c2d36d553f99684f1d96f2496c2c3200f1
4
+ data.tar.gz: 8ff0b77d064a32fae807ed71527057c4d93da371
5
5
  SHA512:
6
- metadata.gz: 17a9db4ed66576898e3fe3f5e6cc33dd4a1316df76b087e6af70eb10810153758acb9bae39855e1bd22782640759aef3efdf5914e1acf4b67501bdeb3c4535f2
7
- data.tar.gz: fa77fad1bc35d081f7552c1ff06583386333bd6c7c6cd357f77aee36f5d31c6701c6c44a4ba16045b416841546c6effc5100aec2d8413c15a825cc8e15f52382
6
+ metadata.gz: dae046f2041647d0fed8de9ff86ba7af46845f80315e3d5999bb2ae3c99f2fa47b6127b6decc3b8134a4b281cf28d4765e1c376b43943902ca8fdf4ffeea764a
7
+ data.tar.gz: b2f0a465bd9e348401bd7d450df1266b4ee47935b9daac808f3f508ec7f4569e0d7160d05f348ea0f5f2393a6317d88537e6587cc1319fc7bd223026275230d3
data/lib/docx_report.rb CHANGED
@@ -3,6 +3,9 @@ require 'docx_report/document'
3
3
  require 'docx_report/parser'
4
4
  require 'docx_report/table'
5
5
  require 'docx_report/record'
6
+ require 'docx_report/field'
7
+ require 'docx_report/content_file'
8
+ require 'docx_report/hyperlink'
6
9
 
7
10
  module DocxReport
8
11
  def self.create_docx_report(template_path)
@@ -0,0 +1,34 @@
1
+ module DocxReport
2
+ class ContentFile
3
+ attr_reader :name, :xml, :rels_name, :rels_xml, :new_rels
4
+
5
+ def initialize(name, zip)
6
+ @name = name
7
+ @xml = Nokogiri::XML(zip.read(name))
8
+ @rels_name = "#{name.sub '/', '/_rels/'}.rels"
9
+ @new_rels = false
10
+ @rels_xml = Nokogiri::XML(if zip.entries.any? { |r| r.name == @rels_name }
11
+ zip.read(@rels_name)
12
+ else
13
+ new_rels_xml
14
+ end)
15
+ end
16
+
17
+ def new_uniqe_id
18
+ (@rels_xml.xpath('//*[@Id]').map do |e|
19
+ e[:Id][3..-1].to_i if e.name == 'Relationship'
20
+ end.compact.max || 0) + 1
21
+ end
22
+
23
+ def new_rels_xml
24
+ @new_rels = true
25
+ '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships'\
26
+ 'xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'\
27
+ '</Relationships>'
28
+ end
29
+
30
+ def rels_has_items?
31
+ @rels_xml.xpath('//*[@Id]').any?
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ module DocxReport
2
+ module DataItem
3
+ attr_reader :fields, :images
4
+
5
+ def add_field(name, value, type = :text)
6
+ field = Field.new name, value, type
7
+ raise 'duplicate field name' if @fields.any? { |f| f.name == field.name }
8
+ @fields << field
9
+ end
10
+ end
11
+ end
@@ -3,52 +3,87 @@ require 'nokogiri'
3
3
 
4
4
  module DocxReport
5
5
  class Document
6
- attr_reader :template_path, :files
6
+ attr_accessor :template_path, :files
7
7
 
8
8
  def initialize(template_path)
9
9
  @template_path = template_path
10
10
  zip = Zip::File.open(template_path)
11
- @files = load_files zip
11
+ load_files zip
12
12
  zip.close
13
13
  end
14
14
 
15
- def save(output_path)
16
- zip = Zip::File.open @template_path
15
+ def save_to_memory
16
+ Zip::OutputStream.write_buffer do |output|
17
+ add_files output
18
+ end.string
19
+ end
20
+
21
+ def save_to_file(output_path)
17
22
  Zip::OutputStream.open(output_path) do |output|
18
- zip.each do |entry|
19
- output.put_next_entry entry.name
20
- if @files.keys.include? entry.name
21
- output.write @files[entry.name].to_xml
22
- else
23
- output.write zip.read(entry.name)
23
+ add_files output
24
+ end
25
+ end
26
+
27
+ def new_uniqe_id(type)
28
+ (@files.map do |file|
29
+ file.xml.xpath('//*[@id]').map { |e| e[:id].to_i if e.name == type }
30
+ end.flatten.compact.max || 0) + 1
31
+ end
32
+
33
+ private
34
+
35
+ def add_files(output)
36
+ Zip::File.open @template_path do |template|
37
+ template.each do |entry|
38
+ write_files entry.name, template, output
39
+ end
40
+ @files.each do |file|
41
+ if file.new_rels && file.rels_has_items?
42
+ output.put_next_entry file.rels_name
43
+ output.write file.rels_xml.to_xml
24
44
  end
25
45
  end
26
46
  end
27
- zip.close()
28
47
  end
29
48
 
30
- private
49
+ def write_files(name, template, output)
50
+ if @files.any? { |f| f.name == name }
51
+ add_data name, @files.detect { |f| f.name == name }.xml.to_xml, output
52
+ elsif @files.any? { |f| f.rels_name == name }
53
+ file = @files.detect { |f| f.rels_name == name }
54
+ add_data name, file.rels_xml.to_xml, output if file.rels_has_items?
55
+ else
56
+ add_data name, template.read(name), output
57
+ end
58
+ end
59
+
60
+ def add_data(name, data, output)
61
+ output.put_next_entry name
62
+ output.write data
63
+ end
31
64
 
32
65
  def content_types_xpath
33
66
  "//*[@ContentType = '#{CONTENT_TYPES.join("' or @ContentType='")}']"
34
67
  end
35
68
 
36
69
  def load_files(zip)
37
- @files = {}
38
- content_type_element = Nokogiri::XML zip.read(CONTENT_TYPE_NAME)
39
- content_type_element.xpath(content_types_xpath).each do |e|
70
+ @files = []
71
+ content_type_node = Nokogiri::XML zip.read(CONTENT_TYPE_NAME)
72
+ content_type_node.xpath(content_types_xpath).each do |e|
40
73
  filename = e['PartName'][1..-1]
41
- @files[filename] = Nokogiri::XML zip.read(filename)
74
+ @files << ContentFile.new(filename, zip)
42
75
  end
43
- @files
44
76
  end
45
77
 
46
78
  CONTENT_TYPE_NAME = '[Content_Types].xml'.freeze
47
79
 
48
80
  CONTENT_TYPES = [
49
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml',
50
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml',
51
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml'
81
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml'\
82
+ '.document.main+xml',
83
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml'\
84
+ '.header+xml',
85
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml'\
86
+ '.footer+xml'
52
87
  ].freeze
53
88
  end
54
89
  end
@@ -0,0 +1,23 @@
1
+ module DocxReport
2
+ class Field
3
+ attr_reader :name, :value, :type
4
+
5
+ def initialize(name, value = nil, type = :text, &block)
6
+ @name = "@#{name}@"
7
+ @type = type
8
+ set_value(value || block)
9
+ end
10
+
11
+ def set_value(value = nil, &block)
12
+ @value = value || block
13
+ end
14
+
15
+ def load_value(item)
16
+ Field.new(name[1..-2], if @value.is_a? Proc
17
+ @value.call(item)
18
+ else
19
+ item.is_a?(Hash) ? item[@value] : item.send(@value)
20
+ end, type)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ module DocxReport
2
+ class Hyperlink
3
+ attr_accessor :target, :file, :id
4
+
5
+ def initialize(target, file, id = nil)
6
+ @target = target
7
+ @file = file
8
+ if id.nil?
9
+ generate_id
10
+ else
11
+ @id = id
12
+ file.rels_xml.xpath("//*[@Id='#{id}']").first[:Target] = target
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def generate_id
19
+ @id = "rId#{file.new_uniqe_id}"
20
+ file.rels_xml.children.first << hyperlink_rels
21
+ end
22
+
23
+ def hyperlink_rels
24
+ Nokogiri::XML(
25
+ format('<Relationship Id="%s" Type="http://schemas.openxmlformats'\
26
+ '.org/officeDocument/2006/relationships/hyperlink" Target="%s" '\
27
+ 'TargetMode="External"/>', @id, @target)).children.first
28
+ end
29
+ end
30
+ end
@@ -7,56 +7,78 @@ module DocxReport
7
7
  end
8
8
 
9
9
  def replace_all_fields(fields)
10
- @document.files.values.each do |xml_element|
11
- replace_element_fields(fields, xml_element)
10
+ @document.files.each do |file|
11
+ replace_node_fields(fields, file.xml)
12
+ replace_node_hyperlinks(fields, file.xml, file)
12
13
  end
13
14
  end
14
15
 
15
16
  def fill_all_tables(tables)
16
- @document.files.values.each do |xml_element|
17
- fill_element_tables(tables, xml_element)
17
+ @document.files.each do |file|
18
+ fill_tables(tables, file.xml, file)
18
19
  end
19
20
  end
20
21
 
21
22
  private
22
23
 
23
- def search_for_text(name, parent_element)
24
- parent_element.xpath(".//*[contains(text(), '#{name}')]")
24
+ def find_text_nodes(name, parent_node)
25
+ parent_node.xpath(".//*[contains(text(), '#{name}')]")
25
26
  end
26
27
 
27
- def replace_element_fields(fields, parent_element)
28
- fields.each do |key, value|
29
- search_for_text(key, parent_element).map do |element|
30
- element.content = element.content.gsub key, value
28
+ def find_hyperlink_nodes(name, parent_node, file)
29
+ links = file.rels_xml.xpath "//*[@Target='#{name}']"
30
+ parent_node.xpath(".//w:hyperlink[@r:id='#{find_by_Id(links)}']")
31
+ end
32
+
33
+ def find_by_Id(links)
34
+ links.map { |link| link[:Id] }.join("' or @r:id='")
35
+ end
36
+
37
+ def replace_node_fields(fields, parent_node)
38
+ fields.select { |f| f.type == :text }.each do |field|
39
+ find_text_nodes(field.name, parent_node).map do |node|
40
+ node.content = node.content.gsub field.name, field.value
41
+ end
42
+ end
43
+ end
44
+
45
+ def replace_node_hyperlinks(fields, parent_node, file, create = false)
46
+ fields.select { |f| f.type == :hyperlink }.each do |field|
47
+ find_hyperlink_nodes(field.name, parent_node, file).each do |node|
48
+ hyperlink = Hyperlink.new(field.value, file,
49
+ (node['r:id'] unless create))
50
+ node['r:id'] = hyperlink.id
31
51
  end
32
52
  end
33
53
  end
34
54
 
35
- def find_table(name, parent_element)
36
- parent_element.xpath(".//w:tbl[//w:tblCaption[@w:val='#{name}']][1]").first
55
+ def find_table(name, parent_node)
56
+ parent_node.xpath(".//w:tbl[//w:tblCaption[@w:val='#{name}']][1]")
57
+ .first
37
58
  end
38
59
 
39
- def find_row(table_element, row_number)
40
- table_element.xpath(".//w:tr[#{row_number}]").first
60
+ def find_row(table, table_node)
61
+ row_number = table.has_header ? 2 : 1
62
+ table_node.xpath(".//w:tr[#{row_number}]").first
41
63
  end
42
64
 
43
- def fill_element_tables(tables, parent_element)
65
+ def fill_tables(tables, parent_node, file)
44
66
  tables.each do |table|
45
- tbl = find_table table.name, parent_element
46
- if tbl
47
- row_number = table.has_header ? 2 : 1
48
- tbl_row = find_row tbl, row_number
49
-
50
- if tbl_row
51
- table.records.each do |record|
52
- new_row = tbl_row.dup
53
- tbl_row.add_previous_sibling new_row
54
- replace_element_fields record.fields, new_row
55
- end
56
- tbl_row.remove
57
- end
58
- end
67
+ tbl = find_table table.name, parent_node
68
+ next if tbl.nil?
69
+ tbl_row = find_row table, tbl
70
+ fill_table_rows(table, tbl_row, file) unless tbl_row.nil?
71
+ end
72
+ end
73
+
74
+ def fill_table_rows(table, row_node, file)
75
+ table.records.each do |record|
76
+ new_row = row_node.dup
77
+ row_node.add_previous_sibling new_row
78
+ replace_node_fields record.fields, new_row
79
+ replace_node_hyperlinks record.fields, new_row, file, true
59
80
  end
81
+ row_node.remove
60
82
  end
61
83
  end
62
84
  end
@@ -1,13 +1,11 @@
1
+ require 'docx_report/data_item'
2
+
1
3
  module DocxReport
2
4
  class Record
3
- attr_accessor :fields
5
+ include DataItem
4
6
 
5
7
  def initialize
6
- @fields = {}
7
- end
8
-
9
- def add_field(name, value)
10
- fields["{@#{name}}"] = value
8
+ @fields = []
11
9
  end
12
10
  end
13
11
  end
@@ -1,20 +1,18 @@
1
- require 'tempfile'
1
+ require 'docx_report/data_item'
2
2
 
3
3
  module DocxReport
4
4
  class Report
5
+ include DataItem
5
6
  attr_reader :fields, :tables
6
7
 
7
8
  def initialize(template_path)
8
9
  @template_path = template_path
9
- @fields = {}
10
+ @fields = []
10
11
  @tables = []
11
12
  end
12
13
 
13
- def add_field(name, value)
14
- @fields["{@#{name}}"] = value
15
- end
16
-
17
14
  def add_table(name, collection = nil, has_header = false)
15
+ raise 'duplicate table name' if @tables.any? { |t| t.name == name }
18
16
  table = Table.new name, has_header
19
17
  @tables << table
20
18
  yield table
@@ -22,18 +20,21 @@ module DocxReport
22
20
  end
23
21
 
24
22
  def generate_docx(filename = nil, template_path = nil)
25
- doc = Document.new template_path || @template_path
26
- parser = Parser.new doc
27
- parser.fill_all_tables @tables
28
- parser.replace_all_fields @fields
29
- temp = Tempfile.new('') if filename.nil?
30
- docx_path = filename || temp.path
31
- begin
32
- doc.save docx_path
33
- File.read docx_path if filename.nil?
34
- ensure
35
- temp.close! if temp
23
+ document = Document.new template_path || @template_path
24
+ apply_changes document
25
+ if filename.nil?
26
+ document.save_to_memory
27
+ else
28
+ document.save_to_file filename
36
29
  end
37
30
  end
31
+
32
+ private
33
+
34
+ def apply_changes(document)
35
+ parser = Parser.new document
36
+ parser.replace_all_fields @fields
37
+ parser.fill_all_tables @tables
38
+ end
38
39
  end
39
40
  end
@@ -1,6 +1,6 @@
1
1
  module DocxReport
2
2
  class Table
3
- attr_accessor :name, :has_header, :records
3
+ attr_reader :name, :has_header, :records
4
4
 
5
5
  def initialize(name, has_header = false)
6
6
  @name = name
@@ -15,28 +15,21 @@ module DocxReport
15
15
  new_record
16
16
  end
17
17
 
18
- def add_field(name, mapped_field = nil, &block)
19
- @fields << { name: name, mapped_field: mapped_field, block: block }
18
+ def add_field(name, value = nil, type = :text, &block)
19
+ field = Field.new(name, value || block, type)
20
+ raise 'duplicate field name' if @fields.any? do |f|
21
+ f.name == field.name
22
+ end
23
+ @fields << field
20
24
  end
21
25
 
22
26
  def load_records(collection)
23
27
  collection.each do |item|
24
28
  record = new_record
25
29
  @fields.each do |field|
26
- if field[:block].nil?
27
- record.add_field field[:name],
28
- mapped_value(item, field[:mapped_field])
29
- else
30
- record.add_field field[:name], field[:block].call(item)
31
- end
30
+ record.fields << field.load_value(item)
32
31
  end
33
32
  end
34
33
  end
35
-
36
- private
37
-
38
- def mapped_value(item, field_name)
39
- item.is_a?(Hash) ? item[field_name] : item.send(field_name)
40
- end
41
34
  end
42
35
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docx_report
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ahmed Abudaqqa
@@ -40,14 +40,19 @@ dependencies:
40
40
  version: '1.2'
41
41
  description: |-
42
42
  docx_report is a light weight gem that generates docx files
43
- by replacing strings on previously created .docx file
43
+ by replacing strings and inserting images on previously
44
+ created .docx template file
44
45
  email: ahmed@abudaqqa.com
45
46
  executables: []
46
47
  extensions: []
47
48
  extra_rdoc_files: []
48
49
  files:
49
50
  - lib/docx_report.rb
51
+ - lib/docx_report/content_file.rb
52
+ - lib/docx_report/data_item.rb
50
53
  - lib/docx_report/document.rb
54
+ - lib/docx_report/field.rb
55
+ - lib/docx_report/hyperlink.rb
51
56
  - lib/docx_report/parser.rb
52
57
  - lib/docx_report/record.rb
53
58
  - lib/docx_report/report.rb
@@ -75,5 +80,6 @@ rubyforge_project:
75
80
  rubygems_version: 2.4.5.1
76
81
  signing_key:
77
82
  specification_version: 4
78
- summary: docx_report generate docx files based on previously created .docx file
83
+ summary: docx_report generate docx files based on previously created .docx template
84
+ file
79
85
  test_files: []