xlsx-template-browser 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/index.cjs +2287 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +2249 -0
- package/dist/index.js.map +1 -0
- package/package.json +26 -4
- package/lib/excelHelpers.js +0 -149
- package/lib/helpers.js +0 -63
- package/lib/index.js +0 -31
- package/lib/main.js +0 -294
package/lib/helpers.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
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
|
-
const value = getDeep(obj, accessors)
|
|
61
|
-
return value || ''
|
|
62
|
-
}
|
|
63
|
-
|
package/lib/index.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { downloadBlob } from './helpers'
|
|
2
|
-
import replace from './main'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Generates an XLSX file by replacing data in a template file.
|
|
6
|
-
* @param {string} templateURL - The URL of the template XLSX file.
|
|
7
|
-
* @param {Object} data - The data object containing values to replace in the template.
|
|
8
|
-
* @returns {Promise<Blob>} A promise that resolves with the Blob of the generated XLSX file.
|
|
9
|
-
* @throws {Error} Throws an error if either the template URL or data is not provided, or if unable to fetch the template file.
|
|
10
|
-
*/
|
|
11
|
-
export const generateXlsx = async (template, data) => {
|
|
12
|
-
if (!template || !data) throw Error('No template or data provided')
|
|
13
|
-
if (typeof template === 'string' && template.match(/^https?:/)) {
|
|
14
|
-
const response = await fetch(template).catch(err => { return false})
|
|
15
|
-
if (!response) throw Error('Can\'t fetch the template URL')
|
|
16
|
-
template = response.arrayBuffer()
|
|
17
|
-
}
|
|
18
|
-
return await replace(template, data)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Downloads an XLSX file by replacing data in a template file and initiates the download in the browser.
|
|
23
|
-
* @param {string} templateURL - The URL of the template XLSX file.
|
|
24
|
-
* @param {Object} data - The data object containing values to replace in the template.
|
|
25
|
-
* @param {string} [fileName] - The name of the file to be downloaded. If not provided, a default name based on the current date will be used.
|
|
26
|
-
* @returns {Promise<void>} A promise that resolves once the download is initiated.
|
|
27
|
-
*/
|
|
28
|
-
export const downloadXlsx = async (template, data, fileName, mimeType) => {
|
|
29
|
-
if (!fileName) fileName = new Date().toISOString().substring(0, 10) + ' - Report.xlsx'
|
|
30
|
-
downloadBlob(await generateXlsx(template, data), fileName, mimeType)
|
|
31
|
-
}
|
package/lib/main.js
DELETED
|
@@ -1,294 +0,0 @@
|
|
|
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
|
-
/* Add custom style for dates */
|
|
38
|
-
const xmlStyle = await res.file('xl/styles.xml').async('string')
|
|
39
|
-
const domStyle = parser.parseFromString(xmlStyle, 'application/xml')
|
|
40
|
-
|
|
41
|
-
// Create date number format
|
|
42
|
-
const styleSheet = domStyle.querySelector('styleSheet')
|
|
43
|
-
let numFmts = domStyle.querySelector('numFmts')
|
|
44
|
-
let stylesAreSet = Boolean(numFmts)
|
|
45
|
-
if (!numFmts) numFmts = document.createElementNS(null, 'numFmts')
|
|
46
|
-
let numFmt = document.createElementNS(null, 'numFmt')
|
|
47
|
-
numFmt.setAttributeNS(null, 'numFmtId', '166')
|
|
48
|
-
numFmt.setAttributeNS(null, 'formatCode', 'yyyy\\-mm\\-dd;@')
|
|
49
|
-
let oldNumFmt = domStyle.querySelectorAll('numFmt')
|
|
50
|
-
let count = 0
|
|
51
|
-
const newNumFmts = numFmts.cloneNode()
|
|
52
|
-
oldNumFmt.forEach(numFmt => {
|
|
53
|
-
newNumFmts.append(numFmt)
|
|
54
|
-
count++
|
|
55
|
-
})
|
|
56
|
-
newNumFmts.append(numFmt)
|
|
57
|
-
count++
|
|
58
|
-
newNumFmts.setAttribute('count', count)
|
|
59
|
-
if (stylesAreSet) {
|
|
60
|
-
numFmts.replaceWith(newNumFmts)
|
|
61
|
-
} else {
|
|
62
|
-
styleSheet.prepend(newNumFmts)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Add a style for the date format
|
|
66
|
-
const cellXfs = domStyle.querySelector('cellXfs')
|
|
67
|
-
const xf = document.createElement('xf')
|
|
68
|
-
xf.setAttributeNS(null, 'numFmtId', 166)
|
|
69
|
-
xf.setAttributeNS(null, 'fontId', 0)
|
|
70
|
-
xf.setAttributeNS(null, 'fillId', 0)
|
|
71
|
-
xf.setAttributeNS(null, 'borderId', 0)
|
|
72
|
-
xf.setAttributeNS(null, 'xfId', 0)
|
|
73
|
-
xf.setAttributeNS(null, 'applyNumberFormat', 1)
|
|
74
|
-
const oldXf = domStyle.querySelectorAll('cellXfs xf')
|
|
75
|
-
count = 0
|
|
76
|
-
const newCellXfs = cellXfs.cloneNode()
|
|
77
|
-
oldXf.forEach(xf => {
|
|
78
|
-
newCellXfs.append(xf)
|
|
79
|
-
count++
|
|
80
|
-
})
|
|
81
|
-
count++
|
|
82
|
-
newCellXfs.append(xf)
|
|
83
|
-
newCellXfs.setAttribute('count', count)
|
|
84
|
-
const dateStyle = count - 1
|
|
85
|
-
cellXfs.replaceWith(newCellXfs)
|
|
86
|
-
const newXfData = serializer.serializeToString(domStyle).replace(/ ?xmlns="(http:\/\/www\.w3\.org\/1999\/xhtml)?"/g, '')
|
|
87
|
-
// Save new shared strings
|
|
88
|
-
await new_zip.file('xl/styles.xml', newXfData)
|
|
89
|
-
|
|
90
|
-
/* PROCESSING OF THE SHARED STRINGS */
|
|
91
|
-
|
|
92
|
-
// Read all shared text values
|
|
93
|
-
let xmlText = await res.file('xl/sharedStrings.xml').async('string')
|
|
94
|
-
// Get the xml with shared shared strings
|
|
95
|
-
const xml = parser.parseFromString(xmlText, 'application/xml')
|
|
96
|
-
// Get the header of shared strings
|
|
97
|
-
const sst = xml.querySelector('sst')
|
|
98
|
-
// Array with new values
|
|
99
|
-
const valuesToReplace = []
|
|
100
|
-
// New list of shared string values
|
|
101
|
-
let newSharedStrings = []
|
|
102
|
-
|
|
103
|
-
// We need to replace only text values with the new ones, therefore we process shared strings first
|
|
104
|
-
// Get all string items
|
|
105
|
-
xml.querySelectorAll('si').forEach((si, i) => {
|
|
106
|
-
// Get all rich format tags
|
|
107
|
-
const r = si.querySelector('r')
|
|
108
|
-
if (r) {
|
|
109
|
-
const xmlString = si.innerHTML
|
|
110
|
-
const newString = xmlString.replace(globalAccessorRegex, s => valueToString(getByNotation(data, s.replace(/\$\{|}/g, ''))))
|
|
111
|
-
si.innerHTML = newString
|
|
112
|
-
newSharedStrings.push(si)
|
|
113
|
-
valuesToReplace[i] = { value: newSharedStrings.length - 1, cellType: 's' }
|
|
114
|
-
return false
|
|
115
|
-
}
|
|
116
|
-
// Get all text tags
|
|
117
|
-
const t = si.querySelector('t')
|
|
118
|
-
if (!t) return false
|
|
119
|
-
// Get text value
|
|
120
|
-
const textValue = t.textContent
|
|
121
|
-
// Get all accessors
|
|
122
|
-
let match = textValue.match(accessorRegex)
|
|
123
|
-
// If the cell does not contain only one placeholder, check for multiple ones
|
|
124
|
-
if (!match) {
|
|
125
|
-
// Check if the text contains any accessors at all
|
|
126
|
-
let newText = textValue.replace(globalAccessorRegex, s => valueToString(getByNotation(data, s.replace(/\$\{|}/g, ''))))
|
|
127
|
-
newSharedStrings.push(createSharedString(newText, si))
|
|
128
|
-
valuesToReplace[i] = { value: newSharedStrings.length - 1, cellType: 's' }
|
|
129
|
-
return false
|
|
130
|
-
}
|
|
131
|
-
// Process a value with an accessor only
|
|
132
|
-
let { accessor, table } = match.groups
|
|
133
|
-
// If we have table values, we need to process them in a separate way
|
|
134
|
-
const isTable = typeof table === 'string'
|
|
135
|
-
let value = getByNotation(data, accessor)
|
|
136
|
-
if (typeof value === 'undefined') return false
|
|
137
|
-
// Save tables for further processing
|
|
138
|
-
if (isTable) {
|
|
139
|
-
if (!value) value = []
|
|
140
|
-
valuesToReplace[i] = { isTable, value: value.map(el => guessDataType(el, { dateStyle })) }
|
|
141
|
-
return false
|
|
142
|
-
}
|
|
143
|
-
if (Array.isArray(value)) {
|
|
144
|
-
valuesToReplace[i] = value.map(el => guessDataType(el, { dateStyle }))
|
|
145
|
-
return false
|
|
146
|
-
}
|
|
147
|
-
valuesToReplace[i] = guessDataType(value, { dateStyle })
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
/* PROCESSING OF WORKSHEETS */
|
|
151
|
-
|
|
152
|
-
// To prevent string duplication, store unique strings (they will be added to shared strings)
|
|
153
|
-
const newStrings = []
|
|
154
|
-
|
|
155
|
-
// Adds a string to the collection, avoiding duplication, and returns its index.
|
|
156
|
-
const addString = (value) => {
|
|
157
|
-
let stringIndex = newStrings.indexOf(value)
|
|
158
|
-
if (stringIndex === -1) {
|
|
159
|
-
newStrings.push(value)
|
|
160
|
-
stringIndex = newStrings.length - 1
|
|
161
|
-
}
|
|
162
|
-
value = stringIndex + newSharedStrings.length
|
|
163
|
-
return value
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Get all worksheets
|
|
167
|
-
const worksheets = Object.keys(res.files).filter(el => /xl\/worksheets\/[^/]+$/.test(el))
|
|
168
|
-
for (let worksheet of worksheets) {
|
|
169
|
-
// Unzip the file
|
|
170
|
-
let xmlText = await res.file(worksheet).async('string')
|
|
171
|
-
// Parse file to DOM
|
|
172
|
-
let xml = parser.parseFromString(xmlText, 'application/xml')
|
|
173
|
-
// Process rows
|
|
174
|
-
let rows = xml.querySelectorAll('sheetData row')
|
|
175
|
-
// Array with the list of the new rows
|
|
176
|
-
const newRows = []
|
|
177
|
-
// Process cells in rows
|
|
178
|
-
let rowOffset = 0
|
|
179
|
-
for (let row of rows) {
|
|
180
|
-
// Since some rows can be skipped, we want to get an actual row number from the template
|
|
181
|
-
let currentRow = parseInt(row.getAttribute('r'))
|
|
182
|
-
// List of new cells
|
|
183
|
-
let newCells = []
|
|
184
|
-
// If we have an array of values to extend the list of cells, we should keep an offset to move static cells
|
|
185
|
-
let cellOffset = 0
|
|
186
|
-
// Process each cell
|
|
187
|
-
let cells = row.querySelectorAll('c').forEach((c) => {
|
|
188
|
-
// Current cell index (note that Excel has 1-based index)
|
|
189
|
-
let index = getExcelColumnIndex(c.getAttribute('r'))
|
|
190
|
-
let newIndex = index + cellOffset
|
|
191
|
-
// Get the cell value tag
|
|
192
|
-
let v = c.querySelector('v') || {}
|
|
193
|
-
// Check if the cell contains formula, and skip
|
|
194
|
-
let isFormula = c.querySelector('f')
|
|
195
|
-
if (isFormula) {
|
|
196
|
-
const value = c
|
|
197
|
-
const cellType = 'f'
|
|
198
|
-
newCells[newIndex] = Object.assign({}, { value, cellType }, { template: c })
|
|
199
|
-
return false
|
|
200
|
-
}
|
|
201
|
-
// Check if the cell contains string
|
|
202
|
-
let isString = c.getAttribute('t')
|
|
203
|
-
if (!isString || isString !== 's') {
|
|
204
|
-
newCells[newIndex] = Object.assign({}, { template: c })
|
|
205
|
-
return false
|
|
206
|
-
}
|
|
207
|
-
// Get the new value to replace
|
|
208
|
-
let newValue = valuesToReplace[v.textContent] || {}
|
|
209
|
-
if (!Array.isArray(newValue)) {
|
|
210
|
-
let { value, cellType, style, isTable } = newValue
|
|
211
|
-
// If the new value is an array from the table, then return an array of values
|
|
212
|
-
if (isTable) {
|
|
213
|
-
newCells[newIndex] = value.map(el => {
|
|
214
|
-
if (!el) return { template: c }
|
|
215
|
-
if (el.cellType === 's' && typeof el.value === 'string') el.value = addString(el.value)
|
|
216
|
-
return { value: el.value, cellType: el.cellType, style: el.style, template: c }
|
|
217
|
-
})
|
|
218
|
-
return false
|
|
219
|
-
}
|
|
220
|
-
// Return a new value
|
|
221
|
-
if (cellType === 's' && typeof value === 'string') value = addString(value)
|
|
222
|
-
newCells[newIndex] = Object.assign({}, { value, cellType, style }, { template: c })
|
|
223
|
-
return false
|
|
224
|
-
}
|
|
225
|
-
// If the value is an array (not from table), then extend the existing list of cells
|
|
226
|
-
for (let i = 0; i < newValue.length; i++) {
|
|
227
|
-
let { value, cellType, style } = newValue[i] || {}
|
|
228
|
-
if (cellType === 's' && typeof value === 'string') value = addString(value)
|
|
229
|
-
newCells[newIndex + i] = Object.assign({}, { value, cellType, style }, { template: c })
|
|
230
|
-
if (i) cellOffset++
|
|
231
|
-
}
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
/* GENERATION OF THE NEW ROWS */
|
|
235
|
-
|
|
236
|
-
// Check if the row contains arrays and the values should be duplicated
|
|
237
|
-
let length = Math.max(...newCells.filter(el => Array.isArray(el)).map(el => el.length))
|
|
238
|
-
|
|
239
|
-
// Create a row
|
|
240
|
-
if (length <= 0) {
|
|
241
|
-
let rowIndex = rowOffset + currentRow
|
|
242
|
-
const rowValues = newCells.map((el, i) => {
|
|
243
|
-
if (!el) return undefined
|
|
244
|
-
return createCell(Object.assign({}, el, { row: rowIndex, column: i }))
|
|
245
|
-
}).filter(Boolean)
|
|
246
|
-
const newRow = row.cloneNode()
|
|
247
|
-
newRow.setAttribute('r', rowIndex)
|
|
248
|
-
rowValues.forEach(value => newRow.append(value))
|
|
249
|
-
newRows.push(newRow)
|
|
250
|
-
continue
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Create table rows
|
|
254
|
-
for (let i = 0; i < length; i++) {
|
|
255
|
-
let rowIndex = rowOffset + currentRow
|
|
256
|
-
rowOffset++
|
|
257
|
-
const rowValues = newCells.map((el, index) => {
|
|
258
|
-
if (!el) return undefined
|
|
259
|
-
if (Array.isArray(el)) {
|
|
260
|
-
if (typeof el[i] === 'undefined') return undefined
|
|
261
|
-
return createCell(Object.assign({}, el[i], { row: rowIndex, column: index }))
|
|
262
|
-
}
|
|
263
|
-
return createCell(Object.assign({}, el, { row: rowIndex, column: index }))
|
|
264
|
-
})
|
|
265
|
-
const newRow = row.cloneNode()
|
|
266
|
-
newRow.setAttribute('r', rowIndex)
|
|
267
|
-
rowValues.forEach(value => {
|
|
268
|
-
if (value) newRow.append(value)
|
|
269
|
-
})
|
|
270
|
-
newRows.push(newRow)
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Generate new sheet data
|
|
275
|
-
const newSheetData = xml.querySelector('sheetData').cloneNode()
|
|
276
|
-
newRows.forEach(row => newSheetData.append(row))
|
|
277
|
-
xml.querySelector('sheetData').replaceWith(newSheetData)
|
|
278
|
-
const newData = serializer.serializeToString(xml).replace(/ ?xmlns="http:\/\/www\.w3\.org\/1999\/xhtml"/g, '')
|
|
279
|
-
// Save the data of the worksheet
|
|
280
|
-
await new_zip.file(worksheet, newData)
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/* FINALIZE NEW SHARED STRINGS */
|
|
284
|
-
newSharedStrings = [...newSharedStrings, ...newStrings.map(el => createSharedString(el))]
|
|
285
|
-
const newSst = sst.cloneNode()
|
|
286
|
-
newSharedStrings.forEach(si => newSst.append(si))
|
|
287
|
-
sst.replaceWith(newSst)
|
|
288
|
-
const newData = serializer.serializeToString(xml).replace(/ ?xmlns="http:\/\/www\.w3\.org\/1999\/xhtml"/g, '')
|
|
289
|
-
// Save new shared strings
|
|
290
|
-
await new_zip.file('xl/sharedStrings.xml', newData)
|
|
291
|
-
return await new_zip.generateAsync({ type: 'blob' })
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export default replace
|