Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,199 @@
|
||||
//! Charts and pivot tables read from the kernel (TABLE-10, 11).
|
||||
|
||||
use std::io;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// The one selected chart.
|
||||
pub const CHART: &str = ".uno:Chart";
|
||||
/// The pivot table at the cursor.
|
||||
pub const PIVOT: &str = ".uno:PivotTable";
|
||||
|
||||
/// Chart kinds the kernel takes; it reads others as "other".
|
||||
pub const CHART_KINDS: [&str; 5] = ["column", "bar", "line", "scatter", "pie"];
|
||||
/// Pivot data functions.
|
||||
pub const PIVOT_FUNCTIONS: [&str; 5] = ["sum", "count", "average", "max", "min"];
|
||||
|
||||
pub(super) fn parse(json: &str, what: &str) -> io::Result<Value> {
|
||||
serde_json::from_str(json).map_err(|error| io::Error::other(format!("bad {what}: {error}")))
|
||||
}
|
||||
|
||||
pub(super) fn text(value: &Value, key: &str) -> io::Result<String> {
|
||||
value[key]
|
||||
.as_str()
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| io::Error::other(format!("no {key} in {value}")))
|
||||
}
|
||||
|
||||
pub(super) fn flag(value: &Value, key: &str) -> io::Result<bool> {
|
||||
value[key]
|
||||
.as_bool()
|
||||
.ok_or_else(|| io::Error::other(format!("no {key} in {value}")))
|
||||
}
|
||||
|
||||
pub(super) fn texts(value: &Value, key: &str) -> io::Result<Vec<String>> {
|
||||
value[key]
|
||||
.as_array()
|
||||
.ok_or_else(|| io::Error::other(format!("no {key} in {value}")))?
|
||||
.iter()
|
||||
.map(|item| {
|
||||
item.as_str()
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| io::Error::other(format!("bad {key} in {value}")))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A chart's kind, titles and legend.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Look {
|
||||
pub kind: String,
|
||||
pub title: String,
|
||||
pub legend: bool,
|
||||
pub x_title: String,
|
||||
pub y_title: String,
|
||||
}
|
||||
|
||||
impl Look {
|
||||
/// The JSON the kernel's `Chart` argument takes.
|
||||
pub fn to_json(&self) -> String {
|
||||
json!({
|
||||
"type": self.kind,
|
||||
"title": self.title,
|
||||
"legend": self.legend,
|
||||
"xtitle": self.x_title,
|
||||
"ytitle": self.y_title,
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// A selected chart: its object name, data and look.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Chart {
|
||||
pub name: String,
|
||||
pub range: String,
|
||||
pub col_headers: bool,
|
||||
pub row_headers: bool,
|
||||
pub look: Look,
|
||||
}
|
||||
|
||||
/// Parses `.uno:Chart`; none when no chart is selected.
|
||||
pub fn chart(json: &str) -> io::Result<Option<Chart>> {
|
||||
let value = parse(json, "chart")?;
|
||||
if value.get("name").is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(Chart {
|
||||
name: text(&value, "name")?,
|
||||
range: text(&value, "range")?,
|
||||
col_headers: flag(&value, "colHeaders")?,
|
||||
row_headers: flag(&value, "rowHeaders")?,
|
||||
look: look(&value)?,
|
||||
}))
|
||||
}
|
||||
|
||||
/// The look members of a chart read.
|
||||
pub(crate) fn look(value: &Value) -> io::Result<Look> {
|
||||
Ok(Look {
|
||||
kind: text(value, "type")?,
|
||||
title: text(value, "title")?,
|
||||
legend: flag(value, "legend")?,
|
||||
x_title: text(value, "xtitle")?,
|
||||
y_title: text(value, "ytitle")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// A pivot table's layout: fields by area, data with functions.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Layout {
|
||||
pub rows: Vec<String>,
|
||||
pub columns: Vec<String>,
|
||||
pub pages: Vec<String>,
|
||||
pub data: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl Layout {
|
||||
/// The `Pivot` argument: from `source` to `dest`, or in place.
|
||||
pub fn to_json(&self, source: &str, dest: &str) -> String {
|
||||
let data: Vec<Value> = self
|
||||
.data
|
||||
.iter()
|
||||
.map(|(field, function)| json!({ "field": field, "function": function }))
|
||||
.collect();
|
||||
json!({
|
||||
"source": source,
|
||||
"dest": dest,
|
||||
"rows": self.rows,
|
||||
"columns": self.columns,
|
||||
"pages": self.pages,
|
||||
"data": data,
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// The pivot table at the cursor.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Pivot {
|
||||
pub name: String,
|
||||
/// Where it shows.
|
||||
pub range: String,
|
||||
/// Its data; empty when not from cells.
|
||||
pub source: String,
|
||||
pub fields: Vec<String>,
|
||||
pub layout: Layout,
|
||||
}
|
||||
|
||||
/// Parses `.uno:PivotTable`; none off a pivot table.
|
||||
pub fn pivot(json: &str) -> io::Result<Option<Pivot>> {
|
||||
let value = parse(json, "pivot table")?;
|
||||
if value.get("name").is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
let data = value["data"]
|
||||
.as_array()
|
||||
.ok_or_else(|| io::Error::other(format!("no data in {value}")))?
|
||||
.iter()
|
||||
.map(|item| Ok((text(item, "field")?, text(item, "function")?)))
|
||||
.collect::<io::Result<_>>()?;
|
||||
Ok(Some(Pivot {
|
||||
name: text(&value, "name")?,
|
||||
range: text(&value, "range")?,
|
||||
source: text(&value, "source")?,
|
||||
fields: texts(&value, "fields")?,
|
||||
layout: Layout {
|
||||
rows: texts(&value, "rows")?,
|
||||
columns: texts(&value, "columns")?,
|
||||
pages: texts(&value, "pages")?,
|
||||
data,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn kernel_reads_parse() {
|
||||
assert_eq!(
|
||||
chart(r#"{"commandName":".uno:Chart"}"#).expect("none"),
|
||||
None
|
||||
);
|
||||
let one = r#"{"name":"Object 1","range":"$S.$A$1:$B$4","colHeaders":true,"rowHeaders":false,"type":"bar","title":"T","legend":false,"xtitle":"","ytitle":"Y"}"#;
|
||||
let one = chart(one).expect("a chart").expect("selected");
|
||||
assert_eq!(
|
||||
(one.look.kind.as_str(), one.look.y_title.as_str()),
|
||||
("bar", "Y")
|
||||
);
|
||||
assert!(chart(r#"{"name":"Object 1"}"#).is_err(), "parts missing");
|
||||
let table = r#"{"name":"P","range":"$S.$E$1:$G$5","source":"$S.$A$1:$C$9","fields":["Region","Year","Sales"],"rows":["Region"],"columns":[],"pages":[],"data":[{"field":"Sales","function":"sum"}]}"#;
|
||||
let table = pivot(table).expect("a pivot").expect("here");
|
||||
assert_eq!(table.layout.data, [("Sales".into(), "sum".into())]);
|
||||
assert_eq!(
|
||||
pivot(r#"{"commandName":".uno:PivotTable"}"#).expect("none"),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
//! The kernel's CSV filter options (TABLE-12, 13).
|
||||
|
||||
/// How the kernel reads one column.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Column {
|
||||
Standard,
|
||||
Text,
|
||||
DateMdy,
|
||||
DateDmy,
|
||||
DateYmd,
|
||||
Skip,
|
||||
}
|
||||
|
||||
pub const COLUMNS: [Column; 6] = [
|
||||
Column::Standard,
|
||||
Column::Text,
|
||||
Column::DateMdy,
|
||||
Column::DateDmy,
|
||||
Column::DateYmd,
|
||||
Column::Skip,
|
||||
];
|
||||
|
||||
impl Column {
|
||||
fn code(self) -> u8 {
|
||||
match self {
|
||||
Column::Standard => 1,
|
||||
Column::Text => 2,
|
||||
Column::DateMdy => 3,
|
||||
Column::DateDmy => 4,
|
||||
Column::DateYmd => 5,
|
||||
Column::Skip => 9,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Text encodings the import offers.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Encoding {
|
||||
Utf8,
|
||||
Utf16,
|
||||
Gb18030,
|
||||
Big5,
|
||||
ShiftJis,
|
||||
EucKr,
|
||||
Windows1252,
|
||||
Windows1251,
|
||||
Latin1,
|
||||
}
|
||||
|
||||
pub const ENCODINGS: [Encoding; 9] = [
|
||||
Encoding::Utf8,
|
||||
Encoding::Utf16,
|
||||
Encoding::Gb18030,
|
||||
Encoding::Big5,
|
||||
Encoding::ShiftJis,
|
||||
Encoding::EucKr,
|
||||
Encoding::Windows1252,
|
||||
Encoding::Windows1251,
|
||||
Encoding::Latin1,
|
||||
];
|
||||
|
||||
impl Encoding {
|
||||
/// The label people know it by.
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Encoding::Utf8 => "UTF-8",
|
||||
Encoding::Utf16 => "UTF-16",
|
||||
Encoding::Gb18030 => "GB18030",
|
||||
Encoding::Big5 => "Big5",
|
||||
Encoding::ShiftJis => "Shift_JIS",
|
||||
Encoding::EucKr => "EUC-KR",
|
||||
Encoding::Windows1252 => "Windows-1252",
|
||||
Encoding::Windows1251 => "Windows-1251",
|
||||
Encoding::Latin1 => "ISO-8859-1",
|
||||
}
|
||||
}
|
||||
|
||||
/// The kernel's `rtl_TextEncoding`.
|
||||
fn kernel(self) -> u16 {
|
||||
match self {
|
||||
Encoding::Utf8 => 76,
|
||||
Encoding::Utf16 => 0xFFFF,
|
||||
Encoding::Gb18030 => 85,
|
||||
Encoding::Big5 => 68,
|
||||
Encoding::ShiftJis => 64,
|
||||
Encoding::EucKr => 79,
|
||||
Encoding::Windows1252 => 1,
|
||||
Encoding::Windows1251 => 34,
|
||||
Encoding::Latin1 => 12,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// US English numbers: `.` decimal, `,` thousands.
|
||||
const LANGUAGE: u16 = 1033;
|
||||
|
||||
/// How a text file becomes a sheet.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Import {
|
||||
pub separator: char,
|
||||
pub quote: Option<char>,
|
||||
pub encoding: Encoding,
|
||||
/// Kinds of the first columns; the rest are standard.
|
||||
pub columns: Vec<Column>,
|
||||
}
|
||||
|
||||
impl Import {
|
||||
/// The kernel's filter options for this import.
|
||||
pub fn filter(&self) -> String {
|
||||
let columns: Vec<String> = self
|
||||
.columns
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, column)| format!("{}/{}", index + 1, column.code()))
|
||||
.collect();
|
||||
// Token order: sc/source/ui/dbgui/asciiopt.cxx.
|
||||
[
|
||||
u32::from(self.separator).to_string(),
|
||||
self.quote.map_or(0, u32::from).to_string(),
|
||||
self.encoding.kernel().to_string(),
|
||||
"1".into(),
|
||||
columns.join("/"),
|
||||
LANGUAGE.to_string(),
|
||||
// Quoted as text, special numbers, two export flags, trim.
|
||||
"false,false,false,false,false".into(),
|
||||
"0".into(),
|
||||
// Formulas stay text; no BOM; scientific numbers read.
|
||||
"false,false,true".into(),
|
||||
]
|
||||
.join(",")
|
||||
}
|
||||
}
|
||||
|
||||
/// Options writing the view's sheet as shown, UTF-8.
|
||||
pub fn export(separator: char) -> String {
|
||||
[
|
||||
u32::from(separator).to_string(),
|
||||
"34".into(),
|
||||
Encoding::Utf8.kernel().to_string(),
|
||||
"1".into(),
|
||||
"1/1".into(),
|
||||
LANGUAGE.to_string(),
|
||||
// Quote all text, special numbers, as shown, formulas, trim.
|
||||
"false,false,true,false,false".into(),
|
||||
"0".into(),
|
||||
"false,false,false".into(),
|
||||
]
|
||||
.join(",")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn imports_name_every_token() {
|
||||
let import = Import {
|
||||
separator: '\t',
|
||||
quote: Some('"'),
|
||||
encoding: Encoding::Gb18030,
|
||||
columns: vec![Column::Text, Column::Standard, Column::DateYmd],
|
||||
};
|
||||
assert_eq!(
|
||||
import.filter(),
|
||||
"9,34,85,1,1/2/2/1/3/5,1033,false,false,false,false,false,0,false,false,true"
|
||||
);
|
||||
let bare = Import {
|
||||
quote: None,
|
||||
columns: Vec::new(),
|
||||
..import
|
||||
};
|
||||
assert!(bare.filter().starts_with("9,0,85,1,,1033,"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exports_write_values_as_shown() {
|
||||
assert_eq!(
|
||||
export(';'),
|
||||
"59,34,76,1,1/1,1033,false,false,true,false,false,0,false,false,false"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
//! Formats, filters, rules and links read from the kernel.
|
||||
|
||||
use std::io;
|
||||
|
||||
/// The current cell's format code and kind.
|
||||
pub const NUMBER: &str = ".uno:NumberFormatCode";
|
||||
/// A filter column's entries.
|
||||
pub const FILTER: &str = ".uno:AutoFilterValues";
|
||||
/// A cell's choices: its validation list.
|
||||
pub const LIST: &str = ".uno:DataSelectValues";
|
||||
/// The current cell's validation.
|
||||
pub const VALIDATION: &str = ".uno:Validation";
|
||||
/// The shown sheet's conditional formats.
|
||||
pub const FORMATS: &str = ".uno:ConditionalFormats";
|
||||
/// Sources whose data shows as last saved (§9.3).
|
||||
pub const LINKS: &str = ".uno:ExternalLinks";
|
||||
|
||||
fn parse(json: &str, what: &str) -> io::Result<serde_json::Value> {
|
||||
serde_json::from_str(json).map_err(|error| io::Error::other(format!("bad {what}: {error}")))
|
||||
}
|
||||
|
||||
fn text(value: &serde_json::Value, key: &str) -> io::Result<String> {
|
||||
value[key]
|
||||
.as_str()
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| io::Error::other(format!("no {key} in {value}")))
|
||||
}
|
||||
|
||||
fn flag(value: &serde_json::Value, key: &str) -> io::Result<bool> {
|
||||
value[key]
|
||||
.as_bool()
|
||||
.ok_or_else(|| io::Error::other(format!("no {key} in {value}")))
|
||||
}
|
||||
|
||||
/// The query that checks a format code.
|
||||
pub fn number_check_query(code: &str) -> String {
|
||||
format!("{NUMBER}?code={}", super::encode(code))
|
||||
}
|
||||
|
||||
/// Parses a code check.
|
||||
pub fn number_valid(json: &str) -> io::Result<bool> {
|
||||
flag(&parse(json, "format check")?, "valid")
|
||||
}
|
||||
|
||||
/// Parses `.uno:NumberFormatCode`: the code and its kind.
|
||||
pub fn number_format(json: &str) -> io::Result<(String, String)> {
|
||||
let value = parse(json, "number format")?;
|
||||
Ok((text(&value, "code")?, text(&value, "type")?))
|
||||
}
|
||||
|
||||
/// The query for a cell's filter entries or choices.
|
||||
pub fn cell_query(base: &str, column: u32, row: u32) -> String {
|
||||
format!("{base}?col={column}&row={row}")
|
||||
}
|
||||
|
||||
/// One filter entry, as the kernel's own menu shows it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Entry {
|
||||
pub text: String,
|
||||
/// Other columns' filters hide its rows.
|
||||
pub hidden: bool,
|
||||
/// The column's own filter keeps it.
|
||||
pub selected: bool,
|
||||
}
|
||||
|
||||
/// A filter column: whether it filters, and its entries.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Filter {
|
||||
pub filtered: bool,
|
||||
pub entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
/// Parses `.uno:AutoFilterValues`.
|
||||
pub fn filter_values(json: &str) -> io::Result<Filter> {
|
||||
let value = parse(json, "filter values")?;
|
||||
let entries = value["values"]
|
||||
.as_array()
|
||||
.ok_or_else(|| io::Error::other(format!("no values in {value}")))?
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
Ok(Entry {
|
||||
text: text(entry, "text")?,
|
||||
hidden: flag(entry, "hidden")?,
|
||||
selected: flag(entry, "selected")?,
|
||||
})
|
||||
})
|
||||
.collect::<io::Result<_>>()?;
|
||||
Ok(Filter {
|
||||
filtered: flag(&value, "filtered")?,
|
||||
entries,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parses `.uno:ExternalLinks`.
|
||||
pub fn links(json: &str) -> io::Result<Vec<String>> {
|
||||
let value = parse(json, "links")?;
|
||||
value["links"]
|
||||
.as_array()
|
||||
.ok_or_else(|| io::Error::other(format!("no links in {value}")))?
|
||||
.iter()
|
||||
.map(|link| {
|
||||
link.as_str()
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| io::Error::other(format!("a bad link: {link}")))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parses `.uno:DataSelectValues`.
|
||||
pub fn list_values(json: &str) -> io::Result<Vec<String>> {
|
||||
let value = parse(json, "choices")?;
|
||||
value["values"]
|
||||
.as_array()
|
||||
.ok_or_else(|| io::Error::other(format!("no values in {value}")))?
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
entry
|
||||
.as_str()
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| io::Error::other(format!("a bad choice: {entry}")))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A validation as the kernel reports and takes it.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Validation {
|
||||
/// any, whole, decimal, date, time, textlength, list, custom.
|
||||
pub kind: String,
|
||||
pub operator: String,
|
||||
pub value1: String,
|
||||
pub value2: String,
|
||||
/// List entries to set; the kernel quotes them.
|
||||
pub list: Vec<String>,
|
||||
pub dropdown: bool,
|
||||
pub ignore_blank: bool,
|
||||
pub show_input: bool,
|
||||
pub input_title: String,
|
||||
pub input_message: String,
|
||||
pub show_error: bool,
|
||||
pub error_title: String,
|
||||
pub error_message: String,
|
||||
/// stop, warning or info.
|
||||
pub error_style: String,
|
||||
}
|
||||
|
||||
impl Validation {
|
||||
/// The rule `rules.validation` takes.
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::json!({
|
||||
"type": self.kind,
|
||||
"operator": self.operator,
|
||||
"value1": self.value1,
|
||||
"value2": self.value2,
|
||||
"list": self.list,
|
||||
"dropdown": self.dropdown,
|
||||
"ignoreBlank": self.ignore_blank,
|
||||
"showInput": self.show_input,
|
||||
"inputTitle": self.input_title,
|
||||
"inputMessage": self.input_message,
|
||||
"showError": self.show_error,
|
||||
"errorTitle": self.error_title,
|
||||
"errorMessage": self.error_message,
|
||||
"errorStyle": self.error_style,
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses `.uno:Validation`.
|
||||
pub fn validation(json: &str) -> io::Result<Validation> {
|
||||
let value = parse(json, "validation")?;
|
||||
let kind = text(&value, "type")?;
|
||||
// A cell without a rule reports its type alone.
|
||||
if value.get("operator").is_none() {
|
||||
return Ok(Validation {
|
||||
kind,
|
||||
..Validation::default()
|
||||
});
|
||||
}
|
||||
Ok(Validation {
|
||||
kind,
|
||||
operator: text(&value, "operator")?,
|
||||
value1: text(&value, "value1")?,
|
||||
value2: text(&value, "value2")?,
|
||||
list: Vec::new(),
|
||||
dropdown: flag(&value, "dropdown")?,
|
||||
ignore_blank: flag(&value, "ignoreBlank")?,
|
||||
show_input: flag(&value, "showInput")?,
|
||||
input_title: text(&value, "inputTitle")?,
|
||||
input_message: text(&value, "inputMessage")?,
|
||||
show_error: flag(&value, "showError")?,
|
||||
error_title: text(&value, "errorTitle")?,
|
||||
error_message: text(&value, "errorMessage")?,
|
||||
error_style: text(&value, "errorStyle")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// One conditional format on the shown sheet.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Format {
|
||||
pub key: u64,
|
||||
pub range: String,
|
||||
/// cell, scale, databar, iconset or other.
|
||||
pub kind: String,
|
||||
/// The cell style a cell rule applies.
|
||||
pub style: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// Parses `.uno:ConditionalFormats`.
|
||||
pub fn formats(json: &str) -> io::Result<Vec<Format>> {
|
||||
let value = parse(json, "conditional formats")?;
|
||||
value["formats"]
|
||||
.as_array()
|
||||
.ok_or_else(|| io::Error::other(format!("no formats in {value}")))?
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
let cell = text(entry, "kind")? == "cell";
|
||||
let optional = |key| {
|
||||
if cell {
|
||||
text(entry, key)
|
||||
} else {
|
||||
Ok(String::new())
|
||||
}
|
||||
};
|
||||
Ok(Format {
|
||||
key: entry["key"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| io::Error::other(format!("no key in {entry}")))?,
|
||||
range: text(entry, "range")?,
|
||||
kind: text(entry, "kind")?,
|
||||
style: optional("style")?,
|
||||
value: optional("value1")?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn kernel_reads_parse() {
|
||||
let filter = r#"{"filtered":true,"values":[{"text":"a","hidden":false,"selected":true},{"text":"","hidden":true,"selected":false}]}"#;
|
||||
let filter = filter_values(filter).expect("entries");
|
||||
assert!(filter.filtered && filter.entries[1].hidden && !filter.entries[1].selected);
|
||||
assert_eq!(
|
||||
list_values(r#"{"values":["Red","Green"]}"#).expect("list"),
|
||||
["Red", "Green"]
|
||||
);
|
||||
assert_eq!(validation(r#"{"type":"any"}"#).expect("none").kind, "any");
|
||||
let help = r#"{"type":"any","operator":"equal","value1":"","value2":"","dropdown":true,"ignoreBlank":true,"showInput":true,"inputTitle":"","inputMessage":"An id","showError":false,"errorTitle":"","errorMessage":"","errorStyle":"stop"}"#;
|
||||
assert_eq!(validation(help).expect("help").input_message, "An id");
|
||||
assert!(
|
||||
validation(r#"{"type":"list","operator":"equal","dropdown":true}"#).is_err(),
|
||||
"a part is missing"
|
||||
);
|
||||
let formats = formats(
|
||||
r#"{"formats":[{"key":1,"range":"A1:A3","kind":"cell","style":"Good","value1":"5"},{"key":2,"range":"B1","kind":"scale"}]}"#,
|
||||
)
|
||||
.expect("formats");
|
||||
assert_eq!(
|
||||
(formats[0].style.as_str(), formats[1].kind.as_str()),
|
||||
("Good", "scale")
|
||||
);
|
||||
assert!(
|
||||
number_format(r#"{"code":"0.00"}"#).is_err(),
|
||||
"a missing kind fails"
|
||||
);
|
||||
assert_eq!(
|
||||
number_check_query("0.0 %"),
|
||||
".uno:NumberFormatCode?code=0.0%20%25"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//! Print layout and sheet protection (TABLE-14, 15).
|
||||
|
||||
use std::io;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::analysis::{flag, parse, texts};
|
||||
|
||||
/// The shown sheet's print layout.
|
||||
pub const LAYOUT: &str = ".uno:PrintLayout";
|
||||
/// The shown sheet's protection.
|
||||
pub const PROTECTION: &str = ".uno:SheetProtection";
|
||||
|
||||
/// Actions a protected sheet may allow, in kernel names.
|
||||
pub const PROTECT_OPTIONS: [&str; 8] = [
|
||||
"selectLocked",
|
||||
"selectUnlocked",
|
||||
"insertColumns",
|
||||
"insertRows",
|
||||
"deleteColumns",
|
||||
"deleteRows",
|
||||
"autofilter",
|
||||
"pivot",
|
||||
];
|
||||
|
||||
/// How printing scales the sheet.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Scale {
|
||||
Percent(u16),
|
||||
/// Pages wide and tall; 0 leaves one free.
|
||||
Fit(u16, u16),
|
||||
/// Pages in all.
|
||||
Pages(u16),
|
||||
}
|
||||
|
||||
/// Rows or columns, first to last, from 1.
|
||||
pub type Span = (u32, u32);
|
||||
|
||||
/// What the kernel prints of the shown sheet.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PrintLayout {
|
||||
/// Print ranges; none prints the used area.
|
||||
pub areas: Vec<String>,
|
||||
pub rows: Option<Span>,
|
||||
pub columns: Option<Span>,
|
||||
pub landscape: bool,
|
||||
pub scale: Scale,
|
||||
/// Sheets sharing the page style.
|
||||
pub sheets: Vec<String>,
|
||||
/// Manual breaks at the cursor.
|
||||
pub row_break: bool,
|
||||
pub column_break: bool,
|
||||
}
|
||||
|
||||
/// The JSON `page.setup` takes.
|
||||
pub fn page_json(landscape: bool, scale: Scale) -> String {
|
||||
match scale {
|
||||
Scale::Percent(percent) => json!({ "landscape": landscape, "scale": percent }),
|
||||
Scale::Fit(wide, tall) => json!({ "landscape": landscape, "fit": [wide, tall] }),
|
||||
Scale::Pages(pages) => json!({ "landscape": landscape, "pages": pages }),
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Repeated rows as `print.area` takes them.
|
||||
pub fn rows_ref(span: Option<Span>) -> String {
|
||||
span.map_or_else(String::new, |(first, last)| format!("${first}:${last}"))
|
||||
}
|
||||
|
||||
/// Repeated columns as `print.area` takes them.
|
||||
pub fn columns_ref(span: Option<Span>) -> String {
|
||||
span.map_or_else(String::new, |(first, last)| {
|
||||
let name = |index: u32| super::column_name(index - 1);
|
||||
format!("${}:${}", name(first), name(last))
|
||||
})
|
||||
}
|
||||
|
||||
fn number(value: &Value, key: &str) -> io::Result<u64> {
|
||||
value[key]
|
||||
.as_u64()
|
||||
.ok_or_else(|| io::Error::other(format!("no {key} in {value}")))
|
||||
}
|
||||
|
||||
fn span(value: &Value, key: &str) -> io::Result<Option<Span>> {
|
||||
let Some(node) = value.get(key) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let bound = |end| -> io::Result<u32> {
|
||||
u32::try_from(number(node, end)?).map_err(|_| io::Error::other(format!("bad {key}")))
|
||||
};
|
||||
Ok(Some((bound("first")?, bound("last")?)))
|
||||
}
|
||||
|
||||
/// Parses `.uno:PrintLayout`.
|
||||
pub fn print_layout(json: &str) -> io::Result<PrintLayout> {
|
||||
let value = parse(json, "print layout")?;
|
||||
let small = |key| -> io::Result<u16> {
|
||||
u16::try_from(number(&value, key)?).map_err(|_| io::Error::other(format!("bad {key}")))
|
||||
};
|
||||
let (percent, wide, tall, pages) = (
|
||||
small("scale")?,
|
||||
small("fitWidth")?,
|
||||
small("fitHeight")?,
|
||||
small("pages")?,
|
||||
);
|
||||
// The kernel's order: pages, then fit, then percent.
|
||||
let scale = match () {
|
||||
_ if pages > 0 => Scale::Pages(pages),
|
||||
_ if wide > 0 || tall > 0 => Scale::Fit(wide, tall),
|
||||
_ if percent > 0 => Scale::Percent(percent),
|
||||
_ => return Err(io::Error::other(format!("no scale in {value}"))),
|
||||
};
|
||||
Ok(PrintLayout {
|
||||
areas: texts(&value, "areas")?,
|
||||
rows: span(&value, "rows")?,
|
||||
columns: span(&value, "columns")?,
|
||||
landscape: flag(&value, "landscape")?,
|
||||
scale,
|
||||
sheets: texts(&value, "sheets")?,
|
||||
row_break: flag(&value, "rowBreak")?,
|
||||
column_break: flag(&value, "columnBreak")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// A sheet's protection as the kernel reports it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Protection {
|
||||
pub protected: bool,
|
||||
/// Unprotecting needs a password.
|
||||
pub password: bool,
|
||||
pub allow: Vec<String>,
|
||||
}
|
||||
|
||||
/// Parses `.uno:SheetProtection`.
|
||||
pub fn protection(json: &str) -> io::Result<Protection> {
|
||||
let value = parse(json, "protection")?;
|
||||
Ok(Protection {
|
||||
protected: flag(&value, "protected")?,
|
||||
password: flag(&value, "password")?,
|
||||
allow: texts(&value, "allow")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// The JSON `sheet.protect` takes to protect.
|
||||
pub fn protect_json(password: &str, allow: &[String]) -> String {
|
||||
json!({ "protect": true, "password": password, "allow": allow }).to_string()
|
||||
}
|
||||
|
||||
/// The JSON `sheet.protect` takes to unprotect.
|
||||
pub fn unprotect_json(password: &str) -> String {
|
||||
json!({ "protect": false, "password": password }).to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn layouts_read_each_scale() {
|
||||
let json = r#"{"areas":["$A$1:$C$4"],"rows":{"first":1,"last":2},"rowBreak":false,
|
||||
"columnBreak":true,"landscape":true,"scale":0,"pages":0,"fitWidth":1,"fitHeight":0,
|
||||
"sheets":["Sheet1","Sheet2"]}"#;
|
||||
let read = print_layout(json).unwrap();
|
||||
assert_eq!(read.areas, ["$A$1:$C$4"]);
|
||||
assert_eq!((read.rows, read.columns), (Some((1, 2)), None));
|
||||
assert_eq!(read.scale, Scale::Fit(1, 0));
|
||||
assert!(read.landscape && read.column_break && !read.row_break);
|
||||
let both = json.replace(r#""scale":0"#, r#""scale":100"#);
|
||||
assert_eq!(
|
||||
print_layout(&both).unwrap().scale,
|
||||
Scale::Fit(1, 0),
|
||||
"fit wins"
|
||||
);
|
||||
let none = json.replace(r#""fitWidth":1"#, r#""fitWidth":0"#);
|
||||
assert!(print_layout(&none).is_err(), "no scale is an error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spans_become_references() {
|
||||
assert_eq!(rows_ref(Some((1, 3))), "$1:$3");
|
||||
assert_eq!(columns_ref(Some((1, 28))), "$A:$AB");
|
||||
assert_eq!(rows_ref(None), "");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user