rbxl 1.0.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.
@@ -0,0 +1,143 @@
1
+ module Rbxl
2
+ class WriteOnlyWorkbook
3
+ attr_reader :worksheets
4
+
5
+ def initialize
6
+ @worksheets = []
7
+ @closed = false
8
+ @saved = false
9
+ end
10
+
11
+ def add_sheet(name)
12
+ ensure_writable!
13
+
14
+ sheet = WriteOnlyWorksheet.new(name: name)
15
+ @worksheets << sheet
16
+ sheet
17
+ end
18
+
19
+ def save(path)
20
+ ensure_writable!
21
+ raise Error, "at least one worksheet is required" if worksheets.empty?
22
+
23
+ Zip::OutputStream.open(path) do |zip|
24
+ write_entry(zip, "[Content_Types].xml", content_types_xml)
25
+ write_entry(zip, "_rels/.rels", root_rels_xml)
26
+ write_entry(zip, "xl/workbook.xml", workbook_xml)
27
+ write_entry(zip, "xl/_rels/workbook.xml.rels", workbook_rels_xml)
28
+ write_entry(zip, "xl/styles.xml", styles_xml)
29
+
30
+ worksheets.each_with_index do |sheet, index|
31
+ write_entry(zip, "xl/worksheets/sheet#{index + 1}.xml", sheet.to_xml)
32
+ end
33
+ end
34
+
35
+ @saved = true
36
+ close
37
+ path
38
+ end
39
+
40
+ def close
41
+ @closed = true
42
+ end
43
+
44
+ def closed?
45
+ @closed
46
+ end
47
+
48
+ private
49
+
50
+ def ensure_writable!
51
+ raise ClosedWorkbookError, "workbook has been closed" if closed?
52
+ raise WorkbookAlreadySavedError, "write-only workbook can only be saved once" if @saved
53
+ end
54
+
55
+ def write_entry(zip, name, content)
56
+ zip.put_next_entry(name)
57
+ zip.write(content)
58
+ end
59
+
60
+ def content_types_xml
61
+ worksheet_overrides = worksheets.each_index.map do |index|
62
+ %(<Override PartName="/xl/worksheets/sheet#{index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>)
63
+ end.join
64
+
65
+ <<~XML.chomp
66
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
67
+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
68
+ <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
69
+ <Default Extension="xml" ContentType="application/xml"/>
70
+ <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
71
+ <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
72
+ #{worksheet_overrides}
73
+ </Types>
74
+ XML
75
+ end
76
+
77
+ def root_rels_xml
78
+ <<~XML.chomp
79
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
80
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
81
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
82
+ </Relationships>
83
+ XML
84
+ end
85
+
86
+ def workbook_xml
87
+ sheet_nodes = worksheets.each_with_index.map do |sheet, index|
88
+ %(<sheet name="#{escape(sheet.name)}" sheetId="#{index + 1}" r:id="rId#{index + 1}"/>)
89
+ end.join
90
+
91
+ <<~XML.chomp
92
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
93
+ <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
94
+ <sheets>#{sheet_nodes}</sheets>
95
+ </workbook>
96
+ XML
97
+ end
98
+
99
+ def workbook_rels_xml
100
+ relationship_nodes = worksheets.each_with_index.map do |_, index|
101
+ %(<Relationship Id="rId#{index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet#{index + 1}.xml"/>)
102
+ end
103
+ relationship_nodes << %(<Relationship Id="rId#{worksheets.length + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>)
104
+
105
+ <<~XML.chomp
106
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
107
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
108
+ #{relationship_nodes.join}
109
+ </Relationships>
110
+ XML
111
+ end
112
+
113
+ def styles_xml
114
+ <<~XML.chomp
115
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
116
+ <styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
117
+ <fonts count="1">
118
+ <font><sz val="11"/><name val="Calibri"/></font>
119
+ </fonts>
120
+ <fills count="1">
121
+ <fill><patternFill patternType="none"/></fill>
122
+ </fills>
123
+ <borders count="1">
124
+ <border><left/><right/><top/><bottom/><diagonal/></border>
125
+ </borders>
126
+ <cellStyleXfs count="1">
127
+ <xf numFmtId="0" fontId="0" fillId="0" borderId="0"/>
128
+ </cellStyleXfs>
129
+ <cellXfs count="1">
130
+ <xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
131
+ </cellXfs>
132
+ <cellStyles count="1">
133
+ <cellStyle name="Normal" xfId="0" builtinId="0"/>
134
+ </cellStyles>
135
+ </styleSheet>
136
+ XML
137
+ end
138
+
139
+ def escape(value)
140
+ CGI.escapeHTML(value.to_s)
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,180 @@
1
+ module Rbxl
2
+ class WriteOnlyWorksheet
3
+ attr_reader :name
4
+
5
+ def initialize(name:)
6
+ @name = name
7
+ @rows = []
8
+ @column_name_cache = []
9
+ end
10
+
11
+ def <<(values)
12
+ append(values)
13
+ end
14
+
15
+ def append(values)
16
+ unless values.is_a?(Array) || values.is_a?(Enumerator)
17
+ raise TypeError, "row must be an Array or Enumerator, got #{values.class}"
18
+ end
19
+
20
+ @rows << Array(values)
21
+ self
22
+ end
23
+
24
+ def to_xml
25
+ if defined?(Rbxl::Native)
26
+ return Rbxl::Native.generate_sheet(@rows)
27
+ end
28
+
29
+ dimension_ref = @rows.empty? ? "A1" : "A1:#{column_name(max_columns)}#{@rows.length}"
30
+ buf = +""
31
+ buf << '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
32
+ buf << "\n"
33
+ buf << '<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">'
34
+ buf << "\n"
35
+ buf << ' <dimension ref="'
36
+ buf << dimension_ref
37
+ buf << '"/>'
38
+ buf << "\n"
39
+ buf << ' <sheetData>'
40
+
41
+ @rows.each_with_index do |row_values, row_index|
42
+ row_num_str = (row_index + 1).to_s
43
+ buf << '<row r="'
44
+ buf << row_num_str
45
+ buf << '">'
46
+ row_values.each_with_index do |value, col_index|
47
+ serialize_cell_to(buf, column_name(col_index + 1), row_num_str, value)
48
+ end
49
+ buf << '</row>'
50
+ end
51
+
52
+ buf << "</sheetData>\n</worksheet>"
53
+ buf
54
+ end
55
+
56
+ private
57
+
58
+ def serialize_cell_to(buf, col_name, row_num_str, value)
59
+ if value.is_a?(WriteOnlyCell)
60
+ serialize_write_only_cell_to(buf, col_name, row_num_str, value)
61
+ return
62
+ end
63
+
64
+ buf << '<c r="'
65
+ buf << col_name
66
+ buf << row_num_str
67
+ case value
68
+ when nil
69
+ buf << '"/>'
70
+ when true
71
+ buf << '" t="b"><v>1</v></c>'
72
+ when false
73
+ buf << '" t="b"><v>0</v></c>'
74
+ when Integer
75
+ buf << '"><v>'
76
+ buf << value.to_s
77
+ buf << '</v></c>'
78
+ when Numeric
79
+ buf << '"><v>'
80
+ buf << value.to_s
81
+ buf << '</v></c>'
82
+ when Date, DateTime, Time
83
+ buf << '" t="inlineStr"><is><t>'
84
+ escape_to(buf, value.iso8601)
85
+ buf << '</t></is></c>'
86
+ else
87
+ buf << '" t="inlineStr"><is><t>'
88
+ escape_to(buf, value.to_s)
89
+ buf << '</t></is></c>'
90
+ end
91
+ end
92
+
93
+ def escape_to(buf, str)
94
+ i = 0
95
+ len = str.bytesize
96
+ start = 0
97
+
98
+ while i < len
99
+ byte = str.getbyte(i)
100
+ case byte
101
+ when 38 # &
102
+ buf << str.byteslice(start, i - start) if i > start
103
+ buf << '&amp;'
104
+ start = i + 1
105
+ when 60 # <
106
+ buf << str.byteslice(start, i - start) if i > start
107
+ buf << '&lt;'
108
+ start = i + 1
109
+ when 62 # >
110
+ buf << str.byteslice(start, i - start) if i > start
111
+ buf << '&gt;'
112
+ start = i + 1
113
+ when 34 # "
114
+ buf << str.byteslice(start, i - start) if i > start
115
+ buf << '&quot;'
116
+ start = i + 1
117
+ end
118
+ i += 1
119
+ end
120
+
121
+ if start == 0
122
+ buf << str
123
+ elsif start < len
124
+ buf << str.byteslice(start, len - start)
125
+ end
126
+ end
127
+
128
+ def column_name(index)
129
+ @column_name_cache[index] ||= begin
130
+ name = +""
131
+ current = index
132
+ while current.positive?
133
+ current -= 1
134
+ name.prepend((65 + (current % 26)).chr)
135
+ current /= 26
136
+ end
137
+ name.freeze
138
+ end
139
+ end
140
+
141
+ def max_columns
142
+ max = 0
143
+ @rows.each { |r| max = r.length if r.length > max }
144
+ max > 0 ? max : 1
145
+ end
146
+
147
+ def serialize_write_only_cell_to(buf, col_name, row_num_str, cell)
148
+ buf << '<c r="'
149
+ buf << col_name
150
+ buf << row_num_str
151
+ buf << '"'
152
+ if cell.style_id
153
+ buf << ' s="'
154
+ buf << cell.style_id.to_s
155
+ buf << '"'
156
+ end
157
+
158
+ case cell.value
159
+ when nil
160
+ buf << '/>'
161
+ when true
162
+ buf << ' t="b"><v>1</v></c>'
163
+ when false
164
+ buf << ' t="b"><v>0</v></c>'
165
+ when Numeric
166
+ buf << '><v>'
167
+ buf << cell.value.to_s
168
+ buf << '</v></c>'
169
+ when Date, DateTime, Time
170
+ buf << ' t="inlineStr"><is><t>'
171
+ escape_to(buf, cell.value.iso8601)
172
+ buf << '</t></is></c>'
173
+ else
174
+ buf << ' t="inlineStr"><is><t>'
175
+ escape_to(buf, cell.value.to_s)
176
+ buf << '</t></is></c>'
177
+ end
178
+ end
179
+ end
180
+ end
data/lib/rbxl.rb ADDED
@@ -0,0 +1,33 @@
1
+ require "cgi"
2
+ require "date"
3
+ require "nokogiri"
4
+ require "stringio"
5
+ require "zip"
6
+
7
+ require_relative "rbxl/cell"
8
+ require_relative "rbxl/empty_cell"
9
+ require_relative "rbxl/errors"
10
+ require_relative "rbxl/read_only_cell"
11
+ require_relative "rbxl/read_only_workbook"
12
+ require_relative "rbxl/read_only_worksheet"
13
+ require_relative "rbxl/row"
14
+ require_relative "rbxl/version"
15
+ require_relative "rbxl/write_only_cell"
16
+ require_relative "rbxl/write_only_workbook"
17
+ require_relative "rbxl/write_only_worksheet"
18
+
19
+ module Rbxl
20
+ class << self
21
+ def open(path, read_only: false)
22
+ raise ArgumentError, "read_only: true is required for this MVP" unless read_only
23
+
24
+ ReadOnlyWorkbook.open(path)
25
+ end
26
+
27
+ def new(write_only: false)
28
+ raise ArgumentError, "write_only: true is required for this MVP" unless write_only
29
+
30
+ WriteOnlyWorkbook.new
31
+ end
32
+ end
33
+ end
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rbxl
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Taro KOBAYASHI
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rubyzip
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.3'
26
+ - !ruby/object:Gem::Dependency
27
+ name: nokogiri
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.19'
33
+ - - "<"
34
+ - !ruby/object:Gem::Version
35
+ version: '2.0'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '1.19'
43
+ - - "<"
44
+ - !ruby/object:Gem::Version
45
+ version: '2.0'
46
+ description: A small Ruby gem for read-only and write-only xlsx workflows.
47
+ email:
48
+ - taro@matzlika.co.jp
49
+ executables: []
50
+ extensions:
51
+ - ext/rbxl_native/extconf.rb
52
+ extra_rdoc_files: []
53
+ files:
54
+ - LICENSE.txt
55
+ - README.md
56
+ - Rakefile
57
+ - ext/rbxl_native/extconf.rb
58
+ - ext/rbxl_native/native.c
59
+ - lib/rbxl.rb
60
+ - lib/rbxl/cell.rb
61
+ - lib/rbxl/empty_cell.rb
62
+ - lib/rbxl/errors.rb
63
+ - lib/rbxl/native.rb
64
+ - lib/rbxl/read_only_cell.rb
65
+ - lib/rbxl/read_only_workbook.rb
66
+ - lib/rbxl/read_only_worksheet.rb
67
+ - lib/rbxl/row.rb
68
+ - lib/rbxl/version.rb
69
+ - lib/rbxl/write_only_cell.rb
70
+ - lib/rbxl/write_only_workbook.rb
71
+ - lib/rbxl/write_only_worksheet.rb
72
+ homepage: https://github.com/matzlika/rbxl
73
+ licenses:
74
+ - MIT
75
+ metadata:
76
+ homepage_uri: https://github.com/matzlika/rbxl
77
+ source_code_uri: https://github.com/matzlika/rbxl
78
+ bug_tracker_uri: https://github.com/matzlika/rbxl/issues
79
+ changelog_uri: https://github.com/matzlika/rbxl/releases
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '3.1'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 4.0.3
95
+ specification_version: 4
96
+ summary: Streaming xlsx reader/writer inspired by openpyxl
97
+ test_files: []