Compare commits
1 Commits
feature/ni
...
feature/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6740a43fe6 |
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -872,12 +872,6 @@ version = "0.19.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustversion"
|
|
||||||
version = "1.0.21"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.17"
|
version = "1.0.17"
|
||||||
@@ -1087,24 +1081,23 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.100"
|
version = "0.2.92"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
|
||||||
"rustversion",
|
|
||||||
"wasm-bindgen-macro",
|
"wasm-bindgen-macro",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-backend"
|
name = "wasm-bindgen-backend"
|
||||||
version = "0.2.100"
|
version = "0.2.92"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"log",
|
"log",
|
||||||
|
"once_cell",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn",
|
||||||
@@ -1125,9 +1118,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.100"
|
version = "0.2.92"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -1135,9 +1128,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.100"
|
version = "0.2.92"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1148,12 +1141,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.100"
|
version = "0.2.92"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||||
dependencies = [
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-test"
|
name = "wasm-bindgen-test"
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ impl Model {
|
|||||||
// FIXME: I think when casting a number we should convert it to_precision(x, 15)
|
// FIXME: I think when casting a number we should convert it to_precision(x, 15)
|
||||||
// See function Exact
|
// See function Exact
|
||||||
match result {
|
match result {
|
||||||
CalcResult::Number(f) => Ok(format!("{f}")),
|
CalcResult::Number(f) => Ok(format!("{}", f)),
|
||||||
CalcResult::String(s) => Ok(s),
|
CalcResult::String(s) => Ok(s),
|
||||||
CalcResult::Boolean(f) => {
|
CalcResult::Boolean(f) => {
|
||||||
if f {
|
if f {
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ impl Lexer {
|
|||||||
pub fn expect(&mut self, tk: TokenType) -> Result<()> {
|
pub fn expect(&mut self, tk: TokenType) -> Result<()> {
|
||||||
let nt = self.next_token();
|
let nt = self.next_token();
|
||||||
if mem::discriminant(&nt) != mem::discriminant(&tk) {
|
if mem::discriminant(&nt) != mem::discriminant(&tk) {
|
||||||
return Err(self.set_error(&format!("Error, expected {tk:?}"), self.position));
|
return Err(self.set_error(&format!("Error, expected {:?}", tk), self.position));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -511,7 +511,7 @@ impl Lexer {
|
|||||||
self.position = position;
|
self.position = position;
|
||||||
chars.parse::<i32>().map_err(|_| LexerError {
|
chars.parse::<i32>().map_err(|_| LexerError {
|
||||||
position,
|
position,
|
||||||
message: format!("Failed to parse to int: {chars}"),
|
message: format!("Failed to parse to int: {}", chars),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,7 +572,9 @@ impl Lexer {
|
|||||||
}
|
}
|
||||||
self.position = position;
|
self.position = position;
|
||||||
match chars.parse::<f64>() {
|
match chars.parse::<f64>() {
|
||||||
Err(_) => Err(self.set_error(&format!("Failed to parse to double: {chars}"), position)),
|
Err(_) => {
|
||||||
|
Err(self.set_error(&format!("Failed to parse to double: {}", chars), position))
|
||||||
|
}
|
||||||
Ok(v) => Ok(v),
|
Ok(v) => Ok(v),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,16 +148,15 @@ impl Lexer {
|
|||||||
let row_left = match row_left.parse::<i32>() {
|
let row_left = match row_left.parse::<i32>() {
|
||||||
Ok(n) => n,
|
Ok(n) => n,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return Err(
|
return Err(self
|
||||||
self.set_error(&format!("Failed parsing row {row_left}"), position)
|
.set_error(&format!("Failed parsing row {}", row_left), position))
|
||||||
)
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let row_right = match row_right.parse::<i32>() {
|
let row_right = match row_right.parse::<i32>() {
|
||||||
Ok(n) => n,
|
Ok(n) => n,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return Err(self
|
return Err(self
|
||||||
.set_error(&format!("Failed parsing row {row_right}"), position))
|
.set_error(&format!("Failed parsing row {}", row_right), position))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if row_left > LAST_ROW {
|
if row_left > LAST_ROW {
|
||||||
|
|||||||
@@ -828,7 +828,7 @@ impl Parser {
|
|||||||
| TokenType::Percent => Node::ParseErrorKind {
|
| TokenType::Percent => Node::ParseErrorKind {
|
||||||
formula: self.lexer.get_formula(),
|
formula: self.lexer.get_formula(),
|
||||||
position: 0,
|
position: 0,
|
||||||
message: format!("Unexpected token: '{next_token:?}'"),
|
message: format!("Unexpected token: '{:?}'", next_token),
|
||||||
},
|
},
|
||||||
TokenType::LeftBracket => Node::ParseErrorKind {
|
TokenType::LeftBracket => Node::ParseErrorKind {
|
||||||
formula: self.lexer.get_formula(),
|
formula: self.lexer.get_formula(),
|
||||||
|
|||||||
@@ -53,24 +53,24 @@ fn move_function(name: &str, args: &Vec<Node>, move_context: &MoveContext) -> St
|
|||||||
arguments = to_string_moved(el, move_context);
|
arguments = to_string_moved(el, move_context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
format!("{name}({arguments})")
|
format!("{}({})", name, arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn to_string_array_node(node: &ArrayNode) -> String {
|
pub(crate) fn to_string_array_node(node: &ArrayNode) -> String {
|
||||||
match node {
|
match node {
|
||||||
ArrayNode::Boolean(value) => format!("{value}").to_ascii_uppercase(),
|
ArrayNode::Boolean(value) => format!("{}", value).to_ascii_uppercase(),
|
||||||
ArrayNode::Number(number) => to_excel_precision_str(*number),
|
ArrayNode::Number(number) => to_excel_precision_str(*number),
|
||||||
ArrayNode::String(value) => format!("\"{value}\""),
|
ArrayNode::String(value) => format!("\"{}\"", value),
|
||||||
ArrayNode::Error(kind) => format!("{kind}"),
|
ArrayNode::Error(kind) => format!("{}", kind),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
||||||
use self::Node::*;
|
use self::Node::*;
|
||||||
match node {
|
match node {
|
||||||
BooleanKind(value) => format!("{value}").to_ascii_uppercase(),
|
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
|
||||||
NumberKind(number) => to_excel_precision_str(*number),
|
NumberKind(number) => to_excel_precision_str(*number),
|
||||||
StringKind(value) => format!("\"{value}\""),
|
StringKind(value) => format!("\"{}\"", value),
|
||||||
ReferenceKind {
|
ReferenceKind {
|
||||||
sheet_name,
|
sheet_name,
|
||||||
sheet_index,
|
sheet_index,
|
||||||
@@ -241,7 +241,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
|||||||
full_row,
|
full_row,
|
||||||
full_column,
|
full_column,
|
||||||
);
|
);
|
||||||
format!("{s1}:{s2}")
|
format!("{}:{}", s1, s2)
|
||||||
}
|
}
|
||||||
WrongReferenceKind {
|
WrongReferenceKind {
|
||||||
sheet_name,
|
sheet_name,
|
||||||
@@ -325,7 +325,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
|||||||
full_row,
|
full_row,
|
||||||
full_column,
|
full_column,
|
||||||
);
|
);
|
||||||
format!("{s1}:{s2}")
|
format!("{}:{}", s1, s2)
|
||||||
}
|
}
|
||||||
OpRangeKind { left, right } => format!(
|
OpRangeKind { left, right } => format!(
|
||||||
"{}:{}",
|
"{}:{}",
|
||||||
@@ -358,7 +358,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
|||||||
}
|
}
|
||||||
_ => to_string_moved(right, move_context),
|
_ => to_string_moved(right, move_context),
|
||||||
};
|
};
|
||||||
format!("{x}{kind}{y}")
|
format!("{}{}{}", x, kind, y)
|
||||||
}
|
}
|
||||||
OpPowerKind { left, right } => format!(
|
OpPowerKind { left, right } => format!(
|
||||||
"{}^{}",
|
"{}^{}",
|
||||||
@@ -403,7 +403,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enclose the whole matrix in braces
|
// Enclose the whole matrix in braces
|
||||||
format!("{{{matrix_string}}}")
|
format!("{{{}}}", matrix_string)
|
||||||
}
|
}
|
||||||
DefinedNameKind((name, ..)) => name.to_string(),
|
DefinedNameKind((name, ..)) => name.to_string(),
|
||||||
TableNameKind(name) => name.to_string(),
|
TableNameKind(name) => name.to_string(),
|
||||||
@@ -418,7 +418,7 @@ fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
|||||||
OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)),
|
OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)),
|
||||||
OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)),
|
OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)),
|
||||||
},
|
},
|
||||||
ErrorKind(kind) => format!("{kind}"),
|
ErrorKind(kind) => format!("{}", kind),
|
||||||
ParseErrorKind {
|
ParseErrorKind {
|
||||||
formula,
|
formula,
|
||||||
message: _,
|
message: _,
|
||||||
|
|||||||
@@ -184,16 +184,16 @@ pub(crate) fn stringify_reference(
|
|||||||
return "#REF!".to_string();
|
return "#REF!".to_string();
|
||||||
}
|
}
|
||||||
let mut row_abs = if absolute_row {
|
let mut row_abs = if absolute_row {
|
||||||
format!("${row}")
|
format!("${}", row)
|
||||||
} else {
|
} else {
|
||||||
format!("{row}")
|
format!("{}", row)
|
||||||
};
|
};
|
||||||
let column = match crate::expressions::utils::number_to_column(column) {
|
let column = match crate::expressions::utils::number_to_column(column) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => return "#REF!".to_string(),
|
None => return "#REF!".to_string(),
|
||||||
};
|
};
|
||||||
let mut col_abs = if absolute_column {
|
let mut col_abs = if absolute_column {
|
||||||
format!("${column}")
|
format!("${}", column)
|
||||||
} else {
|
} else {
|
||||||
column
|
column
|
||||||
};
|
};
|
||||||
@@ -208,27 +208,27 @@ pub(crate) fn stringify_reference(
|
|||||||
format!("{}!{}{}", quote_name(name), col_abs, row_abs)
|
format!("{}!{}{}", quote_name(name), col_abs, row_abs)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
format!("{col_abs}{row_abs}")
|
format!("{}{}", col_abs, row_abs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let row_abs = if absolute_row {
|
let row_abs = if absolute_row {
|
||||||
format!("R{row}")
|
format!("R{}", row)
|
||||||
} else {
|
} else {
|
||||||
format!("R[{row}]")
|
format!("R[{}]", row)
|
||||||
};
|
};
|
||||||
let col_abs = if absolute_column {
|
let col_abs = if absolute_column {
|
||||||
format!("C{column}")
|
format!("C{}", column)
|
||||||
} else {
|
} else {
|
||||||
format!("C[{column}]")
|
format!("C[{}]", column)
|
||||||
};
|
};
|
||||||
match &sheet_name {
|
match &sheet_name {
|
||||||
Some(name) => {
|
Some(name) => {
|
||||||
format!("{}!{}{}", quote_name(name), row_abs, col_abs)
|
format!("{}!{}{}", quote_name(name), row_abs, col_abs)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
format!("{row_abs}{col_abs}")
|
format!("{}{}", row_abs, col_abs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,7 +256,7 @@ fn format_function(
|
|||||||
arguments = stringify(el, context, displace_data, export_to_excel);
|
arguments = stringify(el, context, displace_data, export_to_excel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
format!("{name}({arguments})")
|
format!("{}({})", name, arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
// There is just one representation in the AST (Abstract Syntax Tree) of a formula.
|
// There is just one representation in the AST (Abstract Syntax Tree) of a formula.
|
||||||
@@ -292,9 +292,9 @@ fn stringify(
|
|||||||
) -> String {
|
) -> String {
|
||||||
use self::Node::*;
|
use self::Node::*;
|
||||||
match node {
|
match node {
|
||||||
BooleanKind(value) => format!("{value}").to_ascii_uppercase(),
|
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
|
||||||
NumberKind(number) => to_excel_precision_str(*number),
|
NumberKind(number) => to_excel_precision_str(*number),
|
||||||
StringKind(value) => format!("\"{value}\""),
|
StringKind(value) => format!("\"{}\"", value),
|
||||||
WrongReferenceKind {
|
WrongReferenceKind {
|
||||||
sheet_name,
|
sheet_name,
|
||||||
column,
|
column,
|
||||||
@@ -384,7 +384,7 @@ fn stringify(
|
|||||||
full_row,
|
full_row,
|
||||||
full_column,
|
full_column,
|
||||||
);
|
);
|
||||||
format!("{s1}:{s2}")
|
format!("{}:{}", s1, s2)
|
||||||
}
|
}
|
||||||
WrongRangeKind {
|
WrongRangeKind {
|
||||||
sheet_name,
|
sheet_name,
|
||||||
@@ -433,7 +433,7 @@ fn stringify(
|
|||||||
full_row,
|
full_row,
|
||||||
full_column,
|
full_column,
|
||||||
);
|
);
|
||||||
format!("{s1}:{s2}")
|
format!("{}:{}", s1, s2)
|
||||||
}
|
}
|
||||||
OpRangeKind { left, right } => format!(
|
OpRangeKind { left, right } => format!(
|
||||||
"{}:{}",
|
"{}:{}",
|
||||||
@@ -484,7 +484,7 @@ fn stringify(
|
|||||||
),
|
),
|
||||||
_ => stringify(right, context, displace_data, export_to_excel),
|
_ => stringify(right, context, displace_data, export_to_excel),
|
||||||
};
|
};
|
||||||
format!("{x}{kind}{y}")
|
format!("{}{}{}", x, kind, y)
|
||||||
}
|
}
|
||||||
OpPowerKind { left, right } => {
|
OpPowerKind { left, right } => {
|
||||||
let x = match **left {
|
let x = match **left {
|
||||||
@@ -547,7 +547,7 @@ fn stringify(
|
|||||||
stringify(right, context, displace_data, export_to_excel)
|
stringify(right, context, displace_data, export_to_excel)
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
format!("{x}^{y}")
|
format!("{}^{}", x, y)
|
||||||
}
|
}
|
||||||
InvalidFunctionKind { name, args } => {
|
InvalidFunctionKind { name, args } => {
|
||||||
format_function(name, args, context, displace_data, export_to_excel)
|
format_function(name, args, context, displace_data, export_to_excel)
|
||||||
@@ -582,7 +582,7 @@ fn stringify(
|
|||||||
}
|
}
|
||||||
matrix_string.push_str(&row_string);
|
matrix_string.push_str(&row_string);
|
||||||
}
|
}
|
||||||
format!("{{{matrix_string}}}")
|
format!("{{{}}}", matrix_string)
|
||||||
}
|
}
|
||||||
TableNameKind(value) => value.to_string(),
|
TableNameKind(value) => value.to_string(),
|
||||||
DefinedNameKind((name, ..)) => name.to_string(),
|
DefinedNameKind((name, ..)) => name.to_string(),
|
||||||
@@ -601,7 +601,7 @@ fn stringify(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ErrorKind(kind) => format!("{kind}"),
|
ErrorKind(kind) => format!("{}", kind),
|
||||||
ParseErrorKind {
|
ParseErrorKind {
|
||||||
formula,
|
formula,
|
||||||
position: _,
|
position: _,
|
||||||
|
|||||||
@@ -21,12 +21,14 @@ fn is_date_within_range(date: NaiveDate) -> bool {
|
|||||||
pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
|
pub fn from_excel_date(days: i64) -> Result<NaiveDate, String> {
|
||||||
if days < MINIMUM_DATE_SERIAL_NUMBER as i64 {
|
if days < MINIMUM_DATE_SERIAL_NUMBER as i64 {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Excel date must be greater than {MINIMUM_DATE_SERIAL_NUMBER}"
|
"Excel date must be greater than {}",
|
||||||
|
MINIMUM_DATE_SERIAL_NUMBER
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
if days > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
|
if days > MAXIMUM_DATE_SERIAL_NUMBER as i64 {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Excel date must be less than {MAXIMUM_DATE_SERIAL_NUMBER}"
|
"Excel date must be less than {}",
|
||||||
|
MAXIMUM_DATE_SERIAL_NUMBER
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
#[allow(clippy::expect_used)]
|
#[allow(clippy::expect_used)]
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
// We should have different codepaths for general formatting and errors
|
// We should have different codepaths for general formatting and errors
|
||||||
let value_abs = value.abs();
|
let value_abs = value.abs();
|
||||||
if (1.0e-8..1.0e+11).contains(&value_abs) {
|
if (1.0e-8..1.0e+11).contains(&value_abs) {
|
||||||
let mut text = format!("{value:.9}");
|
let mut text = format!("{:.9}", value);
|
||||||
text = text.trim_end_matches('0').trim_end_matches('.').to_string();
|
text = text.trim_end_matches('0').trim_end_matches('.').to_string();
|
||||||
Formatted {
|
Formatted {
|
||||||
text,
|
text,
|
||||||
@@ -138,7 +138,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
let exponent = value_abs.log10().floor();
|
let exponent = value_abs.log10().floor();
|
||||||
value /= 10.0_f64.powf(exponent);
|
value /= 10.0_f64.powf(exponent);
|
||||||
let sign = if exponent < 0.0 { '-' } else { '+' };
|
let sign = if exponent < 0.0 { '-' } else { '+' };
|
||||||
let s = format!("{value:.5}");
|
let s = format!("{:.5}", value);
|
||||||
Formatted {
|
Formatted {
|
||||||
text: format!(
|
text: format!(
|
||||||
"{}E{}{:02}",
|
"{}E{}{:02}",
|
||||||
@@ -167,33 +167,33 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
for token in tokens {
|
for token in tokens {
|
||||||
match token {
|
match token {
|
||||||
TextToken::Literal(c) => {
|
TextToken::Literal(c) => {
|
||||||
text = format!("{text}{c}");
|
text = format!("{}{}", text, c);
|
||||||
}
|
}
|
||||||
TextToken::Text(t) => {
|
TextToken::Text(t) => {
|
||||||
text = format!("{text}{t}");
|
text = format!("{}{}", text, t);
|
||||||
}
|
}
|
||||||
TextToken::Ghost(_) => {
|
TextToken::Ghost(_) => {
|
||||||
// we just leave a whitespace
|
// we just leave a whitespace
|
||||||
// This is what the TEXT function does
|
// This is what the TEXT function does
|
||||||
text = format!("{text} ");
|
text = format!("{} ", text);
|
||||||
}
|
}
|
||||||
TextToken::Spacer(_) => {
|
TextToken::Spacer(_) => {
|
||||||
// we just leave a whitespace
|
// we just leave a whitespace
|
||||||
// This is what the TEXT function does
|
// This is what the TEXT function does
|
||||||
text = format!("{text} ");
|
text = format!("{} ", text);
|
||||||
}
|
}
|
||||||
TextToken::Raw => {
|
TextToken::Raw => {
|
||||||
text = format!("{text}{value}");
|
text = format!("{}{}", text, value);
|
||||||
}
|
}
|
||||||
TextToken::Digit(_) => {}
|
TextToken::Digit(_) => {}
|
||||||
TextToken::Period => {}
|
TextToken::Period => {}
|
||||||
TextToken::Day => {
|
TextToken::Day => {
|
||||||
let day = date.day() as usize;
|
let day = date.day() as usize;
|
||||||
text = format!("{text}{day}");
|
text = format!("{}{}", text, day);
|
||||||
}
|
}
|
||||||
TextToken::DayPadded => {
|
TextToken::DayPadded => {
|
||||||
let day = date.day() as usize;
|
let day = date.day() as usize;
|
||||||
text = format!("{text}{day:02}");
|
text = format!("{}{:02}", text, day);
|
||||||
}
|
}
|
||||||
TextToken::DayNameShort => {
|
TextToken::DayNameShort => {
|
||||||
let mut day = date.weekday().number_from_monday() as usize;
|
let mut day = date.weekday().number_from_monday() as usize;
|
||||||
@@ -211,11 +211,11 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
}
|
}
|
||||||
TextToken::Month => {
|
TextToken::Month => {
|
||||||
let month = date.month() as usize;
|
let month = date.month() as usize;
|
||||||
text = format!("{text}{month}");
|
text = format!("{}{}", text, month);
|
||||||
}
|
}
|
||||||
TextToken::MonthPadded => {
|
TextToken::MonthPadded => {
|
||||||
let month = date.month() as usize;
|
let month = date.month() as usize;
|
||||||
text = format!("{text}{month:02}");
|
text = format!("{}{:02}", text, month);
|
||||||
}
|
}
|
||||||
TextToken::MonthNameShort => {
|
TextToken::MonthNameShort => {
|
||||||
let month = date.month() as usize;
|
let month = date.month() as usize;
|
||||||
@@ -228,7 +228,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
TextToken::MonthLetter => {
|
TextToken::MonthLetter => {
|
||||||
let month = date.month() as usize;
|
let month = date.month() as usize;
|
||||||
let months_letter = &locale.dates.months_letter[month - 1];
|
let months_letter = &locale.dates.months_letter[month - 1];
|
||||||
text = format!("{text}{months_letter}");
|
text = format!("{}{}", text, months_letter);
|
||||||
}
|
}
|
||||||
TextToken::YearShort => {
|
TextToken::YearShort => {
|
||||||
text = format!("{}{}", text, date.format("%y"));
|
text = format!("{}{}", text, date.format("%y"));
|
||||||
@@ -247,7 +247,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
ParsePart::Number(p) => {
|
ParsePart::Number(p) => {
|
||||||
let mut text = "".to_string();
|
let mut text = "".to_string();
|
||||||
if let Some(c) = p.currency {
|
if let Some(c) = p.currency {
|
||||||
text = format!("{c}");
|
text = format!("{}", c);
|
||||||
}
|
}
|
||||||
let tokens = &p.tokens;
|
let tokens = &p.tokens;
|
||||||
value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma));
|
value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma));
|
||||||
@@ -295,26 +295,26 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
for token in tokens {
|
for token in tokens {
|
||||||
match token {
|
match token {
|
||||||
TextToken::Literal(c) => {
|
TextToken::Literal(c) => {
|
||||||
text = format!("{text}{c}");
|
text = format!("{}{}", text, c);
|
||||||
}
|
}
|
||||||
TextToken::Text(t) => {
|
TextToken::Text(t) => {
|
||||||
text = format!("{text}{t}");
|
text = format!("{}{}", text, t);
|
||||||
}
|
}
|
||||||
TextToken::Ghost(_) => {
|
TextToken::Ghost(_) => {
|
||||||
// we just leave a whitespace
|
// we just leave a whitespace
|
||||||
// This is what the TEXT function does
|
// This is what the TEXT function does
|
||||||
text = format!("{text} ");
|
text = format!("{} ", text);
|
||||||
}
|
}
|
||||||
TextToken::Spacer(_) => {
|
TextToken::Spacer(_) => {
|
||||||
// we just leave a whitespace
|
// we just leave a whitespace
|
||||||
// This is what the TEXT function does
|
// This is what the TEXT function does
|
||||||
text = format!("{text} ");
|
text = format!("{} ", text);
|
||||||
}
|
}
|
||||||
TextToken::Raw => {
|
TextToken::Raw => {
|
||||||
text = format!("{text}{value}");
|
text = format!("{}{}", text, value);
|
||||||
}
|
}
|
||||||
TextToken::Period => {
|
TextToken::Period => {
|
||||||
text = format!("{text}{decimal_separator}");
|
text = format!("{}{}", text, decimal_separator);
|
||||||
}
|
}
|
||||||
TextToken::Digit(digit) => {
|
TextToken::Digit(digit) => {
|
||||||
if digit.number == 'i' {
|
if digit.number == 'i' {
|
||||||
@@ -322,7 +322,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
let index = digit.index;
|
let index = digit.index;
|
||||||
let number_index = ln - digit_count + index;
|
let number_index = ln - digit_count + index;
|
||||||
if index == 0 && is_negative {
|
if index == 0 && is_negative {
|
||||||
text = format!("-{text}");
|
text = format!("-{}", text);
|
||||||
}
|
}
|
||||||
if ln <= digit_count {
|
if ln <= digit_count {
|
||||||
// The number of digits is less or equal than the number of digit tokens
|
// The number of digits is less or equal than the number of digit tokens
|
||||||
@@ -347,7 +347,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
text = format!("{text}{c}{sep}");
|
text = format!("{}{}{}", text, c, sep);
|
||||||
}
|
}
|
||||||
digit_index += 1;
|
digit_index += 1;
|
||||||
} else {
|
} else {
|
||||||
@@ -373,18 +373,18 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
if index < fract_part.len() {
|
if index < fract_part.len() {
|
||||||
text = format!("{}{}", text, fract_part[index]);
|
text = format!("{}{}", text, fract_part[index]);
|
||||||
} else if digit.kind == '0' {
|
} else if digit.kind == '0' {
|
||||||
text = format!("{text}0");
|
text = format!("{}0", text);
|
||||||
} else if digit.kind == '?' {
|
} else if digit.kind == '?' {
|
||||||
text = format!("{text} ");
|
text = format!("{} ", text);
|
||||||
}
|
}
|
||||||
} else if digit.number == 'e' {
|
} else if digit.number == 'e' {
|
||||||
// 3. Exponent part
|
// 3. Exponent part
|
||||||
let index = digit.index;
|
let index = digit.index;
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
if exponent_is_negative {
|
if exponent_is_negative {
|
||||||
text = format!("{text}E-");
|
text = format!("{}E-", text);
|
||||||
} else {
|
} else {
|
||||||
text = format!("{text}E+");
|
text = format!("{}E+", text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let number_index = l_exp - (p.exponent_digit_count - index);
|
let number_index = l_exp - (p.exponent_digit_count - index);
|
||||||
@@ -400,7 +400,7 @@ pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Form
|
|||||||
exponent_part[number_index as usize]
|
exponent_part[number_index as usize]
|
||||||
};
|
};
|
||||||
|
|
||||||
text = format!("{text}{c}");
|
text = format!("{}{}", text, c);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for i in 0..number_index + 1 {
|
for i in 0..number_index + 1 {
|
||||||
@@ -614,7 +614,7 @@ pub(crate) fn parse_formatted_number(
|
|||||||
|
|
||||||
// check if it is a currency in currencies
|
// check if it is a currency in currencies
|
||||||
for currency in currencies {
|
for currency in currencies {
|
||||||
if let Some(p) = value.strip_prefix(&format!("-{currency}")) {
|
if let Some(p) = value.strip_prefix(&format!("-{}", currency)) {
|
||||||
let (f, options) = parse_number(p.trim())?;
|
let (f, options) = parse_number(p.trim())?;
|
||||||
if options.is_scientific {
|
if options.is_scientific {
|
||||||
return Ok((f, Some(scientific_format.to_string())));
|
return Ok((f, Some(scientific_format.to_string())));
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ impl Lexer {
|
|||||||
} else if s == '-' {
|
} else if s == '-' {
|
||||||
Token::ScientificMinus
|
Token::ScientificMinus
|
||||||
} else {
|
} else {
|
||||||
self.set_error(&format!("Unexpected char: {s}. Expected + or -"));
|
self.set_error(&format!("Unexpected char: {}. Expected + or -", s));
|
||||||
Token::ILLEGAL
|
Token::ILLEGAL
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -385,14 +385,14 @@ impl Lexer {
|
|||||||
for c in "eneral".chars() {
|
for c in "eneral".chars() {
|
||||||
let cc = self.read_next_char();
|
let cc = self.read_next_char();
|
||||||
if Some(c) != cc {
|
if Some(c) != cc {
|
||||||
self.set_error(&format!("Unexpected character: {x}"));
|
self.set_error(&format!("Unexpected character: {}", x));
|
||||||
return Token::ILLEGAL;
|
return Token::ILLEGAL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Token::General
|
Token::General
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
self.set_error(&format!("Unexpected character: {x}"));
|
self.set_error(&format!("Unexpected character: {}", x));
|
||||||
Token::ILLEGAL
|
Token::ILLEGAL
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -46,18 +46,18 @@ impl fmt::Display for Complex {
|
|||||||
// it is a bit weird what Excel does but it seems it uses general notation for
|
// it is a bit weird what Excel does but it seems it uses general notation for
|
||||||
// numbers > 1e-20 and scientific notation for the rest
|
// numbers > 1e-20 and scientific notation for the rest
|
||||||
let y_str = if y.abs() <= 9e-20 {
|
let y_str = if y.abs() <= 9e-20 {
|
||||||
format!("{y:E}")
|
format!("{:E}", y)
|
||||||
} else if y == 1.0 {
|
} else if y == 1.0 {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
} else if y == -1.0 {
|
} else if y == -1.0 {
|
||||||
"-".to_string()
|
"-".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("{y}")
|
format!("{}", y)
|
||||||
};
|
};
|
||||||
let x_str = if x.abs() <= 9e-20 {
|
let x_str = if x.abs() <= 9e-20 {
|
||||||
format!("{x:E}")
|
format!("{:E}", x)
|
||||||
} else {
|
} else {
|
||||||
format!("{x}")
|
format!("{}", x)
|
||||||
};
|
};
|
||||||
if y == 0.0 && x == 0.0 {
|
if y == 0.0 && x == 0.0 {
|
||||||
write!(f, "0")
|
write!(f, "0")
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
CalcResult::String(format!("{:0width$X}", HEX_MAX + value, width = 9))
|
CalcResult::String(format!("{:0width$X}", HEX_MAX + value, width = 9))
|
||||||
} else {
|
} else {
|
||||||
let result = format!("{value:X}");
|
let result = format!("{:X}", value);
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if places < result.len() as i32 {
|
if places < result.len() as i32 {
|
||||||
return CalcResult::new_error(
|
return CalcResult::new_error(
|
||||||
@@ -120,7 +120,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
CalcResult::String(format!("{:0width$o}", OCT_MAX + value, width = 9))
|
CalcResult::String(format!("{:0width$o}", OCT_MAX + value, width = 9))
|
||||||
} else {
|
} else {
|
||||||
let result = format!("{value:o}");
|
let result = format!("{:o}", value);
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if places < result.len() as i32 {
|
if places < result.len() as i32 {
|
||||||
return CalcResult::new_error(
|
return CalcResult::new_error(
|
||||||
@@ -163,7 +163,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
value += 1024;
|
value += 1024;
|
||||||
}
|
}
|
||||||
let result = format!("{value:b}");
|
let result = format!("{:b}", value);
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if value_raw > 0.0 && places < result.len() as i32 {
|
if value_raw > 0.0 && places < result.len() as i32 {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||||
@@ -202,7 +202,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
value += HEX_MAX;
|
value += HEX_MAX;
|
||||||
}
|
}
|
||||||
let result = format!("{value:X}");
|
let result = format!("{:X}", value);
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if value_raw > 0.0 && places < result.len() as i32 {
|
if value_raw > 0.0 && places < result.len() as i32 {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||||
@@ -242,7 +242,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
value += OCT_MAX;
|
value += OCT_MAX;
|
||||||
}
|
}
|
||||||
let result = format!("{value:o}");
|
let result = format!("{:o}", value);
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if value_raw > 0.0 && places < result.len() as i32 {
|
if value_raw > 0.0 && places < result.len() as i32 {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||||
@@ -301,7 +301,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
value += 1024;
|
value += 1024;
|
||||||
}
|
}
|
||||||
let result = format!("{value:b}");
|
let result = format!("{:b}", value);
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if places <= 0 || (value > 0 && places < result.len() as i32) {
|
if places <= 0 || (value > 0 && places < result.len() as i32) {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||||
@@ -391,7 +391,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
value += OCT_MAX;
|
value += OCT_MAX;
|
||||||
}
|
}
|
||||||
let result = format!("{value:o}");
|
let result = format!("{:o}", value);
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if places <= 0 || (value > 0 && places < result.len() as i32) {
|
if places <= 0 || (value > 0 && places < result.len() as i32) {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||||
@@ -446,7 +446,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
value += 1024;
|
value += 1024;
|
||||||
}
|
}
|
||||||
let result = format!("{value:b}");
|
let result = format!("{:b}", value);
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if value < 512 && places < result.len() as i32 {
|
if value < 512 && places < result.len() as i32 {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||||
@@ -532,7 +532,7 @@ impl Model {
|
|||||||
if value < 0 {
|
if value < 0 {
|
||||||
value += HEX_MAX;
|
value += HEX_MAX;
|
||||||
}
|
}
|
||||||
let result = format!("{value:X}");
|
let result = format!("{:X}", value);
|
||||||
if let Some(places) = places {
|
if let Some(places) = places {
|
||||||
if value < HEX_MAX_HALF && places < result.len() as i32 {
|
if value < HEX_MAX_HALF && places < result.len() as i32 {
|
||||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ impl Model {
|
|||||||
CalcResult::new_error(
|
CalcResult::new_error(
|
||||||
Error::ERROR,
|
Error::ERROR,
|
||||||
*cell,
|
*cell,
|
||||||
format!("Invalid worksheet index: '{sheet}'"),
|
format!("Invalid worksheet index: '{}'", sheet),
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.dimension()
|
.dimension()
|
||||||
@@ -245,7 +245,7 @@ impl Model {
|
|||||||
CalcResult::new_error(
|
CalcResult::new_error(
|
||||||
Error::ERROR,
|
Error::ERROR,
|
||||||
*cell,
|
*cell,
|
||||||
format!("Invalid worksheet index: '{sheet}'"),
|
format!("Invalid worksheet index: '{}'", sheet),
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.dimension()
|
.dimension()
|
||||||
|
|||||||
@@ -1214,7 +1214,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
// We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE
|
// We make a list with their functions names, but we escape ".": ERROR.TYPE => ERRORTYPE
|
||||||
let iter_list = Function::into_iter()
|
let iter_list = Function::into_iter()
|
||||||
.map(|f| format!("{f}").replace('.', ""))
|
.map(|f| format!("{}", f).replace('.', ""))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let len = iter_list.len();
|
let len = iter_list.len();
|
||||||
|
|||||||
@@ -55,14 +55,14 @@ impl Model {
|
|||||||
let mut result = "".to_string();
|
let mut result = "".to_string();
|
||||||
for arg in args {
|
for arg in args {
|
||||||
match self.evaluate_node_in_context(arg, cell) {
|
match self.evaluate_node_in_context(arg, cell) {
|
||||||
CalcResult::String(value) => result = format!("{result}{value}"),
|
CalcResult::String(value) => result = format!("{}{}", result, value),
|
||||||
CalcResult::Number(value) => result = format!("{result}{value}"),
|
CalcResult::Number(value) => result = format!("{}{}", result, value),
|
||||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||||
CalcResult::Boolean(value) => {
|
CalcResult::Boolean(value) => {
|
||||||
if value {
|
if value {
|
||||||
result = format!("{result}TRUE");
|
result = format!("{}TRUE", result);
|
||||||
} else {
|
} else {
|
||||||
result = format!("{result}FALSE");
|
result = format!("{}FALSE", result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
error @ CalcResult::Error { .. } => return error,
|
error @ CalcResult::Error { .. } => return error,
|
||||||
@@ -82,14 +82,16 @@ impl Model {
|
|||||||
column,
|
column,
|
||||||
}) {
|
}) {
|
||||||
CalcResult::String(value) => {
|
CalcResult::String(value) => {
|
||||||
result = format!("{result}{value}");
|
result = format!("{}{}", result, value);
|
||||||
|
}
|
||||||
|
CalcResult::Number(value) => {
|
||||||
|
result = format!("{}{}", result, value)
|
||||||
}
|
}
|
||||||
CalcResult::Number(value) => result = format!("{result}{value}"),
|
|
||||||
CalcResult::Boolean(value) => {
|
CalcResult::Boolean(value) => {
|
||||||
if value {
|
if value {
|
||||||
result = format!("{result}TRUE");
|
result = format!("{}TRUE", result);
|
||||||
} else {
|
} else {
|
||||||
result = format!("{result}FALSE");
|
result = format!("{}FALSE", result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
error @ CalcResult::Error { .. } => return error,
|
error @ CalcResult::Error { .. } => return error,
|
||||||
@@ -280,7 +282,7 @@ impl Model {
|
|||||||
pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() == 1 {
|
if args.len() == 1 {
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{v}"),
|
CalcResult::Number(v) => format!("{}", v),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
@@ -315,7 +317,7 @@ impl Model {
|
|||||||
pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() == 1 {
|
if args.len() == 1 {
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{v}"),
|
CalcResult::Number(v) => format!("{}", v),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
@@ -350,7 +352,7 @@ impl Model {
|
|||||||
pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() == 1 {
|
if args.len() == 1 {
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{v}"),
|
CalcResult::Number(v) => format!("{}", v),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
@@ -385,7 +387,7 @@ impl Model {
|
|||||||
pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() == 1 {
|
if args.len() == 1 {
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{v}"),
|
CalcResult::Number(v) => format!("{}", v),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
@@ -439,7 +441,7 @@ impl Model {
|
|||||||
pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
|
||||||
if args.len() == 1 {
|
if args.len() == 1 {
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{v}"),
|
CalcResult::Number(v) => format!("{}", v),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
@@ -476,7 +478,7 @@ impl Model {
|
|||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{v}"),
|
CalcResult::Number(v) => format!("{}", v),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
@@ -558,7 +560,7 @@ impl Model {
|
|||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{v}"),
|
CalcResult::Number(v) => format!("{}", v),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
@@ -640,7 +642,7 @@ impl Model {
|
|||||||
return CalcResult::new_args_number_error(cell);
|
return CalcResult::new_args_number_error(cell);
|
||||||
}
|
}
|
||||||
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
let s = match self.evaluate_node_in_context(&args[0], cell) {
|
||||||
CalcResult::Number(v) => format!("{v}"),
|
CalcResult::Number(v) => format!("{}", v),
|
||||||
CalcResult::String(v) => v,
|
CalcResult::String(v) => v,
|
||||||
CalcResult::Boolean(b) => {
|
CalcResult::Boolean(b) => {
|
||||||
if b {
|
if b {
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ pub(crate) fn from_wildcard_to_regex(
|
|||||||
|
|
||||||
// And we have a valid Perl regex! (As Kim Kardashian said before me: "I know, right?")
|
// And we have a valid Perl regex! (As Kim Kardashian said before me: "I know, right?")
|
||||||
if exact {
|
if exact {
|
||||||
return regex::Regex::new(&format!("^{reg}$"));
|
return regex::Regex::new(&format!("^{}$", reg));
|
||||||
}
|
}
|
||||||
regex::Regex::new(reg)
|
regex::Regex::new(reg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,6 @@ static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| {
|
|||||||
pub fn get_language(id: &str) -> Result<&Language, String> {
|
pub fn get_language(id: &str) -> Result<&Language, String> {
|
||||||
let language = LANGUAGES
|
let language = LANGUAGES
|
||||||
.get(id)
|
.get(id)
|
||||||
.ok_or(format!("Language is not supported: '{id}'"))?;
|
.ok_or(format!("Language is not supported: '{}'", id))?;
|
||||||
Ok(language)
|
Ok(language)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,15 +106,15 @@ pub struct Model {
|
|||||||
pub(crate) shared_strings: HashMap<String, usize>,
|
pub(crate) shared_strings: HashMap<String, usize>,
|
||||||
/// An instance of the parser
|
/// An instance of the parser
|
||||||
pub(crate) parser: Parser,
|
pub(crate) parser: Parser,
|
||||||
/// The list of cells with formulas that are evaluated or being evaluated
|
/// The list of cells with formulas that are evaluated of being evaluated
|
||||||
pub(crate) cells: HashMap<(u32, i32, i32), CellState>,
|
pub(crate) cells: HashMap<(u32, i32, i32), CellState>,
|
||||||
/// The locale of the model
|
/// The locale of the model
|
||||||
pub(crate) locale: Locale,
|
pub(crate) locale: Locale,
|
||||||
/// The language used
|
/// Tha language used
|
||||||
pub(crate) language: Language,
|
pub(crate) language: Language,
|
||||||
/// The timezone used to evaluate the model
|
/// The timezone used to evaluate the model
|
||||||
pub(crate) tz: Tz,
|
pub(crate) tz: Tz,
|
||||||
/// The view id. A view consists of a selected sheet and ranges.
|
/// The view id. A view consist of a selected sheet and ranges.
|
||||||
pub(crate) view_id: u32,
|
pub(crate) view_id: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ impl Model {
|
|||||||
_ => CalcResult::new_error(
|
_ => CalcResult::new_error(
|
||||||
Error::ERROR,
|
Error::ERROR,
|
||||||
cell,
|
cell,
|
||||||
format!("Error with Implicit Intersection in cell {cell:?}"),
|
format!("Error with Implicit Intersection in cell {:?}", cell),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
_ => self.evaluate_node_in_context(node, cell),
|
_ => self.evaluate_node_in_context(node, cell),
|
||||||
@@ -355,7 +355,7 @@ impl Model {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let result = format!("{l}{r}");
|
let result = format!("{}{}", l, r);
|
||||||
CalcResult::String(result)
|
CalcResult::String(result)
|
||||||
}
|
}
|
||||||
OpProductKind { kind, left, right } => match kind {
|
OpProductKind { kind, left, right } => match kind {
|
||||||
@@ -375,7 +375,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
FunctionKind { kind, args } => self.evaluate_function(kind, args, cell),
|
FunctionKind { kind, args } => self.evaluate_function(kind, args, cell),
|
||||||
InvalidFunctionKind { name, args: _ } => {
|
InvalidFunctionKind { name, args: _ } => {
|
||||||
CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {name}"))
|
CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {}", name))
|
||||||
}
|
}
|
||||||
ArrayKind(s) => CalcResult::Array(s.to_owned()),
|
ArrayKind(s) => CalcResult::Array(s.to_owned()),
|
||||||
DefinedNameKind((name, scope, _)) => {
|
DefinedNameKind((name, scope, _)) => {
|
||||||
@@ -391,26 +391,26 @@ impl Model {
|
|||||||
ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error(
|
ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error(
|
||||||
Error::NAME,
|
Error::NAME,
|
||||||
cell,
|
cell,
|
||||||
format!("Defined name \"{name}\" is not a reference."),
|
format!("Defined name \"{}\" is not a reference.", name),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
CalcResult::new_error(
|
CalcResult::new_error(
|
||||||
Error::NAME,
|
Error::NAME,
|
||||||
cell,
|
cell,
|
||||||
format!("Defined name \"{name}\" not found."),
|
format!("Defined name \"{}\" not found.", name),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TableNameKind(s) => CalcResult::new_error(
|
TableNameKind(s) => CalcResult::new_error(
|
||||||
Error::NAME,
|
Error::NAME,
|
||||||
cell,
|
cell,
|
||||||
format!("table name \"{s}\" not supported."),
|
format!("table name \"{}\" not supported.", s),
|
||||||
),
|
),
|
||||||
WrongVariableKind(s) => CalcResult::new_error(
|
WrongVariableKind(s) => CalcResult::new_error(
|
||||||
Error::NAME,
|
Error::NAME,
|
||||||
cell,
|
cell,
|
||||||
format!("Variable name \"{s}\" not found."),
|
format!("Variable name \"{}\" not found.", s),
|
||||||
),
|
),
|
||||||
CompareKind { kind, left, right } => {
|
CompareKind { kind, left, right } => {
|
||||||
let l = self.evaluate_node_in_context(left, cell);
|
let l = self.evaluate_node_in_context(left, cell);
|
||||||
@@ -487,7 +487,7 @@ impl Model {
|
|||||||
} => CalcResult::new_error(
|
} => CalcResult::new_error(
|
||||||
Error::ERROR,
|
Error::ERROR,
|
||||||
cell,
|
cell,
|
||||||
format!("Error parsing {formula}: {message}"),
|
format!("Error parsing {}: {}", formula, message),
|
||||||
),
|
),
|
||||||
EmptyArgKind => CalcResult::EmptyArg,
|
EmptyArgKind => CalcResult::EmptyArg,
|
||||||
ImplicitIntersection {
|
ImplicitIntersection {
|
||||||
@@ -500,7 +500,7 @@ impl Model {
|
|||||||
None => CalcResult::new_error(
|
None => CalcResult::new_error(
|
||||||
Error::VALUE,
|
Error::VALUE,
|
||||||
cell,
|
cell,
|
||||||
format!("Error with Implicit Intersection in cell {cell:?}"),
|
format!("Error with Implicit Intersection in cell {:?}", cell),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -697,7 +697,7 @@ impl Model {
|
|||||||
worksheet.color = Some(color.to_string());
|
worksheet.color = Some(color.to_string());
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Err(format!("Invalid color: {color}"))
|
Err(format!("Invalid color: {}", color))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Changes the visibility of a sheet
|
/// Changes the visibility of a sheet
|
||||||
@@ -1027,7 +1027,7 @@ impl Model {
|
|||||||
let source_sheet_name = self
|
let source_sheet_name = self
|
||||||
.workbook
|
.workbook
|
||||||
.worksheet(source.sheet)
|
.worksheet(source.sheet)
|
||||||
.map_err(|e| format!("Could not find source worksheet: {e}"))?
|
.map_err(|e| format!("Could not find source worksheet: {}", e))?
|
||||||
.get_name();
|
.get_name();
|
||||||
if source.sheet != area.sheet {
|
if source.sheet != area.sheet {
|
||||||
return Err("Source and area are in different sheets".to_string());
|
return Err("Source and area are in different sheets".to_string());
|
||||||
@@ -1041,7 +1041,7 @@ impl Model {
|
|||||||
let target_sheet_name = self
|
let target_sheet_name = self
|
||||||
.workbook
|
.workbook
|
||||||
.worksheet(target.sheet)
|
.worksheet(target.sheet)
|
||||||
.map_err(|e| format!("Could not find target worksheet: {e}"))?
|
.map_err(|e| format!("Could not find target worksheet: {}", e))?
|
||||||
.get_name();
|
.get_name();
|
||||||
if let Some(formula) = value.strip_prefix('=') {
|
if let Some(formula) = value.strip_prefix('=') {
|
||||||
let cell_reference = CellReferenceRC {
|
let cell_reference = CellReferenceRC {
|
||||||
@@ -1061,7 +1061,7 @@ impl Model {
|
|||||||
column_delta: target.column - source.column,
|
column_delta: target.column - source.column,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Ok(format!("={formula_str}"))
|
Ok(format!("={}", formula_str))
|
||||||
} else {
|
} else {
|
||||||
Ok(value.to_string())
|
Ok(value.to_string())
|
||||||
}
|
}
|
||||||
@@ -1538,7 +1538,7 @@ impl Model {
|
|||||||
// If the formula fails to parse try adding a parenthesis
|
// If the formula fails to parse try adding a parenthesis
|
||||||
// SUM(A1:A3 => SUM(A1:A3)
|
// SUM(A1:A3 => SUM(A1:A3)
|
||||||
if let Node::ParseErrorKind { .. } = parsed_formula {
|
if let Node::ParseErrorKind { .. } = parsed_formula {
|
||||||
let new_parsed_formula = self.parser.parse(&format!("{formula})"), &cell_reference);
|
let new_parsed_formula = self.parser.parse(&format!("{})", formula), &cell_reference);
|
||||||
match new_parsed_formula {
|
match new_parsed_formula {
|
||||||
Node::ParseErrorKind { .. } => {}
|
Node::ParseErrorKind { .. } => {}
|
||||||
_ => parsed_formula = new_parsed_formula,
|
_ => parsed_formula = new_parsed_formula,
|
||||||
@@ -1931,16 +1931,32 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns markup representation of the given `sheet`.
|
/// Returns markup representation of the given `sheet`.
|
||||||
pub fn get_sheet_markup(&self, sheet: u32) -> Result<String, String> {
|
pub fn get_sheet_markup(
|
||||||
let worksheet = self.workbook.worksheet(sheet)?;
|
&self,
|
||||||
let dimension = worksheet.dimension();
|
sheet: u32,
|
||||||
|
start_row: i32,
|
||||||
|
start_column: i32,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let mut table: Vec<Vec<String>> = Vec::new();
|
||||||
|
if start_row < 1 || start_column < 1 {
|
||||||
|
return Err("Start row and column must be positive".to_string());
|
||||||
|
}
|
||||||
|
if start_row + height >= LAST_ROW || start_column + width >= LAST_COLUMN {
|
||||||
|
return Err("Start row and column exceed the maximum allowed".to_string());
|
||||||
|
}
|
||||||
|
if height <= 0 || width <= 0 {
|
||||||
|
return Err("Height must be positive and width must be positive".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
let mut rows = Vec::new();
|
// a mutable vector to store the column widths of length `width + 1`
|
||||||
|
let mut column_widths: Vec<f64> = vec![0.0; (width + 1) as usize];
|
||||||
|
|
||||||
for row in 1..(dimension.max_row + 1) {
|
for row in start_row..(start_row + height + 1) {
|
||||||
let mut row_markup: Vec<String> = Vec::new();
|
let mut row_markup: Vec<String> = Vec::new();
|
||||||
|
|
||||||
for column in 1..(dimension.max_column + 1) {
|
for column in start_column..(start_column + width + 1) {
|
||||||
let mut cell_markup = match self.get_cell_formula(sheet, row, column)? {
|
let mut cell_markup = match self.get_cell_formula(sheet, row, column)? {
|
||||||
Some(formula) => formula,
|
Some(formula) => formula,
|
||||||
None => self.get_formatted_cell_value(sheet, row, column)?,
|
None => self.get_formatted_cell_value(sheet, row, column)?,
|
||||||
@@ -1949,12 +1965,34 @@ impl Model {
|
|||||||
if style.font.b {
|
if style.font.b {
|
||||||
cell_markup = format!("**{cell_markup}**")
|
cell_markup = format!("**{cell_markup}**")
|
||||||
}
|
}
|
||||||
|
column_widths[(column - start_column) as usize] =
|
||||||
|
column_widths[(column - start_column) as usize].max(cell_markup.len() as f64);
|
||||||
row_markup.push(cell_markup);
|
row_markup.push(cell_markup);
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.push(row_markup.join("|"));
|
table.push(row_markup);
|
||||||
}
|
}
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
for (j, row) in table.iter().enumerate() {
|
||||||
|
if j == 1 {
|
||||||
|
let mut row_markup = String::new();
|
||||||
|
for i in 0..(width + 1) {
|
||||||
|
row_markup.push('|');
|
||||||
|
let wide = column_widths[i as usize] as usize;
|
||||||
|
row_markup.push_str(&"-".repeat(wide));
|
||||||
|
}
|
||||||
|
rows.push(row_markup);
|
||||||
|
}
|
||||||
|
let mut row_markup = String::new();
|
||||||
|
|
||||||
|
for (i, cell) in row.iter().enumerate() {
|
||||||
|
row_markup.push('|');
|
||||||
|
let wide = column_widths[i] as usize;
|
||||||
|
// Add padding to the cell content
|
||||||
|
row_markup.push_str(&format!("{:<wide$}", cell, wide = wide));
|
||||||
|
}
|
||||||
|
rows.push(row_markup);
|
||||||
|
}
|
||||||
Ok(rows.join("\n"))
|
Ok(rows.join("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -168,11 +168,11 @@ impl Model {
|
|||||||
.get_worksheet_names()
|
.get_worksheet_names()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| s.to_uppercase())
|
.map(|s| s.to_uppercase())
|
||||||
.any(|x| x == format!("{base_name_uppercase}{index}"))
|
.any(|x| x == format!("{}{}", base_name_uppercase, index))
|
||||||
{
|
{
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
let sheet_name = format!("{base_name}{index}");
|
let sheet_name = format!("{}{}", base_name, index);
|
||||||
// Now we need a sheet_id
|
// Now we need a sheet_id
|
||||||
let sheet_id = self.get_new_sheet_id();
|
let sheet_id = self.get_new_sheet_id();
|
||||||
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
|
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
|
||||||
@@ -192,7 +192,7 @@ impl Model {
|
|||||||
sheet_id: Option<u32>,
|
sheet_id: Option<u32>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if !is_valid_sheet_name(sheet_name) {
|
if !is_valid_sheet_name(sheet_name) {
|
||||||
return Err(format!("Invalid name for a sheet: '{sheet_name}'"));
|
return Err(format!("Invalid name for a sheet: '{}'", sheet_name));
|
||||||
}
|
}
|
||||||
if self
|
if self
|
||||||
.workbook
|
.workbook
|
||||||
@@ -234,7 +234,7 @@ impl Model {
|
|||||||
if let Some(sheet_index) = self.get_sheet_index_by_name(old_name) {
|
if let Some(sheet_index) = self.get_sheet_index_by_name(old_name) {
|
||||||
return self.rename_sheet_by_index(sheet_index, new_name);
|
return self.rename_sheet_by_index(sheet_index, new_name);
|
||||||
}
|
}
|
||||||
Err(format!("Could not find sheet {old_name}"))
|
Err(format!("Could not find sheet {}", old_name))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renames a sheet and updates all existing references to that sheet.
|
/// Renames a sheet and updates all existing references to that sheet.
|
||||||
@@ -248,10 +248,10 @@ impl Model {
|
|||||||
new_name: &str,
|
new_name: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
if !is_valid_sheet_name(new_name) {
|
if !is_valid_sheet_name(new_name) {
|
||||||
return Err(format!("Invalid name for a sheet: '{new_name}'."));
|
return Err(format!("Invalid name for a sheet: '{}'.", new_name));
|
||||||
}
|
}
|
||||||
if self.get_sheet_index_by_name(new_name).is_some() {
|
if self.get_sheet_index_by_name(new_name).is_some() {
|
||||||
return Err(format!("Sheet already exists: '{new_name}'."));
|
return Err(format!("Sheet already exists: '{}'.", new_name));
|
||||||
}
|
}
|
||||||
// Gets the new name and checks that a sheet with that index exists
|
// Gets the new name and checks that a sheet with that index exists
|
||||||
let old_name = self.workbook.worksheet(sheet_index)?.get_name();
|
let old_name = self.workbook.worksheet(sheet_index)?.get_name();
|
||||||
@@ -362,14 +362,14 @@ impl Model {
|
|||||||
};
|
};
|
||||||
let locale = match get_locale(locale_id) {
|
let locale = match get_locale(locale_id) {
|
||||||
Ok(l) => l.clone(),
|
Ok(l) => l.clone(),
|
||||||
Err(_) => return Err(format!("Invalid locale: {locale_id}")),
|
Err(_) => return Err(format!("Invalid locale: {}", locale_id)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let milliseconds = get_milliseconds_since_epoch();
|
let milliseconds = get_milliseconds_since_epoch();
|
||||||
let seconds = milliseconds / 1000;
|
let seconds = milliseconds / 1000;
|
||||||
let dt = match DateTime::from_timestamp(seconds, 0) {
|
let dt = match DateTime::from_timestamp(seconds, 0) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => return Err(format!("Invalid timestamp: {milliseconds}")),
|
None => return Err(format!("Invalid timestamp: {}", milliseconds)),
|
||||||
};
|
};
|
||||||
// "2020-08-06T21:20:53Z
|
// "2020-08-06T21:20:53Z
|
||||||
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ pub fn to_precision_str(value: f64, precision: usize) -> String {
|
|||||||
let exponent = value.abs().log10().floor();
|
let exponent = value.abs().log10().floor();
|
||||||
let base = value / 10.0_f64.powf(exponent);
|
let base = value / 10.0_f64.powf(exponent);
|
||||||
let base = format!("{0:.1$}", base, precision - 1);
|
let base = format!("{0:.1$}", base, precision - 1);
|
||||||
let value = format!("{base}e{exponent}").parse::<f64>().unwrap_or({
|
let value = format!("{}e{}", base, exponent).parse::<f64>().unwrap_or({
|
||||||
// TODO: do this in a way that does not require a possible error
|
// TODO: do this in a way that does not require a possible error
|
||||||
0.0
|
0.0
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ impl Styles {
|
|||||||
return Ok(cell_style.xf_id);
|
return Ok(cell_style.xf_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(format!("Style '{style_name}' not found"))
|
Err(format!("Style '{}' not found", style_name))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_named_style(&mut self, style_name: &str, style: &Style) -> Result<(), String> {
|
pub fn create_named_style(&mut self, style_name: &str, style: &Style) -> Result<(), String> {
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ fn fn_or_xor_no_arguments() {
|
|||||||
println!("Testing function: {func}");
|
println!("Testing function: {func}");
|
||||||
|
|
||||||
let mut model = new_empty_model();
|
let mut model = new_empty_model();
|
||||||
model._set("A1", &format!("={func}()"));
|
model._set("A1", &format!("={}()", func));
|
||||||
model.evaluate();
|
model.evaluate();
|
||||||
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
assert_eq!(model._get_text("A1"), *"#ERROR!");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ fn test_sheet_markup() {
|
|||||||
model.set_cell_style(0, 4, 1, &style).unwrap();
|
model.set_cell_style(0, 4, 1, &style).unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
model.get_sheet_markup(0),
|
model.get_sheet_markup(0, 1, 1, 4, 2),
|
||||||
Ok("**Item**|**Cost**\nRent|$600\nElectricity|$200\n**Total**|=SUM(B2:B3)".to_string()),
|
Ok("**Item**|**Cost**\nRent|$600\nElectricity|$200\n**Total**|=SUM(B2:B3)".to_string()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,10 @@ fn check_borders(model: &UserModel) {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(top_border),
|
Some(top_border),
|
||||||
top_cell_style.border.bottom,
|
top_cell_style.border.bottom,
|
||||||
"(Top). Sheet: {sheet}, row: {row}, column: {column}"
|
"(Top). Sheet: {}, row: {}, column: {}",
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +65,10 @@ fn check_borders(model: &UserModel) {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(right_border),
|
Some(right_border),
|
||||||
right_cell_style.border.left,
|
right_cell_style.border.left,
|
||||||
"(Right). Sheet: {sheet}, row: {row}, column: {column}"
|
"(Right). Sheet: {}, row: {}, column: {}",
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,7 +80,10 @@ fn check_borders(model: &UserModel) {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(bottom_border),
|
Some(bottom_border),
|
||||||
bottom_cell_style.border.top,
|
bottom_cell_style.border.top,
|
||||||
"(Bottom). Sheet: {sheet}, row: {row}, column: {column}"
|
"(Bottom). Sheet: {}, row: {}, column: {}",
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +94,10 @@ fn check_borders(model: &UserModel) {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(left_border),
|
Some(left_border),
|
||||||
left_cell_style.border.right,
|
left_cell_style.border.right,
|
||||||
"(Left). Sheet: {sheet}, row: {row}, column: {column}"
|
"(Left). Sheet: {}, row: {}, column: {}",
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ fn set_user_input_errors() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn user_model_debug_message() {
|
fn user_model_debug_message() {
|
||||||
let model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
let model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||||
let s = &format!("{model:?}");
|
let s = &format!("{:?}", model);
|
||||||
assert_eq!(s, "UserModel");
|
assert_eq!(s, "UserModel");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ impl UserModel {
|
|||||||
r##"{{
|
r##"{{
|
||||||
"item": {{
|
"item": {{
|
||||||
"style": "thin",
|
"style": "thin",
|
||||||
"color": "{color}"
|
"color": "{}"
|
||||||
}},
|
}},
|
||||||
"type": "All"
|
"type": "All"
|
||||||
}}"##
|
}}"##,
|
||||||
|
color
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let range = &Area {
|
let range = &Area {
|
||||||
@@ -39,10 +40,11 @@ impl UserModel {
|
|||||||
r##"{{
|
r##"{{
|
||||||
"item": {{
|
"item": {{
|
||||||
"style": "thin",
|
"style": "thin",
|
||||||
"color": "{color}"
|
"color": "{}"
|
||||||
}},
|
}},
|
||||||
"type": "{kind}"
|
"type": "{}"
|
||||||
}}"##
|
}}"##,
|
||||||
|
color, kind
|
||||||
))
|
))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let range = &Area {
|
let range = &Area {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ impl Model {
|
|||||||
if cell.contains('!') {
|
if cell.contains('!') {
|
||||||
self.parse_reference(cell).unwrap()
|
self.parse_reference(cell).unwrap()
|
||||||
} else {
|
} else {
|
||||||
self.parse_reference(&format!("Sheet1!{cell}")).unwrap()
|
self.parse_reference(&format!("Sheet1!{}", cell)).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn _set(&mut self, cell: &str, value: &str) {
|
pub fn _set(&mut self, cell: &str, value: &str) {
|
||||||
|
|||||||
@@ -293,6 +293,19 @@ impl UserModel {
|
|||||||
self.model.workbook.name = name.to_string();
|
self.model.workbook.name = name.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get area markdown
|
||||||
|
pub fn get_sheet_markup(
|
||||||
|
&self,
|
||||||
|
sheet: u32,
|
||||||
|
row_start: i32,
|
||||||
|
column_start: i32,
|
||||||
|
row_end: i32,
|
||||||
|
column_end: i32,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
self.model
|
||||||
|
.get_sheet_markup(sheet, row_start, column_start, row_end, column_end)
|
||||||
|
}
|
||||||
|
|
||||||
/// Undoes last change if any, places the change in the redo list and evaluates the model if needed
|
/// Undoes last change if any, places the change in the redo list and evaluates the model if needed
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
@@ -1487,10 +1500,10 @@ impl UserModel {
|
|||||||
return Err(format!("Invalid row: '{first_row}'"));
|
return Err(format!("Invalid row: '{first_row}'"));
|
||||||
}
|
}
|
||||||
if !is_valid_column_number(last_column) {
|
if !is_valid_column_number(last_column) {
|
||||||
return Err(format!("Invalid column: '{last_column}'"));
|
return Err(format!("Invalid column: '{}'", last_column));
|
||||||
}
|
}
|
||||||
if !is_valid_row(last_row) {
|
if !is_valid_row(last_row) {
|
||||||
return Err(format!("Invalid row: '{last_row}'"));
|
return Err(format!("Invalid row: '{}'", last_row));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !is_valid_row(to_column) {
|
if !is_valid_row(to_column) {
|
||||||
@@ -1623,15 +1636,15 @@ impl UserModel {
|
|||||||
text_row.push(text);
|
text_row.push(text);
|
||||||
}
|
}
|
||||||
wtr.write_record(text_row)
|
wtr.write_record(text_row)
|
||||||
.map_err(|e| format!("Error while processing csv: {e}"))?;
|
.map_err(|e| format!("Error while processing csv: {}", e))?;
|
||||||
data.insert(row, data_row);
|
data.insert(row, data_row);
|
||||||
}
|
}
|
||||||
|
|
||||||
let csv = String::from_utf8(
|
let csv = String::from_utf8(
|
||||||
wtr.into_inner()
|
wtr.into_inner()
|
||||||
.map_err(|e| format!("Processing error: '{e}'"))?,
|
.map_err(|e| format!("Processing error: '{}'", e))?,
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Error converting from utf8: '{e}'"))?;
|
.map_err(|e| format!("Error converting from utf8: '{}'", e))?;
|
||||||
|
|
||||||
Ok(Clipboard {
|
Ok(Clipboard {
|
||||||
csv,
|
csv,
|
||||||
@@ -2391,7 +2404,7 @@ mod tests {
|
|||||||
VerticalAlignment::Top,
|
VerticalAlignment::Top,
|
||||||
];
|
];
|
||||||
for a in all {
|
for a in all {
|
||||||
assert_eq!(vertical(&format!("{a}")), Ok(a));
|
assert_eq!(vertical(&format!("{}", a)), Ok(a));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2408,7 +2421,7 @@ mod tests {
|
|||||||
HorizontalAlignment::Right,
|
HorizontalAlignment::Right,
|
||||||
];
|
];
|
||||||
for a in all {
|
for a in all {
|
||||||
assert_eq!(horizontal(&format!("{a}")), Ok(a));
|
assert_eq!(horizontal(&format!("{}", a)), Ok(a));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ impl UserModel {
|
|||||||
/// Sets the the selected sheet
|
/// Sets the the selected sheet
|
||||||
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
|
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
|
||||||
if self.model.workbook.worksheet(sheet).is_err() {
|
if self.model.workbook.worksheet(sheet).is_err() {
|
||||||
return Err(format!("Invalid worksheet index {sheet}"));
|
return Err(format!("Invalid worksheet index {}", sheet));
|
||||||
}
|
}
|
||||||
if let Some(view) = self.model.workbook.views.get_mut(&0) {
|
if let Some(view) = self.model.workbook.views.get_mut(&0) {
|
||||||
view.sheet = sheet;
|
view.sheet = sheet;
|
||||||
@@ -98,7 +98,7 @@ impl UserModel {
|
|||||||
return Err(format!("Invalid row: '{row}'"));
|
return Err(format!("Invalid row: '{row}'"));
|
||||||
}
|
}
|
||||||
if self.model.workbook.worksheet(sheet).is_err() {
|
if self.model.workbook.worksheet(sheet).is_err() {
|
||||||
return Err(format!("Invalid worksheet index {sheet}"));
|
return Err(format!("Invalid worksheet index {}", sheet));
|
||||||
}
|
}
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||||
@@ -138,7 +138,7 @@ impl UserModel {
|
|||||||
return Err(format!("Invalid row: '{end_row}'"));
|
return Err(format!("Invalid row: '{end_row}'"));
|
||||||
}
|
}
|
||||||
if self.model.workbook.worksheet(sheet).is_err() {
|
if self.model.workbook.worksheet(sheet).is_err() {
|
||||||
return Err(format!("Invalid worksheet index {sheet}"));
|
return Err(format!("Invalid worksheet index {}", sheet));
|
||||||
}
|
}
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||||
@@ -147,12 +147,14 @@ impl UserModel {
|
|||||||
// The selected cells must be on one of the corners of the selected range:
|
// The selected cells must be on one of the corners of the selected range:
|
||||||
if selected_row != start_row && selected_row != end_row {
|
if selected_row != start_row && selected_row != end_row {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"The selected cells is not in one of the corners. Row: '{selected_row}' and row range '({start_row}, {end_row})'"
|
"The selected cells is not in one of the corners. Row: '{}' and row range '({}, {})'",
|
||||||
|
selected_row, start_row, end_row
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if selected_column != start_column && selected_column != end_column {
|
if selected_column != start_column && selected_column != end_column {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"The selected cells is not in one of the corners. Column '{selected_column}' and column range '({start_column}, {end_column})'"
|
"The selected cells is not in one of the corners. Column '{}' and column range '({}, {})'",
|
||||||
|
selected_column, start_column, end_column
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
view.range = [start_row, start_column, end_row, end_column];
|
view.range = [start_row, start_column, end_row, end_column];
|
||||||
@@ -305,7 +307,7 @@ impl UserModel {
|
|||||||
return Err(format!("Invalid row: '{top_row}'"));
|
return Err(format!("Invalid row: '{top_row}'"));
|
||||||
}
|
}
|
||||||
if self.model.workbook.worksheet(sheet).is_err() {
|
if self.model.workbook.worksheet(sheet).is_err() {
|
||||||
return Err(format!("Invalid worksheet index {sheet}"));
|
return Err(format!("Invalid worksheet index {}", sheet));
|
||||||
}
|
}
|
||||||
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
|
||||||
if let Some(view) = worksheet.views.get_mut(&0) {
|
if let Some(view) = worksheet.views.get_mut(&0) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ crate-type = ["cdylib"]
|
|||||||
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
|
# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations
|
||||||
ironcalc_base = { path = "../../base", version = "0.5", features = ["use_regex_lite"] }
|
ironcalc_base = { path = "../../base", version = "0.5", features = ["use_regex_lite"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
wasm-bindgen = "0.2.100"
|
wasm-bindgen = "0.2.92"
|
||||||
serde-wasm-bindgen = "0.4"
|
serde-wasm-bindgen = "0.4"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -26,4 +26,4 @@ clean:
|
|||||||
rm -rf pkg
|
rm -rf pkg
|
||||||
rm -f types.js
|
rm -f types.js
|
||||||
|
|
||||||
.PHONY: all lint clean tests
|
.PHONY: all lint clean
|
||||||
|
|||||||
@@ -1,25 +1,231 @@
|
|||||||
|
# Regrettably at the time of writing there is not a perfect way to
|
||||||
|
# generate the TypeScript types from Rust so we basically fix them manually
|
||||||
|
# Hopefully this will suffice for our needs and one day will be automatic
|
||||||
|
|
||||||
header = r"""
|
header = r"""
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
def fix_types(text: str):
|
get_tokens_str = r"""
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
export function getTokens(formula: string): any;
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
get_tokens_str_types = r"""
|
||||||
|
* @returns {MarkedToken[]}
|
||||||
|
*/
|
||||||
|
export function getTokens(formula: string): MarkedToken[];
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
update_style_str = r"""
|
||||||
|
/**
|
||||||
|
* @param {any} range
|
||||||
|
* @param {string} style_path
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
updateRangeStyle(range: any, style_path: string, value: string): void;
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
update_style_str_types = r"""
|
||||||
|
/**
|
||||||
|
* @param {Area} range
|
||||||
|
* @param {string} style_path
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
updateRangeStyle(range: Area, style_path: string, value: string): void;
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
properties = r"""
|
||||||
|
/**
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
getWorksheetsProperties(): any;
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
properties_types = r"""
|
||||||
|
/**
|
||||||
|
* @returns {WorksheetProperties[]}
|
||||||
|
*/
|
||||||
|
getWorksheetsProperties(): WorksheetProperties[];
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
style = r"""
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
getCellStyle(sheet: number, row: number, column: number): any;
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
style_types = r"""
|
||||||
|
* @returns {CellStyle}
|
||||||
|
*/
|
||||||
|
getCellStyle(sheet: number, row: number, column: number): CellStyle;
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
view = r"""
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
getSelectedView(): any;
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
view_types = r"""
|
||||||
|
* @returns {CellStyle}
|
||||||
|
*/
|
||||||
|
getSelectedView(): SelectedView;
|
||||||
|
""".strip()
|
||||||
|
|
||||||
|
autofill_rows = r"""
|
||||||
|
/**
|
||||||
|
* @param {any} source_area
|
||||||
|
* @param {number} to_row
|
||||||
|
*/
|
||||||
|
autoFillRows(source_area: any, to_row: number): void;
|
||||||
|
"""
|
||||||
|
|
||||||
|
autofill_rows_types = r"""
|
||||||
|
/**
|
||||||
|
* @param {Area} source_area
|
||||||
|
* @param {number} to_row
|
||||||
|
*/
|
||||||
|
autoFillRows(source_area: Area, to_row: number): void;
|
||||||
|
"""
|
||||||
|
|
||||||
|
autofill_columns = r"""
|
||||||
|
/**
|
||||||
|
* @param {any} source_area
|
||||||
|
* @param {number} to_column
|
||||||
|
*/
|
||||||
|
autoFillColumns(source_area: any, to_column: number): void;
|
||||||
|
"""
|
||||||
|
|
||||||
|
autofill_columns_types = r"""
|
||||||
|
/**
|
||||||
|
* @param {Area} source_area
|
||||||
|
* @param {number} to_column
|
||||||
|
*/
|
||||||
|
autoFillColumns(source_area: Area, to_column: number): void;
|
||||||
|
"""
|
||||||
|
|
||||||
|
set_cell_style = r"""
|
||||||
|
/**
|
||||||
|
* @param {any} styles
|
||||||
|
*/
|
||||||
|
onPasteStyles(styles: any): void;
|
||||||
|
"""
|
||||||
|
|
||||||
|
set_cell_style_types = r"""
|
||||||
|
/**
|
||||||
|
* @param {CellStyle[][]} styles
|
||||||
|
*/
|
||||||
|
onPasteStyles(styles: CellStyle[][]): void;
|
||||||
|
"""
|
||||||
|
|
||||||
|
set_area_border = r"""
|
||||||
|
/**
|
||||||
|
* @param {any} area
|
||||||
|
* @param {any} border_area
|
||||||
|
*/
|
||||||
|
setAreaWithBorder(area: any, border_area: any): void;
|
||||||
|
"""
|
||||||
|
|
||||||
|
set_area_border_types = r"""
|
||||||
|
/**
|
||||||
|
* @param {Area} area
|
||||||
|
* @param {BorderArea} border_area
|
||||||
|
*/
|
||||||
|
setAreaWithBorder(area: Area, border_area: BorderArea): void;
|
||||||
|
"""
|
||||||
|
|
||||||
|
paste_csv_string = r"""
|
||||||
|
/**
|
||||||
|
* @param {any} area
|
||||||
|
* @param {string} csv
|
||||||
|
*/
|
||||||
|
pasteCsvText(area: any, csv: string): void;
|
||||||
|
"""
|
||||||
|
|
||||||
|
paste_csv_string_types = r"""
|
||||||
|
/**
|
||||||
|
* @param {Area} area
|
||||||
|
* @param {string} csv
|
||||||
|
*/
|
||||||
|
pasteCsvText(area: Area, csv: string): void;
|
||||||
|
"""
|
||||||
|
|
||||||
|
clipboard = r"""
|
||||||
|
/**
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
copyToClipboard(): any;
|
||||||
|
"""
|
||||||
|
|
||||||
|
clipboard_types = r"""
|
||||||
|
/**
|
||||||
|
* @returns {Clipboard}
|
||||||
|
*/
|
||||||
|
copyToClipboard(): Clipboard;
|
||||||
|
"""
|
||||||
|
|
||||||
|
paste_from_clipboard = r"""
|
||||||
|
/**
|
||||||
|
* @param {number} source_sheet
|
||||||
|
* @param {any} source_range
|
||||||
|
* @param {any} clipboard
|
||||||
|
* @param {boolean} is_cut
|
||||||
|
*/
|
||||||
|
pasteFromClipboard(source_sheet: number, source_range: any, clipboard: any, is_cut: boolean): void;
|
||||||
|
"""
|
||||||
|
|
||||||
|
paste_from_clipboard_types = r"""
|
||||||
|
/**
|
||||||
|
* @param {number} source_sheet
|
||||||
|
* @param {[number, number, number, number]} source_range
|
||||||
|
* @param {ClipboardData} clipboard
|
||||||
|
* @param {boolean} is_cut
|
||||||
|
*/
|
||||||
|
pasteFromClipboard(source_sheet: number, source_range: [number, number, number, number], clipboard: ClipboardData, is_cut: boolean): void;
|
||||||
|
"""
|
||||||
|
|
||||||
|
defined_name_list = r"""
|
||||||
|
/**
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
getDefinedNameList(): any;
|
||||||
|
"""
|
||||||
|
|
||||||
|
defined_name_list_types = r"""
|
||||||
|
/**
|
||||||
|
* @returns {DefinedName[]}
|
||||||
|
*/
|
||||||
|
getDefinedNameList(): DefinedName[];
|
||||||
|
"""
|
||||||
|
|
||||||
|
def fix_types(text):
|
||||||
|
text = text.replace(get_tokens_str, get_tokens_str_types)
|
||||||
|
text = text.replace(update_style_str, update_style_str_types)
|
||||||
|
text = text.replace(properties, properties_types)
|
||||||
|
text = text.replace(style, style_types)
|
||||||
|
text = text.replace(view, view_types)
|
||||||
|
text = text.replace(autofill_rows, autofill_rows_types)
|
||||||
|
text = text.replace(autofill_columns, autofill_columns_types)
|
||||||
|
text = text.replace(set_cell_style, set_cell_style_types)
|
||||||
|
text = text.replace(set_area_border, set_area_border_types)
|
||||||
|
text = text.replace(paste_csv_string, paste_csv_string_types)
|
||||||
|
text = text.replace(clipboard, clipboard_types)
|
||||||
|
text = text.replace(paste_from_clipboard, paste_from_clipboard_types)
|
||||||
|
text = text.replace(defined_name_list, defined_name_list_types)
|
||||||
with open("types.ts") as f:
|
with open("types.ts") as f:
|
||||||
types_str = f.read()
|
types_str = f.read()
|
||||||
header_types = "{}\n\n{}".format(header, types_str)
|
header_types = "{}\n\n{}".format(header, types_str)
|
||||||
|
|
||||||
text = text.replace(header, header_types)
|
text = text.replace(header, header_types)
|
||||||
for line in text.splitlines():
|
if text.find("any") != -1:
|
||||||
line = line.lstrip()
|
print("There are 'unfixed' types. Please check.")
|
||||||
# Skip internal methods
|
exit(1)
|
||||||
if line.startswith("readonly model_"):
|
|
||||||
continue
|
|
||||||
if line.find("any") != -1:
|
|
||||||
print("There are 'unfixed' public types. Please check.")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
return text
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
types_file = "pkg/wasm.d.ts"
|
types_file = "pkg/wasm.d.ts"
|
||||||
with open(types_file) as f:
|
with open(types_file) as f:
|
||||||
@@ -37,3 +243,5 @@ if __name__ == "__main__":
|
|||||||
with open(js_file, "wb") as f:
|
with open(js_file, "wb") as f:
|
||||||
f.write(bytes("{}\n{}".format(text_js, text), "utf8"))
|
f.write(bytes("{}\n{}".format(text_js, text), "utf8"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ fn to_js_error(error: String) -> JsError {
|
|||||||
|
|
||||||
/// Return an array with a list of all the tokens from a formula
|
/// Return an array with a list of all the tokens from a formula
|
||||||
/// This is used by the UI to color them according to a theme.
|
/// This is used by the UI to color them according to a theme.
|
||||||
#[wasm_bindgen(js_name = "getTokens", unchecked_return_type = "MarkedToken[]")]
|
#[wasm_bindgen(js_name = "getTokens")]
|
||||||
pub fn get_tokens(formula: &str) -> Result<JsValue, JsError> {
|
pub fn get_tokens(formula: &str) -> Result<JsValue, JsError> {
|
||||||
let tokens = tokenizer(formula);
|
let tokens = tokenizer(formula);
|
||||||
serde_wasm_bindgen::to_value(&tokens).map_err(JsError::from)
|
serde_wasm_bindgen::to_value(&tokens).map_err(JsError::from)
|
||||||
@@ -338,7 +338,7 @@ impl Model {
|
|||||||
#[wasm_bindgen(js_name = "updateRangeStyle")]
|
#[wasm_bindgen(js_name = "updateRangeStyle")]
|
||||||
pub fn update_range_style(
|
pub fn update_range_style(
|
||||||
&mut self,
|
&mut self,
|
||||||
#[wasm_bindgen(unchecked_param_type = "Area")] range: JsValue,
|
range: JsValue,
|
||||||
style_path: &str,
|
style_path: &str,
|
||||||
value: &str,
|
value: &str,
|
||||||
) -> Result<(), JsError> {
|
) -> Result<(), JsError> {
|
||||||
@@ -349,7 +349,7 @@ impl Model {
|
|||||||
.map_err(to_js_error)
|
.map_err(to_js_error)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "getCellStyle", unchecked_return_type = "CellStyle")]
|
#[wasm_bindgen(js_name = "getCellStyle")]
|
||||||
pub fn get_cell_style(
|
pub fn get_cell_style(
|
||||||
&mut self,
|
&mut self,
|
||||||
sheet: u32,
|
sheet: u32,
|
||||||
@@ -365,10 +365,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "onPasteStyles")]
|
#[wasm_bindgen(js_name = "onPasteStyles")]
|
||||||
pub fn on_paste_styles(
|
pub fn on_paste_styles(&mut self, styles: JsValue) -> Result<(), JsError> {
|
||||||
&mut self,
|
|
||||||
#[wasm_bindgen(unchecked_param_type = "CellStyle[][]")] styles: JsValue,
|
|
||||||
) -> Result<(), JsError> {
|
|
||||||
let styles: &Vec<Vec<Style>> =
|
let styles: &Vec<Vec<Style>> =
|
||||||
&serde_wasm_bindgen::from_value(styles).map_err(|e| to_js_error(e.to_string()))?;
|
&serde_wasm_bindgen::from_value(styles).map_err(|e| to_js_error(e.to_string()))?;
|
||||||
self.model.on_paste_styles(styles).map_err(to_js_error)
|
self.model.on_paste_styles(styles).map_err(to_js_error)
|
||||||
@@ -394,10 +391,7 @@ impl Model {
|
|||||||
|
|
||||||
// I don't _think_ serializing to JsValue can't fail
|
// I don't _think_ serializing to JsValue can't fail
|
||||||
// FIXME: Remove this clippy directive
|
// FIXME: Remove this clippy directive
|
||||||
#[wasm_bindgen(
|
#[wasm_bindgen(js_name = "getWorksheetsProperties")]
|
||||||
js_name = "getWorksheetsProperties",
|
|
||||||
unchecked_return_type = "WorksheetProperties[]"
|
|
||||||
)]
|
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
pub fn get_worksheets_properties(&self) -> JsValue {
|
pub fn get_worksheets_properties(&self) -> JsValue {
|
||||||
serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap()
|
serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap()
|
||||||
@@ -416,7 +410,7 @@ impl Model {
|
|||||||
|
|
||||||
// I don't _think_ serializing to JsValue can't fail
|
// I don't _think_ serializing to JsValue can't fail
|
||||||
// FIXME: Remove this clippy directive
|
// FIXME: Remove this clippy directive
|
||||||
#[wasm_bindgen(js_name = "getSelectedView", unchecked_return_type = "SelectedView")]
|
#[wasm_bindgen(js_name = "getSelectedView")]
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
pub fn get_selected_view(&self) -> JsValue {
|
pub fn get_selected_view(&self) -> JsValue {
|
||||||
serde_wasm_bindgen::to_value(&self.model.get_selected_view()).unwrap()
|
serde_wasm_bindgen::to_value(&self.model.get_selected_view()).unwrap()
|
||||||
@@ -475,11 +469,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "autoFillRows")]
|
#[wasm_bindgen(js_name = "autoFillRows")]
|
||||||
pub fn auto_fill_rows(
|
pub fn auto_fill_rows(&mut self, source_area: JsValue, to_row: i32) -> Result<(), JsError> {
|
||||||
&mut self,
|
|
||||||
#[wasm_bindgen(unchecked_param_type = "Area")] source_area: JsValue,
|
|
||||||
to_row: i32,
|
|
||||||
) -> Result<(), JsError> {
|
|
||||||
let area: Area =
|
let area: Area =
|
||||||
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
|
serde_wasm_bindgen::from_value(source_area).map_err(|e| to_js_error(e.to_string()))?;
|
||||||
self.model
|
self.model
|
||||||
@@ -490,7 +480,7 @@ impl Model {
|
|||||||
#[wasm_bindgen(js_name = "autoFillColumns")]
|
#[wasm_bindgen(js_name = "autoFillColumns")]
|
||||||
pub fn auto_fill_columns(
|
pub fn auto_fill_columns(
|
||||||
&mut self,
|
&mut self,
|
||||||
#[wasm_bindgen(unchecked_param_type = "Area")] source_area: JsValue,
|
source_area: JsValue,
|
||||||
to_column: i32,
|
to_column: i32,
|
||||||
) -> Result<(), JsError> {
|
) -> Result<(), JsError> {
|
||||||
let area: Area =
|
let area: Area =
|
||||||
@@ -571,8 +561,8 @@ impl Model {
|
|||||||
#[wasm_bindgen(js_name = "setAreaWithBorder")]
|
#[wasm_bindgen(js_name = "setAreaWithBorder")]
|
||||||
pub fn set_area_with_border(
|
pub fn set_area_with_border(
|
||||||
&mut self,
|
&mut self,
|
||||||
#[wasm_bindgen(unchecked_param_type = "Area")] area: JsValue,
|
area: JsValue,
|
||||||
#[wasm_bindgen(unchecked_param_type = "BorderArea")] border_area: JsValue,
|
border_area: JsValue,
|
||||||
) -> Result<(), JsError> {
|
) -> Result<(), JsError> {
|
||||||
let range: Area =
|
let range: Area =
|
||||||
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
|
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
|
||||||
@@ -599,7 +589,7 @@ impl Model {
|
|||||||
self.model.set_name(name);
|
self.model.set_name(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "copyToClipboard", unchecked_return_type = "Clipboard")]
|
#[wasm_bindgen(js_name = "copyToClipboard")]
|
||||||
pub fn copy_to_clipboard(&self) -> Result<JsValue, JsError> {
|
pub fn copy_to_clipboard(&self) -> Result<JsValue, JsError> {
|
||||||
let data = self
|
let data = self
|
||||||
.model
|
.model
|
||||||
@@ -613,9 +603,8 @@ impl Model {
|
|||||||
pub fn paste_from_clipboard(
|
pub fn paste_from_clipboard(
|
||||||
&mut self,
|
&mut self,
|
||||||
source_sheet: u32,
|
source_sheet: u32,
|
||||||
#[wasm_bindgen(unchecked_param_type = "[number, number, number, number]")]
|
|
||||||
source_range: JsValue,
|
source_range: JsValue,
|
||||||
#[wasm_bindgen(unchecked_param_type = "ClipboardData")] clipboard: JsValue,
|
clipboard: JsValue,
|
||||||
is_cut: bool,
|
is_cut: bool,
|
||||||
) -> Result<(), JsError> {
|
) -> Result<(), JsError> {
|
||||||
let source_range: (i32, i32, i32, i32) =
|
let source_range: (i32, i32, i32, i32) =
|
||||||
@@ -628,11 +617,7 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = "pasteCsvText")]
|
#[wasm_bindgen(js_name = "pasteCsvText")]
|
||||||
pub fn paste_csv_string(
|
pub fn paste_csv_string(&mut self, area: JsValue, csv: &str) -> Result<(), JsError> {
|
||||||
&mut self,
|
|
||||||
#[wasm_bindgen(unchecked_param_type = "Area")] area: JsValue,
|
|
||||||
csv: &str,
|
|
||||||
) -> Result<(), JsError> {
|
|
||||||
let range: Area =
|
let range: Area =
|
||||||
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
|
serde_wasm_bindgen::from_value(area).map_err(|e| to_js_error(e.to_string()))?;
|
||||||
self.model
|
self.model
|
||||||
@@ -640,10 +625,7 @@ impl Model {
|
|||||||
.map_err(|e| to_js_error(e.to_string()))
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(
|
#[wasm_bindgen(js_name = "getDefinedNameList")]
|
||||||
js_name = "getDefinedNameList",
|
|
||||||
unchecked_return_type = "DefinedName[]"
|
|
||||||
)]
|
|
||||||
pub fn get_defined_name_list(&self) -> Result<JsValue, JsError> {
|
pub fn get_defined_name_list(&self) -> Result<JsValue, JsError> {
|
||||||
let data: Vec<DefinedName> = self
|
let data: Vec<DefinedName> = self
|
||||||
.model
|
.model
|
||||||
@@ -690,4 +672,18 @@ impl Model {
|
|||||||
.delete_defined_name(name, scope)
|
.delete_defined_name(name, scope)
|
||||||
.map_err(|e| to_js_error(e.to_string()))
|
.map_err(|e| to_js_error(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = "getSheetMarkup")]
|
||||||
|
pub fn get_sheet_markup(
|
||||||
|
&self,
|
||||||
|
sheet: u32,
|
||||||
|
start_row: i32,
|
||||||
|
start_column: i32,
|
||||||
|
end_row: i32,
|
||||||
|
end_column: i32,
|
||||||
|
) -> Result<String, JsError> {
|
||||||
|
self.model
|
||||||
|
.get_sheet_markup(sheet, start_row, start_column, end_row, end_column)
|
||||||
|
.map_err(to_js_error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -272,7 +272,6 @@ const ColorGridCol = styled.div`
|
|||||||
const ColorSwatch = styled.button<{ $color: string }>`
|
const ColorSwatch = styled.button<{ $color: string }>`
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
padding: 0px;
|
|
||||||
${({ $color }): string => {
|
${({ $color }): string => {
|
||||||
if ($color.toUpperCase() === "#FFFFFF") {
|
if ($color.toUpperCase() === "#FFFFFF") {
|
||||||
return `border: 1px solid ${theme.palette.grey["300"]};`;
|
return `border: 1px solid ${theme.palette.grey["300"]};`;
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ const Editor = (options: EditorOptions) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (type === cell.focus) {
|
if (type === cell.focus) {
|
||||||
textareaRef.current?.focus({ preventScroll: true });
|
textareaRef.current?.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
ArrowMiddleFromLine,
|
ArrowMiddleFromLine,
|
||||||
DecimalPlacesDecreaseIcon,
|
DecimalPlacesDecreaseIcon,
|
||||||
DecimalPlacesIncreaseIcon,
|
DecimalPlacesIncreaseIcon,
|
||||||
|
Markdown,
|
||||||
} from "../../icons";
|
} from "../../icons";
|
||||||
import { theme } from "../../theme";
|
import { theme } from "../../theme";
|
||||||
import BorderPicker from "../BorderPicker/BorderPicker";
|
import BorderPicker from "../BorderPicker/BorderPicker";
|
||||||
@@ -74,6 +75,7 @@ type ToolbarProperties = {
|
|||||||
onClearFormatting: () => void;
|
onClearFormatting: () => void;
|
||||||
onIncreaseFontSize: (delta: number) => void;
|
onIncreaseFontSize: (delta: number) => void;
|
||||||
onDownloadPNG: () => void;
|
onDownloadPNG: () => void;
|
||||||
|
onCopyMarkdown: () => void;
|
||||||
fillColor: string;
|
fillColor: string;
|
||||||
fontColor: string;
|
fontColor: string;
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
@@ -429,6 +431,17 @@ function Toolbar(properties: ToolbarProperties) {
|
|||||||
>
|
>
|
||||||
<ImageDown />
|
<ImageDown />
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
|
<StyledButton
|
||||||
|
type="button"
|
||||||
|
$pressed={false}
|
||||||
|
onClick={() => {
|
||||||
|
properties.onCopyMarkdown();
|
||||||
|
}}
|
||||||
|
disabled={!canEdit}
|
||||||
|
title={t("toolbar.selected_markdown")}
|
||||||
|
>
|
||||||
|
<Markdown />
|
||||||
|
</StyledButton>
|
||||||
|
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
color={properties.fontColor}
|
color={properties.fontColor}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
);
|
);
|
||||||
const focusWorkbook = useCallback(() => {
|
const focusWorkbook = useCallback(() => {
|
||||||
if (rootRef.current) {
|
if (rootRef.current) {
|
||||||
rootRef.current.focus({ preventScroll: true });
|
rootRef.current.focus();
|
||||||
// HACK: We need to select something inside the root for onCopy to work
|
// HACK: We need to select something inside the root for onCopy to work
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
if (selection) {
|
if (selection) {
|
||||||
@@ -558,6 +558,26 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
|
|||||||
onIncreaseFontSize={(delta: number) => {
|
onIncreaseFontSize={(delta: number) => {
|
||||||
onIncreaseFontSize(delta);
|
onIncreaseFontSize(delta);
|
||||||
}}
|
}}
|
||||||
|
onCopyMarkdown={async () => {
|
||||||
|
const {
|
||||||
|
sheet,
|
||||||
|
range: [rowStart, columnStart, rowEnd, columnEnd],
|
||||||
|
} = model.getSelectedView();
|
||||||
|
const row = Math.min(rowStart, rowEnd);
|
||||||
|
const column = Math.min(columnStart, columnEnd);
|
||||||
|
const width = Math.abs(columnEnd - columnStart) + 1;
|
||||||
|
const height = Math.abs(rowEnd - rowStart) + 1;
|
||||||
|
const markdown = model.getSheetMarkup(
|
||||||
|
sheet,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
);
|
||||||
|
// Copy to clipboard
|
||||||
|
// NB: This will not work in non secure contexts or in iframes (i.e storybook)
|
||||||
|
await navigator.clipboard.writeText(markdown);
|
||||||
|
}}
|
||||||
onDownloadPNG={() => {
|
onDownloadPNG={() => {
|
||||||
// creates a new canvas element in the visible part of the the selected area
|
// creates a new canvas element in the visible part of the the selected area
|
||||||
const worksheetCanvas = worksheetRef.current?.getCanvas();
|
const worksheetCanvas = worksheetRef.current?.getCanvas();
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import InsertColumnLeftIcon from "./insert-column-left.svg?react";
|
|||||||
import InsertColumnRightIcon from "./insert-column-right.svg?react";
|
import InsertColumnRightIcon from "./insert-column-right.svg?react";
|
||||||
import InsertRowAboveIcon from "./insert-row-above.svg?react";
|
import InsertRowAboveIcon from "./insert-row-above.svg?react";
|
||||||
import InsertRowBelow from "./insert-row-below.svg?react";
|
import InsertRowBelow from "./insert-row-below.svg?react";
|
||||||
|
import Markdown from "./markdown.svg?react";
|
||||||
|
|
||||||
import IronCalcIcon from "./ironcalc_icon.svg?react";
|
import IronCalcIcon from "./ironcalc_icon.svg?react";
|
||||||
import IronCalcLogo from "./orange+black.svg?react";
|
import IronCalcLogo from "./orange+black.svg?react";
|
||||||
@@ -48,4 +49,5 @@ export {
|
|||||||
IronCalcIcon,
|
IronCalcIcon,
|
||||||
IronCalcLogo,
|
IronCalcLogo,
|
||||||
Fx,
|
Fx,
|
||||||
|
Markdown,
|
||||||
};
|
};
|
||||||
|
|||||||
8
webapp/IronCalc/src/icons/markdown.svg
Normal file
8
webapp/IronCalc/src/icons/markdown.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path fill-rule="nonzero" d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm1 2v14h16V5H4zm3 10.5H5v-7h2l2 2 2-2h2v7h-2v-4l-2 2-2-2v4zm11-3h2l-3 3-3-3h2v-4h2v4z"/>
|
||||||
|
</g>
|
||||||
|
After Width: | Height: | Size: 477 B |
@@ -26,6 +26,7 @@
|
|||||||
"vertical_align_middle": " Align middle",
|
"vertical_align_middle": " Align middle",
|
||||||
"vertical_align_top": "Align top",
|
"vertical_align_top": "Align top",
|
||||||
"selected_png": "Export Selected area as PNG",
|
"selected_png": "Export Selected area as PNG",
|
||||||
|
"selected_markdown": "Export Selected area as Markdown",
|
||||||
"wrap_text": "Wrap text",
|
"wrap_text": "Wrap text",
|
||||||
"format_menu": {
|
"format_menu": {
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import "./App.css";
|
import "./App.css";
|
||||||
import styled from "@emotion/styled";
|
import styled from "@emotion/styled";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FileBar } from "./components/FileBar";
|
import { FileBar } from "./components/FileBar";
|
||||||
import LeftDrawer from "./components/LeftDrawer/LeftDrawer";
|
|
||||||
import {
|
import {
|
||||||
get_documentation_model,
|
get_documentation_model,
|
||||||
get_model,
|
get_model,
|
||||||
@@ -11,8 +10,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
createNewModel,
|
createNewModel,
|
||||||
deleteSelectedModel,
|
deleteSelectedModel,
|
||||||
getModelsMetadata,
|
|
||||||
getSelectedUuid,
|
|
||||||
loadModelFromStorageOrCreate,
|
loadModelFromStorageOrCreate,
|
||||||
saveModelToStorage,
|
saveModelToStorage,
|
||||||
saveSelectedModelInStorage,
|
saveSelectedModelInStorage,
|
||||||
@@ -24,14 +21,6 @@ import { IronCalc, IronCalcIcon, Model, init } from "@ironcalc/workbook";
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [model, setModel] = useState<Model | null>(null);
|
const [model, setModel] = useState<Model | null>(null);
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
|
||||||
const [modelsMetadata, setModelsMetadata] = useState(getModelsMetadata());
|
|
||||||
const [selectedUuid, setSelectedUuid] = useState(getSelectedUuid());
|
|
||||||
|
|
||||||
const refreshModelsData = useCallback(() => {
|
|
||||||
setModelsMetadata(getModelsMetadata());
|
|
||||||
setSelectedUuid(getSelectedUuid());
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function start() {
|
async function start() {
|
||||||
@@ -49,7 +38,6 @@ function App() {
|
|||||||
const importedModel = Model.from_bytes(model_bytes);
|
const importedModel = Model.from_bytes(model_bytes);
|
||||||
localStorage.removeItem("selected");
|
localStorage.removeItem("selected");
|
||||||
setModel(importedModel);
|
setModel(importedModel);
|
||||||
refreshModelsData();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert("Model not found, or failed to load");
|
alert("Model not found, or failed to load");
|
||||||
}
|
}
|
||||||
@@ -59,7 +47,6 @@ function App() {
|
|||||||
const importedModel = Model.from_bytes(model_bytes);
|
const importedModel = Model.from_bytes(model_bytes);
|
||||||
localStorage.removeItem("selected");
|
localStorage.removeItem("selected");
|
||||||
setModel(importedModel);
|
setModel(importedModel);
|
||||||
refreshModelsData();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert("Example file not found, or failed to load");
|
alert("Example file not found, or failed to load");
|
||||||
}
|
}
|
||||||
@@ -67,11 +54,10 @@ function App() {
|
|||||||
// try to load from local storage
|
// try to load from local storage
|
||||||
const newModel = loadModelFromStorageOrCreate();
|
const newModel = loadModelFromStorageOrCreate();
|
||||||
setModel(newModel);
|
setModel(newModel);
|
||||||
refreshModelsData();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
start();
|
start();
|
||||||
}, [refreshModelsData]);
|
}, []);
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return (
|
return (
|
||||||
@@ -93,80 +79,48 @@ function App() {
|
|||||||
// We could use context for model, but the problem is that it should initialized to null.
|
// We could use context for model, but the problem is that it should initialized to null.
|
||||||
// Passing the property down makes sure it is always defined.
|
// Passing the property down makes sure it is always defined.
|
||||||
|
|
||||||
// Handlers for model changes that also update our models state
|
|
||||||
const handleNewModel = () => {
|
|
||||||
const newModel = createNewModel();
|
|
||||||
setModel(newModel);
|
|
||||||
refreshModelsData();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSetModel = (uuid: string) => {
|
|
||||||
const newModel = selectModelFromStorage(uuid);
|
|
||||||
if (newModel) {
|
|
||||||
setModel(newModel);
|
|
||||||
refreshModelsData();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteModel = () => {
|
|
||||||
const newModel = deleteSelectedModel();
|
|
||||||
if (newModel) {
|
|
||||||
setModel(newModel);
|
|
||||||
refreshModelsData();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContainer>
|
<Wrapper>
|
||||||
<LeftDrawer
|
<FileBar
|
||||||
open={isDrawerOpen}
|
model={model}
|
||||||
onClose={() => setIsDrawerOpen(false)}
|
onModelUpload={async (arrayBuffer: ArrayBuffer, fileName: string) => {
|
||||||
newModel={handleNewModel}
|
const blob = await uploadFile(arrayBuffer, fileName);
|
||||||
setModel={handleSetModel}
|
|
||||||
models={modelsMetadata}
|
|
||||||
selectedUuid={selectedUuid}
|
|
||||||
setDeleteDialogOpen={() => {}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MainContent isDrawerOpen={isDrawerOpen}>
|
const bytes = new Uint8Array(await blob.arrayBuffer());
|
||||||
<FileBar
|
const newModel = Model.from_bytes(bytes);
|
||||||
model={model}
|
saveModelToStorage(newModel);
|
||||||
onModelUpload={async (arrayBuffer: ArrayBuffer, fileName: string) => {
|
|
||||||
const blob = await uploadFile(arrayBuffer, fileName);
|
setModel(newModel);
|
||||||
const bytes = new Uint8Array(await blob.arrayBuffer());
|
}}
|
||||||
const newModel = Model.from_bytes(bytes);
|
newModel={() => {
|
||||||
saveModelToStorage(newModel);
|
setModel(createNewModel());
|
||||||
|
}}
|
||||||
|
setModel={(uuid: string) => {
|
||||||
|
const newModel = selectModelFromStorage(uuid);
|
||||||
|
if (newModel) {
|
||||||
setModel(newModel);
|
setModel(newModel);
|
||||||
refreshModelsData();
|
}
|
||||||
}}
|
}}
|
||||||
newModel={handleNewModel}
|
onDelete={() => {
|
||||||
setModel={handleSetModel}
|
const newModel = deleteSelectedModel();
|
||||||
onDelete={handleDeleteModel}
|
if (newModel) {
|
||||||
isDrawerOpen={isDrawerOpen}
|
setModel(newModel);
|
||||||
setIsDrawerOpen={setIsDrawerOpen}
|
}
|
||||||
refreshModelsData={refreshModelsData}
|
}}
|
||||||
/>
|
/>
|
||||||
<IronCalc model={model} />
|
<IronCalc model={model} />
|
||||||
</MainContent>
|
</Wrapper>
|
||||||
</AppContainer>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppContainer = styled("div")`
|
const Wrapper = styled("div")`
|
||||||
display: flex;
|
margin: 0px;
|
||||||
|
padding: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MainContent = styled("div")<{ isDrawerOpen: boolean }>`
|
|
||||||
margin-left: ${({ isDrawerOpen }) => (isDrawerOpen ? "0px" : "-264px")};
|
|
||||||
transition: margin-left 0.3s ease;
|
|
||||||
width: ${({ isDrawerOpen }) =>
|
|
||||||
isDrawerOpen ? "calc(100% - 264px)" : "100%"};
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Loading = styled("div")`
|
const Loading = styled("div")`
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import styled from "@emotion/styled";
|
import styled from "@emotion/styled";
|
||||||
import type { Model } from "@ironcalc/workbook";
|
import type { Model } from "@ironcalc/workbook";
|
||||||
import { Button, IconButton } from "@mui/material";
|
import { IronCalcIcon, IronCalcLogo } from "@ironcalc/workbook";
|
||||||
import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
|
||||||
import { useLayoutEffect, useRef, useState } from "react";
|
import { useLayoutEffect, useRef, useState } from "react";
|
||||||
import { DesktopMenu, MobileMenu } from "./FileMenu";
|
import { FileMenu } from "./FileMenu";
|
||||||
import { ShareButton } from "./ShareButton";
|
import { ShareButton } from "./ShareButton";
|
||||||
import ShareWorkbookDialog from "./ShareWorkbookDialog";
|
import ShareWorkbookDialog from "./ShareWorkbookDialog";
|
||||||
import { WorkbookTitle } from "./WorkbookTitle";
|
import { WorkbookTitle } from "./WorkbookTitle";
|
||||||
@@ -30,15 +29,11 @@ export function FileBar(properties: {
|
|||||||
setModel: (key: string) => void;
|
setModel: (key: string) => void;
|
||||||
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
isDrawerOpen: boolean;
|
|
||||||
setIsDrawerOpen: (open: boolean) => void;
|
|
||||||
refreshModelsData: () => void; // Add this new prop
|
|
||||||
}) {
|
}) {
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const spacerRef = useRef<HTMLDivElement>(null);
|
const spacerRef = useRef<HTMLDivElement>(null);
|
||||||
const [maxTitleWidth, setMaxTitleWidth] = useState(0);
|
const [maxTitleWidth, setMaxTitleWidth] = useState(0);
|
||||||
const width = useWindowWidth();
|
const width = useWindowWidth();
|
||||||
const fileButtonRef = useRef<HTMLButtonElement>(null);
|
|
||||||
|
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We need to update the maxTitleWidth when the width changes
|
// biome-ignore lint/correctness/useExhaustiveDependencies: We need to update the maxTitleWidth when the width changes
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -49,54 +44,34 @@ export function FileBar(properties: {
|
|||||||
}
|
}
|
||||||
}, [width]);
|
}, [width]);
|
||||||
|
|
||||||
// Common handler functions for both menu types
|
|
||||||
const handleDownload = async () => {
|
|
||||||
const model = properties.model;
|
|
||||||
const bytes = model.toBytes();
|
|
||||||
const fileName = model.getName();
|
|
||||||
await downloadModel(bytes, fileName);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FileBarWrapper>
|
<FileBarWrapper>
|
||||||
<DrawerButton
|
<StyledDesktopLogo />
|
||||||
onClick={() => properties.setIsDrawerOpen(!properties.isDrawerOpen)}
|
<StyledIronCalcIcon />
|
||||||
disableRipple
|
<Divider />
|
||||||
|
<FileMenu
|
||||||
|
newModel={properties.newModel}
|
||||||
|
setModel={properties.setModel}
|
||||||
|
onModelUpload={properties.onModelUpload}
|
||||||
|
onDownload={async () => {
|
||||||
|
const model = properties.model;
|
||||||
|
const bytes = model.toBytes();
|
||||||
|
const fileName = model.getName();
|
||||||
|
await downloadModel(bytes, fileName);
|
||||||
|
}}
|
||||||
|
onDelete={properties.onDelete}
|
||||||
|
/>
|
||||||
|
<HelpButton
|
||||||
|
onClick={() => window.open("https://docs.ironcalc.com", "_blank")}
|
||||||
>
|
>
|
||||||
{properties.isDrawerOpen ? <PanelLeftClose /> : <PanelLeftOpen />}
|
Help
|
||||||
</DrawerButton>
|
</HelpButton>
|
||||||
<DesktopButtonsWrapper>
|
|
||||||
<DesktopMenu
|
|
||||||
newModel={properties.newModel}
|
|
||||||
setModel={properties.setModel}
|
|
||||||
onModelUpload={properties.onModelUpload}
|
|
||||||
onDownload={handleDownload}
|
|
||||||
onDelete={properties.onDelete}
|
|
||||||
/>
|
|
||||||
<FileBarButton
|
|
||||||
disableRipple
|
|
||||||
onClick={() => window.open("https://docs.ironcalc.com", "_blank")}
|
|
||||||
>
|
|
||||||
Help
|
|
||||||
</FileBarButton>
|
|
||||||
</DesktopButtonsWrapper>
|
|
||||||
<MobileButtonsWrapper>
|
|
||||||
<MobileMenu
|
|
||||||
newModel={properties.newModel}
|
|
||||||
setModel={properties.setModel}
|
|
||||||
onModelUpload={properties.onModelUpload}
|
|
||||||
onDownload={handleDownload}
|
|
||||||
onDelete={properties.onDelete}
|
|
||||||
/>
|
|
||||||
</MobileButtonsWrapper>
|
|
||||||
<Spacer ref={spacerRef} />
|
|
||||||
<WorkbookTitleWrapper>
|
<WorkbookTitleWrapper>
|
||||||
<WorkbookTitle
|
<WorkbookTitle
|
||||||
name={properties.model.getName()}
|
name={properties.model.getName()}
|
||||||
onNameChange={(name) => {
|
onNameChange={(name) => {
|
||||||
properties.model.setName(name);
|
properties.model.setName(name);
|
||||||
updateNameSelectedWorkbook(properties.model, name);
|
updateNameSelectedWorkbook(properties.model, name);
|
||||||
properties.refreshModelsData();
|
|
||||||
}}
|
}}
|
||||||
maxWidth={maxTitleWidth}
|
maxWidth={maxTitleWidth}
|
||||||
/>
|
/>
|
||||||
@@ -116,8 +91,12 @@ export function FileBar(properties: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We want the workbook title to be exactly an the center of the page,
|
||||||
|
// so we need an absolute position
|
||||||
const WorkbookTitleWrapper = styled("div")`
|
const WorkbookTitleWrapper = styled("div")`
|
||||||
position: relative;
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// The "Spacer" component occupies as much space as possible between the menu and the share button
|
// The "Spacer" component occupies as much space as possible between the menu and the share button
|
||||||
@@ -125,83 +104,51 @@ const Spacer = styled("div")`
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DrawerButton = styled(IconButton)`
|
const StyledDesktopLogo = styled(IronCalcLogo)`
|
||||||
margin-left: 8px;
|
width: 120px;
|
||||||
height: 32px;
|
margin-left: 12px;
|
||||||
width: 32px;
|
@media (max-width: 769px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIronCalcIcon = styled(IronCalcIcon)`
|
||||||
|
width: 36px;
|
||||||
|
margin-left: 10px;
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const HelpButton = styled("div")`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: Inter;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
svg {
|
cursor: pointer;
|
||||||
stroke-width: 2px;
|
|
||||||
stroke: #757575;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
}
|
}
|
||||||
&:active {
|
`;
|
||||||
background-color: #e0e0e0;
|
|
||||||
}
|
const Divider = styled("div")`
|
||||||
|
margin: 0px 8px 0px 16px;
|
||||||
|
height: 12px;
|
||||||
|
border-left: 1px solid #e0e0e0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// The container must be relative positioned so we can position the title absolutely
|
// The container must be relative positioned so we can position the title absolutely
|
||||||
const FileBarWrapper = styled("div")`
|
const FileBarWrapper = styled("div")`
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
min-height: 60px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 1px solid #e0e0e0;
|
border-bottom: 1px solid #e0e0e0;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
box-sizing: border-box;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DesktopButtonsWrapper = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
margin-left: 8px;
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MobileButtonsWrapper = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
@media (min-width: 601px) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FileBarButtonContainer = styled("div")`
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FileBarButton = styled(Button)`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 12px;
|
|
||||||
height: 32px;
|
|
||||||
width: auto;
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-weight: 400;
|
|
||||||
min-width: 0px;
|
|
||||||
text-transform: capitalize;
|
|
||||||
color: #333333;
|
|
||||||
&:hover {
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const DialogContainer = styled("div")`
|
const DialogContainer = styled("div")`
|
||||||
|
|||||||
@@ -1,165 +1,76 @@
|
|||||||
import styled from "@emotion/styled";
|
import styled from "@emotion/styled";
|
||||||
import { Button, IconButton, Menu, MenuItem, Modal } from "@mui/material";
|
import { Menu, MenuItem, Modal } from "@mui/material";
|
||||||
import {
|
import { Check, FileDown, FileUp, Plus, Trash2 } from "lucide-react";
|
||||||
ChevronRight,
|
|
||||||
EllipsisVertical,
|
|
||||||
FileDown,
|
|
||||||
FileUp,
|
|
||||||
Plus,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import DeleteWorkbookDialog from "./DeleteWorkbookDialog";
|
import DeleteWorkbookDialog from "./DeleteWorkbookDialog";
|
||||||
import UploadFileDialog from "./UploadFileDialog";
|
import UploadFileDialog from "./UploadFileDialog";
|
||||||
import { getModelsMetadata, getSelectedUuid } from "./storage";
|
import { getModelsMetadata, getSelectedUuid } from "./storage";
|
||||||
|
|
||||||
export function DesktopMenu(props: {
|
|
||||||
newModel: () => void;
|
|
||||||
setModel: (key: string) => void;
|
|
||||||
onDownload: () => void;
|
|
||||||
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
|
||||||
onDelete: () => void;
|
|
||||||
}) {
|
|
||||||
const [isFileMenuOpen, setFileMenuOpen] = useState(false);
|
|
||||||
const anchorElement = useRef<HTMLButtonElement>(
|
|
||||||
null as unknown as HTMLButtonElement,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FileBarButton
|
|
||||||
onClick={(): void => setFileMenuOpen(!isFileMenuOpen)}
|
|
||||||
ref={anchorElement}
|
|
||||||
disableRipple
|
|
||||||
isOpen={isFileMenuOpen}
|
|
||||||
>
|
|
||||||
File
|
|
||||||
</FileBarButton>
|
|
||||||
<FileMenu
|
|
||||||
newModel={props.newModel}
|
|
||||||
setModel={props.setModel}
|
|
||||||
onDownload={props.onDownload}
|
|
||||||
onModelUpload={props.onModelUpload}
|
|
||||||
onDelete={props.onDelete}
|
|
||||||
isFileMenuOpen={isFileMenuOpen}
|
|
||||||
setFileMenuOpen={setFileMenuOpen}
|
|
||||||
setMobileMenuOpen={() => {}}
|
|
||||||
anchorElement={anchorElement}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MobileMenu(props: {
|
|
||||||
newModel: () => void;
|
|
||||||
setModel: (key: string) => void;
|
|
||||||
onDownload: () => void;
|
|
||||||
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
|
||||||
onDelete: () => void;
|
|
||||||
}) {
|
|
||||||
const [isMobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
||||||
const [isFileMenuOpen, setFileMenuOpen] = useState(false);
|
|
||||||
const anchorElement = useRef<HTMLButtonElement>(
|
|
||||||
null as unknown as HTMLButtonElement,
|
|
||||||
);
|
|
||||||
const [fileMenuAnchorEl, setFileMenuAnchorEl] = useState<HTMLElement | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<MenuButton
|
|
||||||
onClick={(): void => setMobileMenuOpen(true)}
|
|
||||||
ref={anchorElement}
|
|
||||||
disableRipple
|
|
||||||
>
|
|
||||||
<EllipsisVertical />
|
|
||||||
</MenuButton>
|
|
||||||
<StyledMenu
|
|
||||||
open={isMobileMenuOpen}
|
|
||||||
onClose={(): void => setMobileMenuOpen(false)}
|
|
||||||
anchorEl={anchorElement.current}
|
|
||||||
>
|
|
||||||
<MenuItemWrapper
|
|
||||||
onClick={(event) => {
|
|
||||||
setFileMenuOpen(true);
|
|
||||||
setFileMenuAnchorEl(event.currentTarget);
|
|
||||||
}}
|
|
||||||
disableRipple
|
|
||||||
>
|
|
||||||
<MenuItemText>File</MenuItemText>
|
|
||||||
<ChevronRight />
|
|
||||||
</MenuItemWrapper>
|
|
||||||
<MenuDivider />
|
|
||||||
<MenuItemWrapper
|
|
||||||
onClick={() => {
|
|
||||||
window.open("https://docs.ironcalc.com", "_blank");
|
|
||||||
setMobileMenuOpen(false);
|
|
||||||
}}
|
|
||||||
disableRipple
|
|
||||||
>
|
|
||||||
<MenuItemText>Help</MenuItemText>
|
|
||||||
</MenuItemWrapper>
|
|
||||||
</StyledMenu>
|
|
||||||
<FileMenu
|
|
||||||
newModel={props.newModel}
|
|
||||||
setModel={props.setModel}
|
|
||||||
onDownload={props.onDownload}
|
|
||||||
onModelUpload={props.onModelUpload}
|
|
||||||
onDelete={props.onDelete}
|
|
||||||
isFileMenuOpen={isFileMenuOpen}
|
|
||||||
setFileMenuOpen={setFileMenuOpen}
|
|
||||||
setMobileMenuOpen={setMobileMenuOpen}
|
|
||||||
anchorElement={anchorElement}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FileMenu(props: {
|
export function FileMenu(props: {
|
||||||
newModel: () => void;
|
newModel: () => void;
|
||||||
setModel: (key: string) => void;
|
setModel: (key: string) => void;
|
||||||
onDownload: () => void;
|
onDownload: () => void;
|
||||||
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
onModelUpload: (blob: ArrayBuffer, fileName: string) => Promise<void>;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
isFileMenuOpen: boolean;
|
|
||||||
setFileMenuOpen: (open: boolean) => void;
|
|
||||||
setMobileMenuOpen: (open: boolean) => void;
|
|
||||||
anchorElement: React.RefObject<HTMLButtonElement>;
|
|
||||||
}) {
|
}) {
|
||||||
|
const [isMenuOpen, setMenuOpen] = useState(false);
|
||||||
const [isImportMenuOpen, setImportMenuOpen] = useState(false);
|
const [isImportMenuOpen, setImportMenuOpen] = useState(false);
|
||||||
|
const anchorElement = useRef<HTMLDivElement>(null);
|
||||||
const models = getModelsMetadata();
|
const models = getModelsMetadata();
|
||||||
|
const uuids = Object.keys(models);
|
||||||
const selectedUuid = getSelectedUuid();
|
const selectedUuid = getSelectedUuid();
|
||||||
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
const elements = [];
|
||||||
|
for (const uuid of uuids) {
|
||||||
|
elements.push(
|
||||||
|
<MenuItemWrapper
|
||||||
|
key={uuid}
|
||||||
|
onClick={() => {
|
||||||
|
props.setModel(uuid);
|
||||||
|
setMenuOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIndicator>
|
||||||
|
{uuid === selectedUuid ? <StyledCheck /> : ""}
|
||||||
|
</CheckIndicator>
|
||||||
|
<MenuItemText
|
||||||
|
style={{
|
||||||
|
maxWidth: "240px",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{models[uuid]}
|
||||||
|
</MenuItemText>
|
||||||
|
</MenuItemWrapper>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledMenu
|
<FileMenuWrapper
|
||||||
open={props.isFileMenuOpen}
|
onClick={(): void => setMenuOpen(true)}
|
||||||
onClose={(): void => props.setFileMenuOpen(false)}
|
ref={anchorElement}
|
||||||
anchorEl={props.anchorElement.current}
|
>
|
||||||
anchorOrigin={{
|
File
|
||||||
vertical: "bottom",
|
</FileMenuWrapper>
|
||||||
horizontal: "left",
|
<Menu
|
||||||
}}
|
open={isMenuOpen}
|
||||||
transformOrigin={{
|
onClose={(): void => setMenuOpen(false)}
|
||||||
vertical: "top",
|
anchorEl={anchorElement.current}
|
||||||
horizontal: "left",
|
sx={{
|
||||||
}}
|
"& .MuiPaper-root": { borderRadius: "8px", padding: "4px 0px" },
|
||||||
// To prevent closing parent menu when interacting with submenu
|
"& .MuiList-root": { padding: "0" },
|
||||||
onMouseLeave={() => {
|
|
||||||
if (!isImportMenuOpen && !isDeleteDialogOpen) {
|
|
||||||
props.setFileMenuOpen(false);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
// anchorOrigin={properties.anchorOrigin}
|
||||||
>
|
>
|
||||||
<MenuItemWrapper
|
<MenuItemWrapper
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.newModel();
|
props.newModel();
|
||||||
props.setFileMenuOpen(false);
|
setMenuOpen(false);
|
||||||
props.setMobileMenuOpen(false);
|
|
||||||
}}
|
}}
|
||||||
disableRipple
|
|
||||||
>
|
>
|
||||||
<StyledPlus />
|
<StyledPlus />
|
||||||
<MenuItemText>New</MenuItemText>
|
<MenuItemText>New</MenuItemText>
|
||||||
@@ -167,37 +78,30 @@ export function FileMenu(props: {
|
|||||||
<MenuItemWrapper
|
<MenuItemWrapper
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setImportMenuOpen(true);
|
setImportMenuOpen(true);
|
||||||
props.setFileMenuOpen(false);
|
setMenuOpen(false);
|
||||||
props.setMobileMenuOpen(false);
|
|
||||||
}}
|
}}
|
||||||
disableRipple
|
|
||||||
>
|
>
|
||||||
<StyledFileUp />
|
<StyledFileUp />
|
||||||
<MenuItemText>Import</MenuItemText>
|
<MenuItemText>Import</MenuItemText>
|
||||||
</MenuItemWrapper>
|
</MenuItemWrapper>
|
||||||
<MenuItemWrapper
|
<MenuItemWrapper>
|
||||||
onClick={() => {
|
|
||||||
props.onDownload();
|
|
||||||
props.setMobileMenuOpen(false);
|
|
||||||
}}
|
|
||||||
disableRipple
|
|
||||||
>
|
|
||||||
<StyledFileDown />
|
<StyledFileDown />
|
||||||
<MenuItemText>Download (.xlsx)</MenuItemText>
|
<MenuItemText onClick={props.onDownload}>
|
||||||
|
Download (.xlsx)
|
||||||
|
</MenuItemText>
|
||||||
</MenuItemWrapper>
|
</MenuItemWrapper>
|
||||||
<MenuDivider />
|
|
||||||
<MenuItemWrapper
|
<MenuItemWrapper
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDeleteDialogOpen(true);
|
setDeleteDialogOpen(true);
|
||||||
props.setFileMenuOpen(false);
|
setMenuOpen(false);
|
||||||
props.setMobileMenuOpen(false);
|
|
||||||
}}
|
}}
|
||||||
disableRipple
|
|
||||||
>
|
>
|
||||||
<StyledTrash />
|
<StyledTrash />
|
||||||
<MenuItemText>Delete workbook</MenuItemText>
|
<MenuItemText>Delete workbook</MenuItemText>
|
||||||
</MenuItemWrapper>
|
</MenuItemWrapper>
|
||||||
</StyledMenu>
|
<MenuDivider />
|
||||||
|
{elements}
|
||||||
|
</Menu>
|
||||||
<Modal
|
<Modal
|
||||||
open={isImportMenuOpen}
|
open={isImportMenuOpen}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
@@ -229,46 +133,6 @@ export function FileMenu(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuButton = styled(IconButton)`
|
|
||||||
height: 32px;
|
|
||||||
width: 32px;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
svg {
|
|
||||||
stroke-width: 2px;
|
|
||||||
stroke: #757575;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const FileBarButton = styled(Button)<{ isOpen: boolean }>`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 12px;
|
|
||||||
height: 32px;
|
|
||||||
width: auto;
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-weight: 400;
|
|
||||||
min-width: 0px;
|
|
||||||
text-transform: capitalize;
|
|
||||||
color: #333333;
|
|
||||||
background-color: ${({ isOpen }) => (isOpen ? "#f2f2f2" : "none")};
|
|
||||||
&:hover {
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledPlus = styled(Plus)`
|
const StyledPlus = styled(Plus)`
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@@ -297,6 +161,13 @@ const StyledTrash = styled(Trash2)`
|
|||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledCheck = styled(Check)`
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: #333333;
|
||||||
|
padding-right: 10px;
|
||||||
|
`;
|
||||||
|
|
||||||
const MenuDivider = styled("div")`
|
const MenuDivider = styled("div")`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@@ -308,7 +179,6 @@ const MenuDivider = styled("div")`
|
|||||||
const MenuItemText = styled("div")`
|
const MenuItemText = styled("div")`
|
||||||
color: #000;
|
color: #000;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
flex-grow: 1;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const MenuItemWrapper = styled(MenuItem)`
|
const MenuItemWrapper = styled(MenuItem)`
|
||||||
@@ -321,19 +191,23 @@ const MenuItemWrapper = styled(MenuItem)`
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
min-height: 32px;
|
`;
|
||||||
svg {
|
|
||||||
width: 16px;
|
const FileMenuWrapper = styled("div")`
|
||||||
height: 16px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: Inter;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: #f2f2f2;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledMenu = styled(Menu)`
|
const CheckIndicator = styled("span")`
|
||||||
.MuiPaper-root {
|
display: flex;
|
||||||
border-radius: 8px;
|
justify-content: center;
|
||||||
padding: 4px 0px;
|
min-width: 26px;
|
||||||
},
|
|
||||||
.MuiList-root {
|
|
||||||
padding: 0;
|
|
||||||
},
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,384 +0,0 @@
|
|||||||
import styled from "@emotion/styled";
|
|
||||||
import { IronCalcLogo } from "@ironcalc/workbook";
|
|
||||||
import { Avatar, Drawer, IconButton, Menu, MenuItem } from "@mui/material";
|
|
||||||
import {
|
|
||||||
EllipsisVertical,
|
|
||||||
FileDown,
|
|
||||||
FileSpreadsheet,
|
|
||||||
Plus,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import type React from "react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import UserMenu from "../UserMenu";
|
|
||||||
|
|
||||||
interface LeftDrawerProps {
|
|
||||||
open: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
newModel: () => void;
|
|
||||||
setModel: (key: string) => void;
|
|
||||||
models: { [key: string]: string };
|
|
||||||
selectedUuid: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LeftDrawer: React.FC<LeftDrawerProps> = ({
|
|
||||||
open,
|
|
||||||
onClose,
|
|
||||||
newModel,
|
|
||||||
setModel,
|
|
||||||
models,
|
|
||||||
selectedUuid,
|
|
||||||
}) => {
|
|
||||||
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
|
||||||
const [selectedWorkbookUuid, setSelectedWorkbookUuid] = useState<
|
|
||||||
string | null
|
|
||||||
>(null);
|
|
||||||
const [userMenuAnchorEl, setUserMenuAnchorEl] = useState<null | HTMLElement>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMenuOpen = (
|
|
||||||
event: React.MouseEvent<HTMLButtonElement>,
|
|
||||||
uuid: string,
|
|
||||||
) => {
|
|
||||||
console.log("Menu open", uuid);
|
|
||||||
event.stopPropagation();
|
|
||||||
setSelectedWorkbookUuid(uuid);
|
|
||||||
setMenuAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMenuClose = () => {
|
|
||||||
setMenuAnchorEl(null);
|
|
||||||
setSelectedWorkbookUuid(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUserMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
|
||||||
setUserMenuAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUserMenuClose = () => {
|
|
||||||
setUserMenuAnchorEl(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const elements = Object.keys(models)
|
|
||||||
.reverse()
|
|
||||||
.map((uuid) => {
|
|
||||||
const isMenuOpen = menuAnchorEl !== null && selectedWorkbookUuid === uuid;
|
|
||||||
return (
|
|
||||||
<WorkbookListItem
|
|
||||||
key={uuid}
|
|
||||||
onClick={() => {
|
|
||||||
setModel(uuid);
|
|
||||||
}}
|
|
||||||
selected={uuid === selectedUuid}
|
|
||||||
disableRipple
|
|
||||||
>
|
|
||||||
<StorageIndicator>
|
|
||||||
<FileSpreadsheet />
|
|
||||||
</StorageIndicator>
|
|
||||||
<WorkbookListText>{models[uuid]}</WorkbookListText>
|
|
||||||
<EllipsisButton
|
|
||||||
onClick={(e) => handleMenuOpen(e, uuid)}
|
|
||||||
disableRipple
|
|
||||||
isOpen={isMenuOpen}
|
|
||||||
>
|
|
||||||
<EllipsisVertical />
|
|
||||||
</EllipsisButton>
|
|
||||||
|
|
||||||
<StyledMenu
|
|
||||||
anchorEl={menuAnchorEl}
|
|
||||||
open={Boolean(menuAnchorEl)}
|
|
||||||
onClose={handleMenuClose}
|
|
||||||
MenuListProps={{
|
|
||||||
dense: true,
|
|
||||||
}}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: "bottom",
|
|
||||||
horizontal: "right",
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "right",
|
|
||||||
}}
|
|
||||||
disablePortal
|
|
||||||
>
|
|
||||||
<MenuItemWrapper
|
|
||||||
onClick={() => {
|
|
||||||
handleMenuClose();
|
|
||||||
}}
|
|
||||||
disableRipple
|
|
||||||
>
|
|
||||||
<FileDown />
|
|
||||||
Download (.xlsx)
|
|
||||||
</MenuItemWrapper>
|
|
||||||
<MenuDivider />
|
|
||||||
<MenuItemWrapper
|
|
||||||
selected={false}
|
|
||||||
onClick={() => {
|
|
||||||
handleMenuClose();
|
|
||||||
}}
|
|
||||||
disableRipple
|
|
||||||
>
|
|
||||||
<Trash2 size={16} />
|
|
||||||
Delete workbook
|
|
||||||
</MenuItemWrapper>
|
|
||||||
</StyledMenu>
|
|
||||||
</WorkbookListItem>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DrawerWrapper
|
|
||||||
variant="persistent"
|
|
||||||
anchor="left"
|
|
||||||
open={open}
|
|
||||||
onClose={onClose}
|
|
||||||
>
|
|
||||||
<DrawerHeader>
|
|
||||||
<StyledDesktopLogo />
|
|
||||||
<AddButton
|
|
||||||
onClick={() => {
|
|
||||||
newModel();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusIcon />
|
|
||||||
</AddButton>
|
|
||||||
</DrawerHeader>
|
|
||||||
<DrawerContent>
|
|
||||||
<DrawerContentTitle>Your workbooks</DrawerContentTitle>
|
|
||||||
{elements}
|
|
||||||
</DrawerContent>
|
|
||||||
<DrawerFooter>
|
|
||||||
<UserWrapper
|
|
||||||
disableRipple
|
|
||||||
onClick={handleUserMenuOpen}
|
|
||||||
selected={Boolean(userMenuAnchorEl)}
|
|
||||||
>
|
|
||||||
<StyledAvatar
|
|
||||||
alt="Nikola Tesla"
|
|
||||||
src="/path/to/avatar.jpg"
|
|
||||||
sx={{ bgcolor: "#f2994a", width: 24, height: 24 }}
|
|
||||||
/>
|
|
||||||
<Username>Nikola Tesla</Username>
|
|
||||||
</UserWrapper>
|
|
||||||
<UserMenu
|
|
||||||
anchorEl={userMenuAnchorEl}
|
|
||||||
onClose={handleUserMenuClose}
|
|
||||||
onPreferences={() => {
|
|
||||||
console.log("Preferences clicked");
|
|
||||||
handleUserMenuClose();
|
|
||||||
}}
|
|
||||||
onLogout={() => {
|
|
||||||
console.log("Logout clicked");
|
|
||||||
handleUserMenuClose();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DrawerFooter>
|
|
||||||
</DrawerWrapper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const DrawerWrapper = styled(Drawer)`
|
|
||||||
width: 264px;
|
|
||||||
height: 100%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
font-family: "Inter", sans-serif;
|
|
||||||
|
|
||||||
.MuiDrawer-paper {
|
|
||||||
width: 264px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
overflow: hidden;
|
|
||||||
border-right: 1px solid #e0e0e0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DrawerHeader = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 8px 12px 16px;
|
|
||||||
justify-content: space-between;
|
|
||||||
max-height: 60px;
|
|
||||||
min-height: 60px;
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledDesktopLogo = styled(IronCalcLogo)`
|
|
||||||
width: 120px;
|
|
||||||
height: 28px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const AddButton = styled(IconButton)`
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 8px;
|
|
||||||
height: 32px;
|
|
||||||
width: 32px;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-left: 10px;
|
|
||||||
color: #333333;
|
|
||||||
stroke-width: 2px;
|
|
||||||
&:hover {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const PlusIcon = styled(Plus)`
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DrawerContent = styled("div")`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 16px 12px;
|
|
||||||
height: 100%;
|
|
||||||
overflow: scroll;
|
|
||||||
font-size: 12px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DrawerContentTitle = styled("div")`
|
|
||||||
font-weight: 600;
|
|
||||||
color: #9e9e9e;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
padding: 0px 8px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StorageIndicator = styled("div")`
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
svg {
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
stroke: #9e9e9e;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const EllipsisButton = styled(IconButton)<{ isOpen: boolean }>`
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 4px;
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #333333;
|
|
||||||
stroke-width: 2px;
|
|
||||||
background-color: ${({ isOpen }) => (isOpen ? "#E0E0E0" : "none")};
|
|
||||||
opacity: ${({ isOpen }) => (isOpen ? "1" : "0.5")};
|
|
||||||
transition: opacity 0.3s, background-color 0.3s;
|
|
||||||
&:hover {
|
|
||||||
background: none;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background: #bdbdbd;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const WorkbookListItem = styled(MenuItem)<{ selected: boolean }>`
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: flex-start;
|
|
||||||
font-size: 14px;
|
|
||||||
width: 100%;
|
|
||||||
min-width: 172px;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 8px 4px 8px 8px;
|
|
||||||
height: 32px;
|
|
||||||
min-height: 32px;
|
|
||||||
transition: gap 0.5s;
|
|
||||||
background-color: ${({ selected }) =>
|
|
||||||
selected ? "#e0e0e0 !important" : "transparent"};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const WorkbookListText = styled("div")`
|
|
||||||
color: #000;
|
|
||||||
font-size: 12px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 240px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledMenu = styled(Menu)`
|
|
||||||
.MuiPaper-root {
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 4px 0px;
|
|
||||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.01);
|
|
||||||
},
|
|
||||||
.MuiList-root {
|
|
||||||
padding: 0;
|
|
||||||
},
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MenuDivider = styled("div")`
|
|
||||||
width: 100%;
|
|
||||||
margin: auto;
|
|
||||||
margin-top: 4px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
border-top: 1px solid #eeeeee;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MenuItemWrapper = styled(MenuItem)`
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
font-size: 12px;
|
|
||||||
width: calc(100% - 8px);
|
|
||||||
min-width: 140px;
|
|
||||||
margin: 0px 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 8px;
|
|
||||||
height: 32px;
|
|
||||||
gap: 8px;
|
|
||||||
svg {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const DrawerFooter = styled("div")`
|
|
||||||
display: none;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px;
|
|
||||||
justify-content: space-between;
|
|
||||||
max-height: 60px;
|
|
||||||
height: 60px;
|
|
||||||
border-top: 1px solid #e0e0e0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const UserWrapper = styled(MenuItem)<{ selected: boolean }>`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: 8px;
|
|
||||||
border-radius: 8px;
|
|
||||||
max-width: 100%;
|
|
||||||
background-color: ${({ selected }) =>
|
|
||||||
selected ? "#e0e0e0 !important" : "transparent"};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledAvatar = styled(Avatar)`
|
|
||||||
font-size: 14px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Username = styled("div")`
|
|
||||||
font-size: 12px;
|
|
||||||
flex-grow: 1;
|
|
||||||
max-width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default LeftDrawer;
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import styled from "@emotion/styled";
|
|
||||||
import { Menu, MenuItem } from "@mui/material";
|
|
||||||
import { LogOut, Settings } from "lucide-react";
|
|
||||||
|
|
||||||
interface UserMenuProps {
|
|
||||||
anchorEl: null | HTMLElement;
|
|
||||||
onClose: () => void;
|
|
||||||
onPreferences: () => void;
|
|
||||||
onLogout: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserMenu: React.FC<UserMenuProps> = ({
|
|
||||||
anchorEl,
|
|
||||||
onClose,
|
|
||||||
onPreferences,
|
|
||||||
onLogout,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<StyledMenu
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
open={Boolean(anchorEl)}
|
|
||||||
onClose={onClose}
|
|
||||||
MenuListProps={{
|
|
||||||
dense: true,
|
|
||||||
}}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "left",
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: "bottom",
|
|
||||||
horizontal: "left",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItemWrapper onClick={onPreferences} sx={{ gap: 1, fontSize: 14 }}>
|
|
||||||
<Settings size={16} />
|
|
||||||
<MenuItemText>Preferences</MenuItemText>
|
|
||||||
</MenuItemWrapper>
|
|
||||||
<MenuDivider />
|
|
||||||
<MenuItemWrapper onClick={onLogout} sx={{ gap: 1, fontSize: 14 }}>
|
|
||||||
<LogOut size={16} />
|
|
||||||
<MenuItemText>Log out</MenuItemText>
|
|
||||||
</MenuItemWrapper>
|
|
||||||
</StyledMenu>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledMenu = styled(Menu)`
|
|
||||||
& .MuiPaper-root {
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 4px 0px;
|
|
||||||
margin-top: -4px;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
& .MuiList-root {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MenuItemText = styled("div")`
|
|
||||||
color: #000;
|
|
||||||
font-size: 12px;
|
|
||||||
flex-grow: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MenuItemWrapper = styled(MenuItem)`
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
font-size: 14px;
|
|
||||||
width: calc(100% - 8px);
|
|
||||||
min-width: 172px;
|
|
||||||
margin: 0px 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 8px;
|
|
||||||
height: 32px;
|
|
||||||
svg {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MenuDivider = styled("div")`
|
|
||||||
width: 100%;
|
|
||||||
margin: auto;
|
|
||||||
margin-top: 4px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
border-top: 1px solid #eeeeee;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default UserMenu;
|
|
||||||
@@ -72,10 +72,10 @@ export function WorkbookTitle(properties: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled("div")`
|
const Container = styled("div")`
|
||||||
text-align: left;
|
text-align: center;
|
||||||
padding: 6px 4px;
|
padding: 8px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
font-family: Inter;
|
font-family: Inter;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ const TitleInput = styled("input")`
|
|||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
}
|
}
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 1px solid grey;
|
border: 1px solid grey;
|
||||||
}
|
}
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
for row in 1..100 {
|
for row in 1..100 {
|
||||||
for column in 1..100 {
|
for column in 1..100 {
|
||||||
let value = row * column;
|
let value = row * column;
|
||||||
model.set_user_input(0, row, column, format!("{value}"))?;
|
model.set_user_input(0, row, column, format!("{}", value))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Adds a new sheet
|
// Adds a new sheet
|
||||||
model.add_sheet("Calculation")?;
|
model.add_sheet("Calculation")?;
|
||||||
// column 100 is CV
|
// column 100 is CV
|
||||||
let last_column = number_to_column(100).ok_or("Invalid column number")?;
|
let last_column = number_to_column(100).ok_or("Invalid column number")?;
|
||||||
let formula = format!("=SUM(Sheet1!A1:{last_column}100)");
|
let formula = format!("=SUM(Sheet1!A1:{}100)", last_column);
|
||||||
model.set_user_input(1, 1, 1, formula)?;
|
model.set_user_input(1, 1, 1, formula)?;
|
||||||
|
|
||||||
// evaluates
|
// evaluates
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ fn main() {
|
|||||||
let file_name = &args[1];
|
let file_name = &args[1];
|
||||||
println!("Testing file: {file_name}");
|
println!("Testing file: {file_name}");
|
||||||
if let Err(message) = test_file(file_name) {
|
if let Err(message) = test_file(file_name) {
|
||||||
println!("{message}");
|
println!("{}", message);
|
||||||
panic!("Model was evaluated inconsistently with XLSX data.")
|
panic!("Model was evaluated inconsistently with XLSX data.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ pub(crate) fn compare_models(m1: &Model, m2: &Model) -> Result<(), String> {
|
|||||||
diff.reason
|
diff.reason
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(format!("Models are different: {message}"))
|
Err(format!("Models are different: {}", message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(r) => Err(format!("Models are different: {}", r.message)),
|
Err(r) => Err(format!("Models are different: {}", r.message)),
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ pub(crate) fn get_app_xml(_: &Workbook) -> String {
|
|||||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
|
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
|
||||||
<Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties\" \
|
<Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties\" \
|
||||||
xmlns:vt=\"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes\">\
|
xmlns:vt=\"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes\">\
|
||||||
<Application>{APPLICATION}</Application>\
|
<Application>{}</Application>\
|
||||||
<AppVersion>{APP_VERSION}</AppVersion>\
|
<AppVersion>{}</AppVersion>\
|
||||||
</Properties>"
|
</Properties>",
|
||||||
|
APPLICATION, APP_VERSION
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +38,12 @@ pub(crate) fn get_core_xml(workbook: &Workbook, milliseconds: i64) -> Result<Str
|
|||||||
let seconds = milliseconds / 1000;
|
let seconds = milliseconds / 1000;
|
||||||
let dt = match DateTime::from_timestamp(seconds, 0) {
|
let dt = match DateTime::from_timestamp(seconds, 0) {
|
||||||
Some(s) => s,
|
Some(s) => s,
|
||||||
None => return Err(XlsxError::Xml(format!("Invalid timestamp: {milliseconds}"))),
|
None => {
|
||||||
|
return Err(XlsxError::Xml(format!(
|
||||||
|
"Invalid timestamp: {}",
|
||||||
|
milliseconds
|
||||||
|
)))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let last_modified = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
let last_modified = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
@@ -48,15 +54,16 @@ pub(crate) fn get_core_xml(workbook: &Workbook, milliseconds: i64) -> Result<Str
|
|||||||
xmlns:dcmitype=\"http://purl.org/dc/dcmitype/\" \
|
xmlns:dcmitype=\"http://purl.org/dc/dcmitype/\" \
|
||||||
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"> \
|
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"> \
|
||||||
<dc:title></dc:title><dc:subject></dc:subject>\
|
<dc:title></dc:title><dc:subject></dc:subject>\
|
||||||
<dc:creator>{creator}</dc:creator>\
|
<dc:creator>{}</dc:creator>\
|
||||||
<cp:keywords></cp:keywords>\
|
<cp:keywords></cp:keywords>\
|
||||||
<dc:description></dc:description>\
|
<dc:description></dc:description>\
|
||||||
<cp:lastModifiedBy>{last_modified_by}</cp:lastModifiedBy>\
|
<cp:lastModifiedBy>{}</cp:lastModifiedBy>\
|
||||||
<cp:revision></cp:revision>\
|
<cp:revision></cp:revision>\
|
||||||
<dcterms:created xsi:type=\"dcterms:W3CDTF\">{created}</dcterms:created>\
|
<dcterms:created xsi:type=\"dcterms:W3CDTF\">{}</dcterms:created>\
|
||||||
<dcterms:modified xsi:type=\"dcterms:W3CDTF\">{last_modified}</dcterms:modified>\
|
<dcterms:modified xsi:type=\"dcterms:W3CDTF\">{}</dcterms:modified>\
|
||||||
<cp:category></cp:category>\
|
<cp:category></cp:category>\
|
||||||
<cp:contentStatus></cp:contentStatus>\
|
<cp:contentStatus></cp:contentStatus>\
|
||||||
</cp:coreProperties>"
|
</cp:coreProperties>",
|
||||||
|
creator, last_modified_by, created, last_modified
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ fn get_content_types_xml(workbook: &Workbook) -> String {
|
|||||||
pub fn save_to_xlsx(model: &Model, file_name: &str) -> Result<(), XlsxError> {
|
pub fn save_to_xlsx(model: &Model, file_name: &str) -> Result<(), XlsxError> {
|
||||||
let file_path = std::path::Path::new(&file_name);
|
let file_path = std::path::Path::new(&file_name);
|
||||||
if file_path.exists() {
|
if file_path.exists() {
|
||||||
return Err(XlsxError::IO(format!("file {file_name} already exists")));
|
return Err(XlsxError::IO(format!("file {} already exists", file_name)));
|
||||||
}
|
}
|
||||||
let file = fs::File::create(file_path).unwrap();
|
let file = fs::File::create(file_path).unwrap();
|
||||||
let writer = BufWriter::new(file);
|
let writer = BufWriter::new(file);
|
||||||
@@ -140,7 +140,7 @@ pub fn save_xlsx_to_writer<W: Write + Seek>(model: &Model, writer: W) -> Result<
|
|||||||
pub fn save_to_icalc(model: &Model, file_name: &str) -> Result<(), XlsxError> {
|
pub fn save_to_icalc(model: &Model, file_name: &str) -> Result<(), XlsxError> {
|
||||||
let file_path = std::path::Path::new(&file_name);
|
let file_path = std::path::Path::new(&file_name);
|
||||||
if file_path.exists() {
|
if file_path.exists() {
|
||||||
return Err(XlsxError::IO(format!("file {file_name} already exists")));
|
return Err(XlsxError::IO(format!("file {} already exists", file_name)));
|
||||||
}
|
}
|
||||||
let s = bitcode::encode(&model.workbook);
|
let s = bitcode::encode(&model.workbook);
|
||||||
let mut file = fs::File::create(file_path)?;
|
let mut file = fs::File::create(file_path)?;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ fn get_cell_style_attribute(s: i32) -> String {
|
|||||||
if s == 0 {
|
if s == 0 {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!(" s=\"{s}\"")
|
format!(" s=\"{}\"", s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,8 +148,8 @@ pub fn load_from_xlsx(file_name: &str, locale: &str, tz: &str) -> Result<Model,
|
|||||||
/// Loads a [Model] from an `ic` file (a file in the IronCalc internal representation)
|
/// Loads a [Model] from an `ic` file (a file in the IronCalc internal representation)
|
||||||
pub fn load_from_icalc(file_name: &str) -> Result<Model, XlsxError> {
|
pub fn load_from_icalc(file_name: &str) -> Result<Model, XlsxError> {
|
||||||
let contents = fs::read(file_name)
|
let contents = fs::read(file_name)
|
||||||
.map_err(|e| XlsxError::IO(format!("Could not extract workbook name: {e}")))?;
|
.map_err(|e| XlsxError::IO(format!("Could not extract workbook name: {}", e)))?;
|
||||||
let workbook: Workbook = bitcode::decode(&contents)
|
let workbook: Workbook = bitcode::decode(&contents)
|
||||||
.map_err(|e| XlsxError::IO(format!("Failed to decode file: {e}")))?;
|
.map_err(|e| XlsxError::IO(format!("Failed to decode file: {}", e)))?;
|
||||||
Model::from_workbook(workbook).map_err(XlsxError::Workbook)
|
Model::from_workbook(workbook).map_err(XlsxError::Workbook)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,8 +93,7 @@ pub(super) fn load_styles<R: Read + std::io::Seek>(
|
|||||||
let mut b = false;
|
let mut b = false;
|
||||||
let mut i = false;
|
let mut i = false;
|
||||||
let mut strike = false;
|
let mut strike = false;
|
||||||
// Default color is black
|
let mut color = Some("FFFFFF00".to_string());
|
||||||
let mut color = Some("#000000".to_string());
|
|
||||||
let mut family = 2;
|
let mut family = 2;
|
||||||
let mut scheme = FontScheme::default();
|
let mut scheme = FontScheme::default();
|
||||||
for feature in font.children() {
|
for feature in font.children() {
|
||||||
@@ -142,7 +141,7 @@ pub(super) fn load_styles<R: Read + std::io::Seek>(
|
|||||||
}
|
}
|
||||||
"charset" => {}
|
"charset" => {}
|
||||||
_ => {
|
_ => {
|
||||||
println!("Unexpected feature {feature:?}");
|
println!("Unexpected feature {:?}", feature);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ where
|
|||||||
{
|
{
|
||||||
let attr_name = attr_name.into();
|
let attr_name = attr_name.into();
|
||||||
node.attribute(attr_name)
|
node.attribute(attr_name)
|
||||||
.ok_or_else(|| XlsxError::Xml(format!("Missing \"{attr_name:?}\" XML attribute")))
|
.ok_or_else(|| XlsxError::Xml(format!("Missing \"{:?}\" XML attribute", attr_name)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn get_value_or_default(node: &Node, tag_name: &str, default: &str) -> String {
|
pub(super) fn get_value_or_default(node: &Node, tag_name: &str, default: &str) -> String {
|
||||||
@@ -64,7 +64,7 @@ pub(super) fn get_color(node: Node) -> Result<Option<String>, XlsxError> {
|
|||||||
// A boolean value indicating the color is automatic and system color dependent.
|
// A boolean value indicating the color is automatic and system color dependent.
|
||||||
Ok(None)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
println!("Unexpected color node {node:?}");
|
println!("Unexpected color node {:?}", node);
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ pub(super) fn load_workbook<R: Read + std::io::Seek>(
|
|||||||
Some("visible") | None => SheetState::Visible,
|
Some("visible") | None => SheetState::Visible,
|
||||||
Some("hidden") => SheetState::Hidden,
|
Some("hidden") => SheetState::Hidden,
|
||||||
Some("veryHidden") => SheetState::VeryHidden,
|
Some("veryHidden") => SheetState::VeryHidden,
|
||||||
Some(state) => return Err(XlsxError::Xml(format!("Unknown sheet state: {state}"))),
|
Some(state) => return Err(XlsxError::Xml(format!("Unknown sheet state: {}", state))),
|
||||||
};
|
};
|
||||||
sheets.push(Sheet {
|
sheets.push(Sheet {
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ fn parse_cell_reference(cell: &str) -> Result<(i32, i32), String> {
|
|||||||
if let Some(r) = parse_reference_a1(cell) {
|
if let Some(r) = parse_reference_a1(cell) {
|
||||||
Ok((r.row, r.column))
|
Ok((r.row, r.column))
|
||||||
} else {
|
} else {
|
||||||
Err(format!("Invalid cell reference: '{cell}'"))
|
Err(format!("Invalid cell reference: '{}'", cell))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,17 +91,17 @@ fn parse_range(range: &str) -> Result<(i32, i32, i32, i32), String> {
|
|||||||
if let Some(r) = parse_reference_a1(parts[0]) {
|
if let Some(r) = parse_reference_a1(parts[0]) {
|
||||||
Ok((r.row, r.column, r.row, r.column))
|
Ok((r.row, r.column, r.row, r.column))
|
||||||
} else {
|
} else {
|
||||||
Err(format!("Invalid range: '{range}'"))
|
Err(format!("Invalid range: '{}'", range))
|
||||||
}
|
}
|
||||||
} else if parts.len() == 2 {
|
} else if parts.len() == 2 {
|
||||||
match (parse_reference_a1(parts[0]), parse_reference_a1(parts[1])) {
|
match (parse_reference_a1(parts[0]), parse_reference_a1(parts[1])) {
|
||||||
(Some(left), Some(right)) => {
|
(Some(left), Some(right)) => {
|
||||||
return Ok((left.row, left.column, right.row, right.column));
|
return Ok((left.row, left.column, right.row, right.column));
|
||||||
}
|
}
|
||||||
_ => return Err(format!("Invalid range: '{range}'")),
|
_ => return Err(format!("Invalid range: '{}'", range)),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(format!("Invalid range: '{range}'"));
|
return Err(format!("Invalid range: '{}'", range));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +390,7 @@ fn get_cell_from_excel(
|
|||||||
}
|
}
|
||||||
"d" => {
|
"d" => {
|
||||||
// Not implemented
|
// Not implemented
|
||||||
println!("Invalid type (d) in {sheet_name}!{cell_ref}");
|
println!("Invalid type (d) in {}!{}", sheet_name, cell_ref);
|
||||||
Cell::ErrorCell {
|
Cell::ErrorCell {
|
||||||
ei: Error::NIMPL,
|
ei: Error::NIMPL,
|
||||||
s: cell_style,
|
s: cell_style,
|
||||||
@@ -398,7 +398,7 @@ fn get_cell_from_excel(
|
|||||||
}
|
}
|
||||||
"inlineStr" => {
|
"inlineStr" => {
|
||||||
// Not implemented
|
// Not implemented
|
||||||
println!("Invalid type (inlineStr) in {sheet_name}!{cell_ref}");
|
println!("Invalid type (inlineStr) in {}!{}", sheet_name, cell_ref);
|
||||||
Cell::ErrorCell {
|
Cell::ErrorCell {
|
||||||
ei: Error::NIMPL,
|
ei: Error::NIMPL,
|
||||||
s: cell_style,
|
s: cell_style,
|
||||||
@@ -407,7 +407,10 @@ fn get_cell_from_excel(
|
|||||||
"empty" => Cell::EmptyCell { s: cell_style },
|
"empty" => Cell::EmptyCell { s: cell_style },
|
||||||
_ => {
|
_ => {
|
||||||
// error
|
// error
|
||||||
println!("Unexpected type ({cell_type}) in {sheet_name}!{cell_ref}");
|
println!(
|
||||||
|
"Unexpected type ({}) in {}!{}",
|
||||||
|
cell_type, sheet_name, cell_ref
|
||||||
|
);
|
||||||
Cell::ErrorCell {
|
Cell::ErrorCell {
|
||||||
ei: Error::ERROR,
|
ei: Error::ERROR,
|
||||||
s: cell_style,
|
s: cell_style,
|
||||||
@@ -441,15 +444,15 @@ fn get_cell_from_excel(
|
|||||||
f: formula_index,
|
f: formula_index,
|
||||||
ei: get_error_by_english_name(error_name).unwrap_or(Error::ERROR),
|
ei: get_error_by_english_name(error_name).unwrap_or(Error::ERROR),
|
||||||
s: cell_style,
|
s: cell_style,
|
||||||
o: format!("{sheet_name}!{cell_ref}"),
|
o: format!("{}!{}", sheet_name, cell_ref),
|
||||||
m: cell_value.unwrap_or("#ERROR!").to_string(),
|
m: cell_value.unwrap_or("#ERROR!").to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"s" => {
|
"s" => {
|
||||||
// Not implemented
|
// Not implemented
|
||||||
let o = format!("{sheet_name}!{cell_ref}");
|
let o = format!("{}!{}", sheet_name, cell_ref);
|
||||||
let m = Error::NIMPL.to_string();
|
let m = Error::NIMPL.to_string();
|
||||||
println!("Invalid type (s) in {sheet_name}!{cell_ref}");
|
println!("Invalid type (s) in {}!{}", sheet_name, cell_ref);
|
||||||
Cell::CellFormulaError {
|
Cell::CellFormulaError {
|
||||||
f: formula_index,
|
f: formula_index,
|
||||||
ei: Error::NIMPL,
|
ei: Error::NIMPL,
|
||||||
@@ -468,8 +471,8 @@ fn get_cell_from_excel(
|
|||||||
}
|
}
|
||||||
"d" => {
|
"d" => {
|
||||||
// Not implemented
|
// Not implemented
|
||||||
println!("Invalid type (d) in {sheet_name}!{cell_ref}");
|
println!("Invalid type (d) in {}!{}", sheet_name, cell_ref);
|
||||||
let o = format!("{sheet_name}!{cell_ref}");
|
let o = format!("{}!{}", sheet_name, cell_ref);
|
||||||
let m = Error::NIMPL.to_string();
|
let m = Error::NIMPL.to_string();
|
||||||
Cell::CellFormulaError {
|
Cell::CellFormulaError {
|
||||||
f: formula_index,
|
f: formula_index,
|
||||||
@@ -481,9 +484,9 @@ fn get_cell_from_excel(
|
|||||||
}
|
}
|
||||||
"inlineStr" => {
|
"inlineStr" => {
|
||||||
// Not implemented
|
// Not implemented
|
||||||
let o = format!("{sheet_name}!{cell_ref}");
|
let o = format!("{}!{}", sheet_name, cell_ref);
|
||||||
let m = Error::NIMPL.to_string();
|
let m = Error::NIMPL.to_string();
|
||||||
println!("Invalid type (inlineStr) in {sheet_name}!{cell_ref}");
|
println!("Invalid type (inlineStr) in {}!{}", sheet_name, cell_ref);
|
||||||
Cell::CellFormulaError {
|
Cell::CellFormulaError {
|
||||||
f: formula_index,
|
f: formula_index,
|
||||||
ei: Error::NIMPL,
|
ei: Error::NIMPL,
|
||||||
@@ -494,8 +497,11 @@ fn get_cell_from_excel(
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// error
|
// error
|
||||||
println!("Unexpected type ({cell_type}) in {sheet_name}!{cell_ref}");
|
println!(
|
||||||
let o = format!("{sheet_name}!{cell_ref}");
|
"Unexpected type ({}) in {}!{}",
|
||||||
|
cell_type, sheet_name, cell_ref
|
||||||
|
);
|
||||||
|
let o = format!("{}!{}", sheet_name, cell_ref);
|
||||||
let m = Error::ERROR.to_string();
|
let m = Error::ERROR.to_string();
|
||||||
Cell::CellFormulaError {
|
Cell::CellFormulaError {
|
||||||
f: formula_index,
|
f: formula_index,
|
||||||
@@ -880,7 +886,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
|
|||||||
Some(_) => {
|
Some(_) => {
|
||||||
// It's the mother cell. We do not use the ref attribute in IronCalc
|
// It's the mother cell. We do not use the ref attribute in IronCalc
|
||||||
let formula = fs[0].text().unwrap_or("").to_string();
|
let formula = fs[0].text().unwrap_or("").to_string();
|
||||||
let context = format!("{sheet_name}!{cell_ref}");
|
let context = format!("{}!{}", sheet_name, cell_ref);
|
||||||
let formula = from_a1_to_rc(
|
let formula = from_a1_to_rc(
|
||||||
formula,
|
formula,
|
||||||
worksheets,
|
worksheets,
|
||||||
@@ -943,7 +949,7 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
|
|||||||
}
|
}
|
||||||
// Its a cell with a simple formula
|
// Its a cell with a simple formula
|
||||||
let formula = fs[0].text().unwrap_or("").to_string();
|
let formula = fs[0].text().unwrap_or("").to_string();
|
||||||
let context = format!("{sheet_name}!{cell_ref}");
|
let context = format!("{}!{}", sheet_name, cell_ref);
|
||||||
let formula = from_a1_to_rc(
|
let formula = from_a1_to_rc(
|
||||||
formula,
|
formula,
|
||||||
worksheets,
|
worksheets,
|
||||||
@@ -962,7 +968,8 @@ pub(super) fn load_sheet<R: Read + std::io::Seek>(
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
return Err(XlsxError::Xml(format!(
|
return Err(XlsxError::Xml(format!(
|
||||||
"Invalid formula type {formula_type:?}.",
|
"Invalid formula type {:?}.",
|
||||||
|
formula_type,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -350,11 +350,11 @@ fn test_xlsx() {
|
|||||||
for file_path in entries {
|
for file_path in entries {
|
||||||
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
|
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
|
||||||
let file_path_str = file_path.to_str().unwrap();
|
let file_path_str = file_path.to_str().unwrap();
|
||||||
println!("Testing file: {file_path_str}");
|
println!("Testing file: {}", file_path_str);
|
||||||
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
|
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
|
||||||
if let Err(message) = test_file(file_path_str) {
|
if let Err(message) = test_file(file_path_str) {
|
||||||
println!("Error with file: '{file_path_str}'");
|
println!("Error with file: '{file_path_str}'");
|
||||||
println!("{message}");
|
println!("{}", message);
|
||||||
is_error = true;
|
is_error = true;
|
||||||
}
|
}
|
||||||
let t = test_load_and_saving(file_path_str, &dir);
|
let t = test_load_and_saving(file_path_str, &dir);
|
||||||
@@ -389,11 +389,11 @@ fn no_export() {
|
|||||||
for file_path in entries {
|
for file_path in entries {
|
||||||
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
|
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
|
||||||
let file_path_str = file_path.to_str().unwrap();
|
let file_path_str = file_path.to_str().unwrap();
|
||||||
println!("Testing file: {file_path_str}");
|
println!("Testing file: {}", file_path_str);
|
||||||
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
|
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
|
||||||
if let Err(message) = test_file(file_path_str) {
|
if let Err(message) = test_file(file_path_str) {
|
||||||
println!("Error with file: '{file_path_str}'");
|
println!("Error with file: '{file_path_str}'");
|
||||||
println!("{message}");
|
println!("{}", message);
|
||||||
is_error = true;
|
is_error = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -485,7 +485,7 @@ fn test_documentation_xlsx() {
|
|||||||
// Numerically unstable
|
// Numerically unstable
|
||||||
skip.push("TAN.xlsx");
|
skip.push("TAN.xlsx");
|
||||||
let skip: Vec<String> = skip.iter().map(|s| format!("tests/docs/{s}")).collect();
|
let skip: Vec<String> = skip.iter().map(|s| format!("tests/docs/{s}")).collect();
|
||||||
println!("{skip:?}");
|
println!("{:?}", skip);
|
||||||
// dumb counter to make sure we are actually testing the files
|
// dumb counter to make sure we are actually testing the files
|
||||||
assert!(entries.len() > 7);
|
assert!(entries.len() > 7);
|
||||||
let temp_folder = env::temp_dir();
|
let temp_folder = env::temp_dir();
|
||||||
@@ -497,13 +497,13 @@ fn test_documentation_xlsx() {
|
|||||||
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
|
let file_name_str = file_path.file_name().unwrap().to_str().unwrap();
|
||||||
let file_path_str = file_path.to_str().unwrap();
|
let file_path_str = file_path.to_str().unwrap();
|
||||||
if skip.contains(&file_path_str.to_string()) {
|
if skip.contains(&file_path_str.to_string()) {
|
||||||
println!("Skipping file: {file_path_str}");
|
println!("Skipping file: {}", file_path_str);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
println!("Testing file: {file_path_str}");
|
println!("Testing file: {}", file_path_str);
|
||||||
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
|
if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') {
|
||||||
if let Err(message) = test_file(file_path_str) {
|
if let Err(message) = test_file(file_path_str) {
|
||||||
println!("{message}");
|
println!("{}", message);
|
||||||
is_error = true;
|
is_error = true;
|
||||||
}
|
}
|
||||||
assert!(test_load_and_saving(file_path_str, &dir).is_ok());
|
assert!(test_load_and_saving(file_path_str, &dir).is_ok());
|
||||||
|
|||||||
Reference in New Issue
Block a user