calcsheet 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 +7 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +62 -0
- data/LICENSE.txt +21 -0
- data/README.md +921 -0
- data/lib/calcsheet/book.rb +164 -0
- data/lib/calcsheet/cast.rb +578 -0
- data/lib/calcsheet/column.rb +117 -0
- data/lib/calcsheet/column_letter.rb +34 -0
- data/lib/calcsheet/configuration.rb +186 -0
- data/lib/calcsheet/decimal_text.rb +19 -0
- data/lib/calcsheet/entry_name.rb +28 -0
- data/lib/calcsheet/errors.rb +186 -0
- data/lib/calcsheet/excel_serial.rb +65 -0
- data/lib/calcsheet/format_detector.rb +109 -0
- data/lib/calcsheet/formula_protection.rb +31 -0
- data/lib/calcsheet/grid.rb +20 -0
- data/lib/calcsheet/header_keys.rb +66 -0
- data/lib/calcsheet/limited_inflater.rb +326 -0
- data/lib/calcsheet/merge_check.rb +41 -0
- data/lib/calcsheet/odf.rb +45 -0
- data/lib/calcsheet/option_validation.rb +160 -0
- data/lib/calcsheet/options.rb +10 -0
- data/lib/calcsheet/raw_row.rb +18 -0
- data/lib/calcsheet/raw_types.rb +15 -0
- data/lib/calcsheet/readers/csv_dialect.rb +264 -0
- data/lib/calcsheet/readers/csv_input.rb +128 -0
- data/lib/calcsheet/readers/csv_reader.rb +239 -0
- data/lib/calcsheet/readers/ods/cell_formats.rb +115 -0
- data/lib/calcsheet/readers/ods/content_parser.rb +594 -0
- data/lib/calcsheet/readers/ods/count.rb +30 -0
- data/lib/calcsheet/readers/ods/cursor.rb +121 -0
- data/lib/calcsheet/readers/ods/patterns.rb +70 -0
- data/lib/calcsheet/readers/ods/row_seek.rb +157 -0
- data/lib/calcsheet/readers/ods/scanner.rb +302 -0
- data/lib/calcsheet/readers/ods/style_scan.rb +108 -0
- data/lib/calcsheet/readers/ods/tag_text.rb +76 -0
- data/lib/calcsheet/readers/ods_reader.rb +71 -0
- data/lib/calcsheet/readers/ox_errors.rb +40 -0
- data/lib/calcsheet/readers/references.rb +62 -0
- data/lib/calcsheet/readers/row_batches.rb +45 -0
- data/lib/calcsheet/readers/sax_sheet.rb +108 -0
- data/lib/calcsheet/readers/stream_remainder.rb +66 -0
- data/lib/calcsheet/readers/tag_scan.rb +119 -0
- data/lib/calcsheet/readers/xlsx/escapes.rb +36 -0
- data/lib/calcsheet/readers/xlsx/local_name.rb +54 -0
- data/lib/calcsheet/readers/xlsx/number_format.rb +89 -0
- data/lib/calcsheet/readers/xlsx/row_skipper.rb +162 -0
- data/lib/calcsheet/readers/xlsx/shared_strings.rb +209 -0
- data/lib/calcsheet/readers/xlsx/sheet_parser.rb +435 -0
- data/lib/calcsheet/readers/xlsx/styles.rb +156 -0
- data/lib/calcsheet/readers/xlsx/workbook_meta.rb +228 -0
- data/lib/calcsheet/readers/xlsx_reader.rb +122 -0
- data/lib/calcsheet/reading.rb +281 -0
- data/lib/calcsheet/row.rb +110 -0
- data/lib/calcsheet/sample.rb +10 -0
- data/lib/calcsheet/schema.rb +140 -0
- data/lib/calcsheet/sheet_info.rb +21 -0
- data/lib/calcsheet/sheet_names.rb +114 -0
- data/lib/calcsheet/sheet_selection.rb +58 -0
- data/lib/calcsheet/source.rb +263 -0
- data/lib/calcsheet/stream_body.rb +81 -0
- data/lib/calcsheet/time_zone.rb +96 -0
- data/lib/calcsheet/untrusted.rb +32 -0
- data/lib/calcsheet/version.rb +6 -0
- data/lib/calcsheet/write_book.rb +202 -0
- data/lib/calcsheet/write_sheet.rb +282 -0
- data/lib/calcsheet/writers/csv_writer.rb +138 -0
- data/lib/calcsheet/writers/ods_writer.rb +437 -0
- data/lib/calcsheet/writers/xlsx_writer.rb +350 -0
- data/lib/calcsheet/writers/xml_text.rb +35 -0
- data/lib/calcsheet/writers/zip_buffered.rb +55 -0
- data/lib/calcsheet/zip_container.rb +190 -0
- data/lib/calcsheet/zip_directory.rb +202 -0
- data/lib/calcsheet/zip_end_record.rb +117 -0
- data/lib/calcsheet.rb +295 -0
- metadata +200 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ox"
|
|
4
|
+
require_relative "../../cast"
|
|
5
|
+
require_relative "../../configuration"
|
|
6
|
+
require_relative "../../errors"
|
|
7
|
+
require_relative "../../grid"
|
|
8
|
+
require_relative "../../odf"
|
|
9
|
+
require_relative "../../untrusted"
|
|
10
|
+
require_relative "../references"
|
|
11
|
+
require_relative "../sax_sheet"
|
|
12
|
+
require_relative "count"
|
|
13
|
+
|
|
14
|
+
module Calcsheet
|
|
15
|
+
module Readers
|
|
16
|
+
module Ods
|
|
17
|
+
# Streams one table of an ODS +content.xml+ with Ox's SAX parser and hands each non-blank row to a block as a
|
|
18
|
+
# RawRow, cast with a Cast. The stream starts at the table start tag or at a row start tag inside
|
|
19
|
+
# it (see Scanner#io_at_sheet); parsing stops at the end of the table.
|
|
20
|
+
#
|
|
21
|
+
# Rows are numbered by counting +table-row+ elements with their +number-rows-repeated+, cells placed by
|
|
22
|
+
# counting +table-cell+ and +covered-table-cell+ elements with their +number-columns-repeated+.
|
|
23
|
+
# LibreOffice ends a sheet with huge runs of empty repeated rows and cells: a run is counted, never expanded,
|
|
24
|
+
# unless it holds content. Since the file is untrusted, a repeat count that is not a positive number and content
|
|
25
|
+
# beyond column XFD or row 1048576 raise MalformedFileError. A repeated cell with content puts the same value
|
|
26
|
+
# object in each of its columns, and the rows of a repeated row share their value objects (each row has its own
|
|
27
|
+
# +values+ Array): copying them would multiply the memory of a long run. Changing a value in place changes it
|
|
28
|
+
# in every row and column of the run.
|
|
29
|
+
#
|
|
30
|
+
# Values come from +office:value+ (float, percentage, currency), +office:date-value+, +office:time-value+ and
|
|
31
|
+
# +office:boolean-value+, so numbers are exact; strings from +office:string-value+ or the text. The text joins
|
|
32
|
+
# the +text:p+ paragraphs of the cell with "\n", expands +text:s+, +text:tab+ and +text:line-break+, keeps
|
|
33
|
+
# the text of spans and links, and collapses other white space as ODF does: a run of spaces, tabs and line
|
|
34
|
+
# feeds is one space, dropped at the start of a paragraph and after another space. Elements of other
|
|
35
|
+
# namespaces inside a paragraph (annotations, drawings) are ignored, and so is everything in a cell that is not
|
|
36
|
+
# one of its paragraphs. Covered (merged) cells read as nil; LibreOffice's +calcext:value-type="error"+ marks
|
|
37
|
+
# an error cell, read as nil with a :cell_error.
|
|
38
|
+
#
|
|
39
|
+
# The cell style decides how a number or a date reads, as the number format does in XLSX: a date value
|
|
40
|
+
# under a style that shows a time is a datetime (LibreOffice writes a datetime at midnight without its time), one
|
|
41
|
+
# under a style that shows no time a date, and a number under a plain zero-padding style (+00000+) gets its
|
|
42
|
+
# zeros back as a string. The style is the cell's own, else the default cell style of its row, else that of its
|
|
43
|
+
# column; +formats+ (see Scanner#cell_formats) gives the format of each style that has one. Without a known
|
|
44
|
+
# style a date value is a datetime when it has a time.
|
|
45
|
+
#
|
|
46
|
+
# The text of a cell, its +office:string-value+ included, is at most +max_cell_bytes+ long, and a number, date,
|
|
47
|
+
# time or boolean value at most Cast::MAX_NUMBER_BYTES: longer ones raise MalformedFileError before they are
|
|
48
|
+
# joined or cast.
|
|
49
|
+
#
|
|
50
|
+
# The references of a text and of the attribute values the parser reads (+&+, +A+) are decoded by the
|
|
51
|
+
# gem (see References): one to NUL or an invalid code point, which Ox would truncate or change, an unknown entity
|
|
52
|
+
# and a stray "&" raise MalformedFileError. A CDATA section is taken as it is.
|
|
53
|
+
#
|
|
54
|
+
# Elements and attributes are matched by their qualified names, built from the prefixes of the document.
|
|
55
|
+
#
|
|
56
|
+
# @api private
|
|
57
|
+
class ContentParser < Ox::Sax
|
|
58
|
+
include SaxSheet
|
|
59
|
+
|
|
60
|
+
# skip: :skip_off hands over every whitespace-only text, also the one before the end tag of an element with
|
|
61
|
+
# children ("x<text:span>a</text:span> </text:p>"), which :skip_none drops though ODF keeps it as one space.
|
|
62
|
+
# Ox then reports the whitespace of a childless element (+<text:span> </text:span>+) twice; the collapsing of
|
|
63
|
+
# #text drops the second report, which follows a space. symbolize without strip_namespace: element and
|
|
64
|
+
# attribute names come as Symbols with their prefix (+:"table:table-row"+), the keys of +@elements+ and
|
|
65
|
+
# +@attributes+, built for the prefixes of the document (#name_codes). convert_special: false, so that the
|
|
66
|
+
# references reach #text and #attr as written (see References).
|
|
67
|
+
OPTIONS = { skip: :skip_off, convert_special: false, symbolize: true }.freeze
|
|
68
|
+
# What #text collapses: a tab, a line feed, a carriage return or two spaces.
|
|
69
|
+
COLLAPSIBLE = /[\t\n\r]| {2}/
|
|
70
|
+
# The byte of a space.
|
|
71
|
+
SPACE_BYTE = 32
|
|
72
|
+
# The kind of value of each office:value-type; another type is read as text.
|
|
73
|
+
KINDS = { "float" => :number, "percentage" => :number, "currency" => :number, "date" => :date, "time" => :time,
|
|
74
|
+
"boolean" => :boolean, "string" => :string }.freeze
|
|
75
|
+
# The reason of a typed cell without its value attribute.
|
|
76
|
+
MISSING = { date: :invalid_date, boolean: :invalid_boolean }.freeze
|
|
77
|
+
|
|
78
|
+
# Element codes, the values of +@elements+: a Hash lookup and an Integer +case+ per callback.
|
|
79
|
+
TABLE = 1
|
|
80
|
+
ROW = 2
|
|
81
|
+
CELL = 3
|
|
82
|
+
COVERED = 4
|
|
83
|
+
PARAGRAPH = 5
|
|
84
|
+
SPACES = 6
|
|
85
|
+
TAB = 7
|
|
86
|
+
LINE_BREAK = 8
|
|
87
|
+
COLUMN = 9
|
|
88
|
+
# Attribute codes, the values of +@attributes+.
|
|
89
|
+
VALUE_TYPE = 1
|
|
90
|
+
VALUE = 2
|
|
91
|
+
DATE_VALUE = 3
|
|
92
|
+
TIME_VALUE = 4
|
|
93
|
+
BOOLEAN_VALUE = 5
|
|
94
|
+
STRING_VALUE = 6
|
|
95
|
+
CALC_VALUE_TYPE = 7
|
|
96
|
+
COLUMNS_REPEATED = 8
|
|
97
|
+
ROWS_REPEATED = 9
|
|
98
|
+
SPACE_COUNT = 10
|
|
99
|
+
STYLE_NAME = 11
|
|
100
|
+
DEFAULT_STYLE = 12
|
|
101
|
+
# The +formats:+ of a document without cell formats.
|
|
102
|
+
NO_FORMATS = {}.freeze
|
|
103
|
+
# The attribute that holds the value of each kind.
|
|
104
|
+
VALUE_OF = { number: VALUE, date: DATE_VALUE, time: TIME_VALUE, boolean: BOOLEAN_VALUE, string: STRING_VALUE }.freeze
|
|
105
|
+
|
|
106
|
+
# Parses +io+ and yields the rows numbered +start_row+ or later.
|
|
107
|
+
#
|
|
108
|
+
# @param io [#readpartial] the table from its start tag, or from a row start tag (see Scanner::Cut)
|
|
109
|
+
# @param sheet [String] the sheet name, for errors
|
|
110
|
+
# @param prefixes [Hash{Symbol => String}] the prefixes of the office, table, text and calcext namespaces
|
|
111
|
+
# @param cast [Cast]
|
|
112
|
+
# @param types [#[], nil] the declared type per column index (see Reading)
|
|
113
|
+
# @param first_row [Integer] the number of the first row in +io+
|
|
114
|
+
# @param start_row [Integer]
|
|
115
|
+
# @param formats [Hash{String => Array(Symbol, Integer | nil)}] +[kind, pad]+ by cell style name
|
|
116
|
+
# @param max_cell_bytes [Integer, nil] the longest text of a cell; nil for no limit
|
|
117
|
+
# @yieldparam row [RawRow]
|
|
118
|
+
# @raise [MalformedFileError] for damaged XML (the parser error is the +cause+), an invalid repeat count,
|
|
119
|
+
# content beyond the last row or column, text that is not UTF-8 or too long, or another error of the stream
|
|
120
|
+
# (kept as +cause+)
|
|
121
|
+
# @raise [Calcsheet::Error] an error of the stream itself (a zip limit)
|
|
122
|
+
def self.parse(io, sheet:, prefixes:, cast:, types:, first_row: 1, start_row: 1, formats: NO_FORMATS,
|
|
123
|
+
max_cell_bytes: Configuration::DEFAULTS[:max_cell_bytes], &block)
|
|
124
|
+
new(sheet: sheet, prefixes: prefixes, cast: cast, types: types, first_row: first_row, start_row: start_row,
|
|
125
|
+
formats: formats, max_cell_bytes: max_cell_bytes, on_row: block).parse(io)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @api private
|
|
129
|
+
#
|
|
130
|
+
# The state of the parse, in instance variables so that a callback allocates nothing:
|
|
131
|
+
#
|
|
132
|
+
# - set once: +@sheet+, +@cast+, +@types+, +@formats+ (nil when empty), +@start_row+, +@on_row+,
|
|
133
|
+
# +@max_cell_bytes+, and from #name_codes +@elements+, +@attributes+, +@text_qualifier+,
|
|
134
|
+
# +@rows_repeated_name+ and +@columns_repeated_name+;
|
|
135
|
+
# - across rows: +@next_row+ (the number of the next row), +@columns+ (the columns of the table-column elements
|
|
136
|
+
# so far) and +@column_formats+ (+[first, last, format]+ of those whose default cell style has a format),
|
|
137
|
+
# +@yielding+ (see SaxSheet#parse);
|
|
138
|
+
# - the element whose attributes come: +@element+ (an element code, nil inside a cell but for text:s, text:tab
|
|
139
|
+
# and text:line-break);
|
|
140
|
+
# - per table-column: +@column_repeated+, +@column_style+;
|
|
141
|
+
# - per row: +@in_row+, +@row_number+, +@rows_repeated+, +@row_style+, +@values+ (sparse, see
|
|
142
|
+
# SaxSheet#trim_row!), +@errors+, +@last+ and +@column+ (the column of the next cell);
|
|
143
|
+
# - per cell: +@in_cell+, +@covered+, +@depth+ (below the cell element), +@columns_repeated+, +@kind+ (from the
|
|
144
|
+
# value type), +@value+ and +@value_code+ (the value attribute and which one), +@cell_style+, +@error+
|
|
145
|
+
# (+calcext:value-type="error"+), +@missing+ (the reason of a typed cell without its value), +@text+,
|
|
146
|
+
# +@paragraphs+, +@paragraph+ (inside one), +@capture+ (the text is the value), +@ignored+ (the depth of an
|
|
147
|
+
# element of another namespace), +@spaces+ (the text:s spaces so far), +@space_count+ (the +text:c+ of the
|
|
148
|
+
# current text:s) and +@ignore_space+ (a space is dropped here: at the start of a paragraph, after a space).
|
|
149
|
+
def initialize(sheet:, prefixes:, cast:, types:, first_row:, start_row:, formats:, max_cell_bytes:, on_row:)
|
|
150
|
+
super()
|
|
151
|
+
@max_cell_bytes = max_cell_bytes
|
|
152
|
+
@sheet = sheet
|
|
153
|
+
@cast = cast
|
|
154
|
+
@types = types
|
|
155
|
+
@formats = formats.empty? ? nil : formats
|
|
156
|
+
# [first column, last column, format] of the columns whose default cell style has a format.
|
|
157
|
+
@column_formats = []
|
|
158
|
+
@columns = 0
|
|
159
|
+
@next_row = first_row
|
|
160
|
+
@start_row = start_row
|
|
161
|
+
@on_row = on_row
|
|
162
|
+
name_codes(prefixes)
|
|
163
|
+
@yielding = false
|
|
164
|
+
@element = nil
|
|
165
|
+
@in_row = false
|
|
166
|
+
@in_cell = false
|
|
167
|
+
@row_number = nil
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# @api private
|
|
171
|
+
# Inside a cell only its paragraphs count: +@depth+ is the depth below the cell element.
|
|
172
|
+
def start_element(name)
|
|
173
|
+
if @in_cell
|
|
174
|
+
@depth += 1
|
|
175
|
+
@element = nil
|
|
176
|
+
if @paragraph then start_in_paragraph(name)
|
|
177
|
+
elsif @depth == 1 && @elements[name] == PARAGRAPH then begin_paragraph
|
|
178
|
+
end
|
|
179
|
+
return
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
@element = code = @elements[name]
|
|
183
|
+
case code
|
|
184
|
+
when ROW then begin_row
|
|
185
|
+
when CELL, COVERED then begin_cell(code) if @in_row
|
|
186
|
+
when COLUMN then begin_column
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# @api private
|
|
191
|
+
def end_element(name)
|
|
192
|
+
if @in_cell
|
|
193
|
+
return finish_cell if @depth.zero?
|
|
194
|
+
|
|
195
|
+
end_in_paragraph if @paragraph
|
|
196
|
+
@element = nil
|
|
197
|
+
@depth -= 1
|
|
198
|
+
return
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
case @elements[name]
|
|
202
|
+
when ROW then finish_row if @in_row
|
|
203
|
+
when COLUMN then finish_column
|
|
204
|
+
when TABLE then throw STOP
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# @api private
|
|
209
|
+
# The attributes the parser reads have their references decoded where they are used: a value type when it is
|
|
210
|
+
# not a known one, the value of a cell when the cell ends (#raw_value), the others here. An attribute of
|
|
211
|
+
# another element than the one it belongs to is ignored, as are those of the table (Scanner reads its name).
|
|
212
|
+
def attr(name, value)
|
|
213
|
+
case @element
|
|
214
|
+
when CELL
|
|
215
|
+
case (code = @attributes[name])
|
|
216
|
+
when VALUE_TYPE then @kind = KINDS.fetch(value) { KINDS.fetch(plain(name, value), :text) }
|
|
217
|
+
when VALUE, DATE_VALUE, TIME_VALUE, BOOLEAN_VALUE, STRING_VALUE
|
|
218
|
+
@value = value
|
|
219
|
+
@value_code = code
|
|
220
|
+
when CALC_VALUE_TYPE then @error = value == "error" || (value.include?("&") && plain(name, value) == "error")
|
|
221
|
+
when COLUMNS_REPEATED then @columns_repeated = repeat(plain(name, value), @columns_repeated_name)
|
|
222
|
+
when STYLE_NAME then @cell_style = value.include?("&") ? plain(name, value) : value
|
|
223
|
+
end
|
|
224
|
+
when COVERED
|
|
225
|
+
@columns_repeated = repeat(plain(name, value), @columns_repeated_name) if @attributes[name] == COLUMNS_REPEATED
|
|
226
|
+
when ROW
|
|
227
|
+
case @attributes[name]
|
|
228
|
+
when ROWS_REPEATED then @rows_repeated = repeat(plain(name, value), @rows_repeated_name)
|
|
229
|
+
when DEFAULT_STYLE then @row_style = plain(name, value)
|
|
230
|
+
end
|
|
231
|
+
when COLUMN
|
|
232
|
+
case @attributes[name]
|
|
233
|
+
when COLUMNS_REPEATED then @column_repeated = repeat(plain(name, value), @columns_repeated_name)
|
|
234
|
+
when DEFAULT_STYLE then @column_style = plain(name, value)
|
|
235
|
+
end
|
|
236
|
+
when SPACES
|
|
237
|
+
@space_count = plain(name, value) if @attributes[name] == SPACE_COUNT
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# @api private
|
|
242
|
+
# The text of a paragraph, its references decoded unless it is a CDATA section, collapsed as ODF collapses
|
|
243
|
+
# white space.
|
|
244
|
+
def text(value, references = true)
|
|
245
|
+
return unless @capture && @paragraph && @ignored.nil?
|
|
246
|
+
|
|
247
|
+
value = text_references(value) if references && value.include?("&")
|
|
248
|
+
raise malformed("the text is not valid UTF-8", column_index: @column) unless value.valid_encoding?
|
|
249
|
+
|
|
250
|
+
if (@ignore_space && white?(value.getbyte(0))) || COLLAPSIBLE.match?(value)
|
|
251
|
+
# Like gsub(/[ \t\r\n]+/, " "), without the memory a regexp run costs per byte of a long one.
|
|
252
|
+
value = value.tr("\t\r\n", " ").squeeze(" ")
|
|
253
|
+
value = value.byteslice(1, value.bytesize - 1) if @ignore_space && value.getbyte(0) == SPACE_BYTE
|
|
254
|
+
return if value.empty?
|
|
255
|
+
end
|
|
256
|
+
@ignore_space = value.getbyte(-1) == SPACE_BYTE
|
|
257
|
+
append(value)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# @api private
|
|
261
|
+
# A CDATA section of a paragraph, taken as it is.
|
|
262
|
+
def cdata(value)
|
|
263
|
+
text(value, false)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
private
|
|
267
|
+
|
|
268
|
+
def text_references(value)
|
|
269
|
+
References.decode(value)
|
|
270
|
+
rescue References::Invalid => error
|
|
271
|
+
raise malformed("the text holds #{error.message}", column_index: @column)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# The value of an attribute with its references decoded. The column is the cell's (or the next cell's, for a
|
|
275
|
+
# row or a table-column).
|
|
276
|
+
def plain(name, value)
|
|
277
|
+
return value unless value.include?("&")
|
|
278
|
+
|
|
279
|
+
References.decode(value)
|
|
280
|
+
rescue References::Invalid => error
|
|
281
|
+
raise malformed("the attribute #{name} holds #{error.message}", column_index: (@column if @in_row))
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def name_codes(prefixes)
|
|
285
|
+
table = Odf.qualifier(prefixes[:table])
|
|
286
|
+
office = Odf.qualifier(prefixes[:office])
|
|
287
|
+
text = Odf.qualifier(prefixes[:text])
|
|
288
|
+
calcext = Odf.qualifier(prefixes[:calcext])
|
|
289
|
+
@text_qualifier = text
|
|
290
|
+
@elements = {
|
|
291
|
+
"#{table}table": TABLE, "#{table}table-row": ROW, "#{table}table-cell": CELL,
|
|
292
|
+
"#{table}covered-table-cell": COVERED, "#{table}table-column": COLUMN, "#{text}p": PARAGRAPH, "#{text}s": SPACES,
|
|
293
|
+
"#{text}tab": TAB, "#{text}line-break": LINE_BREAK
|
|
294
|
+
}
|
|
295
|
+
@attributes = {
|
|
296
|
+
"#{table}number-rows-repeated": ROWS_REPEATED, "#{table}number-columns-repeated": COLUMNS_REPEATED,
|
|
297
|
+
"#{office}value-type": VALUE_TYPE, "#{office}value": VALUE, "#{office}date-value": DATE_VALUE,
|
|
298
|
+
"#{office}time-value": TIME_VALUE, "#{office}boolean-value": BOOLEAN_VALUE,
|
|
299
|
+
"#{office}string-value": STRING_VALUE, "#{calcext}value-type": CALC_VALUE_TYPE, "#{text}c": SPACE_COUNT,
|
|
300
|
+
"#{table}style-name": STYLE_NAME, "#{table}default-cell-style-name": DEFAULT_STYLE
|
|
301
|
+
}
|
|
302
|
+
@value_names = @attributes.invert.slice(VALUE, DATE_VALUE, TIME_VALUE, BOOLEAN_VALUE, STRING_VALUE)
|
|
303
|
+
@rows_repeated_name = Odf.qualified(prefixes[:table], "number-rows-repeated")
|
|
304
|
+
@columns_repeated_name = Odf.qualified(prefixes[:table], "number-columns-repeated")
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# --- columns --------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
def begin_column
|
|
310
|
+
@column_repeated = 1
|
|
311
|
+
@column_style = nil
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Notes the columns of the element when their default cell style has a format; columns beyond the last one
|
|
315
|
+
# cannot hold cells.
|
|
316
|
+
def finish_column
|
|
317
|
+
first = @columns
|
|
318
|
+
@columns += @column_repeated
|
|
319
|
+
return unless @formats && @column_style && first < Grid::MAX_COLUMNS
|
|
320
|
+
|
|
321
|
+
format = @formats[@column_style] or return
|
|
322
|
+
@column_formats << [ first, @columns - 1, format ]
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# @return [Array(Symbol, Integer | nil), nil] the format of the default cell style of the column
|
|
326
|
+
def column_format(column)
|
|
327
|
+
range = @column_formats.bsearch { |entry| entry[1] >= column }
|
|
328
|
+
range[2] if range && range[0] <= column
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# @return [Array(Symbol, Integer | nil), nil] the format of the cell at +column+: of its own style, else of the
|
|
332
|
+
# default style of its row, else of its column
|
|
333
|
+
def format_at(column)
|
|
334
|
+
return unless @formats
|
|
335
|
+
|
|
336
|
+
style = @cell_style || @row_style
|
|
337
|
+
style ? @formats[style] : column_format(column)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# --- rows -----------------------------------------------------------------------------
|
|
341
|
+
|
|
342
|
+
def begin_row
|
|
343
|
+
@in_row = true
|
|
344
|
+
@row_number = @next_row
|
|
345
|
+
@row_style = nil
|
|
346
|
+
@rows_repeated = 1
|
|
347
|
+
@values = []
|
|
348
|
+
@errors = nil
|
|
349
|
+
@last = -1
|
|
350
|
+
@column = 0
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def finish_row
|
|
354
|
+
@in_row = false
|
|
355
|
+
number = @row_number
|
|
356
|
+
last_number = number + @rows_repeated - 1
|
|
357
|
+
@next_row = last_number + 1
|
|
358
|
+
return if @last.negative? || last_number < @start_row
|
|
359
|
+
raise malformed("the row is beyond the last row #{Grid::MAX_ROWS}", row: number) if last_number > Grid::MAX_ROWS
|
|
360
|
+
|
|
361
|
+
trim_row!
|
|
362
|
+
[ number, @start_row ].max.upto(last_number) { |row| emit(row, last: row == last_number) }
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Yields the row numbered +number+; every row of a repeated run but the last gets copies of the arrays.
|
|
366
|
+
def emit(number, last:)
|
|
367
|
+
yield_row(number, last ? @values : @values.dup, last || @errors.nil? ? @errors : @errors.dup)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# --- cells ----------------------------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
def begin_cell(code)
|
|
373
|
+
@in_cell = true
|
|
374
|
+
@covered = code == COVERED
|
|
375
|
+
@depth = 0
|
|
376
|
+
@columns_repeated = 1
|
|
377
|
+
@kind = :text
|
|
378
|
+
@value = @value_code = nil
|
|
379
|
+
@cell_style = nil
|
|
380
|
+
@error = false
|
|
381
|
+
@text = nil
|
|
382
|
+
@paragraphs = 0
|
|
383
|
+
@paragraph = false
|
|
384
|
+
@capture = false
|
|
385
|
+
@ignored = nil
|
|
386
|
+
@spaces = 0
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# An element inside a paragraph. +@ignored+ is the depth of an element of another namespace, whose content
|
|
390
|
+
# is skipped.
|
|
391
|
+
def start_in_paragraph(name)
|
|
392
|
+
return if @ignored
|
|
393
|
+
|
|
394
|
+
code = @elements[name]
|
|
395
|
+
if code == SPACES || code == TAB || code == LINE_BREAK
|
|
396
|
+
@element = code
|
|
397
|
+
@space_count = nil
|
|
398
|
+
elsif code.nil? && !text_element?(name)
|
|
399
|
+
@ignored = @depth
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def end_in_paragraph
|
|
404
|
+
if @ignored
|
|
405
|
+
@ignored = nil if @ignored == @depth
|
|
406
|
+
elsif @depth == 1
|
|
407
|
+
@paragraph = false
|
|
408
|
+
elsif @capture
|
|
409
|
+
control(@element)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def text_element?(name)
|
|
414
|
+
@text_qualifier.empty? || name.start_with?(@text_qualifier)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def begin_paragraph
|
|
418
|
+
@paragraph = true
|
|
419
|
+
@capture = wants_text? if @paragraphs.zero?
|
|
420
|
+
@paragraphs += 1
|
|
421
|
+
return unless @capture
|
|
422
|
+
|
|
423
|
+
append(+"\n") if @paragraphs > 1
|
|
424
|
+
@ignore_space = true
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Whether the value of the cell is its text: no value attribute of its kind.
|
|
428
|
+
def wants_text?
|
|
429
|
+
!@covered && (@error || @value_code.nil? || @value_code != VALUE_OF[@kind])
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# The end of a text:s, text:tab or text:line-break element of a paragraph.
|
|
433
|
+
def control(code)
|
|
434
|
+
case code
|
|
435
|
+
when SPACES
|
|
436
|
+
count = Count.parse(@space_count) || 1
|
|
437
|
+
@spaces += count
|
|
438
|
+
raise malformed("the text has more than #{Odf::MAX_SPACES} spaces", column_index: @column) if @spaces > Odf::MAX_SPACES
|
|
439
|
+
|
|
440
|
+
append(" " * count)
|
|
441
|
+
when TAB then append(+"\t")
|
|
442
|
+
when LINE_BREAK then append(+"\n")
|
|
443
|
+
else return
|
|
444
|
+
end
|
|
445
|
+
@ignore_space = false
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# The white space of XML: space, tab (9), line feed (10) and carriage return (13).
|
|
449
|
+
def white?(byte)
|
|
450
|
+
byte == SPACE_BYTE || byte == 9 || byte == 10 || byte == 13
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Measured before it is joined: a text longer than max_cell_bytes is never built.
|
|
454
|
+
def append(text)
|
|
455
|
+
if @text
|
|
456
|
+
raise text_too_long if @max_cell_bytes && @text.bytesize + text.bytesize > @max_cell_bytes
|
|
457
|
+
|
|
458
|
+
@text << text
|
|
459
|
+
else
|
|
460
|
+
raise text_too_long if @max_cell_bytes && text.bytesize > @max_cell_bytes
|
|
461
|
+
|
|
462
|
+
@text = text
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def text_too_long
|
|
467
|
+
malformed("the text is longer than max_cell_bytes (#{@max_cell_bytes} bytes)", column_index: @column)
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def finish_cell
|
|
471
|
+
@in_cell = false
|
|
472
|
+
column = @column
|
|
473
|
+
raw = raw_value unless @covered
|
|
474
|
+
repeat = @columns_repeated
|
|
475
|
+
@column = column + repeat
|
|
476
|
+
return if raw.nil?
|
|
477
|
+
|
|
478
|
+
raise malformed("a cell is beyond the last column #{Grid::LAST_COLUMN}") if @column > Grid::MAX_COLUMNS
|
|
479
|
+
|
|
480
|
+
if repeat == 1
|
|
481
|
+
begin
|
|
482
|
+
@values[column] = cast(raw, @types && @types[column], format_of(column))
|
|
483
|
+
rescue Cast::Failure => failure
|
|
484
|
+
(@errors ||= []) << [ column, raw, failure.reason ]
|
|
485
|
+
end
|
|
486
|
+
else
|
|
487
|
+
place(raw, column)
|
|
488
|
+
end
|
|
489
|
+
@last = @column - 1 if (@kind != :text || !blank?(raw)) && @column - 1 > @last
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# Casts the value of a repeated cell into each of its columns, once per declared type and format.
|
|
493
|
+
def place(raw, column)
|
|
494
|
+
type = format = value = failure = nil
|
|
495
|
+
column.upto(@column - 1) do |index|
|
|
496
|
+
wanted = @types && @types[index]
|
|
497
|
+
styled = format_of(index)
|
|
498
|
+
# Identity, not equality: the formats are the frozen Arrays of the +formats+ Hash, so the same style gives
|
|
499
|
+
# the same object, and a check by identity costs nothing per column. An equal format of another style only
|
|
500
|
+
# casts once more.
|
|
501
|
+
if index == column || wanted != type || !styled.equal?(format)
|
|
502
|
+
type = wanted
|
|
503
|
+
format = styled
|
|
504
|
+
begin
|
|
505
|
+
failure = nil
|
|
506
|
+
value = cast(raw, wanted, styled)
|
|
507
|
+
rescue Cast::Failure => error
|
|
508
|
+
failure = error
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
if failure then (@errors ||= []) << [ index, raw, failure.reason ]
|
|
512
|
+
else @values[index] = value
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# The raw value of the cell; leaves its kind in +@kind+: :text (strings), :number, :date, :time, :boolean,
|
|
518
|
+
# :error, or :missing for a typed cell without its value attribute.
|
|
519
|
+
# @return [String, nil] nil for an empty cell
|
|
520
|
+
def raw_value
|
|
521
|
+
kind = @kind
|
|
522
|
+
if @error
|
|
523
|
+
@kind = :error
|
|
524
|
+
utf8(@text || "")
|
|
525
|
+
elsif @value_code && @value_code == VALUE_OF[kind]
|
|
526
|
+
value = @value.include?("&") ? plain(@value_names[@value_code], @value) : @value
|
|
527
|
+
return utf8(number_value(value)) unless kind == :string
|
|
528
|
+
|
|
529
|
+
@kind = :text
|
|
530
|
+
raise text_too_long if @max_cell_bytes && value.bytesize > @max_cell_bytes
|
|
531
|
+
|
|
532
|
+
utf8(value) unless value.empty?
|
|
533
|
+
elsif kind == :text || kind == :string
|
|
534
|
+
@kind = :text
|
|
535
|
+
utf8(@text) if @text && !@text.empty?
|
|
536
|
+
else
|
|
537
|
+
@missing = MISSING.fetch(kind, :invalid_decimal)
|
|
538
|
+
@kind = :missing
|
|
539
|
+
utf8(@text || "")
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def number_value(value)
|
|
544
|
+
return value if value.bytesize <= Cast::MAX_NUMBER_BYTES
|
|
545
|
+
|
|
546
|
+
raise malformed("the value is longer than #{Cast::MAX_NUMBER_BYTES} bytes", column_index: @column)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# The format only matters to numbers and dates.
|
|
550
|
+
def format_of(column)
|
|
551
|
+
format_at(column) if @kind == :number || @kind == :date
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def cast(raw, type, format)
|
|
555
|
+
case @kind
|
|
556
|
+
when :text then @cast.text(raw, type)
|
|
557
|
+
when :number then @cast.number(raw, :general, type, pad: format && format[1])
|
|
558
|
+
when :date then @cast.iso_date(raw, type, format && format[0])
|
|
559
|
+
when :time then @cast.duration(raw, type)
|
|
560
|
+
when :boolean then @cast.boolean(boolean(raw), type)
|
|
561
|
+
when :error then raise Cast::Failure.new(:cell_error, raw)
|
|
562
|
+
else raise Cast::Failure.new(@missing, raw)
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
# Ox tags the values as UTF-8, as the stream declares; it does not check the bytes.
|
|
567
|
+
def utf8(text)
|
|
568
|
+
return text if text.valid_encoding?
|
|
569
|
+
|
|
570
|
+
raise malformed("the text is not valid UTF-8", column_index: @column)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def repeat(value, name)
|
|
574
|
+
count = Count.parse(value)
|
|
575
|
+
return count if count&.positive?
|
|
576
|
+
|
|
577
|
+
raise malformed("#{name} must be a positive number, got #{Untrusted.show(value)}")
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
# The row being read when it is known, else the last row read.
|
|
581
|
+
def current_row
|
|
582
|
+
return @row_number if @in_row
|
|
583
|
+
|
|
584
|
+
@next_row > 1 && @row_number ? @next_row - 1 : nil
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
# The column of the cell being read.
|
|
588
|
+
def current_column
|
|
589
|
+
@column
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Calcsheet
|
|
4
|
+
module Readers
|
|
5
|
+
module Ods
|
|
6
|
+
# The counts written in ODS attributes (repeats, spaces, digits of a number style), parsed alike by Scanner
|
|
7
|
+
# and ContentParser, so that a file reads the same from its first row and from +start_at:+.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module Count
|
|
11
|
+
# More digits are not a count Calcsheet can use: a repeat or a number of spaces is invalid beyond them. 18
|
|
12
|
+
# digits stay below 2**62, so a count is always an immediate Integer (no Bignum) and the arithmetic on row and
|
|
13
|
+
# column numbers stays cheap; every real count (rows, columns, spaces) has far fewer digits.
|
|
14
|
+
MAX_DIGITS = 18
|
|
15
|
+
# A byte that is not an ASCII digit.
|
|
16
|
+
NOT_DIGIT = /[^0-9]/
|
|
17
|
+
|
|
18
|
+
# @param value [String, nil] an attribute value
|
|
19
|
+
# @return [Integer, nil] the number written in +value+: ASCII digits only, at most MAX_DIGITS of them; nil
|
|
20
|
+
# otherwise. A search for another character, not an anchored /\A\d+\z/, whose backtracking costs memory in
|
|
21
|
+
# proportion to the length of the value.
|
|
22
|
+
def self.parse(value)
|
|
23
|
+
return unless value && !value.empty? && value.bytesize <= MAX_DIGITS && value.valid_encoding?
|
|
24
|
+
|
|
25
|
+
value.to_i unless NOT_DIGIT.match?(value)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|