Files
2026-01-05 18:08:52 +08:00

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(/&nbsp;/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;