docxify 0.0.5 → 0.0.7

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
  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