forked from sheetjs/sheetjs
		
	version bump 0.5.7: WTF-ification
- opts.bookDeps parses calc chain; now chains are not parsed by default - getzipdata safe option obviates some try-catch blocks - parsing cell type 'd' (h/t @malphettes, yet again :) - opts.WTF throws on errors
This commit is contained in:
		
							parent
							
								
									b09d5dfe1a
								
							
						
					
					
						commit
						c0a8bef07c
					
				| @ -79,6 +79,7 @@ The exported `read` and `readFile` functions accept an options argument: | ||||
| | cellHTML    | true    | Parse rich text and save HTML to the .h field | | ||||
| | cellNF      | false   | Save number format string to the .z field | | ||||
| | sheetStubs  | false   | Create cell objects for stub cells | | ||||
| | bookDeps    | false   | If true, parse calculation chains | | ||||
| | bookProps   | false   | If true, only parse enough to get book metadata ** | | ||||
| | bookSheets  | false   | If true, only parse enough to get the sheet names | | ||||
| 
 | ||||
| @ -87,6 +88,7 @@ The exported `read` and `readFile` functions accept an options argument: | ||||
| - Even if `cellNF` is false, formatted text (.w) will be generated | ||||
| - In some cases, sheets may be parsed even if `bookSheets` is false. | ||||
| - `bookSheets` and `bookProps` combine to give both sets of information | ||||
| - `Deps` will be an empty object if `bookDeps` is falsy | ||||
| 
 | ||||
| The defaults are enumerated in bits/84_defaults.js | ||||
| 
 | ||||
|  | ||||
| @ -44,13 +44,14 @@ if(!fs.existsSync(filename)) { | ||||
| 	process.exit(2); | ||||
| } | ||||
| 
 | ||||
| if(program.dev) X.verbose = 2; | ||||
| 
 | ||||
| var opts = {}; | ||||
| var opts = {}, wb; | ||||
| if(program.listSheets) opts.bookSheets = true; | ||||
| 
 | ||||
| var wb; | ||||
| if(program.dev) wb = X.readFile(filename, opts); | ||||
| if(program.dev) { | ||||
| 	X.verbose = 2; | ||||
| 	opts.WTF = true; | ||||
| 	wb = X.readFile(filename, opts); | ||||
| } | ||||
| else try { | ||||
| 	wb = X.readFile(filename, opts); | ||||
| } catch(e) { | ||||
|  | ||||
| @ -1 +1 @@ | ||||
| XLSX.version = '0.5.6'; | ||||
| XLSX.version = '0.5.7'; | ||||
|  | ||||
| @ -18,6 +18,12 @@ function getzipfile(zip, file) { | ||||
| 	throw new Error("Cannot find file " + file + " in zip"); | ||||
| } | ||||
| 
 | ||||
| function getzipdata(zip, file, safe) { | ||||
| 	if(!safe) return getdata(getzipfile(zip, file)); | ||||
| 	if(!file) return null; | ||||
| 	try { return getzipdata(zip, file); } catch(e) { return null; } | ||||
| } | ||||
| 
 | ||||
| var _fs, jszip; | ||||
| if(typeof JSZip !== 'undefined') jszip = JSZip; | ||||
| if (typeof exports !== 'undefined') { | ||||
|  | ||||
| @ -25,7 +25,8 @@ function parse_comments_xml(data, opts) { | ||||
| function parse_comments(zip, dirComments, sheets, sheetRels, opts) { | ||||
| 	for(var i = 0; i != dirComments.length; ++i) { | ||||
| 		var canonicalpath=dirComments[i]; | ||||
| 		var comments=parse_comments_xml(getdata(getzipfile(zip, canonicalpath.replace(/^\//,''))), opts); | ||||
| 		var comments=parse_comments_xml(getzipdata(zip, canonicalpath.replace(/^\//,''), true), opts); | ||||
| 		if(!comments || !comments.length) return; | ||||
| 		// find the sheets targeted by these comments
 | ||||
| 		var sheetNames = Object.keys(sheets); | ||||
| 		for(var j = 0; j != sheetNames.length; ++j) { | ||||
|  | ||||
| @ -43,6 +43,7 @@ function parse_ws_xml(data, opts) { | ||||
| 			else p.t = (cell.t ? cell.t : "n"); // default is "n" in schema
 | ||||
| 			if(refguess.s.c > idx) refguess.s.c = idx; | ||||
| 			if(refguess.e.c < idx) refguess.e.c = idx; | ||||
| 			/* 18.18.11 t ST_CellType */ | ||||
| 			switch(p.t) { | ||||
| 				case 'n': p.v = parseFloat(p.v); break; | ||||
| 				case 's': { | ||||
| @ -57,20 +58,14 @@ function parse_ws_xml(data, opts) { | ||||
| 					is = is ? parse_si(is[1]) : {t:"",r:""}; | ||||
| 					p.t = 'str'; p.v = is.t; | ||||
| 					break; // inline string
 | ||||
| 				case 'b': | ||||
| 					switch(p.v) { | ||||
| 						case '0': case 'FALSE': case "false": case false: p.v=false; break; | ||||
| 						case '1': case 'TRUE':  case "true":  case true:  p.v=true;  break; | ||||
| 						default: throw "Unrecognized boolean: " + p.v; | ||||
| 					} break; | ||||
| 				case 'd': | ||||
| 				case 'b': if(typeof p.v !== 'boolean') p.v = parsexmlbool(p.v); break; | ||||
| 				case 'd': /* TODO: date1904 logic */ | ||||
| 					var epoch = Date.parse(p.v); | ||||
| 					p.v = (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000); | ||||
| 					p.t = 'n'; | ||||
| 					break; | ||||
| 				/* in case of error, stick value in .raw */ | ||||
| 				case 'e': p.raw = RBErr[p.v]; break; | ||||
| 				default: throw "Unrecognized cell type: " + p.t; | ||||
| 			} | ||||
| 
 | ||||
| 			/* formatting */ | ||||
| @ -82,7 +77,7 @@ function parse_ws_xml(data, opts) { | ||||
| 			try { | ||||
| 				p.w = SSF.format(fmtid,p.v,_ssfopts); | ||||
| 				if(opts.cellNF) p.z = SSF._table[fmtid]; | ||||
| 			} catch(e) { } | ||||
| 			} catch(e) { if(opts.WTF) throw e; } | ||||
| 			s[cell.r] = p; | ||||
| 		}); | ||||
| 	}); | ||||
|  | ||||
| @ -152,7 +152,7 @@ var parse_ws_bin = function(data, opts) { | ||||
| 				if((cf = styles.CellXf[val[0].iStyleRef])) try { | ||||
| 					p.w = SSF.format(cf.ifmt,p.v,_ssfopts); | ||||
| 					if(opts.cellNF) p.z = SSF._table[cf.ifmt]; | ||||
| 				} catch(e) { } | ||||
| 				} catch(e) { if(opts.WTF) throw e; } | ||||
| 				s[encode_cell({c:val[0].c,r:row.r})] = p; | ||||
| 				break; // TODO
 | ||||
| 
 | ||||
|  | ||||
| @ -6,10 +6,11 @@ function fixopts(opts) { | ||||
| 
 | ||||
| 		['sheetStubs', false], /* emit empty cells */ | ||||
| 
 | ||||
| 		['bookDeps', false], /* parse calculation chains */ | ||||
| 		['bookSheets', false], /* only try to get sheet names (no Sheets) */ | ||||
| 		['bookProps', false], /* only try to get properties (no Sheets) */ | ||||
| 
 | ||||
| 		['WTF', false] /* WTF mode (do not use) */ | ||||
| 		['WTF', false] /* WTF mode (throws errors) */ | ||||
| 	]; | ||||
| 	defaults.forEach(function(d) { if(typeof opts[d[0]] === 'undefined') opts[d[0]] = d[1]; }); | ||||
| } | ||||
|  | ||||
| @ -4,8 +4,9 @@ function parseZip(zip, opts) { | ||||
| 	reset_cp(); | ||||
| 	var entries = Object.keys(zip.files); | ||||
| 	var keys = entries.filter(function(x){return x.substr(-1) != '/';}).sort(); | ||||
| 	var dir = parseCT(getdata(getzipfile(zip, '[Content_Types].xml'))); | ||||
| 	var dir = parseCT(getzipdata(zip, '[Content_Types].xml')); | ||||
| 	var xlsb = false; | ||||
| 	var sheets; | ||||
| 	if(dir.workbooks.length === 0) { | ||||
| 		var binname = "xl/workbook.bin"; | ||||
| 		if(!getzipfile(zip,binname)) throw new Error("Could not find workbook entry"); | ||||
| @ -15,42 +16,43 @@ function parseZip(zip, opts) { | ||||
| 
 | ||||
| 	if(!opts.bookSheets && !opts.bookProps) { | ||||
| 		strs = {}; | ||||
| 		if(dir.sst) strs=parse_sst(getdata(getzipfile(zip, dir.sst.replace(/^\//,''))), dir.sst, opts); | ||||
| 		if(dir.sst) strs=parse_sst(getzipdata(zip, dir.sst.replace(/^\//,'')), dir.sst, opts); | ||||
| 
 | ||||
| 		styles = {}; | ||||
| 		if(dir.style) styles = parse_sty(getdata(getzipfile(zip, dir.style.replace(/^\//,''))),dir.style); | ||||
| 		if(dir.style) styles = parse_sty(getzipdata(zip, dir.style.replace(/^\//,'')),dir.style); | ||||
| 	} | ||||
| 
 | ||||
| 	var wb = parse_wb(getdata(getzipfile(zip, dir.workbooks[0].replace(/^\//,''))), dir.workbooks[0], opts); | ||||
| 	var wb = parse_wb(getzipdata(zip, dir.workbooks[0].replace(/^\//,'')), dir.workbooks[0], opts); | ||||
| 
 | ||||
| 	var props = {}, propdata = ""; | ||||
| 	try { | ||||
| 		propdata = dir.coreprops.length !== 0 ? getdata(getzipfile(zip, dir.coreprops[0].replace(/^\//,''))) : ""; | ||||
| 	propdata += dir.extprops.length !== 0 ? getdata(getzipfile(zip, dir.extprops[0].replace(/^\//,''))) : ""; | ||||
| 		propdata = dir.coreprops.length !== 0 ? getzipdata(zip, dir.coreprops[0].replace(/^\//,'')) : ""; | ||||
| 		propdata += dir.extprops.length !== 0 ? getzipdata(zip, dir.extprops[0].replace(/^\//,'')) : ""; | ||||
| 		props = propdata !== "" ? parseProps(propdata) : {}; | ||||
| 	} catch(e) { } | ||||
| 
 | ||||
| 	var custprops = {}; | ||||
| 	if(!opts.bookSheets || opts.bookProps) { | ||||
| 		if (dir.custprops.length !== 0) try { | ||||
| 			propdata = getdata(getzipfile(zip, dir.custprops[0].replace(/^\//,''))); | ||||
| 			custprops = parseCustomProps(propdata); | ||||
| 		} catch(e) {/*console.error(e);*/} | ||||
| 		if (dir.custprops.length !== 0) { | ||||
| 			propdata = getzipdata(zip, dir.custprops[0].replace(/^\//,''), true); | ||||
| 			if(propdata) custprops = parseCustomProps(propdata); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var out = {}; | ||||
| 	if(opts.bookSheets || opts.bookProps) { | ||||
| 		var sheets; | ||||
| 		if(props.Worksheets && props.SheetNames.length > 0) sheets=props.SheetNames; | ||||
| 		else if(wb.Sheets) sheets = wb.Sheets.map(function(x){ return x.name; }); | ||||
| 		if(opts.bookProps) { out.Props = props; out.Custprops = custprops; } | ||||
| 		if(typeof sheets !== 'undefined') out.SheetNames = sheets; | ||||
| 		if(opts.bookSheets ? out.SheetNames : opts.bookProps) return out; | ||||
| 	} | ||||
| 	sheets = {}; | ||||
| 
 | ||||
| 	var deps = {}; | ||||
| 	if(dir.calcchain) deps=parseDeps(getdata(getzipfile(zip, dir.calcchain.replace(/^\//,'')))); | ||||
| 	var sheets = {}, i=0; | ||||
| 	if(opts.bookDeps && dir.calcchain) deps=parseDeps(getzipdata(zip, dir.calcchain.replace(/^\//,''))); | ||||
| 
 | ||||
| 	var i=0; | ||||
| 	var sheetRels = {}; | ||||
| 	var path, relsPath; | ||||
| 	if(!props.Worksheets) { | ||||
| @ -62,12 +64,12 @@ function parseZip(zip, opts) { | ||||
| 			props.SheetNames[j] = wbsheets[j].name; | ||||
| 		} | ||||
| 		for(i = 0; i != props.Worksheets; ++i) { | ||||
| 			try { /* TODO: remove these guards */ | ||||
| 			try { | ||||
| 				path = 'xl/worksheets/sheet' + (i+1) + (xlsb?'.bin':'.xml'); | ||||
| 				relsPath = path.replace(/^(.*)(\/)([^\/]*)$/, "$1/_rels/$3.rels"); | ||||
| 				sheets[props.SheetNames[i]]=parse_ws(getdata(getzipfile(zip, path)),path,opts); | ||||
| 				sheetRels[props.SheetNames[i]]=parseRels(getdata(getzipfile(zip, relsPath)), path); | ||||
| 			} catch(e) {} | ||||
| 				sheets[props.SheetNames[i]]=parse_ws(getzipdata(zip, path),path,opts); | ||||
| 				sheetRels[props.SheetNames[i]]=parseRels(getzipdata(zip, relsPath, true), path); | ||||
| 			} catch(e) { if(opts.WTF) throw e; } | ||||
| 		} | ||||
| 	} else { | ||||
| 		for(i = 0; i != props.Worksheets; ++i) { | ||||
| @ -75,9 +77,9 @@ function parseZip(zip, opts) { | ||||
| 				//var path = dir.sheets[i].replace(/^\//,'');
 | ||||
| 				path = 'xl/worksheets/sheet' + (i+1) + (xlsb?'.bin':'.xml'); | ||||
| 				relsPath = path.replace(/^(.*)(\/)([^\/]*)$/, "$1/_rels/$3.rels"); | ||||
| 				sheets[props.SheetNames[i]]=parse_ws(getdata(getzipfile(zip, path)),path,opts); | ||||
| 				sheetRels[props.SheetNames[i]]=parseRels(getdata(getzipfile(zip, relsPath)), path); | ||||
| 			} catch(e) {/*console.error(e);*/} | ||||
| 				sheets[props.SheetNames[i]]=parse_ws(getzipdata(zip, path),path,opts); | ||||
| 				sheetRels[props.SheetNames[i]]=parseRels(getzipdata(zip, relsPath, true), path); | ||||
| 			} catch(e) { if(opts.WTF) throw e; } | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
| 	"name": "xlsx", | ||||
| 	"version": "0.5.6", | ||||
| 	"version": "0.5.7", | ||||
| 	"author": "sheetjs", | ||||
| 	"description": "XLSB / XLSX / XLSM parser", | ||||
| 	"keywords": [ "xlsx", "xlsb", "xlsm", "office", "excel", "spreadsheet" ], | ||||
| @ -11,6 +11,7 @@ | ||||
| 	"dependencies": { | ||||
| 		"ssf":"~0.5.6", | ||||
| 		"codepage":"", | ||||
| 		"cfb":"", | ||||
| 		"jszip":"~2.1.0", | ||||
| 		"commander":"" | ||||
| 	}, | ||||
|  | ||||
							
								
								
									
										236
									
								
								test.js
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										236
									
								
								test.js
									
									
									
									
									
								
							| @ -1,7 +1,10 @@ | ||||
| /* vim: set ts=2: */ | ||||
| var XLSX; | ||||
| var fs = require('fs'), assert = require('assert'); | ||||
| describe('source',function(){ it('should load', function(){ XLSX = require('./'); });}); | ||||
| describe('source',function(){it('should load',function(){XLSX=require('./');});}); | ||||
| 
 | ||||
| var opts = {}; | ||||
| if(process.env.WTF) opts.WTF = true; | ||||
| 
 | ||||
| var ex = [".xlsb", ".xlsm", ".xlsx"]; | ||||
| if(process.env.FMTS) ex=process.env.FMTS.split(":").map(function(x){return x[0]==="."?x:"."+x;}); | ||||
| @ -15,9 +18,11 @@ function fixsheetname(x) { return x.substr(0,31); } | ||||
| 
 | ||||
| function normalizecsv(x) { return x.replace(/\t/g,",").replace(/#{255}/g,"").replace(/"/g,"").replace(/[\n\r]+/g,"\n").replace(/\n*$/,""); } | ||||
| 
 | ||||
| var dir = "./test_files/"; | ||||
| 
 | ||||
| function parsetest(x, wb) { | ||||
| 	describe(x + ' should have all bits', function() { | ||||
| 		var sname = './test_files/2011/' + x + '.sheetnames'; | ||||
| 		var sname = dir + '2011/' + x + '.sheetnames'; | ||||
| 		it('should have all sheets', function() { | ||||
| 			wb.SheetNames.forEach(function(y) { assert(wb.Sheets[y], 'bad sheet ' + y); }); | ||||
| 		}); | ||||
| @ -50,7 +55,7 @@ function parsetest(x, wb) { | ||||
| 	}); | ||||
| 	describe(x + ' should generate correct output', function() { | ||||
| 		wb.SheetNames.forEach(function(ws, i) { | ||||
| 			var name = ('./test_files/' + x + '.' + i + '.csv'); | ||||
| 			var name = (dir + x + '.' + i + '.csv'); | ||||
| 			it('#' + i + ' (' + ws + ')', fs.existsSync(name) ? function() { | ||||
| 				var file = fs.readFileSync(name, 'utf-8'); | ||||
| 				var csv = XLSX.utils.make_csv(wb.Sheets[ws]); | ||||
| @ -63,156 +68,169 @@ function parsetest(x, wb) { | ||||
| describe('should parse test files', function() { | ||||
| 	files.forEach(function(x) { | ||||
| 		it(x, x.substr(-8) == ".pending" ? null : function() { | ||||
| 			var wb = XLSX.readFile('./test_files/' + x); | ||||
| 			var wb = XLSX.readFile(dir + x, opts); | ||||
| 			parsetest(x, wb); | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| describe('should have comment as part of cell\'s properties', function(){ | ||||
| 	var ws; | ||||
| 	before(function() { | ||||
| 		XLSX = require('./'); | ||||
| 		var wb = XLSX.readFile('./test_files/apachepoi_SimpleWithComments.xlsx'); | ||||
| 		var sheetName = 'Sheet1'; | ||||
| 		ws = wb.Sheets[sheetName]; | ||||
| 	}); | ||||
| 	it('Parse comments.xml and insert into cell',function(){ | ||||
| 		assert.equal(ws.B1.c.length, 1,"must have 1 comment"); | ||||
| 		assert.equal(ws.B1.c[0].t, "Yegor Kozlov:\r\nfirst cell", "must have the concatenated texts"); | ||||
| 		assert.equal(ws.B1.c[0].h, '<span style="font-weight: bold;">Yegor Kozlov:</span><span style=""><br/>first cell</span>', "must have the html representation"); | ||||
| 		assert.equal(ws.B1.c[0].r, '<r><rPr><b/><sz val="8"/><color indexed="81"/><rFont val="Tahoma"/></rPr><t>Yegor Kozlov:</t></r><r><rPr><sz val="8"/><color indexed="81"/><rFont val="Tahoma"/></rPr><t xml:space="preserve">\r\nfirst cell</t></r>', "must have the rich text representation"); | ||||
| 		assert.equal(ws.B1.c[0].a, "Yegor Kozlov","must have the same author"); | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| describe('options', function() { | ||||
| 	var html_cell_types = ['s']; | ||||
| 	before(function() { | ||||
| 		XLSX = require('./'); | ||||
| 	}); | ||||
| 	it('should generate HTML by default', function() { | ||||
| 		var wb = XLSX.readFile('./test_files/comments_stress_test.xlsx'); | ||||
| 		var ws = wb.Sheets.Sheet1; | ||||
| 		Object.keys(ws).forEach(function(addr) { | ||||
| 			if(addr[0] === "!" || !ws.hasOwnProperty(addr)) return; | ||||
| 			assert(html_cell_types.indexOf(ws[addr].t) === -1 || ws[addr].h); | ||||
| 		}); | ||||
| 	}); | ||||
| 	it('should not generate HTML when requested', function() { | ||||
| 		var wb = XLSX.readFile('./test_files/comments_stress_test.xlsx', {cellHTML: false}); | ||||
| 		var ws = wb.Sheets.Sheet1; | ||||
| 		Object.keys(ws).forEach(function(addr) { | ||||
| 			if(addr[0] === "!" || !ws.hasOwnProperty(addr)) return; | ||||
| 			assert(typeof ws[addr].h === 'undefined'); | ||||
| 		}); | ||||
| 	}); | ||||
| 	it('should generate formulae by default', function() { | ||||
| 		var wb = XLSX.readFile('./test_files/formula_stress_test.xlsb'); | ||||
| 		var found = false; | ||||
| 		wb.SheetNames.forEach(function(s) { | ||||
| 			var ws = wb.Sheets[s]; | ||||
| 	describe('cell', function() { | ||||
| 		it('should generate HTML by default', function() { | ||||
| 			var wb = XLSX.readFile(dir + 'comments_stress_test.xlsx'); | ||||
| 			var ws = wb.Sheets.Sheet1; | ||||
| 			Object.keys(ws).forEach(function(addr) { | ||||
| 				if(addr[0] === "!" || !ws.hasOwnProperty(addr)) return; | ||||
| 				if(typeof ws[addr].f !== 'undefined') return found = true;  | ||||
| 				assert(html_cell_types.indexOf(ws[addr].t) === -1 || ws[addr].h); | ||||
| 			}); | ||||
| 		}); | ||||
| 		assert(found); | ||||
| 	}); | ||||
| 	it('should not generate formulae when requested', function() { | ||||
| 		var wb = XLSX.readFile('./test_files/formula_stress_test.xlsb', {cellFormula: false}); | ||||
| 		wb.SheetNames.forEach(function(s) { | ||||
| 			var ws = wb.Sheets[s]; | ||||
| 		it('should not generate HTML when requested', function() { | ||||
| 			var wb = XLSX.readFile(dir+'comments_stress_test.xlsx', {cellHTML:false}); | ||||
| 			var ws = wb.Sheets.Sheet1; | ||||
| 			Object.keys(ws).forEach(function(addr) { | ||||
| 				if(addr[0] === "!" || !ws.hasOwnProperty(addr)) return; | ||||
| 				assert(typeof ws[addr].f === 'undefined'); | ||||
| 				assert(typeof ws[addr].h === 'undefined'); | ||||
| 			}); | ||||
| 		}); | ||||
| 		it('should generate formulae by default', function() { | ||||
| 			var wb = XLSX.readFile(dir + 'formula_stress_test.xlsb'); | ||||
| 			var found = false; | ||||
| 			wb.SheetNames.forEach(function(s) { | ||||
| 				var ws = wb.Sheets[s]; | ||||
| 				Object.keys(ws).forEach(function(addr) { | ||||
| 					if(addr[0] === "!" || !ws.hasOwnProperty(addr)) return; | ||||
| 					if(typeof ws[addr].f !== 'undefined') return found = true; | ||||
| 				}); | ||||
| 			}); | ||||
| 			assert(found); | ||||
| 		}); | ||||
| 		it('should not generate formulae when requested', function() { | ||||
| 			var wb =XLSX.readFile(dir+'formula_stress_test.xlsb',{cellFormula:false}); | ||||
| 			wb.SheetNames.forEach(function(s) { | ||||
| 				var ws = wb.Sheets[s]; | ||||
| 				Object.keys(ws).forEach(function(addr) { | ||||
| 					if(addr[0] === "!" || !ws.hasOwnProperty(addr)) return; | ||||
| 					assert(typeof ws[addr].f === 'undefined'); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}); | ||||
| 		it('should not generate number formats by default', function() { | ||||
| 			var wb = XLSX.readFile(dir+'number_format.xlsm'); | ||||
| 			wb.SheetNames.forEach(function(s) { | ||||
| 				var ws = wb.Sheets[s]; | ||||
| 				Object.keys(ws).forEach(function(addr) { | ||||
| 					if(addr[0] === "!" || !ws.hasOwnProperty(addr)) return; | ||||
| 					assert(typeof ws[addr].z === 'undefined'); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}); | ||||
| 		it('should generate number formats when requested', function() { | ||||
| 			var wb = XLSX.readFile(dir+'number_format.xlsm', {cellNF: true}); | ||||
| 			wb.SheetNames.forEach(function(s) { | ||||
| 				var ws = wb.Sheets[s]; | ||||
| 				Object.keys(ws).forEach(function(addr) { | ||||
| 					if(addr[0] === "!" || !ws.hasOwnProperty(addr)) return; | ||||
| 					assert(typeof ws[addr].t!== 'n' || typeof ws[addr].z !== 'undefined'); | ||||
| 				}); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
| 	it('should not generate sheet stubs by default', function() { | ||||
| 		var wb = XLSX.readFile('./test_files/merge_cells.xlsx'); | ||||
| 		assert.throws(function() { wb.Sheets.Merge.A2.v; }); | ||||
| 	}); | ||||
| 	it('should generate sheet stubs when requested', function() { | ||||
| 		var wb = XLSX.readFile('./test_files/merge_cells.xlsx', {sheetStubs:true}); | ||||
| 		assert(typeof wb.Sheets.Merge.A2.t !== 'undefined'); | ||||
| 	}); | ||||
| 	it('should not generate number formats by default', function() { | ||||
| 		var wb = XLSX.readFile('./test_files/number_format.xlsm'); | ||||
| 		wb.SheetNames.forEach(function(s) { | ||||
| 			var ws = wb.Sheets[s]; | ||||
| 			Object.keys(ws).forEach(function(addr) { | ||||
| 				if(addr[0] === "!" || !ws.hasOwnProperty(addr)) return; | ||||
| 				assert(typeof ws[addr].z === 'undefined'); | ||||
| 			}); | ||||
| 	describe('sheet', function() { | ||||
| 		it('should not generate sheet stubs by default', function() { | ||||
| 			var wb = XLSX.readFile(dir+'merge_cells.xlsx'); | ||||
| 			assert.throws(function() { wb.Sheets.Merge.A2.v; }); | ||||
| 		}); | ||||
| 	}); | ||||
| 	it('should generate number formats when requested', function() { | ||||
| 		var wb = XLSX.readFile('./test_files/number_format.xlsm', {cellNF: true}); | ||||
| 		wb.SheetNames.forEach(function(s) { | ||||
| 			var ws = wb.Sheets[s]; | ||||
| 			Object.keys(ws).forEach(function(addr) { | ||||
| 				if(addr[0] === "!" || !ws.hasOwnProperty(addr)) return; | ||||
| 				assert(typeof ws[addr].t !== 'n' || typeof ws[addr].z !== 'undefined'); | ||||
| 			}); | ||||
| 		it('should generate sheet stubs when requested', function() { | ||||
| 			var wb = XLSX.readFile(dir+'merge_cells.xlsx', {sheetStubs:true}); | ||||
| 			assert(typeof wb.Sheets.Merge.A2.t !== 'undefined'); | ||||
| 		}); | ||||
| 	}); | ||||
| 	describe('book', function() { | ||||
| 		it('bookSheets should not generate sheets', function() { | ||||
| 			var wb = XLSX.readFile('./test_files/merge_cells.xlsx', {bookSheets:true}); | ||||
| 			var wb = XLSX.readFile(dir+'merge_cells.xlsx', {bookSheets:true}); | ||||
| 			assert(typeof wb.Sheets === 'undefined'); | ||||
| 		}); | ||||
| 		it('bookProps should not generate sheets', function() { | ||||
| 			var wb = XLSX.readFile('./test_files/number_format.xlsb', {bookProps:true}); | ||||
| 			var wb = XLSX.readFile(dir+'number_format.xlsb', {bookProps:true}); | ||||
| 			assert(typeof wb.Sheets === 'undefined'); | ||||
| 		}); | ||||
| 		it('bookProps && bookSheets should not generate sheets', function() { | ||||
| 			var wb = XLSX.readFile('./test_files/LONumbers.xlsx', {bookProps:true, bookSheets:true}); | ||||
| 			var wb = XLSX.readFile(dir+'LONumbers.xlsx', {bookProps:true, bookSheets:true}); | ||||
| 			assert(typeof wb.Sheets === 'undefined'); | ||||
| 		}); | ||||
| 		it('should not generate deps by default', function() { | ||||
| 			var wb = XLSX.readFile(dir+'formula_stress_test.xlsx', {bookDeps:true}); | ||||
| 			assert(typeof wb.Deps !== 'undefined' && wb.Deps.length > 0); | ||||
| 		}); | ||||
| 		it('bookDeps should generate deps', function() { | ||||
| 			var wb = XLSX.readFile(dir+'formula_stress_test.xlsx'); | ||||
| 			assert(typeof wb.Deps === 'undefined' || !(wb.Deps.length>0)); | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| describe('input formats', function() { | ||||
| 	it('should read binary strings', function() { | ||||
| 		XLSX.read(fs.readFileSync('./test_files/comments_stress_test.xlsb', 'binary'), {type: 'binary'});  | ||||
| 		XLSX.read(fs.readFileSync('./test_files/comments_stress_test.xlsx', 'binary'), {type: 'binary'});  | ||||
| 		XLSX.read(fs.readFileSync(dir+'comments_stress_test.xlsb', 'binary'), {type: 'binary'}); | ||||
| 		XLSX.read(fs.readFileSync(dir+'comments_stress_test.xlsx', 'binary'), {type: 'binary'}); | ||||
| 	}); | ||||
| 	it('should read base64 strings', function() { | ||||
| 		XLSX.read(fs.readFileSync('./test_files/comments_stress_test.xlsb', 'base64'), {type: 'base64'});  | ||||
| 		XLSX.read(fs.readFileSync('./test_files/comments_stress_test.xlsx', 'base64'), {type: 'base64'});  | ||||
| 		XLSX.read(fs.readFileSync(dir+'comments_stress_test.xlsb', 'base64'), {type: 'base64'}); | ||||
| 		XLSX.read(fs.readFileSync(dir+'comments_stress_test.xlsx', 'base64'), {type: 'base64'}); | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| describe('should have core properties and custom properties parsed', function() { | ||||
| 	var wb; | ||||
| 	before(function() { | ||||
| 		XLSX = require('./'); | ||||
| 		wb = XLSX.readFile('./test_files/custom_properties.xlsx'); | ||||
| describe('features', function() { | ||||
| 	describe('should have comment as part of cell properties', function(){ | ||||
| 		var ws; | ||||
| 		before(function() { | ||||
| 			XLSX = require('./'); | ||||
| 			var wb = XLSX.readFile(dir+'apachepoi_SimpleWithComments.xlsx'); | ||||
| 			var sheetName = 'Sheet1'; | ||||
| 			ws = wb.Sheets[sheetName]; | ||||
| 		}); | ||||
| 		it('Parse comments.xml and insert into cell',function(){ | ||||
| 			assert.equal(ws.B1.c.length, 1,"must have 1 comment"); | ||||
| 			assert.equal(ws.B1.c[0].t, "Yegor Kozlov:\r\nfirst cell", "must have the concatenated texts"); | ||||
| 			assert.equal(ws.B1.c[0].h, '<span style="font-weight: bold;">Yegor Kozlov:</span><span style=""><br/>first cell</span>', "must have the html representation"); | ||||
| 			assert.equal(ws.B1.c[0].r, '<r><rPr><b/><sz val="8"/><color indexed="81"/><rFont val="Tahoma"/></rPr><t>Yegor Kozlov:</t></r><r><rPr><sz val="8"/><color indexed="81"/><rFont val="Tahoma"/></rPr><t xml:space="preserve">\r\nfirst cell</t></r>', "must have the rich text representation"); | ||||
| 			assert.equal(ws.B1.c[0].a, "Yegor Kozlov","must have the same author"); | ||||
| 		}); | ||||
| 	}); | ||||
| 	it('Must have read the core properties', function() { | ||||
| 		assert.equal(wb.Props.Company, 'Vector Inc'); | ||||
| 		assert.equal(wb.Props.Creator, 'Pony Foo'); | ||||
| 	}); | ||||
| 	it('Must have read the custom properties', function() { | ||||
| 		assert.equal(wb.Custprops['I am a boolean'], true); | ||||
| 		assert.equal(wb.Custprops['Date completed'], '1967-03-09T16:30:00Z'); | ||||
| 		assert.equal(wb.Custprops.Status, 2); | ||||
| 		assert.equal(wb.Custprops.Counter, -3.14); | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| describe.skip('should parse a sheet with a d date cell', function() { | ||||
| 	var wb, ws; | ||||
| 	before(function() { | ||||
| 		XLSX = require('./'); | ||||
| 		wb = XLSX.readFile('./test_files/xlsx-stream-d-date-cell.xlsx'); | ||||
| 		// wb = XLSX.readFile('./test_files/xlsx-stream-array.xlsx');
 | ||||
| 		var sheetName = 'Sheet1'; | ||||
| 		ws = wb.Sheets[sheetName]; | ||||
| 	describe('should have core properties and custom properties parsed', function() { | ||||
| 		var wb; | ||||
| 		before(function() { | ||||
| 			XLSX = require('./'); | ||||
| 			wb = XLSX.readFile(dir+'custom_properties.xlsx'); | ||||
| 		}); | ||||
| 		it('Must have read the core properties', function() { | ||||
| 			assert.equal(wb.Props.Company, 'Vector Inc'); | ||||
| 			assert.equal(wb.Props.Creator, 'Pony Foo'); | ||||
| 		}); | ||||
| 		it('Must have read the custom properties', function() { | ||||
| 			assert.equal(wb.Custprops['I am a boolean'], true); | ||||
| 			assert.equal(wb.Custprops['Date completed'], '1967-03-09T16:30:00Z'); | ||||
| 			assert.equal(wb.Custprops.Status, 2); | ||||
| 			assert.equal(wb.Custprops.Counter, -3.14); | ||||
| 		}); | ||||
| 	}); | ||||
| 	it('Must have read the date', function() { | ||||
| 		var sheet = XLSX.utils.sheet_to_row_object_array(ws); | ||||
| 		assert.equal(sheet[3]['てすと'], '2/14/14'); | ||||
| 
 | ||||
| 	describe('should parse cells with date type', function() { | ||||
| 		var wb, ws; | ||||
| 		before(function() { | ||||
| 			XLSX = require('./'); | ||||
| 			wb = XLSX.readFile(dir+'xlsx-stream-d-date-cell.xlsx'); | ||||
| 			var sheetName = 'Sheet1'; | ||||
| 			ws = wb.Sheets[sheetName]; | ||||
| 		}); | ||||
| 		it('Must have read the date', function() { | ||||
| 			var sheet = XLSX.utils.sheet_to_row_object_array(ws); | ||||
| 			assert.equal(sheet[3]['てすと'], '2/14/14'); | ||||
| 		}); | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
| @ -1 +1 @@ | ||||
| Subproject commit 255a962b8f9a0f07ac75281bded02fea93e00b16 | ||||
| Subproject commit 09abbe1b28237722d93a8510f42c71094c1244fd | ||||
							
								
								
									
										71
									
								
								xlsx.js
									
									
									
									
									
								
							
							
								
								
								
								
								
									
									
								
							
						
						
									
										71
									
								
								xlsx.js
									
									
									
									
									
								
							| @ -424,7 +424,7 @@ SSF.load_table = function(tbl) { for(var i=0; i!=0x0188; ++i) if(tbl[i]) SSF.loa | ||||
| make_ssf(SSF); | ||||
| var XLSX = {}; | ||||
| (function(XLSX){ | ||||
| XLSX.version = '0.5.6'; | ||||
| XLSX.version = '0.5.7'; | ||||
| var current_codepage, current_cptable, cptable; | ||||
| if(typeof module !== "undefined" && typeof require !== 'undefined') { | ||||
| 	if(typeof cptable === 'undefined') cptable = require('codepage'); | ||||
| @ -455,6 +455,12 @@ function getzipfile(zip, file) { | ||||
| 	throw new Error("Cannot find file " + file + " in zip"); | ||||
| } | ||||
| 
 | ||||
| function getzipdata(zip, file, safe) { | ||||
| 	if(!safe) return getdata(getzipfile(zip, file)); | ||||
| 	if(!file) return null; | ||||
| 	try { return getzipdata(zip, file); } catch(e) { return null; } | ||||
| } | ||||
| 
 | ||||
| var _fs, jszip; | ||||
| if(typeof JSZip !== 'undefined') jszip = JSZip; | ||||
| if (typeof exports !== 'undefined') { | ||||
| @ -1239,7 +1245,8 @@ function parse_comments_xml(data, opts) { | ||||
| function parse_comments(zip, dirComments, sheets, sheetRels, opts) { | ||||
| 	for(var i = 0; i != dirComments.length; ++i) { | ||||
| 		var canonicalpath=dirComments[i]; | ||||
| 		var comments=parse_comments_xml(getdata(getzipfile(zip, canonicalpath.replace(/^\//,''))), opts); | ||||
| 		var comments=parse_comments_xml(getzipdata(zip, canonicalpath.replace(/^\//,''), true), opts); | ||||
| 		if(!comments || !comments.length) return; | ||||
| 		// find the sheets targeted by these comments
 | ||||
| 		var sheetNames = Object.keys(sheets); | ||||
| 		for(var j = 0; j != sheetNames.length; ++j) { | ||||
| @ -1329,6 +1336,7 @@ function parse_ws_xml(data, opts) { | ||||
| 			else p.t = (cell.t ? cell.t : "n"); // default is "n" in schema
 | ||||
| 			if(refguess.s.c > idx) refguess.s.c = idx; | ||||
| 			if(refguess.e.c < idx) refguess.e.c = idx; | ||||
| 			/* 18.18.11 t ST_CellType */ | ||||
| 			switch(p.t) { | ||||
| 				case 'n': p.v = parseFloat(p.v); break; | ||||
| 				case 's': { | ||||
| @ -1343,20 +1351,14 @@ function parse_ws_xml(data, opts) { | ||||
| 					is = is ? parse_si(is[1]) : {t:"",r:""}; | ||||
| 					p.t = 'str'; p.v = is.t; | ||||
| 					break; // inline string
 | ||||
| 				case 'b': | ||||
| 					switch(p.v) { | ||||
| 						case '0': case 'FALSE': case "false": case false: p.v=false; break; | ||||
| 						case '1': case 'TRUE':  case "true":  case true:  p.v=true;  break; | ||||
| 						default: throw "Unrecognized boolean: " + p.v; | ||||
| 					} break; | ||||
| 				case 'd': | ||||
| 				case 'b': if(typeof p.v !== 'boolean') p.v = parsexmlbool(p.v); break; | ||||
| 				case 'd': /* TODO: date1904 logic */ | ||||
| 					var epoch = Date.parse(p.v); | ||||
| 					p.v = (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000); | ||||
| 					p.t = 'n'; | ||||
| 					break; | ||||
| 				/* in case of error, stick value in .raw */ | ||||
| 				case 'e': p.raw = RBErr[p.v]; break; | ||||
| 				default: throw "Unrecognized cell type: " + p.t; | ||||
| 			} | ||||
| 
 | ||||
| 			/* formatting */ | ||||
| @ -1368,7 +1370,7 @@ function parse_ws_xml(data, opts) { | ||||
| 			try { | ||||
| 				p.w = SSF.format(fmtid,p.v,_ssfopts); | ||||
| 				if(opts.cellNF) p.z = SSF._table[fmtid]; | ||||
| 			} catch(e) { } | ||||
| 			} catch(e) { if(opts.WTF) throw e; } | ||||
| 			s[cell.r] = p; | ||||
| 		}); | ||||
| 	}); | ||||
| @ -1530,7 +1532,7 @@ var parse_ws_bin = function(data, opts) { | ||||
| 				if((cf = styles.CellXf[val[0].iStyleRef])) try { | ||||
| 					p.w = SSF.format(cf.ifmt,p.v,_ssfopts); | ||||
| 					if(opts.cellNF) p.z = SSF._table[cf.ifmt]; | ||||
| 				} catch(e) { } | ||||
| 				} catch(e) { if(opts.WTF) throw e; } | ||||
| 				s[encode_cell({c:val[0].c,r:row.r})] = p; | ||||
| 				break; // TODO
 | ||||
| 
 | ||||
| @ -2674,10 +2676,11 @@ function fixopts(opts) { | ||||
| 
 | ||||
| 		['sheetStubs', false], /* emit empty cells */ | ||||
| 
 | ||||
| 		['bookDeps', false], /* parse calculation chains */ | ||||
| 		['bookSheets', false], /* only try to get sheet names (no Sheets) */ | ||||
| 		['bookProps', false], /* only try to get properties (no Sheets) */ | ||||
| 
 | ||||
| 		['WTF', false] /* WTF mode (do not use) */ | ||||
| 		['WTF', false] /* WTF mode (throws errors) */ | ||||
| 	]; | ||||
| 	defaults.forEach(function(d) { if(typeof opts[d[0]] === 'undefined') opts[d[0]] = d[1]; }); | ||||
| } | ||||
| @ -2687,8 +2690,9 @@ function parseZip(zip, opts) { | ||||
| 	reset_cp(); | ||||
| 	var entries = Object.keys(zip.files); | ||||
| 	var keys = entries.filter(function(x){return x.substr(-1) != '/';}).sort(); | ||||
| 	var dir = parseCT(getdata(getzipfile(zip, '[Content_Types].xml'))); | ||||
| 	var dir = parseCT(getzipdata(zip, '[Content_Types].xml')); | ||||
| 	var xlsb = false; | ||||
| 	var sheets; | ||||
| 	if(dir.workbooks.length === 0) { | ||||
| 		var binname = "xl/workbook.bin"; | ||||
| 		if(!getzipfile(zip,binname)) throw new Error("Could not find workbook entry"); | ||||
| @ -2698,42 +2702,43 @@ function parseZip(zip, opts) { | ||||
| 
 | ||||
| 	if(!opts.bookSheets && !opts.bookProps) { | ||||
| 		strs = {}; | ||||
| 		if(dir.sst) strs=parse_sst(getdata(getzipfile(zip, dir.sst.replace(/^\//,''))), dir.sst, opts); | ||||
| 		if(dir.sst) strs=parse_sst(getzipdata(zip, dir.sst.replace(/^\//,'')), dir.sst, opts); | ||||
| 
 | ||||
| 		styles = {}; | ||||
| 		if(dir.style) styles = parse_sty(getdata(getzipfile(zip, dir.style.replace(/^\//,''))),dir.style); | ||||
| 		if(dir.style) styles = parse_sty(getzipdata(zip, dir.style.replace(/^\//,'')),dir.style); | ||||
| 	} | ||||
| 
 | ||||
| 	var wb = parse_wb(getdata(getzipfile(zip, dir.workbooks[0].replace(/^\//,''))), dir.workbooks[0], opts); | ||||
| 	var wb = parse_wb(getzipdata(zip, dir.workbooks[0].replace(/^\//,'')), dir.workbooks[0], opts); | ||||
| 
 | ||||
| 	var props = {}, propdata = ""; | ||||
| 	try { | ||||
| 		propdata = dir.coreprops.length !== 0 ? getdata(getzipfile(zip, dir.coreprops[0].replace(/^\//,''))) : ""; | ||||
| 	propdata += dir.extprops.length !== 0 ? getdata(getzipfile(zip, dir.extprops[0].replace(/^\//,''))) : ""; | ||||
| 		propdata = dir.coreprops.length !== 0 ? getzipdata(zip, dir.coreprops[0].replace(/^\//,'')) : ""; | ||||
| 		propdata += dir.extprops.length !== 0 ? getzipdata(zip, dir.extprops[0].replace(/^\//,'')) : ""; | ||||
| 		props = propdata !== "" ? parseProps(propdata) : {}; | ||||
| 	} catch(e) { } | ||||
| 
 | ||||
| 	var custprops = {}; | ||||
| 	if(!opts.bookSheets || opts.bookProps) { | ||||
| 		if (dir.custprops.length !== 0) try { | ||||
| 			propdata = getdata(getzipfile(zip, dir.custprops[0].replace(/^\//,''))); | ||||
| 			custprops = parseCustomProps(propdata); | ||||
| 		} catch(e) {/*console.error(e);*/} | ||||
| 		if (dir.custprops.length !== 0) { | ||||
| 			propdata = getzipdata(zip, dir.custprops[0].replace(/^\//,''), true); | ||||
| 			if(propdata) custprops = parseCustomProps(propdata); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	var out = {}; | ||||
| 	if(opts.bookSheets || opts.bookProps) { | ||||
| 		var sheets; | ||||
| 		if(props.Worksheets && props.SheetNames.length > 0) sheets=props.SheetNames; | ||||
| 		else if(wb.Sheets) sheets = wb.Sheets.map(function(x){ return x.name; }); | ||||
| 		if(opts.bookProps) { out.Props = props; out.Custprops = custprops; } | ||||
| 		if(typeof sheets !== 'undefined') out.SheetNames = sheets; | ||||
| 		if(opts.bookSheets ? out.SheetNames : opts.bookProps) return out; | ||||
| 	} | ||||
| 	sheets = {}; | ||||
| 
 | ||||
| 	var deps = {}; | ||||
| 	if(dir.calcchain) deps=parseDeps(getdata(getzipfile(zip, dir.calcchain.replace(/^\//,'')))); | ||||
| 	var sheets = {}, i=0; | ||||
| 	if(opts.bookDeps && dir.calcchain) deps=parseDeps(getzipdata(zip, dir.calcchain.replace(/^\//,''))); | ||||
| 
 | ||||
| 	var i=0; | ||||
| 	var sheetRels = {}; | ||||
| 	var path, relsPath; | ||||
| 	if(!props.Worksheets) { | ||||
| @ -2745,12 +2750,12 @@ function parseZip(zip, opts) { | ||||
| 			props.SheetNames[j] = wbsheets[j].name; | ||||
| 		} | ||||
| 		for(i = 0; i != props.Worksheets; ++i) { | ||||
| 			try { /* TODO: remove these guards */ | ||||
| 			try { | ||||
| 				path = 'xl/worksheets/sheet' + (i+1) + (xlsb?'.bin':'.xml'); | ||||
| 				relsPath = path.replace(/^(.*)(\/)([^\/]*)$/, "$1/_rels/$3.rels"); | ||||
| 				sheets[props.SheetNames[i]]=parse_ws(getdata(getzipfile(zip, path)),path,opts); | ||||
| 				sheetRels[props.SheetNames[i]]=parseRels(getdata(getzipfile(zip, relsPath)), path); | ||||
| 			} catch(e) {} | ||||
| 				sheets[props.SheetNames[i]]=parse_ws(getzipdata(zip, path),path,opts); | ||||
| 				sheetRels[props.SheetNames[i]]=parseRels(getzipdata(zip, relsPath, true), path); | ||||
| 			} catch(e) { if(opts.WTF) throw e; } | ||||
| 		} | ||||
| 	} else { | ||||
| 		for(i = 0; i != props.Worksheets; ++i) { | ||||
| @ -2758,9 +2763,9 @@ function parseZip(zip, opts) { | ||||
| 				//var path = dir.sheets[i].replace(/^\//,'');
 | ||||
| 				path = 'xl/worksheets/sheet' + (i+1) + (xlsb?'.bin':'.xml'); | ||||
| 				relsPath = path.replace(/^(.*)(\/)([^\/]*)$/, "$1/_rels/$3.rels"); | ||||
| 				sheets[props.SheetNames[i]]=parse_ws(getdata(getzipfile(zip, path)),path,opts); | ||||
| 				sheetRels[props.SheetNames[i]]=parseRels(getdata(getzipfile(zip, relsPath)), path); | ||||
| 			} catch(e) {/*console.error(e);*/} | ||||
| 				sheets[props.SheetNames[i]]=parse_ws(getzipdata(zip, path),path,opts); | ||||
| 				sheetRels[props.SheetNames[i]]=parseRels(getzipdata(zip, relsPath, true), path); | ||||
| 			} catch(e) { if(opts.WTF) throw e; } | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user