@dimarborda/xlsx-editor 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,265 +1,76 @@
1
1
  # @dimarborda/xlsx-editor
2
2
 
3
- Componente para ver y editar libros de Excel (`.xlsx`) en el navegador, dibujados en canvas como los muestra Excel. Por dentro usa un motor escrito en Rust y compilado a WebAssembly, que lee el archivo, resuelve estilos y formatos de número, recalcula fórmulas y guarda sin pérdidas. Ese motor también se usa sin interfaz, en Node.js, desde [`@dimarborda/xlsx-editor/engine`](#motor-sin-interfaz-nodejs). Comparte con [`@dimarborda/docx-editor`](../docx-editor) la capa de paquete OOXML (`crates/ooxml-core`). Funciona en cualquier frontend (HTML, React, Vue, Svelte, Angular, Tauri) y nada sale del navegador.
3
+ View and edit Excel (`.xlsx`) workbooks in the browser, drawn on canvas the way Excel shows them, and work with them from Node.js. A Rust and WebAssembly engine reads the file, recalculates formulas and saves losslessly: what it does not touch stays byte by byte. It works in any frontend, and nothing leaves the browser.
4
4
 
5
- > **Estado: fase 2, edición de celdas.** Se escriben valores y fórmulas en la cuadrícula, se borran, se copian y pegan, y se deshacen. Las fórmulas habituales se recalculan al momento (ver [Limitaciones](#limitaciones)). Con el atributo `readonly` sigue siendo un visor.
5
+ For AI agents, `wb.area()` gives a table's columns, types and totals without reading the whole sheet, and a change that would break something is refused with the reason, leaving the workbook as it was.
6
+
7
+ **Full documentation on GitHub:** [English](https://github.com/dimarborda/rust-web-docx/blob/master/docs/xlsx-editor/en/component.md) · [Español](https://github.com/dimarborda/rust-web-docx/blob/master/docs/xlsx-editor/es/component.md)
8
+
9
+ ## Install
6
10
 
7
11
  ```bash
8
12
  npm install @dimarborda/xlsx-editor
9
13
  ```
10
14
 
11
15
  ```html
12
- <xlsx-editor src="/informe.xlsx" locale="es" style="height: 80vh"></xlsx-editor>
16
+ <xlsx-editor src="/report.xlsx" locale="en" style="height: 80vh"></xlsx-editor>
13
17
  <script type="module">
14
18
  import '@dimarborda/xlsx-editor';
15
- import '@dimarborda/xlsx-editor/fonts'; // opcional: sustitutos de Calibri, Arial…
19
+ import '@dimarborda/xlsx-editor/fonts'; // optional: substitutes for Calibri, Arial…
16
20
  import '@dimarborda/xlsx-editor/style.css';
17
21
  </script>
18
22
  ```
19
23
 
20
- ## Qué muestra
21
-
22
- - **Celdas como en Excel:** fuentes, negrita, cursiva, subrayado, tachado, colores (incluidos los del tema y sus tonos), rellenos y bordes con todos sus estilos.
23
- - **Formatos de número:** separadores según el idioma, moneda, porcentajes, notación científica, fracciones, secciones y condiciones, colores como `[Red]`, y fechas y horas con nombres de meses y días en español o inglés.
24
- - **Alineación:** la de Excel por defecto (números a la derecha, texto a la izquierda), horizontal y vertical, sangría, ajuste de texto, texto que se desborda sobre celdas vacías y `####` en los números que no caben.
25
- - **Cuadrícula:** anchos de columna y altos de fila del archivo, filas y columnas ocultas, celdas combinadas, paneles inmovilizados y líneas de cuadrícula (o su ausencia).
26
- - **Navegación:** desplazamiento fluido en hojas grandes (solo se dibuja lo visible), zoom, pestañas de hojas, selección con ratón y teclado, y una barra con la celda activa y su contenido o fórmula.
27
-
28
- Aún no se dibujan gráficos, imágenes, formato condicional ni comentarios, y el texto girado se dibuja horizontal. Las fórmulas se recalculan tras cada cambio con el motor del paquete; las que usan algo que no admite muestran el último valor que guardó Excel.
29
-
30
- ## Edición
31
-
32
- | Acción | Cómo |
33
- | :--- | :--- |
34
- | Escribir en una celda | Empieza a escribir (sustituye el contenido), pulsa **F2** o haz doble clic (edita el contenido actual), o escribe en la barra de celda. |
35
- | Confirmar | **Enter** (baja), **Mayús+Enter** (sube), **Tab** / **Mayús+Tab** (derecha / izquierda). Si empezaste a escribir sin F2, las flechas también confirman y mueven; **F2** alterna entre ese modo y mover el cursor dentro del texto. |
36
- | Rellenar la selección | **Ctrl/Cmd+Enter** escribe lo mismo en todas las celdas seleccionadas. |
37
- | Salto de línea en la celda | **Alt+Enter**. |
38
- | Cancelar | **Escape**. |
39
- | Borrar | **Supr** o **Retroceso** vacían el rango seleccionado; el formato de las celdas se queda. |
40
- | Copiar, cortar y pegar | **Ctrl/Cmd+C**, **X** y **V**, como texto separado por tabulaciones (TSV), el mismo que intercambian Excel y Google Sheets. Al copiar se copian los textos que muestran las celdas; si se pega en el mismo editor lo que se acaba de copiar, se pegan los valores exactos. Un bloque que cabe un número entero de veces en la selección se repite. Cortar vacía las celdas al momento (no al pegar, como hace Excel). |
41
- | Deshacer y rehacer | **Ctrl/Cmd+Z**, y **Ctrl/Cmd+Y** o **Cmd+Mayús+Z**. Cada edición, borrado o pegado es un paso; deshacer vuelve a la hoja y a la selección de entonces. |
42
-
43
- ### Qué se guarda al escribir
44
-
45
- Lo escrito se interpreta como lo hace Excel en el idioma del editor (`locale`; `es` y sus variantes usan las convenciones españolas y el resto las inglesas):
46
-
47
- | Escribes (`es`) | Escribes (`en`) | Se guarda |
48
- | :--- | :--- | :--- |
49
- | `1234,5` · `1.234,5` · `-0,5` · `(300)` · `1,5E+3` | `1234.5` · `1,234.5` · `-0.5` · `(300)` · `1.5E+3` | Número |
50
- | `1.234,50 €` | `$1,234.50` | Número (el símbolo se descarta) |
51
- | `15%` · `12,5 %` | `15%` · `12.5 %` | Número dividido por 100 (0,15) |
52
- | `15/03/2024` · `15-3-24` · `15/3` · `2024-03-15` | `3/15/2024` · `3-15-24` · `3/15` · `2024-03-15` | Fecha (sin año, el actual; años de dos cifras: 00–29 → 2000, 30–99 → 1900) |
53
- | `15/03/2024 14:30` | `3/15/2024 2:30 PM` | Fecha y hora |
54
- | `14:30` · `14:30:15` | `2:30 PM` | Hora, como fracción del día |
55
- | `VERDADERO` · `FALSO` | `TRUE` · `FALSE` | Booleano (sin distinguir mayúsculas) |
56
- | `'0012` | `'0012` | Texto `0012`: el apóstrofo inicial fuerza texto |
57
- | `=SUM(A1:A3)` | `=SUM(A1:A3)` | Fórmula (ver Limitaciones) |
58
- | Cualquier otra cosa | | Texto |
59
- | (vacío) | | Celda vacía |
60
-
61
- Los valores se escriben con `sheet.setValues` del motor, que conserva el formato de la celda: un número escrito en una celda con formato de moneda se ve como moneda. A las fechas se les da un formato de fecha basado en el de la celda si no lo tenía. Los porcentajes y las horas se guardan como su valor (0,15 o 0,604…) y se ven con el formato que ya tenga la celda. La misma función está disponible como `parseInput(texto, locale)`.
62
-
63
- ### Limitaciones
64
-
65
- - **Fórmulas:** lo que empieza por `=` se guarda como fórmula y se calcula, igual que las fórmulas que dependen de ella. Se escriben en la sintaxis en inglés (`=SUM(A1:A3)`, `,` o `;` entre argumentos) en cualquier idioma del editor. Las funciones admitidas están en el [motor](#fórmulas); con otras, la celda muestra el último valor guardado y Excel la calcula al abrir el archivo (`fullCalcOnLoad`).
66
- - **Formato:** escribir no cambia el formato de número (salvo en fechas), y las columnas no se ensanchan solas: una fecha en una columna estrecha se ve como `####`, como en Excel.
67
- - No se puede sobrescribir la celda de origen de una fórmula compartida (la que usan otras celdas); el editor emite `error` y deja el libro como estaba.
68
-
69
- ### Áreas y contexto para agentes
70
-
71
- - `editor.selectArea(destino)`: selecciona un área y muestra su hoja. Acepta una tabla o un rango con nombre, una dirección (`Ventas!A1:H40`) o un rango de la hoja activa. Devuelve el área (ver [Áreas para agentes](#áreas-para-agentes)).
72
- - `editor.selectionContext({ maxRows })`: contexto de lo seleccionado para un agente: el rango, o con una sola celda la región que la rodea (su tabla, o el bloque que seleccionaría Ctrl+A). Devuelve título, cabecera, tipos y totales por columna, las primeras filas y una tabla Markdown.
73
-
74
- Para dar formato desde código se usa `editor.workbook.sheet(…).setFormat(…)` y después `editor.reload()`.
75
-
76
- ## Atributos
77
-
78
- | Atributo | Para qué |
79
- | :--- | :--- |
80
- | `src` | URL del `.xlsx` |
81
- | `blank` | Empieza con un libro vacío |
82
- | `locale` | Idioma de los formatos (`es`, `en-US`…); por defecto, el de la página |
83
- | `sheet` | Hoja inicial, por nombre o posición |
84
- | `zoom` | 0.25–4 |
85
- | `tabs="false"` | Oculta las pestañas |
86
- | `formula-bar="false"` | Oculta la barra de celda |
87
- | `readonly` | Modo visor: se selecciona y se copia, pero no se edita (se puede quitar y poner en cualquier momento) |
88
- | `quiet` | No escribe los errores en la consola |
89
- | `wasm-url` | Dónde se sirve `xlsx_engine_bg.wasm`, si el bundler no lo hace |
90
-
91
- Eventos en el elemento: `ready` (con `detail.editor`), `load`, `sheetchange`, `selectionchange`, `change` y `error`.
92
-
93
- `change` llega tras cada edición, borrado, pegado, corte, deshacer o rehacer:
94
-
95
- ```js
96
- element.addEventListener('change', ({ detail }) => {
97
- // detail: { sheet: 'Ventas', index: 0, range: 'B3:C4', source: 'edit' | 'clear' | 'paste' | 'cut' | 'undo' | 'redo',
98
- // cells: [{ ref: 'B3', value: 1200 }, …] }
99
- });
100
- ```
101
-
102
- ## API
24
+ With WebAssembly, the production `script-src` needs `'wasm-unsafe-eval'`.
103
25
 
104
- ```js
105
- const element = document.querySelector('xlsx-editor');
106
- element.addEventListener('ready', async ({ detail: { editor } }) => {
107
- await editor.open(file); // URL, File/Blob, ArrayBuffer o Uint8Array
108
- editor.setSheet('Resumen');
109
- editor.select('B3:D7');
110
- editor.setZoom(1.25);
26
+ ## Features
111
27
 
112
- // Cambiar celdas con la API del motor y volver a dibujar
113
- editor.workbook.fillTemplate({ CLIENTE: 'Acme' });
114
- editor.workbook.sheet('Resumen').setValues('B2', [[1200, 0.25]]);
115
- editor.workbook.addSheet('Notas'); // reload() también redibuja las pestañas
116
- editor.reload();
28
+ - **Faithful display:** fonts, colors, borders, number formats, merged cells, frozen panes and hidden lines. Large sheets scroll smoothly.
29
+ - **Editing:** values and formulas, copy and paste with Excel and Google Sheets, undo, and a status bar with average, count and sum.
30
+ - **Rows and columns:** move, insert, delete, resize, autofit, hide and show, updating formulas, names, charts and tables across the workbook.
31
+ - **Productivity:** fill handle with series, autosum, find and replace, keyboard navigation and a context menu.
32
+ - **Formulas:** about 90 functions recalculated instantly; for anything else, Excel's saved value stays and Excel recalculates when opening the file.
33
+ - **Headless engine:** the same API in Node.js.
117
34
 
118
- editor.undo(); // también editor.redo(), editor.canUndo, editor.canRedo
119
- editor.readOnly = true; // lo mismo que el atributo readonly
35
+ Charts, images, conditional formats and comments are kept in the file but not drawn yet.
120
36
 
121
- editor.download('informe.xlsx');
122
- });
123
- ```
124
-
125
- Los cambios hechos desde código con `editor.workbook` no forman un paso de deshacer propio: llama antes a `editor.workbook.checkpoint()` para que lo sean.
126
-
127
- Sin el elemento: `const editor = await XlsxEditor.create(contenedor, { locale: 'es', readOnly: false })`.
128
-
129
- ## Motor sin interfaz (Node.js)
130
-
131
- `@dimarborda/xlsx-editor/engine` es el mismo motor sin cuadrícula ni DOM: lee, escribe, recalcula y da formato a libros en Node.js (scripts, servidores, herramientas de agentes) o en el navegador.
132
-
133
- ```bash
134
- npm install @dimarborda/xlsx-editor
135
- ```
37
+ ## Use it from code
136
38
 
137
39
  ```js
138
40
  import { readFile, writeFile } from 'node:fs/promises';
139
41
  import { openWorkbook } from '@dimarborda/xlsx-editor/engine';
140
42
 
141
- const wb = await openWorkbook(await readFile('plantilla.xlsx'));
142
-
143
- // Variables {{CAMPO}} en cualquier texto del libro
144
- wb.fillTemplate({ CLIENTE: 'Acme S.A.', FECHA: '15/01/2024' });
43
+ const wb = await openWorkbook(await readFile('template.xlsx'));
44
+ const sheet = wb.sheet('Sales');
145
45
 
146
- // Filas de datos a partir de una celda
147
- const hoja = wb.sheet('Ventas');
148
- hoja.setValues('A2', [
149
- ['Teclado', 3, 29.9, new Date(Date.UTC(2024, 0, 15))],
150
- ['Ratón', 5, 12.5, new Date(Date.UTC(2024, 0, 16))],
151
- ]);
46
+ wb.fillTemplate({ CLIENT: 'Acme Inc.' });
47
+ sheet.setValues('A2', [['Keyboard', 3, 29.9], ['Mouse', 5, 12.5]]);
48
+ sheet.setValue('D2', { formula: '=B2*C2' });
49
+ sheet.insertRows(2);
50
+ sheet.fill('D3', 'D3:D4');
152
51
 
153
- console.log(hoja.getValue('B10')); // resultado de una fórmula, ya recalculado
154
- await writeFile('salida.xlsx', wb.toBytes());
52
+ await writeFile('output.xlsx', wb.toBytes());
155
53
  ```
156
54
 
157
- En Node el motor se carga solo. En el navegador, los bundlers (Vite, webpack 5, Rollup) sirven el `.wasm` junto al JavaScript; si lo sirves desde otro sitio, llama antes a `initEngine(url)`. La entrada principal del paquete también exporta todo esto, así que en el navegador basta con `import { openWorkbook } from '@dimarborda/xlsx-editor'`; `editor.workbook` es un `Workbook` de esta misma API.
158
-
159
- ### API
160
-
161
- | | |
162
- |---|---|
163
- | `openWorkbook(bytes)` / `createWorkbook()` | Abre un `.xlsx` o crea un libro con una hoja vacía. |
164
- | `wb.sheetNames` | Nombres de las hojas en el orden de las pestañas. |
165
- | `wb.sheet(nombreOÍndice)` / `wb.sheets` | Una hoja de cálculo (por nombre, sin distinguir mayúsculas, o por posición desde 0) o todas. Las hojas de gráfico no tienen celdas. |
166
- | `hoja.getCell('B3')` | `{ ref, type, value, formula? }`. `type` es `empty`, `number`, `string`, `boolean`, `date` o `error` (`value` es entonces el código, p. ej. `#DIV/0!`). En las fórmulas, `value` es su resultado y `formula` su texto sin `=` (también en las celdas de una fórmula compartida). |
167
- | `hoja.getValue('B3')` / `hoja.getValues('A1:C3')` | Solo los valores; las celdas vacías dan `null`. |
168
- | `hoja.setValue('B3', valor)` | Escribe texto, número, booleano, `Date` o una fórmula (`{ formula: '=SUM(A1:A3)' }`); `null` borra el valor y deja el formato. Las fórmulas que dependen de la celda se recalculan. |
169
- | `hoja.setValues('A2', filas)` | Escribe una matriz desde la celda indicada, creando filas y celdas en orden. `undefined` deja la celda como está. |
170
- | `wb.fillTemplate(valores)` | Sustituye `{{CAMPO}}` (se admiten espacios y una pista: `{{ CAMPO }}`, `{{CAMPO: pista}}`) en las cadenas compartidas y en las cadenas en línea; devuelve cuántas sustituyó. Las variables sin valor se quedan como están. |
171
- | `wb.addSheet(nombre, después?)` | Agrega una hoja vacía detrás de la pestaña `después` (nombre o posición; al final sin ella) y la devuelve. El nombre sigue las reglas de Excel: de 1 a 31 caracteres, sin `[ ] : * ? / \`, sin apóstrofo al principio ni al final, distinto de `History` y de las demás hojas (sin distinguir mayúsculas). |
172
- | `wb.renameSheet(hoja, nombre)` | Renombra una pestaña (nombre o posición) y las referencias a ella en fórmulas, nombres definidos, validaciones de datos, formato condicional y gráficos; devuelve cuántas cambió. Las referencias dentro de textos (`INDIRECT("Hoja1!A1")`) y a otros libros se quedan como están, igual que en Excel. |
173
- | `wb.markers({ kind })` | Marcadores del léxico estándar (el mismo de `@dimarborda/docx-editor`) en las celdas de texto de todas las hojas: `field` para `{{CLAVE}}` / `{clave}`, `pending` para `[…]` (contenido por completar). Cada uno con `sheet`, `sheetName`, `ref` (`B3`), `row`, `col`, `raw`, `key`, `hint`, `offset` y `length`. |
174
- | `wb.checkpoint(selección?)` | Abre un paso de deshacer: los cambios hasta el siguiente `checkpoint` se deshacen juntos. `selección` es un texto libre que `undo()` devuelve. `wb.setSelectionAfter(selección)` guarda la de después, que devuelve `redo()`. |
175
- | `wb.undo()` / `wb.redo()` | Deshace o rehace un paso: `{ selection, parts }`, o `null` si no hay nada. `wb.canUndo` / `wb.canRedo` dicen si se puede. |
176
- | `wb.recalculate()` | Recalcula todas las fórmulas y devuelve cuántas celdas cambiaron de valor. |
177
- | `wb.areas({ sheet })` / `hoja.areas()` | Las áreas del libro (ver [Áreas](#áreas-para-agentes)). |
178
- | `wb.area(destino, { sheet, maxRows, locale })` / `hoja.area(rango)` | Contexto de un área para un agente. |
179
- | `hoja.regionAt('B5')` | La región alrededor de una celda, como la selecciona Ctrl+A en Excel (o su tabla). |
180
- | `hoja.setFormat('A1:C3', formato)` / `hoja.clearFormat(rango)` | Da formato (ver [Formato](#formato)) o lo quita. |
181
- | `hoja.setColumnWidth('B:D', 14)` | Ancho de columnas en caracteres. |
182
- | `hoja.mergeCells('A1:H1')` / `hoja.unmergeCells(rango)` | Combina o separa celdas; al combinar, solo la primera puede tener valor. |
183
- | `wb.toBytes()` | El libro como `.xlsx`. |
184
- | `wb.close()` | Libera la memoria del motor. |
185
-
186
- #### Fórmulas
187
-
188
- Las fórmulas se escriben con `{ formula: '=SUM(B2:B10)*IVA' }` y se calculan en el momento. Cada vez que se escribe una celda (o se rellena una plantilla) se recalculan, en orden de dependencias, todas las fórmulas del libro, y solo se reescribe el `<v>` de las celdas cuyo resultado cambió. `wb.recalculate()` hace lo mismo sin editar nada, por ejemplo para refrescar un libro recién abierto.
189
-
190
- Se usa la sintaxis en inglés que guarda Excel (`SUM`, `IF`, `,` entre argumentos; también se acepta `;`). Se admiten referencias relativas y absolutas, rangos, columnas completas (`A:A`), otras hojas (`'Mis datos'!B2`), nombres definidos, fórmulas compartidas y matrices constantes (`{1,2;3,4}`), con las conversiones de Excel entre texto, número y booleano y sus códigos de error. Funciones:
191
-
192
- - **Matemáticas:** `SUM`, `PRODUCT`, `AVERAGE`, `MIN`, `MAX`, `MEDIAN`, `COUNT`, `COUNTA`, `COUNTBLANK`, `ROUND`, `ROUNDUP`, `ROUNDDOWN`, `INT`, `TRUNC`, `ABS`, `SIGN`, `SQRT`, `POWER`, `MOD`, `PI`, `CEILING`, `FLOOR`, `CEILING.MATH`, `FLOOR.MATH`, `SUMPRODUCT`.
193
- - **Condicionales:** `SUMIF`, `SUMIFS`, `COUNTIF`, `COUNTIFS`, `AVERAGEIF`, `AVERAGEIFS`, `MAXIFS`, `MINIFS` (criterios como `">5"`, `"<>x"`, `"n*"`).
194
- - **Lógicas e información:** `IF`, `IFS`, `SWITCH`, `IFERROR`, `IFNA`, `AND`, `OR`, `XOR`, `NOT`, `TRUE`, `FALSE`, `ISBLANK`, `ISNUMBER`, `ISTEXT`, `ISNONTEXT`, `ISLOGICAL`, `ISERROR`, `ISERR`, `ISNA`, `NA`.
195
- - **Búsqueda:** `VLOOKUP`, `HLOOKUP`, `XLOOKUP`, `MATCH`, `INDEX`, `CHOOSE`, `ROW`, `COLUMN`, `ROWS`, `COLUMNS`.
196
- - **Texto:** `CONCATENATE`, `CONCAT`, `TEXTJOIN`, `LEFT`, `RIGHT`, `MID`, `LEN`, `UPPER`, `LOWER`, `PROPER`, `TRIM`, `SUBSTITUTE`, `REPT`, `EXACT`, `FIND`, `SEARCH`, `VALUE`, `TEXT`.
197
- - **Fechas:** `TODAY`, `NOW`, `DATE`, `YEAR`, `MONTH`, `DAY`, `WEEKDAY`, `EDATE`, `EOMONTH`, `DAYS`.
198
-
199
- Una fórmula que use otra cosa (una función que no está en la lista, `OFFSET`, `INDIRECT`, referencias a otros libros o a tablas, fórmulas matriciales, resultados que se desbordan a varias celdas) o que forme una referencia circular conserva el valor que guardó Excel, y las que dependen de ella lo usan. Al abrirse el archivo, Excel la calcula (ver abajo).
200
-
201
- #### Áreas para agentes
202
-
203
- Un agente no necesita leer toda la hoja: puede pedir las áreas y luego el contexto de la que le interesa. `wb.areas()` usa lo que el propio archivo marca, en este orden:
204
-
205
- 1. **Tablas** de Excel (Dar formato como tabla, `xl/tables/tableN.xml`): nombre, rango, nombres de columna y fila de totales.
206
- 2. **Nombres definidos** que apuntan a un rango (`Ventas = Datos!$A$3:$H$40`) y el **área de impresión**.
207
- 3. **Filtros** (`autoFilter`) de la hoja.
208
- 4. **Regiones**: bloques de celdas llenas contiguas, como los selecciona Ctrl+A, donde el archivo no marca nada. Las filas iniciales con una sola celda (o una combinada) son el **título**, igual que un texto suelto justo encima; la primera fila de textos seguida de datos (o en negrita) es la **cabecera**.
209
-
210
- Cada área es `{ kind, name, sheet, sheetName, ref, address, title, headerRow, totalsRow, body, columns }`; `address` (`Ventas!A1:H64`) sirve como destino en cualquier llamada.
211
-
212
- `wb.area(destino)` acepta el nombre de una tabla o de un rango con nombre, una dirección con hoja o, con `{ sheet }`, un rango de esa hoja. Devuelve la descripción del área y además:
213
-
214
- - `columnsInfo`: por columna, su nombre, tipo (`number`, `text`, `date`, `mixed`…), cuántas celdas tienen valor, suma, mínimo, máximo y media de los números, y cuántas fórmulas hay con un ejemplo (`D4: =B4*C4`).
215
- - `rows`: las primeras `maxRows` filas de datos (30 por defecto) con el texto que muestran las celdas, `firstRow`, `rowCount` y `truncated`.
216
- - `totals`, `merges` y `markdown`: el área como tabla Markdown con números de fila y letras de columna, lista para pasarla a un modelo.
217
-
218
- #### Formato
55
+ In the browser, `element.editor` is the editor and `editor.workbook` this same API:
219
56
 
220
- `hoja.setFormat(rango, formato)` cambia solo lo que se indica y conserva el resto del aspecto de cada celda:
221
-
222
- | Propiedad | Valor |
223
- |---|---|
224
- | `bold`, `italic`, `underline`, `strike` | `true` / `false` |
225
- | `color` | Color de la letra, `#RRGGBB` |
226
- | `fontSize`, `fontName` | Puntos; nombre de fuente |
227
- | `fill` | Fondo `#RRGGBB`; `null` lo quita |
228
- | `numberFormat` | Código de Excel: `#,##0.00`, `0%`, `dd/mm/yyyy`, `#,##0.00 "€"`… |
229
- | `align`, `valign`, `wrap` | `left`, `center`, `right`…; `top`, `center`, `bottom`; ajuste de texto |
230
- | `border` | Un estilo (`'thin'`, `'medium'`, `'thick'`, `'dashed'`, `'dotted'`, `'double'`) en todos los lados, `null` para quitarlos, o `{ style, color, edges }` con `edges`: `all`, `outline`, `inside`, `top`, `bottom`, `left` o `right` |
231
-
232
- Los formatos nuevos se añaden a `xl/styles.xml` y se reutilizan los iguales que ya existan; nada de lo que ya estaba se reescribe.
233
-
234
- #### Fechas
235
-
236
- Excel guarda las fechas como números de serie (días desde 1900, o desde 1904 en libros con ese sistema) que se ven como fecha por el formato de la celda. Al leer, una celda numérica con formato de fecha se devuelve como `Date`; al escribir un `Date`, la celda recibe un formato de fecha basado en el que tenía (misma fuente, relleno y bordes), salvo que ya mostrara fechas.
237
-
238
- Las fechas de Excel no tienen zona horaria: la hora que muestra Excel se representa como hora **UTC** del `Date`. Escribe con `new Date(Date.UTC(2024, 0, 15))` y lee con `getUTCFullYear()`, `getUTCHours()` o `toISOString()`.
239
-
240
- ### Sin pérdidas
241
-
242
- Solo se reescriben las partes del archivo que cambian (la hoja editada, las cadenas compartidas y, si hace falta, los estilos), y dentro de ellas solo las celdas escritas: gráficos, imágenes, formatos condicionales, validaciones, nombres definidos y todo lo demás se guarda byte a byte tal como estaba.
243
-
244
- Cuando un libro cambia, se elimina `xl/calcChain.xml` (Excel lo reconstruye) y se marca `fullCalcOnLoad="1"` para que Excel recalcule las fórmulas al abrirlo: así quedan bien también las que este motor no calcula.
245
-
246
- Limitaciones actuales:
247
-
248
- - Las fórmulas fuera de lo admitido (ver [Fórmulas](#fórmulas)) no se recalculan: `getValue` devuelve su último resultado guardado.
249
- - Las fórmulas se escriben en la sintaxis en inglés (`SUM`, no `SUMA`); los nombres de función en español no se traducen.
250
- - Escribir en la celda de origen de una fórmula compartida (la que usan otras celdas) da error, para no romper esas celdas.
251
- - Una variable `{{CAMPO}}` partida entre fragmentos de texto con formatos distintos no se reconoce.
252
-
253
- ## Content Security Policy
57
+ ```js
58
+ document.querySelector('xlsx-editor').addEventListener('ready', async ({ detail: { editor } }) => {
59
+ await editor.open(file);
60
+ editor.moveRows(4, 2, 9);
61
+ editor.download('report.xlsx');
62
+ });
63
+ ```
254
64
 
255
- Como el motor es WebAssembly, el `script-src` de producción necesita `'wasm-unsafe-eval'`.
65
+ ## Documentation
256
66
 
257
- ## Desarrollo
67
+ | Guide | English | Español |
68
+ | :--- | :--- | :--- |
69
+ | Component: keyboard, attributes, events, editor API | [component](https://github.com/dimarborda/rust-web-docx/blob/master/docs/xlsx-editor/en/component.md) | [component](https://github.com/dimarborda/rust-web-docx/blob/master/docs/xlsx-editor/es/component.md) |
70
+ | Move, insert, delete, fill, find and replace | [operations](https://github.com/dimarborda/rust-web-docx/blob/master/docs/xlsx-editor/en/operations.md) | [operations](https://github.com/dimarborda/rust-web-docx/blob/master/docs/xlsx-editor/es/operations.md) |
71
+ | Engine: full API, formulas, formats, dates | [engine](https://github.com/dimarborda/rust-web-docx/blob/master/docs/xlsx-editor/en/engine.md) | [engine](https://github.com/dimarborda/rust-web-docx/blob/master/docs/xlsx-editor/es/engine.md) |
72
+ | Areas and context for AI agents | [agents](https://github.com/dimarborda/rust-web-docx/blob/master/docs/xlsx-editor/en/agents.md) | [agents](https://github.com/dimarborda/rust-web-docx/blob/master/docs/xlsx-editor/es/agents.md) |
258
73
 
259
- Desde la raíz del repositorio:
74
+ ## License
260
75
 
261
- ```bash
262
- npm run build:wasm:xlsx
263
- npm test -w packages/xlsx-editor
264
- npm run dev # http://localhost:5173/xlsx.html
265
- ```
76
+ MIT
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@dimarborda/xlsx-editor",
3
- "version": "0.2.0",
4
- "description": "Excel (.xlsx) editor for the browser and engine for Node.js: Rust + WebAssembly that edits workbooks without losing what it does not touch, with a canvas grid as a framework-agnostic web component",
3
+ "version": "0.3.0",
4
+ "description": "Excel (.xlsx) editor for the browser and engine for Node.js: Rust + WebAssembly that edits workbooks without losing what it does not touch, from a canvas grid or from code. An AI agent can read a table's structure without loading the whole sheet",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Dimar Borda",
8
+ "homepage": "https://github.com/dimarborda/rust-web-docx/blob/master/docs/xlsx-editor/README.md",
8
9
  "repository": {
9
10
  "type": "git",
10
11
  "url": "git+https://github.com/dimarborda/rust-web-docx.git",
11
12
  "directory": "packages/xlsx-editor"
12
13
  },
13
14
  "keywords": [
15
+ "ai-agents",
14
16
  "xlsx",
15
17
  "excel",
16
18
  "spreadsheet",
package/src/draw.js CHANGED
@@ -26,7 +26,7 @@ export function canvasFont(font, zoom = 1) {
26
26
  }
27
27
 
28
28
  const GRIDLINE = '#d9d9d9';
29
- const PAD = 3; // horizontal text padding, px at zoom 1
29
+ export const PAD = 3; // horizontal text padding, px at zoom 1
30
30
 
31
31
  /** Line width and dash of an Excel border style, at zoom 1 */
32
32
  function borderLook(style) {
@@ -341,8 +341,13 @@ export function drawHeaders(ctx, { rows, cols, colSegs, rowSegs, rowHeaderW, col
341
341
  ctx.clip();
342
342
  for (let c = seg.from; c <= seg.to; c++) {
343
343
  const size = cols.size(c) * zoom;
344
- if (size === 0) continue;
345
344
  const x = seg.start + (cols.offset(c) - cols.offset(seg.from)) * zoom;
345
+ if (size === 0) {
346
+ // Where hidden columns are: a thicker line, as Excel draws it
347
+ ctx.fillStyle = HEADER_TEXT;
348
+ ctx.fillRect(Math.round(x) - 2, 0, 2, colHeaderH);
349
+ continue;
350
+ }
346
351
  const active = selection && inSel(c, selection.first.col, selection.last.col);
347
352
  if (active) {
348
353
  ctx.fillStyle = ACTIVE_BG;
@@ -364,8 +369,12 @@ export function drawHeaders(ctx, { rows, cols, colSegs, rowSegs, rowHeaderW, col
364
369
  ctx.clip();
365
370
  for (let r = seg.from; r <= seg.to; r++) {
366
371
  const size = rows.size(r) * zoom;
367
- if (size === 0) continue;
368
372
  const y = seg.start + (rows.offset(r) - rows.offset(seg.from)) * zoom;
373
+ if (size === 0) {
374
+ ctx.fillStyle = HEADER_TEXT;
375
+ ctx.fillRect(0, Math.round(y) - 2, rowHeaderW, 2);
376
+ continue;
377
+ }
369
378
  const active = selection && inSel(r, selection.first.row, selection.last.row);
370
379
  if (active) {
371
380
  ctx.fillStyle = ACTIVE_BG;
package/src/elements.js CHANGED
@@ -49,6 +49,7 @@ export class XlsxEditorElement extends ElementBase {
49
49
  zoom: Number(this.getAttribute('zoom')) || 1,
50
50
  tabs: this.getAttribute('tabs') !== 'false',
51
51
  formulaBar: this.getAttribute('formula-bar') !== 'false',
52
+ statusBar: this.getAttribute('status-bar') !== 'false',
52
53
  readOnly: this.hasAttribute('readonly'),
53
54
  wasmUrl: this.getAttribute('wasm-url') || undefined,
54
55
  });
package/src/engine.d.ts CHANGED
@@ -150,6 +150,46 @@ export class Sheet {
150
150
  * language `locale` (`es`, `en-US`…; default `en`)
151
151
  */
152
152
  cellViews(locale?: string): CellViewRow[];
153
+ /**
154
+ * Moves `count` whole rows from `first` before the row `before` (numbered as they are now), as
155
+ * Excel's "cut and insert": formats, heights, formulas of the whole workbook, names, charts,
156
+ * tables, validations, conditional formats, images and comments follow. Throws, and changes
157
+ * nothing, when it would tear merged cells, an array formula, a table or a filter. Returns
158
+ * where the first row ends up.
159
+ */
160
+ moveRows(first: number, count: number, before: number): number;
161
+ /** Moves whole columns (see `moveRows`); columns as letters (`'B'`) or numbers */
162
+ moveColumns(first: string | number, count: number, before: string | number): number;
163
+ /**
164
+ * Inserts `count` empty rows before the row `before`, with the height and format of the one
165
+ * above: formulas, names, charts and the rest are updated, and ranges, merged cells and tables
166
+ * that the new rows fall inside grow. Returns `before`.
167
+ */
168
+ insertRows(before: number, count?: number): number;
169
+ /** Inserts empty columns before `before`, like the one to their left (not inside a table) */
170
+ insertColumns(before: string | number, count?: number): number;
171
+ /**
172
+ * Deletes `count` rows from `first`: what pointed only at them becomes `#REF!`, and ranges,
173
+ * merged cells and tables shrink. Returns `first`.
174
+ */
175
+ deleteRows(first: number, count?: number): number;
176
+ /** Deletes columns from `first` (not inside a table) */
177
+ deleteColumns(first: string | number, count?: number): number;
178
+ /** Hides or shows rows (`3`, `'3:5'`); they keep their cells and height */
179
+ setRowsHidden(rows: string | number, hidden: boolean): void;
180
+ /** Hides or shows columns (`'B'`, `'B:D'`, `2`); they keep their cells and width */
181
+ setColumnsHidden(columns: string | number, hidden: boolean): void;
182
+ /**
183
+ * Fills `target` from `source`, the cells at one of its ends, as Excel's fill handle: formats
184
+ * repeat, formulas move their references, and with `series` (by default) numbers continue their
185
+ * trend, one date goes on day by day, texts ending in a number count on and names of months and
186
+ * days follow their order
187
+ */
188
+ fill(source: string, target: string, options?: { series?: boolean }): void;
189
+ /** The cells of a range (`A1:C3`), by row: their type, value and formula, if any */
190
+ getCells(range: string): Cell[][];
191
+ /** Height of a row or rows (`3`, `'3:5'`) in points; `null` goes back to the automatic height */
192
+ setRowHeight(rows: string | number, height: number | null): void;
153
193
  /** Areas of this sheet */
154
194
  areas(): Area[];
155
195
  /** The region around a cell as Excel's Ctrl+A selects it (or its table, or the cell alone) */
@@ -188,10 +228,10 @@ export interface SheetLayout {
188
228
  }
189
229
 
190
230
  /**
191
- * `[row, col, style, text, kind, color?]`: `kind` is `n` (number or date), `s` (text), `b`,
231
+ * `[row, col, style, text, kind, color?, number?]`: `kind` is `n` (number or date), `s` (text), `b`,
192
232
  * `e` (error) or `''` (empty cell with a format); `color` comes from the number format
193
233
  */
194
- export type CellViewRow = [number, number, number, string, 'n' | 's' | 'b' | 'e' | '', string?];
234
+ export type CellViewRow = [number, number, number, string, 'n' | 's' | 'b' | 'e' | '', (string | null)?, number?];
195
235
 
196
236
  export interface FontView {
197
237
  name: string;
package/src/engine.js CHANGED
@@ -73,6 +73,12 @@ function columnNumber(col) {
73
73
  throw new TypeError(`Columna no válida: ${col} (usa una letra como 'B' o un número desde 1).`);
74
74
  }
75
75
 
76
+ /** A row or column number, or a count, for the engine */
77
+ function lineNumber(value, what) {
78
+ if (!Number.isInteger(value) || value < 0) throw new TypeError(`${what} debe ser un entero positivo: ${value}`);
79
+ return value;
80
+ }
81
+
76
82
  /** One worksheet of a workbook */
77
83
  export class Sheet {
78
84
  /**
@@ -96,6 +102,11 @@ export class Sheet {
96
102
  return this.getCell(ref).value;
97
103
  }
98
104
 
105
+ /** The cells of a range (`A1:C3`), by row: their type, value and formula, if any */
106
+ getCells(range) {
107
+ return JSON.parse(call(() => this.workbook._engine.range_json(this.index, range))).map(row => row.map(toCell));
108
+ }
109
+
99
110
  /** The values of a range (`A1:C3`), by row */
100
111
  getValues(range) {
101
112
  return JSON.parse(call(() => this.workbook._engine.range_json(this.index, range))).map(row => row.map(c => toCell(c).value));
@@ -132,8 +143,9 @@ export class Sheet {
132
143
 
133
144
  /**
134
145
  * Drawing API: every cell that shows something, with the text Excel shows for it in the
135
- * language `locale`, as `[row, col, style, text, kind, color?]` (`kind`: `n`, `s`, `b`, `e`
136
- * or `''` for an empty cell with a format; `style` indexes `workbook.styleView().xfs`)
146
+ * language `locale`, as `[row, col, style, text, kind, color?, number?]` (`kind`: `n`, `s`,
147
+ * `b`, `e` or `''` for an empty cell with a format; `style` indexes
148
+ * `workbook.styleView().xfs`; `number` is the value of `n` cells, dates as serial numbers)
137
149
  * @param {string} [locale]
138
150
  */
139
151
  cellViews(locale = 'en') {
@@ -183,6 +195,114 @@ export class Sheet {
183
195
  call(() => this.workbook._engine.set_column_width(this.index, columnNumber(a), columnNumber(b), width));
184
196
  }
185
197
 
198
+ /**
199
+ * Moves whole rows, as Excel's "cut and insert": `count` rows from `first` go before the row
200
+ * `before` (numbered as they are now). Formats, heights, merged cells, formulas of the whole
201
+ * workbook, defined names, charts, tables, validations, conditional formats, images and
202
+ * comments follow. Throws, and changes nothing, when it would tear merged cells, an array
203
+ * formula, a table or a filter. Returns where the first row ends up.
204
+ * @param {number} first
205
+ * @param {number} count
206
+ * @param {number} before
207
+ */
208
+ moveRows(first, count, before) {
209
+ const [a, n, b] = [lineNumber(first, 'La fila'), lineNumber(count, 'La cantidad'), lineNumber(before, 'El destino')];
210
+ return call(() => this.workbook._engine.move_rows(this.index, a, n, b));
211
+ }
212
+
213
+ /**
214
+ * Moves whole columns (see `moveRows`); `first` and `before` are letters (`'B'`) or numbers
215
+ * @param {string | number} first
216
+ * @param {number} count
217
+ * @param {string | number} before
218
+ */
219
+ moveColumns(first, count, before) {
220
+ const [a, n, b] = [columnNumber(first), lineNumber(count, 'La cantidad'), columnNumber(before)];
221
+ return call(() => this.workbook._engine.move_columns(this.index, a, n, b));
222
+ }
223
+
224
+ /**
225
+ * Inserts `count` empty rows before the row `before`, with the height and format of the one
226
+ * above. Formulas, names, charts and everything that `moveRows` follows are updated; ranges and
227
+ * merged cells that the new rows fall inside grow, also tables. Returns `before`.
228
+ * @param {number} before
229
+ * @param {number} [count]
230
+ */
231
+ insertRows(before, count = 1) {
232
+ const [b, n] = [lineNumber(before, 'La fila'), lineNumber(count, 'La cantidad')];
233
+ return call(() => this.workbook._engine.insert_rows(this.index, b, n));
234
+ }
235
+
236
+ /** Inserts empty columns before `before` (letters or number), like the one to their left */
237
+ insertColumns(before, count = 1) {
238
+ const [b, n] = [columnNumber(before), lineNumber(count, 'La cantidad')];
239
+ return call(() => this.workbook._engine.insert_columns(this.index, b, n));
240
+ }
241
+
242
+ /**
243
+ * Deletes `count` rows from `first`. What pointed only at them becomes `#REF!`; ranges and
244
+ * merged cells shrink, also tables. Returns `first`.
245
+ * @param {number} first
246
+ * @param {number} [count]
247
+ */
248
+ deleteRows(first, count = 1) {
249
+ const [a, n] = [lineNumber(first, 'La fila'), lineNumber(count, 'La cantidad')];
250
+ return call(() => this.workbook._engine.delete_rows(this.index, a, n));
251
+ }
252
+
253
+ /** Deletes columns from `first` (letters or number), see `deleteRows` */
254
+ deleteColumns(first, count = 1) {
255
+ const [a, n] = [columnNumber(first), lineNumber(count, 'La cantidad')];
256
+ return call(() => this.workbook._engine.delete_columns(this.index, a, n));
257
+ }
258
+
259
+ /**
260
+ * Height of a row or rows (`3`, `'3:5'`) in points; `null` goes back to the automatic height
261
+ * @param {string | number} rows
262
+ * @param {number | null} height
263
+ */
264
+ setRowHeight(rows, height) {
265
+ const [a, b = a] = typeof rows === 'string' ? rows.split(':').map(Number) : [rows];
266
+ if (height !== null && (typeof height !== 'number' || !Number.isFinite(height))) throw new TypeError('El alto debe ser un número de puntos o null.');
267
+ const [first, last] = [lineNumber(a, 'La fila'), lineNumber(b, 'La fila')];
268
+ call(() => this.workbook._engine.set_row_height(this.index, first, last, height ?? undefined));
269
+ }
270
+
271
+ /**
272
+ * Hides or shows rows (`3`, `'3:5'`); they keep their cells and height
273
+ * @param {string | number} rows
274
+ * @param {boolean} hidden
275
+ */
276
+ setRowsHidden(rows, hidden) {
277
+ const [a, b = a] = typeof rows === 'string' ? rows.split(':').map(Number) : [rows];
278
+ const [first, last] = [lineNumber(a, 'La fila'), lineNumber(b, 'La fila')];
279
+ call(() => this.workbook._engine.set_rows_hidden(this.index, first, last, !!hidden));
280
+ }
281
+
282
+ /**
283
+ * Hides or shows columns (`'B'`, `'B:D'`, `2`); they keep their cells and width
284
+ * @param {string | number} columns
285
+ * @param {boolean} hidden
286
+ */
287
+ setColumnsHidden(columns, hidden) {
288
+ const [a, b = a] = typeof columns === 'string' ? columns.split(':') : [columns];
289
+ const [first, last] = [columnNumber(a), columnNumber(b)];
290
+ call(() => this.workbook._engine.set_columns_hidden(this.index, first, last, !!hidden));
291
+ }
292
+
293
+ /**
294
+ * Fills `target` from `source`, the cells at one of its ends, as Excel's fill handle: formats
295
+ * repeat, formulas move their references, and with `series` (by default) numbers continue their
296
+ * trend, one date goes on day by day, texts ending in a number count on and names of months and
297
+ * days follow their order. Refuses to fill over merged cells.
298
+ * @param {string} source e.g. `A1:A2`
299
+ * @param {string} target e.g. `A1:A20`
300
+ * @param {{series?: boolean}} [options]
301
+ */
302
+ fill(source, target, { series = true } = {}) {
303
+ call(() => this.workbook._engine.fill_range(this.index, source, target, !!series));
304
+ }
305
+
186
306
  /** Merges a range into one cell; only its first cell may hold a value */
187
307
  mergeCells(range) {
188
308
  call(() => this.workbook._engine.merge_cells(this.index, range));
@@ -223,6 +343,16 @@ export class Workbook {
223
343
  return JSON.parse(call(() => this._engine.styles_view_json()));
224
344
  }
225
345
 
346
+ /**
347
+ * Numbers as a cell format shows them: `style` indexes `styleView().xfs`
348
+ * @param {number} style
349
+ * @param {number[]} values
350
+ * @param {string} [locale]
351
+ */
352
+ formatNumbers(style, values, locale = 'en') {
353
+ return call(() => this._engine.format_numbers(style, Float64Array.from(values), locale));
354
+ }
355
+
226
356
  /**
227
357
  * A worksheet by name (ignoring case) or by tab position, from 0
228
358
  * @param {string | number} nameOrIndex