roo-oxide 0.0.2
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/CHANGELOG.md +63 -0
- data/Cargo.lock +341 -0
- data/Cargo.toml +7 -0
- data/LICENSE +21 -0
- data/README.md +220 -0
- data/ext/roo_oxide/Cargo.toml +19 -0
- data/ext/roo_oxide/extconf.rb +14 -0
- data/ext/roo_oxide/src/archive.rs +546 -0
- data/ext/roo_oxide/src/convert/cells.rs +516 -0
- data/ext/roo_oxide/src/convert/csv.rs +209 -0
- data/ext/roo_oxide/src/convert.rs +687 -0
- data/ext/roo_oxide/src/lib.rs +306 -0
- data/ext/roo_oxide/src/numeric.rs +174 -0
- data/ext/roo_oxide/src/reader/attributes.rs +199 -0
- data/ext/roo_oxide/src/reader/tests.rs +475 -0
- data/ext/roo_oxide/src/reader.rs +944 -0
- data/ext/roo_oxide/src/shared_strings.rs +684 -0
- data/ext/roo_oxide/src/sheet_parts.rs +312 -0
- data/ext/roo_oxide/src/store/formatters.rs +177 -0
- data/ext/roo_oxide/src/store.rs +894 -0
- data/lib/roo/calamine/version.rb +5 -0
- data/lib/roo/calamine.rb +3 -0
- data/lib/roo/oxide/context.rb +74 -0
- data/lib/roo/oxide/csv.rb +157 -0
- data/lib/roo/oxide/deferred_sheets.rb +284 -0
- data/lib/roo/oxide/excelx.rb +156 -0
- data/lib/roo/oxide/formatters.rb +114 -0
- data/lib/roo/oxide/row_index.rb +93 -0
- data/lib/roo/oxide/row_iteration.rb +33 -0
- data/lib/roo/oxide/shared_strings.rb +46 -0
- data/lib/roo/oxide/sheet.rb +99 -0
- data/lib/roo/oxide/sheet_doc.rb +138 -0
- data/lib/roo/oxide/sheet_parts.rb +78 -0
- data/lib/roo/oxide/version.rb +7 -0
- data/lib/roo/oxide.rb +100 -0
- metadata +110 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
//! Builds the `Roo::Excelx::Cell` objects that
|
|
2
|
+
//! `Roo::Excelx::SheetDoc#cell_from_xml` builds ([`cells`]), or only the
|
|
3
|
+
//! values those cells return from `#value`.
|
|
4
|
+
|
|
5
|
+
mod cells;
|
|
6
|
+
mod csv;
|
|
7
|
+
|
|
8
|
+
pub use csv::CsvOutput;
|
|
9
|
+
|
|
10
|
+
use std::cell::{Cell as StdCell, RefCell};
|
|
11
|
+
use std::collections::HashMap;
|
|
12
|
+
use std::os::raw::c_int;
|
|
13
|
+
|
|
14
|
+
use magnus::encoding::{self, EncodingCapable};
|
|
15
|
+
use magnus::gc::Marker;
|
|
16
|
+
use magnus::rb_sys::{protect, AsRawValue, FromRawValue};
|
|
17
|
+
use magnus::value::{Lazy, LazyId, Opaque, ReprValue};
|
|
18
|
+
use magnus::{
|
|
19
|
+
prelude::*, DataTypeFunctions, Error, IntoValue, RArray, RClass, RHash, RString, Ruby,
|
|
20
|
+
StaticSymbol, Symbol, TypedData, Value,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
use crate::numeric::{create_numeric, plain_number};
|
|
24
|
+
use crate::reader::{parse_index, Body, Kind, Mode, RawCell, RawRow, Reference, Style};
|
|
25
|
+
use crate::shared_strings::{has_html_tag, HtmlSelection, HtmlStrings};
|
|
26
|
+
|
|
27
|
+
fn constant<T: magnus::TryConvert>(ruby: &Ruby, path: &str) -> T {
|
|
28
|
+
ruby.eval(path).expect("roo is loaded before roo/oxide")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static COORDINATE: Lazy<RClass> = Lazy::new(|r| constant(r, "Roo::Excelx::Coordinate"));
|
|
32
|
+
static CELL_STRING: Lazy<RClass> = Lazy::new(|r| constant(r, "Roo::Excelx::Cell::String"));
|
|
33
|
+
static CELL_BOOLEAN: Lazy<RClass> = Lazy::new(|r| constant(r, "Roo::Excelx::Cell::Boolean"));
|
|
34
|
+
static CELL_NUMBER: Lazy<RClass> = Lazy::new(|r| constant(r, "Roo::Excelx::Cell::Number"));
|
|
35
|
+
static CELL_DATE: Lazy<RClass> = Lazy::new(|r| constant(r, "Roo::Excelx::Cell::Date"));
|
|
36
|
+
static CELL_DATETIME: Lazy<RClass> = Lazy::new(|r| constant(r, "Roo::Excelx::Cell::DateTime"));
|
|
37
|
+
static CELL_TIME: Lazy<RClass> = Lazy::new(|r| constant(r, "Roo::Excelx::Cell::Time"));
|
|
38
|
+
static CELL_EMPTY: Lazy<RClass> = Lazy::new(|r| constant(r, "Roo::Excelx::Cell::Empty"));
|
|
39
|
+
static NUMERIC_OR_FORMULA: Lazy<StaticSymbol> = Lazy::new(|r| r.sym_new("numeric_or_formula"));
|
|
40
|
+
static TYPE_STRING: LazyId = LazyId::new("string");
|
|
41
|
+
static TYPE_BOOLEAN: LazyId = LazyId::new("boolean");
|
|
42
|
+
static TYPE_FLOAT: LazyId = LazyId::new("float");
|
|
43
|
+
static TYPE_DATE: LazyId = LazyId::new("date");
|
|
44
|
+
static TYPE_FORMULA: LazyId = LazyId::new("formula");
|
|
45
|
+
|
|
46
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
47
|
+
enum ValueType {
|
|
48
|
+
Date,
|
|
49
|
+
DateTime,
|
|
50
|
+
Time,
|
|
51
|
+
/// `:float`, `:percentage`, and any other format type roo builds a
|
|
52
|
+
/// `Cell::Number` for.
|
|
53
|
+
Number,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// How `Cell::Number#create_numeric` reads text under a format.
|
|
57
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
58
|
+
enum NumberFormat {
|
|
59
|
+
/// The format matches `/%/` or `/\.0/`: always `Float(text)`.
|
|
60
|
+
Float,
|
|
61
|
+
/// The text decides between `Float` and `Integer`.
|
|
62
|
+
ByText,
|
|
63
|
+
/// Matching the format raises, so roo's constructor runs.
|
|
64
|
+
Unmatchable,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// What a style index means for a value cell, kept with the text and encoding
|
|
68
|
+
/// of its format as first read.
|
|
69
|
+
#[derive(Clone)]
|
|
70
|
+
struct StyleInfo {
|
|
71
|
+
format_text: Vec<u8>,
|
|
72
|
+
format_encoding: encoding::Index,
|
|
73
|
+
value_type: ValueType,
|
|
74
|
+
number_format: NumberFormat,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl StyleInfo {
|
|
78
|
+
/// Whether `format` still has the text and encoding this was read from.
|
|
79
|
+
fn read_from(&self, format: Value) -> bool {
|
|
80
|
+
RString::from_value(format).is_some_and(|text| {
|
|
81
|
+
text.enc_get() == self.format_encoding && unsafe { text.as_slice() } == self.format_text
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// `/\A[-+]?\d+E[-+]?\d+\z/i`, the exponent test in `Cell::Number#create_numeric`.
|
|
87
|
+
fn is_exponent_number(text: &str) -> bool {
|
|
88
|
+
let digits = |part: &str| {
|
|
89
|
+
let part = part.strip_prefix(['-', '+']).unwrap_or(part);
|
|
90
|
+
!part.is_empty() && part.bytes().all(|b| b.is_ascii_digit())
|
|
91
|
+
};
|
|
92
|
+
match text.split_once(['E', 'e']) {
|
|
93
|
+
Some((mantissa, exponent)) => digits(mantissa) && digits(exponent),
|
|
94
|
+
None => false,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Ruby's `Integer(text, 10)` when `badcheck`, else `text.to_i`: the C
|
|
99
|
+
/// function both call, on a String, so errors read as Ruby's do.
|
|
100
|
+
fn ruby_integer(ruby: &Ruby, text: &str, badcheck: bool) -> Result<Value, Error> {
|
|
101
|
+
let text = ruby.str_new(text);
|
|
102
|
+
let raw =
|
|
103
|
+
protect(|| unsafe { rb_sys::rb_str_to_inum(text.as_raw(), 10, c_int::from(badcheck)) })?;
|
|
104
|
+
Ok(unsafe { Value::from_raw(raw) })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Ruby's `Float(text)`: the C function it calls, on a String.
|
|
108
|
+
fn ruby_float(ruby: &Ruby, text: &str) -> Result<Value, Error> {
|
|
109
|
+
let text = ruby.str_new(text);
|
|
110
|
+
let mut number = 0.0;
|
|
111
|
+
protect(|| {
|
|
112
|
+
number = unsafe { rb_sys::rb_str_to_dbl(text.as_raw(), 1) };
|
|
113
|
+
rb_sys::Qnil as rb_sys::VALUE
|
|
114
|
+
})?;
|
|
115
|
+
Ok(ruby.float_from_f64(number).as_value())
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Turns raw rows into roo cells for one worksheet. The Ruby objects it keeps
|
|
119
|
+
/// are pinned by [`DataTypeFunctions::mark`]; caches that may be filled while
|
|
120
|
+
/// Ruby runs live in `Cell`/`RefCell` fields that `mark` does not borrow.
|
|
121
|
+
#[derive(TypedData)]
|
|
122
|
+
#[magnus(class = "Roo::Oxide::Native::Converter", mark, free_immediately, size)]
|
|
123
|
+
pub struct Converter {
|
|
124
|
+
mode: Mode,
|
|
125
|
+
empty_cell: bool,
|
|
126
|
+
/// Whether this roo's cells may be built without their constructors.
|
|
127
|
+
build_cells: bool,
|
|
128
|
+
context: Opaque<Value>,
|
|
129
|
+
hyperlinks: Option<Opaque<RHash>>,
|
|
130
|
+
/// Document reads snapshot hyperlinks; streams keep roo's live hash.
|
|
131
|
+
link_index: Option<HashMap<(i64, i64), Opaque<Value>>>,
|
|
132
|
+
base_date: Opaque<Value>,
|
|
133
|
+
base_timestamp: Opaque<Value>,
|
|
134
|
+
/// `base_timestamp`, when it is an integer a double holds exactly.
|
|
135
|
+
base_seconds: Option<i64>,
|
|
136
|
+
/// Format string per style index, filled on first use.
|
|
137
|
+
formats: Opaque<RArray>,
|
|
138
|
+
styles: RefCell<Vec<Option<StyleInfo>>>,
|
|
139
|
+
/// roo's plain shared-string array and deferred or public HTML table
|
|
140
|
+
/// (`nil` when the HTML wrapper is disabled). Fetched on first use.
|
|
141
|
+
shared_plain: StdCell<Option<Opaque<RArray>>>,
|
|
142
|
+
shared_html: StdCell<Option<Opaque<Value>>>,
|
|
143
|
+
/// `Roo::Excelx::ERROR_VALUES`, which `Cell::Number` keeps as text.
|
|
144
|
+
error_values: Vec<String>,
|
|
145
|
+
/// Document mode: the string picked for each shared-string index. roo
|
|
146
|
+
/// picks every cell's string in one pass over the sheet, with no Ruby
|
|
147
|
+
/// code in between, so one pick per index gives the cells it gives, even
|
|
148
|
+
/// when a caller later changes a picked string in place.
|
|
149
|
+
picked: Option<Opaque<RHash>>,
|
|
150
|
+
picked_indices: StdCell<Option<Opaque<RArray>>>,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
impl DataTypeFunctions for Converter {
|
|
154
|
+
fn mark(&self, marker: &Marker) {
|
|
155
|
+
marker.mark(self.context);
|
|
156
|
+
if let Some(hyperlinks) = self.hyperlinks {
|
|
157
|
+
marker.mark(hyperlinks);
|
|
158
|
+
}
|
|
159
|
+
if let Some(index) = &self.link_index {
|
|
160
|
+
for value in index.values() {
|
|
161
|
+
marker.mark(*value);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
marker.mark(self.base_date);
|
|
165
|
+
marker.mark(self.base_timestamp);
|
|
166
|
+
marker.mark(self.formats);
|
|
167
|
+
if let Some(plain) = self.shared_plain.get() {
|
|
168
|
+
marker.mark(plain);
|
|
169
|
+
}
|
|
170
|
+
if let Some(html) = self.shared_html.get() {
|
|
171
|
+
marker.mark(html);
|
|
172
|
+
}
|
|
173
|
+
if let Some(picked) = self.picked {
|
|
174
|
+
marker.mark(picked);
|
|
175
|
+
}
|
|
176
|
+
if let Some(picked) = self.picked_indices.get() {
|
|
177
|
+
marker.mark(picked);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
impl Converter {
|
|
183
|
+
pub fn formatter_type(
|
|
184
|
+
&self,
|
|
185
|
+
ruby: &Ruby,
|
|
186
|
+
raw: &RawCell,
|
|
187
|
+
) -> Result<Option<&'static LazyId>, Error> {
|
|
188
|
+
let Reference::At { row, column } = raw.reference else {
|
|
189
|
+
return Ok(None);
|
|
190
|
+
};
|
|
191
|
+
if !self.build_cells || self.has_link(ruby, row, column) {
|
|
192
|
+
return Ok(None);
|
|
193
|
+
}
|
|
194
|
+
let kind = match (&raw.body, raw.kind) {
|
|
195
|
+
(Body::Empty, _) => return Ok(None),
|
|
196
|
+
(Body::Inline(_), _) | (Body::Value(_), Kind::Shared | Kind::Str) => &TYPE_STRING,
|
|
197
|
+
(Body::Value(_), Kind::Boolean) => &TYPE_BOOLEAN,
|
|
198
|
+
(Body::Value(_), Kind::InlineStr) => &TYPE_FLOAT,
|
|
199
|
+
(Body::Value(_), Kind::ByFormat) => {
|
|
200
|
+
match self.style_info(ruby, self.style(ruby, raw)?)?.1 {
|
|
201
|
+
ValueType::Number => &TYPE_FLOAT,
|
|
202
|
+
ValueType::Date => &TYPE_DATE,
|
|
203
|
+
ValueType::DateTime | ValueType::Time => return Ok(None),
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
Ok(Some(if raw.formula.is_some() {
|
|
208
|
+
&TYPE_FORMULA
|
|
209
|
+
} else {
|
|
210
|
+
kind
|
|
211
|
+
}))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// `context` is a `Roo::Oxide::Context`.
|
|
215
|
+
pub fn new(ruby: &Ruby, context: Value, stream: bool) -> Result<Self, Error> {
|
|
216
|
+
let hyperlinks: RHash = context.funcall("hyperlinks", ())?;
|
|
217
|
+
let base_timestamp: Value = context.funcall("base_timestamp", ())?;
|
|
218
|
+
let mut link_index = (!stream).then(HashMap::new);
|
|
219
|
+
hyperlinks.foreach(|key: Value, value: Value| {
|
|
220
|
+
if let Some(index) = &mut link_index {
|
|
221
|
+
let key = RArray::from_value(key).filter(|key| key.len() == 2);
|
|
222
|
+
let coordinate = key.and_then(|key| {
|
|
223
|
+
let row = magnus::Integer::from_value(key.entry(0).ok()?)?;
|
|
224
|
+
let column = magnus::Integer::from_value(key.entry(1).ok()?)?;
|
|
225
|
+
Some((
|
|
226
|
+
i64::try_convert(row.as_value()).ok()?,
|
|
227
|
+
i64::try_convert(column.as_value()).ok()?,
|
|
228
|
+
))
|
|
229
|
+
});
|
|
230
|
+
match coordinate {
|
|
231
|
+
Some(key) => {
|
|
232
|
+
index.insert(key, value.into());
|
|
233
|
+
}
|
|
234
|
+
None => link_index = None,
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
238
|
+
})?;
|
|
239
|
+
Ok(Self {
|
|
240
|
+
mode: if stream { Mode::Stream } else { Mode::Document },
|
|
241
|
+
empty_cell: context.funcall::<_, _, Value>("empty_cell", ())?.to_bool(),
|
|
242
|
+
build_cells: context
|
|
243
|
+
.funcall::<_, _, Value>("build_cells?", ())?
|
|
244
|
+
.to_bool(),
|
|
245
|
+
context: context.into(),
|
|
246
|
+
hyperlinks: (!hyperlinks.is_empty()).then(|| hyperlinks.into()),
|
|
247
|
+
link_index,
|
|
248
|
+
base_date: context.funcall::<_, _, Value>("base_date", ())?.into(),
|
|
249
|
+
base_timestamp: base_timestamp.into(),
|
|
250
|
+
base_seconds: i64::try_convert(base_timestamp)
|
|
251
|
+
.ok()
|
|
252
|
+
.filter(|seconds| seconds.unsigned_abs() < 1 << 53),
|
|
253
|
+
formats: ruby.ary_new().into(),
|
|
254
|
+
styles: RefCell::new(Vec::new()),
|
|
255
|
+
shared_plain: StdCell::new(None),
|
|
256
|
+
shared_html: StdCell::new(None),
|
|
257
|
+
error_values: ruby.eval("Roo::Excelx::ERROR_VALUES.to_a")?,
|
|
258
|
+
picked: (!stream).then(|| ruby.hash_new().into()),
|
|
259
|
+
picked_indices: StdCell::new(None),
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// `Roo::Oxide.each_row_values` for a streamed row: the `value` of each
|
|
264
|
+
/// cell that `Sheet#each_row(pad_cells: true)` yields, and `nil` for each
|
|
265
|
+
/// cell it pads.
|
|
266
|
+
pub fn row_values(&self, ruby: &Ruby, row: &RawRow) -> Result<RArray, Error> {
|
|
267
|
+
let values = ruby.ary_new_capa(row.cells.len());
|
|
268
|
+
let mut last_column = 0;
|
|
269
|
+
for raw in &row.cells {
|
|
270
|
+
let (column, value) = self.value(ruby, row, raw)?;
|
|
271
|
+
for _ in last_column + 1..column {
|
|
272
|
+
values.push(ruby.qnil())?;
|
|
273
|
+
}
|
|
274
|
+
values.push(value)?;
|
|
275
|
+
last_column = column;
|
|
276
|
+
}
|
|
277
|
+
Ok(values)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/// The column of a streamed cell and its `value`. A cell with a
|
|
281
|
+
/// hyperlink, a DateTime, or a Time is built as a roo cell and asked for
|
|
282
|
+
/// its value; every other value follows roo's rule without a cell.
|
|
283
|
+
fn value(&self, ruby: &Ruby, row: &RawRow, raw: &RawCell) -> Result<(i64, Value), Error> {
|
|
284
|
+
let column = match (&raw.reference, self.hyperlinks) {
|
|
285
|
+
(Reference::At { column, .. }, None) => *column,
|
|
286
|
+
_ => {
|
|
287
|
+
let coordinate = self.coordinate(ruby, raw, 0, 0)?;
|
|
288
|
+
let column = coordinate.funcall("column", ())?;
|
|
289
|
+
if !self.link(ruby, coordinate).is_nil() {
|
|
290
|
+
return Ok((column, self.cell_value(ruby, row, raw)?));
|
|
291
|
+
}
|
|
292
|
+
column
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
let text = match &raw.body {
|
|
296
|
+
Body::Empty => return Ok((column, ruby.qnil().as_value())),
|
|
297
|
+
Body::Inline(text) => return Ok((column, ruby.str_new(row.text(text)).as_value())),
|
|
298
|
+
Body::Value(text) => row.text(text),
|
|
299
|
+
};
|
|
300
|
+
let (_, format_type, number_format) = self.style_info(ruby, self.style(ruby, raw)?)?;
|
|
301
|
+
let value = match (raw.kind, format_type) {
|
|
302
|
+
(Kind::Shared, _) => self.shared_string(ruby, text)?,
|
|
303
|
+
(Kind::Boolean, _) => {
|
|
304
|
+
let one = i64::try_convert(ruby_integer(ruby, text, false)?).ok() == Some(1);
|
|
305
|
+
one.into_value_with(ruby)
|
|
306
|
+
}
|
|
307
|
+
(Kind::Str, _) => ruby.str_new(text).as_value(),
|
|
308
|
+
(Kind::InlineStr, _) | (Kind::ByFormat, ValueType::Number) => match number_format {
|
|
309
|
+
NumberFormat::Float => self.number(ruby, text, true)?,
|
|
310
|
+
NumberFormat::ByText => self.number(ruby, text, false)?,
|
|
311
|
+
NumberFormat::Unmatchable => self.cell_value(ruby, row, raw)?,
|
|
312
|
+
},
|
|
313
|
+
(Kind::ByFormat, ValueType::Date) => ruby
|
|
314
|
+
.get_inner(self.base_date)
|
|
315
|
+
.funcall("+", (ruby_integer(ruby, text, false)?,))?,
|
|
316
|
+
(Kind::ByFormat, ValueType::DateTime | ValueType::Time) => {
|
|
317
|
+
self.cell_value(ruby, row, raw)?
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
Ok((column, value))
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/// `Cell::Number#create_numeric`.
|
|
324
|
+
fn number(&self, ruby: &Ruby, text: &str, float_format: bool) -> Result<Value, Error> {
|
|
325
|
+
if self.error_values.iter().any(|error| error == text) {
|
|
326
|
+
return Ok(ruby.str_new(text).as_value());
|
|
327
|
+
}
|
|
328
|
+
if text.is_empty() {
|
|
329
|
+
return Ok(ruby.qnil().as_value());
|
|
330
|
+
}
|
|
331
|
+
if let Some(number) = create_numeric(text, float_format) {
|
|
332
|
+
return Ok(number.into_value_with(ruby));
|
|
333
|
+
}
|
|
334
|
+
if float_format || text.contains('.') || is_exponent_number(text) {
|
|
335
|
+
ruby_float(ruby, text)
|
|
336
|
+
} else {
|
|
337
|
+
ruby_integer(ruby, text, true)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
fn cell_value(&self, ruby: &Ruby, row: &RawRow, raw: &RawCell) -> Result<Value, Error> {
|
|
342
|
+
match self.cell(ruby, &row.text, raw, 0, 0, None)? {
|
|
343
|
+
Some((_, cell)) => cell.funcall("value", ()),
|
|
344
|
+
None => Ok(ruby.qnil().as_value()),
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
fn link(&self, ruby: &Ruby, coordinate: Value) -> Value {
|
|
349
|
+
match self.hyperlinks {
|
|
350
|
+
Some(hyperlinks) => ruby
|
|
351
|
+
.get_inner(hyperlinks)
|
|
352
|
+
.get(coordinate)
|
|
353
|
+
.unwrap_or_else(|| ruby.qnil().as_value()),
|
|
354
|
+
None => ruby.qnil().as_value(),
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/// Streamed cells, with optional nil padding. `None` when padding needs
|
|
359
|
+
/// roo's coordinate or constructor rules.
|
|
360
|
+
pub fn row_cells(
|
|
361
|
+
ruby: &Ruby,
|
|
362
|
+
rb_self: &Self,
|
|
363
|
+
row: &crate::Row,
|
|
364
|
+
pad: bool,
|
|
365
|
+
) -> Result<Option<RArray>, Error> {
|
|
366
|
+
if pad
|
|
367
|
+
&& (!rb_self.build_cells
|
|
368
|
+
|| row
|
|
369
|
+
.raw()
|
|
370
|
+
.cells
|
|
371
|
+
.iter()
|
|
372
|
+
.any(|raw| !matches!(raw.reference, Reference::At { .. })))
|
|
373
|
+
{
|
|
374
|
+
return Ok(None);
|
|
375
|
+
}
|
|
376
|
+
let cells = ruby.ary_new_capa(row.raw().cells.len());
|
|
377
|
+
let mut last_column = 0;
|
|
378
|
+
for (index, raw) in row.raw().cells.iter().enumerate() {
|
|
379
|
+
if let Some((_, cell)) = rb_self.cell(ruby, &row.raw().text, raw, 0, index + 1, None)? {
|
|
380
|
+
if pad {
|
|
381
|
+
if let Reference::At { column, .. } = raw.reference {
|
|
382
|
+
for _ in last_column + 1..column {
|
|
383
|
+
cells.push(ruby.qnil())?;
|
|
384
|
+
}
|
|
385
|
+
last_column = column;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
cells.push(cell)?;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
Ok(Some(cells))
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/// `SheetDoc#extract_cells` for one row, adding to `cells`.
|
|
395
|
+
pub fn add_row(
|
|
396
|
+
&self,
|
|
397
|
+
ruby: &Ruby,
|
|
398
|
+
cells: RHash,
|
|
399
|
+
row: &RawRow,
|
|
400
|
+
row_number: usize,
|
|
401
|
+
) -> Result<(), Error> {
|
|
402
|
+
for (index, raw) in row.cells.iter().enumerate() {
|
|
403
|
+
if let Some((coordinate, cell)) =
|
|
404
|
+
self.cell(ruby, &row.text, raw, row_number, index + 1, None)?
|
|
405
|
+
{
|
|
406
|
+
cells.aset(coordinate, cell)?;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
Ok(())
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/// Whether [`Self::cell`] builds `raw` without raising, so it may be built
|
|
413
|
+
/// later than roo would, or never. `false` also for anything this does
|
|
414
|
+
/// not tell apart cheaply, and for a record without a valid `r`.
|
|
415
|
+
pub fn builds_safely(&self, ruby: &Ruby, buffer: &str, raw: &RawCell) -> bool {
|
|
416
|
+
let Reference::At { row, column } = raw.reference else {
|
|
417
|
+
return false;
|
|
418
|
+
};
|
|
419
|
+
let Body::Value(text) = &raw.body else {
|
|
420
|
+
return true;
|
|
421
|
+
};
|
|
422
|
+
let text = &buffer[text.clone()];
|
|
423
|
+
let Ok(style) = self.style(ruby, raw) else {
|
|
424
|
+
return false;
|
|
425
|
+
};
|
|
426
|
+
if cacheable(style).is_none() {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
let Ok((_, format_type, number_format)) = self.style_info(ruby, style) else {
|
|
430
|
+
return false;
|
|
431
|
+
};
|
|
432
|
+
let number_builds =
|
|
433
|
+
|| number_format != NumberFormat::Unmatchable && self.number_builds(text);
|
|
434
|
+
match raw.kind {
|
|
435
|
+
Kind::Shared => self
|
|
436
|
+
.shared_string(ruby, text)
|
|
437
|
+
.is_ok_and(|value| RString::from_value(value).is_some()),
|
|
438
|
+
Kind::Boolean | Kind::Str => true,
|
|
439
|
+
Kind::InlineStr => number_builds(),
|
|
440
|
+
Kind::ByFormat => match format_type {
|
|
441
|
+
ValueType::Number => number_builds(),
|
|
442
|
+
ValueType::Date => true,
|
|
443
|
+
// roo raises for a hyperlink here: `Roo::Link.new` gets a Float,
|
|
444
|
+
// and for a day count whose seconds overflow to infinity.
|
|
445
|
+
ValueType::DateTime | ValueType::Time => {
|
|
446
|
+
let seconds_finite = text.is_empty()
|
|
447
|
+
|| (plain_number(text)
|
|
448
|
+
&& text
|
|
449
|
+
.parse::<f64>()
|
|
450
|
+
.is_ok_and(|days| (days * 86_400.0).is_finite()));
|
|
451
|
+
seconds_finite && !self.has_link(ruby, row, column)
|
|
452
|
+
}
|
|
453
|
+
},
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/// `Cell::Number#create_numeric` returns without raising.
|
|
458
|
+
fn number_builds(&self, text: &str) -> bool {
|
|
459
|
+
text.is_empty() || plain_number(text) || self.error_values.iter().any(|error| error == text)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
pub fn has_link(&self, ruby: &Ruby, row: i64, column: i64) -> bool {
|
|
463
|
+
!self.link_at(ruby, row, column).is_nil()
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
fn link_at(&self, ruby: &Ruby, row: i64, column: i64) -> Value {
|
|
467
|
+
if let Some(index) = &self.link_index {
|
|
468
|
+
return index
|
|
469
|
+
.get(&(row, column))
|
|
470
|
+
.map(|value| ruby.get_inner(*value))
|
|
471
|
+
.unwrap_or_else(|| ruby.qnil().as_value());
|
|
472
|
+
}
|
|
473
|
+
let Some(hyperlinks) = self.hyperlinks else {
|
|
474
|
+
return ruby.qnil().as_value();
|
|
475
|
+
};
|
|
476
|
+
let key =
|
|
477
|
+
ruby.ary_new_from_values(&[row.into_value_with(ruby), column.into_value_with(ruby)]);
|
|
478
|
+
ruby.get_inner(hyperlinks)
|
|
479
|
+
.get(key)
|
|
480
|
+
.unwrap_or_else(|| ruby.qnil().as_value())
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/// `cell&.presence` for the cell [`Self::cell`] builds for `raw`, when it
|
|
484
|
+
/// is known without building the cell.
|
|
485
|
+
pub fn presence(
|
|
486
|
+
&self,
|
|
487
|
+
ruby: &Ruby,
|
|
488
|
+
buffer: &str,
|
|
489
|
+
raw: &RawCell,
|
|
490
|
+
) -> Result<Option<bool>, Error> {
|
|
491
|
+
Ok(match (&raw.body, raw.kind) {
|
|
492
|
+
(Body::Empty, _) => Some(false),
|
|
493
|
+
(Body::Inline(_), _) => Some(true),
|
|
494
|
+
(Body::Value(text), Kind::Shared) => {
|
|
495
|
+
RString::from_value(self.shared_string(ruby, &buffer[text.clone()])?)
|
|
496
|
+
.map(|value| !value.is_empty())
|
|
497
|
+
}
|
|
498
|
+
(Body::Value(text), Kind::Str) => Some(!text.is_empty()),
|
|
499
|
+
(Body::Value(_), _) => Some(true),
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/// Whether roo's `cells` holds a cell for a record with an empty body.
|
|
504
|
+
pub fn empty_cell(&self) -> bool {
|
|
505
|
+
self.empty_cell
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
fn context(&self, ruby: &Ruby) -> Value {
|
|
509
|
+
ruby.get_inner(self.context)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
fn coordinate(
|
|
513
|
+
&self,
|
|
514
|
+
ruby: &Ruby,
|
|
515
|
+
raw: &RawCell,
|
|
516
|
+
row: usize,
|
|
517
|
+
column: usize,
|
|
518
|
+
) -> Result<Value, Error> {
|
|
519
|
+
match (&raw.reference, self.mode) {
|
|
520
|
+
(Reference::At { row, column }, _) => self.new_coordinate(ruby, *row, *column),
|
|
521
|
+
(Reference::Invalid(reference), _) => self
|
|
522
|
+
.context(ruby)
|
|
523
|
+
.funcall("coordinate", (ruby.str_new(reference),)),
|
|
524
|
+
(Reference::Missing, Mode::Document) => self.new_coordinate(ruby, row, column),
|
|
525
|
+
(Reference::Missing, Mode::Stream) => {
|
|
526
|
+
self.context(ruby).funcall("coordinate", (ruby.qnil(),))
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
fn style(&self, ruby: &Ruby, raw: &RawCell) -> Result<Value, Error> {
|
|
532
|
+
match &raw.style {
|
|
533
|
+
Style::Index(index) => Ok(index.into_value_with(ruby)),
|
|
534
|
+
Style::Other(text) => self
|
|
535
|
+
.context(ruby)
|
|
536
|
+
.funcall("style_index", (ruby.str_new(text),)),
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/// The format string of a style index, its value type, and how
|
|
541
|
+
/// `Cell::Number` reads text under it. roo reads the format again for
|
|
542
|
+
/// every cell, so a stream reads a format changed in place again. A
|
|
543
|
+
/// document keeps what each format first read: roo builds every cell
|
|
544
|
+
/// before a caller can change one.
|
|
545
|
+
fn style_info(
|
|
546
|
+
&self,
|
|
547
|
+
ruby: &Ruby,
|
|
548
|
+
style: Value,
|
|
549
|
+
) -> Result<(Value, ValueType, NumberFormat), Error> {
|
|
550
|
+
let formats = ruby.get_inner(self.formats);
|
|
551
|
+
let cacheable = cacheable(style);
|
|
552
|
+
if let Some(index) = cacheable {
|
|
553
|
+
if let Some(Some(info)) = self.styles.borrow().get(index) {
|
|
554
|
+
let format: Value = formats.entry(index as isize)?;
|
|
555
|
+
if self.mode == Mode::Document || info.read_from(format) {
|
|
556
|
+
return Ok((format, info.value_type, info.number_format));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
let info: RArray = self.context(ruby).funcall("style", (style,))?;
|
|
561
|
+
let format: Value = info.entry(0)?;
|
|
562
|
+
let type_symbol: Symbol = info.entry(1)?;
|
|
563
|
+
let float_format: Option<bool> = info.entry(2)?;
|
|
564
|
+
let value_type = match type_symbol.name()?.as_ref() {
|
|
565
|
+
"date" => ValueType::Date,
|
|
566
|
+
"datetime" => ValueType::DateTime,
|
|
567
|
+
"time" => ValueType::Time,
|
|
568
|
+
_ => ValueType::Number,
|
|
569
|
+
};
|
|
570
|
+
let number_format = match float_format {
|
|
571
|
+
Some(true) => NumberFormat::Float,
|
|
572
|
+
Some(false) => NumberFormat::ByText,
|
|
573
|
+
None => NumberFormat::Unmatchable,
|
|
574
|
+
};
|
|
575
|
+
if let (Some(index), Some(text)) = (cacheable, RString::from_value(format)) {
|
|
576
|
+
formats.store(index as isize, format)?;
|
|
577
|
+
let mut styles = self.styles.borrow_mut();
|
|
578
|
+
if styles.len() <= index {
|
|
579
|
+
styles.resize(index + 1, None);
|
|
580
|
+
}
|
|
581
|
+
styles[index] = Some(StyleInfo {
|
|
582
|
+
format_text: unsafe { text.as_slice() }.to_vec(),
|
|
583
|
+
format_encoding: text.enc_get(),
|
|
584
|
+
value_type,
|
|
585
|
+
number_format,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
Ok((format, value_type, number_format))
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/// The style a document read first for a style index.
|
|
592
|
+
fn style_as_read(&self, style: Value) -> Option<StyleInfo> {
|
|
593
|
+
let index = cacheable(style)?;
|
|
594
|
+
self.styles.borrow().get(index).cloned().flatten()
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/// `shared_strings.use_html?(i) ? shared_strings.to_html[i] : shared_strings[i]`.
|
|
598
|
+
/// In stream mode it is decided again for every cell, as roo does, since
|
|
599
|
+
/// a caller may change a shared string in place between rows.
|
|
600
|
+
fn shared_string(&self, ruby: &Ruby, text: &str) -> Result<Value, Error> {
|
|
601
|
+
let index = parse_index(text);
|
|
602
|
+
if self.picked.is_some() && index.is_some() {
|
|
603
|
+
self.load_shared_strings(ruby)?;
|
|
604
|
+
}
|
|
605
|
+
if let (Some(index), Some(indices)) = (index, self.picked_indices.get()) {
|
|
606
|
+
let indices = ruby.get_inner(indices);
|
|
607
|
+
if index < indices.len() {
|
|
608
|
+
return indices.entry(index as isize);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
let Some(picked) = self.picked.map(|picked| ruby.get_inner(picked)) else {
|
|
612
|
+
return self.pick_shared_string(ruby, index, text);
|
|
613
|
+
};
|
|
614
|
+
let key = match index {
|
|
615
|
+
Some(index) => index.into_value_with(ruby),
|
|
616
|
+
None => ruby.str_new(text).funcall("to_i", ())?,
|
|
617
|
+
};
|
|
618
|
+
if let Some(string) = picked.get(key) {
|
|
619
|
+
return Ok(string);
|
|
620
|
+
}
|
|
621
|
+
let string = self.pick_shared_string(ruby, index, text)?;
|
|
622
|
+
picked.aset(key, string)?;
|
|
623
|
+
Ok(string)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
fn load_shared_strings(&self, ruby: &Ruby) -> Result<(), Error> {
|
|
627
|
+
if self.shared_plain.get().is_none() {
|
|
628
|
+
let tables: RArray = self.context(ruby).funcall("shared_strings", ())?;
|
|
629
|
+
let plain: RArray = tables.entry(0)?;
|
|
630
|
+
let html: Value = tables.entry(1)?;
|
|
631
|
+
self.shared_plain.set(Some(plain.into()));
|
|
632
|
+
self.shared_html.set(Some(html.into()));
|
|
633
|
+
if self.picked.is_some()
|
|
634
|
+
&& (html.is_nil()
|
|
635
|
+
|| <&HtmlStrings>::try_convert(html)
|
|
636
|
+
.is_ok_and(|html| html.is_plain(plain.len())))
|
|
637
|
+
{
|
|
638
|
+
self.picked_indices.set(Some(plain.dup().into()));
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
Ok(())
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
fn pick_shared_string(
|
|
645
|
+
&self,
|
|
646
|
+
ruby: &Ruby,
|
|
647
|
+
index: Option<usize>,
|
|
648
|
+
text: &str,
|
|
649
|
+
) -> Result<Value, Error> {
|
|
650
|
+
let Some(index) = index else {
|
|
651
|
+
return self
|
|
652
|
+
.context(ruby)
|
|
653
|
+
.funcall("shared_string", (ruby.str_new(text),));
|
|
654
|
+
};
|
|
655
|
+
self.load_shared_strings(ruby)?;
|
|
656
|
+
let plain = ruby.get_inner(self.shared_plain.get().expect("fetched above"));
|
|
657
|
+
let html = ruby.get_inner(self.shared_html.get().expect("fetched above"));
|
|
658
|
+
let chosen = match RArray::from_value(html) {
|
|
659
|
+
None if html.is_nil() => Some(plain),
|
|
660
|
+
None => match <&HtmlStrings>::try_convert(html)?.select(ruby, index)? {
|
|
661
|
+
HtmlSelection::Html(string) => return Ok(string),
|
|
662
|
+
HtmlSelection::Plain => Some(plain),
|
|
663
|
+
HtmlSelection::Invalid => None,
|
|
664
|
+
},
|
|
665
|
+
Some(html) if index < html.len() => {
|
|
666
|
+
match RString::from_value(html.entry(index as isize)?) {
|
|
667
|
+
Some(string) if has_html_tag(unsafe { string.as_slice() }) => {
|
|
668
|
+
return Ok(string.as_value());
|
|
669
|
+
}
|
|
670
|
+
Some(_) => Some(plain),
|
|
671
|
+
None => None,
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
Some(_) => None,
|
|
675
|
+
};
|
|
676
|
+
if let Some(plain) = chosen.filter(|plain| index < plain.len()) {
|
|
677
|
+
return plain.entry(index as isize);
|
|
678
|
+
}
|
|
679
|
+
self.context(ruby)
|
|
680
|
+
.funcall("shared_string", (ruby.str_new(text),))
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/// The style indexes [`Converter`] keeps a format and type for.
|
|
685
|
+
fn cacheable(style: Value) -> Option<usize> {
|
|
686
|
+
usize::try_convert(style).ok().filter(|i| *i < 1 << 16)
|
|
687
|
+
}
|