docx_report 0.0.5 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []