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:
ZacharyZhang-NY committed 2026-10-08 21:20:08 -04:00
commit b7fca0146d
817 files changed
+143101

No files matched your search

+199
View File
@@ -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
);
}
}
+182
View File
@@ -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"
);
}
}
+277
View File
@@ -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"
);
}
}
+184
View File
@@ -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), "");
}
}