/**
|
* @file parser
|
* @author https://github.com/fb55/htmlparser2
|
* @modify tanghao03 yanghuabei(yanghuabei@outlook.com)
|
* @date 2018/8/16
|
*/
|
|
var Tokenizer = require('./Tokenizer.js');
|
|
/*
|
Options:
|
|
xmlMode: Disables the special behavior for script/style tags (false by default)
|
lowerCaseAttributeNames: call .toLowerCase for each attribute name (true if xmlMode is `false`)
|
lowerCaseTags: call .toLowerCase for each tag name (true if xmlMode is `false`)
|
*/
|
|
/*
|
Callbacks:
|
|
oncdataend,
|
oncdatastart,
|
onclosetag,
|
oncomment,
|
oncommentend,
|
onerror,
|
onopentag,
|
onprocessinginstruction,
|
onreset,
|
ontext
|
*/
|
|
var formTags = {
|
input: true,
|
option: true,
|
optgroup: true,
|
select: true,
|
button: true,
|
datalist: true,
|
textarea: true
|
};
|
|
var openImpliesClose = {
|
tr: {
|
tr: true,
|
th: true,
|
td: true
|
},
|
th: {
|
th: true
|
},
|
td: {
|
thead: true,
|
th: true,
|
td: true
|
},
|
body: {
|
head: true,
|
link: true,
|
script: true
|
},
|
li: {
|
li: true
|
},
|
p: {
|
p: true
|
},
|
h1: {
|
p: true
|
},
|
h2: {
|
p: true
|
},
|
h3: {
|
p: true
|
},
|
h4: {
|
p: true
|
},
|
h5: {
|
p: true
|
},
|
h6: {
|
p: true
|
},
|
select: formTags,
|
input: formTags,
|
output: formTags,
|
button: formTags,
|
datalist: formTags,
|
textarea: formTags,
|
option: {
|
option: true
|
},
|
optgroup: {
|
optgroup: true
|
}
|
};
|
|
var voidElements = {
|
__proto__: null,
|
area: true,
|
base: true,
|
basefont: true,
|
br: true,
|
col: true,
|
command: true,
|
embed: true,
|
frame: true,
|
hr: true,
|
img: true,
|
input: true,
|
isindex: true,
|
keygen: true,
|
link: true,
|
meta: true,
|
param: true,
|
source: true,
|
track: true,
|
wbr: true,
|
|
//common self closing svg elements
|
path: true,
|
circle: true,
|
ellipse: true,
|
line: true,
|
rect: true,
|
use: true,
|
stop: true,
|
polyline: true,
|
polygon: true
|
};
|
|
var re_nameEnd = /\s|\//;
|
|
function Parser(cbs, options) {
|
this._options = options || {};
|
this._cbs = cbs || {};
|
|
this._tagname = '';
|
this._attribname = '';
|
this._attribvalue = '';
|
this._attribs = null;
|
this._stack = [];
|
|
this.startIndex = 0;
|
this.endIndex = null;
|
|
this._lowerCaseTagNames = 'lowerCaseTags' in this._options ? !!this._options.lowerCaseTags : !this._options.xmlMode;
|
this._lowerCaseAttributeNames =
|
'lowerCaseAttributeNames' in this._options ? !!this._options.lowerCaseAttributeNames : !this._options.xmlMode;
|
|
if (this._options.Tokenizer) {
|
Tokenizer = this._options.Tokenizer;
|
}
|
this._tokenizer = new Tokenizer(this._options, this);
|
|
if (this._cbs.onparserinit) this._cbs.onparserinit(this);
|
}
|
|
require('inherits')(Parser, require('events').EventEmitter);
|
|
Parser.prototype._updatePosition = function (initialOffset) {
|
if (this.endIndex === null) {
|
if (this._tokenizer._sectionStart <= initialOffset) {
|
this.startIndex = 0;
|
} else {
|
this.startIndex = this._tokenizer._sectionStart - initialOffset;
|
}
|
} else this.startIndex = this.endIndex + 1;
|
this.endIndex = this._tokenizer.getAbsoluteIndex();
|
};
|
|
//Tokenizer event handlers
|
Parser.prototype.ontext = function (data) {
|
this._updatePosition(1);
|
this.endIndex--;
|
|
if (this._cbs.ontext) this._cbs.ontext(data);
|
};
|
|
Parser.prototype.onopentagname = function (name) {
|
if (this._lowerCaseTagNames) {
|
name = name.toLowerCase();
|
}
|
|
this._tagname = name;
|
|
if (!this._options.xmlMode && name in openImpliesClose) {
|
for (var el;
|
(el = this._stack[this._stack.length - 1]) in openImpliesClose[name]; this.onclosetag(el));
|
}
|
|
if (this._options.xmlMode || !(name in voidElements)) {
|
this._stack.push(name);
|
}
|
|
if (this._cbs.onopentagname) this._cbs.onopentagname(name);
|
if (this._cbs.onopentag) {
|
this._attribs = {};
|
this._singleQuoteAttribs = {};
|
}
|
};
|
|
Parser.prototype.onopentagend = function (isSelfClose = false) {
|
this._updatePosition(1);
|
|
if (this._attribs) {
|
if (this._cbs.onopentag) this._cbs.onopentag(this._tagname, this._attribs, this._singleQuoteAttribs);
|
this._attribs = null;
|
this._singleQuoteAttribs = null;
|
}
|
|
if (!this._options.xmlMode && this._cbs.onclosetag && this._tagname in voidElements) {
|
this._cbs.onclosetag(this._tagname, isSelfClose);
|
}
|
|
this._tagname = '';
|
};
|
|
Parser.prototype.onclosetag = function (name) {
|
this._updatePosition(1);
|
|
if (this._lowerCaseTagNames) {
|
name = name.toLowerCase();
|
}
|
|
if (this._stack.length && (!(name in voidElements) || this._options.xmlMode)) {
|
var pos = this._stack.lastIndexOf(name);
|
if (pos !== -1) {
|
if (this._cbs.onclosetag) {
|
pos = this._stack.length - pos;
|
while (pos--) this._cbs.onclosetag(this._stack.pop());
|
} else this._stack.length = pos;
|
} else if (name === 'p' && !this._options.xmlMode) {
|
this.onopentagname(name);
|
this._closeCurrentTag();
|
}
|
} else if (!this._options.xmlMode && (name === 'br' || name === 'p')) {
|
this.onopentagname(name);
|
this._closeCurrentTag();
|
}
|
};
|
|
Parser.prototype.onselfclosingtag = function () {
|
if (this._options.xmlMode || this._options.recognizeSelfClosing) {
|
this._closeCurrentTag(true);
|
} else {
|
this.onopentagend();
|
}
|
};
|
|
Parser.prototype._closeCurrentTag = function (isSelfClose = false) {
|
var name = this._tagname;
|
|
this.onopentagend(isSelfClose);
|
|
//self-closing tags will be on the top of the stack
|
//(cheaper check than in onclosetag)
|
if (this._stack[this._stack.length - 1] === name) {
|
if (this._cbs.onclosetag) {
|
this._cbs.onclosetag(name, isSelfClose);
|
}
|
this._stack.pop();
|
}
|
};
|
|
Parser.prototype.onattribname = function (name) {
|
if (this._lowerCaseAttributeNames) {
|
name = name.toLowerCase();
|
}
|
this._attribname = name;
|
};
|
|
Parser.prototype.onattribvalueusesinglequote = function () {
|
this._isAttribWithSingleQuote = true;
|
};
|
|
Parser.prototype.onattribdata = function (value) {
|
this._attribvalue += value;
|
};
|
|
Parser.prototype.onattribend = function () {
|
if (this._cbs.onattribute) this._cbs.onattribute(this._attribname, this._attribvalue);
|
if (this._attribs && !Object.prototype.hasOwnProperty.call(this._attribs, this._attribname)) {
|
this._attribs[this._attribname] = this._attribvalue;
|
// 记录一下值是单引号包裹的属性
|
if (this._isAttribWithSingleQuote) {
|
this._singleQuoteAttribs[this._attribname] = true;
|
}
|
}
|
this._attribname = '';
|
this._attribvalue = '';
|
this._isAttribWithSingleQuote = null;
|
};
|
|
Parser.prototype._getInstructionName = function (value) {
|
var idx = value.search(re_nameEnd),
|
name = idx < 0 ? value : value.substr(0, idx);
|
|
if (this._lowerCaseTagNames) {
|
name = name.toLowerCase();
|
}
|
|
return name;
|
};
|
|
Parser.prototype.ondeclaration = function (value) {
|
if (this._cbs.onprocessinginstruction) {
|
var name = this._getInstructionName(value);
|
this._cbs.onprocessinginstruction('!' + name, '!' + value);
|
}
|
};
|
|
Parser.prototype.onprocessinginstruction = function (value) {
|
if (this._cbs.onprocessinginstruction) {
|
var name = this._getInstructionName(value);
|
this._cbs.onprocessinginstruction('?' + name, '?' + value);
|
}
|
};
|
|
Parser.prototype.oncomment = function (value) {
|
this._updatePosition(4);
|
|
if (this._cbs.oncomment) this._cbs.oncomment(value);
|
if (this._cbs.oncommentend) this._cbs.oncommentend();
|
};
|
|
Parser.prototype.oncdata = function (value) {
|
this._updatePosition(1);
|
|
if (this._options.xmlMode || this._options.recognizeCDATA) {
|
if (this._cbs.oncdatastart) this._cbs.oncdatastart();
|
if (this._cbs.ontext) this._cbs.ontext(value);
|
if (this._cbs.oncdataend) this._cbs.oncdataend();
|
} else {
|
this.oncomment('[CDATA[' + value + ']]');
|
}
|
};
|
|
Parser.prototype.onerror = function (err) {
|
if (this._cbs.onerror) this._cbs.onerror(err);
|
};
|
|
Parser.prototype.onend = function () {
|
if (this._cbs.onclosetag) {
|
for (var i = this._stack.length; i > 0; this._cbs.onclosetag(this._stack[--i]));
|
}
|
if (this._cbs.onend) this._cbs.onend();
|
};
|
|
//Resets the parser to a blank state, ready to parse a new HTML document
|
Parser.prototype.reset = function () {
|
if (this._cbs.onreset) this._cbs.onreset();
|
this._tokenizer.reset();
|
|
this._tagname = '';
|
this._attribname = '';
|
this._attribs = null;
|
this._stack = [];
|
|
if (this._cbs.onparserinit) this._cbs.onparserinit(this);
|
};
|
|
//Parses a complete HTML document and pushes it to the handler
|
Parser.prototype.parseComplete = function (data) {
|
this.reset();
|
this.end(data);
|
};
|
|
Parser.prototype.write = function (chunk) {
|
this._tokenizer.write(chunk);
|
};
|
|
Parser.prototype.end = function (chunk) {
|
this._tokenizer.end(chunk);
|
};
|
|
Parser.prototype.pause = function () {
|
this._tokenizer.pause();
|
};
|
|
Parser.prototype.resume = function () {
|
this._tokenizer.resume();
|
};
|
|
//alias for backwards compat
|
Parser.prototype.parseChunk = Parser.prototype.write;
|
Parser.prototype.done = Parser.prototype.end;
|
|
module.exports = Parser;
|