docxify 0.0.5 → 0.0.7

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
  SHA256:
3
- metadata.gz: 19220bad643ba9281144f2af8122d7c3300befdef7ed3dff796b81ccb493850c
4
- data.tar.gz: fd960d429aedf127bfdca4eaf1d7a2aeda5a7c18577081a9f280169ccc74c23e
3
+ metadata.gz: cb6c9076520d0776ed132e2abbc42166a6ff0106fce181d5d178ecf3abff1a96
4
+ data.tar.gz: 042d1e27e5572cd0b4868eb9af065d1b3831e25d80569eafd2086bda130ab7b1
5
5
  SHA512:
6
- metadata.gz: 7ef5802312027442107a8541ab7e34be20b8309403c12506b2d48ae77eaeaeb3c2a19877a20ac54dbcc88af3a92869dbc4e718a7647b1abecfce677a05ace286
7
- data.tar.gz: b79ab3fc2a484c4882a0330c8bc25471ecb5304e8efa9260de945cdce57cd2ec2dae71133adaf7a10cf385f72d02c8f4f9255e0025ce47a442ce752f674939ec
6
+ metadata.gz: 83ee99a55280b56efd7f571c00c8cb09eb026345b821173c389f40f3bffb907bc2f4b57ce08d6d587a0bc653cdbc4efc630a1400dd48e3dee78f005bf574469e
7
+ data.tar.gz: 62a9965149dde87c65db3877642fff5e6cf0cc8f02f373ab59422b78ca9a645522c2df45125268140f8b1e36b29a105b500ea06489f0fc5fee6056bff740bd6a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.0.7
4
+
5
+ Features:
6
+
7
+ - Table support
8
+
9
+ ## 0.0.6
10
+
11
+ Features:
12
+
13
+ - Tab stops and hanging indents done
14
+ - Replacing of {CHECKBOX_EMPTY} and {CHECKBOX_CHECKED} with correct UTF-8 characters done
15
+
3
16
  ## 0.0.5
4
17
 
5
18
  Features:
data/README.md CHANGED
@@ -35,21 +35,32 @@ gem install docxify
35
35
  @docx.add_paragraph "{CHECKBOX_EMPTY}\tEmpty checkbox", tab_stops_cm: [2]
36
36
  @docx.add_paragraph "{CHECKBOX_CHECKED}\tChecked checkbox", tab_stops_cm: [2]
37
37
 
38
- @docx.add_page_break
39
38
  @docx.add_divider
40
39
 
41
40
  @docx.add_image "file_path or data", align: :right, height_cm: 2, width_cm: 4
42
41
 
43
- headers = [
44
- DocXify::Element::TableCell.new("<b>Header 1</b>"),
45
- DocXify::Element::TableCell.new("<b>Header 2</b>")
46
- ]
47
- row = [
48
- DocXify::Element::TableCell.new("Content <b>here</b>", valign: :center, align: :left, nowrap: true, colspan: 3),
49
- DocXify::Element::TableCell.new("Content <b>here</b>")
50
- ]
51
- rows = [row]
52
- @docx.add_table headers, rows, expand: :full
42
+ @docx.add_page_break
43
+
44
+ @docx.container page_width: DocXify::A4_PORTRAIT_HEIGHT, page_height: DocXify::A4_PORTRAIT_WIDTH do |container|
45
+ rows = []
46
+ rows << [
47
+ DocXify::Element::TableCell.new("<b>Header 1</b>"),
48
+ DocXify::Element::TableCell.new("<b>Header 2</b>")
49
+ DocXify::Element::TableCell.new("<b>Header 3</b>")
50
+ DocXify::Element::TableCell.new("<b>Header 4</b>", width_cm: 10)
51
+ ]
52
+ rows << [
53
+ DocXify::Element::TableCell.new("Content 1", valign: :center, align: :left, nowrap: true, colspan: 3),
54
+ DocXify::Element::TableCell.new("Rowspan <b>here</b>", rowspan: true) # merges until a "nil" cell
55
+ ]
56
+ rows << [
57
+ DocXify::Element::TableCell.new("Secondary 1")
58
+ DocXify::Element::TableCell.new("Secondary 2")
59
+ DocXify::Element::TableCell.new("Secondary 3")
60
+ DocXify::Element::TableCell.new(nil) # Word requires that there's an empty cell merged with the previous rowspan
61
+ ]
62
+ container.add_table rows
63
+ end
53
64
 
54
65
  docx_binary_data = @docx.render
55
66
  # or
@@ -39,9 +39,11 @@ module DocXify
39
39
  zip.put_next_entry "word/_rels/document.xml.rels"
40
40
  zip.write document_xml_rels
41
41
 
42
- @document.images.each do |image|
43
- zip.put_next_entry "word/media/#{image.filename}"
44
- zip.write image.data
42
+ @document.relationships.each do |relation|
43
+ if relation.is_a?(DocXify::Element::File)
44
+ zip.put_next_entry "word/media/#{relation.filename}"
45
+ zip.write relation.data
46
+ end
45
47
  end
46
48
  end
47
49
 
@@ -64,8 +66,8 @@ module DocXify
64
66
  <Relationship Id="rId4" Target="fontTable.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable"/>
65
67
  XML
66
68
 
67
- @document.images.each do |image|
68
- xml << "<Relationship Id=\"#{image.reference}\" Target=\"media/#{image.filename}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\"/>"
69
+ @document.relationships.each do |relation|
70
+ xml << relation.to_s
69
71
  end
70
72
 
71
73
  xml << "</Relationships>"
@@ -1,11 +1,11 @@
1
1
  module DocXify
2
2
  class Document
3
3
  attr_accessor :font, :size, :color, :background
4
- attr_reader :content, :images
4
+ attr_reader :content, :relationships, :width
5
5
 
6
6
  def initialize(options = {})
7
7
  @content = []
8
- @images = []
8
+ @relationships = []
9
9
  @width = options[:width] || A4_PORTRAIT_WIDTH
10
10
  @height = options[:height] || A4_PORTRAIT_HEIGHT
11
11
  @font = options[:font] || "Times New Roman"
@@ -74,7 +74,7 @@ module DocXify
74
74
 
75
75
  def add_file(file_path_or_data)
76
76
  file = DocXify::Element::File.new(file_path_or_data)
77
- @images << file
77
+ @relationships << file
78
78
  file
79
79
  end
80
80
 
@@ -91,8 +91,9 @@ module DocXify
91
91
  add DocXify::Element::Paragraph.new(text, options)
92
92
  end
93
93
 
94
- def add_table(headers, rows, options = {})
95
- add DocXify::Element::Table.new(headers, rows, options)
94
+ def add_table(rows, options = {})
95
+ options[:document] = self
96
+ add DocXify::Element::Table.new(rows, options)
96
97
  end
97
98
  end
98
99
  end
@@ -22,7 +22,7 @@ module DocXify
22
22
  if char == "<"
23
23
  state = :tag
24
24
  tag = ""
25
- result << text.strip unless text.strip.empty?
25
+ result << text unless text.empty?
26
26
  text = ""
27
27
  else
28
28
  text << char
@@ -61,7 +61,7 @@ module DocXify
61
61
  when :content # With the content of an HTML tag
62
62
  if char == "<"
63
63
  state = :tag
64
- result.last[:content] = content.strip
64
+ result.last[:content] = content
65
65
  content = ""
66
66
  else
67
67
  content << char
@@ -71,7 +71,7 @@ module DocXify
71
71
  last_char = char
72
72
  end
73
73
 
74
- result << text.strip unless text.strip.empty?
74
+ result << text unless text.empty?
75
75
  result
76
76
  end
77
77
  end
@@ -34,6 +34,10 @@ module DocXify
34
34
  "image-#{Digest::SHA1.hexdigest(@data)[0, 8]}"
35
35
  end
36
36
 
37
+ def to_s
38
+ "<Relationship Id=\"#{reference}\" Target=\"media/#{filename}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image\"/>"
39
+ end
40
+
37
41
  def contains_png_image?(data)
38
42
  data[0, 8] == PNG_SIGNATURE
39
43
  end
@@ -1,7 +1,7 @@
1
1
  module DocXify
2
2
  module Element
3
3
  class Paragraph < Base
4
- attr_accessor :text, :font, :size, :color, :background, :align, :inline_styling, :tab_stops_cm
4
+ attr_accessor :text, :font, :size, :color, :background, :align, :inline_styling, :tab_stops_cm, :hanging_cm
5
5
 
6
6
  def initialize(text, options = {})
7
7
  super()
@@ -14,38 +14,86 @@ module DocXify
14
14
  @background = options[:background] if options[:background]
15
15
  @background ||= @document&.background if @document&.background
16
16
  @align = options[:align] || :left
17
- @inline_styling = options[:inline_styling] || true
17
+ @inline_styling = options.key?(:inline_styling) ? options[:inline_styling] : true
18
18
  @tab_stops_cm = options[:tab_stops_cm] || []
19
+ @hanging_cm = options[:hanging_cm] || 0
19
20
  end
20
21
 
21
22
  def to_s(_container = nil)
22
23
  nodes = if @inline_styling
23
24
  parse_simple_html(@text)
24
25
  else
25
- [@text]
26
+ [@text.gsub("<", "&lt;").gsub(">", "&gt;")]
26
27
  end
27
28
 
28
29
  xml = "<w:p>"
29
- xml << <<~XML
30
- <w:pPr>
31
- <w:jc w:val="#{@align}"/>
32
- </w:pPr>
33
- XML
34
- xml << "<w:r>"
35
- xml << <<~XML
36
- <w:rPr>
37
- <w:rFonts w:ascii="#{@font}" w:cs="#{@font}" w:hAnsi="#{@font}"/>
38
- <w:color w:val="#{@color.gsub("#", "")}"/>
39
- <w:sz w:val="#{DocXify.pt2halfpt(@size)}"/>
40
- <w:szCs w:val="#{DocXify.pt2halfpt(@size)}"/>
41
- #{"<w:highlight w:val=\"yellow\"/>" if @highlight}
42
- </w:rPr>
43
- XML
30
+
31
+ xml << "<w:pPr>"
32
+ xml << "<w:jc w:val=\"#{@align}\"/>" if @align != :left
33
+
34
+ if tab_stops_cm.any?
35
+ xml << "<w:tabs>"
36
+ tab_stops_cm.each do |stop|
37
+ xml << "<w:tab w:pos=\"#{DocXify.cm2dxa(stop)}\" w:val=\"left\"/>"
38
+ end
39
+ xml << "</w:tabs>"
40
+ end
41
+
42
+ if @hanging_cm&.positive?
43
+ xml << "<w:ind w:hanging=\"#{DocXify.cm2dxa(@hanging_cm)}\" w:left=\"#{DocXify.cm2dxa(@hanging_cm)}\"/>"
44
+ end
45
+
46
+ xml << "</w:pPr>"
44
47
 
45
48
  nodes.each do |node|
46
- xml << "<w:t xml:space=\"preserve\">#{node}</w:t>"
49
+ if node.is_a?(Hash) && node[:tag] == "a"
50
+ xml << "<w:hyperlink r:id=\"#{ref_for_href(node[:attributes][:href])}\">"
51
+ end
52
+
53
+ xml << "<w:r>"
54
+ xml << <<~XML
55
+ <w:rPr>
56
+ <w:rFonts w:ascii="#{@font}" w:cs="#{@font}" w:hAnsi="#{@font}"/>
57
+ <w:color w:val="#{@color.gsub("#", "")}"/>
58
+ <w:sz w:val="#{DocXify.pt2halfpt(@size)}"/>
59
+ <w:szCs w:val="#{DocXify.pt2halfpt(@size)}"/>
60
+ #{"<w:highlight w:val=\"yellow\"/>" if @highlight}
61
+ #{"<w:b/><w:bCs/>" if node.is_a?(Hash) && node[:tag] == "b"}
62
+ #{"<w:i/><w:iCs/>" if node.is_a?(Hash) && node[:tag] == "i"}
63
+ #{"<w:u w:val=\"single\"/>" if node.is_a?(Hash) && (node[:tag] == "u" || node[:tag] == "a")}
64
+ #{"<w:rStyle w:val=\"Hyperlink\"/>" if node.is_a?(Hash) && node[:tag] == "a"}
65
+ </w:rPr>
66
+ XML
67
+
68
+ content = node.is_a?(Hash) ? node[:content] : node
69
+ content = content.gsub("{CHECKBOX_EMPTY}", "☐").gsub("{CHECKBOX_CHECKED}", "☒")
70
+ xml << "<w:t xml:space=\"preserve\">#{content}</w:t>"
71
+ xml << "</w:r>"
72
+
73
+ if node.is_a?(Hash) && node[:tag] == "a"
74
+ xml << "</w:hyperlink>"
75
+ end
47
76
  end
48
- xml << "</w:r></w:p>"
77
+
78
+ xml << "</w:p>"
79
+ end
80
+
81
+ def ref_for_href(href)
82
+ relation = nil
83
+
84
+ @document.relationships.select { |r| r.is_a?(DocXify::Element::WebAddress) }.each do |r|
85
+ if r.target == href
86
+ relation = r
87
+ break
88
+ end
89
+ end
90
+
91
+ if relation.nil?
92
+ relation = DocXify::Element::WebAddress.new(href)
93
+ @document.relationships << relation
94
+ end
95
+
96
+ relation.reference
49
97
  end
50
98
  end
51
99
  end
@@ -1,8 +1,106 @@
1
+ # Structure of a table element looks something like this
2
+ # <w:tbl> - Table element
3
+ # <w:tblPr> - Table properties
4
+ # <w:tblGrid> - Table grid (column widths)
5
+ # <w:tr> - Table row
6
+ # <w:tc> - Table cell
7
+ # <w:tcPr> - Table cell properties
8
+ # <w:tcW w:type="dxa" w:w="1502"/> - Table cell width
9
+ # <w:vMerge w:val="restart"/> - Something about merging TODO - figure out
10
+ # <w:tcBorders> - Overriding table cell borders
11
+ # <w:top w:val="nil"/>
12
+ # <w:bottom w:color="auto" w:space="0" w:sz="4" w:val="single"/>
13
+ # <w:right w:val="nil"/>
14
+ # </w:tcBorders>
15
+ # <w:vAlign w:val="bottom"/>
16
+ # </w:tcPr>
17
+ # <w:p> - Paragraph element
18
+
1
19
  module DocXify
2
20
  module Element
3
21
  class Table < Base
22
+ attr_accessor :rows, :column_widths
23
+
24
+ def initialize(rows, options = {})
25
+ super()
26
+ @document = options[:document]
27
+ @rows = rows
28
+ end
29
+
4
30
  def to_s(_container = nil)
5
- # TODO: Implement output
31
+ xml = ""
32
+ xml << table_element_start
33
+ xml << table_properties
34
+ xml << table_grid
35
+ @rows.each do |row|
36
+ xml << table_row(row)
37
+ end
38
+ xml << table_element_end
39
+ xml
40
+ end
41
+
42
+ def table_element_start
43
+ "<w:tbl>"
44
+ end
45
+
46
+ def table_properties
47
+ xml = ""
48
+ xml << "<w:tblPr>"
49
+ xml << '<w:tblStyle w:val="TableGrid"/>'
50
+ xml << '<w:tblW w:type="auto" w:w="0"/>'
51
+ xml << "<w:tblBorders>"
52
+ xml << '<w:top w:color="auto" w:space="0" w:sz="0" w:val="none"/>'
53
+ xml << '<w:left w:color="auto" w:space="0" w:sz="0" w:val="none"/>'
54
+ xml << '<w:bottom w:color="auto" w:space="0" w:sz="0" w:val="none"/>'
55
+ xml << '<w:right w:color="auto" w:space="0" w:sz="0" w:val="none"/>'
56
+ xml << "</w:tblBorders>"
57
+ xml << '<w:tblLook w:firstColumn="1" w:firstRow="1" w:lastColumn="0" w:lastRow="0" w:noHBand="0" w:noVBand="1" w:val="04A0"/>'
58
+ xml << "</w:tblPr>"
59
+ xml
60
+ end
61
+
62
+ def table_grid
63
+ default_equal_width = (@document.width / @rows.first.size).to_i
64
+
65
+ @column_widths = []
66
+ xml = "<w:tblGrid>"
67
+ @rows.first.each do |cell|
68
+ width = if cell.width_cm
69
+ DocXify.cm2dxa(cell.width_cm)
70
+ else
71
+ default_equal_width
72
+ end
73
+ xml << "<w:gridCol w:w=\"#{width}\"/>"
74
+ @column_widths << width
75
+ end
76
+
77
+ xml << "</w:tblGrid>"
78
+ xml
79
+ end
80
+
81
+ def table_row(row)
82
+ xml = "<w:tr>"
83
+ widths = @column_widths.dup
84
+ row.each do |cell|
85
+ if cell.nil?
86
+ cell = DocXify::Element::TableCell.new(nil)
87
+ end
88
+
89
+ if cell.width_cm.nil?
90
+ cell.width_cm = 0
91
+ (cell.colspan || 1).times do
92
+ cell.width_cm += DocXify.dxa2cm(widths.shift)
93
+ end
94
+ end
95
+
96
+ xml << cell.to_s
97
+ end
98
+ xml << "</w:tr>"
99
+ xml
100
+ end
101
+
102
+ def table_element_end
103
+ "</w:tbl>"
6
104
  end
7
105
  end
8
106
  end
@@ -0,0 +1,66 @@
1
+ module DocXify
2
+ module Element
3
+ class TableCell < Base
4
+ attr_accessor :content, :valign, :align, :nowrap, :colspan, :width_cm, :borders, :font, :size, :colour
5
+
6
+ def initialize(content, options = {})
7
+ super()
8
+ @content = content
9
+ @valign = options[:valign] || :top
10
+ @align = options[:align] || :left
11
+ @nowrap = options[:nowrap]
12
+ @colspan = options[:colspan]
13
+ @rowspan = options[:rowspan]
14
+ @width_cm = options[:width_cm]
15
+ @font = options[:font]
16
+ @size = options[:size]
17
+ @color = options[:color]
18
+ @borders = options[:borders]&.map(&:to_sym) || []
19
+ end
20
+
21
+ def to_s
22
+ xml = "<w:tc>"
23
+ xml << "<w:tcPr>"
24
+ xml << %Q(<w:tcW w:type="dxa" w:w="#{DocXify.cm2dxa(@width_cm)}"/>)
25
+ if !@colspan.nil? && @colspan.to_i > 1
26
+ xml << %Q(<w:gridSpan w:val="#{@colspan}"/>')
27
+ end
28
+
29
+ if borders.any?
30
+ xml << "<w:tcBorders>"
31
+ xml << (borders.include?(:top) ? '<w:top w:color="auto" w:space="0" w:sz="4" w:val="single"/>' : '<w:top w:val="nil"/>')
32
+ xml << (borders.include?(:bottom) ? '<w:bottom w:color="auto" w:space="0" w:sz="4" w:val="single"/>' : '<w:bottom w:val="nil"/>')
33
+ xml << (borders.include?(:left) ? '<w:left w:color="auto" w:space="0" w:sz="4" w:val="single"/>' : '<w:left w:val="nil"/>')
34
+ xml << (borders.include?(:right) ? '<w:right w:color="auto" w:space="0" w:sz="4" w:val="single"/>' : '<w:right w:val="nil"/>')
35
+ xml << "</w:tcBorders>"
36
+ end
37
+
38
+ if @valign != :top
39
+ xml << %Q(<w:vAlign w:val="#{@valign}"/>)
40
+ end
41
+
42
+ if @nowrap
43
+ xml << "<w:noWrap/>"
44
+ end
45
+
46
+ if @content.nil?
47
+ @content = "" # Nil is useful for rowspan, but we need to output something or Word breaks
48
+ end
49
+
50
+ if @rowspan
51
+ xml << '<w:vMerge w:val="restart"/>'
52
+ elsif @content == ""
53
+ xml << "<w:vMerge/>"
54
+ end
55
+
56
+ xml << "</w:tcPr>"
57
+
58
+ xml << DocXify::Element::Paragraph.new(@content, document: @document, font: @font, size: @size, color: @color, align: @align).to_s
59
+
60
+ xml << "</w:tc>"
61
+
62
+ xml
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,20 @@
1
+ module DocXify
2
+ module Element
3
+ class WebAddress < Base
4
+ attr_accessor :target
5
+
6
+ def initialize(target)
7
+ super()
8
+ @target = target
9
+ end
10
+
11
+ def reference
12
+ "url-#{Digest::SHA1.hexdigest(target)[0, 8]}"
13
+ end
14
+
15
+ def to_s
16
+ "<Relationship Id=\"#{reference}\" Target=\"#{target}/\" TargetMode=\"External\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink\"/>"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,3 +1,3 @@
1
1
  module DocXify
2
- VERSION = "0.0.5".freeze
2
+ VERSION = "0.0.7".freeze
3
3
  end
data/lib/docxify.rb CHANGED
@@ -10,6 +10,8 @@ require_relative "docxify/element/image"
10
10
  require_relative "docxify/element/page_break"
11
11
  require_relative "docxify/element/paragraph"
12
12
  require_relative "docxify/element/table"
13
+ require_relative "docxify/element/table_cell"
14
+ require_relative "docxify/element/web_address"
13
15
 
14
16
  module DocXify
15
17
  class Error < StandardError; end
@@ -28,6 +30,14 @@ module DocXify
28
30
  (value * UNITS_PER_CM).to_i
29
31
  end
30
32
 
33
+ # Inverse of cm2dxa
34
+ def self.dxa2cm(value)
35
+ value = value.to_f
36
+ raise ArgumentError.new("Value must be greater than or equal to 0") if value.negative?
37
+
38
+ (value / UNITS_PER_CM).to_i
39
+ end
40
+
31
41
  # Used for font sizes
32
42
  def self.pt2halfpt(value)
33
43
  value = value.to_f
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docxify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Jeffries
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-12 00:00:00.000000000 Z
11
+ date: 2024-04-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubyzip
@@ -48,6 +48,8 @@ files:
48
48
  - lib/docxify/element/page_break.rb
49
49
  - lib/docxify/element/paragraph.rb
50
50
  - lib/docxify/element/table.rb
51
+ - lib/docxify/element/table_cell.rb
52
+ - lib/docxify/element/web_address.rb
51
53
  - lib/docxify/template.rb
52
54
  - lib/docxify/version.rb
53
55
  homepage: https://github.com/foundercatalyst/docxify