From b67b3d37658fa40add0a50ccc088046a5424c88d Mon Sep 17 00:00:00 2001 From: bulandent <1326895569@qq.com> Date: Fri, 31 Mar 2023 10:30:35 +0200 Subject: [PATCH] fix: properly parse closing where the last attribute has no value (#485) Closes #486 --- lib/sax.js | 38 +++--- .../__snapshots__/parse-element.test.js.snap | 18 +++ test/parse/parse-element.test.js | 111 ++++++++++++++++++ 3 files changed, 149 insertions(+), 18 deletions(-) diff --git a/lib/sax.js b/lib/sax.js index e81fefac9..bb56b6750 100644 --- a/lib/sax.js +++ b/lib/sax.js @@ -12,7 +12,7 @@ var tagNamePattern = new RegExp('^'+nameStartChar.source+nameChar.source+'*(?:\: //S_TAG, S_ATTR, S_EQ, S_ATTR_NOQUOT_VALUE //S_ATTR_SPACE, S_ATTR_END, S_TAG_SPACE, S_TAG_CLOSE var S_TAG = 0;//tag name offerring -var S_ATTR = 1;//attr name offerring +var S_ATTR = 1;//attr name offerring var S_ATTR_SPACE=2;//attr name end and space offer var S_EQ = 3;//=space? var S_ATTR_NOQUOT_VALUE = 4;//attr value(no quot value only) @@ -36,7 +36,7 @@ ParseError.prototype = new Error(); ParseError.prototype.name = ParseError.name function XMLReader(){ - + } XMLReader.prototype = { @@ -66,7 +66,7 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ function entityReplacer(a){ var k = a.slice(1,-1); if(k in entityMap){ - return entityMap[k]; + return entityMap[k]; }else if(k.charAt(0) === '#'){ return fixedFromCharCode(parseInt(k.substr(1).replace('x','0x'))) }else{ @@ -95,7 +95,7 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ var lineEnd = 0; var linePattern = /.*(?:\r\n?|\n)|.*$/g var locator = domBuilder.locator; - + var parseStack = [{currentNSMap:defaultNSMapCopy}] var closeMap = {}; var start = 0; @@ -120,7 +120,7 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ var tagName = source.substring(tagStart + 2, end).replace(/[ \t\n\r]+$/g, ''); var config = parseStack.pop(); if(end<0){ - + tagName = source.substring(tagStart+2).replace(/[\s<].*/,''); errorHandler.error("end tag name: "+tagName+' is not complete:'+config.tagName); end = tagStart+1+tagName.length; @@ -147,7 +147,7 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ }else{ parseStack.push(config) } - + end++; break; // end elment @@ -166,8 +166,8 @@ function parse(source,defaultNSMapCopy,entityMap,domBuilder,errorHandler){ //elStartEnd var end = parseElementStartPart(source,tagStart,el,currentNSMap,entityReplacer,errorHandler); var len = el.length; - - + + if(!el.closed && fixSelfClosed(source,end,el.tagName,closeMap)){ el.closed = true; if(!entityMap.nbsp){ @@ -297,7 +297,9 @@ function parseElementStartPart(source,start,el,currentNSMap,entityReplacer,error el.closed = true; case S_ATTR_NOQUOT_VALUE: case S_ATTR: - case S_ATTR_SPACE: + break; + case S_ATTR_SPACE: + el.closed = true; break; //case S_EQ: default: @@ -431,7 +433,7 @@ function appendElement(el,domBuilder,currentNSMap){ } //can not set prefix,because prefix !== '' a.localName = localName ; - //prefix == null for no ns prefix attribute + //prefix == null for no ns prefix attribute if(nsPrefix !== false){//hack!! if(localNSMap == null){ localNSMap = {} @@ -441,7 +443,7 @@ function appendElement(el,domBuilder,currentNSMap){ } currentNSMap[nsPrefix] = localNSMap[nsPrefix] = value; a.uri = NAMESPACE.XMLNS - domBuilder.startPrefixMapping(nsPrefix, value) + domBuilder.startPrefixMapping(nsPrefix, value) } } var i = el.length; @@ -453,7 +455,7 @@ function appendElement(el,domBuilder,currentNSMap){ a.uri = NAMESPACE.XML; }if(prefix !== 'xmlns'){ a.uri = currentNSMap[prefix || ''] - + //{console.log('###'+a.qName,domBuilder.locator.systemId+'',currentNSMap,a.uri)} } } @@ -504,7 +506,7 @@ function parseHtmlSpecialContent(source,elStartEnd,tagName,entityReplacer,domBui domBuilder.characters(text,0,text.length); return elEndStart; //} - + } } return elStartEnd+1; @@ -521,7 +523,7 @@ function fixSelfClosed(source,elStartEnd,tagName,closeMap){ closeMap[tagName] =pos } return pos',start+9); domBuilder.startCDATA(); domBuilder.characters(source,start+9,end-start-9); - domBuilder.endCDATA() + domBuilder.endCDATA() return end+3; } //1 && /!doctype/i.test(matchs[0][0])){ @@ -577,7 +579,7 @@ function parseDCC(source,start,domBuilder,errorHandler){//sure start with '", diff --git a/test/parse/parse-element.test.js b/test/parse/parse-element.test.js index 984648a8c..06f47c22c 100644 --- a/test/parse/parse-element.test.js +++ b/test/parse/parse-element.test.js @@ -43,6 +43,117 @@ describe('XML Node Parse', () => { expect(actual).toBe(`Harry Potter`); }); + it('closing tag without attribute value', () => { + const actual = new DOMParser() + .parseFromString( + ``, + 'text/xml' + ) + .toString(); + expect(actual).toBe( + `` + ); + }); + it('closing tag with unquoted value following /', () => { + const actual = new DOMParser() + .parseFromString( + ``, + 'text/xml' + ) + .toString(); + expect(actual).toBe( + `` + ); + }); + it('closing tag with unquoted value following space and /', () => { + const actual = new DOMParser() + .parseFromString( + ``, + 'text/xml' + ) + .toString(); + expect(actual).toBe( + `` + ); + }); + it('closing tag with unquoted value including / followed by space /', () => { + const { errors, parser } = getTestParser(); + const actual = parser + .parseFromString( + ``, + 'text/xml' + ) + .toString(); + expect(errors).toMatchSnapshot(); + expect(actual).toBe( + `` + ); + }); + it('closing tag with unquoted value ending with //', () => { + const { errors, parser } = getTestParser(); + + const actual = parser + .parseFromString( + ``, + 'text/xml' + ) + .toString(); + expect(errors).toMatchSnapshot(); + expect(actual).toBe( + `` + ); + }); + describe('simple attributes', () => { describe('nothing special', () => { it.each([