xlsx-template-browser 0.2.0 → 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,289 +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
- if (!numFmts) numFmts = document.createElementNS(null, 'numFmts')
45
- let numFmt = document.createElementNS(null, 'numFmt')
46
- numFmt.setAttributeNS(null, 'numFmtId', '166')
47
- numFmt.setAttributeNS(null, 'formatCode', 'yyyy\\-mm\\-dd;@')
48
- let oldNumFmt = domStyle.querySelectorAll('numFmt')
49
- let count = 0
50
- const newNumFmts = numFmts.cloneNode()
51
- oldNumFmt.forEach(numFmt => {
52
- newNumFmts.append(numFmt)
53
- count++
54
- })
55
- newNumFmts.append(numFmt)
56
- count++
57
- newNumFmts.setAttribute('count', count)
58
- styleSheet.prepend(newNumFmts)
59
-
60
- // Add a style for the date format
61
- const cellXfs = domStyle.querySelector('cellXfs')
62
- const xf = document.createElement('xf')
63
- xf.setAttributeNS(null, 'numFmtId', 166)
64
- xf.setAttributeNS(null, 'fontId', 0)
65
- xf.setAttributeNS(null, 'fillId', 0)
66
- xf.setAttributeNS(null, 'borderId', 0)
67
- xf.setAttributeNS(null, 'xfId', 0)
68
- xf.setAttributeNS(null, 'applyNumberFormat', 1)
69
- const oldXf = domStyle.querySelectorAll('cellXfs xf')
70
- count = 0
71
- const newCellXfs = cellXfs.cloneNode()
72
- oldXf.forEach(xf => {
73
- newCellXfs.append(xf)
74
- count++
75
- })
76
- count++
77
- newCellXfs.append(xf)
78
- newCellXfs.setAttribute('count', count)
79
- const dateStyle = count - 1
80
- cellXfs.replaceWith(newCellXfs)
81
- const newXfData = serializer.serializeToString(domStyle).replace(/ ?xmlns="(http:\/\/www\.w3\.org\/1999\/xhtml)?"/g, '')
82
- // Save new shared strings
83
- await new_zip.file('xl/styles.xml', newXfData)
84
-
85
- /* PROCESSING OF THE SHARED STRINGS */
86
-
87
- // Read all shared text values
88
- let xmlText = await res.file('xl/sharedStrings.xml').async('string')
89
- // Get the xml with shared shared strings
90
- const xml = parser.parseFromString(xmlText, 'application/xml')
91
- // Get the header of shared strings
92
- const sst = xml.querySelector('sst')
93
- // Array with new values
94
- const valuesToReplace = []
95
- // New list of shared string values
96
- let newSharedStrings = []
97
-
98
- // We need to replace only text values with the new ones, therefore we process shared strings first
99
- // Get all string items
100
- xml.querySelectorAll('si').forEach((si, i) => {
101
- // Get all rich format tags
102
- const r = si.querySelector('r')
103
- if (r) {
104
- const xmlString = si.innerHTML
105
- const newString = xmlString.replace(globalAccessorRegex, s => valueToString(getByNotation(data, s.replace(/\$\{|}/g, ''))))
106
- si.innerHTML = newString
107
- newSharedStrings.push(si)
108
- valuesToReplace[i] = { value: newSharedStrings.length - 1, cellType: 's' }
109
- return false
110
- }
111
- // Get all text tags
112
- const t = si.querySelector('t')
113
- if (!t) return false
114
- // Get text value
115
- const textValue = t.textContent
116
- // Get all accessors
117
- let match = textValue.match(accessorRegex)
118
- // If the cell does not contain only one placeholder, check for multiple ones
119
- if (!match) {
120
- // Check if the text contains any accessors at all
121
- let newText = textValue.replace(globalAccessorRegex, s => valueToString(getByNotation(data, s.replace(/\$\{|}/g, ''))))
122
- newSharedStrings.push(createSharedString(newText, si))
123
- valuesToReplace[i] = { value: newSharedStrings.length - 1, cellType: 's' }
124
- return false
125
- }
126
- // Process a value with an accessor only
127
- let { accessor, table } = match.groups
128
- // If we have table values, we need to process them in a separate way
129
- const isTable = typeof table === 'string'
130
- let value = getByNotation(data, accessor)
131
- if (typeof value === 'undefined') return false
132
- // Save tables for further processing
133
- if (isTable) {
134
- if (!value) value = []
135
- valuesToReplace[i] = { isTable, value: value.map(el => guessDataType(el, { dateStyle })) }
136
- return false
137
- }
138
- if (Array.isArray(value)) {
139
- valuesToReplace[i] = value.map(el => guessDataType(el, { dateStyle }))
140
- return false
141
- }
142
- valuesToReplace[i] = guessDataType(value, { dateStyle })
143
- })
144
-
145
- /* PROCESSING OF WORKSHEETS */
146
-
147
- // To prevent string duplication, store unique strings (they will be added to shared strings)
148
- const newStrings = []
149
-
150
- // Adds a string to the collection, avoiding duplication, and returns its index.
151
- const addString = (value) => {
152
- let stringIndex = newStrings.indexOf(value)
153
- if (stringIndex === -1) {
154
- newStrings.push(value)
155
- stringIndex = newStrings.length - 1
156
- }
157
- value = stringIndex + newSharedStrings.length
158
- return value
159
- }
160
-
161
- // Get all worksheets
162
- const worksheets = Object.keys(res.files).filter(el => /xl\/worksheets\/[^/]+$/.test(el))
163
- for (let worksheet of worksheets) {
164
- // Unzip the file
165
- let xmlText = await res.file(worksheet).async('string')
166
- // Parse file to DOM
167
- let xml = parser.parseFromString(xmlText, 'application/xml')
168
- // Process rows
169
- let rows = xml.querySelectorAll('sheetData row')
170
- // Array with the list of the new rows
171
- const newRows = []
172
- // Process cells in rows
173
- let rowOffset = 0
174
- for (let row of rows) {
175
- // Since some rows can be skipped, we want to get an actual row number from the template
176
- let currentRow = parseInt(row.getAttribute('r'))
177
- // List of new cells
178
- let newCells = []
179
- // If we have an array of values to extend the list of cells, we should keep an offset to move static cells
180
- let cellOffset = 0
181
- // Process each cell
182
- let cells = row.querySelectorAll('c').forEach((c) => {
183
- // Current cell index (note that Excel has 1-based index)
184
- let index = getExcelColumnIndex(c.getAttribute('r'))
185
- let newIndex = index + cellOffset
186
- // Get the cell value tag
187
- let v = c.querySelector('v') || {}
188
- // Check if the cell contains formula, and skip
189
- let isFormula = c.querySelector('f')
190
- if (isFormula) {
191
- const value = c
192
- const cellType = 'f'
193
- newCells[newIndex] = Object.assign({}, { value, cellType }, { template: c })
194
- return false
195
- }
196
- // Check if the cell contains string
197
- let isString = c.getAttribute('t')
198
- if (!isString || isString !== 's') {
199
- newCells[newIndex] = Object.assign({}, { template: c })
200
- return false
201
- }
202
- // Get the new value to replace
203
- let newValue = valuesToReplace[v.textContent] || {}
204
- if (!Array.isArray(newValue)) {
205
- let { value, cellType, style, isTable } = newValue
206
- // If the new value is an array from the table, then return an array of values
207
- if (isTable) {
208
- newCells[newIndex] = value.map(el => {
209
- if (!el) return { template: c }
210
- if (el.cellType === 's' && typeof el.value === 'string') el.value = addString(el.value)
211
- return { value: el.value, cellType: el.cellType, style: el.style, template: c }
212
- })
213
- return false
214
- }
215
- // Return a new value
216
- if (cellType === 's' && typeof value === 'string') value = addString(value)
217
- newCells[newIndex] = Object.assign({}, { value, cellType, style }, { template: c })
218
- return false
219
- }
220
- // If the value is an array (not from table), then extend the existing list of cells
221
- for (let i = 0; i < newValue.length; i++) {
222
- let { value, cellType, style } = newValue[i] || {}
223
- if (cellType === 's' && typeof value === 'string') value = addString(value)
224
- newCells[newIndex + i] = Object.assign({}, { value, cellType, style }, { template: c })
225
- if (i) cellOffset++
226
- }
227
- })
228
-
229
- /* GENERATION OF THE NEW ROWS */
230
-
231
- // Check if the row contains arrays and the values should be duplicated
232
- let length = Math.max(...newCells.filter(el => Array.isArray(el)).map(el => el.length))
233
-
234
- // Create a row
235
- if (length <= 0) {
236
- let rowIndex = rowOffset + currentRow
237
- const rowValues = newCells.map((el, i) => {
238
- if (!el) return undefined
239
- return createCell(Object.assign({}, el, { row: rowIndex, column: i }))
240
- }).filter(Boolean)
241
- const newRow = row.cloneNode()
242
- newRow.setAttribute('r', rowIndex)
243
- rowValues.forEach(value => newRow.append(value))
244
- newRows.push(newRow)
245
- continue
246
- }
247
-
248
- // Create table rows
249
- for (let i = 0; i < length; i++) {
250
- let rowIndex = rowOffset + currentRow
251
- rowOffset++
252
- const rowValues = newCells.map((el, index) => {
253
- if (!el) return undefined
254
- if (Array.isArray(el)) {
255
- if (typeof el[i] === 'undefined') return undefined
256
- return createCell(Object.assign({}, el[i], { row: rowIndex, column: index }))
257
- }
258
- return createCell(Object.assign({}, el, { row: rowIndex, column: index }))
259
- })
260
- const newRow = row.cloneNode()
261
- newRow.setAttribute('r', rowIndex)
262
- rowValues.forEach(value => {
263
- if (value) newRow.append(value)
264
- })
265
- newRows.push(newRow)
266
- }
267
- }
268
-
269
- // Generate new sheet data
270
- const newSheetData = xml.querySelector('sheetData').cloneNode()
271
- newRows.forEach(row => newSheetData.append(row))
272
- xml.querySelector('sheetData').replaceWith(newSheetData)
273
- const newData = serializer.serializeToString(xml).replace(/ ?xmlns="http:\/\/www\.w3\.org\/1999\/xhtml"/g, '')
274
- // Save the data of the worksheet
275
- await new_zip.file(worksheet, newData)
276
- }
277
-
278
- /* FINALIZE NEW SHARED STRINGS */
279
- newSharedStrings = [...newSharedStrings, ...newStrings.map(el => createSharedString(el))]
280
- const newSst = sst.cloneNode()
281
- newSharedStrings.forEach(si => newSst.append(si))
282
- sst.replaceWith(newSst)
283
- const newData = serializer.serializeToString(xml).replace(/ ?xmlns="http:\/\/www\.w3\.org\/1999\/xhtml"/g, '')
284
- // Save new shared strings
285
- await new_zip.file('xl/sharedStrings.xml', newData)
286
- return await new_zip.generateAsync({ type: 'blob' })
287
- }
288
-
289
- export default replace