magix-combine
Version:
合并Magix View的html,js,css成一个js文件,并检测html,js,css中可能存在的问题
651 lines (640 loc) • 26.8 kB
JavaScript
/*
子模板拆分
*/
let chalk = require('chalk');
let tmplCmd = require('./tmpl-cmd');
let slog = require('./util-log');
let tmplParser = require('./tmpl-parser');
let {
getProps,
getBooleanProps
} = require('./tmpl-attr-map');
let holder = '\u001d';
let loopReg = /\u0019/g;
//let slashAnchorReg = /\u0004/g;
//自闭合标签,需要开发者明确写上如 <input />,注意>前的/,不能是<img>
//let selfCloseTag = /<([^>\s\/]+)\s+(mx-guid="g[^"]+")[^>]*?\/>/g;
let extractAttrsReg = /<[^>\s\/]+\s+mx-guid="[^"]+"\s+([^>]+?)\/?>/;
//属性正则
let attrNameValueReg = /([^=\/\s]+)(?:\s*=\s*(?:(["'])[\s\S]*?\2|\S*))?(?=$|\s)/g;
//模板引擎命令被替换的占位符
let tmplCommandAnchorReg = /\u0007\d+\u0007/g;
let tmplCommandAnchorRegTest = /\u0007\d+\u0007/;
let globalTmplRootReg = /[\u0003\u0006]/g;
let virtualRoot = /<mxv-root[^>]*>([\s\S]+)<\/mxv-root>/g;
//let escape$ = str => str.replace(/\$/g, '$&$&');
let escapeHTMLMap = {
'&': 'amp',
'<': 'lt',
'>': 'gt',
'"': '#34',
'\'': '#39',
'`': '#96`'
};
//let escapeQ = str => str.replace(/"/g, '"');
let escapeHTML = str => str.replace(/[&<>"'`]/g, m => '&' + escapeHTMLMap[m] + ';');
//恢复被替换的模板引擎命令
let commandAnchorRecover = (tmpl, refTmplCommands) => tmplCmd.recover(tmpl, refTmplCommands).replace(globalTmplRootReg, '$$$$');
let dataKeysReg = /\u0003\.([\w$]+)\.?/g;
let trimAttrsStart = /^[a-z\-\d]+(?:=(["'])[^\u0007]+?\1)?(?=\s+|\u0007\d+\u0007|$)/g;
let trimAttrsEnd = /(\s+|\u0007\d+\u0007)[a-z\-\d]+(?:=(["'])[^\u0007]+?\2)?$/;
let inputTypeReg = /\btype\s*=\s*(['"])([\s\S]+?)\1/;
let stringReg = /(['"])([a-z]+)\1/g;
let mxViewAttrReg = /\bmx-view\s*=\s*(['"])([^'"]+?)\1/;
//模板,子模板的处理,仍然是配合magix-updater:https://github.com/thx/magix-updater
//生成子模板匹配正则
/*let subReg = (() => {
let temp = '<([^>\\s\\/]+)\\s+(mx-guid="g[^"]+")[^>]*?>(#)</\\1>';
let start = 5; //嵌套9层在同一个view中也足够了
while (start--) {
temp = temp.replace('#', '(?:<\\1[^>]*>#</\\1>|[\\s\\S])*?');
}
temp = temp.replace('#', '[\\s\\S]*?');
return regexp.get(temp, 'ig');
})();*/
let getContentWithoutGuid = (n, tmpl) => {
let removed = [{
start: 0,
end: n.contentStart
}, {
start: n.contentEnd,
end: tmpl.length
}];
let walk = nodes => {
if (nodes) {
for (let n of nodes) {
if (n.guid) {
removed.push(n);
} else {
walk(n.children);
}
}
}
};
walk(n.children);
removed = removed.sort((a, b) => {
return b.start - a.start;
});
for (let r of removed) {
tmpl = tmpl.substring(0, r.start) + tmpl.substring(r.end);
}
return tmpl;
};
let getRange = (start, end, modifiers) => {
let range = [];
let flag = null;
for (let m of modifiers) {
if (m.start > start && m.end < end) {
if (flag) {
if (m.start < flag.start || m.end > flag.end) {
flag = m;
range.push(flag);
}
} else {
flag = m;
range.push(flag);
}
}
}
return range;
};
/*let extractUpdateKeys = (tmpl, refTmplCommands, content, pKeys, extInfo) => {
let attrKeys = Object.create(null);
let tmplKeys = Object.create(null);
tmpl = tmpl.replace(content, ''); //标签加内容,移除内容,只剩标签
//console.log('--------',tmpl,'======',content);
while (subReg.test(content) || selfCloseTag.test(content)) { //清除子模板
content = content.replace(selfCloseTag, '');
content = content.replace(subReg, '');
//break;
}
//console.log('=====', tmpl, '-----', content);
tmpl.replace(tmplCommandAnchorReg, m => {
let temp = refTmplCommands[m];
//console.log(temp);
temp.replace(dataKeysReg, (m, name) => { //数据key
if (!pKeys || !pKeys[name]) { //不在父作用域内
if (extInfo.tmplScopedUpdateBy) {
if (extInfo.tmplScopedUpdateBy[name]) {
attrKeys[name] = 1;
}
} else if (!configs.tmplLazyAnalysis) {
attrKeys[name] = 1;
}
}
});
});
content.replace(tmplCommandAnchorReg, m => { //查找模板命令
let temp = refTmplCommands[m];
temp.replace(dataKeysReg, (m, name) => { //数据key
if (!pKeys || !pKeys[name]) { //不在父作用域内
if (extInfo.tmplScopedUpdateBy) {
if (extInfo.tmplScopedUpdateBy[name]) {
tmplKeys[name] = 1;
}
} else if (!configs.tmplLazyAnalysis) {
tmplKeys[name] = 1;
}
}
});
});
//
Object.assign(attrKeys, tmplKeys);
return {
keys: Object.keys(attrKeys),
//attrKeys: attrKeys,
tmplKeys
};
};*/
let newExtractUpdateKeys = (tmpl, refTmplCommands, content, pKeys, extInfo) => {
let attrKeys = Object.create(null);
let tmplKeys = Object.create(null);
tmpl.replace(tmplCommandAnchorReg, m => {
let temp = refTmplCommands[m];
//console.log(temp);
temp.replace(dataKeysReg, (m, name) => { //数据key
name = name.trim();
if (!pKeys || !pKeys[name]) { //不在父作用域内
if (extInfo.tmplScopedUpdateBy) {
if (extInfo.tmplScopedUpdateBy[name]) {
attrKeys[name] = 1;
}
} else if (!extInfo.tmplScopedConstVars ||
!extInfo.tmplScopedConstVars[name]) {
attrKeys[name] = 1;
}
}
});
});
content.replace(tmplCommandAnchorReg, m => { //查找模板命令
let temp = refTmplCommands[m];
if (temp) {
temp.replace(dataKeysReg, (m, name) => { //数据key
name = name.trim();
if (!pKeys || !pKeys[name]) { //不在父作用域内
if (extInfo.tmplScopedUpdateBy) {
if (extInfo.tmplScopedUpdateBy[name]) {
tmplKeys[name] = 1;
}
} else if (!extInfo.tmplScopedConstVars ||
!extInfo.tmplScopedConstVars[name]) {
tmplKeys[name] = 1;
}
}
});
}
});
//
let allKeys = Object.assign({}, attrKeys, tmplKeys);
return {
keys: Object.keys(allKeys),
attrAKeys: Object.keys(attrKeys),
attrKeys,
allKeys,
tmplKeys
};
};
//添加属性信息
let addAttrs = (tag, tmpl, info, refTmplCommands, e, hasSubView, kInfo) => {
let attrsKeys = kInfo.attrKeys,
tmplKeys = Object.create(null);
tmpl.replace(extractAttrsReg, (match, attr) => {
let originalAttr = attr;
let mxView = '';
attr.replace(mxViewAttrReg, m => mxView = m);
while (trimAttrsStart.test(attr)) {
attr = attr.replace(trimAttrsStart, '').trim();
}
while (trimAttrsEnd.test(attr)) {
attr = attr.replace(trimAttrsEnd, '$1').trim();
}
if (info.tmpl && mxView && !mxViewAttrReg.test(attr)) {
attr += ' ' + mxView;
}
if (!attr) return;
attr.replace(tmplCommandAnchorReg, match => {
let value = refTmplCommands[match];
if (value) {
value.replace(dataKeysReg, (m, vname) => {
attrsKeys[vname] = 1;
});
}
});
let hasProps = Object.create(null);
originalAttr.replace(tmplCommandAnchorReg, '').replace(attrNameValueReg, (match, name) => {
hasProps[name] = 1;
});
let attrs = [];
let attrsMap = Object.create(null);
let type = '';
if (tag == 'input') { //特殊处理input
//console.log(originalAttr);
let ms = originalAttr.match(inputTypeReg);
if (ms) {
type = ms[2];
}
}
let props = [];
let propsMap = getProps(tag, type);
let propsBooleanMap = getBooleanProps(tag, type);
let extractProps = attr.replace(tmplCommandAnchorReg, match => {
let temp = commandAnchorRecover(match, refTmplCommands);
temp.replace(stringReg, (match, q, content) => {
if (!hasProps[content] && propsMap.hasOwnProperty(content)) {
props.push(content);
}
});
return '';
});
//let extractProps = attr.replace(tmplCommandAnchorReg, '');
extractProps.replace(attrNameValueReg, (match, name) => {
props.push(name);
});
for (let i = 0, prop; i < props.length; i++) {
prop = props[i];
if (attrsMap[prop] == 1) {
// if (e.checker.tmplDuplicateAttr) {
// slog.ever('duplicate attr:', chalk.blue(prop), ' near:', e.toTmplSrc(attr, refTmplCommands), ' relate file:', chalk.grey(e.shortFrom));
// }
continue;
}
let t = Object.create(null);
t.n = prop; // name
attrsMap[prop] = 1;
if (prop == 'mx-view') {
t.v = 1; // mx-view
info.hasView = true;
}
if ((tag == 'input' || tag == 'textarea') && prop == 'value') {
t.q = 1; //decode html
}
let propInfo = propsBooleanMap; // getBooleanProps(tag, type);
if (propInfo && propInfo[prop]) {
t.b = 1; // boolean prop
}
propInfo = propsMap; // getProps(tag, type);
if (propInfo) {
let fixedName = propInfo[prop];
if (fixedName) {
t.p = 1; // prop
if (fixedName != prop) {
t.f = fixedName;
}
}
}
attrs.push(t);
}
attr = commandAnchorRecover(attr, refTmplCommands);
if (attr && attrs.length) {
info.attr = attr.trim(); //.replace(slashAnchorReg, '/');
info.attrs = attrs;
}
});
//只有普通标签才可以进行属性与内容刷新优化,对于view来讲,不管是属性还是模板都需要重新渲染
//如果含有子view的节点,只需要重新实例化,不需要渲染模板
//console.log(info,hasSubView, info.hasView);
if ((info.tmpl && info.attr) && (hasSubView || !info.hasView)) { //有模板及属性
//接下来我们处理前面的属性和内容更新问题
info.tmpl.replace(tmplCommandAnchorReg, match => {
let value = refTmplCommands[match];
if (value) {
value.replace(dataKeysReg, (m, vname) => {
tmplKeys[vname] = 1;
});
}
});
//console.log(info.keys, tmplKeys, attrsKeys);
let mask = '';
for (let i = 0, m; i < info.keys.length; i++) {
m = 0;
//如果key存在内容模板中,则m为1
if (tmplKeys[info.keys[i]]) m = 1;
//如果key存在属性中,则m为2或者或上1
//console.log(info.keys);
if (attrsKeys[info.keys[i]]) m = m ? m | 2 : 2;
mask += m + '';
if (m === 0) {
slog.ever('[MXC-Tip(tmpl-partial)] check key word:', chalk.red(info.keys[i]), ' relate file:', chalk.grey(e.shortFrom));
}
}
//最后产出的结果可能如:
/*
{
keys:['a','b','c'],
mask:'211' //a对应2,b,c对应1,则表示a变化时,只更新属性,b,c变化时只更新节点内容
}
*/
if (/[12]/.test(mask))
info.mask = mask;
}
delete info.hasView;
};
/*let g = 0;
//递归构建子模板
let buildTmpl = (tmpl, refTmplCommands, e, extInfo, list, parentOwnKeys, globalKeys) => {
if (!list) {
list = [];
g = 0;
globalKeys = Object.create(null);
}
let subs = [];
let removeGuids = []; //经过tmpl-guid插件之后,所有的标签都会加上guid,但只有具备局部刷新的标签才保留guid,其它的移除,这里用来记录要移除的guid
//子模板
//console.log('input ',tmpl);
//debugger;
tmpl = tmpl.replace(subReg, (match, tag, guid, content) => { //清除子模板后
//match = match.replace(slashAnchorReg, '/');
//console.log('match',match,tag,guid,'=======',content);
//debugger;
let ownKeys = Object.create(null);
for (let p in parentOwnKeys) { //继承父结构的keys
ownKeys[p] = parentOwnKeys[p];
}
let selector = tag + '[' + guid + ']';
if (tag == 'mxv-root') {
selector = '#\u001f';
}
let tmplInfo = {
s: ++g + holder,
keys: [],
tmpl: content,
path: selector
};
if (parentOwnKeys) {
let pKeys = Object.keys(parentOwnKeys);
if (pKeys.length) {
tmplInfo.pKeys = pKeys; //记录父结构有哪些keys,当数据变化且在父结构中时,当前结构是不需要去做更新操作的,由父代劳
}
}
//let datakey = refGuidToKeys[guid];
//let keys = datakey.split(',');
let remain = match;
let kInfo = extractUpdateKeys(match, refTmplCommands, content, parentOwnKeys, extInfo); //从当前匹配到的标签取对应的数据key
//console.log(kInfo);
//console.log('keys', kInfo, match, content);
if (kInfo.keys.length) { //从当前标签分析出了数据key后,再深入分析
for (let i = 0, key; i < kInfo.keys.length; i++) {
key = kInfo.keys[i].trim();
tmplInfo.keys.push(key);
globalKeys[key] = 1;
}
Object.assign(ownKeys, kInfo.tmplKeys);
//ownKeys = kInfo.tmplKeys;
//list.push(tmplInfo); //先记录
if (tag == 'textarea') { //textarea特殊处理,因为textarea可以有节点内容
remain = match;
let addValueAsAttr = remain;
if (tmplCommandAnchorRegTest.test(content)) {
let idx = addValueAsAttr.indexOf('>');
addValueAsAttr = addValueAsAttr.slice(0, idx) + ' value="' + escapeQ(content) + '"' + addValueAsAttr.slice(idx);
}
addAttrs(tag, addValueAsAttr, tmplInfo, refTmplCommands, e);
delete tmplInfo.s; //这3行删除不必要的属性,节省资源
delete tmplInfo.tmpl;
delete tmplInfo.mask;
list.push(tmplInfo);
} else {
//从内容中移除自闭合标签及子模板
let tContent = content.replace(selfCloseTag, '').replace(subReg, '');
let wrapTag;
if (tmplCommandAnchorRegTest.test(tContent)) { //如果剩余有模板命令
//则使用占位符的方式占位
wrapTag = remain = match.replace('>' + content + '<', '>' + g + holder + '<'); //只留包括的标签及占位符
//然后再递归分析子模板
subs.push({
tmpl: content,
ownKeys: ownKeys,
tmplInfo: tmplInfo
});
} else {
//console.log('here', match, content)
//移除后如果没有模板命令,则当前标签最好只有属性里有局部更新
//仍然需要递归子模板
//subs.push({
// tmpl: content
//});
//remain = match; //去除子模板后没有模板命令,则保留所有内容
wrapTag = match.replace('>' + content, '>'); //属性分析时要去除干扰的内容
if (tmplCommandAnchorRegTest.test(content)) {
let info = buildTmpl(content, refTmplCommands, e, extInfo, list, ownKeys, globalKeys);
//console.log(match, '----', content, 'xxx', info.tmpl);
remain = match.replace('>' + content, '>' + escape$(info.tmpl));
} else {
remain = match;
}
delete tmplInfo.tmpl; //删除模板
delete tmplInfo.s; //删除占位
}
//console.log('wrapTag', wrapTag);
//对当前标签分析属性的局部更新
addAttrs(tag, wrapTag, tmplInfo, refTmplCommands, e);
if (!tmplInfo.attr) {
delete tmplInfo.attr;
}
if (!tmplInfo.attr && !tmplInfo.tmpl) { //如果没有属性更新,则删除,减少资源占用
delete tmplInfo.attr;
removeGuids.push(guid);
} else {
list.push(tmplInfo);
}
}
} else { //如果当前标签分析不到数据key,则是不需要局部刷新的节点
removeGuids.push(guid);
}
if (virtualRoot.test(remain)) {
remain = remain.replace(virtualRoot, '$1');
}
return remain;
});
//自闭合
tmpl.replace(selfCloseTag, (match, tag, guid) => {
//match = match.replace(/\u0004/g, '/');
let tmplInfo = {
keys: [],
path: tag + '[' + guid + ']'
};
if (parentOwnKeys) {
let pKeys = Object.keys(parentOwnKeys);
if (pKeys.length) {
tmplInfo.pKeys = pKeys; //记录父结构有哪些keys,当数据变化且在父结构中时,当前结构是不需要去做更新操作的,由父代劳
}
}
//自闭合标签只需要分析属性即可
let kInfo = extractUpdateKeys(match, refTmplCommands, '', parentOwnKeys, extInfo);
if (kInfo.keys.length) { //同样,当包含数据更新的key时才进行深入分析
for (let i = 0, key; i < kInfo.keys.length; i++) {
key = kInfo.keys[i].trim();
tmplInfo.keys.push(key);
}
list.push(tmplInfo);
//属性分析
addAttrs(tag, match, tmplInfo, refTmplCommands, e);
} else { //记录移除的guid
removeGuids.push(guid);
}
});
while (subs.length) { //开始递归调用
let sub = subs.shift();
let i = buildTmpl(sub.tmpl, refTmplCommands, e, extInfo, list, sub.ownKeys, globalKeys);
//if (sub.tmplInfo) {
sub.tmplInfo.tmpl = i.tmpl;
//}
}
tmpl = commandAnchorRecover(tmpl, refTmplCommands); //恢复模板命令
for (let i = removeGuids.length; i >= 0; i--) { //删除没用的guid
tmpl = tmpl.replace(' ' + removeGuids[i], '');
}
//tmpl = tmpl.replace(slashAnchorReg, '/');
return {
list: list,
tmpl: tmpl,
keys: globalKeys
};
};*/
let build = (tmpl, refTmplCommands, e, extInfo) => {
let removedGuids = [];
let modifiers = [];
let list = [];
let g = 0;
let walk = (nodes, parentOwnKeys) => {
for (let n of nodes) {
let ownKeys = Object.create(null);
for (let p in parentOwnKeys) { //继承父结构的keys
ownKeys[p] = parentOwnKeys[p];
}
if (n.guid) {
let guid = 'mx-guid="' + n.guid + '"';
let selector = n.tag + '[' + guid + ']';
if (n.tag == 'mxv-root') {
selector = '#\u001f';
}
let tmplInfo = {
keys: [],
path: selector
};
if (parentOwnKeys) {
let pKeys = Object.keys(parentOwnKeys);
if (pKeys.length) {
tmplInfo.pKeys = pKeys; //记录父结构有哪些keys,当数据变化且在父结构中时,当前结构是不需要去做更新操作的,由父代劳
}
}
let match = tmpl.substring(n.start, n.end);
if (n.hasContent) {
let content = tmpl.substring(n.contentStart, n.contentEnd);
tmplInfo.tmpl = content;
tmplInfo.s = ++g + holder;
tmplInfo.tmplRange = [n.contentStart, n.contentEnd];
let remain = tmpl.substring(n.start, n.contentStart) + tmpl.substring(n.contentEnd, n.end);
let tContent = getContentWithoutGuid(n, tmpl);
let kInfo = newExtractUpdateKeys(remain, refTmplCommands, tContent, parentOwnKeys, extInfo);
if (kInfo.keys.length) {
tmplInfo.keys.push.apply(tmplInfo.keys, kInfo.keys);
//当前节点是view且有子view,则子view的pKeys为当前节点属性和模板中的key
//当前view节点因有子view节点,所以不输出界面
Object.assign(ownKeys, n.hasMxView && n.hasSubView ? kInfo.allKeys : kInfo.tmplKeys);
if (n.tag == 'textarea') { //textarea特殊处理,因为textarea可以有节点内容
if (tmplCommandAnchorRegTest.test(content)) {
let idx = n.contentStart - n.start - 1;
match = match.substring(0, idx) + ' value="' + escapeHTML(content) + '"' + match.substring(idx);
}
addAttrs(n.tag, match, tmplInfo, refTmplCommands, e, false, kInfo);
delete tmplInfo.s; //这3行删除不必要的属性,节省资源
delete tmplInfo.tmpl;
delete tmplInfo.mask;
list.push(tmplInfo);
} else {
//从内容中移除自闭合标签及子模板
if (tmplCommandAnchorRegTest.test(tContent)) { //如果剩余有模板命令
modifiers.push({
replacement: g + holder,
start: n.contentStart,
end: n.contentEnd
});
/*
类似: <div> <%=a%></div>
=> <div>1\u001d</div>
分析属性,同时把内容用占位符占位
*/
} else {
/*
类似<div id="<%=id%>"><span><%=a%></span></div>
只需要分析属性即可
*/
delete tmplInfo.tmpl; //删除模板
delete tmplInfo.s; //删除占位
}
//console.log('wrapTag', wrapTag);
//对当前标签分析属性的局部更新
addAttrs(n.tag, remain, tmplInfo, refTmplCommands, e, n.hasSubView, kInfo);
if (!tmplInfo.attr && !tmplInfo.tmpl) {
removedGuids.push(guid);
} else {
list.push(tmplInfo);
}
}
} else {
removedGuids.push(guid);
}
} else {
//自闭合标签只需要分析属性即可
let kInfo = newExtractUpdateKeys(match, refTmplCommands, '', parentOwnKeys, extInfo);
if (kInfo.keys.length) { //同样,当包含数据更新的key时才进行深入分析
tmplInfo.keys.push.apply(tmplInfo.keys, kInfo.keys);
list.push(tmplInfo);
//属性分析
addAttrs(n.tag, match, tmplInfo, refTmplCommands, e, false, kInfo);
} else { //记录移除的guid
removedGuids.push(guid);
}
}
}
//textarea不需要深入分析
//有内容的节点才需要进入分析
if (n.tag != 'textarea' && n.hasContent && n.children) {
walk(n.children, ownKeys);
}
}
};
let tokens = tmplParser(tmpl, e.shortHTMLFile);
walk(tokens);
let r = getRange(0, tmpl.length, modifiers);
for (let i = r.length, m; i--;) {
m = r[i];
tmpl = tmpl.substring(0, m.start) + m.replacement + tmpl.substring(m.end);
}
tmpl = tmpl.replace(virtualRoot, '$1');
tmpl = commandAnchorRecover(tmpl, refTmplCommands); //恢复模板命令
tmpl = tmpl.replace(loopReg, '');
for (let s of list) {
if (s.tmpl) {
let r = getRange(s.tmplRange[0], s.tmplRange[1], modifiers);
for (let i = r.length, m; i--;) {
m = r[i];
let start = m.start - s.tmplRange[0];
let end = m.end - s.tmplRange[0];
s.tmpl = s.tmpl.substring(0, start) + m.replacement + s.tmpl.substring(end);
}
s.tmpl = commandAnchorRecover(s.tmpl, refTmplCommands);
if (loopReg.test(s.tmpl)) {
//s.l = 1;
s.tmpl = s.tmpl.replace(loopReg, '');
}
}
delete s.tmplRange;
}
for (let g of removedGuids) {
tmpl = tmpl.replace(' ' + g, '');
for (let s of list) {
if (s.tmpl) {
s.tmpl = s.tmpl.replace(' ' + g, '');
}
}
}
return {
tmpl,
list
};
};
module.exports = {
process: build
};