UNPKG

snakeskin

Version:

Компилятор блочных шаблонов c поддержкой наследования.

776 lines (618 loc) 19.7 kB
/*! * API для обработки JS-like конструкций в шаблоне * (фильтры, scope и т.д.) */ (() => { var blackWordMap = { '+': true, '++': true, '-': true, '--': true, '~': true, '~~': true, '!': true, '!!': true, 'break': true, 'case': true, 'catch': true, 'continue': true, 'delete': true, 'do': true, 'else': true, 'false': true, 'finnaly': true, 'for': true, 'function': true, 'if': true, 'in': true, 'of': true, 'instanceof': true, 'new': true, 'null': true, 'return': true, 'switch': true, 'this': true, 'throw': true, 'true': true, 'try': true, 'typeof': true, 'var': true, 'const': true, 'let': true, 'void': true, 'while': true, 'with': true, 'class': true, 'debugger': true, 'interface': true }; var unaryBlackWordMap = { 'new': true, 'typeof': true, 'instanceof': true, 'in': true }; var undefUnaryBlackWordMap = { 'new': true }; var comboBlackWordMap = { 'var': true, 'const': true, 'let': true }; var nextWordCharRgxp = new RegExp(`[${G_MOD}${L_MOD}$+\\-~!${w}[\\]().]`); /** * Вернуть целое слово из заданной строки, начиная с указанной позиции * * @param {string} str - исходная строка * @param {number} pos - начальная позиция * @return {{word: string, finalWord: string, unary: string}} */ DirObj.prototype.getWord = function (str, pos) { var word = '', res = '', nres = ''; var pCount = 0, diff = 0; var start = 0, pContent = null; var unary, unaryStr = ''; for (let i = pos, j = 0; i < str.length; i++, j++) { let el = str.charAt(i); if (pCount || nextWordCharRgxp.test(el) || (el === ' ' && (unary = unaryBlackWordMap[word]))) { if (el === ' ') { word = ''; } else { word += el; } if (unary) { unaryStr = unaryStr || res; unary = false; } if (pContent !== null && (pCount > 1 || (pCount === 1 && !closePMap[el]))) { pContent += el; } if (pMap[el]) { if (pContent === null) { start = j + 1; pContent = ''; } pCount++; } else if (closePMap[el]) { if (pCount) { pCount--; if (!pCount) { if (nres) { nres = nres.substring(0, start + diff) + (pContent && this.prepareOutput(pContent, true, null, null, true)) + nres.substring(j + diff + pContent.length); } else { nres = res.substring(0, start) + (pContent && this.prepareOutput(pContent, true, null, null, true)) + res.substring(j); } diff = nres.length - res.length; pContent = null; } } else { break; } } res += el; if (nres) { nres += el; } } else { break; } } return { word: res, finalWord: nres || res, unary: unaryStr }; }; /** * Вернуть true, если указанное слово является свойством в литерале объекта * * @param {string} str - исходная строка * @param {number} start - начальная позиция слова * @param {number} end - конечная позиция слова * @return {boolean} */ function isSyOL(str, start, end) { var res; for (let i = start; i--;) { let el = str.charAt(i); if (!whiteSpaceRgxp.test(el)) { res = el === '?'; break; } } if (!res) { for (let i = end; i < str.length; i++) { let el = str.charAt(i); if (!whiteSpaceRgxp.test(el)) { return el === ':'; } } } return false; } /** * Вернуть true, если следующий непробельный символ * в указанной строке равен присвоению (=) * * @param {string} str - исходная строка * @param {number} pos - начальная позиция * @return {boolean} */ function isNextAssign(str, pos) { for (let i = pos; i < str.length; i++) { let el = str.charAt(i); if (!whiteSpaceRgxp.test(el)) { return el === '=' && str.charAt(i + 1) !== '='; } } return false; } var unMap = { '!html': true, '!undef': true }; var unUndefLabel = '{undef}', unUndefRgxp = new RegExp(unUndefLabel, 'g'); var ssfRgxp = /__FILTERS__\./; var nextCharRgxp = new RegExp(`[${G_MOD}${L_MOD}$+\\-~!${w}]`), newWordRgxp = new RegExp(`[^${G_MOD}${L_MOD}$${w}[\\].]`); var numRgxp = /[0-9]/, modRgxp = new RegExp(`${L_MOD}(?:\\d+|)`), strongModRgxp = new RegExp(`${L_MOD}(\\d+)`); var multPropRgxp = /\[|\./, firstPropRgxp = /([^.[]+)(.*)/; var propValRgxp = /[^-+!(]+/; var exprimaHackFn = (str) => str .trim() .replace(/^\[(?!\s*])/, '$[') .replace(/\byield\b/g, '') .replace(/(?:break|continue) [_]{2,}I_PROTO__[${w}]+;/, ''); var wrapRgxp = /^\s*\{/, dangerRgxp = /\)\s*(?:{|=>)/, functionRgxp = /\bfunction\b/; /** * Подготовить указанную команду к выводу: * осуществляется привязка к scope и инициализация фильтров * * @param {string} command - исходный текст команды * @param {?boolean=} [opt_sys=false] - если true, то запуск функции считается системным вызовом * @param {?boolean=} [opt_iSys=false] - если true, то запуск функции считается вложенным системным вызовом * @param {?boolean=} [opt_breakFirst=false] - если true, то первое слово в команде пропускается * @param {?boolean=} [opt_validate=true] - если false, то полученная конструкция не валидируется * @return {string} */ DirObj.prototype.prepareOutput = function (command, opt_sys, opt_iSys, opt_breakFirst, opt_validate) { var tplName = this.tplName, struct = this.structure; if (dangerRgxp.test(command)) { this.error('invalid syntax'); return ''; } // ОПРЕДЕЛЕНИЯ: // Скобка = ( var res = command; // Количество открытых скобок в строке // (скобки открытые внутри фильтра не считаются) var pCount = 0; // Количество открытых скобок внутри фильтра: // |foo (1 + 2) / 3 var pCountFilter = 0; // Массив позиций открытия и закрытия скобок (pCount), // идёт в порядке возрастания от вложенных к внешним блокам, например: // ((a + b)) => [[1, 7], [0, 8]] var pContent = []; // true, если идёт декларация фильтра var filterStart = false; // true, если идёт фильтр-враппер, т.е. // (2 / 3)|round var filterWrapper = false; // Массивы итоговых фильтров и истинных фильтров, // например: // {with foo} // {bar |ucfisrt bar()|json} // {end} // // rvFilter => ['ucfisrt bar()', 'json'] // filter => ['ucfisrt foo.bar()', 'json'] var filter = [], rvFilter = []; // true, то можно рассчитывать слово var nword = !opt_breakFirst; // Количество слов для пропуска var posNWord = 0; // Область видимости var scope = this.scope, useWith = Boolean(scope.length); // Сдвиги var addition = 0, wordAddEnd = 0, filterAddEnd = 0; // true, если применяется фильтр !html var unEscape = !this.escapeOutput; // true, если применяется фильтр !undef var unUndef = !this.replaceUndef, globalUnUndef = unUndef; var vars = struct.children ? struct.vars : struct.parent.vars; var ref = this.hasBlock('block', true), type; if (ref) { ref = ref.params.name; type = 'block'; } else if (this.proto) { ref = this.proto.name; type = 'proto'; } if (ref && !scopeCache[type][tplName]) { ref = false; } function search(obj, val, extList) { if (!obj) { return false; } var def = vars[`${val}_${obj.id}`]; if (def) { return def; } else if (extList.length && obj.children[extList[0]]) { return search(obj.children[extList.shift()], val, extList); } return false; } var replacePropVal = (sstr) => { var def = vars[sstr]; if (!def) { let refCache = ref && scopeCache[type][tplName][ref]; if (!refCache || refCache.parent && (!refCache.overridden || this.hasParent('__super__'))) { if (refCache) { def = search(refCache.root, sstr, getExtList(String(tplName))); } let tplCache = tplName && scopeCache['template'][tplName]; if (!def && tplCache && tplCache.parent) { def = search(tplCache.root, sstr, getExtList(String(tplName))); } } if (!def && refCache) { def = vars[`${sstr}_${refCache.id}`]; } if (!def) { def = vars[`${sstr}_${this.module.id}`] || vars[`${sstr}_00`]; } } if (def) { return def.value; } return sstr; }; function addScope(str) { if (multPropRgxp.test(str)) { let fistProp = firstPropRgxp.exec(str); fistProp[1] = fistProp[1].replace(propValRgxp, replacePropVal); str = fistProp.slice(1).join(''); } else { str = str.replace(propValRgxp, replacePropVal); } return str; } if (!command) { this.error('invalid syntax'); return ''; } var commandLength = command.length, end = commandLength - 1; var cacheLink = replacePropVal('$_'); var isFilter, breakNum; for (let i = -1; ++i < commandLength;) { let el = command.charAt(i), next = command.charAt(i + 1), nNext = command.charAt(i + 2); if (!breakNum) { if (el === '(') { // Скобка открылась внутри декларации фильтра if (filterStart) { pCountFilter++; } else { pContent.unshift([i + wordAddEnd]); pCount++; } } // Расчёт scope: // флаг nword показывает, что началось новое слово; // флаг posNWord показывает, сколько новых слов нужно пропустить if (nword && !posNWord && nextCharRgxp.test(el)) { let nextStep = this.getWord(command, i); let word = nextStep.word, finalWord = nextStep.finalWord; let uAdd = wordAddEnd + addition, vres; // true, // если полученное слово не является зарезервированным (blackWordMap), // не является фильтром, // не является числом, // не является константой замены Escaper, // не является названием свойства в литерале объекта ({свойство: ) let canParse = !blackWordMap[word] && !pCountFilter && !ssfRgxp.test(word) && !isFilter && isNaN(Number(word)) && !escaperRgxp.test(word) && !isSyOL(command, i, i + word.length); if (canParse && functionRgxp.test(word)) { this.error('invalid syntax'); return ''; } // Экспорт числовых литералов if (numRgxp.test(el)) { vres = finalWord; // Экспорт глобальный и супер глобальных переменных } else if ((useWith && !modMap[el] || el === G_MOD && (useWith ? next === G_MOD : true)) && canParse) { if (useWith) { vres = next === G_MOD ? finalWord.substring(2) : finalWord; // Супер глобальная переменная внутри with if (next === G_MOD) { vres = `__VARS__${concatProp(vres)}`; } else { vres = addScope(vres); } // Супер глобальная переменная вне with } else { vres = `__VARS__${concatProp(finalWord.substring(next === G_MOD ? 2 : 1))}`; } } else { let rfWord = finalWord.replace(modRgxp, ''); if (canParse && useWith && modMap[el]) { if (el === G_MOD) { rfWord = rfWord.substring(1); } let num = 0; // Уточнение scope if (el === L_MOD) { let val = strongModRgxp.exec(finalWord); num = val ? val[1] : 1; } if (num && (scope.length - num) <= 0) { vres = addScope(rfWord); } else { vres = addScope(scope[scope.length - 1 - num]) + concatProp(rfWord); } } else { if (canParse) { vres = addScope(rfWord); } else if (tplName && rfWord === 'this' && !this.hasParent(this.getGroup('selfThis'))) { vres = '__THIS__'; } else { vres = rfWord; } } } if (canParse && isNextAssign(command, i + word.length) && tplName && constCache[tplName] && constCache[tplName][vres] ) { this.error(`constant "${vres}" is already defined`); return ''; } // Данное слово является составным системным, // т.е. пропускаем его и следующее за ним if (comboBlackWordMap[finalWord]) { posNWord = 2; } else if ( canParse && (!opt_sys || opt_iSys) && !filterStart && (!nextStep.unary || undefUnaryBlackWordMap[nextStep.unary]) && !globalUnUndef ) { vres = `${unUndefLabel}(${vres})`; } wordAddEnd += vres.length - word.length; nword = false; if (filterStart) { let last = filter.length - 1; filter[last] += vres; rvFilter[last] += word; filterAddEnd += vres.length - word.length; } else { res = res.substring(0, i + uAdd) + vres + res.substring(i + word.length + uAdd); } // Дело сделано, теперь с чистой совестью матаем на позицию: // за один символ до конца слова i += word.length - 2; breakNum = 1; continue; // Возможно, скоро начнётся новое слово, // для которого можно посчитать scope } else if (newWordRgxp.test(el)) { nword = true; if (posNWord) { posNWord--; } } if (!filterStart) { if (el === ')') { // Закрылась скобка, а последующие 2 символа не являются фильтром if (next !== FILTER || !filterStartRgxp.test(nNext)) { if (pCount) { pCount--; } pContent.shift(); continue; } else { filterWrapper = true; } } // Составление тела фильтра } else if (el !== ')' || pCountFilter) { let last = filter.length - 1; filter[last] += el; rvFilter[last] += el; } } if (i === end && pCount && !filterWrapper && el !== ')') { this.error('missing closing or opening parenthesis in the template'); return ''; } // Закрылся локальный или глобальный фильтр if (filterStart && !pCountFilter && (el === ')' && !breakNum || i === end)) { let pos = pContent[0]; let fAdd = wordAddEnd - filterAddEnd + addition, fBody = res.substring(pos[0] + (pCount ? addition : 0), pos[1] + fAdd); let arr = []; for (let j = -1; ++j < filter.length;) { let f = filter[j]; if (!unMap[f]) { arr.push(f); if (f.split(' ')[0] === 'default') { unUndef = true; } } else { if (f === '!html' && (!pCount || filterWrapper)) { unEscape = true; } else if (f === '!undef') { unUndef = true; } } } filter = arr; let resTmp = fBody.trim(); if (!resTmp) { resTmp = 'void 0'; } for (let j = -1; ++j < filter.length;) { let params = filter[j].split(' '), input = params.slice(1).join(' ').trim(); let current = params.shift().split('.'), f = ''; for (let k = -1; ++k < current.length;) { f += `['${current[k]}']`; } resTmp = `(${cacheLink} = __FILTERS__${f}` + (filterWrapper || !pCount ? '.call(this,' : '') + resTmp + (input ? ',' + input : '') + (filterWrapper || !pCount ? ')' : '') + ')' ; } resTmp = resTmp.replace(unUndefRgxp, unUndef ? '' : '__FILTERS__.undef'); unUndef = globalUnUndef; let fstr = rvFilter.join().length + 1; res = pCount ? res.substring(0, pos[0] + addition) + resTmp + res.substring(pos[1] + fAdd + fstr) : resTmp; pContent.shift(); filter = []; rvFilter = []; filterStart = false; if (pCount) { pCount--; filterWrapper = false; } wordAddEnd += resTmp.length - fBody.length - fstr; if (!pCount) { addition += wordAddEnd - filterAddEnd; wordAddEnd = 0; filterAddEnd = 0; } } // Закрылась скобка внутри фильтра if (el === ')' && pCountFilter && !breakNum) { pCountFilter--; if (!pCountFilter) { let last = filter.length - 1, cache = filter[last]; filter[last] = this.prepareOutput(cache, true, null, true, false); let length = filter[last].length - cache.length; wordAddEnd += length; filterAddEnd += length; if (i === end) { i--; breakNum = 1; } } } isFilter = el === FILTER; if (breakNum) { breakNum--; } // Через 2 итерации начнётся фильтр if (next === FILTER && filterStartRgxp.test(nNext)) { nword = false; if (!filterStart) { if (pCount) { pContent[0].push(i + 1); } else { pContent.push([0, i + 1]); } } filterStart = true; if (!pCountFilter) { filter.push(nNext); rvFilter.push(nNext); i += 2; } } else if (i === 0 && el === FILTER && filterStartRgxp.test(next)) { nword = false; if (!filterStart) { pContent.push([0, i]); } filterStart = true; if (!pCountFilter) { filter.push(next); rvFilter.push(next); i++; } } } res = res.replace(unUndefRgxp, '__FILTERS__.undef'); if (wrapRgxp.test(res)) { res = `(${res})`; } if (opt_validate !== false) { try { esprima.parse(exprimaHackFn(res)); } catch (err) { this.error(err.message.replace(/.*?: (\w)/, (sstr, $1) => $1.toLowerCase())); return ''; } } return (!unEscape && !opt_sys ? '__FILTERS__.html(' : '') + res + (!unEscape && !opt_sys ? `, ${this.attr}, ${this.attrEscape})` : ''); }; })();