Files
IronCalc/webapp/IronCalc/src/components/Editor/util.tsx
Nicolás Hatcher 3bb49d1e8f FIX: Minor cleanups
2025-11-20 21:44:05 +01:00

237 lines
6.6 KiB
TypeScript

import {
getTokens,
type Model,
type Range,
type Reference,
type TokenType,
} from "@ironcalc/wasm";
import type { JSX } from "react";
import type { ActiveRange } from "../workbookState";
function sliceString(
text: string,
startScalar: number,
endScalar: number,
): string {
const scalarValues = Array.from(text);
const sliced = scalarValues.slice(startScalar, endScalar);
return sliced.join("");
}
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;
}
export function isInReferenceMode(text: string, cursor: number): boolean {
// FIXME
// This is a gross oversimplification
// Returns true if both are true:
// 1. Cursor is at the end
// 2. Last char is one of [',', '(', '+', '*', '-', '/', '<', '>', '=', '&']
// This has many false positives like '="1+' and also likely some false negatives
// The right way of doing this is to have a partial parse of the formula tree
// and check if the next token could be a reference
if (!text.startsWith("=")) {
return false;
}
if (text === "=") {
return true;
}
const l = text.length;
const chars = [",", "(", "+", "*", "-", "/", "<", ">", "=", "&"];
if (cursor === l && chars.includes(text[l - 1])) {
return true;
}
return false;
}
// IronCalc 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})`;
}
function getFormulaHTML(
model: Model,
text: string,
): { html: JSX.Element[]; activeRanges: ActiveRange[] } {
let html: JSX.Element[] = [];
const activeRanges: ActiveRange[] = [];
let colorCount = 0;
if (text.startsWith("=")) {
const formula = text.slice(1);
const tokens = getTokens(formula);
const tokenCount = tokens.length;
const usedColors: Record<string, string> = {};
const sheet = model.getSelectedSheet();
const sheetList = model.getWorksheetsProperties().map((s) => s.name);
for (let index = 0; index < tokenCount; index += 1) {
const { token, start, end } = tokens[index];
if (tokenIsReferenceType(token)) {
const { sheet: refSheet, row, column } = token.Reference;
const sheetIndex = refSheet ? sheetList.indexOf(refSheet) : sheet;
const key = `${sheetIndex}-${row}-${column}`;
let color = usedColors[key];
if (!color) {
color = getColor(colorCount);
usedColors[key] = color;
colorCount += 1;
}
html.push(
<span key={index} style={{ color }}>
{sliceString(formula, start, end)}
</span>,
);
activeRanges.push({
sheet: sheetIndex,
rowStart: row,
columnStart: column,
rowEnd: row,
columnEnd: column,
color,
});
} else if (tokenIsRangeType(token)) {
let {
sheet: refSheet,
left: { row: rowStart, column: columnStart },
right: { row: rowEnd, column: columnEnd },
} = token.Range;
const sheetIndex = refSheet ? sheetList.indexOf(refSheet) : sheet;
const key = `${sheetIndex}-${rowStart}-${columnStart}:${rowEnd}-${columnEnd}`;
let color = usedColors[key];
if (!color) {
color = getColor(colorCount);
usedColors[key] = color;
colorCount += 1;
}
if (rowStart > rowEnd) {
[rowStart, rowEnd] = [rowEnd, rowStart];
}
if (columnStart > columnEnd) {
[columnStart, columnEnd] = [columnEnd, columnStart];
}
html.push(
<span key={index} style={{ color }}>
{sliceString(formula, start, end)}
</span>,
);
colorCount += 1;
activeRanges.push({
sheet: sheetIndex,
rowStart,
columnStart,
rowEnd,
columnEnd,
color,
});
} else {
html.push(<span key={index}>{sliceString(formula, start, end)}</span>);
}
}
html = [<span key="equals">=</span>].concat(html);
} else {
html = [<span key="single">{text}</span>];
}
return { html, activeRanges };
}
// Given a formula (without the equals sign) returns (sheetIndex, rowStart, columnStart, rowEnd, columnEnd)
// if it represent a reference or range like `Sheet1!A1` or `Sheet3!D3:D10` in an existing sheet
// If it is not a reference or range it returns null
export function parseRangeInSheet(
model: Model,
formula: string,
): [number, number, number, number, number] | null {
// HACK: We are checking here the series of tokens in the range formula.
// This is enough for our purposes but probably a more specific ranges in formula method would be better.
const worksheets = model.getWorksheetsProperties();
const tokens = getTokens(formula);
const { token } = tokens[0];
if (tokenIsRangeType(token)) {
const {
sheet: refSheet,
left: { row: rowStart, column: columnStart },
right: { row: rowEnd, column: columnEnd },
} = token.Range;
if (refSheet !== null) {
const sheetIndex = worksheets.findIndex((s) => s.name === refSheet);
if (sheetIndex >= 0) {
return [sheetIndex, rowStart, columnStart, rowEnd, columnEnd];
}
}
} else if (tokenIsReferenceType(token)) {
const { sheet: refSheet, row, column } = token.Reference;
if (refSheet !== null) {
const sheetIndex = worksheets.findIndex((s) => s.name === refSheet);
if (sheetIndex >= 0) {
return [sheetIndex, row, column, row, column];
}
}
}
return null;
}
export default getFormulaHTML;