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,124 @@
|
|
|
1
|
+
//! A1-style references and rectangular ranges, the way data validations and
|
|
2
|
+
//! comments address cells.
|
|
3
|
+
|
|
4
|
+
#[cfg(test)]
|
|
5
|
+
mod tests;
|
|
6
|
+
|
|
7
|
+
use std::fmt;
|
|
8
|
+
use std::str::FromStr;
|
|
9
|
+
|
|
10
|
+
use super::cell::push_cell_reference;
|
|
11
|
+
use super::worksheet::{MAX_COLUMNS, MAX_ROWS, check_column, check_row};
|
|
12
|
+
use crate::{Error, Result};
|
|
13
|
+
|
|
14
|
+
/// A rectangle of cells, zero-based and inclusive: `B2:D9` is rows 1 to 8 of
|
|
15
|
+
/// columns 1 to 3. Parsed from and displayed as A1 text.
|
|
16
|
+
///
|
|
17
|
+
/// ```
|
|
18
|
+
/// use imprimatur::xlsx::CellRange;
|
|
19
|
+
///
|
|
20
|
+
/// let range: CellRange = "D9:b2".parse()?;
|
|
21
|
+
/// assert_eq!(range.to_string(), "B2:D9");
|
|
22
|
+
/// assert_eq!(range.top_left(), (1, 1));
|
|
23
|
+
/// # Ok::<(), imprimatur::Error>(())
|
|
24
|
+
/// ```
|
|
25
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
26
|
+
pub struct CellRange {
|
|
27
|
+
top: u32,
|
|
28
|
+
left: u16,
|
|
29
|
+
bottom: u32,
|
|
30
|
+
right: u16,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl CellRange {
|
|
34
|
+
/// The range between two `(row, column)` corners, in either order.
|
|
35
|
+
///
|
|
36
|
+
/// # Errors
|
|
37
|
+
/// [`Error::RowOutOfRange`] or [`Error::ColumnOutOfRange`].
|
|
38
|
+
pub fn new(first: (u32, u16), last: (u32, u16)) -> Result<Self> {
|
|
39
|
+
for (row, column) in [first, last] {
|
|
40
|
+
check_row(row)?;
|
|
41
|
+
check_column(column)?;
|
|
42
|
+
}
|
|
43
|
+
Ok(Self {
|
|
44
|
+
top: first.0.min(last.0),
|
|
45
|
+
left: first.1.min(last.1),
|
|
46
|
+
bottom: first.0.max(last.0),
|
|
47
|
+
right: first.1.max(last.1),
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// A single cell.
|
|
52
|
+
///
|
|
53
|
+
/// # Errors
|
|
54
|
+
/// As [`new`](Self::new).
|
|
55
|
+
pub fn cell(row: u32, column: u16) -> Result<Self> {
|
|
56
|
+
Self::new((row, column), (row, column))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[must_use]
|
|
60
|
+
pub const fn top_left(&self) -> (u32, u16) {
|
|
61
|
+
(self.top, self.left)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[must_use]
|
|
65
|
+
pub const fn bottom_right(&self) -> (u32, u16) {
|
|
66
|
+
(self.bottom, self.right)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pub(crate) const fn overlaps(&self, other: &Self) -> bool {
|
|
70
|
+
self.top <= other.bottom
|
|
71
|
+
&& other.top <= self.bottom
|
|
72
|
+
&& self.left <= other.right
|
|
73
|
+
&& other.left <= self.right
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl FromStr for CellRange {
|
|
78
|
+
type Err = Error;
|
|
79
|
+
|
|
80
|
+
/// `B2`, or `B2:D9` with the corners in either order. No `$` markers,
|
|
81
|
+
/// sheet names or whole-column ranges.
|
|
82
|
+
fn from_str(text: &str) -> Result<Self> {
|
|
83
|
+
let invalid = |_| Error::InvalidRange(text.to_owned());
|
|
84
|
+
let (first, last) = text.split_once(':').unwrap_or((text, text));
|
|
85
|
+
let first = parse_cell_reference(first).map_err(invalid)?;
|
|
86
|
+
let last = parse_cell_reference(last).map_err(invalid)?;
|
|
87
|
+
Self::new(first, last)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
impl fmt::Display for CellRange {
|
|
92
|
+
fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
93
|
+
let mut text = String::with_capacity(21);
|
|
94
|
+
push_cell_reference(&mut text, self.top, self.left);
|
|
95
|
+
if (self.top, self.left) != (self.bottom, self.right) {
|
|
96
|
+
text.push(':');
|
|
97
|
+
push_cell_reference(&mut text, self.bottom, self.right);
|
|
98
|
+
}
|
|
99
|
+
out.write_str(&text)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// An A1 reference such as `B2`, its letters in either case, as a zero-based
|
|
104
|
+
/// `(row, column)`.
|
|
105
|
+
///
|
|
106
|
+
/// # Errors
|
|
107
|
+
/// [`Error::InvalidRange`] for anything else, or a cell beyond `XFD1048576`.
|
|
108
|
+
pub fn parse_cell_reference(reference: &str) -> Result<(u32, u16)> {
|
|
109
|
+
let invalid = || Error::InvalidRange(reference.to_owned());
|
|
110
|
+
let split = reference.find(|ch: char| !ch.is_ascii_alphabetic()).ok_or_else(invalid)?;
|
|
111
|
+
let (letters, digits) = reference.split_at(split);
|
|
112
|
+
let digits_valid = digits.bytes().all(|byte| byte.is_ascii_digit()) && !digits.starts_with('0');
|
|
113
|
+
((1..=3).contains(&letters.len()) && digits_valid).ok_or_else(invalid)?;
|
|
114
|
+
// Bijective base 26: A is 1, Z 26, AA 27. Three letters stay under 2**15.
|
|
115
|
+
let column = letters
|
|
116
|
+
.bytes()
|
|
117
|
+
.fold(0_u32, |sum, letter| sum * 26 + u32::from(letter.to_ascii_uppercase() - b'A') + 1);
|
|
118
|
+
let column = u16::try_from(column - 1).ok().filter(|&column| column < MAX_COLUMNS);
|
|
119
|
+
let row = digits.parse::<u32>().ok().filter(|&row| row <= MAX_ROWS);
|
|
120
|
+
match (row, column) {
|
|
121
|
+
(Some(row), Some(column)) => Ok((row - 1, column)),
|
|
122
|
+
_ => Err(invalid()),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
//! `xl/styles.xml`: every distinct format in the workbook, split into the
|
|
2
|
+
//! number formats, fonts, fills and borders it refers to.
|
|
3
|
+
|
|
4
|
+
#[cfg(test)]
|
|
5
|
+
mod tests;
|
|
6
|
+
|
|
7
|
+
use std::collections::HashMap;
|
|
8
|
+
use std::hash::Hash;
|
|
9
|
+
use std::sync::Arc;
|
|
10
|
+
|
|
11
|
+
use super::format::{Border, DEFAULT_FONT, Font, Format};
|
|
12
|
+
use super::package::MAIN_NS;
|
|
13
|
+
use super::worksheet::{MAX_FORMATS, Worksheet};
|
|
14
|
+
use crate::style::{Align, Color};
|
|
15
|
+
use crate::xml::XmlWriter;
|
|
16
|
+
use crate::{Error, Result};
|
|
17
|
+
|
|
18
|
+
/// The first id Excel leaves free for custom number formats.
|
|
19
|
+
const FIRST_CUSTOM_NUMBER_FORMAT: u32 = 164;
|
|
20
|
+
|
|
21
|
+
/// Distinct values in order of first use, numbered from a starting id.
|
|
22
|
+
/// There are at most [`MAX_FORMATS`] of them, so ids stay small.
|
|
23
|
+
struct Table<T> {
|
|
24
|
+
ids: HashMap<T, u32>,
|
|
25
|
+
order: Vec<T>,
|
|
26
|
+
next: u32,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl<T: Copy + Eq + Hash> Table<T> {
|
|
30
|
+
fn new(first: u32) -> Self {
|
|
31
|
+
Self { ids: HashMap::new(), order: Vec::new(), next: first }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn id(&mut self, value: &T) -> u32 {
|
|
35
|
+
if let Some(&id) = self.ids.get(value) {
|
|
36
|
+
return id;
|
|
37
|
+
}
|
|
38
|
+
let id = self.next;
|
|
39
|
+
self.next += 1;
|
|
40
|
+
self.order.push(*value);
|
|
41
|
+
self.ids.insert(*value, id);
|
|
42
|
+
id
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// The workbook's cell formats; cell style `n` is `formats[n - 1]`, 0 the default.
|
|
47
|
+
#[derive(Default)]
|
|
48
|
+
pub struct Stylesheet {
|
|
49
|
+
/// Shared with the sheets that use them: merging copies no format.
|
|
50
|
+
ids: HashMap<Arc<Format>, u32>,
|
|
51
|
+
formats: Vec<Arc<Format>>,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
impl Stylesheet {
|
|
55
|
+
/// Merges a sheet's formats in; returns the workbook style for each of
|
|
56
|
+
/// the sheet's own style ids (index 0, the default, stays 0).
|
|
57
|
+
pub fn add_sheet(&mut self, sheet: &Worksheet) -> Result<Vec<u32>> {
|
|
58
|
+
let mut remap = vec![0];
|
|
59
|
+
for format in sheet.formats() {
|
|
60
|
+
if let Some(&id) = self.ids.get(format) {
|
|
61
|
+
remap.push(id);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
(self.formats.len() < MAX_FORMATS).ok_or(Error::TooManyFormats)?;
|
|
65
|
+
self.formats.push(Arc::clone(format));
|
|
66
|
+
let id = u32::try_from(self.formats.len()).map_err(|_| Error::TooManyFormats)?;
|
|
67
|
+
self.ids.insert(Arc::clone(format), id);
|
|
68
|
+
remap.push(id);
|
|
69
|
+
}
|
|
70
|
+
Ok(remap)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pub fn to_xml(&self) -> String {
|
|
74
|
+
let mut number_formats = Table::<&str>::new(FIRST_CUSTOM_NUMBER_FORMAT);
|
|
75
|
+
let mut fonts = Table::<&Font>::new(1);
|
|
76
|
+
let mut fills = Table::<Color>::new(2);
|
|
77
|
+
let mut borders = Table::<Border>::new(1);
|
|
78
|
+
let xfs: Vec<[u32; 4]> = self
|
|
79
|
+
.formats
|
|
80
|
+
.iter()
|
|
81
|
+
.map(|format| {
|
|
82
|
+
let number = match format.number_code.as_deref() {
|
|
83
|
+
None | Some("General") => 0,
|
|
84
|
+
Some(code) => number_formats.id(&code),
|
|
85
|
+
};
|
|
86
|
+
let font = if format.font == Font::default() { 0 } else { fonts.id(&&format.font) };
|
|
87
|
+
let fill = format.fill.map_or(0, |color| fills.id(&color));
|
|
88
|
+
let border = format.border.map_or(0, |border| borders.id(&border));
|
|
89
|
+
[number, font, fill, border]
|
|
90
|
+
})
|
|
91
|
+
.collect();
|
|
92
|
+
|
|
93
|
+
let mut xml = XmlWriter::new();
|
|
94
|
+
xml.open("styleSheet", &[("xmlns", MAIN_NS)]);
|
|
95
|
+
if !number_formats.order.is_empty() {
|
|
96
|
+
let count = number_formats.order.len().to_string();
|
|
97
|
+
xml.open("numFmts", &[("count", &count)]);
|
|
98
|
+
for (offset, code) in (0..).zip(&number_formats.order) {
|
|
99
|
+
let id = (FIRST_CUSTOM_NUMBER_FORMAT + offset).to_string();
|
|
100
|
+
xml.empty("numFmt", &[("numFmtId", &id), ("formatCode", code)]);
|
|
101
|
+
}
|
|
102
|
+
xml.close("numFmts");
|
|
103
|
+
}
|
|
104
|
+
write_fonts(&mut xml, &fonts.order);
|
|
105
|
+
write_fills(&mut xml, &fills.order);
|
|
106
|
+
write_borders(&mut xml, &borders.order);
|
|
107
|
+
xml.open("cellStyleXfs", &[("count", "1")]);
|
|
108
|
+
xml.empty("xf", &[("numFmtId", "0"), ("fontId", "0"), ("fillId", "0"), ("borderId", "0")]);
|
|
109
|
+
xml.close("cellStyleXfs");
|
|
110
|
+
let count = (xfs.len() + 1).to_string();
|
|
111
|
+
xml.open("cellXfs", &[("count", &count)]);
|
|
112
|
+
xml.empty(
|
|
113
|
+
"xf",
|
|
114
|
+
&[
|
|
115
|
+
("numFmtId", "0"),
|
|
116
|
+
("fontId", "0"),
|
|
117
|
+
("fillId", "0"),
|
|
118
|
+
("borderId", "0"),
|
|
119
|
+
("xfId", "0"),
|
|
120
|
+
],
|
|
121
|
+
);
|
|
122
|
+
for (format, ids) in self.formats.iter().zip(&xfs) {
|
|
123
|
+
write_xf(&mut xml, format, *ids);
|
|
124
|
+
}
|
|
125
|
+
xml.close("cellXfs");
|
|
126
|
+
xml.open("cellStyles", &[("count", "1")]);
|
|
127
|
+
xml.empty("cellStyle", &[("name", "Normal"), ("xfId", "0"), ("builtinId", "0")]);
|
|
128
|
+
xml.close("cellStyles");
|
|
129
|
+
xml.close("styleSheet");
|
|
130
|
+
xml.finish()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn write_fonts(xml: &mut XmlWriter, fonts: &[&Font]) {
|
|
135
|
+
let count = (fonts.len() + 1).to_string();
|
|
136
|
+
xml.open("fonts", &[("count", &count)]);
|
|
137
|
+
let default = Font::default();
|
|
138
|
+
for font in std::iter::once(&default).chain(fonts.iter().copied()) {
|
|
139
|
+
xml.open("font", &[]);
|
|
140
|
+
for (on, tag) in [(font.bold, "b"), (font.italic, "i"), (font.underline, "u")] {
|
|
141
|
+
if on {
|
|
142
|
+
xml.empty(tag, &[]);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
xml.empty("sz", &[("val", &font.size())]);
|
|
146
|
+
if let Some(color) = font.color {
|
|
147
|
+
xml.empty("color", &[("rgb", &color.argb())]);
|
|
148
|
+
}
|
|
149
|
+
xml.empty("name", &[("val", font.name())]);
|
|
150
|
+
// The family is a fallback hint; only Calibri's is known (2, swiss).
|
|
151
|
+
if font.name() == DEFAULT_FONT {
|
|
152
|
+
xml.empty("family", &[("val", "2")]);
|
|
153
|
+
}
|
|
154
|
+
xml.close("font");
|
|
155
|
+
}
|
|
156
|
+
xml.close("fonts");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// Excel reserves the first two fills: none and gray125.
|
|
160
|
+
fn write_fills(xml: &mut XmlWriter, fills: &[Color]) {
|
|
161
|
+
let count = (fills.len() + 2).to_string();
|
|
162
|
+
xml.open("fills", &[("count", &count)]);
|
|
163
|
+
for pattern in ["none", "gray125"] {
|
|
164
|
+
xml.open("fill", &[]);
|
|
165
|
+
xml.empty("patternFill", &[("patternType", pattern)]);
|
|
166
|
+
xml.close("fill");
|
|
167
|
+
}
|
|
168
|
+
for color in fills {
|
|
169
|
+
xml.open("fill", &[]);
|
|
170
|
+
xml.open("patternFill", &[("patternType", "solid")]);
|
|
171
|
+
xml.empty("fgColor", &[("rgb", &color.argb())]);
|
|
172
|
+
xml.empty("bgColor", &[("indexed", "64")]);
|
|
173
|
+
xml.close("patternFill");
|
|
174
|
+
xml.close("fill");
|
|
175
|
+
}
|
|
176
|
+
xml.close("fills");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn write_borders(xml: &mut XmlWriter, borders: &[Border]) {
|
|
180
|
+
let count = (borders.len() + 1).to_string();
|
|
181
|
+
xml.open("borders", &[("count", &count)]);
|
|
182
|
+
xml.open("border", &[]);
|
|
183
|
+
for side in ["left", "right", "top", "bottom", "diagonal"] {
|
|
184
|
+
xml.empty(side, &[]);
|
|
185
|
+
}
|
|
186
|
+
xml.close("border");
|
|
187
|
+
for border in borders {
|
|
188
|
+
xml.open("border", &[]);
|
|
189
|
+
for side in ["left", "right", "top", "bottom"] {
|
|
190
|
+
xml.open(side, &[("style", border.as_str())]);
|
|
191
|
+
xml.empty("color", &[("auto", "1")]);
|
|
192
|
+
xml.close(side);
|
|
193
|
+
}
|
|
194
|
+
xml.empty("diagonal", &[]);
|
|
195
|
+
xml.close("border");
|
|
196
|
+
}
|
|
197
|
+
xml.close("borders");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fn write_xf(xml: &mut XmlWriter, format: &Format, [number, font, fill, border]: [u32; 4]) {
|
|
201
|
+
let ids = [number, font, fill, border].map(|id| id.to_string());
|
|
202
|
+
let mut attrs = vec![
|
|
203
|
+
("numFmtId", ids[0].as_str()),
|
|
204
|
+
("fontId", ids[1].as_str()),
|
|
205
|
+
("fillId", ids[2].as_str()),
|
|
206
|
+
("borderId", ids[3].as_str()),
|
|
207
|
+
("xfId", "0"),
|
|
208
|
+
];
|
|
209
|
+
for (applies, name) in [
|
|
210
|
+
(number != 0, "applyNumberFormat"),
|
|
211
|
+
(font != 0, "applyFont"),
|
|
212
|
+
(fill != 0, "applyFill"),
|
|
213
|
+
(border != 0, "applyBorder"),
|
|
214
|
+
(format.has_alignment(), "applyAlignment"),
|
|
215
|
+
] {
|
|
216
|
+
if applies {
|
|
217
|
+
attrs.push((name, "1"));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if !format.has_alignment() {
|
|
221
|
+
xml.empty("xf", &attrs);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
xml.open("xf", &attrs);
|
|
225
|
+
let mut alignment = Vec::new();
|
|
226
|
+
if let Some(align) = format.align {
|
|
227
|
+
alignment.push(("horizontal", Align::as_str(align)));
|
|
228
|
+
}
|
|
229
|
+
if let Some(valign) = format.valign {
|
|
230
|
+
alignment.push(("vertical", valign.as_str()));
|
|
231
|
+
}
|
|
232
|
+
if let Some(direction) = format.direction {
|
|
233
|
+
alignment.push(("readingOrder", direction.reading_order()));
|
|
234
|
+
}
|
|
235
|
+
if format.wrap {
|
|
236
|
+
alignment.push(("wrapText", "1"));
|
|
237
|
+
}
|
|
238
|
+
xml.empty("alignment", &alignment);
|
|
239
|
+
xml.close("xf");
|
|
240
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
//! Cell text: the OOXML escaping for characters XML cannot carry, and the
|
|
2
|
+
//! shared string table.
|
|
3
|
+
|
|
4
|
+
#[cfg(test)]
|
|
5
|
+
mod tests;
|
|
6
|
+
|
|
7
|
+
use std::borrow::Cow;
|
|
8
|
+
use std::collections::HashMap;
|
|
9
|
+
use std::fmt::Write as _;
|
|
10
|
+
|
|
11
|
+
use super::package::MAIN_NS;
|
|
12
|
+
use crate::xml::{XmlWriter, is_forbidden};
|
|
13
|
+
use crate::{Error, Result};
|
|
14
|
+
|
|
15
|
+
/// Characters a cell or a comment holds at most.
|
|
16
|
+
const MAX_TEXT: usize = 32_767;
|
|
17
|
+
|
|
18
|
+
/// Refuses text longer than a cell or a comment holds.
|
|
19
|
+
pub fn check_length(text: &str) -> Result<()> {
|
|
20
|
+
let length = text.chars().count();
|
|
21
|
+
(length <= MAX_TEXT).ok_or(Error::TextTooLong(length))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Escapes text as an `ST_Xstring` (ECMA-376 Part 1, 22.9.2.19): characters
|
|
25
|
+
/// XML cannot carry become `_xHHHH_`, and an underscore that would read as the
|
|
26
|
+
/// start of such an escape becomes `_x005F_`.
|
|
27
|
+
pub fn escape(text: &str) -> Cow<'_, str> {
|
|
28
|
+
let needs = |(at, ch): (usize, char)| is_forbidden(ch) || looks_escaped(text, at);
|
|
29
|
+
if !text.char_indices().any(needs) {
|
|
30
|
+
return Cow::Borrowed(text);
|
|
31
|
+
}
|
|
32
|
+
let mut escaped = String::with_capacity(text.len() + 7);
|
|
33
|
+
for (at, ch) in text.char_indices() {
|
|
34
|
+
if needs((at, ch)) {
|
|
35
|
+
// Writing to a String cannot fail.
|
|
36
|
+
let _ = write!(escaped, "_x{:04X}_", u32::from(ch));
|
|
37
|
+
} else {
|
|
38
|
+
escaped.push(ch);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
Cow::Owned(escaped)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// The inverse of [`escape`]: each `_xHHHH_` back to its character. An
|
|
45
|
+
/// escape of a surrogate, which no `char` holds, stays as written.
|
|
46
|
+
pub fn unescape(text: &str) -> Cow<'_, str> {
|
|
47
|
+
if !text.contains("_x") {
|
|
48
|
+
return Cow::Borrowed(text);
|
|
49
|
+
}
|
|
50
|
+
let mut out = String::with_capacity(text.len());
|
|
51
|
+
let mut rest = text;
|
|
52
|
+
while let Some(at) = rest.find("_x") {
|
|
53
|
+
out.push_str(&rest[..at]);
|
|
54
|
+
let candidate = &rest[at..];
|
|
55
|
+
let decoded = looks_escaped(candidate, 0)
|
|
56
|
+
.then(|| u32::from_str_radix(&candidate[2..6], 16).ok().and_then(char::from_u32))
|
|
57
|
+
.flatten();
|
|
58
|
+
if let Some(ch) = decoded {
|
|
59
|
+
out.push(ch);
|
|
60
|
+
rest = &candidate[7..];
|
|
61
|
+
} else {
|
|
62
|
+
out.push_str("_x");
|
|
63
|
+
rest = &candidate[2..];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
out.push_str(rest);
|
|
67
|
+
Cow::Owned(out)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// True when byte `at` starts `_xHHHH_`. The pattern is ASCII, so checking
|
|
71
|
+
/// bytes from `at` is exact and keeps escaping linear.
|
|
72
|
+
fn looks_escaped(text: &str, at: usize) -> bool {
|
|
73
|
+
let Some([b'_', b'x', digits @ .., b'_']) = text.as_bytes().get(at..at + 7) else {
|
|
74
|
+
return false;
|
|
75
|
+
};
|
|
76
|
+
digits.iter().all(u8::is_ascii_hexdigit)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Whether `<t>` needs `xml:space="preserve"` to keep its edge whitespace.
|
|
80
|
+
pub fn needs_preserve(text: &str) -> bool {
|
|
81
|
+
text.starts_with(char::is_whitespace) || text.ends_with(char::is_whitespace)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Every distinct text in the workbook, numbered in order of first use.
|
|
85
|
+
#[derive(Default)]
|
|
86
|
+
pub struct SharedStrings<'a> {
|
|
87
|
+
index: HashMap<&'a str, usize>,
|
|
88
|
+
order: Vec<&'a str>,
|
|
89
|
+
references: usize,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
impl<'a> SharedStrings<'a> {
|
|
93
|
+
/// The text's index in the table, adding it on first use.
|
|
94
|
+
pub fn intern(&mut self, text: &'a str) -> usize {
|
|
95
|
+
self.references += 1;
|
|
96
|
+
*self.index.entry(text).or_insert_with(|| {
|
|
97
|
+
self.order.push(text);
|
|
98
|
+
self.order.len() - 1
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
pub const fn is_empty(&self) -> bool {
|
|
103
|
+
self.order.is_empty()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// `xl/sharedStrings.xml`.
|
|
107
|
+
pub fn to_xml(&self) -> String {
|
|
108
|
+
let mut xml = XmlWriter::new();
|
|
109
|
+
let count = self.references.to_string();
|
|
110
|
+
let unique = self.order.len().to_string();
|
|
111
|
+
xml.open("sst", &[("xmlns", MAIN_NS), ("count", &count), ("uniqueCount", &unique)]);
|
|
112
|
+
for text in &self.order {
|
|
113
|
+
xml.open("si", &[]);
|
|
114
|
+
let attrs: &[(&str, &str)] =
|
|
115
|
+
if needs_preserve(text) { &[("xml:space", "preserve")] } else { &[] };
|
|
116
|
+
xml.element("t", attrs, &escape(text));
|
|
117
|
+
xml.close("si");
|
|
118
|
+
}
|
|
119
|
+
xml.close("sst");
|
|
120
|
+
xml.finish()
|
|
121
|
+
}
|
|
122
|
+
}
|