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/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