forked from sheetjs/sheetjs
Support for parsing Comments
Comments parts listed in the [Content Types] are parsed. Sheets's relationships are parsed. Comments parts are correlated to their corresponding sheets parts. Comments's contents are added to the ref'ed cells. Rich text styling properties are currently ignored. For example: { "!ref": "A1:B3", "A1": { "v": 1, "t": "n" }, "B1": { "v": "one", "t": "s", "r": "one", "c": [ { "a": "Yegor Kozlov", "t": [ "Yegor Kozlov:", "\r\nfirst cell" ] } ] } }
This commit is contained in:
parent
e298cc8dd3
commit
59d9d9086b
11
test.js
11
test.js
@ -39,3 +39,14 @@ describe('should parse test files', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should have comment as part of cell\'s properties', function(){
|
||||
it('Parse comments.xml and insert into cell',function(){
|
||||
var wb = XLSX.readFile('./test_files/SimpleWithComments.xlsx');
|
||||
var sheetName = 'Sheet1';
|
||||
var ws = wb.Sheets[sheetName];
|
||||
assert.equal(ws.B1.c.length, 1,"must have 1 comment");
|
||||
assert.equal(ws.B1.c[0].t.length, 2,"must have 2 texts");
|
||||
assert.equal(ws.B1.c[0].a, 'Yegor Kozlov',"must have the same author");
|
||||
});
|
||||
});
|
||||
|
150
xlsx.js
150
xlsx.js
@ -588,6 +588,7 @@ var ct2type = {
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml": "strs",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml":"styles",
|
||||
"application/vnd.openxmlformats-officedocument.theme+xml":"themes",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml": "comments",
|
||||
"foo": "bar"
|
||||
};
|
||||
|
||||
@ -821,7 +822,7 @@ var ctext = {};
|
||||
function parseCT(data) {
|
||||
if(!data || !data.match) return data;
|
||||
var ct = { workbooks: [], sheets: [], calcchains: [], themes: [], styles: [],
|
||||
coreprops: [], extprops: [], strs:[], xmlns: "" };
|
||||
coreprops: [], extprops: [], strs:[], comments: [], xmlns: "" };
|
||||
(data.match(/<[^>]*>/g)||[]).forEach(function(x) {
|
||||
var y = parsexmltag(x);
|
||||
switch(y[0]) {
|
||||
@ -1026,6 +1027,104 @@ function parseStyles(data) {
|
||||
return styles;
|
||||
}
|
||||
|
||||
/* 9.3.2 OPC Relationships Markup */
|
||||
function parseRels(data, currentFilePath) {
|
||||
if (!data) return data;
|
||||
if (currentFilePath.charAt(0) !== '/') {
|
||||
currentFilePath = '/'+currentFilePath;
|
||||
}
|
||||
var rels = {};
|
||||
|
||||
var resolveRelativePathIntoAbsolute = function (to) {
|
||||
var toksFrom = currentFilePath.split('/');
|
||||
toksFrom.pop(); // folder path
|
||||
var toksTo = to.split('/');
|
||||
var reversed = [];
|
||||
while (toksTo.length !== 0) {
|
||||
var tokTo = toksTo.shift();
|
||||
if (tokTo === '..') {
|
||||
toksFrom.pop();
|
||||
} else if (tokTo !== '.') {
|
||||
toksFrom.push(tokTo);
|
||||
}
|
||||
}
|
||||
return toksFrom.join('/');
|
||||
}
|
||||
|
||||
data.match(/<[^>]*>/g).forEach(function(x) {
|
||||
var y = parsexmltag(x);
|
||||
/* 9.3.2.2 OPC_Relationships */
|
||||
if (y[0] === '<Relationship') {
|
||||
var rel = {}; rel.Type = y.Type; rel.Target = y.Target; rel.Id = y.Id; rel.TargetMode = y.TargetMode;
|
||||
var canonictarget = resolveRelativePathIntoAbsolute(y.Target);
|
||||
rels[canonictarget] = rel;
|
||||
}
|
||||
});
|
||||
|
||||
return rels;
|
||||
}
|
||||
|
||||
/* 18.7.3 CT_Comment */
|
||||
function parseComments(data) {
|
||||
if(data.match(/<comments *\/>/)) {
|
||||
throw new Error('Not a valid comments xml');
|
||||
}
|
||||
var authors = [];
|
||||
var commentList = [];
|
||||
data.match(/<authors>([^\u2603]*)<\/authors>/m)[1].split('</author>').forEach(function(x) {
|
||||
if(x === "" || x.trim() === "") return;
|
||||
authors.push(x.match(/<author[^>]*>(.*)/)[1]);
|
||||
});
|
||||
data.match(/<commentList>([^\u2603]*)<\/commentList>/m)[1].split('</comment>').forEach(function(x, index) {
|
||||
if(x === "" || x.trim() === "") return;
|
||||
var y = parsexmltag(x.match(/<comment[^>]*>/)[0]);
|
||||
var comment = { author: y.authorId && authors[y.authorId] ? authors[y.authorId] : undefined, ref: y.ref, guid: y.guid, texts:[] };
|
||||
x.match(/<text>([^\u2603]*)<\/text>/m)[1].split('</r>').forEach(function(r) {
|
||||
if(r === "" || r.trim() === "") return;
|
||||
/* 18.4.12 t ST_Xstring */
|
||||
var ct = r.match(matchtag('t'));
|
||||
comment.texts.push(utf8read(unescapexml(ct[1])));
|
||||
// TODO: parse rich text format
|
||||
});
|
||||
commentList.push(comment);
|
||||
});
|
||||
return commentList;
|
||||
}
|
||||
|
||||
function parseCommentsAddToSheets(zip, dirComments, sheets, sheetRels) {
|
||||
for(var i = 0; i != dirComments.length; ++i) {
|
||||
var canonicalpath=dirComments[i];
|
||||
var comments=parseComments(getdata(getzipfile(zip, canonicalpath.replace(/^\//,''))));
|
||||
// find the sheets targeted by these comments
|
||||
var sheetNames = Object.keys(sheets);
|
||||
for(var j = 0; j != sheetNames.length; ++j) {
|
||||
var sheetName = sheetNames[j];
|
||||
var rels = sheetRels[sheetName];
|
||||
if (rels) {
|
||||
var rel = rels[canonicalpath];
|
||||
if (rel) {
|
||||
insertCommentsIntoSheet(sheetName, sheets[sheetName], comments);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function insertCommentsIntoSheet(sheetName, sheet, comments) {
|
||||
comments.forEach(function(comment) {
|
||||
var cell = sheet[comment.ref];
|
||||
if (!cell) {
|
||||
cell = {};
|
||||
sheet[comment.ref] = cell;
|
||||
}
|
||||
|
||||
if (!cell.c) {
|
||||
cell.c = [];
|
||||
}
|
||||
cell.c.push({a: comment.author, t: comment.texts});
|
||||
});
|
||||
}
|
||||
|
||||
function getdata(data) {
|
||||
if(!data) return null;
|
||||
if(data.data) return data.data;
|
||||
@ -1058,26 +1157,37 @@ function parseZip(zip) {
|
||||
var deps = {};
|
||||
if(dir.calcchain) deps=parseDeps(getdata(getzipfile(zip, dir.calcchain.replace(/^\//,''))));
|
||||
var sheets = {}, i=0;
|
||||
var sheetRels = {};
|
||||
if(!props.Worksheets) {
|
||||
/* Google Docs doesn't generate the appropriate metadata, so we impute: */
|
||||
var wbsheets = wb.Sheets;
|
||||
props.Worksheets = wbsheets.length;
|
||||
props.SheetNames = [];
|
||||
for(var j = 0; j != wbsheets.length; ++j) {
|
||||
props.SheetNames[j] = wbsheets[j].name;
|
||||
}
|
||||
for(i = 0; i != props.Worksheets; ++i) {
|
||||
try { /* TODO: remove these guards */
|
||||
sheets[props.SheetNames[i]]=parseSheet(getdata(getzipfile(zip, 'xl/worksheets/sheet' + (i+1) + '.xml')));
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for(i = 0; i != props.Worksheets; ++i) {
|
||||
try {
|
||||
sheets[props.SheetNames[i]]=parseSheet(getdata(getzipfile(zip, dir.sheets[i].replace(/^\//,''))));
|
||||
} catch(e) {}
|
||||
}
|
||||
/* Google Docs doesn't generate the appropriate metadata, so we impute: */
|
||||
var wbsheets = wb.Sheets;
|
||||
props.Worksheets = wbsheets.length;
|
||||
props.SheetNames = [];
|
||||
for(var j = 0; j != wbsheets.length; ++j) {
|
||||
props.SheetNames[j] = wbsheets[j].name;
|
||||
}
|
||||
for(i = 0; i != props.Worksheets; ++i) {
|
||||
try { /* TODO: remove these guards */
|
||||
var path = 'xl/worksheets/sheet' + (i+1) + '.xml';
|
||||
var relsPath = path.replace(/^(.*)(\/)([^\/]*)$/, "$1/_rels/$3.rels");
|
||||
sheets[props.SheetNames[i]]=parseSheet(getdata(getzipfile(zip, path)));
|
||||
sheetRels[props.SheetNames[i]]=parseRels(getdata(getzipfile(zip, relsPath)), path);
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
else {
|
||||
for(i = 0; i != props.Worksheets; ++i) {
|
||||
try {
|
||||
var path = dir.sheets[i].replace(/^\//,'');
|
||||
var relsPath = path.replace(/^(.*)(\/)([^\/]*)$/, "$1/_rels/$3.rels");
|
||||
sheets[props.SheetNames[i]]=parseSheet(getdata(getzipfile(zip, path)));
|
||||
sheetRels[props.SheetNames[i]]=parseRels(getdata(getzipfile(zip, relsPath)), path);
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
if(dir.comments) {
|
||||
parseCommentsAddToSheets(zip, dir.comments, sheets, sheetRels);
|
||||
}
|
||||
return {
|
||||
Directory: dir,
|
||||
|
Loading…
Reference in New Issue
Block a user