snakeskin
Version:
Компилятор блочных шаблонов c поддержкой наследования.
531 lines (411 loc) • 12.5 kB
JavaScript
/**
* Номер итерации,
* где был декларирован активный шаблон
* @type {number}
*/
DirObj.prototype.startTemplateI = 0;
/**
* Номер строки,
* где был декларирован активный шаблон
* @type {?number}
*/
DirObj.prototype.startTemplateLine = null;
/**
* True, если декларируется шаблон-генератор
* @type {?boolean}
*/
DirObj.prototype.generator = null;
/**
* Название активного шаблона
* @type {?string}
*/
DirObj.prototype.tplName = null;
/**
* Название родительского активного шаблона
* @type {?string}
*/
DirObj.prototype.parentTplName = null;
/**
* Массив декларированных констант
* @type {Array}
*/
DirObj.prototype.consts = null;
/**
* Название родительского BEM класса
* @type {string}
*/
DirObj.prototype.bemRef = '';
var template = ['template', 'interface', 'placeholder'],
scopeModRgxp = new RegExp(`^${G_MOD}+`);
function concatProp(str) {
return str.charAt(0) === '[' ? str : `.${str}`;
}
for (let i = -1; ++i < template.length;) {
Snakeskin.addDirective(
template[i],
{
block: true,
placement: 'global',
notEmpty: true,
group: [
'template',
'rootTemplate',
'define'
]
},
function (command, commandLength, type, jsDoc) {
var lastName = '',
proto = this.proto;
var rank = {
'template': 2,
'interface': 1,
'placeholder': 0
};
this.startDir(
!proto && this.renderAs && rank[this.renderAs] < rank[type] ?
this.renderAs : null
);
var iface =
this.name === 'interface';
this.startTemplateI = this.i + 1;
this.startTemplateLine = this.info['line'];
var nameRgxp = new RegExp(`^[^${symbols}_$[]`, 'i'),
esprimaNameHackRgxp = new RegExp(`[${G_MOD}${L_MOD}]`, 'g');
var tmpTplName = this.getFnName(command),
tplName = this.pasteDangerBlocks(tmpTplName);
if (!proto) {
tmpTplName = this.replaceFileName(tmpTplName);
let prfx = '',
pos;
// Шаблон-генератор
if (/\*/.test(tmpTplName)) {
prfx = '*';
tmpTplName = tmpTplName.replace(prfx, '');
}
tplName = this.pasteDangerBlocks(tmpTplName);
this.generator = Boolean(prfx);
try {
if (!tplName || nameRgxp.test(tplName)) {
throw false;
}
esprima.parse(tplName.replace(esprimaNameHackRgxp, ''));
} catch (ignore) {
return this.error(`invalid "${this.name}" name`);
}
if (tplName === 'init') {
return this.error(`can't declare template "${tplName}", try another name`);
}
this.info['template'] =
this.tplName = tplName;
if (this.name !== 'template' && !write[tplName]) {
write[tplName] = false;
}
// Для возможности удобного пост-парсинга,
// каждая функция снабжается комментарием вида:
// /* Snakeskin template: название шаблона; параметры через запятую */
this.save(
(pos = `/* Snakeskin template: ${tplName}; ${this.getFnArgs(command).join(',').replace(/=(.*?)(?:,|$)/g, '')} */`),
iface,
jsDoc
);
if (jsDoc) {
jsDoc += pos.length;
}
let tmpArr = tmpTplName
.replace(nmssRgxp, '%')
.replace(nmsRgxp, '.%')
.replace(nmeRgxp, '')
.split('.');
let str = tmpArr[0],
length = tmpArr.length,
first = str.charAt(0),
short = '';
if (first === '%') {
try {
str = /* cbws */`['${
applyDefEscape(
this.returnEvalVal(
this.prepareOutput(str.substring(1), true)
)
)
}']`;
} catch (err) {
return this.error(err.message);
}
} else {
short = str;
}
for (let i = 0; ++i < length;) {
let el = tmpArr[i],
custom = el.charAt(0) === '%';
if (custom) {
el = el.substring(1);
}
let def = `this${concatProp(str)}`;
this.save(
(pos = /* cbws */`
if (${def} == null) {
${def} = {};
}
${i === 1 && short ? `var ${short} = ${def};` : ''}
`),
iface,
jsDoc
);
if (jsDoc) {
jsDoc += pos.length;
}
if (custom) {
try {
str += /* cbws */`['${
applyDefEscape(
this.returnEvalVal(
this.prepareOutput(el, true)
)
)
}']`;
} catch (err) {
return this.error(err.message);
}
continue;
} else if (i === length - 1) {
lastName = el;
}
str += `.${el}`;
}
tplName = str;
this.save(`${length === 1 && short ? `var ${short} = ` : ''}this${concatProp(tplName)} = function ${prfx}${length > 1 ? lastName : short}(`, iface);
}
this.info['template'] =
this.tplName = tplName;
this.blockStructure = {
name: 'root',
parent: null,
children: []
};
this.blockTable = {};
this.varCache[tplName] = {};
if (proto) {
this.sysSpace = proto.sysSpace;
this.strongSpace = proto.strongSpace;
this.chainSpace = proto.chainSpace;
this.space = proto.space;
return;
}
// Валидация шаблона для наследования
var parentTplName;
if (/\)\s+extends\s+/.test(command)) {
try {
parentTplName = /\)\s+extends\s+(.*?(?:@|$))/
.exec(command)[1]
.replace(/\s*@$/, '');
if (!parentTplName || nameRgxp.test(parentTplName)) {
throw false;
}
esprima.parse(parentTplName.replace(esprimaNameHackRgxp, ''));
} catch (ignore) {
return this.error(`invalid "${this.name}" name for extend`);
}
try {
parentTplName =
this.parentTplName = this.prepareNameDecl(parentTplName);
} catch (err) {
return this.error(err.message);
}
if (cache[parentTplName] == null) {
if (!this.renderAs || this.renderAs === 'template') {
return this.error(`the specified template "${parentTplName}" for inheritance is not defined`);
}
parentTplName =
this.parentTplName = null;
}
}
this.initTemplateCache(tplName);
if (tplName in extMap) {
clearScopeCache(tplName);
}
let scope = scopeCache['template'],
parent = scope[parentTplName];
scope[tplName] = {
id: this.module.id,
parent: parent,
children: {}
};
scope[tplName].root = parent ?
parent.root : scope[tplName];
if (parent) {
parent.children[tplName] = scope[tplName];
}
argsCache[tplName] = {};
argsResCache[tplName] = {};
outputCache[tplName] = {};
extMap[tplName] = parentTplName;
delete extListCache[tplName];
var baseParams = {};
if (!parentTplName) {
forIn(this.params[this.params.length - 1], (el, key) => {
if (key !== 'renderAs' && key.charAt(0) !== '@' && el !== void 0) {
baseParams[key] = el;
}
});
}
var flags = command.split('@=');
if (parentTplName && flags.length === 1) {
flags.push('@skip true');
}
for (let i = 0; ++i < flags.length;) {
let el = flags[i].trim(),
name = el.split(' ')[0];
if (baseParams[name]) {
delete baseParams[name];
}
Snakeskin.Directions['__setSSFlag__'].call(this, el);
}
forIn(baseParams, (el, key) => {
Snakeskin.Directions['__setSSFlag__'].call(this, [key, el]);
});
var args = this.prepareArgs(command, 'template', tplName, parentTplName);
this.save(`${args.str}) {`, iface);
if (args.scope) {
this.scope.push(args.scope);
}
var predefs = [
'callee',
'blocks',
'getTplResult',
'clearTplResult',
'$0',
'TPL_NAME',
'PARENT_TPL_NAME'
];
for (let i = -1; ++i < predefs.length;) {
this.structure.vars[predefs[i]] = {
value: predefs[i],
scope: 0
};
}
this.save(/* cbws */`
var __THIS__ = this,
__CALLEE__ = __ROOT__${concatProp(tplName)},
callee = __CALLEE__;
if (!callee.Blocks) {
var __BLOCKS__ = __CALLEE__.Blocks = {},
blocks = __BLOCKS__;
}
var __RESULT__ = ${this.declResult()},
__COMMENT_RESULT__,
__NODE__,
$0;
function getTplResult(opt_clear) {
var res = ${this.returnResult()};
if (opt_clear) {
__RESULT__ = ${this.declResult()};
}
return res;
}
function clearTplResult() {
__RESULT__ = ${this.declResult()};
}
var __RETURN__ = false,
__RETURN_VAL__;
var TPL_NAME = "${escapeDoubleQuote(tplName)}",
PARENT_TPL_NAME${parentTplName ? ` = "${escapeDoubleQuote(parentTplName)}"` : ''};
${args.defParams}
`);
var preDefs = this.preDefs[tplName];
// Подкючение внешних блоков и прототипов
if ((!extMap[tplName] || parentTplName) && preDefs) {
this.source = this.source.substring(0, this.i + 1) +
preDefs.text +
this.source.substring(this.i + 1);
delete this.preDefs[tplName];
}
},
function (command, commandLength) {
var tplName = String(this.tplName),
proto = this.proto;
if (proto) {
// Вызовы не объявленных прототипов внутри прототипа
if (this.backTableI) {
let ctx = proto.ctx;
ctx.backTableI += this.backTableI;
forIn(this.backTable, (arr, key) => {
for (let i = -1; ++i < arr.length;) {
let el = arr[i];
el.pos += proto.pos;
el.outer = true;
el.vars = this.structure.vars;
}
ctx.backTable[key] = ctx.backTable[key] ?
ctx.backTable[key].concat(arr) : arr;
});
}
return;
}
var diff = this.getDiff(commandLength);
cache[tplName] = this.source.substring(this.startTemplateI, this.i - diff);
table[tplName] = this.blockTable;
// Обработка наследования:
// тело шаблона объединяется с телом родителя
// и обработка шаблона начинается заново,
// но уже как атомарного (без наследования)
if (this.parentTplName) {
this.info['line'] = this.startTemplateLine;
this.lines.splice(this.startTemplateLine, this.lines.length);
this.source = this.source.substring(0, this.startTemplateI) +
this.getFullBody(tplName) +
this.source.substring(this.i - diff);
this.initTemplateCache(tplName);
this.startDir(this.structure.name);
this.i = this.startTemplateI - 1;
this.parentTplName = null;
this.blockTable = {};
this.varCache[tplName] = {};
return;
}
// Вызовы не объявленных прототипов
if (this.backTableI) {
forIn(this.backTable, (arr, key) => {
for (let i = -1; ++i < arr.length;) {
let el = arr[i];
if (!el.outer) {
continue;
}
let tmp = protoCache[tplName][key];
if (!tmp) {
return this.error(`proto "${key}" is not defined`);
}
this.res = this.res.substring(0, el.pos) +
this.res.substring(el.pos).replace(
el.label,
(el.argsStr || '') + (el.recursive ? tmp.i + '++;' : tmp.body)
);
}
});
this.backTable = {};
this.backTableI = 0;
}
var iface = this.structure.name === 'interface';
if (iface) {
this.save('};', true);
} else {
this.save(/* cbws */`
${this.consts.join('')}
return ${this.returnResult()};
};
Snakeskin.cache["${escapeDoubleQuote(tplName)}"] = this${concatProp(tplName)};
`);
}
this.save('/* Snakeskin template. */', iface);
if (this.params[this.params.length - 1]['@tplName'] === this.tplName) {
this.popParams();
}
this.canWrite = true;
this.tplName = null;
delete this.info['template'];
if (this.scope.length) {
this.scope.pop();
}
}
);
}