Skip to content

Improve worksheets' naming validation logic. #2257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 1 addition & 29 deletions lib/doc/workbook.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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,
Expand Down Expand Up @@ -235,7 +208,6 @@ class Workbook {
state,
workbook: this,
}));

worksheet.model = worksheetModel;
});

Expand Down
51 changes: 48 additions & 3 deletions lib/doc/worksheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ const {copyStyle} = require('../utils/copy-style');
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';
Expand All @@ -47,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(
{},
Expand Down Expand Up @@ -128,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;
}
Expand Down
78 changes: 65 additions & 13 deletions spec/integration/worksheet.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -677,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');
});
Expand All @@ -702,11 +736,14 @@ describe('Worksheet', () => {
it('throws an error', () => {
const workbook = new ExcelJS.Workbook();

const invalidCharacters = ['*', '?', ':', '/', '\\', '[', ']', '*', '?', ':', '¥', '/', '\', '[', ']'];
const invalidCharacters = ['*', '?', ':', '/', '\\', '[', ']'];

for (const invalidCharacter of invalidCharacters) {
expect(() => workbook.addWorksheet(invalidCharacter)).to.throw(
`Worksheet name ${invalidCharacter} cannot include any of the following characters: * ? : \\ / [ ] * ? : ¥ / \ [ ]`
expect(() => {
const ws = workbook.addWorksheet();
ws.name = invalidCharacter;
}).to.throw(
`Worksheet name ${invalidCharacter} cannot include any of the following characters: * ? : \\ / [ ]`
);
}
});
Expand All @@ -717,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}`
);
}
Expand All @@ -732,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', () => {
Expand All @@ -744,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);
});
});
});
Expand Down