v8 crate breaking changes

This commit is contained in:
SheetJS 2026-01-08 22:51:22 -05:00
parent 1327b2f98c
commit f1a4f192d3
19 changed files with 338 additions and 199 deletions

@ -469,14 +469,16 @@ The result is an array of "simple" objects with no nesting:
## Create a Workbook
With the cleaned dataset, `XLSX.utils.json_to_sheet`[^3] generates a worksheet:
The [`json_to_sheet`](/docs/api/utilities/array#array-of-objects-input) method
can generate a SheetJS worksheet from the cleaned dataset:
```js
const worksheet = XLSX.utils.json_to_sheet(rows);
```
`XLSX.utils.book_new`[^4] creates a new workbook and `XLSX.utils.book_append_sheet`[^5]
appends a worksheet to the workbook. The new worksheet will be called "Dates":
[`XLSX.utils.book_new`](/docs/api/utilities/wb) creates a new workbook and
[`XLSX.utils.book_append_sheet`](/docs/api/utilities/wb) appends a worksheet to
the workbook. The new worksheet will be called "Dates":
```js
const workbook = XLSX.utils.book_new();
@ -503,10 +505,12 @@ cell styling and frozen rows.
<summary><b>Changing Header Names</b> (click to show)</summary>
By default, `json_to_sheet` creates a worksheet with a header row. In this case,
the headers come from the JS object keys: "name" and "birthday".
the headers come from the JS object keys: "name" and "birthday". The headers are
written to the first row.
The headers are in cells `A1` and `B1`. `XLSX.utils.sheet_add_aoa`[^6] can write
text values to the existing worksheet starting at cell `A1`:
[`XLSX.utils.sheet_add_aoa`](/docs/api/utilities/array#array-of-arrays-input)
can overwrite data in the worksheet. The following line will set `A1` to "Name"
and set `B1` to "Birthday":
```js
XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });
@ -518,7 +522,7 @@ XLSX.utils.sheet_add_aoa(worksheet, [["Name", "Birthday"]], { origin: "A1" });
<summary><b>Changing Column Widths</b> (click to show)</summary>
Some of the names are longer than the default column width. Column widths are
set by setting the `"!cols"` worksheet property.[^7]
stored in the [`"!cols"` worksheet property](/docs/csf/features/colprops).
The following line sets the width of column A to approximately 10 characters:
@ -541,9 +545,9 @@ After cleanup, the generated workbook looks like the screenshot below:
## Export a File
`XLSX.writeFile`[^8] creates a spreadsheet file and tries to write it to the
system. In the browser, it will try to prompt the user to download the file. In
NodeJS, it will write to the local directory.
[`XLSX.writeFile`](/docs/api/write-options) creates a spreadsheet file and tries
to write it to the system. In the browser, it will try to prompt the user to
download the file. In NodeJS, it will write to the local directory.
```js
XLSX.writeFile(workbook, "Presidents.xlsx", { compression: true });
@ -1170,9 +1174,3 @@ see a preview of the data. The Numbers app can open the file.
dedicated the content to the public domain. When this demo was last tested,
[^2]: See ["The Executive Branch"](https://github.com/unitedstates/congress-legislators#the-executive-branch)
in the dataset documentation.
[^3]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input)
[^4]: See [`book_new` in "Utilities"](/docs/api/utilities/wb)
[^5]: See [`book_append_sheet` in "Utilities"](/docs/api/utilities/wb)
[^6]: See [`sheet_add_aoa` in "Utilities"](/docs/api/utilities/array#array-of-arrays-input)
[^7]: See ["Column Properties"](/docs/csf/features/colprops)
[^8]: See [`writeFile` in "Writing Files"](/docs/api/write-options)

@ -148,14 +148,15 @@ The file data is stored in an `ArrayBuffer`.
## Parse File
With the file data in hand, `XLSX.read`[^2] parses the workbook:
With the file data in hand, [`XLSX.read`](/docs/api/parse-options) parses the
file and generates a SheetJS workbook object:
```js
const workbook = XLSX.read(file);
```
The `workbook` object follows the "Common Spreadsheet Format"[^3], an in-memory
format for representing workbooks, worksheets, cells, and spreadsheet features.
The `workbook` object follows the ["Common Spreadsheet Format"](/docs/csf/), an
in-memory schema for workbooks, worksheets, cells, and spreadsheet features.
## Explore Dataset
@ -170,8 +171,8 @@ To determine how to process the data, it is best to inspect the file first.
### List Sheet Names
As explained in the "Workbook Object"[^4] section, the `SheetNames` property is
a ordered list of the sheet names in the workbook.
As explained in the ["Workbook Object"](/docs/csf/book) page, the `SheetNames`
property is a list of the sheet names in the workbook.
The following live code block displays an ordered list of the sheet names:
@ -195,21 +196,25 @@ function SheetJSheetNames() {
### Inspect Worksheet Data
The `Sheets` property of the workbook object[^5] is an object whose keys are
sheet names and whose values are sheet objects. For example, the first worksheet
is pulled by indexing `SheetNames` and using the name to index `Sheets`:
The [`Sheets` property of the workbook object](/docs/csf/book) is an object
whose keys are sheet names and whose values are sheet objects.
For example, the first worksheet can be pulled by indexing `SheetNames` and
using the name to index `Sheets`:
```js
var first_sheet = workbook.Sheets[workbook.SheetNames[0]];
```
The actual worksheet object can be inspected directly[^6], but it is strongly
recommended to use utility functions to present JS-friendly data structures.
The [worksheet object](/docs/csf/sheet) can be inspected directly, but it is
strongly recommended to use utility functions to extract relevant data.
### Preview HTML
The `sheet_to_html` utility function[^7] generates an HTML table from worksheet
objects. The following live example shows the first 20 rows of data in a table:
The [`sheet_to_html` function](/docs/api/utilities/html#html-table-output)
generates an HTML table from worksheet objects.
The following live example shows the first 20 rows of data in a table:
<details>
<summary><b>Live example</b> (click to show)</summary>
@ -254,7 +259,8 @@ The key points from looking at the table are:
### Extract Raw Data
`XLSX.utils.sheet_to_json`[^8] generates arrays of data from worksheet objects.
The [`sheet_to_json` function](/docs/api/utilities/array#array-output) generates
arrays of data from worksheet objects.
For a complex layout like this, it is easiest to generate an "array of arrays"
where each row is an array of cell values. The screenshot shows rows 5-8:
@ -277,7 +283,7 @@ Row 7 includes the data for FY2007:
```
`XLSX.utils.sheet_to_json` will generate an array of arrays if the option
`header: 1` is specified[^9]:
[`header: 1`](/docs/api/utilities/array#array-output) is specified:
```js
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
@ -332,7 +338,9 @@ function SheetJSAoAHoles() {
</details>
The worksheet `!merges` property[^10] includes every merge range in the sheet.
The [worksheet `!merges` property](/docs/csf/features/merges) includes every
merge range in the sheet.
It is possible to loop through every merge block and fill cells, but in this
case it is easier to post-process the raw data:
@ -539,7 +547,7 @@ Looking at the headers:
![Rows 5-8](pathname:///sl.png)
The desired data is in column `I`. The column index can be calculated using
`XLSX.utils.decode_col`[^11].
the [`decode_col` utility function](/docs/csf/general#column-names).
<details>
<summary><b>Column Index calculation</b> (click to show)</summary>
@ -617,7 +625,7 @@ At this point, `objects` is an array of objects.
### ReactJS
The live demos in this example use ReactJS. In ReactJS, arrays of objects are
best presented in simple HTML tables[^12]:
best presented in [simple tables](/docs/demos/frontend/react#array-of-objects):
```jsx
<table>
@ -1053,7 +1061,7 @@ Press `a` to run on Android.
:::info Device Testing
The demo also runs on real Android devices! After enabling USB debugging[^13],
The demo also runs on real Android devices! After enabling USB debugging[^2],
the Android device can be connected to the computer with a USB cable.
:::
@ -1088,15 +1096,4 @@ When the app is loaded, the data will be displayed in rows.
</Tabs>
[^1]: The dataset URL has changed many times over the years. The current location for the CC0-licensed dataset can be found by [searching for "National Student Loan Data System" on `data.gov`](https://catalog.data.gov/dataset/?q=national+student+loan+data+system&publisher=Office+of+Federal+Student+Aid+%28FSA%29&organization=ed-gov). `PortfolioSummary.xls` is the file name within the dataset.
[^2]: See [`read` in "Reading Files"](/docs/api/parse-options)
[^3]: See ["SheetJS Data Model"](/docs/csf/)
[^4]: See ["Workbook Object"](/docs/csf/book)
[^5]: See ["Workbook Object"](/docs/csf/book)
[^6]: See ["Sheet Objects"](/docs/csf/sheet)
[^7]: See [`sheet_to_html` in "Utilities"](/docs/api/utilities/html#html-table-output)
[^8]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
[^9]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
[^10]: See ["Merged Cells" in "SheetJS Data Model"](/docs/csf/features/merges)
[^11]: See ["Column Names" in "Addresses and Ranges"](/docs/csf/general#column-names)
[^12]: See ["Array of Objects" in "ReactJS"](/docs/demos/frontend/react#array-of-objects)
[^13]: See ["Running on Device"](https://reactnative.dev/docs/running-on-device) in the React Native documentation for more details.
[^2]: See ["Running on Device"](https://reactnative.dev/docs/running-on-device) in the React Native documentation for more details.

@ -98,11 +98,40 @@ Each browser demo was tested in the following environments:
### XMLHttpRequest
For downloading data, the `arraybuffer` response type generates an `ArrayBuffer`
that can be viewed as an `Uint8Array` and fed to the SheetJS `read` method. For
legacy browsers, the option `type: "array"` should be specified:
`XMLHttpRequest` is a browser API for performing network requests. Files can be
downloaded from external sources in `GET` and `POST` requests.
The `responseType` property controls how `XMLHttpRequest` processes data. The
SheetJS [`read`](/docs/api/parse-options/) method accepts a `type` option that
specifies the data representation. SheetJS and `XMLHttpRequest` options must be
set in tandem.
In modern browsers, when the response type is `"arraybuffer"`, `XMLHttpRequest`
will generate an `ArrayBuffer` of raw data. The SheetJS `read` method directly
processes `ArrayBuffer` objects.
```mermaid
---
title: XMLHttpRequest + SheetJS in modern browsers
---
flowchart LR
file[(workbook\nfile)]
ab[(file bytes\nArrayBuffer)]
wb(((SheetJS\nWorkbook)))
file --> |XMLHttpRequest\nresponseType=&quot;arraybuffer&quot;| ab
ab --> |SheetJS read\n\n|wb
```
The following example fetches a file, generates an HTML `TABLE` from the first
sheet using the [`sheet_to_html`](/docs/api/utilities/html#html-table-output)
method, and inserts the `TABLE` in a `DIV` container:
```html title="Download and parse files with XMLHttpRequest and SheetJS"
<div id="sheetjs-tbl"></div>
<script>
/* This file will be downloaded and processed */
var url = "https://docs.sheetjs.com/pres.numbers";
```js
/* set up an async GET request */
var req = new XMLHttpRequest();
req.open("GET", url, true);
@ -110,11 +139,19 @@ req.responseType = "arraybuffer";
req.onload = function(e) {
/* parse the data when it is received */
var data = new Uint8Array(req.response);
var workbook = XLSX.read(data, {type:"array"});
var workbook = XLSX.read(req.response);
/* DO SOMETHING WITH workbook HERE */
/* generate HTML from the first worksheet */
var first_sheet = workbook.Sheets[workbook.SheetNames[0]];
var table_html = XLSX.utils.sheet_to_html(first_sheet);
/* add to page */
document.getElementById("sheetjs-tbl").innerHTML = table_html;
};
req.send();
</script>
```
<details>
@ -135,7 +172,7 @@ function SheetJSXHRDL() {
req.responseType = "arraybuffer";
req.onload = e => {
/* Parse file */
const wb = XLSX.read(new Uint8Array(req.response));
const wb = XLSX.read(req.response);
const ws = wb.Sheets[wb.SheetNames[0]];
/* Generate HTML */
@ -150,6 +187,35 @@ function SheetJSXHRDL() {
</details>
In browsers that do not support `ArrayBuffer`, including Internet Explorer and
older versions of Firefox, `XMLHttpRequest` will return an array of unsigned
bytes. In this case, the `read` method requires the option `type: "array"`:
```mermaid
---
title: XMLHttpRequest + SheetJS in browsers lacking ArrayBuffer support
---
flowchart LR
file[(workbook\nfile)]
ab[(file bytes\nArray)]
wb(((SheetJS\nWorkbook)))
file --> |XMLHttpRequest\nresponseType=&quot;arraybuffer&quot;| ab
ab --> |SheetJS read\ntype=&quot;array&quot;|wb
```
```js title="Download and parse files in legacy browsers (snippet)"
/* set up an async GET request */
var req = new XMLHttpRequest();
req.open("GET", url, true);
req.responseType = "arraybuffer";
req.onload = function(e) {
/* parse the data when it is received */
var workbook = XLSX.read(req.response, {type: "array"});
/* DO SOMETHING WITH workbook HERE */
};
req.send();
```
### fetch

@ -42,8 +42,8 @@ loaded in NodeJS scripts that use KnexJS.
The KnexJS `select` method[^1] creates a `SELECT` query. The return value is a
Promise that resolves to an array of objects.
The SheetJS `json_to_sheet` method[^2] can generate a worksheet object[^3] from
the array of objects:
The SheetJS [`json_to_sheet`](/docs/api/utilities/array#array-of-objects-input)
method can generate a [worksheet object](/docs/csf/sheet) from the array:
```js
const table_name = "Tabeller1"; // name of table
@ -55,8 +55,9 @@ const aoo = await knex.select("*").from(table_name);
const worksheet = XLSX.utils.json_to_sheet(aoo);
```
A workbook object can be built from the worksheet using utility functions[^4].
The workbook can be exported using the SheetJS `writeFile` method[^5]:
Using [`book_new` and `book_append_sheet`](/docs/api/utilities/wb), a workbook
object can be created. This workbook is typically exported to the filesystem
with the [`writeFile`](/docs/api/write-options) method:
```js
/* create a new workbook and add the worksheet */
@ -69,10 +70,10 @@ XLSX.writeFile(wb, "SheetJSKnexJSExport.xlsx");
### Importing Data
The SheetJS `sheet_to_json` function[^6] takes a worksheet object and generates
an array of objects.
The SheetJS [`sheet_to_json` function](/docs/api/utilities/array#array-output)
accepts a worksheet object and generates an array of objects.
The KnexJS `insert` method[^7] creates `INSERT` queries. The return value is a
The KnexJS `insert` method[^2] creates `INSERT` queries. The return value is a
Promise that resolves when the query is executed:
```js
@ -87,8 +88,8 @@ await knex.insert(aoo).into(table_name);
### Creating a Table
The KnexJS Schema Builder supports creating tables with `createTable`[^8] and
dropping tables with `dropTableIfExists`[^9].
The KnexJS Schema Builder supports creating tables with `createTable`[^3] and
dropping tables with `dropTableIfExists`[^4].
The array of objects can be scanned to determine column names and types.
@ -272,11 +273,6 @@ Older versions of KnexJS do not support the `better-sqlite3` module. The
:::
[^1]: See [`select`](https://knexjs.org/guide/query-builder.html#select) in the KnexJS query builder documentation.
[^2]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input)
[^3]: See ["Sheet Objects"](/docs/csf/sheet) in "SheetJS Data Model" for more details.
[^4]: See ["Workbook Helpers" in "Utilities"](/docs/api/utilities/wb) for details on `book_new` and `book_append_sheet`.
[^5]: See [`writeFile` in "Writing Files"](/docs/api/write-options)
[^6]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
[^7]: See [`insert`](https://knexjs.org/guide/query-builder.html#insert) in the KnexJS query builder documentation.
[^8]: See [`createTable`](https://knexjs.org/guide/schema-builder.html#createtable) in the KnexJS Schema Builder documentation.
[^9]: See [`dropTableIfExists`](https://knexjs.org/guide/schema-builder.html#droptableifexists) in the KnexJS Schema Builder documentation.
[^2]: See [`insert`](https://knexjs.org/guide/query-builder.html#insert) in the KnexJS query builder documentation.
[^3]: See [`createTable`](https://knexjs.org/guide/schema-builder.html#createtable) in the KnexJS Schema Builder documentation.
[^4]: See [`dropTableIfExists`](https://knexjs.org/guide/schema-builder.html#droptableifexists) in the KnexJS Schema Builder documentation.

@ -58,8 +58,8 @@ other PostgreSQL libraries.
`Client#query` returns a Promise that resolves to a result set. The `rows`
property of the result is an array of objects.
The SheetJS `json_to_sheet` method[^2] can generate a worksheet object[^3] from
the array of objects:
The SheetJS [`json_to_sheet`](/docs/api/utilities/array#array-of-objects-input)
method can generate a [worksheet object](/docs/csf/sheet) from the array:
```js
const table_name = "Tabeller1"; // name of table
@ -71,8 +71,8 @@ const res = await client.query(`SELECT * FROM ${table_name}`);
const worksheet = XLSX.utils.json_to_sheet(res.rows);
```
A workbook object can be built from the worksheet using utility functions[^4].
The workbook can be exported using the SheetJS `writeFile` method[^5]:
[Utility functions](/docs/api/utilities/wb) can build a SheetJS workbook object.
The workbook can be exported using the [`writeFile`](/docs/api/write-options):
```js
/* create a new workbook and add the worksheet */
@ -85,8 +85,8 @@ XLSX.writeFile(wb, "SheetJSPGExport.xlsx");
### Importing Data
The SheetJS `sheet_to_json` function[^6] takes a worksheet object and generates
an array of objects.
The SheetJS [`sheet_to_json` function](/docs/api/utilities/array#array-output)
accepts a worksheet object and generates an array of objects.
Queries must be manually generated from the objects. Assuming the field names
in the object match the column headers, a loop can generate `INSERT` queries.
@ -101,7 +101,7 @@ INSERT INTO table_name (?) VALUES (?);
```
Queries are generated manually. To help prevent SQL injection vulnerabilities,
the `pg-format`[^7] module escapes identifiers and fields.
the `pg-format`[^2] module escapes identifiers and fields.
:::
@ -308,7 +308,7 @@ sudo -u postgres createuser -P $USER
sudo -u postgres psql -c "ALTER USER $USER WITH SUPERUSER;"
```
If running the optional user creation steps above, a PostgreSQL password will be required. [^8]
If running the optional user creation steps above, a PostgreSQL password will be required. [^3]
Run the command to start a local database instance.
@ -538,10 +538,5 @@ psql SheetJSPG -c 'SELECT * FROM "Presidents";'
[^1]: See [the official `pg` website](https://node-postgres.com/) for more info.
[^2]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input)
[^3]: See ["Sheet Objects"](/docs/csf/sheet) in "SheetJS Data Model" for more details.
[^4]: See ["Workbook Helpers" in "Utilities"](/docs/api/utilities/wb) for details on `book_new` and `book_append_sheet`.
[^5]: See [`writeFile` in "Writing Files"](/docs/api/write-options)
[^6]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
[^7]: The [`pg-format`](https://npm.im/pg-format) package is available on the public NPM registry. Even though the project is marked as deprecated, the official [`pg` website still recommends `pg-format`](https://node-postgres.com/features/queries#parameterized-query:~:text=use%20pg%2Dformat%20package%20for%20handling%20escaping)
[^8]: PostgreSQL on Linux uses [SCRAM authentication by default, which requires a password](https://www.postgresql.org/docs/current/auth-password.html)
[^2]: The [`pg-format`](https://npm.im/pg-format) package is available on the public NPM registry. Even though the project is marked as deprecated, the official [`pg` website still recommends `pg-format`](https://node-postgres.com/features/queries#parameterized-query:~:text=use%20pg%2Dformat%20package%20for%20handling%20escaping)
[^3]: PostgreSQL on Linux uses [SCRAM authentication by default, which requires a password](https://www.postgresql.org/docs/current/auth-password.html)

@ -53,8 +53,8 @@ to other MariaDB and MySQL libraries.
`Connection#execute` returns a Promise that resolves to a result array. The
first entry of the result is an array of objects.
The SheetJS `json_to_sheet` method[^2] can generate a worksheet object[^3] from
the array of objects:
The SheetJS [`json_to_sheet`](/docs/api/utilities/array#array-of-objects-input)
method can generate a [worksheet object](/docs/csf/sheet) from the array:
```js
const mysql = require("mysql2/promise"), XLSX = require("xlsx");
@ -72,8 +72,9 @@ const [rows, fields] = await conn.execute(`SELECT * FROM ${mysql.escapeId(table_
const worksheet = XLSX.utils.json_to_sheet(rows);
```
A workbook object can be built from the worksheet using utility functions[^4].
The workbook can be exported using the SheetJS `writeFile` method[^5]:
Using [`book_new` and `book_append_sheet`](/docs/api/utilities/wb), a workbook
object can be created. This workbook is typically exported to the filesystem
with the [`writeFile`](/docs/api/write-options) method:
```js
/* create a new workbook and add the worksheet */
@ -86,8 +87,8 @@ XLSX.writeFile(wb, "SheetJSMariaDBExport.xlsx");
### Importing Data
The SheetJS `sheet_to_json` function[^6] takes a worksheet object and generates
an array of objects.
The SheetJS [`sheet_to_json` function](/docs/api/utilities/array#array-output)
accepts a worksheet object and generates an array of objects.
Queries must be manually generated from the objects. Assuming the field names
in the object match the column headers, a loop can generate `INSERT` queries.
@ -102,7 +103,7 @@ INSERT INTO table_name (?) VALUES (?);
```
Queries are generated manually. To help prevent SQL injection vulnerabilities,
the undocumented `escapeId` method [^7] escapes identifiers and fields.
the undocumented `escapeId` method [^2] escapes identifiers and fields.
:::
@ -408,9 +409,4 @@ The output should be consistent with the following table:
```
[^1]: See [the official `mysql2` website](https://sidorares.github.io/node-mysql2/docs) for more info.
[^2]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input)
[^3]: See ["Sheet Objects"](/docs/csf/sheet) in "SheetJS Data Model" for more details.
[^4]: See ["Workbook Helpers" in "Utilities"](/docs/api/utilities/wb) for details on `book_new` and `book_append_sheet`.
[^5]: See [`writeFile` in "Writing Files"](/docs/api/write-options)
[^6]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
[^7]: The `mysql2` connector library `escapeId` method is not mentioned in the documentation but is present in the TypeScript definitions.
[^2]: The `mysql2` connector library `escapeId` method is not mentioned in the documentation but is present in the TypeScript definitions.

@ -45,8 +45,10 @@ a row in the table.
#### Importing Data
Data stored in an array of objects can be added to MongoDB Collections using
`Collection#insertMany`[^1]. The SheetJS `sheet_to_json` method[^2] can generate
data from worksheets:
`Collection#insertMany`[^1].
The SheetJS [`sheet_to_json` method](/docs/api/utilities/array#array-output) can
generate data from worksheets:
```js
/* import data from a worksheet to a collection */
@ -54,15 +56,15 @@ const aoo = XLSX.utils.sheet_to_json(ws);
await collection.insertMany(aoo, {ordered: true});
```
Typically worksheet objects are extracted from workbook objects[^3] generated
from the SheetJS `read` or `readFile` methods[^4].
Typically worksheets are extracted from [workbook objects](/docs/csf/book) that
are created by SheetJS [`read` or `readFile`](/docs/api/parse-options) methods.
#### Exporting Data
`Collection#find`[^5] can pull an array of objects from a MongoDB Collection.
`Collection#find`[^2] can pull an array of objects from a MongoDB Collection.
The SheetJS `json_to_sheet` method[^6] can take the result and generate a
worksheet object.
The SheetJS [`json_to_sheet`](/docs/api/utilities/array#array-of-objects-input)
method can take the result and generate a worksheet object.
:::info pass
@ -79,13 +81,13 @@ const aoo = await collection.find({}, {projection:{_id:0}}).toArray();
const ws = utils.json_to_sheet(aoo);
```
Using `book_new` and `book_append_sheet`[^7], a workbook object can be created.
This workbook is typically exported to the filesystem with `writeFile`[^8].
Using [`book_new` and `book_append_sheet`](/docs/api/utilities/wb), a workbook
object can be created. This workbook is typically exported to the filesystem
with the [`writeFile`](/docs/api/write-options) method.
## Complete Example
0) Install a MongoDB-compatible server. Options include MongoDB CE[^9] and
FerretDB[^10]
0) Install a MongoDB-compatible server (MongoDB CE[^3] or FerretDB[^4]).
1) Start a server on `localhost` (follow official instructions).
@ -247,12 +249,6 @@ There should be no errors in the terminal. The script will generate the file
`SheetJSMongoCRUD.xlsx`. That file can be opened in a spreadsheet editor.
[^1]: See [`insertMany`](https://mongodb.github.io/node-mongodb-native/5.7/classes/Collection.html#insertMany) in the MongoDB documentation.
[^2]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
[^3]: See ["Workbook Object"](/docs/csf/book)
[^4]: See [`read` and `readFile` in "Reading Files"](/docs/api/parse-options)
[^5]: See [`find`](https://mongodb.github.io/node-mongodb-native/5.7/classes/Collection.html#find) in the MongoDB documentation.
[^6]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input)
[^7]: See ["Workbook Helpers" in "Utilities"](/docs/api/utilities/wb) for details on `book_new` and `book_append_sheet`.
[^8]: See [`writeFile` in "Writing Files"](/docs/api/write-options)
[^9]: See ["Install MongoDB Community Edition"](https://www.mongodb.com/docs/manual/administration/install-community/#std-label-install-mdb-community-edition) in the MongoDB documentation.
[^10]: See ["SQLite Setup with Docker Compose"](https://docs.ferretdb.io/quickstart-guide/docker/#sqlite-setup-with-docker-compose) in the FerretDB documentation.
[^2]: See [`find`](https://mongodb.github.io/node-mongodb-native/5.7/classes/Collection.html#find) in the MongoDB documentation.
[^3]: See ["Install MongoDB Community Edition"](https://www.mongodb.com/docs/manual/administration/install-community/#std-label-install-mdb-community-edition) in the MongoDB documentation.
[^4]: See ["SQLite Setup with Docker Compose"](https://docs.ferretdb.io/quickstart-guide/docker/#sqlite-setup-with-docker-compose) in the FerretDB documentation.

@ -29,13 +29,13 @@ This demo was tested in the following environments:
| PouchDB | Date |
|:--------|:----------:|
| `9.0.0` | 2025-01-19 |
| `8.0.1` | 2025-01-19 |
| `7.3.1` | 2025-01-19 |
| `6.4.3` | 2025-01-19 |
| `5.4.5` | 2025-01-19 |
| `4.0.3` | 2025-01-19 |
| `3.6.0` | 2025-01-19 |
| `9.0.0` | 2026-01-08 |
| `8.0.1` | 2026-01-08 |
| `7.3.1` | 2026-01-08 |
| `6.4.3` | 2026-01-08 |
| `5.4.5` | 2026-01-08 |
| `4.0.3` | 2026-01-08 |
| `3.6.0` | 2026-01-08 |
:::
@ -53,20 +53,18 @@ The `PouchDB` constructor returns a `Database` object.
#### Importing Data
`Database#bulkDocs`[^2] is the standard approach for bulk data import. The method
accepts "arrays of objects" that can be generated through the SheetJS
`sheet_to_json`[^3] method.
accepts "arrays of objects" that can be generated using the SheetJS
[`sheet_to_json`](/docs/api/utilities/array#array-output) method.
If rows do not include the `_id` parameter, the database will automatically
assign an ID per row. It is strongly recommended to generate the `_id` directly.
This method starts from a SheetJS workbook object[^4] and uses data from the
first sheet. `read` and `readFile`[^5] can generate workbook objects from files.
```js
async function push_first_sheet_to_pouchdb(db, wb, _id_) {
/* get first worksheet */
const ws = wb.Sheets[wb.SheetNames[0]];
This method starts from a SheetJS [worksheet object](/docs/csf/sheet) and uses
data from the first sheet. [`read` and `readFile`](/docs/api/parse-options) can
generate workbook objects from files.
```js title="Extract data from a SheetJS worksheet and push to PouchDB"
async function push_sheet_to_pouchdb(db, ws, _id_) {
/* generate array of objects */
const aoo = XLSX.utils.sheet_to_json(ws);
@ -86,14 +84,15 @@ Existing data can be erased with `Database#destroy`.
#### Exporting Data
`Database#allDocs`[^6] is the standard approach for bulk data export. Generated
`Database#allDocs`[^3] is the standard approach for bulk data export. Generated
row objects have additional `_id` and `_rev` keys that should be removed.
After removing the PouchDB internal fields, the SheetJS `json_to_sheet`[^7]
method can generate a worksheet. Other utility functions[^8] can construct a
workbook. The workbook can be exported with the SheetJS `writeFile`[^9] method:
The SheetJS [`json_to_sheet`](/docs/api/utilities/array#array-of-objects-input)
method can generate a worksheet. [API functions](/docs/api/utilities/wb) can
generate a workbook object. The [`writeFile`](/docs/api/write-options) method
will attempt to download the generated workbook:
```js
```js title="Extract data from PouchDB and export to XLSX using SheetJS"
function export_pouchdb_to_xlsx(db) {
/* fetch all rows, including the underlying data */
db.allDocs({include_docs: true}, function(err, doc) {
@ -171,8 +170,8 @@ cd getting-started-todo-master
<script src="https://cdn.sheetjs.com/xlsx-${current}/package/dist/xlsx.full.min.js"></script>
<button id="xport">Export!</button>
<!-- highlight-end -->
<section id="todoapp">`}
</CodeBlock>
<section id="todoapp">
`}</CodeBlock>
3) Near the end of `index.html`, look for a script tag referencing a CDN:
@ -180,7 +179,7 @@ cd getting-started-todo-master
<script src="//cdn.jsdelivr.net/pouchdb/3.2.0/pouchdb.min.js"></script>
```
Upgrade PouchDB by changing the `src` attribute to the production build[^10]:
Upgrade PouchDB by changing the `src` attribute to the production build[^4]:
```html title="index.html (replace line)"
<script src="//cdn.jsdelivr.net/npm/pouchdb@8.0.1/dist/pouchdb.min.js"></script>
@ -255,11 +254,5 @@ export named "SheetJSPouch.xlsx"
[^1]: See ["Setting up PouchDB"](https://pouchdb.com/guides/setup-pouchdb.html) in the PouchDB documentation.
[^2]: See ["Create/update a batch of documents"](https://pouchdb.com/api.html#batch_create) in the PouchDB API documentation
[^3]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
[^4]: See ["SheetJS Data Model"](/docs/csf)
[^5]: See [`read` in "Reading Files"](/docs/api/parse-options)
[^6]: See ["Fetch a batch of documents"](https://pouchdb.com/api.html#batch_fetch) in the PouchDB API documentation
[^7]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input)
[^8]: See ["Workbook Helpers" in "Utilities"](/docs/api/utilities/wb) for details on `book_new` and `book_append_sheet`.
[^9]: See [`writeFile` in "Writing Files"](/docs/api/write-options)
[^10]: The ["Quick Start" section of "Download"](https://pouchdb.com/download.html#file) in the PouchDB website describes the recommended CDN for PouchDB scripts.
[^3]: See ["Fetch a batch of documents"](https://pouchdb.com/api.html#batch_fetch) in the PouchDB API documentation
[^4]: The ["Quick Start" section of "Download"](https://pouchdb.com/download.html#file) in the PouchDB website describes the recommended CDN for PouchDB scripts.

@ -8,7 +8,7 @@ pagination_next: demos/extensions/index
import current from '/version.js';
import CodeBlock from '@theme/CodeBlock';
[Deno Deploy](https://dash.deno.com/) offers distributed "Serverless Functions"
[Deno Deploy](https://deno.com/deploy) offers distributed "Serverless Functions"
powered by Deno.
[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing
@ -22,7 +22,7 @@ types of spreadsheets to HTML tables and CSV rows.
:::caution pass
When the demo was last tested, Deno Deploy required a GitHub account.
When the demo was last tested, Deno Deploy required a GitHub or Google account.
:::
@ -82,9 +82,9 @@ class SheetJSResource extends Drash.Resource {
## Demo
0) Create a new GitHub account or sign into an existing account.
0) Create a new Github or Google account, or sign into an existing account.
1) Open the [main Deno Deploy portal](https://dash.deno.com/) in a browser.
1) Open the [main Deno Deploy portal](https://console.deno.com/) in a browser.
2) If the account never signed into Deno Deploy, click "Continue with Github".

@ -1115,7 +1115,7 @@ This demo was tested in the following deployments:
| `darwin-x64` | `2.2.1` | 2025-03-31 |
| `darwin-arm` | `2.2.1` | 2025-03-31 |
| `win11-x64` | `2.2.1` | 2025-04-17 |
| `linux-x64` | `2.2.1` | 2025-04-18 |
| `linux-x64` | `2.2.1` | 2026-01-08 |
| `linux-arm` | `2.2.1` | 2025-04-18 |
:::

@ -184,7 +184,7 @@ cd /usr/local/lib
:::note pass
If this step throws a permission error, run the following commands:
If this step throws an error, run the following commands to fix permissions:
```bash
sudo mkdir -p /usr/local/lib
@ -1078,18 +1078,14 @@ may not work on every platform.
The `v8` crate[^6] provides binary builds and straightforward bindings. The Rust
code is similar to the C++ code.
Pulling data from an `ArrayBuffer` back into Rust involves an unsafe operation:
Pulling data from an `ArrayBuffer` back into Rust involves an unsafe conversion
from a raw pointer and byte length to `Vec<u8>`:
```rust
/* assuming JS code returns an ArrayBuffer, copy result to a Vec<u8> */
fn eval_code_ab(scope: &mut v8::HandleScope, code: &str) -> Vec<u8> {
let source = v8::String::new(scope, code).unwrap();
let script = v8::Script::compile(scope, source, None).unwrap();
let result: v8::Local<v8::ArrayBuffer> = script.run(scope).unwrap().try_into().unwrap();
/* In C++, `Data` returns a pointer. Collecting data into Vec<u8> is unsafe */
```rust title="Pull ArrayBuffer data from V8 to Rust vector of bytes"
fn pull_ab_to_vecu8(ab: v8::Local<v8::ArrayBuffer>) -> Vec<u8> {
unsafe { return std::slice::from_raw_parts_mut(
result.data().unwrap().cast::<u8>().as_ptr(),
result.byte_length()
ab.data().unwrap().cast::<u8>().as_ptr(),
ab.byte_length()
).to_vec(); }
}
```
@ -1103,7 +1099,7 @@ This demo was last tested in the following deployments:
| `darwin-x64` | `136.0.0` | 2025-04-21 |
| `darwin-arm` | `134.3.0` | 2025-02-13 |
| `win11-x64` | `137.1.0` | 2025-05-11 |
| `linux-x64` | `137.2.0` | 2025-06-16 |
| `linux-x64` | `142.2.0` | 2026-01-08 |
| `linux-arm` | `134.4.0` | 2025-02-15 |
:::
@ -1177,13 +1173,12 @@ curl.exe -L -o src/main.rs https://docs.sheetjs.com/v8/main.rs
:::info pass
There was a breaking change in version `0.102.0` affecting `v8::Context::new`.
When targeting older versions of the crate, remove the second argument:
There were multiple breaking changes in the `v8` crate. This example was tested
against version `142.2.0`.
```rust title="src/main.rs"
let context = v8::Context::new(handle_scope); // v8 <= 0.101.0
//let context = v8::Context::new(handle_scope, Default::default()); // v8 >= 0.102.0
```
Older versions of this demo script were designed for older `v8` versions. They
are available in the source repo for the documentation. Please reach out to the
[SheetJS chat](https://sheetjs.com/chat) for more details.
:::

@ -82,7 +82,8 @@ Boa supports `ArrayBuffer` natively. This snippet reads data from a file into
```rust
/* read file */
let data: Vec<u8> = std::fs::read("pres.xlsx").unwrap();
let array: boa_engine::object::builtins::JsArrayBuffer = boa_engine::object::builtins::JsArrayBuffer::from_byte_block(file, context).unwrap();
let aligned = boa_engine::object::builtins::AlignedVec::from_iter(0, file.into_iter());
let array: boa_engine::object::builtins::JsArrayBuffer = boa_engine::object::builtins::JsArrayBuffer::from_byte_block(aligned, context).unwrap();
let attrs = boa_engine::property::Attribute::WRITABLE | boa_engine::property::Attribute::ENUMERABLE | boa_engine::property::Attribute::CONFIGURABLE;
let _ = context.register_global_property(boa_engine::js_string!("buf"), array, attrs);
@ -108,7 +109,7 @@ This demo was tested in the following deployments:
| `darwin-arm` | `0.20.0` | 2025-09-03 |
| `win11-x64` | `0.20.0` | 2025-04-28 |
| `win11-arm` | `0.20.0` | 2025-02-23 |
| `linux-x64` | `0.20.0` | 2025-04-21 |
| `linux-x64` | `0.21.0` | 2026-01-08 |
| `linux-arm` | `0.20.0` | 2025-02-15 |
:::

@ -8,6 +8,23 @@ hide_table_of_contents: true
Demos include complete examples and short discussions. For features that can
run in the web browser, demos will include interactive examples.
:::info pass
**These demos were tested or verified by SheetJS teammates!**
Many demos were borne from requests by [SheetJS Pro](https://sheetjs.com/pro)
customers and written based on experience from interactive debugging sessions.
When possible, SheetJS teammates test demos on local infrastructure. Some cloud
service tests were verified in screen-share sessions.
The web ecosystem moves fast and break things. Demos that worked a week ago may
not work today if a dependency makes a breaking change. Please leave a note in
the [issue tracker](https://git.sheetjs.com/sheetjs/docs.sheetjs.com/issues) if
a demo does not currently work.
:::
:::tip pass
If a demo for a library or framework is not included here, please leave a note

@ -26,7 +26,7 @@ HTML format and HTML table utilities.
var html = XLSX.utils.sheet_to_html(ws, opts);
```
The `sheet_to_html` method generates HTML strings from SheetJS worksheets[^1].
The `sheet_to_html` method generates HTML strings from [SheetJS worksheet objects](/docs/csf/sheet).
The following options are supported:
@ -513,5 +513,3 @@ browser. ["Browser Automation"](/docs/demos/net/headless) covers some browsers.
Some ecosystems provide DOM-like frameworks that are compatible with SheetJS.
Examples are included in the ["Synthetic DOM"](/docs/demos/net/dom) demo
[^1]: See ["Worksheet Object" in "SheetJS Data Model"](/docs/csf/sheet) for more details.

@ -48,7 +48,7 @@ Alias variables (supported in DTA versions 120-121) are not processed.
This demo fetches a [sample DTA file](pathname:///dta/pres.dta), parses the data
using the SheetJS DTA Codec and displays the data in an HTML table using the
`sheet_to_html` method[^1].
[`sheet_to_html` utility function](/docs/api/utilities/html#html-table-output):
:::tip pass
@ -97,5 +97,3 @@ function SheetJSDTA() {
</> );
}
```
[^1]: See [`sheet_to_html` in "Utilities"](/docs/api/utilities/html#html-table-output)

@ -31,9 +31,10 @@ fn main() {
let path: String = iter.nth(1).expect("must specify a file name");
let file: Vec<u8> = std::fs::read(path.clone()).unwrap();
/* push data to boa */
let array: boa_engine::object::builtins::JsArrayBuffer = boa_engine::object::builtins::JsArrayBuffer::from_byte_block(file, context).unwrap();
let aligned = boa_engine::object::builtins::AlignedVec::from_iter(0, file.into_iter());
let array: boa_engine::object::builtins::JsArrayBuffer = boa_engine::object::builtins::JsArrayBuffer::from_byte_block(aligned, context).unwrap();
let attrs = boa_engine::property::Attribute::WRITABLE | boa_engine::property::Attribute::ENUMERABLE | boa_engine::property::Attribute::CONFIGURABLE;
let _ = context.register_global_property(boa_engine::js_string!("buf"), array, attrs);
}

@ -1,6 +1,6 @@
/*! sheetjs (C) SheetJS -- https://sheetjs.com */
/* run code, get result as a Rust String */
fn eval_code(scope: &mut v8::HandleScope, code: &str) -> std::string::String {
fn eval_code(scope: &mut v8::ContextScope<v8::HandleScope>, code: &str) -> std::string::String {
let source = v8::String::new(scope, code).unwrap();
let script = v8::Script::compile(scope, source, None).unwrap();
let result = script.run(scope).unwrap();
@ -8,7 +8,7 @@ fn eval_code(scope: &mut v8::HandleScope, code: &str) -> std::string::String {
}
/* assuming JS code returns an ArrayBuffer, copy result to a Vec<u8> */
fn eval_code_ab(scope: &mut v8::HandleScope, code: &str) -> Vec<u8> {
fn eval_code_ab(scope: &mut v8::ContextScope<v8::HandleScope>, code: &str) -> Vec<u8> {
let source = v8::String::new(scope, code).unwrap();
let script = v8::Script::compile(scope, source, None).unwrap();
let result: v8::Local<v8::ArrayBuffer> = script.run(scope).unwrap().try_into().unwrap();
@ -26,20 +26,19 @@ fn main() {
v8::V8::initialize();
let isolate = &mut v8::Isolate::new(Default::default());
let handle_scope = &mut v8::HandleScope::new(isolate);
//let context = v8::Context::new(handle_scope); // v8 <= 0.101.0
let context = v8::Context::new(handle_scope, Default::default()); // v8 >= 0.102.0
let context_scope = &mut v8::ContextScope::new(handle_scope, context);
v8::scope!(let handle_scope, isolate);
let context = v8::Context::new(handle_scope, Default::default());
let scope = &mut v8::ContextScope::new(handle_scope, context);
/* load library */
{
let script = std::fs::read_to_string("./xlsx.full.min.js").expect("Error reading xlsx.full.min.js");
let _result = eval_code(context_scope, &script);
let _result = eval_code(scope, &script);
}
/* get version string */
{
let result = eval_code(context_scope, "XLSX.version");
let result = eval_code(scope, "XLSX.version");
println!("SheetJS library version {}", result);
}
@ -48,26 +47,26 @@ fn main() {
let data: Vec<u8> = std::fs::read(path.clone()).unwrap();
let back: v8::UniqueRef<v8::BackingStore> = v8::ArrayBuffer::new_backing_store_from_vec(data);
let shared = back.make_shared();
let ab: v8::Local<v8::ArrayBuffer> = v8::ArrayBuffer::with_backing_store(context_scope, &shared);
let key = v8::String::new(context_scope, "buf").unwrap();
context.global(context_scope).set(context_scope, key.into(), ab.into());
let ab: v8::Local<v8::ArrayBuffer> = v8::ArrayBuffer::with_backing_store(scope, &shared);
let key = v8::String::new(scope, "buf").unwrap();
context.global(scope).set(scope, key.into(), ab.into());
println!("Loaded file {}", path);
}
/* parse workbook and assign to global `wb` property */
{
let _result = eval_code(context_scope, "void (globalThis.wb = XLSX.read(buf))");
let _result = eval_code(scope, "void (globalThis.wb = XLSX.read(buf))");
}
/* print CSV of first worksheet */
{
let result = eval_code(context_scope, "XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]])");
let result = eval_code(scope, "XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[0]])");
println!("{}", result);
}
/* write sheetjsw.xlsb */
{
let result = eval_code_ab(context_scope, "XLSX.write(wb, {type:'array', bookType:'xlsb'})");
let result = eval_code_ab(scope, "XLSX.write(wb, {type:'array', bookType:'xlsb'})");
std::fs::write("sheetjsw.xlsb", result).unwrap();
}
}

91
tests/data/pouchdb.sh Executable file

@ -0,0 +1,91 @@
#!/bin/bash
# https://docs.sheetjs.com/docs/demos/data/pouchdb
cd /tmp
rm -rf sheetjs-pouch
mkdir sheetjs-pouch
cd sheetjs-pouch
# official url is https://github.com/nickcolley/getting-started-todo/archive/master.zip
curl -LO https://docs.sheetjs.com/pouchdb/master.zip
unzip master.zip
cd getting-started-todo-master
awk 'NR>1{print p} {p = $0}' js/app.js > export_code.js
cat >> export_code.js << 'EOF'
document.getElementById("xport").addEventListener("click", function() {
console.log("clicked");
db.allDocs({include_docs: true, descending: true}, function(err, doc) {
const aoo = doc.rows.map(r => {
const { _id, _rev, ...rest } = r.doc;
return rest;
});
const ws = XLSX.utils.json_to_sheet(aoo);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "Sheet1");
XLSX.writeFile(wb, "SheetJSPouch.xlsx");
});
});
})();
EOF
cp export_code.js js/app.js
sed -i.bak 's|<body>|<body><script src="https:\/\/cdn.sheetjs.com\/xlsx-latest\/package\/dist\/xlsx.full.min.js"><\/script><button id="xport">Export!<\/button>|' index.html
npm init -y
npm i --save puppeteer express@4
cat >test.js <<EOF
const puppeteer = require('puppeteer');
const express = require('express');
const app = express();
app.use(express.static('./'));
app.listen(7262, async() => {
await new Promise((res,rej) => setTimeout(res, 1000));
const browser = await puppeteer.launch();
const page = await browser.newPage();
page.on("console", msg => console.log("PAGE LOG:", msg.text()));
await page.setViewport({width: 1920, height: 1080});
const client = await page.target().createCDPSession();
await client.send('Browser.setDownloadBehavior', {
behavior: 'allow',
downloadPath: require("path").resolve('./')
});
page.on('request', req => console.log(req.url()));
await page.goto('http://localhost:7262/');
await new Promise((res,rej) => setTimeout(res, 3000));
await page.focus('#new-todo');
await page.keyboard.type('JS');
await page.keyboard.press('Enter');
await new Promise((res,rej) => setTimeout(res, 1000));
await page.focus('#new-todo');
await page.keyboard.type('Sheet');
await page.keyboard.press('Enter');
await new Promise((res,rej) => setTimeout(res, 1000));
const toggles = await page.\$\$('.toggle');
await toggles[0].click();
await new Promise((res,rej) => setTimeout(res, 1000));
// Click export button
await page.click('#xport');
await new Promise((res,rej) => setTimeout(res, 3000));
await browser.close();
process.exit();
});
EOF
for version in 9.0.0 8.0.1 7.3.1 6.4.3 5.4.5 4.0.3 3.6.0; do
echo "PouchDB $version"
sed -i.bak "s|pouchdb/3.2.0/pouchdb.min.js|npm/pouchdb@${version}/dist/pouchdb.min.js|" index.html
node test.js
npx -y xlsx-cli SheetJSPouch.xlsx | head -n 3
rm -f SheetJSPouch.xlsx
cp index.html.bak index.html
done

@ -6,11 +6,13 @@ rm -rf sheetjs-puppeteer-deno
mkdir sheetjs-puppeteer-deno
cd sheetjs-puppeteer-deno
# NOTE: this was tested against Deno 2.6.4
env PUPPETEER_PRODUCT=chrome deno run -A --unstable https://deno.land/x/puppeteer@16.2.0/install.ts
cat >SheetJSPuppeteer.ts <<EOF
import puppeteer from "https://deno.land/x/puppeteer@16.2.0/mod.ts";
import { decode } from "https://deno.land/std/encoding/base64.ts"
import { decodeBase64 } from "https://deno.land/std/encoding/base64.ts"
/* (1) Load the target page */
const browser = await puppeteer.launch();
@ -35,7 +37,7 @@ const b64 = await page.evaluate(() => {
return XLSX.write(wb, {type: "base64", bookType: "xlsb"});
});
/* (4) write data to file */
Deno.writeFileSync("SheetJSPuppeteer.xlsb", decode(b64));
Deno.writeFileSync("SheetJSPuppeteer.xlsb", decodeBase64(b64));
await browser.close();
EOF