diff --git a/README.md b/README.md index bdb8fda..3000dcf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ExcelJS +# @zurmokeeper/exceljs [![Build status](https://github.com/exceljs/exceljs/workflows/ExcelJS/badge.svg)](https://github.com/exceljs/exceljs/actions?query=workflow%3AExcelJS) @@ -16,6 +16,33 @@ Reverse engineered from Excel spreadsheet files as a project. npm install @zurmokeeper/exceljs ``` +# V4.4.2 New Features! + +Change Log: + +* 1: Fixbug: [Internal hyperlink does not work on wps office](https://github.com/zurmokeeper/excelize/issues/4). (Break change) and support new internal hyperlink methods。 +* 2:Add type definition for WorksheetModel.merges, Thank you ytjmt, Merged PR2281. +* 3:Add type definition for WorksheetProtection.spinCount,Thank you damingerdai, Merged PR2284. + +PS: Since V4.4.2 @zurmokeeper/exceljs new cell insertion internal hyperlink support `Sheet2!A1:B1` and `A1:B1` and other forms, the original only supports `Sheet2!A1`, use the following way:: + + +``` +const wb = new ExcelJS.Workbook(); +const ws1 = wb.addWorksheet('Sheet1'); +const ws2 = wb.addWorksheet('Sheet2'); + +'#' is required, @zurmokeeper/exceljs is to distinguish internal hyperlink by '#', the default will be considered non-internal hyperlink, older versions also need to manually add '#' , how not to add if +// internal hyperlink +ws1.getCell('A1').value = { text: 'Sheet2', hyperlink: '#Sheet2!A1' }; + +// internal hyperlink +ws1.getCell('A1').value = { text: 'Sheet2', hyperlink: '#Sheet2!A1:B1' }; + +// internal hyperlink +ws1.getCell('A1').value = { text: 'Sheet2', hyperlink: '#A1:B1' }; +``` + # V4.4.1 New Features! Change Log: @@ -227,7 +254,7 @@ To be clear, all contributions added to this library will be included in the lib # Importing[⬆](#contents) ```javascript -const ExcelJS = require('exceljs'); +const ExcelJS = require('@zurmokeeper/exceljs'); ``` ## ES5 Imports[⬆](#contents) diff --git a/README_zh.md b/README_zh.md index 83ce845..4854ecc 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,4 +1,4 @@ -# ExcelJS +# @zurmokeeper/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) @@ -14,6 +14,33 @@ npm install @zurmokeeper/exceljs ``` +# V4.4.2 新的功能! + +变更日志: + +* 1: Fixbug: [Internal hyperlink does not work on wps office](https://github.com/zurmokeeper/excelize/issues/4). (Break change) 和支持新的内部链接方式。 +* 2:Add type definition for WorksheetModel.merges, Thank you ytjmt, Merged PR2281. +* 3:Add type definition for WorksheetProtection.spinCount,Thank you damingerdai, Merged PR2284. + +PS: 自 V4.4.2 @zurmokeeper/exceljs 新增单元格插入内部链接支持 Sheet2!A1:B1 和 A1:B1等形式,原来只支持 Sheet2!A1,使用方式如下: + + +``` +const wb = new ExcelJS.Workbook(); +const ws1 = wb.addWorksheet('Sheet1'); +const ws2 = wb.addWorksheet('Sheet2'); + +'#'是必须的,@zurmokeeper/exceljs 是通过'#'来区分内部链接的,默认会被认为是非内部链接,旧版本使用也要手动加上 '#' ,如何没加的话 +// internal hyperlink +ws1.getCell('A1').value = { text: 'Sheet2', hyperlink: '#Sheet2!A1' }; + +// internal hyperlink +ws1.getCell('A1').value = { text: 'Sheet2', hyperlink: '#Sheet2!A1:B1' }; + +// internal hyperlink +ws1.getCell('A1').value = { text: 'Sheet2', hyperlink: '#A1:B1' }; +``` + # V4.4.1 新的功能! 变更日志: @@ -188,7 +215,7 @@ npm install @zurmokeeper/exceljs # 导入[⬆](#目录) ```javascript -const ExcelJS = require('exceljs'); +const ExcelJS = require('@zurmokeeper/exceljs'); ``` ## ES5 导入[⬆](#目录) diff --git a/index.d.ts b/index.d.ts index 1303f77..be2012e 100644 --- a/index.d.ts +++ b/index.d.ts @@ -880,6 +880,7 @@ export interface WorksheetProtection { sort: boolean; autoFilter: boolean; pivotTables: boolean; + spinCount: number; } export interface Image { extension: 'jpeg' | 'png' | 'gif'; @@ -989,6 +990,7 @@ export interface WorksheetModel { views: WorksheetView[]; autoFilter: AutoFilter; media: Media[]; + merges: Range['range'][]; } export type WorksheetState = 'visible' | 'hidden' | 'veryHidden'; diff --git a/lib/xlsx/xform/sheet/hyperlink-xform.js b/lib/xlsx/xform/sheet/hyperlink-xform.js index 88f4ee2..499ea2f 100644 --- a/lib/xlsx/xform/sheet/hyperlink-xform.js +++ b/lib/xlsx/xform/sheet/hyperlink-xform.js @@ -7,11 +7,14 @@ class HyperlinkXform extends BaseXform { render(xmlStream, model) { if (this.isInternalLink(model)) { + // Remove '#' example #sheet1!A1 -> sheet1!A1 + model.target = model.target ? model.target.slice(1) : model.target; xmlStream.leafNode('hyperlink', { ref: model.address, - 'r:id': model.rId, + // 'r:id': model.rId, // Internal hyperlink don't need 'r:id', it's enough to have location tooltip: model.tooltip, location: model.target, + display: model.tooltip, // TODO: For the time being, this is compatible with google sheet. https://www.google.cn/sheets/about/ }); } else { xmlStream.leafNode('hyperlink', { @@ -45,9 +48,23 @@ class HyperlinkXform extends BaseXform { return false; } + /** + * @desc example Sheet2!D3 Sheet2!D3:E3 D3:E3 + * @returns + */ isInternalLink(model) { // @example: Sheet2!D3, return true - return model.target && /^[^!]+![a-zA-Z]+[\d]+$/.test(model.target); + // return model.target && /^[^!]+![a-zA-Z]+[\d]+$/.test(model.target); + + // Using regular expressions is not enough to cover all cases like the one below, + // An example of the xlsx library, which is also generic + // https://docs.sheetjs.com/docs/csf/features/hyperlinks#internal-links + // ws["C1"].l = { Target: "#SheetJSDN", Tooltip: "Defined Name" }; + // wb.Workbook = { + // Names: [{Name: "SheetJSDN", Ref:"Sheet2!A1:B2"}] + // } + // an example of the xlsx library, so instead pass '#' manually to determine if it is an internal hyperlink. + return model.target && model.target.slice(0, 1) === '#'; } } diff --git a/lib/xlsx/xform/sheet/worksheet-xform.js b/lib/xlsx/xform/sheet/worksheet-xform.js index 4b36e37..ce861ff 100644 --- a/lib/xlsx/xform/sheet/worksheet-xform.js +++ b/lib/xlsx/xform/sheet/worksheet-xform.js @@ -152,15 +152,23 @@ class WorkSheetXform extends BaseXform { return `rId${r.length + 1}`; } + // TODO: The same as HyperlinkXform. isInternalLink + function isInternalLink(m) { + return m.target && m.target.slice(0, 1) === '#'; + } + model.hyperlinks.forEach(hyperlink => { const rId = nextRid(rels); hyperlink.rId = rId; - rels.push({ - Id: rId, - Type: RelType.Hyperlink, - Target: hyperlink.target, - TargetMode: 'External', - }); + // Internal hyperlink do not need to generate worksheets/_rels/sheetx.xml.rels, but external hyperlink do. + if (!isInternalLink(hyperlink)) { + rels.push({ + Id: rId, + Type: RelType.Hyperlink, + Target: hyperlink.target, + TargetMode: 'External', + }); + } }); // prepare comment relationships @@ -221,9 +229,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; @@ -405,11 +411,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/package.json b/package.json index 13ae6b2..e6fc009 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,12 @@ { "name": "@zurmokeeper/exceljs", - "version": "4.4.1", + "version": "4.4.2", "description": "Excel Workbook Manager - Read and Write xlsx and csv Files.", "private": false, "license": "MIT", + "publishConfig": { + "access": "public" + }, "author": { "name": "zurmokeeper", "email": "3382272560@qq.com" diff --git a/spec/integration/issues/issue-2247-cryptor.spec.js b/spec/integration/issues/issue-2247-cryptor.spec.js index 7130d19..15ba7d6 100644 --- a/spec/integration/issues/issue-2247-cryptor.spec.js +++ b/spec/integration/issues/issue-2247-cryptor.spec.js @@ -15,7 +15,7 @@ describe('pr related issues', () => { }); const sheetName = workbook.getWorksheet(1).name; expect(sheetName).to.equal('Sheet1'); - }); + }).timeout(10000); it('workbook.xlsx.readFile, ecma376_agile encryption method decrypted successfully', async () => { const workbook = new ExcelJS.Workbook(); @@ -24,7 +24,7 @@ describe('pr related issues', () => { }); const sheetName = workbook.getWorksheet(1).name; expect(sheetName).to.equal('Sheet1'); - }); + }).timeout(10000); it('workbook.xlsx.load, ecma376_standard encryption method decrypted successfully ', async () => { const workbook = new ExcelJS.Workbook(); @@ -34,7 +34,7 @@ describe('pr related issues', () => { }); const sheetName = workbook.getWorksheet(1).name; expect(sheetName).to.equal('Sheet1'); - }); + }).timeout(10000); it('workbook.xlsx.load, ecma376_agile encryption method decrypted successfully ', async () => { const workbook = new ExcelJS.Workbook(); @@ -47,7 +47,7 @@ describe('pr related issues', () => { ); const sheetName = workbook.getWorksheet(1).name; expect(sheetName).to.equal('Sheet1'); - }); + }).timeout(10000); it('workbook.xlsx.load, options.base64 = true, ecma376_standard encryption method decrypted successfully ', async () => { const workbook = new ExcelJS.Workbook(); @@ -60,7 +60,7 @@ describe('pr related issues', () => { }); const sheetName = workbook.getWorksheet(1).name; expect(sheetName).to.equal('Sheet1'); - }); + }).timeout(10000); it('workbook.xlsx.load, options.base64 = true, ecma376_agile encryption method decrypted successfully ', async () => { const workbook = new ExcelJS.Workbook(); @@ -73,7 +73,7 @@ describe('pr related issues', () => { }); const sheetName = workbook.getWorksheet(1).name; expect(sheetName).to.equal('Sheet1'); - }); + }).timeout(10000); it('workbook.xlsx.read, ecma376_standard encryption method decrypted successfully ', async () => { const workbook = new ExcelJS.Workbook(); @@ -83,7 +83,7 @@ describe('pr related issues', () => { }); const sheetName = workbook.getWorksheet(1).name; expect(sheetName).to.equal('Sheet1'); - }); + }).timeout(10000); it('workbook.xlsx.read, ecma376_agile encryption method decrypted successfully ', async () => { const workbook = new ExcelJS.Workbook(); @@ -93,6 +93,6 @@ describe('pr related issues', () => { }); const sheetName = workbook.getWorksheet(1).name; expect(sheetName).to.equal('Sheet1'); - }); + }).timeout(10000); }); }); diff --git a/spec/unit/xlsx/xform/sheet/hyperlink-xform.spec.js b/spec/unit/xlsx/xform/sheet/hyperlink-xform.spec.js index 9e25bbc..d064b56 100644 --- a/spec/unit/xlsx/xform/sheet/hyperlink-xform.spec.js +++ b/spec/unit/xlsx/xform/sheet/hyperlink-xform.spec.js @@ -16,16 +16,43 @@ const expectations = [ tests: ['render', 'renderIn', 'parse'], }, { - title: 'Internal Link', + title: 'Internal Link sheet1!B2', create() { return new HyperlinkXform(); }, - preparedModel: {address: 'B6', rId: 'rId1', target: 'sheet1!B2'}, + preparedModel: {address: 'B6', target: '#sheet1!B2'}, get parsedModel() { return this.preparedModel; }, - xml: '', - tests: ['render', 'renderIn', 'parse'], + xml: '', + // tests: ['render', 'renderIn', 'parse'], + tests: ['render', 'renderIn'], + }, + { + title: 'Internal Link B2:C4', + create() { + return new HyperlinkXform(); + }, + preparedModel: {address: 'B6', target: '#B2:C4'}, + get parsedModel() { + return this.preparedModel; + }, + xml: '', + // tests: ['render', 'renderIn', 'parse'], + tests: ['render', 'renderIn'], + }, + { + title: 'Internal Link sheet1!B2:C4', + create() { + return new HyperlinkXform(); + }, + preparedModel: {address: 'B6', target: '#sheet1!B2:C4'}, + get parsedModel() { + return this.preparedModel; + }, + xml: '', + // tests: ['render', 'renderIn', 'parse'], + tests: ['render', 'renderIn'], }, ]; diff --git a/spec/unit/xlsx/xform/test-xform-helper.js b/spec/unit/xlsx/xform/test-xform-helper.js index 8cadea8..a74f5f3 100644 --- a/spec/unit/xlsx/xform/test-xform-helper.js +++ b/spec/unit/xlsx/xform/test-xform-helper.js @@ -99,7 +99,7 @@ const its = { const xmlStream = new XmlStream(); xform.render(xmlStream, model); - // console.log(xmlStream.xml); + // console.log(xmlStream.xml, result); expect(xmlStream.xml).xml.to.equal(result); resolve();