mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
fix(preview): index rows and cells by their present keys, cap theme size
ExcelJS keeps a row's cells at `_cells[col - 1]`, so a row whose only
cell sits in XFD is a dictionary-mode array that `eachCell` and
`hasValues` (behind `eachRow`) walk to index 16,384. The worker walked
each row four times at load and once per tile, so a small file of
far-column rows took seconds to load and to tile.
`worksheetMetadata` now builds each sheet's row and cell index from
`Object.keys(sheet._rows)` and `Object.keys(row._cells)`, sorted
numerically, skipping falsy and Null-type cells exactly as
`eachCell({ includeEmpty: false })` does and keeping a row only when it
holds one such cell (`hasValues`). Styles, extent and row heights come
from that one pass and merges from `sheet._merges`; `sendTile` reads
each row's cells from the index.
`parseThemePalette` returns the default palette for a theme above
64 * 1024 characters, since its patterns are quadratic on unclosed tags.
This commit is contained in:
1 parent
c1811fd716
commit
7d3e27fb6d
7 files changed
+193
-23
No files matched your search
@@ -24,6 +24,8 @@ importScripts(`vendor/fflate.min.js${spreadsheetAssetQuery}`, `spreadsheet-xlsx-
|
||||
const core = self.CodemanSpreadsheetXlsxCore;
|
||||
let workbook = null;
|
||||
let sheetsById = new Map();
|
||||
// Per sheet: its populated rows in order, each with its populated cells in
|
||||
// column order, built once at load from the keys that exist (`populatedRowIndex`).
|
||||
let populatedRowsById = new Map();
|
||||
// Merges read once at load: `sheet.model` rebuilds every row and cell model,
|
||||
// which is far too much to pay on every tile.
|
||||
@@ -78,23 +80,56 @@ function normalizeStyle(cell) {
|
||||
return id;
|
||||
}
|
||||
|
||||
// Ascending numeric own keys of a sparse array. ExcelJS keeps rows at
|
||||
// `_rows[r - 1]` and a row's cells at `_cells[col - 1]`, and one far index puts
|
||||
// the array in dictionary mode, where its own `eachRow`, `eachCell` and
|
||||
// `hasValues` (forEach/some) visit every index up to the largest: a single XFD
|
||||
// cell per row costs 16,384 steps a row. Walking the keys that exist does not.
|
||||
function presentIndices(sparse) {
|
||||
const indices = [];
|
||||
for (const key of Object.keys(sparse || [])) {
|
||||
const index = Number(key);
|
||||
if (Number.isInteger(index) && index >= 0) indices.push(index);
|
||||
}
|
||||
return indices.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
// The rows and cells `sheet.eachRow({ includeEmpty: false })` and
|
||||
// `row.eachCell({ includeEmpty: false })` would visit, in the same order: a
|
||||
// cell counts when it exists and its type is not `ValueType.Null`, and a row
|
||||
// counts when it holds at least one such cell (ExcelJS's `row.hasValues`).
|
||||
function populatedRowIndex(sheet) {
|
||||
const nullType = self.ExcelJS.ValueType.Null;
|
||||
const rows = [];
|
||||
for (const rowIndex of presentIndices(sheet._rows)) {
|
||||
const row = sheet._rows[rowIndex];
|
||||
if (!row) continue;
|
||||
const cells = [];
|
||||
for (const cellIndex of presentIndices(row._cells)) {
|
||||
const cell = row._cells[cellIndex];
|
||||
if (cell && cell.type !== nullType) cells.push(cell);
|
||||
}
|
||||
if (cells.length > 0) rows.push({ number: row.number, row, cells });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function worksheetMetadata(sheet) {
|
||||
const cellRefs = [];
|
||||
const populatedRows = [];
|
||||
sheet.eachRow({ includeEmpty: false }, (row) => {
|
||||
populatedRows.push(row.number);
|
||||
row.eachCell({ includeEmpty: false }, (cell) => {
|
||||
const rowOverrides = [];
|
||||
const populatedRows = populatedRowIndex(sheet);
|
||||
for (const { number, row, cells } of populatedRows) {
|
||||
for (const cell of cells) {
|
||||
cellRefs.push(cell.address);
|
||||
normalizeStyle(cell);
|
||||
});
|
||||
});
|
||||
const merges = Array.from(sheet.model?.merges || []);
|
||||
}
|
||||
if (row.hidden) rowOverrides.push([number, 0]);
|
||||
else if (row.height) rowOverrides.push([number, Math.min(546, Math.max(0, row.height * (4 / 3)))]);
|
||||
}
|
||||
// `sheet.model` rebuilds every row and cell model, so merges come straight
|
||||
// from ExcelJS's own merge map, in the order the model getter would list them.
|
||||
const merges = Object.values(sheet._merges || {}).map((merge) => merge.range);
|
||||
const extent = core.deriveExtent(cellRefs, merges);
|
||||
const rowOverrides = [];
|
||||
sheet.eachRow({ includeEmpty: false }, (row) => {
|
||||
if (row.hidden) rowOverrides.push([row.number, 0]);
|
||||
else if (row.height) rowOverrides.push([row.number, Math.min(546, Math.max(0, row.height * (4 / 3)))]);
|
||||
});
|
||||
const columnOverrides = [];
|
||||
for (let col = 1; col <= extent.cols; col += 1) {
|
||||
const column = sheet.getColumn(col);
|
||||
@@ -207,16 +242,16 @@ function sendTile(message) {
|
||||
});
|
||||
};
|
||||
const populatedRows = populatedRowsById.get(String(message.sheetId)) || [];
|
||||
for (const rowNumber of populatedRows) {
|
||||
for (const populated of populatedRows) {
|
||||
if (truncated) break;
|
||||
if (rowNumber < range.r1) continue;
|
||||
if (rowNumber > range.r2) break;
|
||||
const row = sheet.getRow(rowNumber);
|
||||
if (row.hidden) continue;
|
||||
row.eachCell({ includeEmpty: false }, (cell) => {
|
||||
if (cell.col < range.c1 || cell.col > range.c2) return;
|
||||
if (populated.number < range.r1) continue;
|
||||
if (populated.number > range.r2) break;
|
||||
if (populated.row.hidden) continue;
|
||||
for (const cell of populated.cells) {
|
||||
if (cell.col < range.c1) continue;
|
||||
if (cell.col > range.c2) break;
|
||||
addCell(cell);
|
||||
});
|
||||
}
|
||||
}
|
||||
const merges = core.intersectingMerges(mergesById.get(String(message.sheetId)) || [], range);
|
||||
for (const merge of merges) {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
(function initSpreadsheetPreview(global) {
|
||||
'use strict';
|
||||
|
||||
const SPREADSHEET_ASSET_VERSION = '5bd72f3f823f';
|
||||
const SPREADSHEET_ASSET_VERSION = '6e843e269018';
|
||||
const MAX_PREVIEW_BYTES = 10 * 1024 * 1024;
|
||||
const DEFAULT_TIMEOUT_MS = 20000;
|
||||
const MAX_SCROLL_PX = 8000000;
|
||||
|
||||
@@ -744,9 +744,17 @@
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Real theme1.xml files are under 10 KB. The scheme and slot patterns below
|
||||
// rescan to the end of the text for every opening tag that has no close, so a
|
||||
// padded theme is quadratic (1 MB of `<a:clrScheme>` took 21.8 s); above this
|
||||
// many characters (UTF-16 code units of the decoded XML) the default palette
|
||||
// is used instead.
|
||||
const MAX_THEME_XML_CHARS = 64 * 1024;
|
||||
|
||||
function parseThemePalette(xml) {
|
||||
const palette = DEFAULT_THEME_PALETTE.slice();
|
||||
const text = typeof xml === 'string' ? xml : '';
|
||||
if (text.length > MAX_THEME_XML_CHARS) return palette;
|
||||
const scheme = /<(?:[A-Za-z0-9_]+:)?clrScheme\b[^>]*>([\s\S]*?)<\/(?:[A-Za-z0-9_]+:)?clrScheme\s*>/.exec(text);
|
||||
if (!scheme) return palette;
|
||||
// Fresh pattern per call: a shared /g regex would carry lastIndex across calls.
|
||||
|
||||
Reference in new issue
Block a user