325 lines
8.4 KiB
JavaScript
325 lines
8.4 KiB
JavaScript
//DomHandler.js
|
|
const CssTokenizer = require('./CssTokenizer.js');
|
|
const CanIUse = require('./api.js').versionHigherThan('2.7.1');
|
|
const Common = 1,
|
|
Rich = 2;
|
|
const trustTag = {
|
|
a: Rich,
|
|
abbr: Common,
|
|
audio: Rich,
|
|
b: Common,
|
|
blockquote: Common,
|
|
br: Rich,
|
|
code: Common,
|
|
col: Rich,
|
|
colgroup: Rich,
|
|
dd: Common,
|
|
del: Common,
|
|
dl: Common,
|
|
dt: Common,
|
|
div: Common,
|
|
em: Common,
|
|
fieldset: Rich,
|
|
font: Common,
|
|
h1: Rich,
|
|
h2: Rich,
|
|
h3: Rich,
|
|
h4: Rich,
|
|
h5: Rich,
|
|
h6: Rich,
|
|
hr: Rich,
|
|
i: Common,
|
|
img: Common,
|
|
ins: Common,
|
|
label: Common,
|
|
legend: Rich,
|
|
li: Rich,
|
|
ol: Rich,
|
|
p: Common,
|
|
q: Common,
|
|
source: Rich,
|
|
span: Common,
|
|
strong: Common,
|
|
sub: Rich,
|
|
sup: Rich,
|
|
table: Rich,
|
|
tbody: Rich,
|
|
td: Rich,
|
|
tfoot: Rich,
|
|
th: Rich,
|
|
thead: Rich,
|
|
tr: Rich,
|
|
u: Common,
|
|
ul: Rich,
|
|
video: Common
|
|
};
|
|
const blockTag = {
|
|
address: true,
|
|
article: true,
|
|
aside: true,
|
|
body: true,
|
|
center: true,
|
|
cite: true,
|
|
footer: true,
|
|
header: true,
|
|
html: true,
|
|
nav: true,
|
|
pre: true,
|
|
section: true
|
|
}
|
|
const textTag = {
|
|
a: true,
|
|
abbr: true,
|
|
b: true,
|
|
big: true,
|
|
code: true,
|
|
del: true,
|
|
em: true,
|
|
font: true,
|
|
i: true,
|
|
ins: true,
|
|
label: true,
|
|
mark: true,
|
|
q: true,
|
|
s: true,
|
|
small: true,
|
|
span: true,
|
|
strong: true,
|
|
u: true
|
|
};
|
|
const ignoreTag = {
|
|
area: true,
|
|
base: true,
|
|
basefont: true,
|
|
canvas: true,
|
|
circle: true,
|
|
command: true,
|
|
ellipse: true,
|
|
embed: true,
|
|
frame: true,
|
|
head: true,
|
|
iframe: true,
|
|
input: true,
|
|
isindex: true,
|
|
keygen: true,
|
|
line: true,
|
|
link: true,
|
|
map: true,
|
|
meta: true,
|
|
param: true,
|
|
path: true,
|
|
polygon: true,
|
|
polyline: true,
|
|
rect: true,
|
|
script: true,
|
|
stop: true,
|
|
textarea: true,
|
|
title: true,
|
|
track: true,
|
|
use: true,
|
|
wbr: true
|
|
};
|
|
if (CanIUse) {
|
|
trustTag.bdi = Rich;
|
|
trustTag.bdo = Rich;
|
|
trustTag.caption = Rich;
|
|
trustTag.rt = Rich;
|
|
trustTag.ruby = Rich;
|
|
ignoreTag.rp = true;
|
|
trustTag.big = Common;
|
|
trustTag.small = Common;
|
|
trustTag.pre = Rich;
|
|
delete blockTag.pre;
|
|
}
|
|
//添加默认值
|
|
function initStyle(tagStyle) {
|
|
tagStyle.a = "display:inline;color:#366092;word-break:break-all;" + (tagStyle.a || "");
|
|
tagStyle.address = "font-style:italic;" + (tagStyle.address || "");
|
|
tagStyle.blockquote = tagStyle.blockquote || 'background-color:#f6f6f6;border-left:3px solid #dbdbdb;color:#6c6c6c;padding:5px 0 5px 10px';
|
|
tagStyle.center = 'text-align:center;' + (tagStyle.center || "");
|
|
tagStyle.cite = "font-style:italic;" + (tagStyle.cite || "");
|
|
tagStyle.code = tagStyle.code || 'padding:0 1px 0 1px;margin-left:2px;margin-right:2px;background-color:#f8f8f8;border:1px solid #cccccc;border-radius:3px';
|
|
tagStyle.dd = "margin-left:40px;" + (tagStyle.dd || "");
|
|
tagStyle.img = "max-width:100%;" + (tagStyle.img || "");
|
|
tagStyle.mark = "display:inline;background-color:yellow;" + (tagStyle.mark || "");
|
|
tagStyle.pre = "overflow:scroll;" + (tagStyle.pre || 'background-color:#f6f8fa;padding:5px;border-radius:5px;');
|
|
tagStyle.s = "display:inline;text-decoration:line-through;" + (tagStyle.s || "");
|
|
tagStyle.u = "display:inline;text-decoration:underline;" + (tagStyle.u || "");
|
|
//低版本兼容
|
|
if (!CanIUse) {
|
|
blockTag.caption = true;
|
|
tagStyle.big = "display:inline;font-size:1.2em;" + (tagStyle.big || "");
|
|
tagStyle.small = "display:inline;font-size:0.8em;" + (tagStyle.small || "");
|
|
tagStyle.pre = "font-family:monospace;white-space:pre;" + tagStyle.pre;
|
|
}
|
|
return tagStyle;
|
|
}
|
|
|
|
function DomHandler(style, tagStyle = {}) {
|
|
this.imgList = [];
|
|
this.nodes = [];
|
|
this.title = "";
|
|
this._videoNum = 0;
|
|
this._audioNum = 0;
|
|
this._style = new CssTokenizer(style, initStyle(tagStyle)).parse();
|
|
this._tagStack = [];
|
|
}
|
|
DomHandler.prototype._addDomElement = function(element) {
|
|
let parent = this._tagStack[this._tagStack.length - 1];
|
|
let siblings = parent ? parent.children : this.nodes;
|
|
siblings.push(element);
|
|
};
|
|
DomHandler.prototype._bubbling = function() {
|
|
for (let i = this._tagStack.length - 1; i >= 0; i--) {
|
|
if (trustTag[this._tagStack[i].name] == Common) this._tagStack[i].continue = true;
|
|
else return this._tagStack[i].name;
|
|
}
|
|
}
|
|
DomHandler.prototype.onopentag = function(name, attrs) {
|
|
let element = {
|
|
children: []
|
|
};
|
|
//匹配样式
|
|
let matched = this._style[name] ? (this._style[name] + ';') : '';
|
|
if (attrs.id) {
|
|
matched += (this._style['#' + attrs.id] ? (this._style['#' + attrs.id] + ';') : '');
|
|
delete attrs.id;
|
|
}
|
|
if (attrs.class) {
|
|
for (var Class of attrs.class.split(' ')) {
|
|
matched += (this._style['.' + Class] ? (this._style['.' + Class] + ';') : '');
|
|
}
|
|
delete attrs.class;
|
|
}
|
|
//处理属性
|
|
switch (name) {
|
|
case 'div':
|
|
case 'p':
|
|
if (attrs.align) {
|
|
attrs.style += (';text-align:' + attrs.align);
|
|
delete attrs.align;
|
|
}
|
|
break;
|
|
case 'img':
|
|
if (attrs.width) {
|
|
attrs.style = 'width:' + attrs.width + (/[0-9]/.test(attrs.width[attrs.width.length - 1]) ? 'px' : '') + ';' + attrs.style;
|
|
delete attrs.width;
|
|
}
|
|
if (attrs['data-src']) {
|
|
attrs.src = attrs.src || attrs['data-src'];
|
|
delete attrs['data-src'];
|
|
}
|
|
if (!attrs.hasOwnProperty('ignore') && attrs.src) {
|
|
this.imgList.push(attrs.src);
|
|
if (this._bubbling() == 'a') attrs.ignore = "";
|
|
};
|
|
break;
|
|
case 'font':
|
|
name = 'span';
|
|
if (attrs.color) {
|
|
attrs.style += (';color:' + attrs.color);
|
|
delete attrs.color;
|
|
}
|
|
if (attrs.face) {
|
|
attrs.style += (";font-family:" + attrs.face);
|
|
delete attrs.face;
|
|
}
|
|
if (attrs.size) {
|
|
var size = parseInt(attrs.size);
|
|
if (size < 1) size = 1;
|
|
else if (size > 7) size = 7;
|
|
switch (size) {
|
|
case 1:
|
|
size = 10;
|
|
break;
|
|
case 2:
|
|
size = 13;
|
|
break;
|
|
case 3:
|
|
size = 16;
|
|
break;
|
|
case 4:
|
|
size = 18;
|
|
break;
|
|
case 5:
|
|
size = 24;
|
|
break;
|
|
case 6:
|
|
size = 32;
|
|
break;
|
|
case 7:
|
|
size = 48;
|
|
break;
|
|
}
|
|
attrs.style += (";font-size:" + size + "px");
|
|
delete attrs.size;
|
|
}
|
|
break;
|
|
case 'a':
|
|
this._bubbling();
|
|
break;
|
|
case 'video':
|
|
case 'audio':
|
|
attrs.loop = attrs.hasOwnProperty('loop');
|
|
attrs.controls = attrs.hasOwnProperty('controls');
|
|
attrs.autoplay = attrs.hasOwnProperty('autoplay');
|
|
if (name == 'video') {
|
|
attrs.muted = attrs.hasOwnProperty('muted');
|
|
if (attrs.width) {
|
|
attrs.style = 'width:' + parseFloat(attrs.width) + 'px;' + attrs.style;
|
|
delete attrs.width;
|
|
}
|
|
if (attrs.height) {
|
|
attrs.style = 'height:' + parseFloat(attrs.height) + 'px;' + attrs.style;
|
|
delete attrs.height;
|
|
}
|
|
}
|
|
attrs.id = (name + (++this['_' + name + 'Num']));
|
|
attrs.source = [];
|
|
if (attrs.src) attrs.source.push(attrs.src);
|
|
if (!attrs.controls && !attrs.autoplay)
|
|
console.warn('存在没有controls属性的' + name + '标签,可能导致无法播放', attrs);
|
|
this._bubbling();
|
|
break;
|
|
case 'source':
|
|
let parent = this._tagStack[this._tagStack.length - 1];
|
|
if (parent && (parent.name == 'video' || parent.name == 'audio')) {
|
|
parent.attrs.source.push(attrs.src);
|
|
if (!parent.attrs.src) parent.attrs.src = attrs.src;
|
|
}
|
|
this._tagStack.push(element);
|
|
return;
|
|
}
|
|
attrs.style = matched + attrs.style;
|
|
if (textTag[name]) element.continue = true;
|
|
else if (blockTag[name]) name = 'div';
|
|
else if (!trustTag[name]) name = 'span';
|
|
element.name = name;
|
|
element.attrs = attrs;
|
|
this._addDomElement(element);
|
|
this._tagStack.push(element);
|
|
};
|
|
DomHandler.prototype.ontext = function(data) {
|
|
if (/\S/.test(data)) {
|
|
let element = {
|
|
text: data.replace(/ /g, '\u00A0'),
|
|
type: 'text'
|
|
};
|
|
if (/&#*((?!sp|lt|gt).){2,5};/.test(data)) element.decode = true;
|
|
this._addDomElement(element);
|
|
}
|
|
};
|
|
DomHandler.prototype.onclosetag = function(name) {
|
|
let element = this._tagStack.pop();
|
|
if (ignoreTag[name]) {
|
|
if (name == 'title') {
|
|
try {
|
|
this.title = element.children[0].text;
|
|
} catch (e) {}
|
|
}
|
|
let parent = this._tagStack[this._tagStack.length - 1];
|
|
let siblings = parent ? parent.children : this.nodes;
|
|
siblings.pop();
|
|
}
|
|
};
|
|
module.exports = DomHandler; |