diff --git a/README.md b/README.md
index 50d7bda..1f72139 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,12 @@ do not work in MDX v2. Instead, string literals and concatenation must be used:
{"Foo" + current + ""}
```
+**Tables**
+
+MDX inconsistently requires different indentation levels for `TD` / `TH`, `TR`,
+`THEAD` / `TBODY` / `TFOOT`, and `TABLE` tags. Unconventional indentation is
+intentional.
+
diff --git a/docz/docs/03-demos/02-frontend/08-dojo.md b/docz/docs/03-demos/02-frontend/08-dojo.md
index 434f7b1..66da80d 100644
--- a/docz/docs/03-demos/02-frontend/08-dojo.md
+++ b/docz/docs/03-demos/02-frontend/08-dojo.md
@@ -72,7 +72,11 @@ The script https://docs.sheetjs.com/dojo/dojo.js was fetched from the official
:::note Tested Deployments
-The demos were last tested on 2023-12-04.
+This demo was tested in the following environments:
+
+| Platform | Date |
+|:-------------|:-----------|
+| Chromium 125 | 2024-06-08 |
Demos exclusively using Dojo Core were tested using Dojo Toolkit `1.17.3`.
diff --git a/docz/docs/03-demos/02-frontend/19-bundler/20-parcel.md b/docz/docs/03-demos/02-frontend/19-bundler/20-parcel.md
index 62529dd..f764f89 100644
--- a/docz/docs/03-demos/02-frontend/19-bundler/20-parcel.md
+++ b/docz/docs/03-demos/02-frontend/19-bundler/20-parcel.md
@@ -34,8 +34,8 @@ This demo was tested in the following environments:
| Version | Date |
|:---------|:-----------|
-| `2.10.3` | 2023-12-04 |
-| `1.12.3` | 2023-12-04 |
+| `2.12.0` | 2024-06-08 |
+| `1.12.4` | 2024-06-08 |
:::
@@ -135,7 +135,7 @@ main `index.html` page should load the `index.js` script:
SheetJS export demo
-
+
```
@@ -208,7 +208,7 @@ yarn add https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz`}
3) Run the ParcelJS development server:
```bash
-npx -y parcel@2.10.3 index.html
+npx -y parcel index.html
```
The process will print a URL:
@@ -231,7 +231,7 @@ a web browser and click the "Click to Export!" button to generate a file.
6) Build the production site:
```bash
-npx -y parcel@2.10.0 build index.html
+npx -y parcel build index.html
```
The production site will be stored in the `dist` folder
diff --git a/docz/docs/03-demos/30-cloud/21-gsheet.md b/docz/docs/03-demos/30-cloud/21-gsheet.md
index eb2800e..08c16e3 100644
--- a/docz/docs/03-demos/30-cloud/21-gsheet.md
+++ b/docz/docs/03-demos/30-cloud/21-gsheet.md
@@ -33,8 +33,8 @@ libraries and written to a Google Sheets Document
- "Exporting data": Data in Google Sheets will be pulled into arrays of objects.
A workbook will be assembled and exported to Excel Binary workbooks (XLSB).
-- "Exporting files": SheetJS libraries will read XLSX and ODS files exported by
-Google Sheets and generate CSV rows from every worksheet.
+- "Exporting files": SheetJS libraries will read XLSX files exported by Google
+Sheets and generate CSV rows from every worksheet.
:::danger pass
@@ -53,10 +53,8 @@ referenced APIs will be available in the future.
## Integration Details
-This demo uses the following NodeJS modules:
-
-- `google-auth-library`[^1] simplifies authentication with Google APIs
-- `node-google-spreadsheet`[^2] interacts with Google Sheets v4 API
+This demo uses the Sheets v4 and Drive v3 APIs through the official `googleapis`
+connector module.
:::info Initial Setup
@@ -65,6 +63,31 @@ for an account. The [Complete Example](#complete-example) covers the process.
:::
+### Document Duality
+
+Each Google Sheets document is identified with a unique ID. This ID can be found
+from the Google Sheets edit URL.
+
+The edit URL starts with `https://docs.google.com/spreadsheets/d/` and includes
+`/edit`. The ID is the string of characters between the slashes. For example:
+
+```
+https://docs.google.com/spreadsheets/d/a_long_string_of_characters/edit#gid=0
+ |^^^^^^^^^^^^^^^^^^^^^^^^^^^|--- ID
+```
+
+The same ID is used in Google Drive operations.
+
+The following operations are covered in this demo:
+
+| Operation | API |
+|:------------------------------|:-------|
+| Create Google Sheets Document | Sheets |
+| Add and Remove worksheets | Sheets |
+| Modify data in worksheets | Sheets |
+| Share Sheets with other users | Drive |
+| Generate raw file exports | Drive |
+
### Authentication
It is strongly recommended to use a service account for Google API operations.
@@ -75,52 +98,44 @@ The generated JSON key file includes `client_email` and `private_key` fields.
These fields can be used in JWT authentication:
```js title="JWT Authentication using a JSON key file"
-import { JWT } from 'google-auth-library'
-import { GoogleSpreadsheet } from 'google-spreadsheet';
+import { google } from "googleapis";
// adjust the path to the actual key file.
// highlight-next-line
import creds from './sheetjs-test-726272627262.json' assert { type: "json" };
-const jwt = new JWT({
+/* connect to google services */
+const jwt = new google.auth.JWT({
email: creds.client_email,
key: creds.private_key,
scopes: [
- 'https://www.googleapis.com/auth/spreadsheets',
- 'https://www.googleapis.com/auth/drive.file',
+ 'https://www.googleapis.com/auth/spreadsheets', // Google Sheets
+ 'https://www.googleapis.com/auth/drive.file', // Google Drive
]
});
```
-### Connecting to Documents
+### Connecting to Services
-To connect to existing documents, the document ID must be specified. This ID can
-be found from the edit URL.
+The `google` named export includes special methods to connect to various APIs.
-The edit URL starts with `https://docs.google.com/spreadsheets/d/` and includes
-`/edit`. The ID is the string of characters between the slashes. For example:
+#### Google Sheets
-```
-https://docs.google.com/spreadsheets/d/a_long_string_of_characters/edit#gid=0
- |^^^^^^^^^^^^^^^^^^^^^^^^^^^|--- ID
+```js
+const sheets = google.sheets({ version: "v4", auth: jwt });
```
-The `GoogleSpreadsheet` constructor accepts a document ID and auth object:
+`google.sheets` takes an options argument that includes API version number and
+authentication details.
-```js title="Connect to a document"
-const doc = new GoogleSpreadsheet('DOCUMENT_ID', jwt);
-await doc.loadInfo();
+#### Google Drive
+
+```js
+const drive = google.drive({ version: "v3", auth: jwt });
```
-### Creating New Documents
-
-The `createNewSpreadsheetDocument` makes a request to create a new document. It
-is strongly recommended to create a blank sheet.
-
-```js title="Create a new document with blank sheet"
-const doc = await GoogleSpreadsheet.createNewSpreadsheetDocument(jwt, { title: 'Document Title' });
-const newSheet = await doc.addSheet({ title: 'Sheet Title' });
-```
+`google.drive` takes an options argument that includes API version number and
+authentication details.
### Array of Arrays
@@ -130,8 +145,8 @@ row arrays, and each row array includes data.
SheetJS provides methods for working with Arrays of Arrays:
-- `aoa_to_sheet`[^3] creates SheetJS worksheet objects from arrays of arrays
-- `sheet_to_json`[^4] can generate arrays of arrays from SheetJS worksheets
+- `aoa_to_sheet`[^1] creates SheetJS worksheet objects from arrays of arrays
+- `sheet_to_json`[^2] can generate arrays of arrays from SheetJS worksheets
## Export Document Data
@@ -140,205 +155,274 @@ not natively support the XLSB format. SheetJS fills the gap.
### Convert a Single Sheet
-The idea is to extract the raw data from the Google Sheet headers and combine
-with the raw data rows to produce a large array of arrays.
+`sheets.spreadsheets.values.get` returns data from an existing Google Sheet. The
+method expects a range. Passing the sheet name as the title will pull all rows.
+
+If successful, the response object will have a `data` property. It will be an
+object with a `values` property. The values will be represented as an Array of
+Arrays of values. This array of arrays can be converted to a SheetJS sheet:
```js
-async function wb_append_sheet(sheet, name, wb) {
- /* get the header and data rows */
- await sheet.loadHeaderRow();
- const header = sheet.headerValues;
- const rows = await sheet.getRows();
+async function gsheet_ws_to_sheetjs_ws(id, sheet_name) {
+ /* get values */
+ const res = await sheets.spreadsheets.values.get({
+ spreadsheetId: id,
+ range: `'${sheet_name}'`
+ });
+ const values = res.data.values;
- /* construct the array of arrays */
- const aoa = [header].concat(rows.map(r => r._rawData));
-
- /* generate a SheetJS Worksheet */
- const ws = XLSX.utils.aoa_to_sheet(aoa);
-
- /* add to workbook */
- XLSX.utils.book_append_sheet(wb, ws, name);
+ /* create SheetJS worksheet */
+ const ws = XLSX.utils.aoa_to_sheet(values);
+ return ws;
}
```
### Convert a Workbook
-`doc.sheetsByIndex` is an array of worksheets in the Google Sheet Document. By
-looping across the sheets, the entire workbook can be written:
+`sheets.spreadsheets.get` returns metadata about the Google Sheets document. In
+the result object, the `data` property is an object which has a `sheets`
+property. The value of the `sheets` property is an array of sheet objects.
+
+The SheetJS `book_new` [^3] method creates blank SheetJS workbook objects. The
+`book_append_sheet` [^4] method adds SheetJS worksheet objects to the workbook.
+
+By looping across the sheets, the entire workbook can be converted:
```js
-async function doc_to_wb(doc) {
+async function gsheet_doc_to_sheetjs_wb(doc) {
/* Create a new workbook object */
const wb = XLSX.utils.book_new();
- /* Loop across the Document sheets */
- for(let i = 0; i < doc.sheetsByIndex.length; ++i) {
- const sheet = doc.sheetsByIndex[i];
- /* Get the worksheet name */
- const name = sheet.title;
+ /* Get metadata */
+ const wsheet = await sheets.spreadsheets.get({spreadsheetId: id});
- /* Add sheet to workbook */
- await add_sheet_to_wb(sheet, name, wb);
+ /* Loop across the Document sheets */
+ for(let sheet of wsheet.data.sheets) {
+ /* Get the worksheet name */
+ const name = sheet.properties.title;
+
+ /* Convert Google Docs sheet to SheetJS worksheet */
+ const ws = await gsheet_ws_to_sheetjs_ws(id, name);
+
+ /* Append worksheet to workbook */
+ XLSX.utils.book_append_sheet(wb, ws, name);
}
return wb;
}
```
+This method returns a SheetJS workbook object that can be exported with the
+`writeFile` and `write` methods.[^5]
+
## Update Document Data
The goal is to import data from a NUMBERS file to Google Sheets. Google Sheets
does not natively support the NUMBERS format. SheetJS fills the gap.
-### Clear the Document
+### Create New Document
-Google Sheets does not allow users to delete every worksheet. This function
-deletes every worksheet after the first, then clears the first worksheet:
+`sheets.spreadsheets.create` creates a new Google Sheets document. It can accept
+a document title. It will generate a new workbook with a blank "Sheet1" sheet.
+The response includes the document ID for use in subsequent operations:
-```js
-/* clear google sheets doc */
-async function doc_clear(doc) {
- /* delete all sheets after the first sheet */
- const old_sheets = doc.sheetsByIndex;
- for(let i = 1; i < old_sheets.length; ++i) await old_sheets[i].delete();
-
- /* clear first worksheet */
- old_sheets[0].clear();
-}
-```
-
-### Update First Sheet
-
-There are two steps: "update worksheet name" and "update worksheet data":
-
-#### Update Sheet Name
-
-The worksheet name is assigned by using the `updateProperties` method. The
-desired sheet name is the name of the first worksheet from the file.
-
-```js
-async function doc_update_first_sheet_name(doc, wb) {
- /* get first worksheet name */
- const wsname = wb.SheetNames[0];
-
- /* get first gsheet */
- const sheet = doc.sheetsByIndex[0];
-
- /* update worksheet name */
- await sheet.updateProperties({title: wsname});
-}
-```
-
-#### Update Sheet Data
-
-`sheet.addRows` reads an Array of Arrays of values. `XLSX.utils.sheet_to_json`
-can generate this exact shape with the option `header: 1`. Unfortunately
-Google Sheets requires at least one "Header Row". This can be implemented by
-converting the entire worksheet to an Array of Arrays and setting the header
-row to the first row of the result:
-
-```js
-async function doc_update_first_sheet_data(doc, wb) {
- /* get first worksheet */
- const ws = wb.Sheets[wb.SheetNames[0]];
- /* generate array of arrays from the first worksheet */
- const aoa = XLSX.utils.sheet_to_json(ws, {header: 1});
-
- /* get first gsheet */
- const sheet = doc.sheetsByIndex[0];
- /* set document header row to first row of the AOA */
- await sheet.setHeaderRow(aoa[0]);
-
- /* add the remaining rows */
- await sheet.addRows(aoa.slice(1));
-}
-```
-
-### Append Remaining Worksheets
-
-Each name in the SheetJS Workbook `SheetNames` array maps to a worksheet. The
-list of names not including the first sheet is `wb.SheetNames.slice(1)`.
-
-There are two steps for each sheet: "create new sheet" and "load data".
-
-Due to JavaScript `async` idiosyncrasies, a plain `for` loop must be used:
-
-```js
-async function doc_append_remaining_sheets(doc, wb) {
- const names = wb.SheetNames.slice(1);
-
- /* loop across names */
- for(let i = 0; i < names.length; ++i) {
- /* wb.SheetNames[i] is the sheet name */
- const name = wb.SheetNames[i];
- /* wb.Sheets[name] is the worksheet object */
- const ws = wb.Sheets[name];
-
- /* create new google sheet */
- const sheet = await doc_add_new_sheet(doc, name);
- /* load sheet with data */
- await sheet_load_from_ws(sheet, ws);
+```js title="Create a new document with blank sheet"
+const res = await sheets.spreadsheets.create({
+ requestBody: {
+ properties: {
+ /* Document Title */
+ title: "SheetJS Test"
+ }
}
+});
+const id = res.data.spreadsheetId;
+```
+
+:::caution pass
+
+When using a service worker, the main account does not have access to the new
+document by default. The document has to be shared with the main account using
+the Drive API:
+
+```js title="Sharing the generated sheet with the main account"
+await drive.permissions.create({
+ fileId: id, // this ID was returned in the response to the create request
+ fields: "id",
+ requestBody: {
+ type: "user",
+ role: "writer",
+ emailAddress: "YOUR_ADDRESS@gmail.com" // main address
+ }
+});
+```
+
+:::
+
+### Delete Non-Initial Sheets
+
+Google Sheets does not allow users to delete every worksheet.
+
+The recommended approach involves deleting every worksheet after the first.
+
+The delete operation requires a unique identifier for a sheet within the Google
+Sheets document. These IDs are found in the `sheets.spreadsheets.get` response.
+
+The following snippet performs one bulk operation using `batchUpdate`:
+
+```js title="Deleting non-initial sheets"
+/* get existing sheets */
+const wsheet = await sheets.spreadsheets.get({spreadsheetId: id});
+
+/* remove all sheets after the first */
+if(wsheet.data.sheets.length > 1) await sheets.spreadsheets.batchUpdate({
+ spreadsheetId: id,
+ requestBody: { requests: wsheet.data.sheets.slice(1).map(s => ({
+ deleteSheet: {
+ sheetId: s.properties.sheetId
+ }
+ }))}
+});
+```
+
+### Rename First Sheet
+
+The first sheet must be renamed so that the append operations do not collide
+with the legacy name. Since most SheetJS-supported file formats and most
+spreadsheet applications limit worksheet name lengths to 32 characters, it is
+safe to set a name that exceeds 33 characters.
+
+The `updateSheetProperties` update method can rename individual sheets:
+
+```js title="Rename legacy first sheet"
+/* rename first worksheet to avoid collisions */
+await sheets.spreadsheets.batchUpdate({
+ spreadsheetId: id,
+ requestBody: { requests: [{
+ updateSheetProperties: {
+ fields: "title",
+ properties: {
+ sheetId: wsheet.data.sheets[0].properties.sheetId,
+ // the new title is 34 characters, to be exact
+ title: "thistitleisatleast33characterslong"
+ }
+ }
+ }]}
+});
+```
+
+### Append Worksheets
+
+:::note pass
+
+The [`read` and `readFile` methods](/docs/api/parse-options) generate SheetJS
+workbook objects from existing worksheet files.
+
+:::
+
+Starting from a SheetJS workbook, the `SheetNames` property[^6] is an array of
+worksheet names and the `Sheets` property is an object that maps sheet names to
+worksheet objects.
+
+Looping over the worksheet names, there are two steps to appending a sheet:
+
+1) "Append a blank worksheet": The `addSheet` request, submitted through the
+`sheets.spreadsheets.batchUpdate` method, accepts a new title and creates a new
+worksheet. The new worksheet will be added at the end.
+
+2) "Write data to the new sheet": The SheetJS `sheet_to_json` method with the
+option `header: 1`[^7] will generate an array of arrays of data. This structure
+is compatible with the `sheets.spreadsheets.values.update` operation.
+
+The following snippet pushes all worksheets from a SheetJS workbook into a
+Google Sheets document:
+
+```js title="Push data from a SheetJS workbook to a Google Sheets Document"
+/* add sheets from file */
+for(let name of wb.SheetNames) {
+ /* (1) Create a new Google Sheets sheet */
+ await sheets.spreadsheets.batchUpdate({
+ spreadsheetId: id,
+ requestBody: { requests: [
+ /* add new sheet */
+ { addSheet: { properties: { title: name } } },
+ ] }
+ });
+
+ /* (2) Push data */
+ const aoa = XLSX.utils.sheet_to_json(wb.Sheets[name], {header:1});
+ await sheets.spreadsheets.values.update({
+ spreadsheetId: id,
+ range: `'${name}'!A1`,
+ valueInputOption: "USER_ENTERED",
+ resource: { values: aoa }
+ });
}
```
-#### Add a New Worksheet
+### Delete Initial Sheet
-`doc.addSheet` accepts a properties object that includes the worksheet name:
+After adding new worksheets, the final step involves removing the initial sheet.
-```js
-async function doc_add_new_sheet(doc, name) {
- return await doc.addSheet({title: name});
-}
-```
+The initial sheet ID can be pulled from the worksheet metadata fetched when the
+[non-initial sheets were removed](#delete-non-initial-sheets):
-This creates a new worksheet, sets the tab name, and returns a reference to the
-created worksheet.
-
-#### Update Worksheet Data
-
-```js
-async function sheet_load_from_ws(sheet, ws) {
- /* generate array of arrays from the first worksheet */
- const aoa = XLSX.utils.sheet_to_json(ws, {header: 1});
-
- /* set document header row to first row of the AOA */
- await sheet.setHeaderRow(aoa[0]);
-
- /* add the remaining rows */
- await sheet.addRows(aoa.slice(1));
-}
+```js title="Deleting the Initial sheet"
+/* remove first sheet */
+await sheets.spreadsheets.batchUpdate({
+ spreadsheetId: id,
+ requestBody: { requests: [
+ /* remove old first sheet */
+ { deleteSheet: { sheetId: wsheet.data.sheets[0].properties.sheetId } }
+ ] }
+});
```
## Raw File Exports
-In the web interface, Google Sheets can export documents to `XLSX` or `ODS`. The
-NodeJS library includes similar methods to perform the download[^5]:
+In the web interface, Google Sheets can export documents to `XLSX` or `ODS`.
-| Format | Google Sheets Description | Method |
-|:-------|:--------------------------|:-----------------|
-| XLSX | Microsoft Excel (.xlsx) | `downloadAsXLSX` |
-| ODS | OpenDocument (.ods) | `downloadAsODS` |
+Raw file exports are exposed through the `files.export` method in the Drive API:
-The functions resolve to `Buffer` data. The `Buffer` objects can be parsed using
-the SheetJS `read`[^6] method:
+```js title="Export XLSX workbook"
+const drive = google.drive({ version: "v3", auth: jwt });
+
+/* Request XLSX export */
+const file = await drive.files.export({
+ /* XLSX MIME type */
+ mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ fileId: id
+});
+```
+
+The response object has a `data` field whose value will be a `Blob` object. Data
+can be pulled into an `ArrayBuffer` and passed to the SheetJS `read`[^8] method:
```js
-/* download XLSX */
-const ab = await doc.downloadAsXLSX();
+/* Obtain ArrayBuffer */
+const ab = await file.data.arrayBuffer();
-/* parse */
+/* Parse */
const wb = XLSX.read(buf);
```
-At this point `wb` is a SheetJS workbook object[^7].
+At this point `wb` is a SheetJS workbook object[^9].
## Complete Example
:::note Tested Deployments
-This demo was last tested on 2023 September 17 using `google-auth-library` for
-authentication (`v8.9.0`) and `google-spreadsheet` for API access (`v4.1.0`).
+This demo was last tested on 2024 June 08 using `googleapis` version `140.0.0`.
+The demo uses Sheets v4 and Drive v3 APIs.
+
+:::
+
+:::caution pass
+
+**The Google Cloud web interface changes frequently!**
+
+The screenshots and detailed descriptions may be out of date. Please report any
+issues [to the docs repo](https://git.sheetjs.com/sheetjs/docs.sheetjs.com/) or
+reach out to [the SheetJS Discord server](https://sheetjs.com/chat).
:::
@@ -352,8 +436,11 @@ A valid phone number (for SMS verification) may be required.
:::
-1) Open https://console.cloud.google.com in a web browser. Review the Google
-Cloud Platform Terms of Service.
+1) Open https://console.cloud.google.com in a web browser.
+
+If this is the first time accessing Google Cloud resources, a terms of service
+modal will be displayed. Review the Google Cloud Platform Terms of Service by
+clicking the "Google Cloud Platform Terms of Service" link.
:::danger pass
@@ -361,17 +448,34 @@ You must agree to the Google Cloud Platform Terms of Service to use the APIs.
:::
+Check the box under "Terms of Service" and click "AGREE AND CONTINUE".
+
### Project Setup
-2) Create a new Project.
+:::info pass
-If the account does not have an existing project, click "CREATE PROJECT"
+The goal of this section is to create a new project.
-If the account has an existing project, click the project selector (`:·` icon)
-and click "NEW PROJECT" in the modal.
+:::
+
+2) Open the Project Selector.
+
+In the top bar, between the "Google Cloud" logo and the search bar, there will
+be a selection box. Click the `▼` icon to show the modal.
+
+![Project Selector](pathname:///gsheet/selector.png)
+
+If the selection box is missing, expand the browser window.
+
+3) Click "NEW PROJECT" in the top right corner of the modal.
+
+4) In the New Project screen, enter "SheetJS Test" in the Project name textbox
+and select "No organization" in the Location box. Click "CREATE".
+
+A notification will confirm that the project was created:
+
+![Project notification](pathname:///gsheet/notification.png)
-3) In the New Project screen, enter "SheetJS Test" in the Project name textbox
-and select "No organization" in the Location box. Click "CREATE"
### API Setup
@@ -381,23 +485,34 @@ The goal of this section is to enable Google Sheets API and Google Drive API.
:::
-4) Click the Project Selector (`:·` icon) and select "SheetJS Test"
+5) Open the Project Selector (`▼` icon) and select "SheetJS Test"
-5) In the left sidebar, click "Enabled APIs and services".
+6) In the search bar, type "Enabled" and select "Enabled APIs & services". This
+item will be in the "PRODUCTS & PAGES" part of the search results.
-6) Near the top of the page, click "+ ENABLE APIS AND SERVICES".
+#### Enable Google Sheets API
-7) In the search bar near the middle of the page, type "Sheets" and look for
-"Google Sheets API". Click the card
+7) Near the top of the page, click "+ ENABLE APIS AND SERVICES".
-8) In the Product Details screen, click the blue "ENABLE" button.
+8) In the search bar near the middle of the page (not the search bar at the top),
+type "Sheets" and press Enter.
-9) Click the left arrow (`<-`) next to API/Service details.
+In the results page, look for "Google Sheets API". Click the card
-10) In the search bar near the middle of the page, type "Drive" and look for
-"Google Drive API". Click the card.
+9) In the Product Details screen, click the blue "ENABLE" button.
-11) In the Product Details screen, click the blue "ENABLE" button.
+10) Click the left arrow (`<-`) next to "API/Service details".
+
+#### Enable Google Drive API
+
+11) Near the top of the page, click "+ ENABLE APIS AND SERVICES".
+
+12) In the search bar near the middle of the page (not the search bar at the top),
+type "Drive" and press Enter.
+
+In the results page, look for "Google Drive API". Click the card
+
+13) In the Product Details screen, click the blue "ENABLE" button.
### Service Account Setup
@@ -407,19 +522,21 @@ The goal of this section is to create a service account and generate a JSON key.
:::
+14) Go to https://console.cloud.google.com or click the "Google Cloud" image in
+the top bar.
+
#### Create Service Account
-12) Go to https://console.cloud.google.com.
+15) Click the Project Selector (`:·` icon) and select "SheetJS Test".
-13) Click the Project Selector (`:·` icon) and select "SheetJS Test".
+16) In the search bar, type "Credentials" and select the "Credentials" item with
+subtitle "APIs & Services". This item will be in the "PRODUCTS & PAGES" group:
-14) Click "Dashboard".
+![Credentials](pathname:///gsheet/creds.png)
-15) In the left sidebar, hover over "APIs and Services" and select "Credentials"
+17) Click "+ CREATE CREDENTIALS". In the dropdown, select "Service Account"
-16) Click "+ CREATE CREDENTIALS". In the dropdown, select "Service Account"
-
-17) Enter "SheetJService" for Service account name. Click "CREATE AND CONTINUE"
+18) Enter "SheetJService" for Service account name. Click "CREATE AND CONTINUE"
:::note pass
@@ -427,23 +544,22 @@ The Service account ID is generated automatically.
:::
-18) In Step 2 "Grant this service account access to project", click CONTINUE
+19) In Step 2 "Grant this service account access to project", click CONTINUE
-19) In Step 3 click "DONE". You will be taken back to the credentials screen
+20) In Step 3 click "DONE". You will be taken back to the credentials screen
#### Create JSON Key
-20) Look for "SheetJService" in the "Service Accounts" table and click the email
-address in the row
-
-21) Click the email address of the account in the "Service Accounts" table.
+21) Look for "SheetJService" in the "Service Accounts" table and click the email
+address in the row.
22) Click "KEYS" in the horizontal bar near the top of the page.
23) Click "ADD KEY" and select "Create new key" in the dropdown.
-24) In the popup, select the "JSON" radio button and click "CREATE". The page
-will download a JSON file.
+24) In the popup, select the "JSON" radio button and click "CREATE".
+
+The page will download a JSON file. If prompted, allow the download.
25) Click "CLOSE"
@@ -469,35 +585,27 @@ npm init -y
28) Install dependencies:
{`\
-npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz google-spreadsheet google-auth-library`}
+npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz googleapis`}
-29) Save the following script to `init.mjs`:
+29) Download [`init.mjs`](pathname:///gsheet/init.mjs):
-```js title="init.mjs"
-import { JWT } from 'google-auth-library'
-import { GoogleSpreadsheet } from 'google-spreadsheet';
+```bash
+curl -LO https://docs.sheetjs.com/gsheet/init.mjs
+```
+Edit the marked lines near the top of the file:
+
+```js title="init.mjs (edit highlighted lines)"
+/* Change this import statement to point to the credentials JSON file */
// highlight-next-line
import creds from './sheetjs-test-726272627262.json' assert { type: "json" };
-const jwt = new JWT({
- email: creds.client_email,
- key: creds.private_key,
- scopes: [
- 'https://www.googleapis.com/auth/spreadsheets',
- 'https://www.googleapis.com/auth/drive.file',
- ]
-});
-
-const doc = await GoogleSpreadsheet.createNewSpreadsheetDocument(jwt, { title: 'test from NodeJS' });
-const newSheet = await doc.addSheet({ title: 'SheetJSTest' });
+/* Change this to the primary account address, NOT THE SERVICE ACCOUNT */
// highlight-next-line
-await doc.share('YOUR_ADDRESS@gmail.com');
+const acct = "YOUR_ADDRESS@gmail.com";
```
-Edit the highlighted lines as follows:
-
- `'./sheetjs-test-726272627262.json'` should be replaced with the name of the
JSON file in step 27. The `./` prefix is required!
@@ -510,10 +618,63 @@ address from step 0.
node init.mjs
```
-31) Sign into Google Sheets. A shared document "test from NodeJS" should be
+The script will print three lines:
+
+```
+Created Google Workbook a-long-string-of-characters
+Created Google Worksheets "SheetJS1" and "SheetJS2"
+Shared a-long-string-of-characters with YOUR_ACCOUNT@gmail.com
+```
+
+The long string of characters after "Created Google Workbook" is the ID. Take
+note of this ID.
+
+31) Sign into Google Sheets. A shared document "SheetJS Test" should be
displayed in the table. It will be owned by the service account.
-32) Open the shared document from step 31.
+32) Open the shared document from step 31 and confirm that the document has two
+worksheets named "SheetJS1" and "SheetJS2".
+
+Confirm the worksheet data matches the following screenshots:
+
+
33) Copy the URL and extract the document ID.
@@ -527,6 +688,8 @@ https://docs.google.com/spreadsheets/d/a_long_string_of_characters/edit#gid=0
The ID is a long string of letters and numbers and underscore characters (`_`)
just before the `/edit` part of the URL.
+Confirm that this ID matches the ID printed in step 30.
+
### Load Data from NUMBERS
:::info pass
@@ -542,83 +705,26 @@ NUMBERS file.
curl -LO https://docs.sheetjs.com/pres.numbers
```
-35) Save the following script to `load.mjs`:
+35) Download [`load.mjs`](pathname:///gsheet/load.mjs):
-```js title="load.mjs"
-import { JWT } from 'google-auth-library'
-import { GoogleSpreadsheet } from 'google-spreadsheet';
-
-import { set_fs, readFile, utils } from 'xlsx';
-import * as fs from 'fs';
-set_fs(fs);
-
-// highlight-next-line
-import creds from './sheetjs-test-726272627262.json' assert { type: "json" };
-
-const jwt = new JWT({
- email: creds.client_email,
- key: creds.private_key,
- scopes: [
- 'https://www.googleapis.com/auth/spreadsheets',
- 'https://www.googleapis.com/auth/drive.file',
- ]
-});
-
-// highlight-next-line
-const doc = new GoogleSpreadsheet('DOCUMENT_ID', jwt);
-await doc.loadInfo();
-
-const wb = readFile("pres.numbers");
-
-/* clear workbook */
-{
- /* delete all sheets after the first sheet */
- const old_sheets = doc.sheetsByIndex;
- for(let i = 1; i < old_sheets.length; ++i) {
- await old_sheets[i].delete();
- }
- /* clear first worksheet */
- old_sheets[0].clear();
-}
-
-/* write worksheets */
-{
- const name = wb.SheetNames[0];
- const ws = wb.Sheets[name];
- /* first worksheet already exists */
- const sheet = doc.sheetsByIndex[0];
-
- /* update worksheet name */
- await sheet.updateProperties({title: name});
-
- /* generate array of arrays from the first worksheet */
- const aoa = utils.sheet_to_json(ws, {header: 1});
-
- /* set document header row to first row of the AOA */
- await sheet.setHeaderRow(aoa[0])
-
- /* add the remaining rows */
- await sheet.addRows(aoa.slice(1));
-
- /* the other worksheets must be created manually */
- for(let i = 1; i < wb.SheetNames.length; ++i) {
- const name = wb.SheetNames[i];
- const ws = wb.Sheets[name];
-
- const sheet = await doc.addSheet({title: name});
- const aoa = utils.sheet_to_json(ws, {header: 1});
- await sheet.setHeaderRow(aoa[0])
- await sheet.addRows(aoa.slice(1));
- }
-}
+```bash
+curl -LO https://docs.sheetjs.com/gsheet/load.mjs
```
-Edit the highlighted lines as follows:
+Edit the marked lines near the top of the file:
+
+```js title="load.mjs (edit highlighted lines)"
+/* Change this import statement to point to the credentials JSON file */
+import creds from './sheetjs-test-726272627262.json' assert { type: "json" };
+
+/* Change this to the spreadsheet ID */
+const id = "SOME-SPREADSHEETJS-ID";
+```
- `'./sheetjs-test-726272627262.json'` should be replaced with the name of the
JSON file in step 27. The `./` prefix is required!
-- `'DOCUMENT_ID'` should be replaced with the Document ID from step 33.
+- `"SOME-SPREADSHEETJS-ID"` should be replaced with the Document ID from step 33.
36) Run the script:
@@ -626,7 +732,7 @@ JSON file in step 27. The `./` prefix is required!
node load.mjs
```
-37) Sign into Google Sheets and open the "test from NodeJS" shared document. It
+37) Sign into Google Sheets and open the "SheetJS Test" shared document. It
should show a list of Presidents, matching the contents of the test file.
### Export Data to XLSB
@@ -637,61 +743,26 @@ The goal of this section is to export the raw data from Google Sheets to XLSB.
:::
-38) Save the following script to `dump.mjs`:
+38) Download [`dump.mjs`](pathname:///gsheet/dump.mjs):
-```js title="dump.mjs"
-import { JWT } from 'google-auth-library'
-import { GoogleSpreadsheet } from 'google-spreadsheet';
-
-import { set_fs, writeFile, utils } from 'xlsx';
-import * as fs from 'fs';
-set_fs(fs);
-
-// highlight-next-line
-import creds from './sheetjs-test-726272627262.json' assert { type: "json" };
-
-const jwt = new JWT({
- email: creds.client_email,
- key: creds.private_key,
- scopes: [
- 'https://www.googleapis.com/auth/spreadsheets',
- 'https://www.googleapis.com/auth/drive.file',
- ]
-});
-
-// highlight-next-line
-const doc = new GoogleSpreadsheet('DOCUMENT_ID', jwt);
-await doc.loadInfo();
-
-const wb = utils.book_new();
-
-for(let i = 0; i < doc.sheetsByIndex.length; ++i) {
- const sheet = doc.sheetsByIndex[i];
- const name = sheet.title;
-
- /* get the header and data rows */
- await sheet.loadHeaderRow();
- const header = sheet.headerValues;
- const rows = await sheet.getRows();
- const aoa = [header].concat(rows.map(r => r._rawData));
-
- /* generate a SheetJS Worksheet */
- const ws = utils.aoa_to_sheet(aoa);
-
- /* add to workbook */
- utils.book_append_sheet(wb, ws, name);
-}
-
-/* write to SheetJS.xlsb */
-writeFile(wb, "SheetJS.xlsb");
+```bash
+curl -LO https://docs.sheetjs.com/gsheet/dump.mjs
```
-Edit the highlighted lines as follows:
+Edit the marked lines near the top of the file:
+
+```js title="dump.mjs (edit highlighted lines)"
+/* Change this import statement to point to the credentials JSON file */
+import creds from './sheetjs-test-726272627262.json' assert { type: "json" };
+
+/* Change this to the spreadsheet ID */
+const id = "SOME-SPREADSHEETJS-ID";
+```
- `'./sheetjs-test-726272627262.json'` should be replaced with the name of the
JSON file in step 27. The `./` prefix is required!
-- `'DOCUMENT_ID'` should be replaced with the Document ID from step 33.
+- `"SOME-SPREADSHEETJS-ID"` should be replaced with the Document ID from step 33.
39) Run the script:
@@ -699,11 +770,8 @@ JSON file in step 27. The `./` prefix is required!
node dump.mjs
```
-The script should create a file `SheetJS.xlsb` in the project folder. This file
-can be opened in Excel
-
-40) Sign into Google Sheets and open the "test from NodeJS" shared document. It
-should show a list of Presidents, matching the contents of the test file.
+The script should create a file `SheetJSExport.xlsb` in the project folder. This
+file can be opened in Excel.
### Export Raw Files
@@ -714,74 +782,63 @@ CSV files for each worksheet.
:::
-41) Sign into Google Sheets and open the "test from NodeJS" shared document.
+40) Sign into Google Sheets and open the "SheetJS Test" shared document.
-42) Click the Plus (`+`) icon in the lower left corner to create a new worksheet.
+41) Click the Plus (`+`) icon in the lower left corner to create a new worksheet.
-43) In the new worksheet, set cell A1 to the formula `=SEQUENCE(3,5)`. This will
+42) In the new worksheet, set cell A1 to the formula `=SEQUENCE(3,5)`. This will
assign a grid of values
-44) Save the following script to `raw.mjs`:
+43) Download [`raw.mjs`](pathname:///gsheet/raw.mjs):
-```js title="raw.mjs"
-import { JWT } from 'google-auth-library'
-import { GoogleSpreadsheet } from 'google-spreadsheet';
-
-import { read, utils } from 'xlsx';
-
-// highlight-next-line
-import creds from './sheetjs-test-726272627262.json' assert { type: "json" };
-
-const jwt = new JWT({
- email: creds.client_email,
- key: creds.private_key,
- scopes: [
- 'https://www.googleapis.com/auth/spreadsheets',
- 'https://www.googleapis.com/auth/drive.file',
- ]
-});
-
-// highlight-next-line
-const doc = new GoogleSpreadsheet('DOCUMENT_ID', jwt);
-await doc.loadInfo();
-
-const buf = await doc.downloadAsXLSX();
-
-/* Parse with SheetJS */
-const wb = read(buf);
-
-/* Loop over the worksheet names */
-wb.SheetNames.forEach(name => {
- /* Print the name to the console */
- console.log(name);
-
- /* Get the corresponding worksheet object */
- const sheet = wb.Sheets[name];
-
- /* Print a CSV export of the worksheet */
- console.log(utils.sheet_to_csv(sheet));
-});
+```bash
+curl -LO https://docs.sheetjs.com/gsheet/raw.mjs
```
-Edit the highlighted lines as follows:
+Edit the marked lines near the top of the file:
+
+```js title="raw.mjs (edit highlighted lines)"
+/* Change this import statement to point to the credentials JSON file */
+import creds from './sheetjs-test-726272627262.json' assert { type: "json" };
+
+/* Change this to the spreadsheet ID */
+const id = "SOME-SPREADSHEETJS-ID";
+```
- `'./sheetjs-test-726272627262.json'` should be replaced with the name of the
JSON file in step 27. The `./` prefix is required!
-- `'DOCUMENT_ID'` should be replaced with the Document ID from step 33.
+- `"SOME-SPREADSHEETJS-ID"` should be replaced with the Document ID from step 33.
-45) Run the script:
+44) Run the script:
```bash
node raw.mjs
```
-The script will display the sheet names and CSV rows from both worksheets.
+The script will display the sheet names and CSV rows from both worksheets:
-[^1]: The package name is [`google-auth-library`](https://www.npmjs.com/package/google-auth-library)
-[^2]: The project name is [`node-google-spreadsheet`](https://github.com/theoephraim/node-google-spreadsheet) but the module name is `google-spreadsheet`.
-[^3]: See [`aoa_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-arrays-input)
-[^4]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
-[^5]: See ["Exporting Data"](https://theoephraim.github.io/node-google-spreadsheet/#/guides/exports) in the `node-google-spreadsheet` documentation
-[^6]: See [`read` in "Reading Files"](/docs/api/parse-options)
-[^7]: See ["Workbook Object"](/docs/csf/book) for a description of the workbook object or ["API Reference"](/docs/api) for various methods to work with workbook and sheet objects.
+```
+#### Sheet1
+Name,Index
+Bill Clinton,42
+GeorgeW Bush,43
+Barack Obama,44
+Donald Trump,45
+Joseph Biden,46
+
+#### Sheet14
+1,2,3,4,5
+6,7,8,9,10
+11,12,13,14,15
+```
+
+[^1]: See [`aoa_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-arrays-input)
+[^2]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
+[^3]: See [`book_new` in "Utilities"](/docs/api/utilities/wb)
+[^4]: See [`book_append_sheet` in "Utilities"](/docs/api/utilities/wb)
+[^5]: See [`writeFile` in "Writing Files"](/docs/api/write-options)
+[^6]: See ["Workbook Object"](/docs/csf/book) for more details.
+[^7]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
+[^8]: See [`read` in "Reading Files"](/docs/api/parse-options)
+[^9]: See ["Workbook Object"](/docs/csf/book) for a description of the workbook object or ["API Reference"](/docs/api) for various methods to work with workbook and sheet objects.
diff --git a/docz/docs/03-demos/32-extensions/12-maple.md b/docz/docs/03-demos/32-extensions/12-maple.md
index b097332..be7f0d6 100644
--- a/docz/docs/03-demos/32-extensions/12-maple.md
+++ b/docz/docs/03-demos/32-extensions/12-maple.md
@@ -32,7 +32,12 @@ flowchart LR
:::note Tested Deployments
-This demo was last tested by SheetJS users on 2024-04-25 in Maple 2024.
+This demo was tested by SheetJS users in the following deployments:
+
+| Architecture | Version | Date |
+|:-------------|:--------|:-----------|
+| `darwin-x64` | 2024 | 2024-04-25 |
+| `win10-x64` | 2024 | 2024-04-25 |
:::
diff --git a/docz/static/gsheet/SheetJS1.png b/docz/static/gsheet/SheetJS1.png
new file mode 100644
index 0000000..01c3266
Binary files /dev/null and b/docz/static/gsheet/SheetJS1.png differ
diff --git a/docz/static/gsheet/SheetJS2.png b/docz/static/gsheet/SheetJS2.png
new file mode 100644
index 0000000..d0553f9
Binary files /dev/null and b/docz/static/gsheet/SheetJS2.png differ
diff --git a/docz/static/gsheet/creds.png b/docz/static/gsheet/creds.png
new file mode 100644
index 0000000..f04edee
Binary files /dev/null and b/docz/static/gsheet/creds.png differ
diff --git a/docz/static/gsheet/dump.mjs b/docz/static/gsheet/dump.mjs
new file mode 100644
index 0000000..f850f3b
--- /dev/null
+++ b/docz/static/gsheet/dump.mjs
@@ -0,0 +1,39 @@
+import { google } from "googleapis";
+
+import { set_fs, writeFile, utils } from 'xlsx';
+import * as fs from 'fs';
+set_fs(fs);
+
+/* Change this import statement to point to the credentials JSON file */
+import creds from './sheetjs-test-726272627262.json' assert { type: "json" };
+
+/* Change this to the spreadsheet ID */
+const id = "SOME-SPREADSHEETJS-ID";
+
+/* connect to google services */
+const jwt = new google.auth.JWT({
+ email: creds.client_email,
+ key: creds.private_key,
+ scopes: [
+ 'https://www.googleapis.com/auth/spreadsheets',
+ 'https://www.googleapis.com/auth/drive.file',
+ ]
+});
+
+const sheets = google.sheets({ version: "v4", auth: jwt });
+
+/* get existing sheets */
+const wsheet = await sheets.spreadsheets.get({spreadsheetId: id});
+
+/* create a workbook */
+const wb = utils.book_new();
+
+for(let sheet of wsheet.data.sheets) {
+ const name = sheet.properties.title;
+ const res = await sheets.spreadsheets.values.get({
+ spreadsheetId: id,
+ range: `'${name}'`
+ });
+ utils.book_append_sheet(wb, utils.aoa_to_sheet(res.data.values), name);
+}
+writeFile(wb, "SheetJSExport.xlsb");
diff --git a/docz/static/gsheet/init.mjs b/docz/static/gsheet/init.mjs
new file mode 100644
index 0000000..13f6ae5
--- /dev/null
+++ b/docz/static/gsheet/init.mjs
@@ -0,0 +1,87 @@
+import { google } from "googleapis";
+
+/* Change this import statement to point to the credentials JSON file */
+import creds from './sheetjs-test-726272627262.json' assert { type: "json" };
+
+/* Change this to the primary account address, NOT THE SERVICE ACCOUNT */
+const acct = "YOUR_ADDRESS@gmail.com";
+
+/* connect to google services */
+const jwt = new google.auth.JWT({
+ email: creds.client_email,
+ key: creds.private_key,
+ scopes: [
+ 'https://www.googleapis.com/auth/spreadsheets',
+ 'https://www.googleapis.com/auth/drive.file',
+ ]
+});
+
+const sheets = google.sheets({ version: "v4", auth: jwt });
+const drive = google.drive({version: "v3", auth: jwt });
+
+/* create new google workbook */
+const [id, sheet0id] = await (async() => {
+ const res = await sheets.spreadsheets.create({
+ requestBody: {
+ properties: {
+ title: "SheetJS Test"
+ }
+ }
+ });
+ const id = res.data.spreadsheetId;
+ const sheet0id = res.data.sheets[0].properties.sheetId;
+ return [id, sheet0id];
+})();
+console.log(`Created Google Workbook ${id}`);
+
+/* create new google worksheet and delete initial sheet */
+const [sheet1id, sheet2id] = await (async() => {
+ const res = await sheets.spreadsheets.batchUpdate({
+ spreadsheetId: id,
+ requestBody: { requests: [
+ /* add SheetJS1 */
+ { addSheet: { properties: { title: "SheetJS1" } } },
+ /* add SheetJS2 */
+ { addSheet: { properties: { title: "SheetJS2" } } },
+ /* remove default sheet */
+ { deleteSheet: { sheetId: sheet0id } },
+ ] }
+ });
+ console.log(`Created Google Worksheets "SheetJS1" and "SheetJS2"`);
+ return res.data.replies.slice(0,2).map(r => r.addSheet.properties.sheetId);
+})();
+
+await sheets.spreadsheets.values.update({
+ spreadsheetId: id,
+ range: "SheetJS1!A1",
+ valueInputOption: "USER_ENTERED",
+ resource: { values: [
+ ["Sheet", "JS"],
+ [72, 62]
+ ]}
+});
+
+await sheets.spreadsheets.values.update({
+ spreadsheetId: id,
+ range: "SheetJS2!A1",
+ valueInputOption: "USER_ENTERED",
+ resource: { values: [
+ ["Area Code", "Part 1", "Part 2"],
+ [201, 867, 5309],
+ [281, 330, 8004],
+ ]}
+});
+
+/* Share new Document with the primary account */
+try {
+ await drive.permissions.create({
+ fileId: id,
+ fields: "id",
+ requestBody: {
+ type: "user",
+ role: "writer",
+ emailAddress: acct
+ }
+ });
+ console.log(`Shared ${id} with ${acct}`);
+} catch(e) { console.log(e); }
diff --git a/docz/static/gsheet/load.mjs b/docz/static/gsheet/load.mjs
new file mode 100644
index 0000000..2d5c14d
--- /dev/null
+++ b/docz/static/gsheet/load.mjs
@@ -0,0 +1,87 @@
+import { google } from "googleapis";
+
+import { set_fs, readFile, utils } from 'xlsx';
+import * as fs from 'fs';
+set_fs(fs);
+
+/* Change this import statement to point to the credentials JSON file */
+import creds from './sheetjs-test-726272627262.json' assert { type: "json" };
+
+/* Change this to the spreadsheet ID */
+const id = "SOME-SPREADSHEETJS-ID";
+
+/* connect to google services */
+const jwt = new google.auth.JWT({
+ email: creds.client_email,
+ key: creds.private_key,
+ scopes: [
+ 'https://www.googleapis.com/auth/spreadsheets',
+ 'https://www.googleapis.com/auth/drive.file',
+ ]
+});
+
+const sheets = google.sheets({ version: "v4", auth: jwt });
+
+/* get existing sheets */
+const wsheet = await sheets.spreadsheets.get({spreadsheetId: id});
+
+/* remove all sheets after the first */
+if(wsheet.data.sheets.length > 1) await sheets.spreadsheets.batchUpdate({
+ spreadsheetId: id,
+ requestBody: { requests:
+ wsheet.data.sheets.slice(1).map(s => ({
+ deleteSheet: {
+ sheetId: s.properties.sheetId
+ }
+ }))
+ }
+});
+
+/* read file */
+const wb = readFile("pres.numbers");
+
+/* rename first worksheet to avoid collisions */
+const props0 = wsheet.data.sheets[0].properties;
+if(wb.SheetNames.map(n => n.toLowerCase()).includes(props0.title.toLowerCase())) {
+ await sheets.spreadsheets.batchUpdate({
+ spreadsheetId: id,
+ requestBody: { requests: [{
+ updateSheetProperties: {
+ fields: "title",
+ properties: {
+ sheetId: props0.sheetId,
+ title: "thistitleisatleast33characterslong"
+ }
+ }
+ }]}
+ });
+ console.log(`renamed "${props0.title}" to "thistitleisatleast33characterslong"`);
+}
+
+/* add sheets from file */
+for(let name of wb.SheetNames) {
+ const aoa = utils.sheet_to_json(wb.Sheets[name], {header:1});
+ await sheets.spreadsheets.batchUpdate({
+ spreadsheetId: id,
+ requestBody: { requests: [
+ /* add new sheet */
+ { addSheet: { properties: { title: name } } },
+ ] }
+ });
+ await sheets.spreadsheets.values.update({
+ spreadsheetId: id,
+ range: `'${name}'!A1`,
+ valueInputOption: "USER_ENTERED",
+ resource: { values: aoa }
+ });
+ console.log(`Created Google Worksheet "${name}"`);
+}
+
+/* remove first sheet */
+const res = await sheets.spreadsheets.batchUpdate({
+ spreadsheetId: id,
+ requestBody: { requests: [
+ /* remove old first sheet */
+ { deleteSheet: { sheetId: wsheet.data.sheets[0].properties.sheetId } }
+ ] }
+});
diff --git a/docz/static/gsheet/notification.png b/docz/static/gsheet/notification.png
new file mode 100644
index 0000000..24b37b6
Binary files /dev/null and b/docz/static/gsheet/notification.png differ
diff --git a/docz/static/gsheet/raw.mjs b/docz/static/gsheet/raw.mjs
new file mode 100644
index 0000000..440d565
--- /dev/null
+++ b/docz/static/gsheet/raw.mjs
@@ -0,0 +1,38 @@
+import { google } from "googleapis";
+
+import { read, utils } from 'xlsx';
+
+/* Change this import statement to point to the credentials JSON file */
+import creds from './sheetjs-test-726272627262.json' assert { type: "json" };
+
+/* Change this to the spreadsheet ID */
+const id = "SOME-SPREADSHEETJS-ID";
+
+/* connect to google services */
+const jwt = new google.auth.JWT({
+ email: creds.client_email,
+ key: creds.private_key,
+ scopes: [
+ 'https://www.googleapis.com/auth/spreadsheets',
+ 'https://www.googleapis.com/auth/drive.file',
+ ]
+});
+
+const drive = google.drive({ version: "v3", auth: jwt });
+
+/* get XLSX export */
+const file = await drive.files.export({
+ mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ fileId: id
+});
+const ab = await file.data.arrayBuffer();
+
+/* parse with SheetJS */
+const wb = read(ab);
+
+/* print CSV data from each worksheet */
+wb.SheetNames.forEach(n => {
+ console.log(`#### ${n}`);
+ console.log(utils.sheet_to_csv(wb.Sheets[n]));
+ console.log("");
+})
\ No newline at end of file
diff --git a/docz/static/gsheet/selector.png b/docz/static/gsheet/selector.png
new file mode 100644
index 0000000..90c35a5
Binary files /dev/null and b/docz/static/gsheet/selector.png differ
diff --git a/tests/bundler-parcel.sh b/tests/bundler-parcel.sh
new file mode 100755
index 0000000..947022a
--- /dev/null
+++ b/tests/bundler-parcel.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+# https://docs.sheetjs.com/docs/demos/frontend/bundler/parcel
+cd /tmp
+rm -rf sheetjs-parceljs
+
+mkdir sheetjs-parceljs
+cd sheetjs-parceljs
+npm init -y
+
+npm i --save https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz
+
+cat >index.html <
+