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,684 @@
|
|
|
1
|
+
//! Reads `sharedStrings.xml` into the two arrays roo 4.0.0's
|
|
2
|
+
//! `Roo::Excelx::SharedStrings` builds from it: `#to_a`, the plain text of
|
|
3
|
+
//! each `/sst/si`, and `#to_html`, the same text with rich-text runs wrapped
|
|
4
|
+
//! in HTML tags.
|
|
5
|
+
//!
|
|
6
|
+
//! roo reads the file with Nokogiri and removes its namespaces, so elements
|
|
7
|
+
//! match by local name. [`SheetReader`] first checks that Nokogiri reads the
|
|
8
|
+
//! file as quick-xml does; anything it rejects is left to roo.
|
|
9
|
+
|
|
10
|
+
use std::cell::RefCell;
|
|
11
|
+
use std::fs::File;
|
|
12
|
+
use std::io::{BufRead, BufReader};
|
|
13
|
+
use std::panic::resume_unwind;
|
|
14
|
+
use std::path::Path;
|
|
15
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
16
|
+
|
|
17
|
+
use magnus::gc::Marker;
|
|
18
|
+
use magnus::value::{Opaque, ReprValue};
|
|
19
|
+
use magnus::{prelude::*, DataTypeFunctions, Error, RArray, RString, Ruby, TypedData, Value};
|
|
20
|
+
use quick_xml::events::{BytesRef, BytesStart, Event};
|
|
21
|
+
use quick_xml::XmlVersion;
|
|
22
|
+
|
|
23
|
+
use crate::reader::{Mode, SheetReader, Unsupported};
|
|
24
|
+
use crate::{unsupported, without_gvl};
|
|
25
|
+
|
|
26
|
+
type Result<T> = std::result::Result<T, Unsupported>;
|
|
27
|
+
|
|
28
|
+
/// The text `SharedStrings#fix_invalid_shared_strings` replaces with a line
|
|
29
|
+
/// feed in the document behind `#to_a`, but not in the one behind `#to_html`.
|
|
30
|
+
const CARRIAGE_RETURN: &str = "_x000D_";
|
|
31
|
+
|
|
32
|
+
/// A list of strings kept in one buffer.
|
|
33
|
+
#[derive(Default)]
|
|
34
|
+
struct Strings {
|
|
35
|
+
text: String,
|
|
36
|
+
ends: Vec<u32>,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
impl Strings {
|
|
40
|
+
fn push(&mut self, string: &str) -> Result<()> {
|
|
41
|
+
let end = string_end(self.text.len(), string.len())?;
|
|
42
|
+
self.text.push_str(string);
|
|
43
|
+
self.ends.push(end);
|
|
44
|
+
Ok(())
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn len(&self) -> usize {
|
|
48
|
+
self.ends.len()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fn iter(&self) -> impl Iterator<Item = &str> {
|
|
52
|
+
let starts = std::iter::once(0).chain(self.ends.iter().copied());
|
|
53
|
+
starts
|
|
54
|
+
.zip(self.ends.iter().copied())
|
|
55
|
+
.map(|(start, end)| &self.text[start as usize..end as usize])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fn get(&self, index: usize) -> &str {
|
|
59
|
+
let start = index.checked_sub(1).map_or(0, |i| self.ends[i]);
|
|
60
|
+
&self.text[start as usize..self.ends[index] as usize]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fn into_array(self, ruby: &Ruby) -> std::result::Result<RArray, Error> {
|
|
64
|
+
let out = ruby.ary_new_capa(self.len());
|
|
65
|
+
for string in self.iter() {
|
|
66
|
+
out.push(ruby.str_new(string))?;
|
|
67
|
+
}
|
|
68
|
+
Ok(out)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn string_end(start: usize, len: usize) -> Result<u32> {
|
|
73
|
+
start
|
|
74
|
+
.checked_add(len)
|
|
75
|
+
.and_then(|end| u32::try_from(end).ok())
|
|
76
|
+
.ok_or_else(|| Unsupported("shared strings exceed 32-bit offsets".into()))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// `string[/<([biu]|sup|sub)>/]`, the test in `SharedStrings#use_html?`.
|
|
80
|
+
pub(crate) fn has_html_tag(string: &[u8]) -> bool {
|
|
81
|
+
string.windows(3).enumerate().any(|(i, w)| {
|
|
82
|
+
w[0] == b'<'
|
|
83
|
+
&& (matches!(w, [_, b'b' | b'i' | b'u', b'>'])
|
|
84
|
+
|| string[i..].starts_with(b"<sup>")
|
|
85
|
+
|| string[i..].starts_with(b"<sub>"))
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub(crate) enum HtmlSelection {
|
|
90
|
+
Plain,
|
|
91
|
+
Html(Value),
|
|
92
|
+
Invalid,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[derive(TypedData)]
|
|
96
|
+
#[magnus(
|
|
97
|
+
class = "Roo::Oxide::Native::HtmlStrings",
|
|
98
|
+
mark,
|
|
99
|
+
free_immediately,
|
|
100
|
+
size
|
|
101
|
+
)]
|
|
102
|
+
pub(crate) struct HtmlStrings {
|
|
103
|
+
strings: RefCell<Option<Strings>>,
|
|
104
|
+
cached: Opaque<RArray>,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
impl DataTypeFunctions for HtmlStrings {
|
|
108
|
+
fn mark(&self, marker: &Marker) {
|
|
109
|
+
marker.mark(self.cached);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn size(&self) -> usize {
|
|
113
|
+
size_of::<Self>()
|
|
114
|
+
+ self.strings.borrow().as_ref().map_or(0, |strings| {
|
|
115
|
+
strings.text.capacity() + strings.ends.capacity() * size_of::<u32>()
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
impl HtmlStrings {
|
|
121
|
+
pub(crate) fn is_plain(&self, len: usize) -> bool {
|
|
122
|
+
self.strings.borrow().as_ref().is_some_and(|strings| {
|
|
123
|
+
strings.len() >= len
|
|
124
|
+
&& strings
|
|
125
|
+
.iter()
|
|
126
|
+
.all(|string| !has_html_tag(string.as_bytes()))
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
pub(crate) fn select(
|
|
131
|
+
&self,
|
|
132
|
+
ruby: &Ruby,
|
|
133
|
+
index: usize,
|
|
134
|
+
) -> std::result::Result<HtmlSelection, Error> {
|
|
135
|
+
let cached = ruby.get_inner(self.cached);
|
|
136
|
+
if index < cached.len() {
|
|
137
|
+
let value: Value = cached.entry(index as isize)?;
|
|
138
|
+
if !value.is_nil() || self.strings.borrow().is_none() {
|
|
139
|
+
return Ok(match RString::from_value(value) {
|
|
140
|
+
Some(string) if has_html_tag(unsafe { string.as_slice() }) => {
|
|
141
|
+
HtmlSelection::Html(value)
|
|
142
|
+
}
|
|
143
|
+
Some(_) => HtmlSelection::Plain,
|
|
144
|
+
None => HtmlSelection::Invalid,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
{
|
|
149
|
+
let strings = self.strings.borrow();
|
|
150
|
+
let Some(strings) = strings.as_ref().filter(|strings| index < strings.len()) else {
|
|
151
|
+
return Ok(HtmlSelection::Invalid);
|
|
152
|
+
};
|
|
153
|
+
if !has_html_tag(strings.get(index).as_bytes()) {
|
|
154
|
+
return Ok(HtmlSelection::Plain);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
self.materialize(ruby, index).map(HtmlSelection::Html)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
fn materialize(&self, ruby: &Ruby, index: usize) -> std::result::Result<Value, Error> {
|
|
161
|
+
let cached = ruby.get_inner(self.cached);
|
|
162
|
+
let value: Value = cached.entry(index as isize)?;
|
|
163
|
+
if !value.is_nil() {
|
|
164
|
+
return Ok(value);
|
|
165
|
+
}
|
|
166
|
+
let html = {
|
|
167
|
+
let strings = self.strings.borrow();
|
|
168
|
+
format!(
|
|
169
|
+
"<html>{}</html>",
|
|
170
|
+
strings.as_ref().expect("not materialized").get(index)
|
|
171
|
+
)
|
|
172
|
+
};
|
|
173
|
+
let string = ruby.str_new(&html);
|
|
174
|
+
cached.store(index as isize, string)?;
|
|
175
|
+
Ok(string.as_value())
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn to_a(ruby: &Ruby, rb_self: &Self) -> std::result::Result<RArray, Error> {
|
|
179
|
+
let len = rb_self.strings.borrow().as_ref().map(Strings::len);
|
|
180
|
+
if let Some(len) = len {
|
|
181
|
+
for index in 0..len {
|
|
182
|
+
rb_self.materialize(ruby, index)?;
|
|
183
|
+
}
|
|
184
|
+
rb_self.strings.borrow_mut().take();
|
|
185
|
+
}
|
|
186
|
+
Ok(ruby.get_inner(rb_self.cached))
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// HTML tags of a rich-text run, in the order roo nests them.
|
|
191
|
+
#[derive(Clone, Copy, Default)]
|
|
192
|
+
struct Format {
|
|
193
|
+
sub: bool,
|
|
194
|
+
sup: bool,
|
|
195
|
+
b: bool,
|
|
196
|
+
i: bool,
|
|
197
|
+
u: bool,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
impl Format {
|
|
201
|
+
fn tags(&self) -> impl DoubleEndedIterator<Item = &'static str> {
|
|
202
|
+
[
|
|
203
|
+
(self.sub, "sub"),
|
|
204
|
+
(self.sup, "sup"),
|
|
205
|
+
(self.b, "b"),
|
|
206
|
+
(self.i, "i"),
|
|
207
|
+
(self.u, "u"),
|
|
208
|
+
]
|
|
209
|
+
.into_iter()
|
|
210
|
+
.filter_map(|(on, tag)| on.then_some(tag))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// `SharedStrings#create_html`.
|
|
214
|
+
fn wrap(&self, text: &str, out: &mut String) {
|
|
215
|
+
for tag in self.tags() {
|
|
216
|
+
out.push('<');
|
|
217
|
+
out.push_str(tag);
|
|
218
|
+
out.push('>');
|
|
219
|
+
}
|
|
220
|
+
out.push_str(text);
|
|
221
|
+
for tag in self.tags().rev() {
|
|
222
|
+
out.push_str("</");
|
|
223
|
+
out.push_str(tag);
|
|
224
|
+
out.push('>');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
struct SharedStringsReader<R: BufRead> {
|
|
230
|
+
xml: quick_xml::Reader<R>,
|
|
231
|
+
buf: Vec<u8>,
|
|
232
|
+
/// Whether to build `#to_html` too.
|
|
233
|
+
html: bool,
|
|
234
|
+
depth: usize,
|
|
235
|
+
in_sst: bool,
|
|
236
|
+
plain: Strings,
|
|
237
|
+
html_strings: Strings,
|
|
238
|
+
item_plain: String,
|
|
239
|
+
item_html: String,
|
|
240
|
+
/// The content of the `t` being read.
|
|
241
|
+
content: String,
|
|
242
|
+
/// Where each text or CDATA node of `content` after the first starts:
|
|
243
|
+
/// Nokogiri keeps text and CDATA in separate nodes.
|
|
244
|
+
node_starts: Vec<usize>,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
impl SharedStringsReader<BufReader<File>> {
|
|
248
|
+
fn open(path: &Path, html: bool) -> Result<Self> {
|
|
249
|
+
let file = File::open(path).map_err(|e| Unsupported(e.to_string()))?;
|
|
250
|
+
Ok(Self::new(BufReader::with_capacity(64 * 1024, file), html))
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
255
|
+
enum Name {
|
|
256
|
+
T,
|
|
257
|
+
R,
|
|
258
|
+
RPr,
|
|
259
|
+
B,
|
|
260
|
+
I,
|
|
261
|
+
U,
|
|
262
|
+
VertAlign,
|
|
263
|
+
Other,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fn name(e: &BytesStart<'_>) -> Name {
|
|
267
|
+
match e.local_name().as_ref() {
|
|
268
|
+
"t" => Name::T,
|
|
269
|
+
"r" => Name::R,
|
|
270
|
+
"rPr" => Name::RPr,
|
|
271
|
+
"b" => Name::B,
|
|
272
|
+
"i" => Name::I,
|
|
273
|
+
"u" => Name::U,
|
|
274
|
+
"vertAlign" => Name::VertAlign,
|
|
275
|
+
_ => Name::Other,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
impl<R: BufRead> SharedStringsReader<R> {
|
|
280
|
+
fn new(input: R, html: bool) -> Self {
|
|
281
|
+
Self {
|
|
282
|
+
xml: quick_xml::Reader::from_reader(input),
|
|
283
|
+
buf: Vec::with_capacity(16 * 1024),
|
|
284
|
+
html,
|
|
285
|
+
depth: 0,
|
|
286
|
+
in_sst: false,
|
|
287
|
+
plain: Strings::default(),
|
|
288
|
+
html_strings: Strings::default(),
|
|
289
|
+
item_plain: String::new(),
|
|
290
|
+
item_html: String::new(),
|
|
291
|
+
content: String::new(),
|
|
292
|
+
node_starts: Vec::new(),
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// Reads up to the end of the next `/sst/si`. `false` at the end of the
|
|
297
|
+
/// document.
|
|
298
|
+
fn next_item(&mut self) -> Result<bool> {
|
|
299
|
+
let mut buf = std::mem::take(&mut self.buf);
|
|
300
|
+
let read = self.find_item(&mut buf);
|
|
301
|
+
self.buf = buf;
|
|
302
|
+
read
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
fn find_item(&mut self, buf: &mut Vec<u8>) -> Result<bool> {
|
|
306
|
+
loop {
|
|
307
|
+
buf.clear();
|
|
308
|
+
match self.read_event(buf)? {
|
|
309
|
+
Event::Start(e) => {
|
|
310
|
+
self.depth += 1;
|
|
311
|
+
match self.depth {
|
|
312
|
+
1 => self.in_sst = e.local_name().as_ref() == "sst",
|
|
313
|
+
2 if self.in_sst && e.local_name().as_ref() == "si" => {
|
|
314
|
+
self.read_si(buf)?;
|
|
315
|
+
self.depth -= 1;
|
|
316
|
+
return Ok(true);
|
|
317
|
+
}
|
|
318
|
+
_ => {
|
|
319
|
+
self.skip(buf)?;
|
|
320
|
+
self.depth -= 1;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
Event::Empty(e) => {
|
|
325
|
+
if self.depth == 1 && self.in_sst && e.local_name().as_ref() == "si" {
|
|
326
|
+
self.plain.push("")?;
|
|
327
|
+
if self.html {
|
|
328
|
+
self.html_strings.push("")?;
|
|
329
|
+
}
|
|
330
|
+
return Ok(true);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
Event::End(_) => self.depth = self.depth.saturating_sub(1),
|
|
334
|
+
Event::Eof => return Ok(false),
|
|
335
|
+
_ => {}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// `SharedStrings#extract_shared_strings` and `#extract_html` for one
|
|
341
|
+
/// open `si`.
|
|
342
|
+
fn read_si(&mut self, buf: &mut Vec<u8>) -> Result<()> {
|
|
343
|
+
self.item_plain.clear();
|
|
344
|
+
self.item_html.clear();
|
|
345
|
+
loop {
|
|
346
|
+
buf.clear();
|
|
347
|
+
let (child, empty) = match self.read_event(buf)? {
|
|
348
|
+
Event::Start(e) => (name(&e), false),
|
|
349
|
+
Event::Empty(e) => (name(&e), true),
|
|
350
|
+
Event::End(_) => break,
|
|
351
|
+
Event::Eof => return Err(Unsupported("unexpected end of a shared string".into())),
|
|
352
|
+
_ => continue,
|
|
353
|
+
};
|
|
354
|
+
match (child, empty) {
|
|
355
|
+
(Name::T, false) => {
|
|
356
|
+
self.read_t(buf)?;
|
|
357
|
+
self.item_plain.clear();
|
|
358
|
+
self.push_fixed_content();
|
|
359
|
+
if self.html {
|
|
360
|
+
self.item_html.push_str(&self.content);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
(Name::T, true) => self.item_plain.clear(),
|
|
364
|
+
(Name::R, false) => self.read_run(buf)?,
|
|
365
|
+
(_, false) => self.skip(buf)?,
|
|
366
|
+
(_, true) => {}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
self.plain.push(&self.item_plain)?;
|
|
370
|
+
if self.html {
|
|
371
|
+
self.html_strings.push(&self.item_html)?;
|
|
372
|
+
}
|
|
373
|
+
Ok(())
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/// `SharedStrings#extract_html_r` for one open `r`, which also adds the
|
|
377
|
+
/// text of its `t` children to the plain string.
|
|
378
|
+
fn read_run(&mut self, buf: &mut Vec<u8>) -> Result<()> {
|
|
379
|
+
let mut format = Format::default();
|
|
380
|
+
loop {
|
|
381
|
+
buf.clear();
|
|
382
|
+
let (child, empty) = match self.read_event(buf)? {
|
|
383
|
+
Event::Start(e) => (name(&e), false),
|
|
384
|
+
Event::Empty(e) => (name(&e), true),
|
|
385
|
+
Event::End(_) => return Ok(()),
|
|
386
|
+
Event::Eof => return Err(Unsupported("unexpected end of a run".into())),
|
|
387
|
+
_ => continue,
|
|
388
|
+
};
|
|
389
|
+
match (child, empty) {
|
|
390
|
+
(Name::T, _) => {
|
|
391
|
+
if empty {
|
|
392
|
+
self.content.clear();
|
|
393
|
+
self.node_starts.clear();
|
|
394
|
+
} else {
|
|
395
|
+
self.read_t(buf)?;
|
|
396
|
+
}
|
|
397
|
+
self.push_fixed_content();
|
|
398
|
+
if self.html {
|
|
399
|
+
format.wrap(&self.content, &mut self.item_html);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
(Name::RPr, false) => self.read_run_properties(buf, &mut format)?,
|
|
403
|
+
(_, false) => self.skip(buf)?,
|
|
404
|
+
(_, true) => {}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/// The children of an open `rPr` that turn on an HTML tag.
|
|
410
|
+
fn read_run_properties(&mut self, buf: &mut Vec<u8>, format: &mut Format) -> Result<()> {
|
|
411
|
+
loop {
|
|
412
|
+
buf.clear();
|
|
413
|
+
match self.read_event(buf)? {
|
|
414
|
+
Event::Start(e) => {
|
|
415
|
+
self.run_property(&e, format)?;
|
|
416
|
+
self.skip(buf)?;
|
|
417
|
+
}
|
|
418
|
+
Event::Empty(e) => self.run_property(&e, format)?,
|
|
419
|
+
Event::End(_) => return Ok(()),
|
|
420
|
+
Event::Eof => return Err(Unsupported("unexpected end of run properties".into())),
|
|
421
|
+
_ => {}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
fn run_property(&self, e: &BytesStart<'_>, format: &mut Format) -> Result<()> {
|
|
427
|
+
match name(e) {
|
|
428
|
+
Name::B => format.b = true,
|
|
429
|
+
Name::I => format.i = true,
|
|
430
|
+
Name::U => format.u = true,
|
|
431
|
+
Name::VertAlign if self.html => match vert_align(e)?.as_ref() {
|
|
432
|
+
"subscript" => (format.sub, format.sup) = (true, false),
|
|
433
|
+
"superscript" => (format.sub, format.sup) = (false, true),
|
|
434
|
+
_ => {}
|
|
435
|
+
},
|
|
436
|
+
_ => {}
|
|
437
|
+
}
|
|
438
|
+
Ok(())
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/// Nokogiri's `content` of an open `t`, into `self.content`. Any markup
|
|
442
|
+
/// inside it is left to roo: roo's `_x000D_` fix serializes the document
|
|
443
|
+
/// with indentation, which changes the content of such a `t`.
|
|
444
|
+
fn read_t(&mut self, buf: &mut Vec<u8>) -> Result<()> {
|
|
445
|
+
self.content.clear();
|
|
446
|
+
self.node_starts.clear();
|
|
447
|
+
let mut in_cdata = false;
|
|
448
|
+
loop {
|
|
449
|
+
buf.clear();
|
|
450
|
+
match self.read_event(buf)? {
|
|
451
|
+
Event::Text(t) => {
|
|
452
|
+
if in_cdata {
|
|
453
|
+
self.node_starts.push(self.content.len());
|
|
454
|
+
in_cdata = false;
|
|
455
|
+
}
|
|
456
|
+
self.content.push_str(&t.xml10_content());
|
|
457
|
+
}
|
|
458
|
+
Event::GeneralRef(r) => {
|
|
459
|
+
if in_cdata {
|
|
460
|
+
self.node_starts.push(self.content.len());
|
|
461
|
+
in_cdata = false;
|
|
462
|
+
}
|
|
463
|
+
push_reference(&mut self.content, &r)?;
|
|
464
|
+
}
|
|
465
|
+
Event::CData(t) => {
|
|
466
|
+
// libxml2 merges adjacent CDATA sections into one node.
|
|
467
|
+
if !in_cdata && !self.content.is_empty() {
|
|
468
|
+
self.node_starts.push(self.content.len());
|
|
469
|
+
}
|
|
470
|
+
in_cdata = true;
|
|
471
|
+
self.content.push_str(&t.xml10_content());
|
|
472
|
+
}
|
|
473
|
+
Event::End(_) => return Ok(()),
|
|
474
|
+
Event::Eof => return Err(Unsupported("unexpected end of a `t`".into())),
|
|
475
|
+
_ => return Err(Unsupported("markup inside a shared string's `t`".into())),
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/// Appends `self.content` to the plain string as roo's
|
|
481
|
+
/// `fix_invalid_shared_strings` leaves it: `_x000D_` becomes a line feed
|
|
482
|
+
/// within each text or CDATA node, which the serialized document keeps
|
|
483
|
+
/// apart.
|
|
484
|
+
fn push_fixed_content(&mut self) {
|
|
485
|
+
if !self.content.contains(CARRIAGE_RETURN) {
|
|
486
|
+
self.item_plain.push_str(&self.content);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
let starts = std::iter::once(0).chain(self.node_starts.iter().copied());
|
|
490
|
+
let ends = self
|
|
491
|
+
.node_starts
|
|
492
|
+
.iter()
|
|
493
|
+
.copied()
|
|
494
|
+
.chain(std::iter::once(self.content.len()));
|
|
495
|
+
for (start, end) in starts.zip(ends) {
|
|
496
|
+
self.item_plain
|
|
497
|
+
.push_str(&self.content[start..end].replace(CARRIAGE_RETURN, "\n"));
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/// Skips the rest of an open element.
|
|
502
|
+
fn skip(&mut self, buf: &mut Vec<u8>) -> Result<()> {
|
|
503
|
+
let mut depth = 0usize;
|
|
504
|
+
loop {
|
|
505
|
+
buf.clear();
|
|
506
|
+
match self.read_event(buf)? {
|
|
507
|
+
Event::Start(_) => depth += 1,
|
|
508
|
+
Event::End(_) if depth == 0 => return Ok(()),
|
|
509
|
+
Event::End(_) => depth -= 1,
|
|
510
|
+
Event::Eof => return Err(Unsupported("unexpected end of element".into())),
|
|
511
|
+
_ => {}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
fn read_event<'b>(&mut self, buf: &'b mut Vec<u8>) -> Result<Event<'b>> {
|
|
517
|
+
let event = self.xml.read_event_into(buf)?;
|
|
518
|
+
match &event {
|
|
519
|
+
// roo's fix also replaces `_x000D_` in names, which breaks them.
|
|
520
|
+
Event::Start(e) | Event::Empty(e) if e.contains(CARRIAGE_RETURN) => {
|
|
521
|
+
Err(Unsupported(format!("{CARRIAGE_RETURN} in a tag")))
|
|
522
|
+
}
|
|
523
|
+
// Nokogiri names a processing instruction by its target, so roo
|
|
524
|
+
// takes `<?t x?>` for a `t`.
|
|
525
|
+
Event::PI(_) => Err(Unsupported("a processing instruction".into())),
|
|
526
|
+
_ => Ok(event),
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/// `vertAlign.xpath('@val').first.value`. roo raises without a `val`; a
|
|
532
|
+
/// prefixed one is left to roo, which removes namespaces from attributes too.
|
|
533
|
+
fn vert_align(e: &BytesStart<'_>) -> Result<String> {
|
|
534
|
+
let mut value = None;
|
|
535
|
+
for attribute in e.attributes() {
|
|
536
|
+
let attribute = attribute?;
|
|
537
|
+
let key = attribute.key;
|
|
538
|
+
if key.local_name().as_ref() != "val" {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if key.prefix().is_some() {
|
|
542
|
+
return Err(Unsupported("a prefixed `val` on a `vertAlign`".into()));
|
|
543
|
+
}
|
|
544
|
+
value = Some(
|
|
545
|
+
attribute
|
|
546
|
+
.normalized_value(XmlVersion::Implicit1_0)?
|
|
547
|
+
.into_owned(),
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
value.ok_or_else(|| Unsupported("a `vertAlign` without a `val`".into()))
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
fn push_reference(text: &mut String, reference: &BytesRef<'_>) -> Result<()> {
|
|
554
|
+
if let Some(c) = reference.resolve_char_ref()? {
|
|
555
|
+
text.push(c);
|
|
556
|
+
return Ok(());
|
|
557
|
+
}
|
|
558
|
+
text.push(match reference.xml10_content().as_ref() {
|
|
559
|
+
"lt" => '<',
|
|
560
|
+
"gt" => '>',
|
|
561
|
+
"amp" => '&',
|
|
562
|
+
"apos" => '\'',
|
|
563
|
+
"quot" => '"',
|
|
564
|
+
other => return Err(Unsupported(format!("entity reference &{other};"))),
|
|
565
|
+
});
|
|
566
|
+
Ok(())
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/// Calls `step` until it returns `false` (`Ok(true)`) or `interrupted`
|
|
570
|
+
/// turns true (`Ok(false)`).
|
|
571
|
+
fn until(interrupted: &AtomicBool, mut step: impl FnMut() -> Result<bool>) -> Result<bool> {
|
|
572
|
+
while !interrupted.load(Ordering::Acquire) {
|
|
573
|
+
if !step()? {
|
|
574
|
+
return Ok(true);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
Ok(false)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/// Reads the shared strings at `path` with the GVL released, while another
|
|
581
|
+
/// thread checks that Nokogiri reads the file the same way.
|
|
582
|
+
fn read(
|
|
583
|
+
ruby: &Ruby,
|
|
584
|
+
path: &Path,
|
|
585
|
+
html: bool,
|
|
586
|
+
) -> std::result::Result<SharedStringsReader<BufReader<File>>, Error> {
|
|
587
|
+
let mut check = SheetReader::open(path, Mode::Document).map_err(|e| unsupported(ruby, e))?;
|
|
588
|
+
let mut reader = SharedStringsReader::open(path, html).map_err(|e| unsupported(ruby, e))?;
|
|
589
|
+
let (mut checked, mut read) = (false, false);
|
|
590
|
+
while !(checked && read) {
|
|
591
|
+
let (checking, reading) = without_gvl(|interrupted| {
|
|
592
|
+
std::thread::scope(|scope| {
|
|
593
|
+
let checking = if checked {
|
|
594
|
+
None
|
|
595
|
+
} else {
|
|
596
|
+
let thread = std::thread::Builder::new()
|
|
597
|
+
.name("roo-oxide".into())
|
|
598
|
+
.spawn_scoped(scope, || {
|
|
599
|
+
until(interrupted, || Ok(check.next_row()?.is_some()))
|
|
600
|
+
})
|
|
601
|
+
.map_err(|e| Unsupported(e.to_string()))?;
|
|
602
|
+
Some(thread)
|
|
603
|
+
};
|
|
604
|
+
let reading = read || until(interrupted, || reader.next_item())?;
|
|
605
|
+
let checking = match checking {
|
|
606
|
+
Some(thread) => thread.join().unwrap_or_else(|panic| resume_unwind(panic))?,
|
|
607
|
+
None => true,
|
|
608
|
+
};
|
|
609
|
+
Ok((checking, reading))
|
|
610
|
+
})
|
|
611
|
+
})?
|
|
612
|
+
.map_err(|e| unsupported(ruby, e))?;
|
|
613
|
+
(checked, read) = (checking, reading);
|
|
614
|
+
}
|
|
615
|
+
Ok(reader)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/// `Native.shared_strings(path, html, plain)`: plain strings and deferred HTML
|
|
619
|
+
/// for roo's `SharedStrings`, with `nil` for an unrequested table. Raises
|
|
620
|
+
/// `Roo::Oxide::UnsupportedError` for a file roo reads differently.
|
|
621
|
+
fn shared_strings(
|
|
622
|
+
ruby: &Ruby,
|
|
623
|
+
path: String,
|
|
624
|
+
html: bool,
|
|
625
|
+
plain: bool,
|
|
626
|
+
) -> std::result::Result<(Option<RArray>, Option<HtmlStrings>), Error> {
|
|
627
|
+
let build_plain = plain;
|
|
628
|
+
let SharedStringsReader {
|
|
629
|
+
plain,
|
|
630
|
+
html_strings,
|
|
631
|
+
..
|
|
632
|
+
} = read(ruby, Path::new(&path), html)?;
|
|
633
|
+
let plain = if build_plain {
|
|
634
|
+
Some(plain.into_array(ruby)?)
|
|
635
|
+
} else {
|
|
636
|
+
drop(plain);
|
|
637
|
+
None
|
|
638
|
+
};
|
|
639
|
+
let html = if html {
|
|
640
|
+
Some(HtmlStrings {
|
|
641
|
+
cached: ruby.ary_new().into(),
|
|
642
|
+
strings: RefCell::new(Some(html_strings)),
|
|
643
|
+
})
|
|
644
|
+
} else {
|
|
645
|
+
None
|
|
646
|
+
};
|
|
647
|
+
Ok((plain, html))
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
pub fn define(native: magnus::RModule) -> std::result::Result<(), Error> {
|
|
651
|
+
let ruby = Ruby::get().expect("GVL held");
|
|
652
|
+
let html = native.define_class("HtmlStrings", ruby.class_object())?;
|
|
653
|
+
html.define_method("to_a", magnus::method!(HtmlStrings::to_a, 0))?;
|
|
654
|
+
native.define_singleton_method("shared_strings", magnus::function!(shared_strings, 3))?;
|
|
655
|
+
Ok(())
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
#[cfg(test)]
|
|
659
|
+
mod tests {
|
|
660
|
+
use super::{has_html_tag, string_end};
|
|
661
|
+
|
|
662
|
+
#[test]
|
|
663
|
+
fn finds_the_tags_roo_looks_for() {
|
|
664
|
+
for html in [
|
|
665
|
+
"<html><b>x</b></html>",
|
|
666
|
+
"a<i>",
|
|
667
|
+
"<u>",
|
|
668
|
+
"x<sup>2</sup>",
|
|
669
|
+
"<sub>",
|
|
670
|
+
] {
|
|
671
|
+
assert!(has_html_tag(html.as_bytes()), "{html}");
|
|
672
|
+
}
|
|
673
|
+
for plain in ["<html>x</html>", "<br>", "<bb>", "b>", "<s>", "<su>", "<"] {
|
|
674
|
+
assert!(!has_html_tag(plain.as_bytes()), "{plain}");
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
#[test]
|
|
679
|
+
fn offsets_reject_overflow_instead_of_wrapping() {
|
|
680
|
+
assert_eq!(string_end(u32::MAX as usize - 1, 1).unwrap(), u32::MAX);
|
|
681
|
+
assert!(string_end(u32::MAX as usize, 1).is_err());
|
|
682
|
+
assert!(string_end(usize::MAX, 1).is_err());
|
|
683
|
+
}
|
|
684
|
+
}
|