Compare commits
1 Commits
feature/ni
...
feature/ni
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a71eaf1dd7 |
1
.gitignore
vendored
@@ -1,2 +1 @@
|
||||
target/*
|
||||
.DS_Store
|
||||
12
Cargo.lock
generated
@@ -370,6 +370,7 @@ dependencies = [
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -678,6 +679,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
|
||||
12
Makefile
@@ -1,6 +1,13 @@
|
||||
all:
|
||||
cargo build --release
|
||||
cd bindings/wasm/ && make
|
||||
cd webapp && npm install && npm run build
|
||||
|
||||
lint:
|
||||
cargo fmt -- --check
|
||||
cargo clippy --all-targets --all-features
|
||||
# TODO: See issue #33
|
||||
# cargo clippy --all-targets --all-features -- -D warnings -D clippy::expect_used -D clippy::unwrap_used -D clippy::panic
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
|
||||
format:
|
||||
cargo fmt
|
||||
@@ -10,7 +17,7 @@ tests: lint
|
||||
./target/debug/documentation
|
||||
cmp functions.md wiki/functions.md || exit 1
|
||||
make remove-artifacts
|
||||
cd bindings/wasm/ && wasm-pack build --target nodejs && node tests/test.mjs
|
||||
cd bindings/wasm/ && make tests
|
||||
|
||||
remove-artifacts:
|
||||
rm -f xlsx/hello-calc.xlsx
|
||||
@@ -25,6 +32,7 @@ clean: remove-artifacts
|
||||
rm -f cargo-test-*
|
||||
rm -f base/cargo-test-*
|
||||
rm -f xlsx/cargo-test-*
|
||||
rm -r -f webapp/node_modules
|
||||
|
||||
|
||||
coverage:
|
||||
|
||||
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 441 B |
|
Before Width: | Height: | Size: 729 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 33 KiB |
@@ -1,8 +0,0 @@
|
||||
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="600" height="600" rx="20" fill="#F2994A"/>
|
||||
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 100C348.98 166.034 322.748 229.362 276.055 276.055C268.163 283.947 259.796 291.255 251.021 297.95L251.021 500L348.98 500H251.021C251.021 433.966 277.252 370.637 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05L348.98 100Z" fill="white"/>
|
||||
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M251.021 100.068C251.003 140.096 235.094 178.481 206.788 206.787C178.466 235.109 140.053 251.02 100 251.02V348.979C154.873 348.979 207.877 330.866 251.021 297.95V100.068Z" fill="white"/>
|
||||
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 499.882C349.011 459.872 364.918 421.507 393.213 393.213C421.534 364.891 459.947 348.98 500 348.98V251.02C445.128 251.02 392.123 269.134 348.98 302.05V499.882Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.055 276.055C322.748 229.362 348.98 166.034 348.98 100H251.021V297.95C259.796 291.255 268.163 283.947 276.055 276.055Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M348.98 302.05V499.895C348.98 499.93 348.98 499.965 348.98 500L251.021 500C251.021 499.946 251.02 499.891 251.021 499.837C251.064 433.862 277.291 370.599 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
BIN
assets/logo.png
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
@@ -12,6 +12,8 @@ readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_repr = "0.1"
|
||||
ryu = "1.0"
|
||||
chrono = "0.4"
|
||||
chrono-tz = "0.9"
|
||||
@@ -19,9 +21,6 @@ regex = "1.0"
|
||||
once_cell = "1.16.0"
|
||||
bitcode = "0.6.0"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
js-sys = { version = "0.3.69" }
|
||||
|
||||
|
||||
@@ -69,26 +69,21 @@ impl Model {
|
||||
target_row: i32,
|
||||
target_column: i32,
|
||||
) -> Result<(), String> {
|
||||
if let Some(source_cell) = self
|
||||
let source_cell = self
|
||||
.workbook
|
||||
.worksheet(sheet)?
|
||||
.cell(source_row, source_column)
|
||||
{
|
||||
let style = source_cell.get_style();
|
||||
// FIXME: we need some user_input getter instead of get_text
|
||||
let formula_or_value = self
|
||||
.get_cell_formula(sheet, source_row, source_column)?
|
||||
.unwrap_or_else(|| {
|
||||
source_cell.get_text(&self.workbook.shared_strings, &self.language)
|
||||
});
|
||||
self.set_user_input(sheet, target_row, target_column, formula_or_value);
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_cell_style(target_row, target_column, style);
|
||||
self.cell_clear_all(sheet, source_row, source_column)?;
|
||||
} else {
|
||||
self.cell_clear_all(sheet, target_row, target_column)?;
|
||||
}
|
||||
.ok_or("Expected Cell to exist")?;
|
||||
let style = source_cell.get_style();
|
||||
// FIXME: we need some user_input getter instead of get_text
|
||||
let formula_or_value = self
|
||||
.get_cell_formula(sheet, source_row, source_column)?
|
||||
.unwrap_or_else(|| source_cell.get_text(&self.workbook.shared_strings, &self.language));
|
||||
self.set_user_input(sheet, target_row, target_column, formula_or_value);
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_cell_style(target_row, target_column, style);
|
||||
self.cell_clear_all(sheet, source_row, source_column)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -111,7 +106,7 @@ impl Model {
|
||||
return Err("Cannot add a negative number of cells :)".to_string());
|
||||
}
|
||||
// check if it is possible:
|
||||
let dimensions = self.workbook.worksheet(sheet)?.get_dimension();
|
||||
let dimensions = self.workbook.worksheet(sheet)?.dimension();
|
||||
let last_column = dimensions.max_column + column_count;
|
||||
if last_column > LAST_COLUMN {
|
||||
return Err(
|
||||
@@ -268,7 +263,7 @@ impl Model {
|
||||
return Err("Cannot add a negative number of cells :)".to_string());
|
||||
}
|
||||
// Check if it is possible:
|
||||
let dimensions = self.workbook.worksheet(sheet)?.get_dimension();
|
||||
let dimensions = self.workbook.worksheet(sheet)?.dimension();
|
||||
let last_row = dimensions.max_row + row_count;
|
||||
if last_row > LAST_ROW {
|
||||
return Err(
|
||||
@@ -372,162 +367,13 @@ impl Model {
|
||||
}
|
||||
}
|
||||
self.workbook.worksheets[sheet as usize].rows = new_rows;
|
||||
self.displace_cells(&DisplaceData::Row {
|
||||
sheet,
|
||||
row,
|
||||
delta: -row_count,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_cells_and_shift_left(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
let max_column = worksheet.get_dimension().max_column;
|
||||
|
||||
// Delete all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column..column + column_delta {
|
||||
self.cell_clear_all(sheet, r, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Move all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column + 1..max_column + 1 {
|
||||
println!("{r}-{c}");
|
||||
self.move_cell(sheet, r, c, r, c - column_delta)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::ShiftCellsRight {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
column_delta: -column_delta,
|
||||
row_delta,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert cells and shift right
|
||||
pub fn insert_cells_and_shift_right(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
let max_column = worksheet.get_dimension().max_column;
|
||||
|
||||
// Move all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in (column..max_column + 1).rev() {
|
||||
self.move_cell(sheet, r, c, r, c + column_delta)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column..column + column_delta {
|
||||
self.cell_clear_all(sheet, r, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::ShiftCellsRight {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
column_delta,
|
||||
row_delta,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert cells and shift down
|
||||
pub fn insert_cells_and_shift_down(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
let max_row = worksheet.get_dimension().max_row;
|
||||
|
||||
// Move all cells in the range
|
||||
for r in (row..row + max_row + 1).rev() {
|
||||
for c in column..column + column_delta {
|
||||
self.move_cell(sheet, r, c, r, c + column_delta)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column..column + column_delta {
|
||||
self.cell_clear_all(sheet, r, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::ShiftCellsDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
column_delta,
|
||||
row_delta,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_cells_and_shift_up(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let worksheet = &mut self.workbook.worksheet_mut(sheet)?;
|
||||
let max_row = worksheet.get_dimension().max_row;
|
||||
|
||||
// Delete all cells in the range
|
||||
for r in row..row + row_delta {
|
||||
for c in column..column + column_delta {
|
||||
self.cell_clear_all(sheet, r, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Move all cells in the range
|
||||
for r in row..max_row + 1 {
|
||||
for c in column + 1..column + column_delta {
|
||||
self.move_cell(sheet, r, c, r - row_delta, c)?;
|
||||
}
|
||||
}
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::ShiftCellsDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
column_delta,
|
||||
row_delta: -row_delta,
|
||||
});
|
||||
|
||||
self.displace_cells(
|
||||
&(DisplaceData::Row {
|
||||
sheet,
|
||||
row,
|
||||
delta: -row_count,
|
||||
}),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use crate::{
|
||||
expressions::token::Error, language::Language, number_format::to_excel_precision_str, types::*,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
/// A CellValue is the representation of the cell content.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum CellValue {
|
||||
None,
|
||||
String(String),
|
||||
@@ -11,6 +14,17 @@ pub enum CellValue {
|
||||
Boolean(bool),
|
||||
}
|
||||
|
||||
impl CellValue {
|
||||
pub fn to_json_str(&self) -> String {
|
||||
match &self {
|
||||
CellValue::None => "null".to_string(),
|
||||
CellValue::String(s) => json!(s).to_string(),
|
||||
CellValue::Number(f) => json!(f).to_string(),
|
||||
CellValue::Boolean(b) => json!(b).to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for CellValue {
|
||||
fn from(value: f64) -> Self {
|
||||
Self::Number(value)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
//! A tokenizer for spreadsheet formulas.
|
||||
//!
|
||||
//! This is meant to feed a formula parser.
|
||||
@@ -9,10 +7,8 @@
|
||||
//! It supports two working modes:
|
||||
//!
|
||||
//! 1. A1 or display mode
|
||||
//!
|
||||
//! This is for user formulas. References are like `D4`, `D$4` or `F5:T10`
|
||||
//! 2. R1C1, internal or runtime mode
|
||||
//!
|
||||
//! A reference like R1C1 refers to $A$1 and R3C4 to $D$4
|
||||
//! R[2]C[5] refers to a cell two rows below and five columns to the right
|
||||
//! It uses the 'en' locale and language.
|
||||
@@ -59,8 +55,7 @@ use super::token::{Error, TokenType};
|
||||
use super::types::*;
|
||||
use super::utils;
|
||||
|
||||
/// Returns an iterator over tokens together with their position in the byte string.
|
||||
pub mod marked_token;
|
||||
pub mod util;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
@@ -68,28 +63,17 @@ mod test;
|
||||
mod ranges;
|
||||
mod structured_references;
|
||||
|
||||
/// This is the TokenType we return if we cannot recognize a token
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct LexerError {
|
||||
/// Position of the beginning of the token in the byte string.
|
||||
pub position: usize,
|
||||
/// Message describing what we think the error is.
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub(super) type Result<T> = std::result::Result<T, LexerError>;
|
||||
|
||||
/// Whether we try to parse formulas in A1 mode or in the internal R1C1 mode
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum LexerMode {
|
||||
/// Cell references are written `=S34`. This is the display mode
|
||||
A1,
|
||||
/// R1C1, internal or runtime mode
|
||||
///
|
||||
/// A reference like R1C1 refers to $A$1 and R3C4 to $D$4
|
||||
/// R[2]C[5] refers to a cell two rows below and five columns to the right
|
||||
/// It uses the 'en' locale and language.
|
||||
/// This is used internally at runtime.
|
||||
R1C1,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod test_common;
|
||||
mod test_language;
|
||||
mod test_locale;
|
||||
mod test_marked_token;
|
||||
mod test_ranges;
|
||||
mod test_tables;
|
||||
mod test_util;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::expressions::{
|
||||
lexer::marked_token::{get_tokens, MarkedToken},
|
||||
lexer::util::get_tokens,
|
||||
token::{OpCompare, OpSum, TokenType},
|
||||
};
|
||||
|
||||
@@ -22,29 +22,6 @@ fn test_get_tokens() {
|
||||
assert_eq!(l.end, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chinese_characters() {
|
||||
let formula = "\"你好\" & \"世界!\"";
|
||||
let marked_tokens = get_tokens(formula);
|
||||
assert_eq!(marked_tokens.len(), 3);
|
||||
let first_t = MarkedToken {
|
||||
token: TokenType::String("你好".to_string()),
|
||||
start: 0,
|
||||
end: 4,
|
||||
};
|
||||
let second_t = MarkedToken {
|
||||
token: TokenType::And,
|
||||
start: 4,
|
||||
end: 6,
|
||||
};
|
||||
let third_t = MarkedToken {
|
||||
token: TokenType::String("世界!".to_string()),
|
||||
start: 6,
|
||||
end: 12,
|
||||
};
|
||||
assert_eq!(marked_tokens, vec![first_t, second_t, third_t]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_tokens() {
|
||||
assert_eq!(
|
||||
@@ -1,5 +1,3 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::expressions::token;
|
||||
@@ -11,11 +9,8 @@ use super::{Lexer, LexerMode};
|
||||
/// A MarkedToken is a token together with its position on a formula
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MarkedToken {
|
||||
/// Token type (see [token::TokenType])
|
||||
pub token: token::TokenType,
|
||||
/// Position of the start of the token (in bytes)
|
||||
pub start: i32,
|
||||
/// Position of the end of the token (in bytes)
|
||||
pub end: i32,
|
||||
}
|
||||
|
||||
@@ -24,7 +19,7 @@ pub struct MarkedToken {
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ironcalc_base::expressions::{
|
||||
/// lexer::marked_token::{get_tokens, MarkedToken},
|
||||
/// lexer::util::{get_tokens, MarkedToken},
|
||||
/// token::{OpSum, TokenType},
|
||||
/// };
|
||||
///
|
||||
@@ -1,3 +1,4 @@
|
||||
// public modules
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
pub mod token;
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
//! # GRAMMAR
|
||||
//!
|
||||
//! <pre class="rust">
|
||||
//! opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
|
||||
//! opFactor => '*' | '/'
|
||||
//! unaryOp => '-' | '+'
|
||||
//!
|
||||
//! expr => concat (opComp concat)*
|
||||
//! concat => term ('&' term)*
|
||||
//! term => factor (opFactor factor)*
|
||||
//! factor => prod (opProd prod)*
|
||||
//! prod => power ('^' power)*
|
||||
//! power => (unaryOp)* range '%'*
|
||||
//! range => primary (':' primary)?
|
||||
//! primary => '(' expr ')'
|
||||
//! => number
|
||||
//! => function '(' f_args ')'
|
||||
//! => name
|
||||
//! => string
|
||||
//! => '{' a_args '}'
|
||||
//! => bool
|
||||
//! => bool()
|
||||
//! => error
|
||||
//!
|
||||
//! f_args => e (',' e)*
|
||||
//! </pre>
|
||||
/*!
|
||||
# GRAMAR
|
||||
|
||||
<pre class="rust">
|
||||
opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
|
||||
opFactor => '*' | '/'
|
||||
unaryOp => '-' | '+'
|
||||
|
||||
expr => concat (opComp concat)*
|
||||
concat => term ('&' term)*
|
||||
term => factor (opFactor factor)*
|
||||
factor => prod (opProd prod)*
|
||||
prod => power ('^' power)*
|
||||
power => (unaryOp)* range '%'*
|
||||
range => primary (':' primary)?
|
||||
primary => '(' expr ')'
|
||||
=> number
|
||||
=> function '(' f_args ')'
|
||||
=> name
|
||||
=> string
|
||||
=> '{' a_args '}'
|
||||
=> bool
|
||||
=> bool()
|
||||
=> error
|
||||
|
||||
f_args => e (',' e)*
|
||||
</pre>
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -42,15 +44,21 @@ use super::utils::number_to_column;
|
||||
|
||||
use token::OpCompare;
|
||||
|
||||
pub(crate) mod move_formula;
|
||||
pub(crate) mod walk;
|
||||
|
||||
/// Produces a string representation of a formula from the AST.
|
||||
pub mod move_formula;
|
||||
pub mod stringify;
|
||||
pub mod walk;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_ranges;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_move_formula;
|
||||
#[cfg(test)]
|
||||
mod test_tables;
|
||||
|
||||
pub(crate) fn parse_range(formula: &str) -> Result<(i32, i32, i32, i32), String> {
|
||||
let mut lexer = lexer::Lexer::new(
|
||||
formula,
|
||||
@@ -214,7 +222,7 @@ impl Parser {
|
||||
|
||||
pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node {
|
||||
self.lexer.set_formula(formula);
|
||||
self.context.clone_from(context);
|
||||
self.context = context.clone();
|
||||
self.parse_expr()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,75 +1,43 @@
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use super::{super::utils::quote_name, Node, Reference};
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::token::OpUnary;
|
||||
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
|
||||
|
||||
/// Displaced data
|
||||
pub enum DisplaceData {
|
||||
/// Displaces columns (inserting or deleting columns)
|
||||
Column {
|
||||
/// Sheet in which the displace data applies
|
||||
sheet: u32,
|
||||
/// Column from which the data is displaced
|
||||
column: i32,
|
||||
/// Number of columns displaced (might be negative, e.g. when deleting columns)
|
||||
delta: i32,
|
||||
},
|
||||
/// Displaces rows (Inserting or deleting rows)
|
||||
Row {
|
||||
/// Sheet in which the displace data applies
|
||||
sheet: u32,
|
||||
/// Row from which the data is displaced
|
||||
row: i32,
|
||||
/// Number of rows displaced (might be negative, e.g. when deleting rows)
|
||||
delta: i32,
|
||||
},
|
||||
/// Displaces cells horizontally
|
||||
ShiftCellsRight {
|
||||
/// Sheet in which the displace data applies
|
||||
CellHorizontal {
|
||||
sheet: u32,
|
||||
/// Row of the to left corner
|
||||
row: i32,
|
||||
/// Column of the top left corner
|
||||
column: i32,
|
||||
/// Number of rows to be displaced
|
||||
row_delta: i32,
|
||||
/// Number of columns to be displaced (might be negative)
|
||||
column_delta: i32,
|
||||
delta: i32,
|
||||
},
|
||||
/// Displaces cells vertically
|
||||
ShiftCellsDown {
|
||||
/// Sheet in which the displace data applies
|
||||
CellVertical {
|
||||
sheet: u32,
|
||||
/// Row of the to left corner
|
||||
row: i32,
|
||||
/// Column of the top left corner
|
||||
column: i32,
|
||||
/// Number of rows displaced (might be negative)
|
||||
row_delta: i32,
|
||||
/// Number of columns to be displaced
|
||||
column_delta: i32,
|
||||
delta: i32,
|
||||
},
|
||||
/// Displaces data due to a column move from column to column + delta
|
||||
ColumnMove {
|
||||
/// Sheet in which the displace data applies
|
||||
sheet: u32,
|
||||
/// Column that is moved
|
||||
column: i32,
|
||||
/// The position of the new column is column + delta (might be negative)
|
||||
delta: i32,
|
||||
},
|
||||
/// Doesn't do any cell displacement
|
||||
None,
|
||||
}
|
||||
|
||||
/// Stringifies the AST formula in its internal R1C1 format
|
||||
pub fn to_rc_format(node: &Node) -> String {
|
||||
stringify(node, None, &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
/// Stringifies the formula applying the _displace_data_.
|
||||
pub fn to_string_displaced(
|
||||
node: &Node,
|
||||
context: &CellReferenceRC,
|
||||
@@ -78,12 +46,10 @@ pub fn to_string_displaced(
|
||||
stringify(node, Some(context), displace_data, false)
|
||||
}
|
||||
|
||||
/// Stringifies a formula from the AST
|
||||
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
/// Stringifies the formula for Excel compatibility
|
||||
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, true)
|
||||
}
|
||||
@@ -150,49 +116,41 @@ pub(crate) fn stringify_reference(
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::ShiftCellsRight {
|
||||
DisplaceData::CellHorizontal {
|
||||
sheet,
|
||||
row: displace_row,
|
||||
column: displace_column,
|
||||
column_delta,
|
||||
row_delta,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet
|
||||
&& displace_row >= &row
|
||||
&& *displace_row < row + *row_delta
|
||||
{
|
||||
if *column_delta < 0 {
|
||||
if sheet_index == *sheet && displace_row == &row {
|
||||
if *delta < 0 {
|
||||
if &column >= displace_column {
|
||||
if column < displace_column - *column_delta {
|
||||
if column < displace_column - *delta {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
column += *column_delta;
|
||||
column += *delta;
|
||||
}
|
||||
} else if &column >= displace_column {
|
||||
column += *column_delta;
|
||||
column += *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::ShiftCellsDown {
|
||||
DisplaceData::CellVertical {
|
||||
sheet,
|
||||
row: displace_row,
|
||||
column: displace_column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet
|
||||
&& displace_column >= &column
|
||||
&& *displace_column < column + *column_delta
|
||||
{
|
||||
if *row_delta < 0 {
|
||||
if sheet_index == *sheet && displace_column == &column {
|
||||
if *delta < 0 {
|
||||
if &row >= displace_row {
|
||||
if row < displace_row - *row_delta {
|
||||
if row < displace_row - *delta {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
row += *row_delta;
|
||||
row += *delta;
|
||||
}
|
||||
} else if &row >= displace_row {
|
||||
row += *row_delta;
|
||||
row += *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::lexer::LexerMode;
|
||||
use crate::expressions::parser::stringify::{to_string_displaced, DisplaceData};
|
||||
use crate::expressions::parser::{
|
||||
stringify::{to_rc_format, to_string},
|
||||
Node, Parser,
|
||||
use crate::expressions::parser::stringify::DisplaceData;
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
use super::{
|
||||
super::parser::{
|
||||
stringify::{to_rc_format, to_string},
|
||||
Node,
|
||||
},
|
||||
stringify::to_string_displaced,
|
||||
};
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
|
||||
struct Formula<'a> {
|
||||
initial: &'a str,
|
||||
@@ -1,4 +0,0 @@
|
||||
mod test_genertal;
|
||||
mod test_move_formula;
|
||||
mod test_ranges;
|
||||
mod test_tables;
|
||||
@@ -1,8 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::move_formula::{move_formula, MoveContext};
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::{Area, CellReferenceRC};
|
||||
use crate::expressions::types::Area;
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
|
||||
#[test]
|
||||
fn test_move_formula() {
|
||||
@@ -2,9 +2,9 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::lexer::LexerMode;
|
||||
|
||||
use crate::expressions::parser::stringify::{to_rc_format, to_string};
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
use super::super::parser::stringify::{to_rc_format, to_string};
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
|
||||
struct Formula<'a> {
|
||||
formula_a1: &'a str,
|
||||
@@ -6,8 +6,8 @@ use crate::expressions::parser::stringify::to_string;
|
||||
use crate::expressions::utils::{number_to_column, parse_reference_a1};
|
||||
use crate::types::{Table, TableColumn, TableStyleInfo};
|
||||
|
||||
use crate::expressions::parser::Parser;
|
||||
use crate::expressions::types::CellReferenceRC;
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
|
||||
fn create_test_table(
|
||||
table_name: &str,
|
||||
@@ -2,6 +2,7 @@ use std::fmt;
|
||||
|
||||
use bitcode::{Decode, Encode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
|
||||
use crate::language::Language;
|
||||
|
||||
@@ -80,7 +81,8 @@ impl fmt::Display for OpProduct {
|
||||
/// * "#ERROR!" means there was an error processing the formula (for instance "=A1+")
|
||||
/// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc
|
||||
/// Note that they are serialized/deserialized by index
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize_repr, Deserialize_repr, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum Error {
|
||||
REF,
|
||||
NAME,
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::fmt;
|
||||
use crate::{
|
||||
calc_result::CalcResult,
|
||||
expressions::{
|
||||
lexer::marked_token::get_tokens,
|
||||
lexer::util::get_tokens,
|
||||
parser::Node,
|
||||
token::{Error, OpSum, TokenType},
|
||||
types::CellReferenceIndex,
|
||||
|
||||
@@ -221,7 +221,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -229,7 +229,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
@@ -284,7 +284,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -292,7 +292,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
@@ -360,7 +360,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -368,7 +368,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
@@ -866,7 +866,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -874,7 +874,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
|
||||
@@ -132,7 +132,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -140,7 +140,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
@@ -199,7 +199,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -207,7 +207,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
|
||||
@@ -385,7 +385,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(first_range.left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension();
|
||||
.dimension();
|
||||
let max_row = dimension.max_row;
|
||||
let max_column = dimension.max_column;
|
||||
|
||||
@@ -530,7 +530,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(sum_range.left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if left_column == 1 && right_column == LAST_COLUMN {
|
||||
@@ -538,7 +538,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(sum_range.left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
|
||||
|
||||
@@ -892,7 +892,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -900,7 +900,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
|
||||
@@ -255,7 +255,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
@@ -263,7 +263,7 @@ impl Model {
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.get_dimension()
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
let left = CellReferenceIndex {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
PfrendeesD<>VRAITRUEWAHRVERDADEROTVFAUXFALSEFALSCHFALSOUw#REF!#REF!#BEZUG!#¡REF!e<>#NOM?#NAME?#NAME?#¿NOMBRE?x<>#VALEUR!#VALUE!#WERT!#¡VALOR!w<>#DIV/0!#DIV/0!#DIV/0!#¡DIV/0!<04>#N/A#N/A#NV#N/AXv#NOMBRE!#NUM!#ZAHL!#¡NUM!<02><>#N/IMPL!#N/IMPL!#N/IMPL!#N/IMPL!w{#SPILL!#SPILL!#ÜBERLAUF!#SPILL!ff#CALC!#CALC!#CALC!#CALC!ff#CIRC!#CIRC!#CIRC!#CIRC!ww#ERROR!#ERROR!#ERROR!#ERROR!ff#NULL!#NULL!#NULL!#NULL!
|
||||
@@ -1,15 +1,15 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bitcode::{Decode, Encode};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Booleans {
|
||||
pub r#true: String,
|
||||
pub r#false: String,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Errors {
|
||||
pub r#ref: String,
|
||||
pub name: String,
|
||||
@@ -25,14 +25,14 @@ pub struct Errors {
|
||||
pub null: String,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Language {
|
||||
pub booleans: Booleans,
|
||||
pub errors: Errors,
|
||||
}
|
||||
|
||||
static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| {
|
||||
bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file")
|
||||
serde_json::from_str(include_str!("language.json")).expect("Failed parsing language file")
|
||||
});
|
||||
|
||||
pub fn get_language(id: &str) -> Result<&Language, String> {
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
use bitcode::{Decode, Encode};
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Locale {
|
||||
pub dates: Dates,
|
||||
pub numbers: NumbersProperties,
|
||||
pub currency: Currency,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Currency {
|
||||
pub iso: String,
|
||||
pub symbol: String,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct NumbersProperties {
|
||||
#[serde(rename = "symbols-numberSystem-latn")]
|
||||
pub symbols: NumbersSymbols,
|
||||
#[serde(rename = "decimalFormats-numberSystem-latn")]
|
||||
pub decimal_formats: DecimalFormats,
|
||||
#[serde(rename = "currencyFormats-numberSystem-latn")]
|
||||
pub currency_formats: CurrencyFormats,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Dates {
|
||||
pub day_names: Vec<String>,
|
||||
pub day_names_short: Vec<String>,
|
||||
@@ -32,7 +35,8 @@ pub struct Dates {
|
||||
pub months_letter: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NumbersSymbols {
|
||||
pub decimal: String,
|
||||
pub group: String,
|
||||
@@ -50,23 +54,31 @@ pub struct NumbersSymbols {
|
||||
}
|
||||
|
||||
// See: https://cldr.unicode.org/translation/number-currency-formats/number-and-currency-patterns
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct CurrencyFormats {
|
||||
pub standard: String,
|
||||
#[serde(rename = "standard-alphaNextToNumber")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub standard_alpha_next_to_number: Option<String>,
|
||||
#[serde(rename = "standard-noCurrency")]
|
||||
pub standard_no_currency: String,
|
||||
pub accounting: String,
|
||||
#[serde(rename = "accounting-alphaNextToNumber")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub accounting_alpha_next_to_number: Option<String>,
|
||||
#[serde(rename = "accounting-noCurrency")]
|
||||
pub accounting_no_currency: String,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone)]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DecimalFormats {
|
||||
pub standard: String,
|
||||
}
|
||||
|
||||
static LOCALES: Lazy<HashMap<String, Locale>> =
|
||||
Lazy::new(|| bitcode::decode(include_bytes!("locales.bin")).expect("Failed parsing locale"));
|
||||
static LOCALES: Lazy<HashMap<String, Locale>> = Lazy::new(|| {
|
||||
serde_json::from_str(include_str!("locales.json")).expect("Failed parsing locale")
|
||||
});
|
||||
|
||||
pub fn get_locale(id: &str) -> Result<&Locale, String> {
|
||||
// TODO: pass the locale once we implement locales in Rust
|
||||
|
||||
@@ -1788,7 +1788,7 @@ impl Model {
|
||||
/// Returns markup representation of the given `sheet`.
|
||||
pub fn get_sheet_markup(&self, sheet: u32) -> Result<String, String> {
|
||||
let worksheet = self.workbook.worksheet(sheet)?;
|
||||
let dimension = worksheet.get_dimension();
|
||||
let dimension = worksheet.dimension();
|
||||
|
||||
let mut rows = Vec::new();
|
||||
|
||||
|
||||
@@ -6,16 +6,14 @@ use crate::{
|
||||
calc_result::Range,
|
||||
expressions::{
|
||||
lexer::LexerMode,
|
||||
parser::{
|
||||
stringify::{rename_sheet_in_node, to_rc_format},
|
||||
Parser,
|
||||
},
|
||||
parser::stringify::{rename_sheet_in_node, to_rc_format},
|
||||
parser::Parser,
|
||||
types::CellReferenceRC,
|
||||
},
|
||||
language::get_language,
|
||||
locale::get_locale,
|
||||
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
|
||||
types::{Metadata, Selection, SheetState, Workbook, WorkbookSettings, Worksheet},
|
||||
types::{Metadata, SheetState, Workbook, WorkbookSettings, Worksheet},
|
||||
utils::ParsedReference,
|
||||
};
|
||||
|
||||
@@ -50,12 +48,6 @@ impl Model {
|
||||
color: Default::default(),
|
||||
frozen_columns: 0,
|
||||
frozen_rows: 0,
|
||||
selection: Selection {
|
||||
is_selected: false,
|
||||
row: 1,
|
||||
column: 1,
|
||||
range: [1, 1, 1, 1],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
mod test_fn_average;
|
||||
mod test_fn_averageifs;
|
||||
mod test_fn_choose;
|
||||
mod test_fn_concatenate;
|
||||
mod test_fn_count;
|
||||
mod test_fn_exact;
|
||||
mod test_fn_financial;
|
||||
mod test_fn_if;
|
||||
mod test_fn_maxifs;
|
||||
mod test_fn_minifs;
|
||||
mod test_fn_offset;
|
||||
mod test_fn_product;
|
||||
mod test_fn_rept;
|
||||
mod test_fn_sum;
|
||||
mod test_fn_sumifs;
|
||||
mod test_fn_textbefore;
|
||||
mod test_fn_textjoin;
|
||||
mod test_fn_type;
|
||||
@@ -1,5 +1,3 @@
|
||||
mod engineering;
|
||||
mod functions;
|
||||
mod test_actions;
|
||||
mod test_binary_search;
|
||||
mod test_cell;
|
||||
@@ -10,30 +8,49 @@ mod test_criteria;
|
||||
mod test_currency;
|
||||
mod test_date_and_time;
|
||||
mod test_error_propagation;
|
||||
mod test_escape_quotes;
|
||||
mod test_extend;
|
||||
mod test_fn_average;
|
||||
mod test_fn_averageifs;
|
||||
mod test_fn_choose;
|
||||
mod test_fn_concatenate;
|
||||
mod test_fn_count;
|
||||
mod test_fn_exact;
|
||||
mod test_fn_financial;
|
||||
mod test_fn_if;
|
||||
mod test_fn_maxifs;
|
||||
mod test_fn_minifs;
|
||||
mod test_fn_product;
|
||||
mod test_fn_rept;
|
||||
mod test_fn_sum;
|
||||
mod test_fn_sumifs;
|
||||
mod test_fn_textbefore;
|
||||
mod test_fn_textjoin;
|
||||
mod test_forward_references;
|
||||
mod test_frozen_rows_and_columns;
|
||||
mod test_frozen_rows_columns;
|
||||
mod test_general;
|
||||
mod test_get_cell_content;
|
||||
mod test_math;
|
||||
mod test_metadata;
|
||||
mod test_model_cell_clear_all;
|
||||
mod test_model_is_empty_cell;
|
||||
mod test_move_formula;
|
||||
mod test_number_format;
|
||||
mod test_percentage;
|
||||
mod test_quote_prefix;
|
||||
mod test_set_user_input;
|
||||
mod test_sheet_markup;
|
||||
mod test_sheets;
|
||||
mod test_shift_cells;
|
||||
mod test_styles;
|
||||
mod test_today;
|
||||
mod test_trigonometric;
|
||||
mod test_types;
|
||||
mod test_workbook;
|
||||
mod test_worksheet;
|
||||
mod user_model;
|
||||
pub(crate) mod util;
|
||||
|
||||
mod engineering;
|
||||
mod test_fn_offset;
|
||||
mod test_number_format;
|
||||
|
||||
mod test_escape_quotes;
|
||||
mod test_extend;
|
||||
mod test_fn_type;
|
||||
mod test_frozen_rows_and_columns;
|
||||
mod test_get_cell_content;
|
||||
mod test_percentage;
|
||||
mod test_today;
|
||||
mod user_model;
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn shift_cells_right() {
|
||||
let mut model = new_empty_model();
|
||||
let (sheet, row, column) = (0, 5, 3); // C5
|
||||
model.set_user_input(sheet, row, column, "Hi".to_string());
|
||||
model.set_user_input(sheet, row, column + 1, "world".to_string());
|
||||
model.set_user_input(sheet, row, column + 2, "!".to_string());
|
||||
|
||||
model
|
||||
.insert_cells_and_shift_right(sheet, row, column, 1, 1)
|
||||
.unwrap();
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model.get_cell_content(0, 5, 3), Ok("".to_string()));
|
||||
assert_eq!(model.get_cell_content(0, 5, 4), Ok("Hi".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_cells_right_with_formulas() {
|
||||
let mut model = new_empty_model();
|
||||
let (sheet, row, column) = (0, 5, 3); // C5
|
||||
model.set_user_input(sheet, row, column - 1, "23".to_string());
|
||||
model.set_user_input(sheet, row, column, "42".to_string());
|
||||
model.set_user_input(sheet, row, column + 1, "=C5*2".to_string());
|
||||
model.set_user_input(sheet, row, column + 2, "=A5+2".to_string());
|
||||
model.set_user_input(sheet, 20, 3, "=C5*A2".to_string());
|
||||
model.evaluate();
|
||||
|
||||
model
|
||||
.insert_cells_and_shift_right(sheet, row, column, 1, 1)
|
||||
.unwrap();
|
||||
|
||||
model.evaluate();
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column - 1),
|
||||
Ok("23".to_string())
|
||||
);
|
||||
assert_eq!(model.get_cell_content(0, row, column), Ok("".to_string()));
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 1),
|
||||
Ok("42".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 2),
|
||||
Ok("=D5*2".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 3),
|
||||
Ok("=A5+2".to_string())
|
||||
);
|
||||
assert_eq!(model.get_cell_content(0, 20, 3), Ok("=D5*A2".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_cells_left() {
|
||||
let mut model = new_empty_model();
|
||||
let (sheet, row, column) = (0, 5, 10); // J5
|
||||
model.set_user_input(sheet, row, column - 1, "23".to_string());
|
||||
model.set_user_input(sheet, row, column, "42".to_string());
|
||||
model.set_user_input(sheet, row, column + 1, "Hi".to_string());
|
||||
model.set_user_input(sheet, row, column + 2, "honey!".to_string());
|
||||
model.evaluate();
|
||||
|
||||
model
|
||||
.delete_cells_and_shift_left(sheet, row, column, 1, 1)
|
||||
.unwrap();
|
||||
|
||||
model.evaluate();
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column - 1),
|
||||
Ok("23".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(model.get_cell_content(0, row, column), Ok("Hi".to_string()));
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 1),
|
||||
Ok("honey!".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_cells_left_with_formulas() {
|
||||
let mut model = new_empty_model();
|
||||
let (sheet, row, column) = (0, 5, 10); // J5
|
||||
model.set_user_input(sheet, row, column - 1, "23".to_string());
|
||||
model.set_user_input(sheet, row, column, "42".to_string());
|
||||
model.set_user_input(sheet, row, column + 1, "33".to_string());
|
||||
model.set_user_input(sheet, row, column + 2, "=K5*A5".to_string());
|
||||
|
||||
model.set_user_input(sheet, row, column + 20, "=K5*A5".to_string());
|
||||
model.evaluate();
|
||||
|
||||
model
|
||||
.delete_cells_and_shift_left(sheet, row, column, 1, 1)
|
||||
.unwrap();
|
||||
|
||||
model.evaluate();
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 1),
|
||||
Ok("=J5*A5".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_cell_content(0, row, column + 19),
|
||||
Ok("=J5*A5".to_string())
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::types::{Alignment, HorizontalAlignment, VerticalAlignment};
|
||||
|
||||
#[test]
|
||||
fn alignment_default() {
|
||||
let alignment = Alignment::default();
|
||||
assert_eq!(
|
||||
alignment,
|
||||
Alignment {
|
||||
horizontal: HorizontalAlignment::General,
|
||||
vertical: VerticalAlignment::Bottom,
|
||||
wrap_text: false
|
||||
}
|
||||
);
|
||||
|
||||
let s = serde_json::to_string(&alignment).unwrap();
|
||||
// defaults stringifies as an empty object
|
||||
assert_eq!(s, "{}");
|
||||
|
||||
let a: Alignment = serde_json::from_str("{}").unwrap();
|
||||
|
||||
assert_eq!(a, alignment)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
fn test_worksheet_dimension_empty_sheet() {
|
||||
let model = new_empty_model();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 1,
|
||||
min_column: 1,
|
||||
@@ -25,7 +25,7 @@ fn test_worksheet_dimension_single_cell() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("W11", "1");
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 11,
|
||||
min_column: 23,
|
||||
@@ -41,7 +41,7 @@ fn test_worksheet_dimension_single_cell_set_empty() {
|
||||
model._set("W11", "1");
|
||||
model.cell_clear_contents(0, 11, 23).unwrap();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 11,
|
||||
min_column: 23,
|
||||
@@ -57,7 +57,7 @@ fn test_worksheet_dimension_single_cell_deleted() {
|
||||
model._set("W11", "1");
|
||||
model.cell_clear_all(0, 11, 23).unwrap();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 1,
|
||||
min_column: 1,
|
||||
@@ -77,7 +77,7 @@ fn test_worksheet_dimension_multiple_cells() {
|
||||
model._set("B19", "1");
|
||||
model.cell_clear_all(0, 11, 23).unwrap();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 11,
|
||||
min_column: 2,
|
||||
@@ -91,7 +91,7 @@ fn test_worksheet_dimension_multiple_cells() {
|
||||
fn test_worksheet_dimension_progressive() {
|
||||
let mut model = new_empty_model();
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 1,
|
||||
min_column: 1,
|
||||
@@ -102,7 +102,7 @@ fn test_worksheet_dimension_progressive() {
|
||||
|
||||
model.set_user_input(0, 30, 50, "Hello World".to_string());
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 30,
|
||||
min_column: 50,
|
||||
@@ -113,7 +113,7 @@ fn test_worksheet_dimension_progressive() {
|
||||
|
||||
model.set_user_input(0, 10, 15, "Hello World".to_string());
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 10,
|
||||
min_column: 15,
|
||||
@@ -124,7 +124,7 @@ fn test_worksheet_dimension_progressive() {
|
||||
|
||||
model.set_user_input(0, 5, 25, "Hello World".to_string());
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 5,
|
||||
min_column: 15,
|
||||
@@ -135,7 +135,7 @@ fn test_worksheet_dimension_progressive() {
|
||||
|
||||
model.set_user_input(0, 10, 250, "Hello World".to_string());
|
||||
assert_eq!(
|
||||
model.workbook.worksheet(0).unwrap().get_dimension(),
|
||||
model.workbook.worksheet(0).unwrap().dimension(),
|
||||
WorksheetDimension {
|
||||
min_row: 5,
|
||||
min_column: 15,
|
||||
|
||||
@@ -5,7 +5,6 @@ mod test_evaluation;
|
||||
mod test_general;
|
||||
mod test_rename_sheet;
|
||||
mod test_row_column;
|
||||
mod test_shift_cells;
|
||||
mod test_styles;
|
||||
mod test_to_from_bytes;
|
||||
mod test_undo_redo;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::UserModel;
|
||||
|
||||
#[test]
|
||||
fn shift_cells_general() {
|
||||
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
|
||||
// some reference value in A1
|
||||
model.set_user_input(0, 1, 1, "42").unwrap();
|
||||
|
||||
// We put some values in row 5
|
||||
model.set_user_input(0, 5, 3, "=1 + 1").unwrap(); // C5
|
||||
model.set_user_input(0, 5, 7, "=C5*A1").unwrap();
|
||||
|
||||
// Insert one cell in C5 and push right
|
||||
model.insert_cells_and_shift_right(0, 5, 3, 1, 1).unwrap();
|
||||
|
||||
// C5 should now be empty
|
||||
assert_eq!(model.get_cell_content(0, 5, 3), Ok("".to_string()));
|
||||
|
||||
// D5 should have 2
|
||||
assert_eq!(model.get_cell_content(0, 5, 4), Ok("=1+1".to_string()));
|
||||
}
|
||||
@@ -144,18 +144,13 @@ fn basic_fill() {
|
||||
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, None);
|
||||
assert_eq!(style.fill.fg_color, None);
|
||||
|
||||
// bg_color
|
||||
model
|
||||
.update_range_style(&range, "fill.bg_color", "#F2F2F2")
|
||||
.unwrap();
|
||||
model
|
||||
.update_range_style(&range, "fill.fg_color", "#F3F4F5")
|
||||
.unwrap();
|
||||
let style = model.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned()));
|
||||
assert_eq!(style.fill.fg_color, Some("#F3F4F5".to_owned()));
|
||||
|
||||
let send_queue = model.flush_send_queue();
|
||||
|
||||
@@ -164,7 +159,6 @@ fn basic_fill() {
|
||||
|
||||
let style = model2.get_cell_style(0, 1, 1).unwrap();
|
||||
assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned()));
|
||||
assert_eq!(style.fill.fg_color, Some("#F3F4F5".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -177,15 +171,9 @@ fn fill_errors() {
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "fill.bg_color", "#FFF"),
|
||||
Err("Invalid color: '#FFF'.".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
model.update_range_style(&range, "fill.fg_color", "#FFF"),
|
||||
Err("Invalid color: '#FFF'.".to_string())
|
||||
);
|
||||
assert!(model
|
||||
.update_range_style(&range, "fill.bg_color", "#FFF")
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4,15 +4,37 @@ use std::{collections::HashMap, fmt::Display};
|
||||
|
||||
use crate::expressions::token::Error;
|
||||
|
||||
// Useful for `#[serde(default = "default_as_true")]`
|
||||
fn default_as_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_as_false() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// Useful for `#[serde(skip_serializing_if = "is_true")]`
|
||||
fn is_true(b: &bool) -> bool {
|
||||
*b
|
||||
}
|
||||
|
||||
fn is_false(b: &bool) -> bool {
|
||||
!*b
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
fn is_zero(num: &i32) -> bool {
|
||||
*num == 0
|
||||
}
|
||||
|
||||
fn is_default_alignment(o: &Option<Alignment>) -> bool {
|
||||
o.is_none() || *o == Some(Alignment::default())
|
||||
}
|
||||
|
||||
fn hashmap_is_empty(h: &HashMap<String, Table>) -> bool {
|
||||
h.values().len() == 0
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Metadata {
|
||||
pub application: String,
|
||||
pub app_version: String,
|
||||
@@ -22,13 +44,14 @@ pub struct Metadata {
|
||||
pub last_modified: String, //"2020-11-20T16:24:35"
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct WorkbookSettings {
|
||||
pub tz: String,
|
||||
pub locale: String,
|
||||
}
|
||||
/// An internal representation of an IronCalc Workbook
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Workbook {
|
||||
pub shared_strings: Vec<String>,
|
||||
pub defined_names: Vec<DefinedName>,
|
||||
@@ -37,14 +60,17 @@ pub struct Workbook {
|
||||
pub name: String,
|
||||
pub settings: WorkbookSettings,
|
||||
pub metadata: Metadata,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "hashmap_is_empty")]
|
||||
pub tables: HashMap<String, Table>,
|
||||
}
|
||||
|
||||
/// A defined name. The `sheet_id` is the sheet index in case the name is local
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct DefinedName {
|
||||
pub name: String,
|
||||
pub formula: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sheet_id: Option<u32>,
|
||||
}
|
||||
|
||||
@@ -54,7 +80,8 @@ pub struct DefinedName {
|
||||
/// * state:
|
||||
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
|
||||
/// hidden, veryHidden, visible
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SheetState {
|
||||
Visible,
|
||||
Hidden,
|
||||
@@ -71,16 +98,8 @@ impl Display for SheetState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Selection {
|
||||
pub is_selected: bool,
|
||||
pub row: i32,
|
||||
pub column: i32,
|
||||
pub range: [i32; 4],
|
||||
}
|
||||
|
||||
/// Internal representation of a worksheet Excel object
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Worksheet {
|
||||
pub dimension: String,
|
||||
pub cols: Vec<Col>,
|
||||
@@ -90,12 +109,16 @@ pub struct Worksheet {
|
||||
pub shared_formulas: Vec<String>,
|
||||
pub sheet_id: u32,
|
||||
pub state: SheetState,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub color: Option<String>,
|
||||
pub merge_cells: Vec<String>,
|
||||
pub comments: Vec<Comment>,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "is_zero")]
|
||||
pub frozen_rows: i32,
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "is_zero")]
|
||||
pub frozen_columns: i32,
|
||||
pub selection: Selection,
|
||||
}
|
||||
|
||||
/// Internal representation of Excel's sheet_data
|
||||
@@ -103,7 +126,7 @@ pub struct Worksheet {
|
||||
pub type SheetData = HashMap<i32, HashMap<i32, Cell>>;
|
||||
|
||||
// ECMA-376-1:2016 section 18.3.1.73
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Row {
|
||||
/// Row index
|
||||
pub r: i32,
|
||||
@@ -111,19 +134,23 @@ pub struct Row {
|
||||
pub custom_format: bool,
|
||||
pub custom_height: bool,
|
||||
pub s: i32,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
// ECMA-376-1:2016 section 18.3.1.13
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
|
||||
pub struct Col {
|
||||
// Column definitions are defined on ranges, unlike rows which store unique, per-row entries.
|
||||
/// First column affected by this record. Settings apply to column in \[min, max\] range.
|
||||
pub min: i32,
|
||||
/// Last column affected by this record. Settings apply to column in \[min, max\] range.
|
||||
pub max: i32,
|
||||
|
||||
pub width: f64,
|
||||
pub custom_width: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub style: Option<i32>,
|
||||
}
|
||||
|
||||
@@ -138,55 +165,32 @@ pub enum CellType {
|
||||
CompoundData = 128,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, Clone, PartialEq)]
|
||||
#[serde(tag = "t", deny_unknown_fields)]
|
||||
pub enum Cell {
|
||||
EmptyCell {
|
||||
s: i32,
|
||||
},
|
||||
|
||||
BooleanCell {
|
||||
v: bool,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
NumberCell {
|
||||
v: f64,
|
||||
s: i32,
|
||||
},
|
||||
#[serde(rename = "empty")]
|
||||
EmptyCell { s: i32 },
|
||||
#[serde(rename = "b")]
|
||||
BooleanCell { v: bool, s: i32 },
|
||||
#[serde(rename = "n")]
|
||||
NumberCell { v: f64, s: i32 },
|
||||
// Maybe we should not have this type. In Excel this is just a string
|
||||
ErrorCell {
|
||||
ei: Error,
|
||||
s: i32,
|
||||
},
|
||||
#[serde(rename = "e")]
|
||||
ErrorCell { ei: Error, s: i32 },
|
||||
// Always a shared string
|
||||
SharedString {
|
||||
si: i32,
|
||||
s: i32,
|
||||
},
|
||||
#[serde(rename = "s")]
|
||||
SharedString { si: i32, s: i32 },
|
||||
// Non evaluated Formula
|
||||
CellFormula {
|
||||
f: i32,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
CellFormulaBoolean {
|
||||
f: i32,
|
||||
v: bool,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
CellFormulaNumber {
|
||||
f: i32,
|
||||
v: f64,
|
||||
s: i32,
|
||||
},
|
||||
#[serde(rename = "u")]
|
||||
CellFormula { f: i32, s: i32 },
|
||||
#[serde(rename = "fb")]
|
||||
CellFormulaBoolean { f: i32, v: bool, s: i32 },
|
||||
#[serde(rename = "fn")]
|
||||
CellFormulaNumber { f: i32, v: f64, s: i32 },
|
||||
// always inline string
|
||||
CellFormulaString {
|
||||
f: i32,
|
||||
v: String,
|
||||
s: i32,
|
||||
},
|
||||
|
||||
#[serde(rename = "str")]
|
||||
CellFormulaString { f: i32, v: String, s: i32 },
|
||||
#[serde(rename = "fe")]
|
||||
CellFormulaError {
|
||||
f: i32,
|
||||
ei: Error,
|
||||
@@ -205,16 +209,17 @@ impl Default for Cell {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Comment {
|
||||
pub text: String,
|
||||
pub author_name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub author_id: Option<String>,
|
||||
pub cell_ref: String,
|
||||
}
|
||||
|
||||
// ECMA-376-1:2016 section 18.5.1.2
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Table {
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
@@ -222,24 +227,34 @@ pub struct Table {
|
||||
pub reference: String,
|
||||
pub totals_row_count: u32,
|
||||
pub header_row_count: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub header_row_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub totals_row_dxf_id: Option<u32>,
|
||||
pub columns: Vec<TableColumn>,
|
||||
pub style_info: TableStyleInfo,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub has_filters: bool,
|
||||
}
|
||||
|
||||
// totals_row_label vs totals_row_function might be mutually exclusive. Use an enum?
|
||||
// the totals_row_function is an enum not String methinks
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct TableColumn {
|
||||
pub id: u32,
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub totals_row_label: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub header_row_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub totals_row_dxf_id: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub totals_row_function: Option<String>,
|
||||
}
|
||||
|
||||
@@ -257,16 +272,25 @@ impl Default for TableColumn {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
||||
pub struct TableStyleInfo {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub show_first_column: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub show_last_column: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub show_row_stripes: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub show_column_stripes: bool,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Styles {
|
||||
pub num_fmts: Vec<NumFmt>,
|
||||
pub fonts: Vec<Font>,
|
||||
@@ -302,7 +326,7 @@ pub struct Style {
|
||||
pub quote_prefix: bool,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct NumFmt {
|
||||
pub num_fmt_id: i32,
|
||||
pub format_code: String,
|
||||
@@ -492,17 +516,29 @@ pub struct Alignment {
|
||||
pub wrap_text: bool,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct CellStyleXfs {
|
||||
pub num_fmt_id: i32,
|
||||
pub font_id: i32,
|
||||
pub fill_id: i32,
|
||||
pub border_id: i32,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_number_format: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_border: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_alignment: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_protection: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_font: bool,
|
||||
#[serde(default = "default_as_true")]
|
||||
#[serde(skip_serializing_if = "is_true")]
|
||||
pub apply_fill: bool,
|
||||
}
|
||||
|
||||
@@ -523,24 +559,39 @@ impl Default for CellStyleXfs {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
|
||||
pub struct CellXfs {
|
||||
pub xf_id: i32,
|
||||
pub num_fmt_id: i32,
|
||||
pub font_id: i32,
|
||||
pub fill_id: i32,
|
||||
pub border_id: i32,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_number_format: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_border: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_alignment: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_protection: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_font: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub apply_fill: bool,
|
||||
#[serde(default = "default_as_false")]
|
||||
#[serde(skip_serializing_if = "is_false")]
|
||||
pub quote_prefix: bool,
|
||||
#[serde(skip_serializing_if = "is_default_alignment")]
|
||||
pub alignment: Option<Alignment>,
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct CellStyles {
|
||||
pub name: String,
|
||||
pub xf_id: i32,
|
||||
|
||||
@@ -91,36 +91,6 @@ enum Diff {
|
||||
column: i32,
|
||||
old_data: Box<ColumnData>,
|
||||
},
|
||||
InsertCellsShiftRight {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
},
|
||||
InsertCellsShiftDown {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
},
|
||||
DeleteCellsShiftLeft {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
old_data: Vec<Vec<Option<Cell>>>,
|
||||
},
|
||||
DeleteCellsShiftUp {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
old_data: Vec<Vec<Option<Cell>>>,
|
||||
},
|
||||
SetFrozenRowsCount {
|
||||
sheet: u32,
|
||||
new_value: i32,
|
||||
@@ -743,123 +713,6 @@ impl UserModel {
|
||||
self.model.delete_columns(sheet, column, 1)
|
||||
}
|
||||
|
||||
/// Insert cells in the area pushing the existing ones to the right
|
||||
///
|
||||
/// See also:
|
||||
/// * [Model::insert_cells_and_shift_right]
|
||||
pub fn insert_cells_and_shift_right(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let diff_list = vec![Diff::InsertCellsShiftRight {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model
|
||||
.insert_cells_and_shift_right(sheet, row, column, row_delta, column_delta)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert cells in the area pushing the existing ones down
|
||||
pub fn insert_cells_and_shift_down(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let diff_list = vec![Diff::InsertCellsShiftDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model
|
||||
.insert_cells_and_shift_down(sheet, row, column, row_delta, column_delta)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete cells in the specified area and then shift cells left to fill the gap.
|
||||
pub fn delete_cells_and_shift_left(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let mut old_data = Vec::new();
|
||||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||
for r in row..row + row_delta {
|
||||
let mut row_data = Vec::new();
|
||||
for c in column..column + column_delta {
|
||||
let cell = worksheet.cell(r, c);
|
||||
row_data.push(cell.cloned());
|
||||
}
|
||||
old_data.push(row_data);
|
||||
}
|
||||
let diff_list = vec![Diff::DeleteCellsShiftLeft {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model
|
||||
.delete_cells_and_shift_left(sheet, row, column, row_delta, column_delta)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete cells in the specified area and then shift cells upward to fill the gap.
|
||||
pub fn delete_cells_and_shift_up(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
row_delta: i32,
|
||||
column_delta: i32,
|
||||
) -> Result<(), String> {
|
||||
let mut old_data = Vec::new();
|
||||
let worksheet = self.model.workbook.worksheet(sheet)?;
|
||||
for r in row..row + row_delta {
|
||||
let mut row_data = Vec::new();
|
||||
for c in column..column + column_delta {
|
||||
let cell = worksheet.cell(r, c);
|
||||
row_data.push(cell.cloned());
|
||||
}
|
||||
old_data.push(row_data);
|
||||
}
|
||||
let diff_list = vec![Diff::DeleteCellsShiftUp {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data,
|
||||
}];
|
||||
self.push_diff_list(diff_list);
|
||||
self.model
|
||||
.delete_cells_and_shift_up(sheet, row, column, row_delta, column_delta)?;
|
||||
self.model.evaluate();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the width of a column
|
||||
///
|
||||
/// See also:
|
||||
@@ -996,7 +849,7 @@ impl UserModel {
|
||||
style.fill.fg_color = color(value)?;
|
||||
}
|
||||
"num_fmt" => {
|
||||
value.clone_into(&mut style.num_fmt);
|
||||
style.num_fmt = value.to_owned();
|
||||
}
|
||||
"border.left" => {
|
||||
style.border.left = border(value)?;
|
||||
@@ -1245,94 +1098,6 @@ impl UserModel {
|
||||
} => {
|
||||
self.model.set_sheet_color(*index, old_value)?;
|
||||
}
|
||||
Diff::InsertCellsShiftRight {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
self.model.delete_cells_and_shift_left(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
}
|
||||
Diff::InsertCellsShiftDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
self.model.delete_cells_and_shift_up(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
}
|
||||
Diff::DeleteCellsShiftLeft {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
// Sets old data
|
||||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||||
for r in *row..*row + *row_delta {
|
||||
for c in *column..*column + *column_delta {
|
||||
if let Some(cell) = &old_data[r as usize][c as usize] {
|
||||
worksheet.update_cell(r, c, cell.clone());
|
||||
} else {
|
||||
worksheet.cell_clear_contents(r, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.model.insert_cells_and_shift_right(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
}
|
||||
Diff::DeleteCellsShiftUp {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data,
|
||||
} => {
|
||||
needs_evaluation = true;
|
||||
// Sets old data
|
||||
let worksheet = self.model.workbook.worksheet_mut(*sheet)?;
|
||||
for r in *row..*row + *row_delta {
|
||||
for c in *column..*column + *column_delta {
|
||||
if let Some(cell) = &old_data[r as usize][c as usize] {
|
||||
worksheet.update_cell(r, c, cell.clone());
|
||||
} else {
|
||||
worksheet.cell_clear_contents(r, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.model.insert_cells_and_shift_down(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if needs_evaluation {
|
||||
@@ -1453,72 +1218,6 @@ impl UserModel {
|
||||
} => {
|
||||
self.model.set_sheet_color(*index, new_value)?;
|
||||
}
|
||||
Diff::InsertCellsShiftRight {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
} => {
|
||||
self.model.insert_cells_and_shift_right(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::InsertCellsShiftDown {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
} => {
|
||||
self.model.insert_cells_and_shift_down(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::DeleteCellsShiftLeft {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data: _,
|
||||
} => {
|
||||
self.model.delete_cells_and_shift_left(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
Diff::DeleteCellsShiftUp {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
row_delta,
|
||||
column_delta,
|
||||
old_data: _,
|
||||
} => {
|
||||
self.model.delete_cells_and_shift_up(
|
||||
*sheet,
|
||||
*row,
|
||||
*column,
|
||||
*row_delta,
|
||||
*column_delta,
|
||||
)?;
|
||||
needs_evaluation = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -394,7 +394,7 @@ impl Worksheet {
|
||||
}
|
||||
|
||||
/// Calculates dimension of the sheet. This function isn't cheap to calculate.
|
||||
pub fn get_dimension(&self) -> WorksheetDimension {
|
||||
pub fn dimension(&self) -> WorksheetDimension {
|
||||
// FIXME: It's probably better to just track the size as operations happen.
|
||||
if self.sheet_data.is_empty() {
|
||||
return WorksheetDimension {
|
||||
|
||||
@@ -4,7 +4,7 @@ use wasm_bindgen::{
|
||||
};
|
||||
|
||||
use ironcalc_base::{
|
||||
expressions::{lexer::marked_token::get_tokens as tokenizer, types::Area},
|
||||
expressions::{lexer::util::get_tokens as tokenizer, types::Area},
|
||||
types::CellType,
|
||||
UserModel as BaseModel,
|
||||
};
|
||||
|
||||
3
webapp/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/*
|
||||
dist/*
|
||||
example.json
|
||||
19
webapp/.storybook/main.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-onboarding',
|
||||
'@storybook/addon-interactions',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
29
webapp/.storybook/preview.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Preview } from '@storybook/react';
|
||||
import i18n from '../src/i18n';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import React from 'react';
|
||||
|
||||
|
||||
const withI18next = (Story: any) => {
|
||||
return (
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Story />
|
||||
</I18nextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export const decorators = [withI18next];
|
||||
export default preview;
|
||||
21
webapp/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# IronCalc Web App
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
# Deploy
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
BIN
webapp/example.xlsx
Normal file
16
webapp/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- <meta name="theme-color" content="#1bb566"> -->
|
||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#F2994A" />
|
||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="black" />
|
||||
<title>Spreadsheet</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
15
webapp/jest.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Config } from "jest";
|
||||
// import {defaults} from 'jest-config';
|
||||
|
||||
const config: Config = {
|
||||
// testMatch:["**.jest.mjs"],
|
||||
moduleFileExtensions: ["js", "ts", "mts", "mjs"],
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
},
|
||||
moduleNameMapper: {
|
||||
"^@ironcalc/wasm$": "<rootDir>/node_modules/@ironcalc/nodejs/"
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
17153
webapp/package-lock.json
generated
Normal file
55
webapp/package.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"restore": "cp node_modules/@ironcalc/wasm/wasm_bg.wasm node_modules/.vite/deps/",
|
||||
"dev": "vite",
|
||||
"test": "jest",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@ironcalc/wasm": "file:../bindings/wasm/pkg",
|
||||
"@mui/material": "^5.15.15",
|
||||
"i18next": "^23.11.1",
|
||||
"lucide-react": "^0.292.0",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^13.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^7.6.17",
|
||||
"@storybook/addon-interactions": "^7.6.17",
|
||||
"@storybook/addon-links": "^7.6.17",
|
||||
"@storybook/addon-onboarding": "^1.0.11",
|
||||
"@storybook/blocks": "^7.5.3",
|
||||
"@storybook/react": "^7.5.3",
|
||||
"@storybook/react-vite": "^7.6.17",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/react": "^18.2.75",
|
||||
"@types/react-dom": "^18.2.24",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"eslint-plugin-storybook": "^0.6.15",
|
||||
"jest": "^29.7.0",
|
||||
"storybook": "^7.6.17",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
}
|
||||
}
|
||||
6
webapp/src/App.css
Normal file
@@ -0,0 +1,6 @@
|
||||
#root {
|
||||
position: absolute;
|
||||
inset: 10px;
|
||||
border: 1px solid #AAA;
|
||||
border-radius: 4px;
|
||||
}
|
||||
40
webapp/src/App.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import "./App.css";
|
||||
import Workbook from "./components/workbook";
|
||||
import "./i18n";
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
import init, { Model } from "@ironcalc/wasm";
|
||||
import { WorkbookState } from "./components/workbookState";
|
||||
import WorkbookContext from "./components/workbookContext";
|
||||
|
||||
function App() {
|
||||
const [model, setModel] = useState<Model | null>(null);
|
||||
const [workbookState, setWorkbookState] = useState<WorkbookState | null>(
|
||||
null
|
||||
);
|
||||
useEffect(() => {
|
||||
async function start() {
|
||||
await init();
|
||||
const model_bytes = new Uint8Array(await (await fetch("./example.ic")).arrayBuffer());
|
||||
const _model = Model.from_bytes(model_bytes);
|
||||
// const _model = new Model("en", "UTC");
|
||||
if (!model) setModel(_model);
|
||||
if (!workbookState) setWorkbookState(new WorkbookState());
|
||||
}
|
||||
start();
|
||||
}, []);
|
||||
|
||||
if (!model || !workbookState) {
|
||||
return <div>Loading</div>;
|
||||
}
|
||||
|
||||
// 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.
|
||||
return (
|
||||
// <WorkbookContext.Provider value={{}}>
|
||||
<Workbook model={model} workbookState={workbookState} />
|
||||
// </WorkbookContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
12
webapp/src/components/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Keyboard and mouse events architecture
|
||||
|
||||
This document describes the architecture of the keyboard navigation and mouse events in IronCalc Web
|
||||
|
||||
There are two modes for mouse events:
|
||||
|
||||
* Normal mode: clicking a cell selects it, clicking on a sheet opens it
|
||||
* Browse mode: clicking on a cell updates the formula, etc
|
||||
|
||||
While in browse mode some mouse events might end the browse mode
|
||||
|
||||
We follow Excel's way of navigating a spreadsheet
|
||||
25
webapp/src/components/WorksheetCanvas/constants.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export const headerCornerBackground = '#FFF';
|
||||
export const headerTextColor = '#333';
|
||||
export const headerBackground = '#FFF';
|
||||
export const headerGlobalSelectorColor = '#EAECF4';
|
||||
export const headerSelectedBackground = '#EEEEEE';
|
||||
export const headerFullSelectedBackground = '#D3D6E9';
|
||||
export const headerSelectedColor = '#333';
|
||||
export const headerBorderColor = '#DEE0EF';
|
||||
|
||||
export const gridColor = '#D3D6E9';
|
||||
export const gridSeparatorColor = '#D3D6E9';
|
||||
export const defaultTextColor = '#2E414D';
|
||||
|
||||
export const outlineColor = '#F2994A';
|
||||
export const outlineBackgroundColor = '#F2994A1A';
|
||||
|
||||
export const LAST_COLUMN = 16_384;
|
||||
export const LAST_ROW = 1_048_576;
|
||||
|
||||
// FIXME: Browsers cannot have a height that big
|
||||
// For now we will go A-IZ and 10_000 rows
|
||||
export const lastColumn = 260; // TODO: Excel supports up to 16_384
|
||||
// I know of a world with one million moons.
|
||||
// Carl Sagan in The cosmic connection Chapter 7 "Space Exploration as a Human Enterprise"
|
||||
export const lastRow = 10_000; // TODO: Excel supports up to 1_048_576
|
||||
23
webapp/src/components/WorksheetCanvas/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface Cell {
|
||||
row: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export interface Area {
|
||||
rowStart: number;
|
||||
rowEnd: number;
|
||||
columnStart: number;
|
||||
columnEnd: number;
|
||||
}
|
||||
|
||||
export interface SheetArea extends Area {
|
||||
sheet: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface AreaWithBorderInterface extends Area {
|
||||
border: "left" | "top" | "right" | "bottom";
|
||||
}
|
||||
|
||||
export type AreaWithBorder = AreaWithBorderInterface | null;
|
||||
|
||||
396
webapp/src/components/WorksheetCanvas/util.ts
Normal file
@@ -0,0 +1,396 @@
|
||||
const letters = [
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
'K',
|
||||
'L',
|
||||
'M',
|
||||
'N',
|
||||
'O',
|
||||
'P',
|
||||
'Q',
|
||||
'R',
|
||||
'S',
|
||||
'T',
|
||||
'U',
|
||||
'V',
|
||||
'W',
|
||||
'X',
|
||||
'Y',
|
||||
'Z',
|
||||
];
|
||||
interface Reference {
|
||||
row: number;
|
||||
column: number;
|
||||
absoluteRow: boolean;
|
||||
absoluteColumn: boolean;
|
||||
}
|
||||
|
||||
export function referenceToString(rf: Reference): string {
|
||||
const absC = rf.absoluteColumn ? '$' : '';
|
||||
const absR = rf.absoluteRow ? '$' : '';
|
||||
return absC + columnNameFromNumber(rf.column) + absR + rf.row;
|
||||
}
|
||||
|
||||
export function columnNameFromNumber(column: number): string {
|
||||
let columnName = '';
|
||||
let index = column;
|
||||
while (index > 0) {
|
||||
columnName = `${letters[(index - 1) % 26]}${columnName}`;
|
||||
index = Math.floor((index - 1) / 26);
|
||||
}
|
||||
return columnName;
|
||||
}
|
||||
|
||||
export function columnNumberFromName(columnName: string): number {
|
||||
let column = 0;
|
||||
for (const character of columnName) {
|
||||
const index = (character.codePointAt(0) ?? 0) - 64;
|
||||
column = column * 26 + index;
|
||||
}
|
||||
return column;
|
||||
}
|
||||
|
||||
// EqualTo Color Palette
|
||||
export function getColor(index: number, alpha = 1): string {
|
||||
const colors = [
|
||||
{
|
||||
name: 'Cyan',
|
||||
rgba: [89, 185, 188, 1],
|
||||
hex: '#59B9BC',
|
||||
},
|
||||
{
|
||||
name: 'Flamingo',
|
||||
rgba: [236, 87, 83, 1],
|
||||
hex: '#EC5753',
|
||||
},
|
||||
{
|
||||
hex: '#3358B7',
|
||||
rgba: [51, 88, 183, 1],
|
||||
name: 'Blue',
|
||||
},
|
||||
{
|
||||
hex: '#F8CD3C',
|
||||
rgba: [248, 205, 60, 1],
|
||||
name: 'Yellow',
|
||||
},
|
||||
{
|
||||
hex: '#3BB68A',
|
||||
rgba: [59, 182, 138, 1],
|
||||
name: 'Emerald',
|
||||
},
|
||||
{
|
||||
hex: '#523E93',
|
||||
rgba: [82, 62, 147, 1],
|
||||
name: 'Violet',
|
||||
},
|
||||
{
|
||||
hex: '#A23C52',
|
||||
rgba: [162, 60, 82, 1],
|
||||
name: 'Burgundy',
|
||||
},
|
||||
{
|
||||
hex: '#8CB354',
|
||||
rgba: [162, 60, 82, 1],
|
||||
name: 'Wasabi',
|
||||
},
|
||||
{
|
||||
hex: '#D03627',
|
||||
rgba: [208, 54, 39, 1],
|
||||
name: 'Red',
|
||||
},
|
||||
{
|
||||
hex: '#1B717E',
|
||||
rgba: [27, 113, 126, 1],
|
||||
name: 'Teal',
|
||||
},
|
||||
];
|
||||
if (alpha === 1) {
|
||||
return colors[index % 10].hex;
|
||||
}
|
||||
const { rgba } = colors[index % 10];
|
||||
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${alpha})`;
|
||||
}
|
||||
|
||||
export function mergedAreas(area1: Area, area2: Area): Area {
|
||||
return {
|
||||
rowStart: Math.min(area1.rowStart, area2.rowStart, area1.rowEnd, area2.rowEnd),
|
||||
rowEnd: Math.max(area1.rowStart, area2.rowStart, area1.rowEnd, area2.rowEnd),
|
||||
columnStart: Math.min(area1.columnStart, area2.columnStart, area1.columnEnd, area2.columnEnd),
|
||||
columnEnd: Math.max(area1.columnStart, area2.columnStart, area1.columnEnd, area2.columnEnd),
|
||||
};
|
||||
}
|
||||
|
||||
export function getExpandToArea(area: Area, cell: Cell): AreaWithBorder {
|
||||
let { rowStart, rowEnd, columnStart, columnEnd } = area;
|
||||
if (rowStart > rowEnd) {
|
||||
[rowStart, rowEnd] = [rowEnd, rowStart];
|
||||
}
|
||||
if (columnStart > columnEnd) {
|
||||
[columnStart, columnEnd] = [columnEnd, columnStart];
|
||||
}
|
||||
const { row, column } = cell;
|
||||
if (row <= rowEnd && row >= rowStart && column >= columnStart && column <= columnEnd) {
|
||||
return null;
|
||||
}
|
||||
// Two rules:
|
||||
// * The extendTo area must be larger than the selected area
|
||||
// * The extendTo area must be of the same width or the same height as the selected area
|
||||
if (row >= rowEnd && column >= columnStart) {
|
||||
// Normal case: we are expanding down and right
|
||||
if (row - rowEnd > column - columnEnd) {
|
||||
// Expanding by rows (down)
|
||||
return {
|
||||
rowStart: rowEnd + 1,
|
||||
rowEnd: row,
|
||||
columnStart,
|
||||
columnEnd,
|
||||
border: 'top',
|
||||
};
|
||||
}
|
||||
// expanding by columns (right)
|
||||
return {
|
||||
rowStart,
|
||||
rowEnd,
|
||||
columnStart: columnEnd + 1,
|
||||
columnEnd: column,
|
||||
border: 'left',
|
||||
};
|
||||
}
|
||||
if (row >= rowEnd && column <= columnStart) {
|
||||
// We are expanding down and left
|
||||
if (row - rowEnd > columnStart - column) {
|
||||
// Expanding by rows (down)
|
||||
return {
|
||||
rowStart: rowEnd + 1,
|
||||
rowEnd: row,
|
||||
columnStart,
|
||||
columnEnd,
|
||||
border: 'top',
|
||||
};
|
||||
}
|
||||
// Expanding by columns (left)
|
||||
return {
|
||||
rowStart,
|
||||
rowEnd,
|
||||
columnStart: column,
|
||||
columnEnd: columnStart - 1,
|
||||
border: 'right',
|
||||
};
|
||||
}
|
||||
if (row <= rowEnd && column >= columnEnd) {
|
||||
// We are expanding up and right
|
||||
if (rowStart - row > column - columnEnd) {
|
||||
// Expanding by rows (up)
|
||||
return {
|
||||
rowStart: row,
|
||||
rowEnd: rowStart - 1,
|
||||
columnStart,
|
||||
columnEnd,
|
||||
border: 'bottom',
|
||||
};
|
||||
}
|
||||
// Expanding by columns (right)
|
||||
return {
|
||||
rowStart,
|
||||
rowEnd,
|
||||
columnStart: columnEnd + 1,
|
||||
columnEnd: column,
|
||||
border: 'left',
|
||||
};
|
||||
}
|
||||
if (row <= rowEnd && column <= columnStart) {
|
||||
// We are expanding up and left
|
||||
if (rowStart - row > columnStart - column) {
|
||||
// Expanding by rows (up)
|
||||
return {
|
||||
rowStart: row,
|
||||
rowEnd: rowStart - 1,
|
||||
columnStart,
|
||||
columnEnd,
|
||||
border: 'bottom',
|
||||
};
|
||||
}
|
||||
// Expanding by columns (left)
|
||||
return {
|
||||
rowStart,
|
||||
rowEnd,
|
||||
columnStart: column,
|
||||
columnEnd: columnStart - 1,
|
||||
border: 'right',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the keypress should start editing
|
||||
*/
|
||||
export function isEditingKey(key: string): boolean {
|
||||
if (key.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
const code = key.codePointAt(0) ?? 0;
|
||||
if (code > 0 && code < 255) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// / Common types
|
||||
|
||||
export interface Area {
|
||||
rowStart: number;
|
||||
rowEnd: number;
|
||||
columnStart: number;
|
||||
columnEnd: number;
|
||||
}
|
||||
|
||||
interface AreaWithBorderInterface extends Area {
|
||||
border: 'left' | 'top' | 'right' | 'bottom';
|
||||
}
|
||||
|
||||
export type AreaWithBorder = AreaWithBorderInterface | null;
|
||||
|
||||
export interface Cell {
|
||||
row: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export interface ScrollPosition {
|
||||
left: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
export interface StateSettings {
|
||||
selectedCell: Cell;
|
||||
selectedArea: Area;
|
||||
scrollPosition: ScrollPosition;
|
||||
extendToArea: AreaWithBorder;
|
||||
}
|
||||
|
||||
export type Dispatch<A> = (value: A) => void;
|
||||
export type SetStateAction<S> = S | ((prevState: S) => S);
|
||||
|
||||
export enum FocusType {
|
||||
Cell = 'cell',
|
||||
FormulaBar = 'formula-bar',
|
||||
}
|
||||
|
||||
/**
|
||||
* In Excel there are two "modes" of editing
|
||||
* * `init`: When you start typing in a cell. In this mode arrow keys will move away from the cell
|
||||
* * `edit`: If you double click on a cell or click in the cell while editing.
|
||||
* In this mode arrow keys will move within the cell.
|
||||
*
|
||||
* In a formula bar mode is always `edit`.
|
||||
*/
|
||||
export type CellEditMode = 'init' | 'edit';
|
||||
export interface CellEditingType {
|
||||
/**
|
||||
* ID of cell editing. Useful when one edit transforms into another and some code needs to run
|
||||
* when target changes.
|
||||
*
|
||||
* Due to problems with focus management (see #339) it's possible to start a new cell editing
|
||||
* without properly cleaning up previous one (lose focus in workbook, regain focus NOT in
|
||||
* the input and then use the keyboard.
|
||||
*/
|
||||
id: number;
|
||||
sheet: number;
|
||||
row: number;
|
||||
column: number;
|
||||
text: string;
|
||||
base: string;
|
||||
mode: CellEditMode;
|
||||
focus: FocusType;
|
||||
}
|
||||
|
||||
export type NavigationKey = 'ArrowRight' | 'ArrowLeft' | 'ArrowDown' | 'ArrowUp' | 'Home' | 'End';
|
||||
|
||||
export const isNavigationKey = (key: string): key is NavigationKey =>
|
||||
['ArrowRight', 'ArrowLeft', 'ArrowDown', 'ArrowUp', 'Home', 'End'].includes(key);
|
||||
|
||||
function nameNeedsQuoting(name: string): boolean {
|
||||
const chars = [' ', '(', ')', "'", '$', ',', ';', '-', '+', '{', '}'];
|
||||
const l = chars.length;
|
||||
for (let index = 0; index < l; index += 1) {
|
||||
if (name.includes(chars[index])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// FIXME: We should use the function of a similar name in the rust code.
|
||||
export const quoteSheetName = (name: string): string => {
|
||||
if (nameNeedsQuoting(name)) {
|
||||
return `'${name.replace("'", "''")}'`;
|
||||
}
|
||||
return name;
|
||||
};
|
||||
|
||||
export function cellReprToRowColumn(cellRepr: string): { row: number; column: number } {
|
||||
let row = 0;
|
||||
let column = 0;
|
||||
for (const character of cellRepr) {
|
||||
if (Number.isNaN(Number.parseInt(character, 10))) {
|
||||
column *= 26;
|
||||
const characterCode = character.codePointAt(0);
|
||||
const ACharacterCode = 'A'.codePointAt(0);
|
||||
if (typeof characterCode === 'undefined' || typeof ACharacterCode === 'undefined') {
|
||||
throw new TypeError('Failed to find character code');
|
||||
}
|
||||
const deltaCodes = characterCode - ACharacterCode;
|
||||
if (deltaCodes < 0) {
|
||||
throw new Error('Incorrect character');
|
||||
}
|
||||
column += deltaCodes + 1;
|
||||
} else {
|
||||
row *= 10;
|
||||
row += Number.parseInt(character, 10);
|
||||
}
|
||||
}
|
||||
return { row, column };
|
||||
}
|
||||
|
||||
export const getMessageCellText = (
|
||||
cell: string,
|
||||
getMessageSheetNumber: (sheet: string) => number | undefined,
|
||||
getCellText?: (sheet: number, row: number, column: number) => string | undefined,
|
||||
) => {
|
||||
const messageMatch = /^=?(?<sheet>\w+)!(?<cell>\w+)/.exec(cell);
|
||||
if (messageMatch && messageMatch.groups) {
|
||||
const messageSheet = getMessageSheetNumber(messageMatch.groups.sheet);
|
||||
const dynamicIconCell = cellReprToRowColumn(messageMatch.groups.cell);
|
||||
if (messageSheet !== undefined && getCellText) {
|
||||
return getCellText(messageSheet, dynamicIconCell.row, dynamicIconCell.column) || '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getCellAddress = (selectedArea: Area, selectedCell?: Cell) => {
|
||||
const isSingleCell =
|
||||
selectedArea.rowStart === selectedArea.rowEnd &&
|
||||
selectedArea.columnEnd === selectedArea.columnStart;
|
||||
|
||||
return isSingleCell && selectedCell
|
||||
? `${columnNameFromNumber(selectedCell.column)}${selectedCell.row}`
|
||||
: `${columnNameFromNumber(selectedArea.columnStart)}${
|
||||
selectedArea.rowStart
|
||||
}:${columnNameFromNumber(selectedArea.columnEnd)}${selectedArea.rowEnd}`;
|
||||
};
|
||||
|
||||
export enum Border {
|
||||
Top = 'top',
|
||||
Bottom = 'bottom',
|
||||
Right = 'right',
|
||||
Left = 'left',
|
||||
}
|
||||
1328
webapp/src/components/WorksheetCanvas/worksheetCanvas.ts
Normal file
566
webapp/src/components/borderPicker.tsx
Normal file
@@ -0,0 +1,566 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
BorderBottomIcon,
|
||||
BorderCenterHIcon,
|
||||
BorderCenterVIcon,
|
||||
BorderInnerIcon,
|
||||
BorderLeftIcon,
|
||||
BorderOuterIcon,
|
||||
BorderRightIcon,
|
||||
BorderTopIcon,
|
||||
BorderNoneIcon,
|
||||
BorderStyleIcon,
|
||||
} from "../icons";
|
||||
import ColorPicker from "./colorPicker";
|
||||
import Popover, { PopoverOrigin } from "@mui/material/Popover";
|
||||
import {
|
||||
Check,
|
||||
ChevronDown,
|
||||
Grid2X2 as BorderAllIcon,
|
||||
PencilLine,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
import { styled } from "@mui/material/styles";
|
||||
import { theme } from "../theme";
|
||||
import { BorderOptions, BorderStyle, BorderType } from "@ironcalc/wasm";
|
||||
|
||||
type BorderPickerProps = {
|
||||
className?: string;
|
||||
onChange: (border: BorderOptions) => void;
|
||||
anchorEl: React.RefObject<HTMLElement>;
|
||||
anchorOrigin?: PopoverOrigin;
|
||||
transformOrigin?: PopoverOrigin;
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
const BorderPicker = (properties: BorderPickerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [borderSelected, setBorderSelected] = useState(BorderType.None);
|
||||
const [borderColor, setBorderColor] = useState("#000000");
|
||||
const [borderStyle, setBorderStyle] = useState(BorderStyle.Thin);
|
||||
const [colorPickerOpen, setColorPickerOpen] = useState(false);
|
||||
const [stylePickerOpen, setStylePickerOpen] = useState(false);
|
||||
const closePicker = (): void => {
|
||||
properties.onChange({
|
||||
color: borderColor,
|
||||
style: borderStyle,
|
||||
border: borderSelected,
|
||||
});
|
||||
};
|
||||
const borderColorButton = useRef(null);
|
||||
const borderStyleButton = useRef(null);
|
||||
return (
|
||||
<>
|
||||
<StyledPopover
|
||||
open={properties.open}
|
||||
onClose={(): void => closePicker()}
|
||||
anchorEl={properties.anchorEl.current}
|
||||
anchorOrigin={
|
||||
properties.anchorOrigin || { vertical: "bottom", horizontal: "left" }
|
||||
}
|
||||
transformOrigin={
|
||||
properties.transformOrigin || { vertical: "top", horizontal: "left" }
|
||||
}
|
||||
>
|
||||
<BorderPickerDialog>
|
||||
<Borders>
|
||||
<Line>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderAll}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderAll) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderAll);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderAllIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderInner}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderInner) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderInner);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderInnerIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderCenterH}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderCenterH) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderCenterH);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderCenterHIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderCenterV}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderCenterV) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderCenterV);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderCenterVIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderOuter}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderOuter) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderOuter);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderOuterIcon />
|
||||
</Button>
|
||||
</Line>
|
||||
<Line>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderNone}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderNone) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderNone);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderNoneIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderTop}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderTop) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderTop);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderTopIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderRight}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderRight) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderRight);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderRightIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderBottom}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderBottom) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderBottom);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderBottomIcon />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={borderSelected === BorderType.BorderLeft}
|
||||
onClick={() => {
|
||||
if (borderSelected === BorderType.BorderLeft) {
|
||||
setBorderSelected(BorderType.None);
|
||||
} else {
|
||||
setBorderSelected(BorderType.BorderLeft);
|
||||
}
|
||||
}}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderLeftIcon />
|
||||
</Button>
|
||||
</Line>
|
||||
</Borders>
|
||||
<Divider />
|
||||
<Styles>
|
||||
<ButtonWrapper onClick={() => setColorPickerOpen(true)}>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={false}
|
||||
disabled={false}
|
||||
ref={borderColorButton}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<PencilLine />
|
||||
</Button>
|
||||
<div style={{flexGrow:2}}>Border color</div>
|
||||
<ChevronRightStyled />
|
||||
</ButtonWrapper>
|
||||
<ButtonWrapper onClick={() => setStylePickerOpen(true)} ref={borderStyleButton}>
|
||||
<Button
|
||||
type="button"
|
||||
$pressed={false}
|
||||
disabled={false}
|
||||
title={t("workbook.toolbar.borders_button_title")}
|
||||
>
|
||||
<BorderStyleIcon />
|
||||
</Button>
|
||||
<div style={{flexGrow:2}}>Border style</div>
|
||||
<ChevronRightStyled />
|
||||
</ButtonWrapper>
|
||||
</Styles>
|
||||
</BorderPickerDialog>
|
||||
<ColorPicker
|
||||
color={borderColor}
|
||||
onChange={(color): void => {
|
||||
setBorderColor(color);
|
||||
setColorPickerOpen(false);
|
||||
}}
|
||||
anchorEl={borderColorButton}
|
||||
open={colorPickerOpen}
|
||||
/>
|
||||
<StyledPopover
|
||||
open={stylePickerOpen}
|
||||
onClose={(): void => {
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
anchorEl={borderStyleButton.current}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: 38, horizontal: -6 }}
|
||||
>
|
||||
<BorderStyleDialog>
|
||||
<LineWrapper
|
||||
onClick={() => {
|
||||
setBorderStyle(BorderStyle.Dashed);
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
$checked={borderStyle === BorderStyle.None}
|
||||
>
|
||||
<BorderDescription>None</BorderDescription>
|
||||
<NoneLine />
|
||||
</LineWrapper>
|
||||
<LineWrapper
|
||||
onClick={() => {
|
||||
setBorderStyle(BorderStyle.Thin);
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
$checked={borderStyle === BorderStyle.Thin}
|
||||
>
|
||||
<BorderDescription>Thin</BorderDescription>
|
||||
<SolidLine />
|
||||
</LineWrapper>
|
||||
<LineWrapper
|
||||
onClick={() => {
|
||||
setBorderStyle(BorderStyle.Medium);
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
$checked={borderStyle === BorderStyle.Medium}
|
||||
>
|
||||
<BorderDescription>Medium</BorderDescription>
|
||||
<MediumLine />
|
||||
</LineWrapper>
|
||||
<LineWrapper
|
||||
onClick={() => {
|
||||
setBorderStyle(BorderStyle.Thick);
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
$checked={borderStyle === BorderStyle.Thick}
|
||||
>
|
||||
<BorderDescription>Thick</BorderDescription>
|
||||
<ThickLine />
|
||||
</LineWrapper>
|
||||
<LineWrapper
|
||||
onClick={() => {
|
||||
setBorderStyle(BorderStyle.Dotted);
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
$checked={borderStyle === BorderStyle.Dotted}
|
||||
>
|
||||
<BorderDescription>Dotted</BorderDescription>
|
||||
<DottedLine />
|
||||
</LineWrapper>
|
||||
<LineWrapper
|
||||
onClick={() => {
|
||||
setBorderStyle(BorderStyle.Dashed);
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
$checked={borderStyle === BorderStyle.Dashed}
|
||||
>
|
||||
<BorderDescription>Dashed</BorderDescription>
|
||||
<DashedLine />
|
||||
</LineWrapper>
|
||||
<LineWrapper
|
||||
onClick={() => {
|
||||
setBorderStyle(BorderStyle.Dashed);
|
||||
setStylePickerOpen(false);
|
||||
}}
|
||||
$checked={borderStyle === BorderStyle.Double}
|
||||
>
|
||||
<BorderDescription>Double</BorderDescription>
|
||||
<DoubleLine />
|
||||
</LineWrapper>
|
||||
</BorderStyleDialog>
|
||||
</StyledPopover>
|
||||
</StyledPopover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type LineWrapperProperties = { $checked: boolean };
|
||||
const LineWrapper = styled("div")<LineWrapperProperties>`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: ${({ $checked }): string => {
|
||||
if ($checked) {
|
||||
return '#EEEEEE;';
|
||||
} else {
|
||||
return 'inherit;';
|
||||
}
|
||||
}};
|
||||
&:hover {
|
||||
border: 1px solid #EEEEEE;
|
||||
}
|
||||
padding:8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
border: 1px solid white;
|
||||
`;
|
||||
|
||||
const CheckIconWrapper = styled("div")`
|
||||
width: 12px;
|
||||
`;
|
||||
|
||||
type CheckIconProperties = { $checked: boolean };
|
||||
const CheckIcon = styled("div")<CheckIconProperties>`
|
||||
width: 2px;
|
||||
background-color: #EEE;
|
||||
height: 28px;
|
||||
visibility: ${({ $checked }): string => {
|
||||
if ($checked) {
|
||||
return "visible";
|
||||
}
|
||||
return "hidden";
|
||||
}};
|
||||
`;
|
||||
const NoneLine = styled("div")`
|
||||
width: 68px;
|
||||
border-top: 1px solid #E0E0E0;
|
||||
`;
|
||||
const SolidLine = styled("div")`
|
||||
width: 68px;
|
||||
border-top: 1px solid #333333;
|
||||
`;
|
||||
const MediumLine = styled("div")`
|
||||
width: 68px;
|
||||
border-top: 2px solid #333333;
|
||||
`;
|
||||
const ThickLine = styled("div")`
|
||||
width: 68px;
|
||||
border-top: 3px solid #333333;
|
||||
`;
|
||||
const DashedLine = styled("div")`
|
||||
width: 68px;
|
||||
border-top: 1px dashed #333333;
|
||||
`;
|
||||
const DottedLine = styled("div")`
|
||||
width: 68px;
|
||||
border-top: 1px dotted #333333;
|
||||
`;
|
||||
const DoubleLine = styled('div')`
|
||||
width: 68px;
|
||||
border-top: 3px double #333333;
|
||||
`;
|
||||
|
||||
const Divider = styled("div")`
|
||||
display: inline-flex;
|
||||
heigh: 1px;
|
||||
border-bottom: 1px solid #EEE;
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
`;
|
||||
|
||||
const Borders = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 4px;
|
||||
`;
|
||||
|
||||
const Styles = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const Line = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const ButtonWrapper = styled("div")`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
background-color: #EEE;
|
||||
border-top-color: ${(): string => theme.palette.grey["400"]};
|
||||
}
|
||||
cursor: pointer;
|
||||
padding: 8px
|
||||
`;
|
||||
|
||||
const BorderStyleDialog = styled("div")`
|
||||
background: ${({ theme }): string => theme.palette.background.default};
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledPopover = styled(Popover)`
|
||||
.MuiPopover-paper {
|
||||
border-radius: 10px;
|
||||
border: 0px solid ${({ theme }): string => theme.palette.background.default};
|
||||
box-shadow: 1px 2px 8px rgba(139, 143, 173, 0.5);
|
||||
}
|
||||
.MuiPopover-padding {
|
||||
padding: 0px;
|
||||
}
|
||||
.MuiList-padding {
|
||||
padding: 0px;
|
||||
}
|
||||
font-family: ${({ theme }) => theme.typography.fontFamily};
|
||||
font-size: 13px;
|
||||
`;
|
||||
|
||||
const BorderPickerDialog = styled("div")`
|
||||
background: ${({ theme }): string => theme.palette.background.default};
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const BorderDescription = styled("div")`
|
||||
width: 70px;
|
||||
`;
|
||||
|
||||
// type TypeButtonProperties = { $pressed: boolean; $underlinedColor?: string };
|
||||
// const Button = styled.button<TypeButtonProperties>`
|
||||
// width: 23px;
|
||||
// height: 23px;
|
||||
// display: inline-flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
// font-size: 14px;
|
||||
// border-radius: 2px;
|
||||
// margin-right: 5px;
|
||||
// transition: all 0.2s;
|
||||
|
||||
// ${({ theme, disabled, $pressed, $underlinedColor }): string => {
|
||||
// if (disabled) {
|
||||
// return `
|
||||
// color: ${theme.palette.grey['600']};
|
||||
// cursor: default;
|
||||
// `;
|
||||
// }
|
||||
// return `
|
||||
// border-top: ${$underlinedColor ? '3px solid #FFF' : 'none'};
|
||||
// border-bottom: ${$underlinedColor ? `3px solid ${$underlinedColor}` : 'none'};
|
||||
// color: ${theme.palette.text.primary};
|
||||
// background-color: ${$pressed ? theme.palette.grey['600'] : '#FFF'};
|
||||
// &:hover {
|
||||
// background-color: ${theme.palette.grey['400']};
|
||||
// border-top-color: ${theme.palette.grey['400']};
|
||||
// }
|
||||
// `;
|
||||
// }}
|
||||
// `;
|
||||
|
||||
type TypeButtonProperties = { $pressed: boolean; $underlinedColor?: string };
|
||||
const Button = styled("button")<TypeButtonProperties>(
|
||||
({ disabled, $pressed, $underlinedColor }) => {
|
||||
let result: Record<string, any> = {
|
||||
width: "24px",
|
||||
height: "24px",
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
// fontSize: "26px",
|
||||
border: "0px solid #fff",
|
||||
borderRadius: "2px",
|
||||
marginRight: "5px",
|
||||
transition: "all 0.2s",
|
||||
cursor: "pointer",
|
||||
padding: "0px",
|
||||
};
|
||||
if (disabled) {
|
||||
result.color = theme.palette.grey["600"];
|
||||
result.cursor = "default";
|
||||
} else {
|
||||
result.borderTop = $underlinedColor ? "3px solid #FFF" : "none";
|
||||
result.borderBottom = $underlinedColor
|
||||
? `3px solid ${$underlinedColor}`
|
||||
: "none";
|
||||
(result.color = "#21243A"),
|
||||
(result.backgroundColor = $pressed
|
||||
? theme.palette.grey["600"]
|
||||
: "inherit");
|
||||
result["&:hover"] = {
|
||||
backgroundColor: "#F1F2F8",
|
||||
borderTopColor: "#F1F2F8",
|
||||
};
|
||||
result["svg"] = {
|
||||
width: "16px",
|
||||
height: "16px",
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
);
|
||||
|
||||
const ChevronRightStyled = styled(ChevronRight)`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
`;
|
||||
|
||||
export default BorderPicker;
|
||||
261
webapp/src/components/colorPicker.tsx
Normal file
@@ -0,0 +1,261 @@
|
||||
import styled from "@emotion/styled";
|
||||
import Popover, { PopoverOrigin } from "@mui/material/Popover";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
||||
import { theme } from "../theme";
|
||||
|
||||
type ColorPickerProps = {
|
||||
className?: string;
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
anchorEl: React.RefObject<HTMLElement>;
|
||||
anchorOrigin?: PopoverOrigin;
|
||||
transformOrigin?: PopoverOrigin;
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
const colorPickerWidth = 240;
|
||||
const colorPickerPadding = 15;
|
||||
const colorfulHeight = 185; // 150 + 15 + 20
|
||||
|
||||
const ColorPicker = (properties: ColorPickerProps) => {
|
||||
const [color, setColor] = useState<string>(properties.color);
|
||||
const recentColors = useRef<string[]>([]);
|
||||
|
||||
const closePicker = (newColor: string): void => {
|
||||
const maxRecentColors = 14;
|
||||
properties.onChange(newColor);
|
||||
const colors = recentColors.current.filter((c) => c !== newColor);
|
||||
recentColors.current = [newColor, ...colors].slice(0, maxRecentColors);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setColor(properties.color);
|
||||
}, [properties.color]);
|
||||
|
||||
const presetColors = [
|
||||
"#FFFFFF",
|
||||
"#1B717E",
|
||||
"#59B9BC",
|
||||
"#3BB68A",
|
||||
"#8CB354",
|
||||
"#F8CD3C",
|
||||
"#EC5753",
|
||||
"#A23C52",
|
||||
"#D03627",
|
||||
"#523E93",
|
||||
"#3358B7",
|
||||
];
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={properties.open}
|
||||
onClose={(): void => closePicker(color)}
|
||||
anchorEl={properties.anchorEl.current}
|
||||
anchorOrigin={
|
||||
properties.anchorOrigin || { vertical: "bottom", horizontal: "left" }
|
||||
}
|
||||
transformOrigin={
|
||||
properties.transformOrigin || { vertical: "top", horizontal: "left" }
|
||||
}
|
||||
>
|
||||
<ColorPickerDialog>
|
||||
<HexColorPicker
|
||||
color={color}
|
||||
onChange={(newColor): void => {
|
||||
setColor(newColor);
|
||||
}}
|
||||
/>
|
||||
<ColorPickerInput>
|
||||
<HexWrapper>
|
||||
<HexLabel>{"Hex"}</HexLabel>
|
||||
<HexColorInputBox>
|
||||
<HashLabel>{"#"}</HashLabel>
|
||||
<HexColorInput
|
||||
color={color}
|
||||
onChange={(newColor): void => {
|
||||
setColor(newColor);
|
||||
}}
|
||||
/>
|
||||
</HexColorInputBox>
|
||||
</HexWrapper>
|
||||
<Swatch $color={color} />
|
||||
</ColorPickerInput>
|
||||
<HorizontalDivider />
|
||||
<ColorList>
|
||||
{presetColors.map((presetColor) => (
|
||||
<Button
|
||||
key={presetColor}
|
||||
$color={presetColor}
|
||||
onClick={(): void => {
|
||||
closePicker(presetColor);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ColorList>
|
||||
<HorizontalDivider />
|
||||
<RecentLabel>{"Recent"}</RecentLabel>
|
||||
<ColorList>
|
||||
{recentColors.current.map((recentColor) => (
|
||||
<Button
|
||||
key={recentColor}
|
||||
$color={recentColor}
|
||||
onClick={(): void => {
|
||||
closePicker(recentColor);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ColorList>
|
||||
</ColorPickerDialog>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
const RecentLabel = styled.div`
|
||||
font-size: 12px;
|
||||
color: ${theme.palette.text.secondary};
|
||||
`;
|
||||
|
||||
const ColorList = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const Button = styled.button<{ $color: string }>`
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
${({ $color }): string => {
|
||||
if ($color.toUpperCase() === "#FFFFFF") {
|
||||
return `border: 1px solid ${theme.palette.grey['600']};`;
|
||||
}
|
||||
return `border: 1px solid ${$color};`;
|
||||
}}
|
||||
background-color: ${({ $color }): string => {
|
||||
return $color;
|
||||
}};
|
||||
box-sizing: border-box;
|
||||
margin-top: 10px;
|
||||
margin-right: 10px;
|
||||
border-radius: 2px;
|
||||
`;
|
||||
|
||||
const HorizontalDivider = styled.div`
|
||||
height: 0px;
|
||||
width: 100%;
|
||||
border-top: 1px solid ${theme.palette.grey['400']};
|
||||
margin-top: 15px;
|
||||
margin-bottom: 5px;
|
||||
`;
|
||||
|
||||
// const StyledPopover = styled(Popover)`
|
||||
// .MuiPopover-paper {
|
||||
// border-radius: 10px;
|
||||
// border: 0px solid ${theme.palette.background.default};
|
||||
// box-shadow: 1px 2px 8px rgba(139, 143, 173, 0.5);
|
||||
// }
|
||||
// .MuiPopover-padding {
|
||||
// padding: 0px;
|
||||
// }
|
||||
// .MuiList-padding {
|
||||
// padding: 0px;
|
||||
// }
|
||||
// `;
|
||||
|
||||
const ColorPickerDialog = styled.div`
|
||||
background: ${theme.palette.background.default};
|
||||
width: ${colorPickerWidth}px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& .react-colorful {
|
||||
height: ${colorfulHeight}px;
|
||||
width: ${colorPickerWidth - colorPickerPadding * 2}px;
|
||||
}
|
||||
& .react-colorful__saturation {
|
||||
border-bottom: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
& .react-colorful__hue {
|
||||
height: 20px;
|
||||
margin-top: 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
& .react-colorful__saturation-pointer {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
& .react-colorful__hue-pointer {
|
||||
width: 7px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
`;
|
||||
|
||||
const HashLabel = styled.div`
|
||||
margin: auto 0px auto 10px;
|
||||
font-size: 13px;
|
||||
color: #7d8ec2;
|
||||
font-family: ${theme.typography.button.fontFamily};
|
||||
`;
|
||||
|
||||
const HexLabel = styled.div`
|
||||
margin: auto 10px auto 0px;
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
font-family: ${theme.typography.button.fontFamily};
|
||||
`;
|
||||
|
||||
const HexColorInputBox = styled.div`
|
||||
display: inline-flex;
|
||||
flex-grow: 1;
|
||||
margin-right: 10px;
|
||||
width: 140px;
|
||||
height: 28px;
|
||||
border: 1px solid ${theme.palette.grey['600']};
|
||||
border-radius: 5px;
|
||||
`;
|
||||
|
||||
const HexWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
& input {
|
||||
min-width: 0px;
|
||||
border: 0px;
|
||||
background: ${theme.palette.background.default};
|
||||
outline: none;
|
||||
font-family: ${theme.typography.button.fontFamily};
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
& input:focus {
|
||||
border-color: #4298ef;
|
||||
}
|
||||
`;
|
||||
|
||||
const Swatch = styled.div<{ $color: string }>`
|
||||
display: inline-flex;
|
||||
${({ $color }): string => {
|
||||
if ($color.toUpperCase() === "#FFFFFF") {
|
||||
return `border: 1px solid ${theme.palette.grey['600']};`;
|
||||
}
|
||||
return `border: 1px solid ${$color};`;
|
||||
}}
|
||||
background-color: ${({ $color }): string => $color};
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 5px;
|
||||
`;
|
||||
|
||||
const ColorPickerInput = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
`;
|
||||
|
||||
export default ColorPicker;
|
||||
420
webapp/src/components/editor/editor.tsx
Normal file
@@ -0,0 +1,420 @@
|
||||
import {
|
||||
CSSProperties,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
KeyboardEvent,
|
||||
useContext,
|
||||
} from "react";
|
||||
import { useRef } from "react";
|
||||
import EditorContext, { Area } from "./editorContext";
|
||||
import { getStringRange } from "./util";
|
||||
|
||||
/**
|
||||
* This is the Cell Editor for IronCalc
|
||||
* I uses a transparent textarea and a styled mask. What you see is the HTML styled content of the mask
|
||||
* and the caret from the textarea. The alternative would be to have a 'contenteditable' div.
|
||||
* That turns out to be a much more difficult implementation.
|
||||
*
|
||||
* The editor grows horizontally with text if it fits in the screen.
|
||||
* If it doesn't fit, it wraps and grows vertically. If it doesn't fit vertically it scrolls.
|
||||
*
|
||||
* Many keyboard and mouse events are handled gracefully by the textarea in full or in part.
|
||||
* For example letter key strokes like 'q' or '1' are handled full by the textarea.
|
||||
* Some keyboard events like "RightArrow" might need to be handled separately and let them bubble up,
|
||||
* or might be handled by the textarea, depending on the "editor mode".
|
||||
* Some other like "Enter" we need to intercept and change the normal behaviour.
|
||||
*/
|
||||
|
||||
const commonCSS: CSSProperties = {
|
||||
fontWeight: "inherit",
|
||||
fontFamily: "inherit",
|
||||
fontSize: "inherit",
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
whiteSpace: "pre",
|
||||
width: "100%",
|
||||
padding: 0,
|
||||
};
|
||||
|
||||
interface Cell {
|
||||
sheet: number;
|
||||
row: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
interface EditorOptions {
|
||||
minimalWidth: number;
|
||||
minimalHeight: number;
|
||||
textColor: string;
|
||||
originalText: string;
|
||||
getStyledText: (
|
||||
text: string,
|
||||
insertRangeText: string
|
||||
) => {
|
||||
html: JSX.Element[];
|
||||
isInReferenceMode: boolean;
|
||||
};
|
||||
onEditEnd: (text: string) => void;
|
||||
display: boolean;
|
||||
cell: Cell;
|
||||
sheetNames: string[];
|
||||
}
|
||||
|
||||
// You can either be editing a formula or content.
|
||||
// When editing content (behaviour is common to Excel and Google Sheets):
|
||||
// * If you start editing by typing you are in *accept* mode
|
||||
// * If you start editing by F2 you are in *cruise* mode
|
||||
// * If you start editing by double click you are in *cruise* mode
|
||||
// In Google Sheets "Enter" starts editing and puts you in *cruise* mode. We do not do that
|
||||
// Once you are in cruise mode it is not possible to switch to accept mode
|
||||
// The only way to go from accept mode to cruise mode is clicking in the content somewhere
|
||||
|
||||
// When editing a formula.
|
||||
// In Google Sheets you are either in insert mode or cruise mode.
|
||||
// You can get back to accept mode if you delete the whole formula
|
||||
// In Excel you can be either in insert or accept but if you click in the formula body
|
||||
// you switch to cruise mode. Once in cruise mode you can go to insert mode by selecting a range.
|
||||
// Then you are back in accept/insert modes
|
||||
|
||||
const Editor = (options: EditorOptions) => {
|
||||
const {
|
||||
minimalWidth,
|
||||
minimalHeight,
|
||||
textColor,
|
||||
onEditEnd,
|
||||
originalText,
|
||||
display,
|
||||
cell,
|
||||
sheetNames,
|
||||
} = options;
|
||||
|
||||
const [width, setWidth] = useState(minimalWidth);
|
||||
const [height, setHeight] = useState(minimalHeight);
|
||||
|
||||
const { editorContext, setEditorContext } = useContext(EditorContext);
|
||||
|
||||
const setBaseText = (newText: string) => {
|
||||
console.log('Calling setBaseText');
|
||||
setEditorContext((c) => {
|
||||
return {
|
||||
...c,
|
||||
baseText: newText,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const insertRangeText = editorContext.insertRange
|
||||
? getStringRange(editorContext.insertRange, sheetNames)
|
||||
: "";
|
||||
|
||||
const baseText = editorContext.baseText;
|
||||
const text = baseText + insertRangeText;
|
||||
console.log('baseText', baseText, 'insertRange:', insertRangeText);
|
||||
|
||||
const formulaRef = useRef<HTMLDivElement>(null);
|
||||
const maskRef = useRef<HTMLDivElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// useEffect(() => {
|
||||
// setBaseText(originalText);
|
||||
// }, [cell]);
|
||||
|
||||
const { html: styledFormula, isInReferenceMode } = options.getStyledText(
|
||||
baseText,
|
||||
insertRangeText
|
||||
);
|
||||
|
||||
if (display && textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (formulaRef.current) {
|
||||
const scrollWidth = formulaRef.current.scrollWidth;
|
||||
if (scrollWidth > width) {
|
||||
setWidth(scrollWidth);
|
||||
} else if (scrollWidth <= minimalWidth) {
|
||||
setWidth(minimalWidth);
|
||||
}
|
||||
const scrollHeight = formulaRef.current.scrollHeight;
|
||||
if (scrollHeight > height) {
|
||||
setHeight(scrollHeight);
|
||||
}
|
||||
}
|
||||
}, [text]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isInReferenceMode) {
|
||||
setEditorContext((c) => {
|
||||
return {
|
||||
...c,
|
||||
mode: "insert",
|
||||
};
|
||||
});
|
||||
} else {
|
||||
setEditorContext((c) => {
|
||||
return {
|
||||
...c,
|
||||
mode: "cruise",
|
||||
insertRange: null,
|
||||
};
|
||||
});
|
||||
}
|
||||
}, [isInReferenceMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (display && textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
}
|
||||
}, [display]);
|
||||
|
||||
console.log("Ok, this is running", text, editorContext.id);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
const { key, shiftKey, altKey } = event;
|
||||
const textarea = textareaRef.current;
|
||||
const mode = editorContext.mode;
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
switch (key) {
|
||||
case "Enter": {
|
||||
if (altKey) {
|
||||
// new line
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const newText = text.slice(0, start) + "\n" + text.slice(end);
|
||||
setBaseText(newText);
|
||||
setTimeout(() => {
|
||||
textarea.setSelectionRange(start + 1, start + 1);
|
||||
}, 1);
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return;
|
||||
} else {
|
||||
// end edit
|
||||
onEditEnd(text);
|
||||
textarea.blur();
|
||||
// event bubbles up
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Escape": {
|
||||
setBaseText(originalText);
|
||||
textarea.blur();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
case "ArrowLeft": {
|
||||
if (mode === "accept") {
|
||||
onEditEnd(text);
|
||||
textarea.blur();
|
||||
// event bubbles up
|
||||
return;
|
||||
} else if (mode == "insert") {
|
||||
if (shiftKey) {
|
||||
// increase the inserted range to the left
|
||||
if (!editorContext.insertRange) {
|
||||
setEditorContext((c) => {
|
||||
return {
|
||||
...c,
|
||||
insertRange: {
|
||||
absoluteColumnEnd: false,
|
||||
absoluteColumnStart: false,
|
||||
absoluteRowEnd: false,
|
||||
absoluteRowStart: false,
|
||||
sheet: cell.sheet,
|
||||
rowStart: cell.row,
|
||||
rowEnd: cell.row,
|
||||
columnStart: cell.column,
|
||||
columnEnd: cell.column,
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// const r = insertRage;
|
||||
// r.columnStart = Math.max(r.columnStart - 1, 1);
|
||||
// setInsertRange(r);
|
||||
}
|
||||
} else {
|
||||
// move inserted cell to the left
|
||||
if (!editorContext.insertRange) {
|
||||
setEditorContext((c) => {
|
||||
return {
|
||||
...c,
|
||||
insertRange: {
|
||||
absoluteColumnEnd: false,
|
||||
absoluteColumnStart: false,
|
||||
absoluteRowEnd: false,
|
||||
absoluteRowStart: false,
|
||||
sheet: cell.sheet,
|
||||
rowStart: cell.row,
|
||||
rowEnd: cell.row,
|
||||
columnStart: cell.column,
|
||||
columnEnd: cell.column,
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
setEditorContext((c) => {
|
||||
const range = c.insertRange as Area;
|
||||
const row = range.rowStart;
|
||||
let column = range.columnStart - 1;
|
||||
if (column < 1) {
|
||||
column = 1;
|
||||
}
|
||||
return {
|
||||
...c,
|
||||
insertRange: {
|
||||
absoluteColumnEnd: false,
|
||||
absoluteColumnStart: false,
|
||||
absoluteRowEnd: false,
|
||||
absoluteRowStart: false,
|
||||
sheet: range.sheet,
|
||||
rowStart: row,
|
||||
rowEnd: row,
|
||||
columnStart: column,
|
||||
columnEnd: column,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
// We don't do anything in "cruise mode" and rely on the textarea default behaviour
|
||||
break;
|
||||
}
|
||||
case "ArrowDown": {
|
||||
if (mode === "accept") {
|
||||
onEditEnd(text);
|
||||
textarea.blur();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ArrowRight": {
|
||||
if (mode === "accept") {
|
||||
onEditEnd(text);
|
||||
textarea.blur();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "ArrowUp": {
|
||||
if (mode === "accept") {
|
||||
onEditEnd(text);
|
||||
textarea.blur();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Tab": {
|
||||
onEditEnd(text);
|
||||
textarea.blur();
|
||||
// event bubbles up
|
||||
}
|
||||
}
|
||||
if (editorContext.mode === "insert") {
|
||||
setBaseText(text);
|
||||
setEditorContext((context) => {
|
||||
return {
|
||||
...context,
|
||||
mode: "cruise",
|
||||
insertRange: null,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
[text, editorContext]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width,
|
||||
height,
|
||||
overflow: "hidden",
|
||||
background: "#FFF",
|
||||
display: display ? "block" : "none",
|
||||
}}
|
||||
onClick={(_event) => {
|
||||
console.log("Click on wrapper");
|
||||
}}
|
||||
onPointerDown={() => {
|
||||
console.log("On pointer down wrapper");
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={maskRef}
|
||||
style={{
|
||||
...commonCSS,
|
||||
textAlign: "left",
|
||||
pointerEvents: "none",
|
||||
height,
|
||||
}}
|
||||
onClick={(_event) => {
|
||||
console.log("Click on mask");
|
||||
}}
|
||||
onPointerDown={() => {
|
||||
console.log("On pointer down mask");
|
||||
}}
|
||||
>
|
||||
<div ref={formulaRef}>{styledFormula}</div>
|
||||
</div>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
style={{
|
||||
...commonCSS,
|
||||
color: "transparent",
|
||||
backgroundColor: "transparent",
|
||||
caretColor: textColor,
|
||||
outline: "none",
|
||||
resize: "none",
|
||||
border: "none",
|
||||
height,
|
||||
}}
|
||||
spellCheck="false"
|
||||
value={text}
|
||||
onChange={(event) => {
|
||||
console.log("onChange", event.target.value);
|
||||
setBaseText(event.target.value);
|
||||
}}
|
||||
onScroll={() => {
|
||||
if (maskRef.current && textareaRef.current) {
|
||||
maskRef.current.style.left = `-${textareaRef.current.scrollLeft}px`;
|
||||
maskRef.current.style.top = `-${textareaRef.current.scrollTop}px`;
|
||||
}
|
||||
}}
|
||||
onKeyDown={onKeyDown}
|
||||
onClick={(event) => {
|
||||
console.log("Setting mode");
|
||||
setEditorContext((c) => {
|
||||
return {
|
||||
...c,
|
||||
mode: "cruise",
|
||||
};
|
||||
});
|
||||
console.log("here");
|
||||
// if (display) {
|
||||
event.stopPropagation();
|
||||
// }
|
||||
}}
|
||||
onBlur={() => {
|
||||
// on blur
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
></textarea>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
45
webapp/src/components/editor/editorContext.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Dispatch, SetStateAction, createContext } from "react";
|
||||
|
||||
export interface Area {
|
||||
sheet: number | null;
|
||||
rowStart: number;
|
||||
rowEnd: number;
|
||||
columnStart: number;
|
||||
columnEnd: number;
|
||||
absoluteRowStart: boolean;
|
||||
absoluteRowEnd: boolean;
|
||||
absoluteColumnStart: boolean;
|
||||
absoluteColumnEnd: boolean;
|
||||
}
|
||||
|
||||
// Arrow keys behave in different ways depending on the "edit mode":
|
||||
// * In _cruise_ mode arrowy keys navigate within the editor
|
||||
// * In _accept_ mode pressing an arrow key will end editing
|
||||
// * In _insert_ mode arrow keys will change the selected range
|
||||
export type EditorMode = "cruise" | "accept" | "insert";
|
||||
|
||||
export interface EditorState {
|
||||
mode: EditorMode;
|
||||
insertRange: null | Area;
|
||||
baseText: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
interface EditorContextType {
|
||||
editorContext: EditorState;
|
||||
setEditorContext: Dispatch<
|
||||
SetStateAction<{ mode: EditorMode; insertRange: null | Area }>
|
||||
>;
|
||||
}
|
||||
|
||||
const EditorContext = createContext<EditorContextType>({
|
||||
editorContext: {
|
||||
mode: "accept",
|
||||
insertRange: null,
|
||||
baseText: '',
|
||||
id: Math.floor(Math.random()*1000),
|
||||
},
|
||||
setEditorContext: () => {},
|
||||
});
|
||||
|
||||
export default EditorContext;
|
||||
3
webapp/src/components/editor/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default } from './editor';
|
||||
|
||||
|
||||
92
webapp/src/components/editor/tokenTypes.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
type ErrorType =
|
||||
| 'REF'
|
||||
| 'NAME'
|
||||
| 'VALUE'
|
||||
| 'DIV'
|
||||
| 'NA'
|
||||
| 'NUM'
|
||||
| 'ERROR'
|
||||
| 'NIMPL'
|
||||
| 'SPILL'
|
||||
| 'CALC'
|
||||
| 'CIRC';
|
||||
|
||||
type OpCompareType =
|
||||
| 'LessThan'
|
||||
| 'GreaterThan'
|
||||
| 'Equal'
|
||||
| 'LessOrEqualThan'
|
||||
| 'GreaterOrEqualThan'
|
||||
| 'NonEqual';
|
||||
|
||||
type OpSumType = 'Add' | 'Minus';
|
||||
|
||||
type OpProductType = 'Times' | 'Divide';
|
||||
|
||||
interface ReferenceType {
|
||||
sheet: string | null;
|
||||
row: number;
|
||||
column: number;
|
||||
absolute_column: boolean;
|
||||
absolute_row: boolean;
|
||||
}
|
||||
|
||||
interface ParsedReferenceType {
|
||||
column: number;
|
||||
row: number;
|
||||
absolute_column: boolean;
|
||||
absolute_row: boolean;
|
||||
}
|
||||
|
||||
interface Reference {
|
||||
Reference: ReferenceType;
|
||||
}
|
||||
|
||||
interface Range {
|
||||
Range: {
|
||||
sheet: string | null;
|
||||
left: ParsedReferenceType;
|
||||
right: ParsedReferenceType;
|
||||
};
|
||||
}
|
||||
|
||||
export type TokenType =
|
||||
| 'Illegal'
|
||||
| 'Eof'
|
||||
| { Ident: string }
|
||||
| { String: string }
|
||||
| { Boolean: boolean }
|
||||
| { Number: number }
|
||||
| { ERROR: ErrorType }
|
||||
| { COMPARE: OpCompareType }
|
||||
| { SUM: OpSumType }
|
||||
| { PRODUCT: OpProductType }
|
||||
| 'POWER'
|
||||
| 'LPAREN'
|
||||
| 'RPAREN'
|
||||
| 'COLON'
|
||||
| 'SEMICOLON'
|
||||
| 'LBRACKET'
|
||||
| 'RBRACKET'
|
||||
| 'LBRACE'
|
||||
| 'RBRACE'
|
||||
| 'COMMA'
|
||||
| 'BANG'
|
||||
| 'PERCENT'
|
||||
| 'AND'
|
||||
| Reference
|
||||
| Range;
|
||||
|
||||
export interface MarkedToken {
|
||||
token: TokenType;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export function tokenIsReferenceType(token: TokenType): token is Reference {
|
||||
return typeof token === 'object' && 'Reference' in token;
|
||||
}
|
||||
|
||||
export function tokenIsRangeType(token: TokenType): token is Range {
|
||||
return typeof token === 'object' && 'Range' in token;
|
||||
}
|
||||