Compare commits

..

1 Commits

Author SHA1 Message Date
Nicolás Hatcher
6740a43fe6 UPDATE: Add "Export Area to markdown" 2025-06-08 12:40:54 +02:00
58 changed files with 778 additions and 1155 deletions

32
Cargo.lock generated
View File

@@ -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"

View File

@@ -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 {

View File

@@ -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),
} }
} }

View File

@@ -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 {

View File

@@ -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(),

View File

@@ -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: _,

View File

@@ -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: _,

View File

@@ -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)]

View File

@@ -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())));

View File

@@ -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
} }
}, },

View File

@@ -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")

View File

@@ -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());

View File

@@ -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()

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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)
} }

View File

@@ -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)
} }

View File

@@ -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"))
} }

View File

@@ -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();

View File

@@ -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
}); });

View File

@@ -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> {

View File

@@ -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!");
} }

View File

@@ -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()),
) )
} }

View File

@@ -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
); );
} }
} }

View File

@@ -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");
} }

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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));
} }
} }
} }

View File

@@ -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) {

View File

@@ -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]

View File

@@ -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

View File

@@ -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"))

View File

@@ -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)
}
} }

View File

@@ -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"]};`;

View File

@@ -114,7 +114,7 @@ const Editor = (options: EditorOptions) => {
} }
} }
if (type === cell.focus) { if (type === cell.focus) {
textareaRef.current?.focus({ preventScroll: true }); textareaRef.current?.focus();
} }
}); });

View File

@@ -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}

View File

@@ -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();

View File

@@ -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,
}; };

View 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

View File

@@ -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",

View File

@@ -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")`

View File

@@ -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")`

View File

@@ -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;
},
`; `;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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.")
} }

View File

@@ -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)),

View File

@@ -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
)) ))
} }

View File

@@ -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)?;

View File

@@ -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)
} }
} }

View File

@@ -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)
} }

View File

@@ -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);
} }
} }
} }

View File

@@ -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)
} }
} }

View File

@@ -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,

View File

@@ -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,
))); )));
} }
} }

View File

@@ -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());