diff --git a/.github/workflows/asset-size.yml b/.github/workflows/asset-size.yml index 2ac84661d..f9c813146 100644 --- a/.github/workflows/asset-size.yml +++ b/.github/workflows/asset-size.yml @@ -4,6 +4,7 @@ on: [pull_request] jobs: compare: + timeout-minutes: 15 runs-on: ubuntu-latest steps: diff --git a/.github/workflows/exceljs.yml b/.github/workflows/tests.yml similarity index 66% rename from .github/workflows/exceljs.yml rename to .github/workflows/tests.yml index 876b57eae..7ae75016b 100644 --- a/.github/workflows/exceljs.yml +++ b/.github/workflows/tests.yml @@ -1,4 +1,4 @@ -name: ExcelJS +name: Tests on: push: @@ -8,12 +8,12 @@ on: jobs: test: + timeout-minutes: 10 name: Node v${{ matrix.node-version }} on ${{ matrix.os }} strategy: fail-fast: false matrix: - # https://github.com/actions/setup-node/issues/27 - node-version: [8.17.0, 10.x, 12.x, 14.x, 16.x] + node-version: [10.x, 12.x, 14.x, 16.x, 17.x, 18.x, 19.x, 20.x] os: [ubuntu-latest, macOS-latest, windows-latest] runs-on: ${{ matrix.os }} @@ -24,15 +24,15 @@ jobs: git config --global core.autocrlf false git config --global core.symlinks true if: runner.os == 'Windows' - - uses: actions/checkout@v2 + - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 #latest v2. TODO upgrade - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 #latest v2. TODO upgrade with: node-version: ${{ matrix.node-version }} - name: Create the npm cache directory run: mkdir npm-cache && npm config set cache ./npm-cache --global - name: Cache node modules - uses: actions/cache@v1 + uses: actions/cache@8492260343ad570701412c2f464a5877dc76bace #latest v2 TODO upgrade with: path: ./npm-cache key: v1-${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('**/package.json') }} @@ -44,18 +44,19 @@ jobs: CI: true benchmark: + timeout-minutes: 15 name: Measure performance impact of changes runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 #latest v2. TODO upgrade + - uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 #latest v2. TODO upgrade with: - node-version: 12.x + node-version: 18 - name: Create the npm cache directory run: mkdir npm-cache && npm config set cache ./npm-cache --global - name: Cache node modules - uses: actions/cache@v1 + uses: actions/cache@8492260343ad570701412c2f464a5877dc76bace #latest v2 TODO upgrade with: path: ./npm-cache key: v1-npm-${{ hashFiles('**/package.json') }} @@ -67,18 +68,19 @@ jobs: CI: true typescript: + timeout-minutes: 15 name: Ensure typescript compatibility runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 + - uses: actions/checkout@ee0669bd1cc54295c223e0bb666b733df41de1c5 #latest v2. TODO upgrade + - uses: actions/setup-node@7c12f8017d5436eb855f1ed4399f037a36fbd9e8 #latest v2. TODO upgrade with: - node-version: 12.x + node-version: 18 - name: Create the npm cache directory run: mkdir npm-cache && npm config set cache ./npm-cache --global - name: Cache node modules - uses: actions/cache@v1 + uses: actions/cache@8492260343ad570701412c2f464a5877dc76bace #latest v2 TODO upgrade with: path: ./npm-cache key: v1-npm-${{ hashFiles('**/package.json') }} diff --git a/.gitignore b/.gitignore index b2e50bdae..8026a7080 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,5 @@ _SpecRunner.html .DS_Store # Ignore xlsx files generated during testing -*.xlsx \ No newline at end of file +*.xlsx +enpm-lock.yaml \ No newline at end of file diff --git a/README.md b/README.md index 5b8d3a250..e70b1d2be 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # ExcelJS -[![Build status](https://github.com/exceljs/exceljs/workflows/ExcelJS/badge.svg)](https://github.com/exceljs/exceljs/actions?query=workflow%3AExcelJS) -[![Code Quality: Javascript](https://img.shields.io/lgtm/grade/javascript/g/exceljs/exceljs.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/exceljs/exceljs/context:javascript) -[![Total Alerts](https://img.shields.io/lgtm/alerts/g/exceljs/exceljs.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/exceljs/exceljs/alerts) +[![Build Status](https://github.com/exceljs/exceljs/actions/workflows/tests.yml/badge.svg?branch=master&event=push)](https://github.com/exceljs/exceljs/actions/workflows/tests.yml) Read, manipulate and write spreadsheet data and styles to XLSX and JSON. @@ -20,59 +18,40 @@ npm install exceljs # New Features! - - - +* Merged [fix: styles rendering in case when "numFmt" is present in conditional formatting rules (resolves #1814) #1815](https://github.com/exceljs/exceljs/pull/1815).
Many thanks to [@andreykrupskii](https://github.com/andreykrupskii) for this contribution! +* Merged [inlineStr cell type support #1575 #1576](https://github.com/exceljs/exceljs/pull/1576).
Many thanks to [@drdmitry](https://github.com/drdmitry) for this contribution! +* Merged [Fix parsing of boolean attributes #1849](https://github.com/exceljs/exceljs/pull/1849).
Many thanks to [@bno1](https://github.com/bno1) for this contribution! +* Merged [add optional custom auto-filter to table #1670](https://github.com/exceljs/exceljs/pull/1670).
Many thanks to [@thambley](https://github.com/thambley) for this contribution! +* Merged [Deep copy inherited style #1850](https://github.com/exceljs/exceljs/pull/1850).
Many thanks to [@ikzhr](https://github.com/ikzhr) for this contribution! +* Merged [Upgrade actions/cache and actions/setup-node #1846](https://github.com/exceljs/exceljs/pull/1846).
Many thanks to [@cclauss](https://github.com/cclauss) for this contribution! +* Merged [Check object keys in isEqual #1831](https://github.com/exceljs/exceljs/pull/1831).
Many thanks to [@bno1](https://github.com/bno1) for this contribution! +* Merged [Add v17 to testing workflow #1856](https://github.com/exceljs/exceljs/pull/1856).
Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [Upgrade jszip to its latest version to date. This version does not have any vulnerability found by Snyk so far #1895](https://github.com/exceljs/exceljs/pull/1895).
Many thanks to [@ValerioSevilla](https://github.com/ValerioSevilla) for this contribution! +* Merged [Update README.md #1677](https://github.com/exceljs/exceljs/pull/1677).
Many thanks to [@xjrcode](https://github.com/xjrcode) for this contribution! +* Merged [(docs): set prototype of RegExp correctly. #1700](https://github.com/exceljs/exceljs/pull/1700).
Many thanks to [@joeldenning](https://github.com/joeldenning) for this contribution! +* Merged [Added timeouts to github actions #1733](https://github.com/exceljs/exceljs/pull/1733).
Many thanks to [@alexbjorlig](https://github.com/alexbjorlig) for this contribution! +* Merged [fix issue 1676 #1701](https://github.com/exceljs/exceljs/pull/1701).
Many thanks to [@skypesky](https://github.com/skypesky) for this contribution! +* Merged [ExcelJS/ExcelJS#2237 : Update CI Tests, Drop support for Node v8 #2242](https://github.com/exceljs/exceljs/pull/2242).
Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [Fix types for getWorksheet() #2223](https://github.com/exceljs/exceljs/pull/2223).
Many thanks to [@hfhchan-plb](https://github.com/hfhchan-plb) for this contribution! +* Merged [add characters cannot be used for worksheet name #2126](https://github.com/exceljs/exceljs/pull/2126).
Many thanks to [@tkm-kj](https://github.com/tkm-kj) for this contribution! +* Merged [Fix issue #1753 Reject promise when workbook reader is writing to temporary file stream and error occurs #1756](https://github.com/exceljs/exceljs/pull/1756).
Many thanks to [@pauliusg](https://github.com/pauliusg) for this contribution! +* Merged [README.md to have correct link for Streaming XLSX #2186](https://github.com/exceljs/exceljs/pull/2186).
Many thanks to [@wulfsolter](https://github.com/wulfsolter) for this contribution! +* Merged [Added a polyfill of promise.finally to support lower versions of Firefox. #1982](https://github.com/exceljs/exceljs/pull/1982).
Many thanks to [@DemoJj](https://github.com/DemoJj) for this contribution! +* Merged [Fix read this.worksheet before assign it #1934](https://github.com/exceljs/exceljs/pull/1934).
Many thanks to [@ZyqGitHub1](https://github.com/ZyqGitHub1) for this contribution! +* Merged [chore: upgrade jszip to ^3.10.1 #2211](https://github.com/exceljs/exceljs/pull/2211).
Many thanks to [@jarrod-cocoon](https://github.com/jarrod-cocoon) for this contribution! +* Merged [fixed spelling error in README.md file #2208](https://github.com/exceljs/exceljs/pull/2208).
Many thanks to [@HugoP27](https://github.com/HugoP27) for this contribution! +* Merged [fix: Fix xlsx.writeFile() not catching error when error occurs #2244](https://github.com/exceljs/exceljs/pull/2244).
Many thanks to [@zurmokeeper](https://github.com/zurmokeeper) for this contribution! +* Merged [Improve worksheets' naming validation logic. #2257](https://github.com/exceljs/exceljs/pull/2257).
Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [fix issue 2125 - spliceRows remove last row #2140](https://github.com/exceljs/exceljs/pull/2140).
Many thanks to [@babu-ch](https://github.com/babu-ch) for this contribution! +* Merged [fix: fix the loss of column attributes due to incorrect column order #2222](https://github.com/exceljs/exceljs/pull/2222).
Many thanks to [@cpaiyueyue](https://github.com/cpaiyueyue) for this contribution! +* Merged [Fix: Sheet Properties Types #2327](https://github.com/exceljs/exceljs/pull/2327).
Many thanks to [@albeniraouf](https://github.com/albeniraouf) for this contribution! +* Merged [Use node 18 LTS for tsc, and benchmark. Add node 20. to test matrix. … #2354](https://github.com/exceljs/exceljs/pull/2354).
Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [Add missing tooltip attribute to CellHyperlinkValue index.d.ts #2350](https://github.com/exceljs/exceljs/pull/2350).
Many thanks to [@NiklasPor](https://github.com/NiklasPor) for this contribution! +* Merged [Increase resilience to generating large workbooks #2320](https://github.com/exceljs/exceljs/pull/2320).
Many thanks to [@hfhchan-plb](https://github.com/hfhchan-plb) for this contribution! +* Merged [repair all 'c2fo.io' links ('c2fo.github.io') #2324](https://github.com/exceljs/exceljs/pull/2324).
Many thanks to [@justintunev7](https://github.com/justintunev7) for this contribution! +* Merged [fix: fix type definitions about last column, formula values and protection #2309](https://github.com/exceljs/exceljs/pull/2309).
Many thanks to [@gltjk](https://github.com/gltjk) for this contribution! +* Merged [fix: add spinCount field for WorksheetProtection type #2284](https://github.com/exceljs/exceljs/pull/2284).
Many thanks to [@damingerdai](https://github.com/damingerdai) for this contribution! +* Merged [Add type definition for WorksheetModel.merges #2281](https://github.com/exceljs/exceljs/pull/2281).
Many thanks to [@ytjmt](https://github.com/ytjmt) for this contribution! # Contributions @@ -86,6 +65,10 @@ Versions are updated on release and any change will most likely result in merge To be clear, all contributions added to this library will be included in the library's MIT licence. +### Let's chat together: + +[![SiemaTeam](https://discordapp.com/api/guilds/976854442009825321/widget.png?style=banner2)](https://discord.gg/siema) + # Contents @@ -239,7 +222,7 @@ try { } return new RegExp(pattern, flags); }; - global.RegExp.prototype = RegExp; + global.RegExp.prototype = RegExp.prototype; } ``` @@ -373,7 +356,7 @@ workbook.worksheets[0]; //the first one; ``` It's important to know that `workbook.getWorksheet(1) != Workbook.worksheets[0]` and `workbook.getWorksheet(1) != Workbook.worksheets[1]`, -becouse `workbook.worksheets[0].id` may have any value. +because `workbook.worksheets[0].id` may have any value. ## Worksheet State[⬆](#contents) @@ -2157,6 +2140,12 @@ faster or more resilient. #### Reading XLSX[⬆](#contents) +Options supported when reading CSV files. + +| Field | Required | Type |Description | +| ---------------- | ----------- | ----------- | ----------- | +| ignoreNodes | N | Array | A list of node names to ignore while loading the XLSX document. Improves performance in some situations.
Available: `sheetPr`, `dimension`, `sheetViews `, `sheetFormatPr`, `cols `, `sheetData`, `autoFilter `, `mergeCells `, `rowBreaks`, `hyperlinks `, `pageMargins`, `dataValidations`, `pageSetup`, `headerFooter `, `printOptions `, `picture`, `drawing`, `sheetProtection`, `tableParts `, `conditionalFormatting`, `extLst`,| + ```javascript // read from a file const workbook = new Excel.Workbook(); @@ -2174,6 +2163,16 @@ await workbook.xlsx.read(stream); const workbook = new Excel.Workbook(); await workbook.xlsx.load(data); // ... use workbook + + +// using additional options +const workbook = new Excel.Workbook(); +await workbook.xlsx.load(data, { + ignoreNodes: [ + 'dataValidations' // ignores the workbook's Data Validations + ], +}); +// ... use workbook ``` #### Writing XLSX[⬆](#contents) @@ -2201,7 +2200,7 @@ Options supported when reading CSV files. | dateFormats | N | Array | Specify the date encoding format of dayjs. | | map | N | Function | Custom Array.prototype.map() callback function for processing data. | | sheetName | N | String | Specify worksheet name. | -| parserOptions | N | Object | [parseOptions options](https://c2fo.io/fast-csv/docs/parsing/options) @fast-csv/format module to write csv data. | +| parserOptions | N | Object | [parseOptions options](https://c2fo.github.io/fast-csv/docs/parsing/options) @fast-csv/format module to write csv data. | ```javascript // read from a file @@ -2244,7 +2243,7 @@ const options = { return parseFloat(value); } }, - // https://c2fo.io/fast-csv/docs/parsing/options + // https://c2fo.github.io/fast-csv/docs/parsing/options parserOptions: { delimiter: '\t', quote: false, @@ -2280,7 +2279,7 @@ Options supported when writing to a CSV file. | map | N | Function | Custom Array.prototype.map() callback function for processing row values. | | sheetName | N | String | Specify worksheet name. | | sheetId | N | Number | Specify worksheet ID. | -| formatterOptions | N | Object | [formatterOptions options](https://c2fo.io/fast-csv/docs/formatting/options/) @fast-csv/format module to write csv data. | +| formatterOptions | N | Object | [formatterOptions options](https://c2fo.github.io/fast-csv/docs/formatting/options/) @fast-csv/format module to write csv data. | ```javascript @@ -2321,7 +2320,7 @@ const options = { return value; } }, - // https://c2fo.io/fast-csv/docs/formatting/options + // https://c2fo.github.io/fast-csv/docs/formatting/options formatterOptions: { delimiter: '\t', quote: false, @@ -2631,10 +2630,13 @@ An Excel formula for calculating values on the fly. Note that ExcelJS cannot process the formula to generate a result, it must be supplied. +Note that function semantic names must be in English and the separator must be a comma. + E.g. ```javascript worksheet.getCell('A3').value = { formula: 'A1+A2', result: 7 }; +worksheet.getCell('A3').value = { formula: 'SUM(A1,A2)', result: 7 }; ``` Cells also support convenience getters to access the formula and result: @@ -2875,7 +2877,7 @@ If any splice operation affects a merged cell, the merge group will not be moved # Release History[⬆](#contents) | Version | Changes | -| ------- | ------- | +|---------| ------- | | 0.0.9 | | | 0.1.0 | | | 0.1.1 | | @@ -3018,5 +3020,4 @@ If any splice operation affects a merged cell, the merge group will not be moved | 4.1.1 | | | 4.2.0 | | | 4.2.1 | | - - +| 4.3.0 | | diff --git a/README_zh.md b/README_zh.md index dc05bf49a..5f1100363 100644 --- a/README_zh.md +++ b/README_zh.md @@ -16,20 +16,40 @@ npm install exceljs # 新的功能! - +* Merged [fix: styles rendering in case when "numFmt" is present in conditional formatting rules (resolves #1814) #1815](https://github.com/exceljs/exceljs/pull/1815).
Many thanks to [@andreykrupskii](https://github.com/andreykrupskii) for this contribution! +* Merged [inlineStr cell type support #1575 #1576](https://github.com/exceljs/exceljs/pull/1576).
Many thanks to [@drdmitry](https://github.com/drdmitry) for this contribution! +* Merged [Fix parsing of boolean attributes #1849](https://github.com/exceljs/exceljs/pull/1849).
Many thanks to [@bno1](https://github.com/bno1) for this contribution! +* Merged [add optional custom auto-filter to table #1670](https://github.com/exceljs/exceljs/pull/1670).
Many thanks to [@thambley](https://github.com/thambley) for this contribution! +* Merged [Deep copy inherited style #1850](https://github.com/exceljs/exceljs/pull/1850).
Many thanks to [@ikzhr](https://github.com/ikzhr) for this contribution! +* Merged [Upgrade actions/cache and actions/setup-node #1846](https://github.com/exceljs/exceljs/pull/1846).
Many thanks to [@cclauss](https://github.com/cclauss) for this contribution! +* Merged [Check object keys in isEqual #1831](https://github.com/exceljs/exceljs/pull/1831).
Many thanks to [@bno1](https://github.com/bno1) for this contribution! +* Merged [Add v17 to testing workflow #1856](https://github.com/exceljs/exceljs/pull/1856).
Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [Upgrade jszip to its latest version to date. This version does not have any vulnerability found by Snyk so far #1895](https://github.com/exceljs/exceljs/pull/1895).
Many thanks to [@ValerioSevilla](https://github.com/ValerioSevilla) for this contribution! +* Merged [Update README.md #1677](https://github.com/exceljs/exceljs/pull/1677).
Many thanks to [@xjrcode](https://github.com/xjrcode) for this contribution! +* Merged [(docs): set prototype of RegExp correctly. #1700](https://github.com/exceljs/exceljs/pull/1700).
Many thanks to [@joeldenning](https://github.com/joeldenning) for this contribution! +* Merged [Added timeouts to github actions #1733](https://github.com/exceljs/exceljs/pull/1733).
Many thanks to [@alexbjorlig](https://github.com/alexbjorlig) for this contribution! +* Merged [fix issue 1676 #1701](https://github.com/exceljs/exceljs/pull/1701).
Many thanks to [@skypesky](https://github.com/skypesky) for this contribution! +* Merged [ExcelJS/ExcelJS#2237 : Update CI Tests, Drop support for Node v8 #2242](https://github.com/exceljs/exceljs/pull/2242).
Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [Fix types for getWorksheet() #2223](https://github.com/exceljs/exceljs/pull/2223).
Many thanks to [@hfhchan-plb](https://github.com/hfhchan-plb) for this contribution! +* Merged [add characters cannot be used for worksheet name #2126](https://github.com/exceljs/exceljs/pull/2126).
Many thanks to [@tkm-kj](https://github.com/tkm-kj) for this contribution! +* Merged [Fix issue #1753 Reject promise when workbook reader is writing to temporary file stream and error occurs #1756](https://github.com/exceljs/exceljs/pull/1756).
Many thanks to [@pauliusg](https://github.com/pauliusg) for this contribution! +* Merged [README.md to have correct link for Streaming XLSX #2186](https://github.com/exceljs/exceljs/pull/2186).
Many thanks to [@wulfsolter](https://github.com/wulfsolter) for this contribution! +* Merged [Added a polyfill of promise.finally to support lower versions of Firefox. #1982](https://github.com/exceljs/exceljs/pull/1982).
Many thanks to [@DemoJj](https://github.com/DemoJj) for this contribution! +* Merged [Fix read this.worksheet before assign it #1934](https://github.com/exceljs/exceljs/pull/1934).
Many thanks to [@ZyqGitHub1](https://github.com/ZyqGitHub1) for this contribution! +* Merged [chore: upgrade jszip to ^3.10.1 #2211](https://github.com/exceljs/exceljs/pull/2211).
Many thanks to [@jarrod-cocoon](https://github.com/jarrod-cocoon) for this contribution! +* Merged [fixed spelling error in README.md file #2208](https://github.com/exceljs/exceljs/pull/2208).
Many thanks to [@HugoP27](https://github.com/HugoP27) for this contribution! +* Merged [fix: Fix xlsx.writeFile() not catching error when error occurs #2244](https://github.com/exceljs/exceljs/pull/2244).
Many thanks to [@zurmokeeper](https://github.com/zurmokeeper) for this contribution! +* Merged [Improve worksheets' naming validation logic. #2257](https://github.com/exceljs/exceljs/pull/2257).
Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [fix issue 2125 - spliceRows remove last row #2140](https://github.com/exceljs/exceljs/pull/2140).
Many thanks to [@babu-ch](https://github.com/babu-ch) for this contribution! +* Merged [fix: fix the loss of column attributes due to incorrect column order #2222](https://github.com/exceljs/exceljs/pull/2222).
Many thanks to [@cpaiyueyue](https://github.com/cpaiyueyue) for this contribution! +* Merged [Fix: Sheet Properties Types #2327](https://github.com/exceljs/exceljs/pull/2327).
Many thanks to [@albeniraouf](https://github.com/albeniraouf) for this contribution! +* Merged [Use node 18 LTS for tsc, and benchmark. Add node 20. to test matrix. … #2354](https://github.com/exceljs/exceljs/pull/2354).
Many thanks to [@Siemienik](https://github.com/Siemienik) for this contribution! +* Merged [Add missing tooltip attribute to CellHyperlinkValue index.d.ts #2350](https://github.com/exceljs/exceljs/pull/2350).
Many thanks to [@NiklasPor](https://github.com/NiklasPor) for this contribution! +* Merged [Increase resilience to generating large workbooks #2320](https://github.com/exceljs/exceljs/pull/2320).
Many thanks to [@hfhchan-plb](https://github.com/hfhchan-plb) for this contribution! +* Merged [repair all 'c2fo.io' links ('c2fo.github.io') #2324](https://github.com/exceljs/exceljs/pull/2324).
Many thanks to [@justintunev7](https://github.com/justintunev7) for this contribution! +* Merged [fix: fix type definitions about last column, formula values and protection #2309](https://github.com/exceljs/exceljs/pull/2309).
Many thanks to [@gltjk](https://github.com/gltjk) for this contribution! +* Merged [fix: add spinCount field for WorksheetProtection type #2284](https://github.com/exceljs/exceljs/pull/2284).
Many thanks to [@damingerdai](https://github.com/damingerdai) for this contribution! +* Merged [Add type definition for WorksheetModel.merges #2281](https://github.com/exceljs/exceljs/pull/2281).
Many thanks to [@ytjmt](https://github.com/ytjmt) for this contribution! # 贡献 @@ -196,7 +216,7 @@ try { } return new RegExp(pattern, flags); }; - global.RegExp.prototype = RegExp; + global.RegExp.prototype = RegExp.prototype; } ``` @@ -2089,7 +2109,7 @@ const buffer = await workbook.xlsx.writeBuffer(); | dateFormats | N | Array | 指定 dayjs 的日期编码格式。 | | map | N | Function | 自定义`Array.prototype.map()` 回调函数,用于处理数据。 | | sheetName | N | String | 指定工作表名称。 | -| parserOptions | N | Object | [parseOptions 选项](https://c2fo.io/fast-csv/docs/parsing/options) @fast-csv/format 模块以写入 csv 数据。 | +| parserOptions | N | Object | [parseOptions 选项](https://c2fo.github.io/fast-csv/docs/parsing/options) @fast-csv/format 模块以写入 csv 数据。 | ```javascript // 从文件读取 @@ -2132,7 +2152,7 @@ const options = { return parseFloat(value); } }, - // https://c2fo.io/fast-csv/docs/parsing/options + // https://c2fo.github.io/fast-csv/docs/parsing/options parserOptions: { delimiter: '\t', quote: false, @@ -2165,7 +2185,7 @@ CSV 解析器使用 [fast-csv](https://www.npmjs.com/package/fast-csv) 读取CSV | map | N | Function | 自定义`Array.prototype.map()` 回调函数,用于处理行值。 | | sheetName | N | String | 指定工作表名称。 | | sheetId | N | Number | 指定工作表 ID。 | -| formatterOptions | N | Object | [formatterOptions 选项](https://c2fo.io/fast-csv/docs/formatting/options/) @fast-csv/format 模块写入csv 数据。 | +| formatterOptions | N | Object | [formatterOptions 选项](https://c2fo.github.io/fast-csv/docs/formatting/options/) @fast-csv/format 模块写入csv 数据。 | ```javascript @@ -2205,7 +2225,7 @@ const options = { return value; } }, - // https://c2fo.io/fast-csv/docs/formatting/options + // https://c2fo.github.io/fast-csv/docs/formatting/options formatterOptions: { delimiter: '\t', quote: false, @@ -2711,19 +2731,19 @@ sudo apt-get install libfontconfig # 发布历史[⬆](#目录) -| 版本 | 变化 | -| ------- | ------- | -| 0.0.9 | | -| 0.1.0 | | -| 0.1.1 | | -| 0.1.2 | | -| 0.1.3 | | -| 0.1.5 | | -| 0.1.6 | | -| 0.1.8 | | -| 0.1.9 | | -| 0.1.10 | | -| 0.1.11 | | +| Version | Changes | +|---------| ------- | +| 0.0.9 | | +| 0.1.0 | | +| 0.1.1 | | +| 0.1.2 | | +| 0.1.3 | | +| 0.1.5 | | +| 0.1.6 | | +| 0.1.8 | | +| 0.1.9 | | +| 0.1.10 | | +| 0.1.11 | | | 0.2.0 | | | 0.2.2 | | | 0.2.3 | | @@ -2848,3 +2868,11 @@ sudo apt-get install libfontconfig | 3.8.0 | | | 3.8.1 | | | 3.8.2 | | +| 3.9.0 | | +| 3.10.0 | | +| 4.0.1 | | +| 4.1.0 | | +| 4.1.1 | | +| 4.2.0 | | +| 4.2.1 | | +| 4.3.0 | | diff --git a/gruntfile.js b/gruntfile.js index cc1574170..384b04500 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -27,14 +27,17 @@ module.exports = function(grunt) { browserify: { options: { transform: [ - ['babelify', { - // enable babel transpile for node_modules - global: true, - presets: ['@babel/preset-env'], - // core-js should not be transpiled - // See https://github.com/zloirock/core-js/issues/514 - ignore: [/node_modules[\\/]core-js/], - }], + [ + 'babelify', + { + // enable babel transpile for node_modules + global: true, + presets: ['@babel/preset-env'], + // core-js should not be transpiled + // See https://github.com/zloirock/core-js/issues/514 + ignore: [/node_modules[\\/]core-js/], + }, + ], ], browserifyOptions: { // enable source map for browserify @@ -119,6 +122,10 @@ module.exports = function(grunt) { }, jasmine: { + options: { + version: '3.8.0', + noSandbox: true, + }, dev: { src: ['./dist/exceljs.js'], options: { diff --git a/index.d.ts b/index.d.ts index c5174c0d2..5979806b6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -230,7 +230,7 @@ export interface Font { } export type BorderStyle = - | 'thin' | 'dotted' | 'hair' | 'medium' | 'double' | 'thick' | 'dashDot' + | 'thin' | 'dotted' | 'hair' | 'medium' | 'double' | 'thick' | 'dashed' | 'dashDot' | 'dashDotDot' | 'slantDashDot' | 'mediumDashed' | 'mediumDashDotDot' | 'mediumDashDot'; export interface Color { @@ -289,6 +289,7 @@ export interface Alignment { export interface Protection { locked: boolean; + hidden: boolean; } export interface Style { @@ -344,19 +345,20 @@ export interface CellRichTextValue { export interface CellHyperlinkValue { text: string; hyperlink: string; + tooltip?: string; } export interface CellFormulaValue { formula: string; - result?: number | string | Date | { error: CellErrorValue }; - date1904: boolean; + result?: number | string | boolean | Date | CellErrorValue; + date1904?: boolean; } export interface CellSharedFormulaValue { sharedFormula: string; readonly formula?: string; - result?: number | string | Date | { error: CellErrorValue }; - date1904: boolean; + result?: number | string | boolean | Date | CellErrorValue; + date1904?: boolean; } export declare enum ValueType { @@ -880,6 +882,7 @@ export interface WorksheetProtection { sort: boolean; autoFilter: boolean; pivotTables: boolean; + spinCount: number; } export interface Image { extension: 'jpeg' | 'png' | 'gif'; @@ -989,6 +992,7 @@ export interface WorksheetModel { views: WorksheetView[]; autoFilter: AutoFilter; media: Media[]; + merges: Range['range'][]; } export type WorksheetState = 'visible' | 'hidden' | 'veryHidden'; @@ -1139,7 +1143,7 @@ export interface Worksheet { /** * Get the last column in a worksheet */ - readonly lastColumn: Column; + readonly lastColumn: Column | undefined; /** * A count of the number of columns that have values. @@ -1401,6 +1405,13 @@ export interface WorksheetProperties { */ outlineLevelRow: number; + /** + * The outline properties which controls how it will summarize rows and columns + */ + outlineProperties: { + summaryBelow: boolean, + summaryRight: boolean, + }; /** * Default row height (default: 15) */ @@ -1446,6 +1457,13 @@ export interface JSZipGeneratorOptions { }; } +export interface XlsxReadOptions { + /** + * The list of XML node names to ignore while parsing an XLSX file + */ + ignoreNodes: string[]; +} + export interface XlsxWriteOptions extends stream.xlsx.WorkbookWriterOptions { /** * The option passed to JsZip#generateAsync(options) @@ -1457,19 +1475,19 @@ export interface Xlsx { /** * read from a file */ - readFile(path: string): Promise; + readFile(path: string, options?: Partial): Promise; /** * read from a stream * @param stream */ - read(stream: import('stream').Stream): Promise; + read(stream: import('stream').Stream, options?: Partial): Promise; /** * load from an array buffer * @param buffer */ - load(buffer: Buffer): Promise; + load(buffer: Buffer, options?: Partial): Promise; /** * write to a buffer @@ -1487,7 +1505,7 @@ export interface Xlsx { write(stream: import('stream').Stream, options?: Partial): Promise; } -// https://c2fo.io/fast-csv/docs/parsing/options +// https://c2fo.github.io/fast-csv/docs/parsing/options type HeaderArray = (string | undefined | null)[]; type HeaderTransformFunction = (headers: HeaderArray) => HeaderArray; @@ -1528,7 +1546,7 @@ interface RowTransformFunction { (row: Rows): Rows; } -// https://c2fo.io/fast-csv/docs/formatting/options/ +// https://c2fo.github.io/fast-csv/docs/formatting/options/ export interface FastCsvFormatterOptionsArgs { objectMode: boolean; delimiter: string; @@ -1743,7 +1761,7 @@ export class Workbook { /** * fetch sheet by name or id */ - getWorksheet(indexOrName: number | string): Worksheet; + getWorksheet(indexOrName?: number | string): Worksheet | undefined; /** * Iterate over all sheets. @@ -2014,4 +2032,4 @@ export namespace stream { getColumn(c: number): Column; } } -} +} \ No newline at end of file diff --git a/lib/doc/anchor.js b/lib/doc/anchor.js index 9aa5f70e2..dd3509a74 100644 --- a/lib/doc/anchor.js +++ b/lib/doc/anchor.js @@ -4,6 +4,8 @@ const colCache = require('../utils/col-cache'); class Anchor { constructor(worksheet, address, offset = 0) { + this.worksheet = worksheet; + if (!address) { this.nativeCol = 0; this.nativeColOff = 0; @@ -29,8 +31,6 @@ class Anchor { this.nativeRow = 0; this.nativeRowOff = 0; } - - this.worksheet = worksheet; } static asInstance(model) { diff --git a/lib/doc/column.js b/lib/doc/column.js index f7636172c..c7fc282fe 100644 --- a/lib/doc/column.js +++ b/lib/doc/column.js @@ -297,6 +297,13 @@ class Column { const columns = []; let count = 1; let index = 0; + /** + * sort cols by min + * If it is not sorted, the subsequent column configuration will be overwritten + * */ + cols = cols.sort(function(pre, next) { + return pre.min - next.min; + }); while (index < cols.length) { const col = cols[index++]; while (count < col.min) { diff --git a/lib/doc/workbook.js b/lib/doc/workbook.js index a18383317..8e7f46ecd 100644 --- a/lib/doc/workbook.js +++ b/lib/doc/workbook.js @@ -53,30 +53,6 @@ class Workbook { addWorksheet(name, options) { const id = this.nextId; - if (name && name.length > 31) { - // eslint-disable-next-line no-console - console.warn(`Worksheet name ${name} exceeds 31 chars. This will be truncated`); - } - - // Illegal character in worksheet name: asterisk (*), question mark (?), - // colon (:), forward slash (/ \), or bracket ([]) - if (/[*?:/\\[\]]/.test(name)) { - throw new Error( - `Worksheet name ${name} cannot include any of the following characters: * ? : \\ / [ ]` - ); - } - - if (/(^')|('$)/.test(name)) { - throw new Error( - `The first or last character of worksheet name cannot be a single quotation mark: ${name}` - ); - } - - name = (name || `sheet${id}`).substring(0, 31); - if (this._worksheets.find(ws => ws && ws.name.toLowerCase() === name.toLowerCase())) { - throw new Error(`Worksheet name already exists: ${name}`); - } - // if options is a color, call it tabColor (and signal deprecated message) if (options) { if (typeof options === 'string') { @@ -102,10 +78,7 @@ class Workbook { } } - const lastOrderNo = this._worksheets.reduce( - (acc, ws) => ((ws && ws.orderNo) > acc ? ws.orderNo : acc), - 0 - ); + const lastOrderNo = this._worksheets.reduce((acc, ws) => ((ws && ws.orderNo) > acc ? ws.orderNo : acc), 0); const worksheetOptions = Object.assign({}, options, { id, name, @@ -235,7 +208,6 @@ class Workbook { state, workbook: this, })); - worksheet.model = worksheetModel; }); diff --git a/lib/doc/worksheet.js b/lib/doc/worksheet.js index ac2f4613b..13e414140 100644 --- a/lib/doc/worksheet.js +++ b/lib/doc/worksheet.js @@ -9,6 +9,7 @@ const Image = require('./image'); const Table = require('./table'); const DataValidations = require('./data-validations'); const Encryptor = require('../utils/encryptor'); +const {copyStyle} = require('../utils/copy-style'); // Worksheet requirements // Operate as sheet inside workbook or standalone @@ -19,13 +20,14 @@ const Encryptor = require('../utils/encryptor'); class Worksheet { constructor(options) { options = options || {}; + this._workbook = options.workbook; // in a workbook, each sheet will have a number this.id = options.id; this.orderNo = options.orderNo; // and a name - this.name = options.name || `Sheet${this.id}`; + this.name = options.name; // add a state this.state = options.state || 'visible'; @@ -46,8 +48,6 @@ class Worksheet { // record of all row and column pageBreaks this.rowBreaks = []; - this._workbook = options.workbook; - // for tabColor, default row height, outline levels, etc this.properties = Object.assign( {}, @@ -127,6 +127,52 @@ class Worksheet { this.conditionalFormattings = []; } + get name() { + return this._name; + } + + set name(name) { + if (name === undefined) { + name = `sheet${this.id}`; + } + + if (this._name === name) return; + + if (typeof name !== 'string') { + throw new Error('The name has to be a string.'); + } + + if (name === '') { + throw new Error('The name can\'t be empty.'); + } + + if (name === 'History') { + throw new Error('The name "History" is protected. Please use a different name.'); + } + + // Illegal character in worksheet name: asterisk (*), question mark (?), + // colon (:), forward slash (/ \), or bracket ([]) + if (/[*?:/\\[\]]/.test(name)) { + throw new Error(`Worksheet name ${name} cannot include any of the following characters: * ? : \\ / [ ]`); + } + + if (/(^')|('$)/.test(name)) { + throw new Error(`The first or last character of worksheet name cannot be a single quotation mark: ${name}`); + } + + if (name && name.length > 31) { + // eslint-disable-next-line no-console + console.warn(`Worksheet name ${name} exceeds 31 chars. This will be truncated`); + name = name.substring(0, 31); + } + + if (this._workbook._worksheets.find(ws => ws && ws.name.toLowerCase() === name.toLowerCase())) { + throw new Error(`Worksheet name already exists: ${name}`); + } + + this._name = name; + } + get workbook() { return this._workbook; } @@ -406,10 +452,10 @@ class Worksheet { _copyStyle(src, dest, styleEmpty = false) { const rSrc = this.getRow(src); const rDst = this.getRow(dest); - rDst.style = Object.freeze({...rSrc.style}); + rDst.style = copyStyle(rSrc.style); // eslint-disable-next-line no-loop-func rSrc.eachCell({includeEmpty: styleEmpty}, (cell, colNumber) => { - rDst.getCell(colNumber).style = Object.freeze({...cell.style}); + rDst.getCell(colNumber).style = copyStyle(cell.style); }); rDst.height = rSrc.height; } @@ -444,6 +490,9 @@ class Worksheet { let rSrc; if (nExpand < 0) { // remove rows + if (start === nEnd) { + this._rows[nEnd - 1] = undefined; + } for (i = nKeep; i <= nEnd; i++) { rSrc = this._rows[i - 1]; if (rSrc) { diff --git a/lib/exceljs.browser.js b/lib/exceljs.browser.js index 80f76c082..d7558203a 100644 --- a/lib/exceljs.browser.js +++ b/lib/exceljs.browser.js @@ -1,5 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies,node/no-unpublished-require */ require('core-js/modules/es.promise'); +require('core-js/modules/es.promise.finally'); require('core-js/modules/es.object.assign'); require('core-js/modules/es.object.keys'); require('core-js/modules/es.object.values'); diff --git a/lib/stream/xlsx/workbook-reader.js b/lib/stream/xlsx/workbook-reader.js index 0a2a3c65c..0d3afd62a 100644 --- a/lib/stream/xlsx/workbook-reader.js +++ b/lib/stream/xlsx/workbook-reader.js @@ -119,6 +119,7 @@ class WorkbookReader extends EventEmitter { waitingWorkSheets.push({sheetNo, path, tempFileCleanupCallback}); const tempStream = fs.createWriteStream(path); + tempStream.on('error', reject); entry.pipe(tempStream); return tempStream.on('finish', () => { return resolve(); @@ -298,11 +299,8 @@ class WorkbookReader extends EventEmitter { options: this.options, }); - const matchingRel = (this.workbookRels || []).find( - rel => rel.Target === `worksheets/sheet${sheetNo}.xml` - ); - const matchingSheet = - matchingRel && (this.model.sheets || []).find(sheet => sheet.rId === matchingRel.Id); + const matchingRel = (this.workbookRels || []).find(rel => rel.Target === `worksheets/sheet${sheetNo}.xml`); + const matchingSheet = matchingRel && (this.model.sheets || []).find(sheet => sheet.rId === matchingRel.Id); if (matchingSheet) { worksheetReader.id = matchingSheet.id; worksheetReader.name = matchingSheet.name; diff --git a/lib/stream/xlsx/worksheet-reader.js b/lib/stream/xlsx/worksheet-reader.js index ade13f426..8cecd5c17 100644 --- a/lib/stream/xlsx/worksheet-reader.js +++ b/lib/stream/xlsx/worksheet-reader.js @@ -214,6 +214,12 @@ class WorksheetReader extends EventEmitter { current = c.v = {text: ''}; } break; + case 'is': + case 't': + if (c) { + current = c.v = {text: ''}; + } + break; case 'mergeCell': break; default: @@ -307,6 +313,7 @@ class WorksheetReader extends EventEmitter { break; } + case 'inlineStr': case 'str': cell.value = utils.xmlDecode(c.v.text); break; diff --git a/lib/utils/copy-style.js b/lib/utils/copy-style.js new file mode 100644 index 000000000..532bf5701 --- /dev/null +++ b/lib/utils/copy-style.js @@ -0,0 +1,43 @@ +const oneDepthCopy = (obj, nestKeys) => ({ + ...obj, + ...nestKeys.reduce((memo, key) => { + if (obj[key]) memo[key] = {...obj[key]}; + return memo; + }, {}), +}); + +const setIfExists = (src, dst, key, nestKeys = []) => { + if (src[key]) dst[key] = oneDepthCopy(src[key], nestKeys); +}; + +const isEmptyObj = obj => Object.keys(obj).length === 0; + +const copyStyle = style => { + if (!style) return style; + if (isEmptyObj(style)) return {}; + + const copied = {...style}; + + setIfExists(style, copied, 'font', ['color']); + setIfExists(style, copied, 'alignment'); + setIfExists(style, copied, 'protection'); + if (style.border) { + setIfExists(style, copied, 'border'); + setIfExists(style.border, copied.border, 'top', ['color']); + setIfExists(style.border, copied.border, 'left', ['color']); + setIfExists(style.border, copied.border, 'bottom', ['color']); + setIfExists(style.border, copied.border, 'right', ['color']); + setIfExists(style.border, copied.border, 'diagonal', ['color']); + } + + if (style.fill) { + setIfExists(style, copied, 'fill', ['fgColor', 'bgColor', 'center']); + if (style.fill.stops) { + copied.fill.stops = style.fill.stops.map(s => oneDepthCopy(s, ['color'])); + } + } + + return copied; +}; + +exports.copyStyle = copyStyle; diff --git a/lib/utils/under-dash.js b/lib/utils/under-dash.js index 65c931d22..b6f8a1312 100644 --- a/lib/utils/under-dash.js +++ b/lib/utils/under-dash.js @@ -55,6 +55,7 @@ const _ = { const bType = typeof b; const aArray = Array.isArray(a); const bArray = Array.isArray(b); + let keys; if (aType !== bType) { return false; @@ -73,6 +74,24 @@ const _ = { } return false; } + + if (a === null || b === null) { + return a === b; + } + + // Compare object keys and values + keys = Object.keys(a); + + if (Object.keys(b).length !== keys.length) { + return false; + } + + for (const key of keys) { + if (!b.hasOwnProperty(key)) { + return false; + } + } + return _.every(a, (aValue, key) => { const bValue = b[key]; return _.isEqual(aValue, bValue); diff --git a/lib/utils/utils.js b/lib/utils/utils.js index a85af676d..84cd212c2 100644 --- a/lib/utils/utils.js +++ b/lib/utils/utils.js @@ -163,6 +163,10 @@ const utils = { toIsoDateString(dt) { return dt.toIsoString().subsstr(0, 10); }, + + parseBoolean(value) { + return value === true || value === 'true' || value === 1 || value === '1'; + }, }; module.exports = utils; diff --git a/lib/utils/xml-stream.js b/lib/utils/xml-stream.js index 7d606bb83..46a59695e 100644 --- a/lib/utils/xml-stream.js +++ b/lib/utils/xml-stream.js @@ -7,24 +7,19 @@ const OPEN_ANGLE = '<'; const CLOSE_ANGLE = '>'; const OPEN_ANGLE_SLASH = ''; -const EQUALS_QUOTE = '="'; -const QUOTE = '"'; -const SPACE = ' '; function pushAttribute(xml, name, value) { - xml.push(SPACE); - xml.push(name); - xml.push(EQUALS_QUOTE); - xml.push(utils.xmlEncode(value.toString())); - xml.push(QUOTE); + xml.push(` ${name}="${utils.xmlEncode(value.toString())}"`); } function pushAttributes(xml, attributes) { if (attributes) { + const tmp = []; _.each(attributes, (value, name) => { if (value !== undefined) { - pushAttribute(xml, name, value); + pushAttribute(tmp, name, value); } }); + xml.push(tmp.join("")); } } diff --git a/lib/xlsx/xform/sheet/col-xform.js b/lib/xlsx/xform/sheet/col-xform.js index 93003cbdf..0d47b42c5 100644 --- a/lib/xlsx/xform/sheet/col-xform.js +++ b/lib/xlsx/xform/sheet/col-xform.js @@ -1,3 +1,4 @@ +const utils = require('../../../utils/utils'); const BaseXform = require('../base-xform'); class ColXform extends BaseXform { @@ -51,21 +52,16 @@ class ColXform extends BaseXform { if (node.attributes.style) { model.styleId = parseInt(node.attributes.style, 10); } - if ( - node.attributes.hidden === true || - node.attributes.hidden === 'true' || - node.attributes.hidden === 1 || - node.attributes.hidden === '1' - ) { + if (utils.parseBoolean(node.attributes.hidden)) { model.hidden = true; } - if (node.attributes.bestFit) { + if (utils.parseBoolean(node.attributes.bestFit)) { model.bestFit = true; } if (node.attributes.outlineLevel) { model.outlineLevel = parseInt(node.attributes.outlineLevel, 10); } - if (node.attributes.collapsed) { + if (utils.parseBoolean(node.attributes.collapsed)) { model.collapsed = true; } return true; diff --git a/lib/xlsx/xform/sheet/data-validations-xform.js b/lib/xlsx/xform/sheet/data-validations-xform.js index 5f20f33e3..58d6f098a 100644 --- a/lib/xlsx/xform/sheet/data-validations-xform.js +++ b/lib/xlsx/xform/sheet/data-validations-xform.js @@ -12,19 +12,11 @@ function assign(definedName, attributes, name, defaultValue) { definedName[name] = defaultValue; } } -function parseBool(value) { - switch (value) { - case '1': - case 'true': - return true; - default: - return false; - } -} + function assignBool(definedName, attributes, name, defaultValue) { const value = attributes[name]; if (value !== undefined) { - definedName[name] = parseBool(value); + definedName[name] = utils.parseBoolean(value); } else if (defaultValue !== undefined) { definedName[name] = defaultValue; } diff --git a/lib/xlsx/xform/sheet/hyperlink-xform.js b/lib/xlsx/xform/sheet/hyperlink-xform.js index 3fb560a6d..88f4ee2ec 100644 --- a/lib/xlsx/xform/sheet/hyperlink-xform.js +++ b/lib/xlsx/xform/sheet/hyperlink-xform.js @@ -6,11 +6,20 @@ class HyperlinkXform extends BaseXform { } render(xmlStream, model) { - xmlStream.leafNode('hyperlink', { - ref: model.address, - 'r:id': model.rId, - tooltip: model.tooltip, - }); + if (this.isInternalLink(model)) { + xmlStream.leafNode('hyperlink', { + ref: model.address, + 'r:id': model.rId, + tooltip: model.tooltip, + location: model.target, + }); + } else { + xmlStream.leafNode('hyperlink', { + ref: model.address, + 'r:id': model.rId, + tooltip: model.tooltip, + }); + } } parseOpen(node) { @@ -20,6 +29,11 @@ class HyperlinkXform extends BaseXform { rId: node.attributes['r:id'], tooltip: node.attributes.tooltip, }; + + // This is an internal link + if (node.attributes.location) { + this.model.target = node.attributes.location; + } return true; } return false; @@ -30,6 +44,11 @@ class HyperlinkXform extends BaseXform { parseClose() { return false; } + + isInternalLink(model) { + // @example: Sheet2!D3, return true + return model.target && /^[^!]+![a-zA-Z]+[\d]+$/.test(model.target); + } } module.exports = HyperlinkXform; diff --git a/lib/xlsx/xform/sheet/row-xform.js b/lib/xlsx/xform/sheet/row-xform.js index 4ccb73fee..890a0fc8a 100644 --- a/lib/xlsx/xform/sheet/row-xform.js +++ b/lib/xlsx/xform/sheet/row-xform.js @@ -1,4 +1,5 @@ const BaseXform = require('../base-xform'); +const utils = require('../../../utils/utils'); const CellXform = require('./cell-xform'); @@ -79,15 +80,10 @@ class RowXform extends BaseXform { if (node.attributes.s) { model.styleId = parseInt(node.attributes.s, 10); } - if ( - node.attributes.hidden === true || - node.attributes.hidden === 'true' || - node.attributes.hidden === 1 || - node.attributes.hidden === '1' - ) { + if (utils.parseBoolean(node.attributes.hidden)) { model.hidden = true; } - if (node.attributes.bestFit) { + if (utils.parseBoolean(node.attributes.bestFit)) { model.bestFit = true; } if (node.attributes.ht) { @@ -96,7 +92,7 @@ class RowXform extends BaseXform { if (node.attributes.outlineLevel) { model.outlineLevel = parseInt(node.attributes.outlineLevel, 10); } - if (node.attributes.collapsed) { + if (utils.parseBoolean(node.attributes.collapsed)) { model.collapsed = true; } return true; diff --git a/lib/xlsx/xform/sheet/worksheet-xform.js b/lib/xlsx/xform/sheet/worksheet-xform.js index 4b36e37ba..b38930042 100644 --- a/lib/xlsx/xform/sheet/worksheet-xform.js +++ b/lib/xlsx/xform/sheet/worksheet-xform.js @@ -92,7 +92,10 @@ class WorkSheetXform extends BaseXform { constructor(options) { super(); - const {maxRows, maxCols} = options || {}; + const {maxRows, maxCols, ignoreNodes} = options || {}; + + this.ignoreNodes = ignoreNodes || []; + this.map = { sheetPr: new SheetPropertiesXform(), dimension: new DimensionXform(), @@ -221,9 +224,7 @@ class WorkSheetXform extends BaseXform { }); } let rIdImage = - this.preImageId === medium.imageId - ? drawingRelsHash[medium.imageId] - : drawingRelsHash[drawing.rels.length]; + this.preImageId === medium.imageId ? drawingRelsHash[medium.imageId] : drawingRelsHash[drawing.rels.length]; if (!rIdImage) { rIdImage = nextRid(drawing.rels); drawingRelsHash[drawing.rels.length] = rIdImage; @@ -368,8 +369,8 @@ class WorkSheetXform extends BaseXform { return true; } - this.parser = this.map[node.name]; - if (this.parser) { + if (this.map[node.name] && !this.ignoreNodes.includes(node.name)) { + this.parser = this.map[node.name]; this.parser.parseOpen(node); } return true; @@ -405,11 +406,7 @@ class WorkSheetXform extends BaseXform { false, margins: this.map.pageMargins.model, }; - const pageSetup = Object.assign( - sheetProperties, - this.map.pageSetup.model, - this.map.printOptions.model - ); + const pageSetup = Object.assign(sheetProperties, this.map.pageSetup.model, this.map.printOptions.model); const conditionalFormattings = mergeConditionalFormattings( this.map.conditionalFormatting.model, this.map.extLst.model && this.map.extLst.model['x14:conditionalFormattings'] diff --git a/lib/xlsx/xform/style/alignment-xform.js b/lib/xlsx/xform/style/alignment-xform.js index 6cd7eb11a..75391be98 100644 --- a/lib/xlsx/xform/style/alignment-xform.js +++ b/lib/xlsx/xform/style/alignment-xform.js @@ -145,8 +145,8 @@ class AlignmentXform extends BaseXform { 'vertical', node.attributes.vertical === 'center' ? 'middle' : node.attributes.vertical ); - add(node.attributes.wrapText, 'wrapText', !!node.attributes.wrapText); - add(node.attributes.shrinkToFit, 'shrinkToFit', !!node.attributes.shrinkToFit); + add(node.attributes.wrapText, 'wrapText', utils.parseBoolean(node.attributes.wrapText)); + add(node.attributes.shrinkToFit, 'shrinkToFit', utils.parseBoolean(node.attributes.shrinkToFit)); add(node.attributes.indent, 'indent', parseInt(node.attributes.indent, 10)); add( node.attributes.textRotation, diff --git a/lib/xlsx/xform/style/border-xform.js b/lib/xlsx/xform/style/border-xform.js index 3066c8188..577058864 100644 --- a/lib/xlsx/xform/style/border-xform.js +++ b/lib/xlsx/xform/style/border-xform.js @@ -1,5 +1,6 @@ /* eslint-disable max-classes-per-file */ const BaseXform = require('../base-xform'); +const utils = require('../../../utils/utils'); const ColorXform = require('./color-xform'); @@ -88,6 +89,7 @@ class EdgeXform extends BaseXform { EdgeXform.validStyleValues = [ 'thin', + 'dashed', 'dotted', 'dashDot', 'hair', @@ -156,8 +158,8 @@ class BorderXform extends BaseXform { switch (node.name) { case 'border': this.reset(); - this.diagonalUp = !!node.attributes.diagonalUp; - this.diagonalDown = !!node.attributes.diagonalDown; + this.diagonalUp = utils.parseBoolean(node.attributes.diagonalUp); + this.diagonalDown = utils.parseBoolean(node.attributes.diagonalDown); return true; default: this.parser = this.map[node.name]; diff --git a/lib/xlsx/xform/style/dxf-xform.js b/lib/xlsx/xform/style/dxf-xform.js index ea22d365f..5cb2d78b9 100644 --- a/lib/xlsx/xform/style/dxf-xform.js +++ b/lib/xlsx/xform/style/dxf-xform.js @@ -39,8 +39,9 @@ class DxfXform extends BaseXform { if (model.font) { this.map.font.render(xmlStream, model.font); } - if (model.numFmt) { - this.map.numFmt.render(xmlStream, model.numFmt); + if (model.numFmt && model.numFmtId) { + const numFmtModel = {id: model.numFmtId, formatCode: model.numFmt}; + this.map.numFmt.render(xmlStream, numFmtModel); } if (model.fill) { this.map.fill.render(xmlStream, model.fill); diff --git a/lib/xlsx/xform/style/styles-xform.js b/lib/xlsx/xform/style/styles-xform.js index e1071c424..bf749e7bc 100644 --- a/lib/xlsx/xform/style/styles-xform.js +++ b/lib/xlsx/xform/style/styles-xform.js @@ -135,9 +135,7 @@ class StylesXform extends BaseXform { }); xmlStream.closeNode(); - this.map.cellStyleXfs.render(xmlStream, [ - {numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0}, - ]); + this.map.cellStyleXfs.render(xmlStream, [{numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0}]); xmlStream.openNode('cellXfs', {count: model.styles.length}); model.styles.forEach(styleXml => { @@ -150,9 +148,7 @@ class StylesXform extends BaseXform { this.map.fonts.render(xmlStream, model.fonts); this.map.fills.render(xmlStream, model.fills); this.map.borders.render(xmlStream, model.borders); - this.map.cellStyleXfs.render(xmlStream, [ - {numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0}, - ]); + this.map.cellStyleXfs.render(xmlStream, [{numFmtId: 0, fontId: 0, fillId: 0, borderId: 0, xfId: 0}]); this.map.cellXfs.render(xmlStream, model.styles); } @@ -313,8 +309,7 @@ class StylesXform extends BaseXform { // ------------------------------------------------------- // number format if (style.numFmtId) { - const numFmt = - this.index.numFmt[style.numFmtId] || NumFmtXform.getDefaultFmtCode(style.numFmtId); + const numFmt = this.index.numFmt[style.numFmtId] || NumFmtXform.getDefaultFmtCode(style.numFmtId); if (numFmt) { model.numFmt = numFmt; } @@ -349,6 +344,11 @@ class StylesXform extends BaseXform { } addDxfStyle(style) { + if (style.numFmt) { + // register numFmtId to use it during dxf-xform rendering + style.numFmtId = this._addNumFmtStr(style.numFmt); + } + this.model.dxfs.push(style); return this.model.dxfs.length - 1; } diff --git a/lib/xlsx/xform/table/custom-filter-xform.js b/lib/xlsx/xform/table/custom-filter-xform.js new file mode 100644 index 000000000..d0e9d272f --- /dev/null +++ b/lib/xlsx/xform/table/custom-filter-xform.js @@ -0,0 +1,33 @@ +const BaseXform = require('../base-xform'); + +class CustomFilterXform extends BaseXform { + get tag() { + return 'customFilter'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, { + val: model.val, + operator: model.operator, + }); + } + + parseOpen(node) { + if (node.name === this.tag) { + this.model = { + val: node.attributes.val, + operator: node.attributes.operator, + }; + return true; + } + return false; + } + + parseText() {} + + parseClose() { + return false; + } +} + +module.exports = CustomFilterXform; diff --git a/lib/xlsx/xform/table/filter-column-xform.js b/lib/xlsx/xform/table/filter-column-xform.js index 20fd24628..c900996d4 100644 --- a/lib/xlsx/xform/table/filter-column-xform.js +++ b/lib/xlsx/xform/table/filter-column-xform.js @@ -1,6 +1,29 @@ const BaseXform = require('../base-xform'); +const ListXform = require('../list-xform'); + +const CustomFilterXform = require('./custom-filter-xform'); +const FilterXform = require('./filter-xform'); class FilterColumnXform extends BaseXform { + constructor() { + super(); + + this.map = { + customFilters: new ListXform({ + tag: 'customFilters', + count: false, + empty: true, + childXform: new CustomFilterXform(), + }), + filters: new ListXform({ + tag: 'filters', + count: false, + empty: true, + childXform: new FilterXform(), + }), + }; + } + get tag() { return 'filterColumn'; } @@ -10,6 +33,17 @@ class FilterColumnXform extends BaseXform { } render(xmlStream, model) { + if (model.customFilters) { + xmlStream.openNode(this.tag, { + colId: model.colId, + hiddenButton: model.filterButton ? '0' : '1', + }); + + this.map.customFilters.render(xmlStream, model.customFilters); + + xmlStream.closeNode(); + return true; + } xmlStream.leafNode(this.tag, { colId: model.colId, hiddenButton: model.filterButton ? '0' : '1', @@ -18,20 +52,44 @@ class FilterColumnXform extends BaseXform { } parseOpen(node) { - if (node.name === this.tag) { - const {attributes} = node; - this.model = { - filterButton: attributes.hiddenButton === '0', - }; + if (this.parser) { + this.parser.parseOpen(node); return true; } - return false; + const {attributes} = node; + switch (node.name) { + case this.tag: + this.model = { + filterButton: attributes.hiddenButton === '0', + }; + return true; + default: + this.parser = this.map[node.name]; + if (this.parser) { + this.parseOpen(node); + return true; + } + throw new Error(`Unexpected xml node in parseOpen: ${JSON.stringify(node)}`); + } } parseText() {} - parseClose() { - return false; + parseClose(name) { + if (this.parser) { + if (!this.parser.parseClose(name)) { + this.parser = undefined; + } + return true; + } + switch (name) { + case this.tag: + this.model.customFilters = this.map.customFilters.model; + return false; + default: + // could be some unrecognised tags + return true; + } } } diff --git a/lib/xlsx/xform/table/filter-xform.js b/lib/xlsx/xform/table/filter-xform.js new file mode 100644 index 000000000..91a620a02 --- /dev/null +++ b/lib/xlsx/xform/table/filter-xform.js @@ -0,0 +1,31 @@ +const BaseXform = require('../base-xform'); + +class FilterXform extends BaseXform { + get tag() { + return 'filter'; + } + + render(xmlStream, model) { + xmlStream.leafNode(this.tag, { + val: model.val, + }); + } + + parseOpen(node) { + if (node.name === this.tag) { + this.model = { + val: node.attributes.val, + }; + return true; + } + return false; + } + + parseText() {} + + parseClose() { + return false; + } +} + +module.exports = FilterXform; diff --git a/lib/xlsx/xlsx.js b/lib/xlsx/xlsx.js index 4f6bc02c5..ff2d9a117 100644 --- a/lib/xlsx/xlsx.js +++ b/lib/xlsx/xlsx.js @@ -22,7 +22,7 @@ const TableXform = require('./xform/table/table-xform'); const CommentsXform = require('./xform/comment/comments-xform'); const VmlNotesXform = require('./xform/comment/vml-notes-xform'); -const theme1Xml = require('./xml/theme1.js'); +const theme1Xml = require('./xml/theme1'); function fsReadFileAsync(filename, options) { return new Promise((resolve, reject) => { @@ -285,9 +285,11 @@ class XLSX { entryName = entryName.substr(1); } let stream; - if (entryName.match(/xl\/media\//) || + if ( + entryName.match(/xl\/media\//) || // themes are not parsed as stream - entryName.match(/xl\/theme\/([a-zA-Z0-9]+)[.]xml/)) { + entryName.match(/xl\/theme\/([a-zA-Z0-9]+)[.]xml/) + ) { stream = new PassThrough(); stream.write(await entry.async('nodebuffer')); } else { @@ -597,8 +599,7 @@ class XLSX { model.created = model.created || new Date(); model.modified = model.modified || new Date(); - model.useSharedStrings = - options.useSharedStrings !== undefined ? options.useSharedStrings : true; + model.useSharedStrings = options.useSharedStrings !== undefined ? options.useSharedStrings : true; model.useStyles = options.useStyles !== undefined ? options.useStyles : true; // Manage the shared strings @@ -675,6 +676,8 @@ class XLSX { this.write(stream, options).then(() => { stream.end(); + }).catch(err=>{ + reject(err); }); }); } diff --git a/package.json b/package.json index 807ad66b8..7a70a5e13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "exceljs", - "version": "4.3.0", + "version": "4.4.0", "description": "Excel Workbook Manager - Read and Write xlsx and csv Files.", "private": false, "license": "MIT", @@ -99,7 +99,7 @@ "archiver": "^5.0.0", "dayjs": "^1.8.34", "fast-csv": "^4.3.1", - "jszip": "^3.5.0", + "jszip": "^3.10.1", "readable-stream": "^3.6.0", "saxes": "^5.0.1", "tmp": "^0.2.0", diff --git a/spec/integration/data/.gitignore b/spec/integration/data/.gitignore new file mode 100644 index 000000000..9c689962a --- /dev/null +++ b/spec/integration/data/.gitignore @@ -0,0 +1 @@ +!*.xlsx diff --git a/spec/integration/data/test-issue-1575.xlsx b/spec/integration/data/test-issue-1575.xlsx new file mode 100644 index 000000000..8a8caa24a Binary files /dev/null and b/spec/integration/data/test-issue-1575.xlsx differ diff --git a/spec/integration/data/test-issue-1669.xlsx b/spec/integration/data/test-issue-1669.xlsx new file mode 100644 index 000000000..cc9dab8f1 Binary files /dev/null and b/spec/integration/data/test-issue-1669.xlsx differ diff --git a/spec/integration/data/test-issue-1842.xlsx b/spec/integration/data/test-issue-1842.xlsx new file mode 100644 index 000000000..013639766 Binary files /dev/null and b/spec/integration/data/test-issue-1842.xlsx differ diff --git a/spec/integration/issues/issue-1669-optional-custom-autofilter-on-table.spec.js b/spec/integration/issues/issue-1669-optional-custom-autofilter-on-table.spec.js new file mode 100644 index 000000000..3a030c090 --- /dev/null +++ b/spec/integration/issues/issue-1669-optional-custom-autofilter-on-table.spec.js @@ -0,0 +1,8 @@ +const ExcelJS = verquire('exceljs'); + +describe('github issues', () => { + it('issue 1669 - optional autofilter and custom autofilter on tables', () => { + const wb = new ExcelJS.Workbook(); + return wb.xlsx.readFile('./spec/integration/data/test-issue-1669.xlsx'); + }).timeout(6000); +}); diff --git a/spec/integration/issues/issue-1842-dataValidations-memory-overload.spec.js b/spec/integration/issues/issue-1842-dataValidations-memory-overload.spec.js new file mode 100644 index 000000000..2070b2f26 --- /dev/null +++ b/spec/integration/issues/issue-1842-dataValidations-memory-overload.spec.js @@ -0,0 +1,32 @@ +const {join} = require('path'); +const {readFileSync} = require('fs'); + +const ExcelJS = verquire('exceljs'); + +const fileName = './spec/integration/data/test-issue-1842.xlsx'; + +describe('github issues', () => { + describe('issue 1842 - Memory overload when unnecessary dataValidations apply', () => { + it('when using readFile', async () => { + const wb = new ExcelJS.Workbook(); + await wb.xlsx.readFile(fileName, { + ignoreNodes: ['dataValidations'], + }); + + // arriving here is success + expect(true).to.equal(true); + }); + + it('when loading an in memory buffer', async () => { + const filePath = join(process.cwd(), fileName); + const buffer = readFileSync(filePath); + const wb = new ExcelJS.Workbook(); + await wb.xlsx.load(buffer, { + ignoreNodes: ['dataValidations'], + }); + + // arriving here is success + expect(true).to.equal(true); + }); + }); +}); diff --git a/spec/integration/issues/issue-2125-spliceRows-last-row.spec.js b/spec/integration/issues/issue-2125-spliceRows-last-row.spec.js new file mode 100644 index 000000000..6b8737de7 --- /dev/null +++ b/spec/integration/issues/issue-2125-spliceRows-last-row.spec.js @@ -0,0 +1,13 @@ +const ExcelJS = verquire('exceljs'); + +describe('github issues', () => { + it('issue 2125 - spliceRows remove last row', () => { + const wb = new ExcelJS.Workbook(); + const ws = wb.addWorksheet(); + ws.addRows([['1st'], ['2nd'], ['3rd']]); + + ws.spliceRows(ws.rowCount, 1); + + expect(ws.getRow(ws.rowCount).getCell(1).value).to.equal('2nd'); + }); +}); diff --git a/spec/integration/pr/test-pr-1576.spec.js b/spec/integration/pr/test-pr-1576.spec.js new file mode 100644 index 000000000..bd575be4c --- /dev/null +++ b/spec/integration/pr/test-pr-1576.spec.js @@ -0,0 +1,23 @@ +const ExcelJS = verquire('exceljs'); + +describe('github issues', () => { + describe('pull request 1576 - inlineStr cell type support', () => { + it('Reading test-issue-1575.xlsx', () => { + const wb = new ExcelJS.Workbook(); + return wb.xlsx + .readFile('./spec/integration/data/test-issue-1575.xlsx') + .then(() => { + const ws = wb.getWorksheet('Sheet1'); + expect(ws.getCell('A1').value).to.equal('A'); + expect(ws.getCell('B1').value).to.equal('B'); + expect(ws.getCell('C1').value).to.equal('C'); + expect(ws.getCell('A2').value).to.equal('1.0'); + expect(ws.getCell('B2').value).to.equal('2.0'); + expect(ws.getCell('C2').value).to.equal('3.0'); + expect(ws.getCell('A3').value).to.equal('4.0'); + expect(ws.getCell('B3').value).to.equal('5.0'); + expect(ws.getCell('C3').value).to.equal('6.0'); + }); + }); + }); +}); diff --git a/spec/integration/pr/test-pr-2244.spec.js b/spec/integration/pr/test-pr-2244.spec.js new file mode 100644 index 000000000..667446c4c --- /dev/null +++ b/spec/integration/pr/test-pr-2244.spec.js @@ -0,0 +1,23 @@ +const ExcelJS = verquire('exceljs'); + +describe('pull request 2244', () => { + it('pull request 2244- Fix xlsx.writeFile() not catching error when error occurs', async () => { + async function test() { + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('sheet'); + const imageId1 = workbook.addImage({ + filename: 'path/to/image.jpg', // Non-existent file + extension: 'jpeg', + }); + worksheet.addImage(imageId1, 'B2:D6'); + await workbook.xlsx.writeFile('test.xlsx'); + } + let error; + try { + await test(); + } catch (err) { + error = err; + } + expect(error).to.be.an('error'); + }); +}); diff --git a/spec/integration/workbook-xlsx-writer/workbook-xlsx-writer.spec.js b/spec/integration/workbook-xlsx-writer/workbook-xlsx-writer.spec.js index 6feb93aca..b74d8f496 100644 --- a/spec/integration/workbook-xlsx-writer/workbook-xlsx-writer.spec.js +++ b/spec/integration/workbook-xlsx-writer/workbook-xlsx-writer.spec.js @@ -588,5 +588,30 @@ describe('WorkbookWriter', () => { testUtils.checkTestBook(wb2, 'xlsx', ['conditionalFormatting']); }); }); + + it('with conditional formatting that contains numFmt (#1814)', async () => { + const sheet = 'conditionalFormatting'; + const options = {filename: TEST_XLSX_FILE_NAME, useStyles: true}; + + // generate file with conditional formatting that contains styles with numFmt + const wb1 = new ExcelJS.stream.xlsx.WorkbookWriter(options); + const ws1 = wb1.addWorksheet(sheet); + const cf1 = testUtils.conditionalFormatting.abbreviation; + ws1.addConditionalFormatting(cf1); + await wb1.commit(); + + // read generated file and extract saved conditional formatting rule + const wb2 = new ExcelJS.Workbook(); + await wb2.xlsx.readFile(TEST_XLSX_FILE_NAME); + const ws2 = wb2.getWorksheet(sheet); + const [cf2] = ws2.conditionalFormattings; + + // verify that rules from generated file contain styles with valid numFmt + cf2.rules.forEach(rule => { + expect(rule.style.numFmt).to.exist(); + expect(rule.style.numFmt.id).to.be.a('number'); + expect(rule.style.numFmt.formatCode).to.be.a('string'); + }); + }); }); }); diff --git a/spec/integration/worksheet.spec.js b/spec/integration/worksheet.spec.js index 705746bed..a70dfcef8 100644 --- a/spec/integration/worksheet.spec.js +++ b/spec/integration/worksheet.spec.js @@ -598,6 +598,38 @@ describe('Worksheet', () => { } }); + it('should style of the inserted row with inherited style be mutable', () => { + const wb = new ExcelJS.Workbook(); + const ws = wb.addWorksheet('blort'); + + const dateValue1 = new Date(1970, 1, 1); + const dateValue2 = new Date(1965, 1, 7); + + ws.addRow([1, 'John Doe', dateValue1]); + ws.getRow(1).font = testutils.styles.fonts.comicSansUdB16; + + ws.insertRow(2, [3, 'Jane Doe', dateValue2], 'i'); + ws.insertRow(2, [2, 'Jane Doe', dateValue2], 'o'); + + ws.getRow(2).font = testutils.styles.fonts.broadwayRedOutline20; + ws.getRow(3).font = testutils.styles.fonts.broadwayRedOutline20; + ws.getCell('A2').font = testutils.styles.fonts.arialBlackUI14; + ws.getCell('A3').font = testutils.styles.fonts.arialBlackUI14; + + expect(ws.getRow(2).font).not.deep.equal( + testutils.styles.fonts.comicSansUdB16 + ); + expect(ws.getRow(3).font).not.deep.equal( + testutils.styles.fonts.comicSansUdB16 + ); + expect(ws.getCell('A2').font).not.deep.equal( + testutils.styles.fonts.comicSansUdB16 + ); + expect(ws.getCell('A3').font).not.deep.equal( + testutils.styles.fonts.comicSansUdB16 + ); + }); + it('iterates over rows', () => { const wb = new ExcelJS.Workbook(); const ws = wb.addWorksheet('blort'); @@ -645,22 +677,56 @@ describe('Worksheet', () => { expect(ws.getRows(1, 0)).to.equal(undefined); }); - context('when worksheet name is less than or equal 31', () => { it('save the original name', () => { const wb = new ExcelJS.Workbook(); - let ws = wb.addWorksheet('ThisIsAWorksheetName'); + let ws = wb.addWorksheet(); + ws.name = 'ThisIsAWorksheetName'; expect(ws.name).to.equal('ThisIsAWorksheetName'); - ws = wb.addWorksheet('ThisIsAWorksheetNameWith31Chars'); + ws = wb.addWorksheet(); + ws.name = 'ThisIsAWorksheetNameWith31Chars'; expect(ws.name).to.equal('ThisIsAWorksheetNameWith31Chars'); }); }); + context('name is be not empty string', () => { + it('when empty should thrown an error', () => { + const wb = new ExcelJS.Workbook(); + + expect(() => { + const ws = wb.addWorksheet(); + ws.name = ''; + }).to.throw('The name can\'t be empty.'); + }); + it('when isn\'t string should thrown an error', () => { + const wb = new ExcelJS.Workbook(); + + expect(() => { + const ws = wb.addWorksheet(); + ws.name = 0; + }).to.throw('The name has to be a string.'); + }); + }); + + context('when worksheet name is `History`', () => { + it('thrown an error', () => { + const wb = new ExcelJS.Workbook(); + + expect(() => { + const ws = wb.addWorksheet(); + ws.name = 'History'; + }).to.throw( + 'The name "History" is protected. Please use a different name.' + ); + }); + }); + context('when worksheet name is longer than 31', () => { it('keep first 31 characters', () => { const wb = new ExcelJS.Workbook(); - const ws = wb.addWorksheet('ThisIsAWorksheetNameThatIsLongerThan31'); + const ws = wb.addWorksheet(); + ws.name = 'ThisIsAWorksheetNameThatIsLongerThan31'; expect(ws.name).to.equal('ThisIsAWorksheetNameThatIsLonge'); }); @@ -673,7 +739,10 @@ describe('Worksheet', () => { const invalidCharacters = ['*', '?', ':', '/', '\\', '[', ']']; for (const invalidCharacter of invalidCharacters) { - expect(() => workbook.addWorksheet(invalidCharacter)).to.throw( + expect(() => { + const ws = workbook.addWorksheet(); + ws.name = invalidCharacter; + }).to.throw( `Worksheet name ${invalidCharacter} cannot include any of the following characters: * ? : \\ / [ ]` ); } @@ -685,7 +754,10 @@ describe('Worksheet', () => { const invalidNames = ['\'sheetName', 'sheetName\'']; for (const invalidName of invalidNames) { - expect(() => workbook.addWorksheet(invalidName)).to.throw( + expect(() => { + const ws = workbook.addWorksheet(); + ws.name = invalidName; + }).to.throw( `The first or last character of worksheet name cannot be a single quotation mark: ${invalidName}` ); } @@ -700,9 +772,13 @@ describe('Worksheet', () => { const invalideName = 'THISISAWORKSHEETNAMEINUPPERCASE'; const expectedError = `Worksheet name already exists: ${invalideName}`; - wb.addWorksheet(validName); + const ws = wb.addWorksheet(); + ws.name = validName; - expect(() => wb.addWorksheet(invalideName)).to.throw(expectedError); + expect(() => { + const newWs = wb.addWorksheet(); + newWs.name = invalideName; + }).to.throw(expectedError); }); it('throws an error', () => { @@ -712,10 +788,18 @@ describe('Worksheet', () => { const invalideName = 'ThisIsAWorksheetNameThatIsLongerThan31'; const expectedError = `Worksheet name already exists: ${validName}`; - wb.addWorksheet(validName); + const ws = wb.addWorksheet(); + ws.name = validName; + + expect(() => { + const newWs = wb.addWorksheet(); + newWs.name = validName; + }).to.throw(expectedError); - expect(() => wb.addWorksheet(validName)).to.throw(expectedError); - expect(() => wb.addWorksheet(invalideName)).to.throw(expectedError); + expect(() => { + const newWs = wb.addWorksheet(); + newWs.name = invalideName; + }).to.throw(expectedError); }); }); }); diff --git a/spec/unit/utils/copy-style.spec.js b/spec/unit/utils/copy-style.spec.js new file mode 100644 index 000000000..d54d74860 --- /dev/null +++ b/spec/unit/utils/copy-style.spec.js @@ -0,0 +1,39 @@ +const testUtils = require('../../utils/index'); + +const {copyStyle} = verquire('utils/copy-style'); + +const style1 = { + numFmt: testUtils.styles.numFmts.numFmt1, + font: testUtils.styles.fonts.broadwayRedOutline20, + alignment: testUtils.styles.namedAlignments.topLeft, + border: testUtils.styles.borders.thickRainbow, + fill: testUtils.styles.fills.redGreenDarkTrellis, +}; +const style2 = { + fill: testUtils.styles.fills.rgbPathGrad, +}; + +describe('copyStyle', () => { + it('should copy a style deeply', () => { + const copied = copyStyle(style1); + expect(copied).to.deep.equal(style1); + expect(copied.font).to.not.equal(style1.font); + expect(copied.alignment).to.not.equal(style1.alignment); + expect(copied.border).to.not.equal(style1.border); + expect(copied.fill).to.not.equal(style1.fill); + + expect(copyStyle({})).to.deep.equal({}); + }); + + it('should copy fill.stops deeply', () => { + const copied = copyStyle(style2); + expect(copied.fill.stops).to.deep.equal(style2.fill.stops); + expect(copied.fill.stops).to.not.equal(style2.fill.stops); + expect(copied.fill.stops[0]).to.not.equal(style2.fill.stops[0]); + }); + + it('should return the argument if a falsy value passed', () => { + expect(copyStyle(null)).to.equal(null); + expect(copyStyle(undefined)).to.equal(undefined); + }); +}); diff --git a/spec/unit/utils/under-dash.spec.js b/spec/unit/utils/under-dash.spec.js new file mode 100644 index 000000000..32f47bf12 --- /dev/null +++ b/spec/unit/utils/under-dash.spec.js @@ -0,0 +1,79 @@ +const _ = verquire('utils/under-dash'); +const util = require('util'); + +describe('under-dash', () => { + describe('isEqual', () => { + const values = [ + 0, + 1, + true, + false, + 'string', + 'foobar', + 'other string', + [], + ['array'], + ['array', 'foobar'], + ['array2'], + ['array2', 'foobar'], + {}, + {object: 1}, + {object: 2}, + {object: 1, foobar: 'quux'}, + {object: 2, foobar: 'quux'}, + null, + undefined, + () => {}, + () => {}, + Symbol('foo'), + Symbol('foo'), + Symbol('bar'), + ]; + + function showVal(o) { + return util.inspect(o, {compact: true}); + } + + it('works on simple values', () => { + for (let i = 0; i < values.length; i++) { + for (let j = 0; j < values.length; j++) { + const a = values[i]; + const b = values[j]; + + const assertion = `${showVal(a)} ${i === j ? '==' : '!='} ${showVal( + b + )}`; + expect(_.isEqual(a, b)).to.equal(i === j, `expected ${assertion}`); + } + } + }); + + it('works on complex arrays', () => { + for (let i = 0; i < values.length; i++) { + for (let j = 0; j < values.length; j++) { + const a = [values[i]]; + const b = [values[j]]; + + const assertion = `${showVal(a)} ${i === j ? '==' : '!='} ${showVal( + b + )}`; + expect(_.isEqual(a, b)).to.equal(i === j, `expected ${assertion}`); + } + } + }); + + it('works on complex objects', () => { + for (let i = 0; i < values.length; i++) { + for (let j = 0; j < values.length; j++) { + const a = {key: values[i]}; + const b = {key: values[j]}; + + const assertion = `${showVal(a)} ${i === j ? '==' : '!='} ${showVal( + b + )}`; + expect(_.isEqual(a, b)).to.equal(i === j, `expected ${assertion}`); + } + } + }); + }); +}); diff --git a/spec/unit/xlsx/xform/sheet/hyperlink-xform.spec.js b/spec/unit/xlsx/xform/sheet/hyperlink-xform.spec.js index 4292dfe9d..9e25bbc61 100644 --- a/spec/unit/xlsx/xform/sheet/hyperlink-xform.spec.js +++ b/spec/unit/xlsx/xform/sheet/hyperlink-xform.spec.js @@ -15,6 +15,18 @@ const expectations = [ xml: '', tests: ['render', 'renderIn', 'parse'], }, + { + title: 'Internal Link', + create() { + return new HyperlinkXform(); + }, + preparedModel: {address: 'B6', rId: 'rId1', target: 'sheet1!B2'}, + get parsedModel() { + return this.preparedModel; + }, + xml: '', + tests: ['render', 'renderIn', 'parse'], + }, ]; describe('HyperlinkXform', () => { diff --git a/spec/unit/xlsx/xform/table/custom-filter-xform.spec.js b/spec/unit/xlsx/xform/table/custom-filter-xform.spec.js new file mode 100644 index 000000000..971ac53b7 --- /dev/null +++ b/spec/unit/xlsx/xform/table/custom-filter-xform.spec.js @@ -0,0 +1,30 @@ +const testXformHelper = require('../test-xform-helper'); + +const CustomFilterXform = verquire('xlsx/xform/table/custom-filter-xform'); + +const expectations = [ + { + title: 'custom filter', + create() { + return new CustomFilterXform(); + }, + preparedModel: {val: '*brandywine*'}, + xml: '', + parsedModel: {val: '*brandywine*'}, + tests: ['render', 'renderIn', 'parse'], + }, + { + title: 'custom filter with operator', + create() { + return new CustomFilterXform(); + }, + preparedModel: {operator: 'notEqual', val: '4'}, + xml: '', + parsedModel: {operator: 'notEqual', val: '4'}, + tests: ['render', 'renderIn', 'parse'], + }, +]; + +describe('CustomFilterXform', () => { + testXformHelper(expectations); +}); diff --git a/spec/unit/xlsx/xform/table/filter-column-xform.spec.js b/spec/unit/xlsx/xform/table/filter-column-xform.spec.js index 2d9900c17..c15f802fe 100644 --- a/spec/unit/xlsx/xform/table/filter-column-xform.spec.js +++ b/spec/unit/xlsx/xform/table/filter-column-xform.spec.js @@ -31,6 +31,25 @@ const expectations = [ tests: ['prepare', 'render', 'renderIn', 'parse'], options: {index: 1}, }, + { + title: 'with custom filter', + create() { + return new FilterColumnXform(); + }, + initialModel: {filterButton: false, customFilters: [{val: '*brandywine*'}]}, + preparedModel: { + colId: '0', + filterButton: false, + customFilters: [{val: '*brandywine*'}], + }, + xml: + '', + get parsedModel() { + return this.initialModel; + }, + tests: ['prepare', 'render', 'renderIn', 'parse'], + options: {index: 0}, + }, ]; describe('FilterColumnXform', () => { diff --git a/spec/utils/data/conditional-formatting.json b/spec/utils/data/conditional-formatting.json index 7a58a3ee0..d80b53998 100644 --- a/spec/utils/data/conditional-formatting.json +++ b/spec/utils/data/conditional-formatting.json @@ -61,6 +61,35 @@ } ] }, + "abbreviation": { + "ref": "A:A", + "rules": [ + { + "type": "cellIs", + "operator": "between", + "formulae": [1000, 1000000], + "style": { "numFmt": "#,##0.000,\\K;-#,##0.000,\\K" } + }, + { + "type": "cellIs", + "operator": "between", + "formulae": [-1000, -1000000], + "style": { "numFmt": "#,##0.000,\\K;-#,##0.000,\\K" } + }, + { + "type": "cellIs", + "operator": "greaterThan", + "formulae": [1000000], + "style": { "numFmt": "#,##0.000,,\\M;-#,##0.000,,\\M" } + }, + { + "type": "cellIs", + "operator": "lessThan", + "formulae": [-1000000], + "style": { "numFmt": "#,##0.000,,\\M;-#,##0.000,,\\M" } + } + ] + }, "types": [ "expression", "cellIs", @@ -71,4 +100,4 @@ "containsText", "timePeriod" ] -} \ No newline at end of file +}