imprimatur 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/CHANGELOG.md +8 -0
- data/Cargo.lock +835 -0
- data/Cargo.toml +44 -0
- data/LICENSE.txt +29 -0
- data/README.md +314 -0
- data/ext/imprimatur_native/cli/Cargo.toml +30 -0
- data/ext/imprimatur_native/cli/src/app.rs +183 -0
- data/ext/imprimatur_native/cli/src/cli/completion.rs +26 -0
- data/ext/imprimatur_native/cli/src/cli/grep.rs +359 -0
- data/ext/imprimatur_native/cli/src/cli/images.rs +137 -0
- data/ext/imprimatur_native/cli/src/cli/limits.rs +49 -0
- data/ext/imprimatur_native/cli/src/cli/markdown.rs +128 -0
- data/ext/imprimatur_native/cli/src/cli.rs +71 -0
- data/ext/imprimatur_native/cli/src/main.rs +15 -0
- data/ext/imprimatur_native/core/Cargo.toml +33 -0
- data/ext/imprimatur_native/core/LICENSE +29 -0
- data/ext/imprimatur_native/core/README.md +105 -0
- data/ext/imprimatur_native/core/src/doc/paragraph.rs +112 -0
- data/ext/imprimatur_native/core/src/doc/table.rs +41 -0
- data/ext/imprimatur_native/core/src/doc.rs +210 -0
- data/ext/imprimatur_native/core/src/docx.rs +343 -0
- data/ext/imprimatur_native/core/src/error.rs +113 -0
- data/ext/imprimatur_native/core/src/lib.rs +38 -0
- data/ext/imprimatur_native/core/src/opc.rs +51 -0
- data/ext/imprimatur_native/core/src/pdf/font.rs +96 -0
- data/ext/imprimatur_native/core/src/pdf/layout/wrap.rs +161 -0
- data/ext/imprimatur_native/core/src/pdf/layout.rs +280 -0
- data/ext/imprimatur_native/core/src/pdf/metrics.rs +139 -0
- data/ext/imprimatur_native/core/src/pdf/objects.rs +141 -0
- data/ext/imprimatur_native/core/src/pdf.rs +84 -0
- data/ext/imprimatur_native/core/src/read/budget.rs +140 -0
- data/ext/imprimatur_native/core/src/read/content.rs +266 -0
- data/ext/imprimatur_native/core/src/read/docx/styles.rs +118 -0
- data/ext/imprimatur_native/core/src/read/docx.rs +336 -0
- data/ext/imprimatur_native/core/src/read/image/dimensions.rs +215 -0
- data/ext/imprimatur_native/core/src/read/image.rs +233 -0
- data/ext/imprimatur_native/core/src/read/inflate.rs +44 -0
- data/ext/imprimatur_native/core/src/read/json.rs +172 -0
- data/ext/imprimatur_native/core/src/read/markdown.rs +254 -0
- data/ext/imprimatur_native/core/src/read/package.rs +191 -0
- data/ext/imprimatur_native/core/src/read/pdf/cmap.rs +216 -0
- data/ext/imprimatur_native/core/src/read/pdf/content/geometry.rs +79 -0
- data/ext/imprimatur_native/core/src/read/pdf/content/operands.rs +156 -0
- data/ext/imprimatur_native/core/src/read/pdf/content.rs +439 -0
- data/ext/imprimatur_native/core/src/read/pdf/crypt.rs +256 -0
- data/ext/imprimatur_native/core/src/read/pdf/document.rs +420 -0
- data/ext/imprimatur_native/core/src/read/pdf/filter.rs +258 -0
- data/ext/imprimatur_native/core/src/read/pdf/font.rs +315 -0
- data/ext/imprimatur_native/core/src/read/pdf/glyphs.rs +179 -0
- data/ext/imprimatur_native/core/src/read/pdf/glyphs.txt +6298 -0
- data/ext/imprimatur_native/core/src/read/pdf/images.rs +365 -0
- data/ext/imprimatur_native/core/src/read/pdf/layout.rs +451 -0
- data/ext/imprimatur_native/core/src/read/pdf/lexer.rs +255 -0
- data/ext/imprimatur_native/core/src/read/pdf/object/name.rs +44 -0
- data/ext/imprimatur_native/core/src/read/pdf/object.rs +261 -0
- data/ext/imprimatur_native/core/src/read/pdf/xref.rs +346 -0
- data/ext/imprimatur_native/core/src/read/pdf.rs +53 -0
- data/ext/imprimatur_native/core/src/read/span.rs +22 -0
- data/ext/imprimatur_native/core/src/read/xlsx/numbers.rs +155 -0
- data/ext/imprimatur_native/core/src/read/xlsx.rs +340 -0
- data/ext/imprimatur_native/core/src/read.rs +145 -0
- data/ext/imprimatur_native/core/src/style.rs +82 -0
- data/ext/imprimatur_native/core/src/test_support.rs +163 -0
- data/ext/imprimatur_native/core/src/xlsx/cell.rs +112 -0
- data/ext/imprimatur_native/core/src/xlsx/comment.rs +256 -0
- data/ext/imprimatur_native/core/src/xlsx/date.rs +173 -0
- data/ext/imprimatur_native/core/src/xlsx/format.rs +226 -0
- data/ext/imprimatur_native/core/src/xlsx/package.rs +268 -0
- data/ext/imprimatur_native/core/src/xlsx/range.rs +124 -0
- data/ext/imprimatur_native/core/src/xlsx/styles.rs +240 -0
- data/ext/imprimatur_native/core/src/xlsx/text.rs +122 -0
- data/ext/imprimatur_native/core/src/xlsx/validation.rs +258 -0
- data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +350 -0
- data/ext/imprimatur_native/core/src/xlsx.rs +120 -0
- data/ext/imprimatur_native/core/src/xml/reader.rs +238 -0
- data/ext/imprimatur_native/core/src/xml.rs +95 -0
- data/ext/imprimatur_native/core/src/zip/reader.rs +144 -0
- data/ext/imprimatur_native/core/src/zip.rs +161 -0
- data/ext/imprimatur_native/extconf.rb +45 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +21 -0
- data/ext/imprimatur_native/ffi/src/convert.rs +132 -0
- data/ext/imprimatur_native/ffi/src/document.rs +138 -0
- data/ext/imprimatur_native/ffi/src/lib.rs +301 -0
- data/ext/imprimatur_native/ffi/src/read.rs +182 -0
- data/ext/imprimatur_native/ffi/src/style.rs +85 -0
- data/ext/imprimatur_native/ffi/src/validation.rs +56 -0
- data/lib/imprimatur/document.rb +127 -0
- data/lib/imprimatur/error.rb +22 -0
- data/lib/imprimatur/formula.rb +13 -0
- data/lib/imprimatur/native.rb +17 -0
- data/lib/imprimatur/reader.rb +59 -0
- data/lib/imprimatur/style.rb +31 -0
- data/lib/imprimatur/validation.rb +35 -0
- data/lib/imprimatur/version.rb +5 -0
- data/lib/imprimatur/xlsx.rb +183 -0
- data/lib/imprimatur.rb +111 -0
- metadata +157 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
//! Data validation: drop-down lists and formula rules on ranges of cells
|
|
2
|
+
//! (ECMA-376 Part 1, 18.3.1.32).
|
|
3
|
+
|
|
4
|
+
#[cfg(test)]
|
|
5
|
+
mod tests;
|
|
6
|
+
|
|
7
|
+
use super::range::CellRange;
|
|
8
|
+
use super::text::escape;
|
|
9
|
+
use crate::xml::XmlWriter;
|
|
10
|
+
use crate::{Error, Result};
|
|
11
|
+
|
|
12
|
+
/// Excel's limit on a list typed into the rule, commas included.
|
|
13
|
+
const MAX_LIST: usize = 255;
|
|
14
|
+
/// Excel's limit on a formula.
|
|
15
|
+
const MAX_FORMULA: usize = 8_192;
|
|
16
|
+
/// Excel's limits on the titles and on the messages of prompts and alerts.
|
|
17
|
+
const MAX_TITLE: usize = 32;
|
|
18
|
+
const MAX_MESSAGE: usize = 255;
|
|
19
|
+
|
|
20
|
+
/// What happens when someone types a value the rule rejects.
|
|
21
|
+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
|
|
22
|
+
pub enum Alert {
|
|
23
|
+
/// The value is refused.
|
|
24
|
+
#[default]
|
|
25
|
+
Stop,
|
|
26
|
+
/// The value is kept if the user confirms it.
|
|
27
|
+
Warning,
|
|
28
|
+
/// The value is kept; the message only informs.
|
|
29
|
+
Information,
|
|
30
|
+
/// The value is kept silently: lists still offer their drop-down.
|
|
31
|
+
Off,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
35
|
+
enum Kind {
|
|
36
|
+
List,
|
|
37
|
+
Custom,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// A rule for the cells of a range, added with
|
|
41
|
+
/// [`Worksheet::add_validation`](super::Worksheet::add_validation):
|
|
42
|
+
///
|
|
43
|
+
/// ```
|
|
44
|
+
/// use imprimatur::xlsx::{Validation, Workbook};
|
|
45
|
+
///
|
|
46
|
+
/// let mut book = Workbook::new();
|
|
47
|
+
/// let sheet = book.add_sheet("Orders")?;
|
|
48
|
+
/// let yes_no = Validation::list(["Yes", "No"])?.error("Invalid", "Pick Yes or No.")?;
|
|
49
|
+
/// sheet.add_validation("C2:C100".parse()?, yes_no)?;
|
|
50
|
+
/// sheet.add_validation("D2:D100".parse()?, Validation::list_from("Units!$A$2:$A$20")?)?;
|
|
51
|
+
/// # Ok::<(), imprimatur::Error>(())
|
|
52
|
+
/// ```
|
|
53
|
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
54
|
+
pub struct Validation {
|
|
55
|
+
kind: Kind,
|
|
56
|
+
formula: String,
|
|
57
|
+
allow_blank: bool,
|
|
58
|
+
dropdown: bool,
|
|
59
|
+
alert: Alert,
|
|
60
|
+
prompt_title: Option<String>,
|
|
61
|
+
prompt: Option<String>,
|
|
62
|
+
error_title: Option<String>,
|
|
63
|
+
error: Option<String>,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
impl Validation {
|
|
67
|
+
const fn new(kind: Kind, formula: String) -> Self {
|
|
68
|
+
Self {
|
|
69
|
+
kind,
|
|
70
|
+
formula,
|
|
71
|
+
allow_blank: true,
|
|
72
|
+
dropdown: true,
|
|
73
|
+
alert: Alert::Stop,
|
|
74
|
+
prompt_title: None,
|
|
75
|
+
prompt: None,
|
|
76
|
+
error_title: None,
|
|
77
|
+
error: None,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// A drop-down of these values. Excel keeps such a list in the rule
|
|
82
|
+
/// itself, so it is short: 255 characters with the commas between values.
|
|
83
|
+
///
|
|
84
|
+
/// # Errors
|
|
85
|
+
/// [`Error::InvalidValidation`] for no values, a value with a comma (the
|
|
86
|
+
/// separator) or a control character, or a list over 255 characters.
|
|
87
|
+
pub fn list<S: AsRef<str>>(values: impl IntoIterator<Item = S>) -> Result<Self> {
|
|
88
|
+
let mut joined = String::new();
|
|
89
|
+
for (index, value) in values.into_iter().enumerate() {
|
|
90
|
+
let value = value.as_ref();
|
|
91
|
+
if value.contains(',') || value.chars().any(char::is_control) {
|
|
92
|
+
return Err(invalid(format!(
|
|
93
|
+
"list value {value:?} has a comma or control character"
|
|
94
|
+
)));
|
|
95
|
+
}
|
|
96
|
+
if index > 0 {
|
|
97
|
+
joined.push(',');
|
|
98
|
+
}
|
|
99
|
+
joined.push_str(value);
|
|
100
|
+
}
|
|
101
|
+
if joined.is_empty() {
|
|
102
|
+
return Err(invalid("a list needs at least one value".to_owned()));
|
|
103
|
+
}
|
|
104
|
+
let length = joined.chars().count();
|
|
105
|
+
(length <= MAX_LIST).ok_or_else(|| {
|
|
106
|
+
invalid(format!("the list is {length} characters; Excel takes {MAX_LIST} at most"))
|
|
107
|
+
})?;
|
|
108
|
+
// A formula string literal: quoted, with its quotes doubled.
|
|
109
|
+
Ok(Self::new(Kind::List, format!("\"{}\"", joined.replace('"', "\"\""))))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// A drop-down of the values in a range, such as `Units!$A$2:$A$20`, or
|
|
113
|
+
/// of what a formula returns. A leading `=` is accepted and dropped.
|
|
114
|
+
///
|
|
115
|
+
/// # Errors
|
|
116
|
+
/// [`Error::InvalidValidation`] when empty or over 8,192 characters.
|
|
117
|
+
pub fn list_from(source: impl Into<String>) -> Result<Self> {
|
|
118
|
+
Ok(Self::new(Kind::List, formula(source.into())?))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Accepts a value when the formula is true. References in it are
|
|
122
|
+
/// relative to the top-left cell of the range, as in Excel: on `B2:B9`,
|
|
123
|
+
/// `LEN(B2)<=100` checks each cell's own length.
|
|
124
|
+
///
|
|
125
|
+
/// # Errors
|
|
126
|
+
/// As [`list_from`](Self::list_from).
|
|
127
|
+
pub fn custom(formula_text: impl Into<String>) -> Result<Self> {
|
|
128
|
+
Ok(Self::new(Kind::Custom, formula(formula_text.into())?))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Whether an empty cell passes, true by default.
|
|
132
|
+
#[must_use]
|
|
133
|
+
pub const fn allow_blank(mut self, allow: bool) -> Self {
|
|
134
|
+
self.allow_blank = allow;
|
|
135
|
+
self
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Whether a list shows its in-cell drop-down arrow, true by default.
|
|
139
|
+
#[must_use]
|
|
140
|
+
pub const fn dropdown(mut self, shown: bool) -> Self {
|
|
141
|
+
self.dropdown = shown;
|
|
142
|
+
self
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#[must_use]
|
|
146
|
+
pub const fn alert(mut self, alert: Alert) -> Self {
|
|
147
|
+
self.alert = alert;
|
|
148
|
+
self
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// A note shown while a cell of the range is selected; either part may
|
|
152
|
+
/// be empty.
|
|
153
|
+
///
|
|
154
|
+
/// # Errors
|
|
155
|
+
/// [`Error::InvalidValidation`] for a title over 32 characters or a
|
|
156
|
+
/// message over 255.
|
|
157
|
+
pub fn prompt(mut self, title: impl Into<String>, message: impl Into<String>) -> Result<Self> {
|
|
158
|
+
self.prompt_title = text("prompt title", title.into(), MAX_TITLE)?;
|
|
159
|
+
self.prompt = text("prompt", message.into(), MAX_MESSAGE)?;
|
|
160
|
+
Ok(self)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// The title and message of the alert a rejected value raises; Excel
|
|
164
|
+
/// uses its own when they are empty.
|
|
165
|
+
///
|
|
166
|
+
/// # Errors
|
|
167
|
+
/// As [`prompt`](Self::prompt).
|
|
168
|
+
pub fn error(mut self, title: impl Into<String>, message: impl Into<String>) -> Result<Self> {
|
|
169
|
+
self.error_title = text("error title", title.into(), MAX_TITLE)?;
|
|
170
|
+
self.error = text("error", message.into(), MAX_MESSAGE)?;
|
|
171
|
+
Ok(self)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// `<dataValidation>` for `range`.
|
|
175
|
+
pub(crate) fn write(&self, xml: &mut XmlWriter, range: &CellRange) {
|
|
176
|
+
let sqref = range.to_string();
|
|
177
|
+
let mut attrs = vec![(
|
|
178
|
+
"type",
|
|
179
|
+
match self.kind {
|
|
180
|
+
Kind::List => "list",
|
|
181
|
+
Kind::Custom => "custom",
|
|
182
|
+
},
|
|
183
|
+
)];
|
|
184
|
+
match self.alert {
|
|
185
|
+
Alert::Stop | Alert::Off => {}
|
|
186
|
+
Alert::Warning => attrs.push(("errorStyle", "warning")),
|
|
187
|
+
Alert::Information => attrs.push(("errorStyle", "information")),
|
|
188
|
+
}
|
|
189
|
+
if self.allow_blank {
|
|
190
|
+
attrs.push(("allowBlank", "1"));
|
|
191
|
+
}
|
|
192
|
+
// Inverted in the file format: showDropDown="1" hides the arrow.
|
|
193
|
+
if self.kind == Kind::List && !self.dropdown {
|
|
194
|
+
attrs.push(("showDropDown", "1"));
|
|
195
|
+
}
|
|
196
|
+
if self.prompt_title.is_some() || self.prompt.is_some() {
|
|
197
|
+
attrs.push(("showInputMessage", "1"));
|
|
198
|
+
}
|
|
199
|
+
if self.alert != Alert::Off {
|
|
200
|
+
attrs.push(("showErrorMessage", "1"));
|
|
201
|
+
}
|
|
202
|
+
let texts = [
|
|
203
|
+
("errorTitle", &self.error_title),
|
|
204
|
+
("error", &self.error),
|
|
205
|
+
("promptTitle", &self.prompt_title),
|
|
206
|
+
("prompt", &self.prompt),
|
|
207
|
+
]
|
|
208
|
+
.map(|(name, value)| (name, value.as_deref().map(escape)));
|
|
209
|
+
for (name, value) in &texts {
|
|
210
|
+
if let Some(value) = value {
|
|
211
|
+
attrs.push((name, value));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
attrs.push(("sqref", &sqref));
|
|
215
|
+
xml.open("dataValidation", &attrs);
|
|
216
|
+
xml.element("formula1", &[], &escape(&self.formula));
|
|
217
|
+
xml.close("dataValidation");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// `<dataValidations>`, nothing when there are none.
|
|
222
|
+
pub fn write_all(xml: &mut XmlWriter, validations: &[(CellRange, Validation)]) {
|
|
223
|
+
if validations.is_empty() {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
let count = validations.len().to_string();
|
|
227
|
+
xml.open("dataValidations", &[("count", &count)]);
|
|
228
|
+
for (range, validation) in validations {
|
|
229
|
+
validation.write(xml, range);
|
|
230
|
+
}
|
|
231
|
+
xml.close("dataValidations");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const fn invalid(reason: String) -> Error {
|
|
235
|
+
Error::InvalidValidation(reason)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fn formula(mut source: String) -> Result<String> {
|
|
239
|
+
if source.starts_with('=') {
|
|
240
|
+
source.remove(0);
|
|
241
|
+
}
|
|
242
|
+
let length = source.chars().count();
|
|
243
|
+
if source.trim().is_empty() || length > MAX_FORMULA {
|
|
244
|
+
return Err(invalid(format!(
|
|
245
|
+
"a formula must be 1 to {MAX_FORMULA} characters, not {length}"
|
|
246
|
+
)));
|
|
247
|
+
}
|
|
248
|
+
Ok(source)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// `None` for empty text, which Excel treats as absent.
|
|
252
|
+
fn text(what: &str, value: String, limit: usize) -> Result<Option<String>> {
|
|
253
|
+
let length = value.chars().count();
|
|
254
|
+
(length <= limit).ok_or_else(|| {
|
|
255
|
+
invalid(format!("the {what} is {length} characters; Excel takes {limit} at most"))
|
|
256
|
+
})?;
|
|
257
|
+
Ok((!value.is_empty()).then_some(value))
|
|
258
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#[cfg(test)]
|
|
2
|
+
mod tests;
|
|
3
|
+
|
|
4
|
+
use std::collections::{BTreeMap, HashMap};
|
|
5
|
+
use std::sync::Arc;
|
|
6
|
+
|
|
7
|
+
use super::text::check_length;
|
|
8
|
+
use super::{Cell, CellRange, Comment, Format, Validation};
|
|
9
|
+
use crate::{Error, Result};
|
|
10
|
+
|
|
11
|
+
/// Rows in a sheet (Excel 2007 and later).
|
|
12
|
+
pub const MAX_ROWS: u32 = 1_048_576;
|
|
13
|
+
/// Columns in a sheet: `A` to `XFD`.
|
|
14
|
+
pub const MAX_COLUMNS: u16 = 16_384;
|
|
15
|
+
const MAX_FORMULA: usize = 8_192;
|
|
16
|
+
const MAX_COLUMN_WIDTH: f64 = 255.0;
|
|
17
|
+
const DATE_FORMAT: &str = "yyyy-mm-dd";
|
|
18
|
+
const DATE_TIME_FORMAT: &str = "yyyy-mm-dd hh:mm:ss";
|
|
19
|
+
/// Excel's limit on distinct cell formats in a workbook.
|
|
20
|
+
pub const MAX_FORMATS: usize = 64_000;
|
|
21
|
+
|
|
22
|
+
/// A written cell and its format: 0 for the default look, otherwise
|
|
23
|
+
/// `formats[style - 1]`.
|
|
24
|
+
#[derive(Debug)]
|
|
25
|
+
pub struct Entry {
|
|
26
|
+
pub cell: Cell,
|
|
27
|
+
pub style: u32,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// One sheet's cells, kept sparse and in row-major order, with the formats
|
|
31
|
+
/// they use interned once each.
|
|
32
|
+
#[derive(Debug)]
|
|
33
|
+
pub struct Worksheet {
|
|
34
|
+
name: String,
|
|
35
|
+
rows: BTreeMap<u32, BTreeMap<u16, Entry>>,
|
|
36
|
+
next_row: u32,
|
|
37
|
+
/// Shared with `format_ids`, so each format is stored once.
|
|
38
|
+
formats: Vec<Arc<Format>>,
|
|
39
|
+
format_ids: HashMap<Arc<Format>, u32>,
|
|
40
|
+
/// The style of an unformatted date, then of an unformatted date and time.
|
|
41
|
+
date_styles: [Option<u32>; 2],
|
|
42
|
+
widths: BTreeMap<u16, f64>,
|
|
43
|
+
right_to_left: bool,
|
|
44
|
+
validations: Vec<(CellRange, Validation)>,
|
|
45
|
+
comments: BTreeMap<(u32, u16), Comment>,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl Worksheet {
|
|
49
|
+
pub(super) fn new(name: String) -> Self {
|
|
50
|
+
Self {
|
|
51
|
+
name,
|
|
52
|
+
rows: BTreeMap::new(),
|
|
53
|
+
next_row: 0,
|
|
54
|
+
formats: Vec::new(),
|
|
55
|
+
format_ids: HashMap::new(),
|
|
56
|
+
date_styles: [None; 2],
|
|
57
|
+
widths: BTreeMap::new(),
|
|
58
|
+
right_to_left: false,
|
|
59
|
+
validations: Vec::new(),
|
|
60
|
+
comments: BTreeMap::new(),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[must_use]
|
|
65
|
+
pub fn name(&self) -> &str {
|
|
66
|
+
&self.name
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Sets one cell, replacing what was there. Row and column are zero-based.
|
|
70
|
+
///
|
|
71
|
+
/// # Errors
|
|
72
|
+
/// Out-of-range positions, text or formulas over Excel's limits, and
|
|
73
|
+
/// non-finite numbers.
|
|
74
|
+
pub fn write(&mut self, row: u32, column: u16, value: impl Into<Cell>) -> Result<&mut Self> {
|
|
75
|
+
self.write_cell(row, column, value.into(), None)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// As [`write`](Self::write), with a format.
|
|
79
|
+
///
|
|
80
|
+
/// # Errors
|
|
81
|
+
/// As [`write`](Self::write).
|
|
82
|
+
pub fn write_with_format(
|
|
83
|
+
&mut self,
|
|
84
|
+
row: u32,
|
|
85
|
+
column: u16,
|
|
86
|
+
value: impl Into<Cell>,
|
|
87
|
+
format: &Format,
|
|
88
|
+
) -> Result<&mut Self> {
|
|
89
|
+
self.write_cell(row, column, value.into(), Some(format))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Writes `cells` from column `A` of the row after the last one written.
|
|
93
|
+
/// Nothing is written unless every cell is valid.
|
|
94
|
+
///
|
|
95
|
+
/// # Errors
|
|
96
|
+
/// As [`write`](Self::write); also when the sheet is full.
|
|
97
|
+
pub fn append_row<C: Into<Cell>>(
|
|
98
|
+
&mut self,
|
|
99
|
+
cells: impl IntoIterator<Item = C>,
|
|
100
|
+
) -> Result<&mut Self> {
|
|
101
|
+
self.append_sparse_row(cells.into_iter().map(|cell| Some(cell.into())), None)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// As [`append_row`](Self::append_row), every cell with `format`.
|
|
105
|
+
///
|
|
106
|
+
/// # Errors
|
|
107
|
+
/// As [`append_row`](Self::append_row).
|
|
108
|
+
pub fn append_row_with_format<C: Into<Cell>>(
|
|
109
|
+
&mut self,
|
|
110
|
+
cells: impl IntoIterator<Item = C>,
|
|
111
|
+
format: &Format,
|
|
112
|
+
) -> Result<&mut Self> {
|
|
113
|
+
self.append_sparse_row(cells.into_iter().map(|cell| Some(cell.into())), Some(format))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// As [`append_row`](Self::append_row), where `None` leaves its column
|
|
117
|
+
/// empty and `format`, if any, applies to every cell written.
|
|
118
|
+
///
|
|
119
|
+
/// # Errors
|
|
120
|
+
/// As [`append_row`](Self::append_row).
|
|
121
|
+
pub fn append_sparse_row(
|
|
122
|
+
&mut self,
|
|
123
|
+
cells: impl IntoIterator<Item = Option<Cell>>,
|
|
124
|
+
format: Option<&Format>,
|
|
125
|
+
) -> Result<&mut Self> {
|
|
126
|
+
self.append_styled_row(cells.into_iter().map(|cell| (cell, format)))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// As [`append_sparse_row`](Self::append_sparse_row), each cell with a
|
|
130
|
+
/// format of its own.
|
|
131
|
+
///
|
|
132
|
+
/// # Errors
|
|
133
|
+
/// As [`append_row`](Self::append_row).
|
|
134
|
+
pub fn append_styled_row<'f>(
|
|
135
|
+
&mut self,
|
|
136
|
+
cells: impl IntoIterator<Item = (Option<Cell>, Option<&'f Format>)>,
|
|
137
|
+
) -> Result<&mut Self> {
|
|
138
|
+
let row = self.next_row;
|
|
139
|
+
check_row(row)?;
|
|
140
|
+
let mut valid = Vec::new();
|
|
141
|
+
for (index, (cell, format)) in cells.into_iter().enumerate() {
|
|
142
|
+
let column = u16::try_from(index)
|
|
143
|
+
.ok()
|
|
144
|
+
.filter(|&column| column < MAX_COLUMNS)
|
|
145
|
+
.ok_or(Error::ColumnOutOfRange(index as u64))?;
|
|
146
|
+
if let Some(cell) = cell {
|
|
147
|
+
validate(&cell)?;
|
|
148
|
+
valid.push((column, cell, format));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Formats are interned only once the whole row is known to be valid.
|
|
152
|
+
let mut values = BTreeMap::new();
|
|
153
|
+
for (column, cell, format) in valid {
|
|
154
|
+
let style = self.style_for(&cell, format)?;
|
|
155
|
+
values.insert(column, Entry { cell, style });
|
|
156
|
+
}
|
|
157
|
+
if !values.is_empty() {
|
|
158
|
+
self.rows.insert(row, values);
|
|
159
|
+
}
|
|
160
|
+
self.next_row = row + 1;
|
|
161
|
+
Ok(self)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Sets a column's width, in characters of the default font.
|
|
165
|
+
///
|
|
166
|
+
/// # Errors
|
|
167
|
+
/// [`Error::ColumnOutOfRange`], or [`Error::InvalidColumnWidth`] outside
|
|
168
|
+
/// 0 to 255.
|
|
169
|
+
pub fn set_column_width(&mut self, column: u16, width: f64) -> Result<&mut Self> {
|
|
170
|
+
check_column(column)?;
|
|
171
|
+
(width.is_finite() && (0.0..=MAX_COLUMN_WIDTH).contains(&width))
|
|
172
|
+
.ok_or(Error::InvalidColumnWidth(width))?;
|
|
173
|
+
self.widths.insert(column, width);
|
|
174
|
+
Ok(self)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// Lays the sheet out right to left: column `A` on the right, as Excel
|
|
178
|
+
/// shows sheets for Arabic and Hebrew.
|
|
179
|
+
pub const fn set_right_to_left(&mut self, right_to_left: bool) -> &mut Self {
|
|
180
|
+
self.right_to_left = right_to_left;
|
|
181
|
+
self
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#[must_use]
|
|
185
|
+
pub const fn is_right_to_left(&self) -> bool {
|
|
186
|
+
self.right_to_left
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Applies a rule to the cells of `range`, whether or not they hold values.
|
|
190
|
+
///
|
|
191
|
+
/// # Errors
|
|
192
|
+
/// [`Error::OverlappingValidation`] when the range shares a cell with
|
|
193
|
+
/// one already validated: Excel applies a single rule per cell.
|
|
194
|
+
pub fn add_validation(
|
|
195
|
+
&mut self,
|
|
196
|
+
range: CellRange,
|
|
197
|
+
validation: Validation,
|
|
198
|
+
) -> Result<&mut Self> {
|
|
199
|
+
if self.validations.iter().any(|(taken, _)| taken.overlaps(&range)) {
|
|
200
|
+
return Err(Error::OverlappingValidation(range.to_string()));
|
|
201
|
+
}
|
|
202
|
+
self.validations.push((range, validation));
|
|
203
|
+
Ok(self)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// The rules added, in order, each with its range.
|
|
207
|
+
#[must_use]
|
|
208
|
+
pub fn validations(&self) -> &[(CellRange, Validation)] {
|
|
209
|
+
&self.validations
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// Attaches a comment to a cell, replacing any it had. The cell need not
|
|
213
|
+
/// hold a value.
|
|
214
|
+
///
|
|
215
|
+
/// # Errors
|
|
216
|
+
/// [`Error::RowOutOfRange`] or [`Error::ColumnOutOfRange`].
|
|
217
|
+
pub fn set_comment(&mut self, row: u32, column: u16, comment: Comment) -> Result<&mut Self> {
|
|
218
|
+
check_row(row)?;
|
|
219
|
+
check_column(column)?;
|
|
220
|
+
self.comments.insert((row, column), comment);
|
|
221
|
+
Ok(self)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// The comment on a cell, if it has one.
|
|
225
|
+
#[must_use]
|
|
226
|
+
pub fn comment(&self, row: u32, column: u16) -> Option<&Comment> {
|
|
227
|
+
self.comments.get(&(row, column))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// The cell at a position, if one was written.
|
|
231
|
+
#[must_use]
|
|
232
|
+
pub fn get(&self, row: u32, column: u16) -> Option<&Cell> {
|
|
233
|
+
Some(&self.entry(row, column)?.cell)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// The format of the cell at a position, if it has one.
|
|
237
|
+
#[must_use]
|
|
238
|
+
pub fn format_at(&self, row: u32, column: u16) -> Option<&Format> {
|
|
239
|
+
let style = self.entry(row, column)?.style;
|
|
240
|
+
self.formats.get(usize::try_from(style).ok()?.checked_sub(1)?).map(AsRef::as_ref)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// The zero-based row an [`append_row`](Self::append_row) writes next.
|
|
244
|
+
#[must_use]
|
|
245
|
+
pub const fn next_row(&self) -> u32 {
|
|
246
|
+
self.next_row
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// Rows that hold at least one cell, in order, each with its cells in
|
|
250
|
+
/// column order.
|
|
251
|
+
pub(crate) fn rows(&self) -> impl Iterator<Item = (u32, &BTreeMap<u16, Entry>)> {
|
|
252
|
+
self.rows.iter().map(|(&row, cells)| (row, cells))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/// The formats cells refer to, `style - 1` indexed.
|
|
256
|
+
pub(crate) fn formats(&self) -> &[Arc<Format>] {
|
|
257
|
+
&self.formats
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
pub(crate) const fn column_widths(&self) -> &BTreeMap<u16, f64> {
|
|
261
|
+
&self.widths
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// Comments by `(row, column)`, in row-major order.
|
|
265
|
+
pub(crate) const fn comments(&self) -> &BTreeMap<(u32, u16), Comment> {
|
|
266
|
+
&self.comments
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fn entry(&self, row: u32, column: u16) -> Option<&Entry> {
|
|
270
|
+
self.rows.get(&row)?.get(&column)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn write_cell(
|
|
274
|
+
&mut self,
|
|
275
|
+
row: u32,
|
|
276
|
+
column: u16,
|
|
277
|
+
cell: Cell,
|
|
278
|
+
format: Option<&Format>,
|
|
279
|
+
) -> Result<&mut Self> {
|
|
280
|
+
check_row(row)?;
|
|
281
|
+
check_column(column)?;
|
|
282
|
+
validate(&cell)?;
|
|
283
|
+
let style = self.style_for(&cell, format)?;
|
|
284
|
+
self.rows.entry(row).or_default().insert(column, Entry { cell, style });
|
|
285
|
+
self.next_row = self.next_row.max(row + 1);
|
|
286
|
+
Ok(self)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// The style id for a cell: dates without a number format get one, so
|
|
290
|
+
/// they do not show as bare serial numbers.
|
|
291
|
+
fn style_for(&mut self, cell: &Cell, format: Option<&Format>) -> Result<u32> {
|
|
292
|
+
if let Cell::DateTime(date) = cell
|
|
293
|
+
&& !format.is_some_and(Format::has_number_format)
|
|
294
|
+
{
|
|
295
|
+
let slot = usize::from(date.has_time());
|
|
296
|
+
// The common case, a date with no format at all, is cached.
|
|
297
|
+
if format.is_none()
|
|
298
|
+
&& let Some(id) = self.date_styles[slot]
|
|
299
|
+
{
|
|
300
|
+
return Ok(id);
|
|
301
|
+
}
|
|
302
|
+
let code = if date.has_time() { DATE_TIME_FORMAT } else { DATE_FORMAT };
|
|
303
|
+
let mut dated = format.cloned().unwrap_or_default();
|
|
304
|
+
dated.number_code = Some(code.to_owned());
|
|
305
|
+
let id = self.intern(&dated)?;
|
|
306
|
+
if format.is_none() {
|
|
307
|
+
self.date_styles[slot] = Some(id);
|
|
308
|
+
}
|
|
309
|
+
return Ok(id);
|
|
310
|
+
}
|
|
311
|
+
match format {
|
|
312
|
+
Some(format) if *format != Format::default() => self.intern(format),
|
|
313
|
+
_ => Ok(0),
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
fn intern(&mut self, format: &Format) -> Result<u32> {
|
|
318
|
+
if let Some(&id) = self.format_ids.get(format) {
|
|
319
|
+
return Ok(id);
|
|
320
|
+
}
|
|
321
|
+
let id = u32::try_from(self.formats.len() + 1)
|
|
322
|
+
.ok()
|
|
323
|
+
.filter(|_| self.formats.len() < MAX_FORMATS)
|
|
324
|
+
.ok_or(Error::TooManyFormats)?;
|
|
325
|
+
let shared = Arc::new(format.clone());
|
|
326
|
+
self.formats.push(Arc::clone(&shared));
|
|
327
|
+
self.format_ids.insert(shared, id);
|
|
328
|
+
Ok(id)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
pub(super) fn check_row(row: u32) -> Result<()> {
|
|
333
|
+
(row < MAX_ROWS).ok_or(Error::RowOutOfRange(row.into()))
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
pub(super) fn check_column(column: u16) -> Result<()> {
|
|
337
|
+
(column < MAX_COLUMNS).ok_or(Error::ColumnOutOfRange(column.into()))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fn validate(cell: &Cell) -> Result<()> {
|
|
341
|
+
match cell {
|
|
342
|
+
Cell::Text(text) => check_length(text),
|
|
343
|
+
Cell::Formula(formula) => {
|
|
344
|
+
let length = formula.chars().count();
|
|
345
|
+
(length <= MAX_FORMULA).ok_or(Error::FormulaTooLong(length))
|
|
346
|
+
}
|
|
347
|
+
Cell::Number(number) => number.is_finite().ok_or(Error::NonFiniteNumber(*number)),
|
|
348
|
+
Cell::Bool(_) | Cell::DateTime(_) => Ok(()),
|
|
349
|
+
}
|
|
350
|
+
}
|