xlsx-template-browser 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +153 -0
- package/lib/excelHelpers.js +113 -0
- package/lib/helpers.js +62 -0
- package/lib/index.js +16 -0
- package/lib/main.js +213 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Kyrylo Zakharov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# XLSX Template in Browser
|
|
2
|
+
|
|
3
|
+
On-the-fly generation of .xlsx (Excel) files directly within the browser using
|
|
4
|
+
templates constructed in Excel.
|
|
5
|
+
|
|
6
|
+
The basic principle is this: You create a template in Excel. This can be
|
|
7
|
+
formatted as you wish, contain formulae etc. In this file, you put placeholders
|
|
8
|
+
using a specific syntax (see below). In code, you build a map of placeholders
|
|
9
|
+
to values and then load the template, substitute the placeholders for the
|
|
10
|
+
relevant values, and generate a new .xlsx file that you can then serve to the
|
|
11
|
+
user.
|
|
12
|
+
|
|
13
|
+
## Placeholders
|
|
14
|
+
|
|
15
|
+
Placeholders are inserted in cells in a spreadsheet. It does not matter how
|
|
16
|
+
those cells are formatted, so e.g. it is OK to insert a placeholder (which is
|
|
17
|
+
text content) into a cell formatted as a number or currecy or date, if you
|
|
18
|
+
expect the placeholder to resolve to a number or currency or date.
|
|
19
|
+
|
|
20
|
+
### Scalars
|
|
21
|
+
|
|
22
|
+
Simple placholders take the format `${name}`. Here, `name` is the name of a
|
|
23
|
+
key in the placeholders map. The value of this placholder here should be a
|
|
24
|
+
scalar, i.e. not an array or object. The placeholder may appear on its own in a
|
|
25
|
+
cell, or as part of a text string. For example:
|
|
26
|
+
|
|
27
|
+
| Extracted on: | ${extractDate} |
|
|
28
|
+
|
|
29
|
+
might result in (depending on date formatting in the second cell):
|
|
30
|
+
|
|
31
|
+
| Extracted on: | Jun-01-2013 |
|
|
32
|
+
|
|
33
|
+
Here, `extractDate` may be a date and the second cell may be formatted as a
|
|
34
|
+
number.
|
|
35
|
+
|
|
36
|
+
Inside scalars there possibility to use array indexers.
|
|
37
|
+
For example:
|
|
38
|
+
|
|
39
|
+
Given data
|
|
40
|
+
|
|
41
|
+
var template = { extractDates: ["Jun-01-2113", "Jun-01-2013" ]}
|
|
42
|
+
|
|
43
|
+
which will be applied to following template
|
|
44
|
+
|
|
45
|
+
| Extracted on: | ${extractDates[0]} |
|
|
46
|
+
|
|
47
|
+
will results in the
|
|
48
|
+
|
|
49
|
+
| Extracted on: | Jun-01-2113 |
|
|
50
|
+
|
|
51
|
+
### Columns
|
|
52
|
+
|
|
53
|
+
You can use arrays as placeholder values to indicate that the placeholder cell
|
|
54
|
+
is to be replicated across columns. In this case, the placeholder cannot appear
|
|
55
|
+
inside a text string - it must be the only thing in its cell. For example,
|
|
56
|
+
if the placehodler value `dates` is an array of dates:
|
|
57
|
+
|
|
58
|
+
| ${dates} |
|
|
59
|
+
|
|
60
|
+
might result in:
|
|
61
|
+
|
|
62
|
+
| Jun-01-2013 | Jun-02-2013 | Jun-03-2013 |
|
|
63
|
+
|
|
64
|
+
### Tables
|
|
65
|
+
|
|
66
|
+
Finally, you can build tables made up of multiple rows. In this case, each
|
|
67
|
+
placeholder should be prefixed by `table:` and contain both the name of the
|
|
68
|
+
placeholder variable (a list of objects) and a key (in each object in the list).
|
|
69
|
+
For example:
|
|
70
|
+
|
|
71
|
+
| Name | Age |
|
|
72
|
+
| ${table:people.name} | ${table:people.age} |
|
|
73
|
+
|
|
74
|
+
If the replacement value under `people` is an array of objects, and each of
|
|
75
|
+
those objects have keys `name` and `age`, you may end up with something like:
|
|
76
|
+
|
|
77
|
+
| Name | Age |
|
|
78
|
+
| John Smith | 20 |
|
|
79
|
+
| Bob Johnson | 22 |
|
|
80
|
+
|
|
81
|
+
If a particular value is an array, then it will be repeated across columns as
|
|
82
|
+
above.
|
|
83
|
+
|
|
84
|
+
## Generating reports
|
|
85
|
+
|
|
86
|
+
To use the library, you need the code like this:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
import { generateXlsx, downloadXlsx } from 'xlsx-template-browser'
|
|
90
|
+
|
|
91
|
+
// Set your values to fill the template
|
|
92
|
+
const values = {
|
|
93
|
+
extractDate: new Date(),
|
|
94
|
+
dates: [ new Date("2013-06-01"), new Date("2013-06-02"), new Date("2013-06-03") ],
|
|
95
|
+
people: [
|
|
96
|
+
{name: "John Smith", age: 20},
|
|
97
|
+
{name: "Bob Johnson", age: 22}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// You can provide URL to the Excel template or the array buffer
|
|
102
|
+
const template = '[URL TO YOUR TEMPLATE]'
|
|
103
|
+
|
|
104
|
+
// You can generate the report and keep it in memory
|
|
105
|
+
const generateReport = async () => {
|
|
106
|
+
return await generateXlsx(template, values)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Alternatively, you can download the generated report immediately
|
|
110
|
+
const downloadReport = async () => {
|
|
111
|
+
return await downloadXlsx(template, values, 'My fabulous report.xlsx')
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Caveats
|
|
116
|
+
|
|
117
|
+
* The spreadsheet must be saved in `.xlsx` format. `.xls`, `.xlsb` or `.xlsm`
|
|
118
|
+
won't work.
|
|
119
|
+
* Column (array) and table (array-of-objects) insertions cause rows and cells to
|
|
120
|
+
be inserted or removed. When this happens, only a limited number of
|
|
121
|
+
adjustments are made:
|
|
122
|
+
* Merged cells and named cells/ranges to the right of cells where insertions
|
|
123
|
+
or deletions are made are moved right or left, appropriately. This may
|
|
124
|
+
not work well if cells are merged across rows, unless all rows have the
|
|
125
|
+
same number of insertions.
|
|
126
|
+
* Merged cells, named tables or named cells/ranges below rows where further
|
|
127
|
+
rows are inserted are moved down.
|
|
128
|
+
Formulae are not adjusted.
|
|
129
|
+
* As a corollary to this, it is not always easy to build formulae that refer
|
|
130
|
+
to cells in a table (e.g. summing all rows) where the exact number of rows
|
|
131
|
+
or columns is not known in advance. There are two strategies for dealing
|
|
132
|
+
with this:
|
|
133
|
+
* Put the table as the last (or only) thing on a particular sheet, and
|
|
134
|
+
use a formula that includes a large number of rows or columns in the
|
|
135
|
+
hope that the actual table will be smaller than this number.
|
|
136
|
+
* Use named tables. When a placeholder in a named table causes columns or
|
|
137
|
+
rows to be added, the table definition (i.e. the cells included in the
|
|
138
|
+
table) will be updated accordingly. You can then use things like
|
|
139
|
+
`TableName[ColumnName]` in your formula to refer to all values in a given
|
|
140
|
+
column in the table as a logical range.
|
|
141
|
+
* Placeholders only work in simple cells and tables.
|
|
142
|
+
|
|
143
|
+
## Alternatives
|
|
144
|
+
|
|
145
|
+
There are few altertantives for this library:
|
|
146
|
+
|
|
147
|
+
* [xlsx-template](https://www.npmjs.com/package/xlsx-template) - this library was the source of
|
|
148
|
+
inspiration. The only issue that it works in Node only. Also it allows you to insert images and
|
|
149
|
+
has more dependencies.
|
|
150
|
+
* [node-xlsx](https://www.npmjs.com/package/node-xlsx) - this library allows to manipulate the
|
|
151
|
+
data in worksheet, but the style of cells will be lost. The fork
|
|
152
|
+
[js-xlsx](https://github.com/protobi/js-xlsx/) might address the issue, but it hasn't been updated
|
|
153
|
+
for 4 years and could be considered overkill. Also the module is under Apache2 license.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a value to a string.
|
|
3
|
+
* @param {*} value - The value to be converted.
|
|
4
|
+
* @returns {string} - The converted string representation of the value.
|
|
5
|
+
*/
|
|
6
|
+
export const valueToString = (value) => {
|
|
7
|
+
if (!value) return ''
|
|
8
|
+
if (Array.isArray(value)) return value.map(valueToString).toString()
|
|
9
|
+
if (typeof value === 'object') value = JSON.stringify(value)
|
|
10
|
+
return value.toString()
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Converts dates to Excel serial index.
|
|
15
|
+
* @param {Date} value - The date to be converted.
|
|
16
|
+
* @returns {number} - The Excel serial index representing the date.
|
|
17
|
+
*/
|
|
18
|
+
const dateToExcel = (value) => {
|
|
19
|
+
let date = new Date(value)
|
|
20
|
+
return 25569 + ((date.getTime() - (date.getTimezoneOffset() * 60 * 1000)) / (1000 * 60 * 60 * 24))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Guesses the Excel data type for the given JavaScript primitive value.
|
|
25
|
+
* @param {*} value - The JavaScript primitive value to be converted.
|
|
26
|
+
* @returns {Object|undefined} - An object with 'cellType' and 'value' properties representing the Excel data type,
|
|
27
|
+
* or undefined if the data type is not recognized.
|
|
28
|
+
*/
|
|
29
|
+
export const guessDataType = (value) => {
|
|
30
|
+
if (typeof value === 'undefined') return undefined
|
|
31
|
+
if (typeof value === 'number') {
|
|
32
|
+
if (isFinite(value)) return { cellType: 'n', value }
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
if (value instanceof Date) {
|
|
36
|
+
if (isNaN(value)) return undefined
|
|
37
|
+
return { cellType: 'n', value: dateToExcel(value) }
|
|
38
|
+
}
|
|
39
|
+
if (typeof value === 'boolean') return { cellType: 'b', value: value + 0 }
|
|
40
|
+
return { cellType: 's', value: valueToString(value )}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a new shared string for an Excel file.
|
|
45
|
+
* @param {string} text - The text content of the shared string.
|
|
46
|
+
* @param {Element} template - (Optional) An existing node with cell to clone for the shared string.
|
|
47
|
+
* @returns {Element} - The newly created or cloned shared string element.
|
|
48
|
+
*/
|
|
49
|
+
export const createSharedString = (text, template) => {
|
|
50
|
+
const si = template ? template.cloneNode() : document.createElement('si')
|
|
51
|
+
let t = document.createElement('t')
|
|
52
|
+
t.textContent = text
|
|
53
|
+
si.appendChild(t)
|
|
54
|
+
return si
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates a cell with the specified value.
|
|
59
|
+
* @param {Object} options - An object containing parameters for cell creation.
|
|
60
|
+
* @param {*} options.value - The value to be set in the cell.
|
|
61
|
+
* @param {string|number} options.row - The row reference for the cell.
|
|
62
|
+
* @param {string|number} options.column - The column reference (either string or numeric) for the cell.
|
|
63
|
+
* @param {Element} options.template - (Optional) An existing template to clone for the cell.
|
|
64
|
+
* @param {string} options.cellType - The type of the cell (e.g., 's', 'n', 'b') for Excel formatting.
|
|
65
|
+
* @returns {Element} - The created cell element.
|
|
66
|
+
*/
|
|
67
|
+
export const createCell = ({ value, row, column, template, cellType }) => {
|
|
68
|
+
// Clone initial node with the style or create a new one
|
|
69
|
+
const cell = template ? template.cloneNode() : document.createElement('c')
|
|
70
|
+
// Create a cell reference
|
|
71
|
+
if (row && column) {
|
|
72
|
+
column = typeof column === 'number' ? getExcelColumnName(column) : column
|
|
73
|
+
cell.setAttribute('r', column + row)
|
|
74
|
+
}
|
|
75
|
+
// If no value is provided, return cell
|
|
76
|
+
if (typeof value === 'undefined') return cell
|
|
77
|
+
const valueTag = document.createElement('v')
|
|
78
|
+
cell.setAttribute('t', cellType)
|
|
79
|
+
valueTag.textContent = value
|
|
80
|
+
cell.appendChild(valueTag)
|
|
81
|
+
return cell
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Converts a numeric index to an Excel column name.
|
|
86
|
+
* @param {number} index - The numeric index to be converted.
|
|
87
|
+
* @returns {string} - The Excel column name corresponding to the index.
|
|
88
|
+
*/
|
|
89
|
+
const getExcelColumnName = (index) => {
|
|
90
|
+
let result = ''
|
|
91
|
+
while (index > 0) {
|
|
92
|
+
const remainder = (index - 1) % 26
|
|
93
|
+
result = String.fromCharCode(65 + remainder) + result
|
|
94
|
+
index = Math.floor((index - 1) / 26)
|
|
95
|
+
}
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Converts an Excel column name to its numeric index.
|
|
101
|
+
* @param {string} columnName - The Excel column name to be converted.
|
|
102
|
+
* @returns {number} - The numeric index corresponding to the Excel column name.
|
|
103
|
+
*/
|
|
104
|
+
export const getExcelColumnIndex = (columnName) => {
|
|
105
|
+
columnName = columnName.replace(/\d+/g, '')
|
|
106
|
+
let index = 0
|
|
107
|
+
for (let i = 0; i < columnName.length; i++) {
|
|
108
|
+
const charCode = columnName.charCodeAt(i) - 64
|
|
109
|
+
index = index * 26 + charCode
|
|
110
|
+
}
|
|
111
|
+
return index
|
|
112
|
+
}
|
|
113
|
+
|
package/lib/helpers.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Downloads a file using the provided data and file name.
|
|
3
|
+
* @param {string} data - The file data
|
|
4
|
+
* @param {string} fileName - The desired name for the downloaded file
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const downloadURL = (data, fileName) => {
|
|
8
|
+
const a = document.createElement('a')
|
|
9
|
+
a.href = data
|
|
10
|
+
a.download = fileName
|
|
11
|
+
a.style = 'display: none'
|
|
12
|
+
document.body.appendChild(a)
|
|
13
|
+
a.click()
|
|
14
|
+
a.remove()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Converts an array to a Blob and initiates the download with the specified file name and MIME type.
|
|
19
|
+
* @param {Array} data - The array data to be converted to a Blob.
|
|
20
|
+
* @param {string} fileName - The desired name for the downloaded file.
|
|
21
|
+
* @param {string} mimeType - The MIME type of the file.
|
|
22
|
+
*/
|
|
23
|
+
export const downloadBlob = (data, fileName, mimeType) => {
|
|
24
|
+
const blob = new Blob([data], { type: mimeType })
|
|
25
|
+
const url = window.URL.createObjectURL(blob)
|
|
26
|
+
downloadURL(url, fileName)
|
|
27
|
+
setTimeout(() => { return window.URL.revokeObjectURL(url) }, 100)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
// Regular expression for extracting fields with deep notation data
|
|
32
|
+
const notationRegex = /(?<=\[(?<qoute>['"]))[^'"\]].*?(?=\k<qoute>\])|(?<=\[)[^'"\]].*?(?=\])|[^.\["''\]]+(?=\.|\[|$)/g
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Retrieves nested properties from an object using an array of accessors.
|
|
36
|
+
* @param {*} obj - The object from which to retrieve the nested properties.
|
|
37
|
+
* @param {Array} accessors - An array of accessors representing the path to the desired properties.
|
|
38
|
+
* @returns {*} - The value of the nested properties, or undefined if any accessor along the path is undefined.
|
|
39
|
+
*/
|
|
40
|
+
const getDeep = (obj, accessors) => {
|
|
41
|
+
let length = accessors.length
|
|
42
|
+
for (let i = 0; i < length; i++) {
|
|
43
|
+
if (typeof obj === 'undefined') return undefined
|
|
44
|
+
if (Array.isArray(obj) && typeof accessors[i] === 'string') return obj.map(el => getDeep(el, accessors.slice(i)))
|
|
45
|
+
obj = obj[accessors[i]]
|
|
46
|
+
}
|
|
47
|
+
return obj
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Retrieves nested properties from an object using dot and bracket notation.
|
|
52
|
+
* @param {*} obj - The object from which to retrieve the nested properties.
|
|
53
|
+
* @param {string} props - The notation representing the path to the desired properties.
|
|
54
|
+
* @returns {*} - The value of the nested properties, or the original object if the notation is empty.
|
|
55
|
+
*/
|
|
56
|
+
export const getByNotation = (obj, props) => {
|
|
57
|
+
let accessors = props.match(notationRegex)
|
|
58
|
+
if (!accessors.length) return obj
|
|
59
|
+
accessors = accessors.map(el => /^\d+$/.test(el) ? parseInt(el) : el)
|
|
60
|
+
return getDeep(obj, accessors)
|
|
61
|
+
}
|
|
62
|
+
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { downloadBlob } from './helpers'
|
|
2
|
+
import replace from './main'
|
|
3
|
+
|
|
4
|
+
export const generateXlsx = async (template, data) => {
|
|
5
|
+
if (!template || !data) throw Error('No template or data provided')
|
|
6
|
+
if (typeof template.match === 'string' && template.match(/^https:/)) {
|
|
7
|
+
const response = await fetch(template)
|
|
8
|
+
template = response.arrayBuffer()
|
|
9
|
+
}
|
|
10
|
+
return await replace(template, data)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const downloadXlsx = async (template, data, fileName, mimeType) => {
|
|
14
|
+
if (!fileName) fileName = new Date().toISOString().substring(0, 10) + ' - Report.xlsx'
|
|
15
|
+
downloadBlob(await generateXlsx(template, data), fileName, mimeType)
|
|
16
|
+
}
|
package/lib/main.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Refer to Excel OpenXML format:
|
|
2
|
+
// https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.cell?view=openxml-2.8.1
|
|
3
|
+
|
|
4
|
+
// Import JSZip library for working with ZIP files
|
|
5
|
+
import JSZip from 'jszip'
|
|
6
|
+
import {
|
|
7
|
+
createCell,
|
|
8
|
+
createSharedString,
|
|
9
|
+
getExcelColumnIndex,
|
|
10
|
+
guessDataType,
|
|
11
|
+
valueToString
|
|
12
|
+
} from './excelHelpers'
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
getByNotation
|
|
16
|
+
} from './helpers'
|
|
17
|
+
|
|
18
|
+
// Regular expression for parsing a property accessor enclosed in ${}
|
|
19
|
+
const accessorRegex = /(?<=^\$\{)(?<table>table:)?(?<accessor>[^}]+)(?=}$)/
|
|
20
|
+
|
|
21
|
+
// Global regular expression for extracting all property accessors enclosed in ${}
|
|
22
|
+
const globalAccessorRegex = /\$\{[^}]+}/g
|
|
23
|
+
|
|
24
|
+
const replace = async (template, data) => {
|
|
25
|
+
if (!template || !data) return template
|
|
26
|
+
|
|
27
|
+
/* INITIALIZATION */
|
|
28
|
+
|
|
29
|
+
// Create a new DOMParser instance for parsing XML. Refer to https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
|
|
30
|
+
const parser = new DOMParser()
|
|
31
|
+
const serializer = new XMLSerializer()
|
|
32
|
+
// Create a new instance of JSZip for ZIP file manipulation
|
|
33
|
+
const new_zip = new JSZip()
|
|
34
|
+
// Load xlsx (zipped file)
|
|
35
|
+
let res = await new_zip.loadAsync(template)
|
|
36
|
+
|
|
37
|
+
/* PROCESSING OF THE SHARED STRINGS */
|
|
38
|
+
|
|
39
|
+
// Read all shared text values
|
|
40
|
+
let xmlText = await res.file('xl/sharedStrings.xml').async('string')
|
|
41
|
+
// Get the xml with shared shared strings
|
|
42
|
+
const xml = parser.parseFromString(xmlText, 'application/xml')
|
|
43
|
+
// Get the header of shared strings
|
|
44
|
+
const sst = xml.querySelector('sst')
|
|
45
|
+
// Array with new values
|
|
46
|
+
const valuesToReplace = []
|
|
47
|
+
// New list of shared string values
|
|
48
|
+
let newSharedStrings = []
|
|
49
|
+
|
|
50
|
+
// We need to replace only text values with the new ones, therefore we process shared strings first
|
|
51
|
+
// Get all string items
|
|
52
|
+
xml.querySelectorAll('si').forEach((si, i) => {
|
|
53
|
+
// Get all text tags
|
|
54
|
+
const t = si.querySelector('t')
|
|
55
|
+
if (!t) return false
|
|
56
|
+
// Get text value
|
|
57
|
+
const textValue = t.textContent
|
|
58
|
+
// Get all accessors
|
|
59
|
+
let match = textValue.match(accessorRegex)
|
|
60
|
+
// If the cell does not contain only one placeholder, check for multiple ones
|
|
61
|
+
if (!match) {
|
|
62
|
+
// Check if the text contains any accessors at all
|
|
63
|
+
let newText = textValue.replace(globalAccessorRegex, s => valueToString(getByNotation(data, s.replace(/\$\{|}/g, ''))))
|
|
64
|
+
newSharedStrings.push(createSharedString(newText, si))
|
|
65
|
+
valuesToReplace[i] = { value: newSharedStrings.length - 1, cellType: 's' }
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
// Process a value with an accessor only
|
|
69
|
+
let { accessor, table } = match.groups
|
|
70
|
+
// If we have table values, we need to process them in a separate way
|
|
71
|
+
const isTable = typeof table === 'string'
|
|
72
|
+
const value = getByNotation(data, accessor)
|
|
73
|
+
// Save tables for further processing
|
|
74
|
+
if (isTable) {
|
|
75
|
+
valuesToReplace[i] = { isTable, value: value.map(guessDataType) }
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(value)) {
|
|
79
|
+
valuesToReplace[i] = value.map(guessDataType)
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
valuesToReplace[i] = guessDataType(value)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
/* PROCESSING OF WORKSHEETS */
|
|
86
|
+
|
|
87
|
+
// To prevent string duplication, store unique strings (they will be added to shared strings)
|
|
88
|
+
const newStrings = []
|
|
89
|
+
|
|
90
|
+
// Adds a string to the collection, avoiding duplication, and returns its index.
|
|
91
|
+
const addString = (value) => {
|
|
92
|
+
let stringIndex = newStrings.indexOf(value)
|
|
93
|
+
if (stringIndex === -1) {
|
|
94
|
+
newStrings.push(value)
|
|
95
|
+
stringIndex = newStrings.length - 1
|
|
96
|
+
}
|
|
97
|
+
value = stringIndex + newSharedStrings.length
|
|
98
|
+
return value
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Get all worksheets
|
|
102
|
+
const worksheets = Object.keys(res.files).filter(el => /xl\/worksheets\/[^/]+$/.test(el))
|
|
103
|
+
for (let worksheet of worksheets) {
|
|
104
|
+
// Unzip the file
|
|
105
|
+
let xmlText = await res.file(worksheet).async('string')
|
|
106
|
+
// Parse file to DOM
|
|
107
|
+
let xml = parser.parseFromString(xmlText, 'application/xml')
|
|
108
|
+
// Process rows
|
|
109
|
+
let rows = xml.querySelectorAll('sheetData row')
|
|
110
|
+
// Array with the list of the new rows
|
|
111
|
+
const newRows = []
|
|
112
|
+
// Process cells in rows
|
|
113
|
+
let rowOffset = 0
|
|
114
|
+
for (let row of rows) {
|
|
115
|
+
// Since some rows can be skipped, we want to get an actual row number from the template
|
|
116
|
+
let currentRow = parseInt(row.getAttribute('r'))
|
|
117
|
+
// List of new cells
|
|
118
|
+
let newCells = []
|
|
119
|
+
// If we have an array of values to extend the list of cells, we should keep an offset to move static cells
|
|
120
|
+
let cellOffset = 0
|
|
121
|
+
// Process each cell
|
|
122
|
+
let cells = row.querySelectorAll('c').forEach((c) => {
|
|
123
|
+
// Current cell index (note that Excel has 1-based index)
|
|
124
|
+
let index = getExcelColumnIndex(c.getAttribute('r'))
|
|
125
|
+
let newIndex = index + cellOffset
|
|
126
|
+
// Get the cell value tag
|
|
127
|
+
let v = c.querySelector('v') || {}
|
|
128
|
+
// Get the new value to replace
|
|
129
|
+
let newValue = valuesToReplace[v.textContent] || {}
|
|
130
|
+
if (!Array.isArray(newValue)) {
|
|
131
|
+
let { value, cellType, isTable } = newValue
|
|
132
|
+
// If the new value is an array from the table, then return an array of values
|
|
133
|
+
if (isTable) {
|
|
134
|
+
newCells[newIndex] = value.map(el => {
|
|
135
|
+
if (!el) return { template: c }
|
|
136
|
+
if (el.cellType === 's' && typeof el.value === 'string') el.value = addString(el.value)
|
|
137
|
+
return { value: el.value, cellType: el.cellType, template: c }
|
|
138
|
+
})
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
// Return a new value
|
|
142
|
+
if (cellType === 's' && typeof value === 'string') value = addString(value)
|
|
143
|
+
newCells[newIndex] = Object.assign({}, { value, cellType }, { template: c })
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
// If the value is an array (not from table), then extend the existing list of cells
|
|
147
|
+
for (let i = 0; i < newValue.length; i++) {
|
|
148
|
+
let { value, cellType } = newValue[i]
|
|
149
|
+
if (cellType === 's' && typeof value === 'string') value = addString(value)
|
|
150
|
+
newCells[newIndex + i] = Object.assign({}, { value, cellType }, { template: c })
|
|
151
|
+
cellOffset++
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
/* GENERATION OF THE NEW ROWS */
|
|
156
|
+
|
|
157
|
+
// Check if the row contains arrays and the values should be duplicated
|
|
158
|
+
let length = Math.max(...newCells.filter(el => Array.isArray(el)).map(el => el.length))
|
|
159
|
+
|
|
160
|
+
// Create a row
|
|
161
|
+
if (length <= 0) {
|
|
162
|
+
let rowIndex = rowOffset + currentRow
|
|
163
|
+
const rowValues = newCells.map((el, i) => {
|
|
164
|
+
if (!el) return undefined
|
|
165
|
+
return createCell(Object.assign({}, el, { row: rowIndex, column: i }))
|
|
166
|
+
}).filter(Boolean)
|
|
167
|
+
const newRow = row.cloneNode()
|
|
168
|
+
newRow.setAttribute('r', rowIndex)
|
|
169
|
+
rowValues.forEach(value => newRow.append(value))
|
|
170
|
+
newRows.push(newRow)
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Create table rows
|
|
175
|
+
for (let i = 0; i < length; i++) {
|
|
176
|
+
let rowIndex = rowOffset + currentRow
|
|
177
|
+
rowOffset++
|
|
178
|
+
const rowValues = newCells.map((el, index) => {
|
|
179
|
+
if (!el) return undefined
|
|
180
|
+
if (Array.isArray(el)) {
|
|
181
|
+
if (typeof el[i] === 'undefined') return undefined
|
|
182
|
+
return createCell(Object.assign({}, el[i], { row: rowIndex, column: index }))
|
|
183
|
+
}
|
|
184
|
+
return createCell(Object.assign({}, el, { row: rowIndex, column: index }))
|
|
185
|
+
})
|
|
186
|
+
const newRow = row.cloneNode()
|
|
187
|
+
newRow.setAttribute('r', rowIndex)
|
|
188
|
+
rowValues.forEach(value => newRow.append(value))
|
|
189
|
+
newRows.push(newRow)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Generate new sheet data
|
|
194
|
+
const newSheetData = xml.querySelector('sheetData').cloneNode()
|
|
195
|
+
newRows.forEach(row => newSheetData.append(row))
|
|
196
|
+
xml.querySelector('sheetData').replaceWith(newSheetData)
|
|
197
|
+
const newData = serializer.serializeToString(xml).replace(/ ?xmlns="http:\/\/www\.w3\.org\/1999\/xhtml"/g, '')
|
|
198
|
+
// Save the data of the worksheet
|
|
199
|
+
await new_zip.file(worksheet, newData)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* FINALIZE NEW SHARED STRINGS */
|
|
203
|
+
newSharedStrings = [...newSharedStrings, ...newStrings.map(el => createSharedString(el))]
|
|
204
|
+
const newSst = sst.cloneNode()
|
|
205
|
+
newSharedStrings.forEach(si => newSst.append(si))
|
|
206
|
+
sst.replaceWith(newSst)
|
|
207
|
+
const newData = serializer.serializeToString(xml).replace(/ ?xmlns="http:\/\/www\.w3\.org\/1999\/xhtml"/g, '')
|
|
208
|
+
// Save new shared strings
|
|
209
|
+
await new_zip.file('xl/sharedStrings.xml', newData)
|
|
210
|
+
return await new_zip.generateAsync({ type: 'blob' })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export default replace
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xlsx-template-browser",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "On-the-fly generation of .xlsx (Excel) files directly within the browser using templates constructed in Excel.",
|
|
5
|
+
"main": "./lib/index",
|
|
6
|
+
"files": [
|
|
7
|
+
"lib"
|
|
8
|
+
],
|
|
9
|
+
"author": {
|
|
10
|
+
"name": "Kyrylo Zakharov"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/Amice13/xlsx-template-browser"
|
|
16
|
+
},
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/Amice13/xlsx-template-browser/issues"
|
|
19
|
+
},
|
|
20
|
+
"licenses": [
|
|
21
|
+
{
|
|
22
|
+
"type": "MIT",
|
|
23
|
+
"url": "https://raw.githubusercontent.com/Amice13/xlsx-template-browser/main/LICENSE"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"jszip": "^3.10.1"
|
|
28
|
+
},
|
|
29
|
+
"readme": "",
|
|
30
|
+
"readmeFilename": "README.md"
|
|
31
|
+
}
|