raz
Version:
Razor like HTML template engine for NodeJS Express library based on ASP.NET MVC Razor syntax. Template your views by mixing HTML markup with JavaScript server-side code!
1,204 lines (1,061 loc) • 61.5 kB
JavaScript
'use strict';
require('./utils');
function compilePageSync(html, model, viewData, scope, isDebugMode) {
let vm = html._vm;
if (vm) {
let sandbox = html._sandbox;
// Creates cope variables.
if (scope) {
Object.keys(scope).forEach((k) => {
defineConstant(sandbox, k, scope[k]);
});
}
defineConstant(sandbox, "Html", html);
defineConstant(sandbox, "Model", model);
defineConstant(sandbox, "ViewData", viewData);
defineConstant(sandbox, "debug", isDebugMode);
vm.runInNewContext(html._js, sandbox);
}
else {
const argNames = ["Html", "Model", "ViewData", "debug"];
const argValues = [html, model, viewData, isDebugMode];
if (scope) {
// Add cope variables (we should but can't make them constants because of `eval` limitation in sctict-mode).
Object.keys(scope).forEach((k) => {
argNames.push(k);
argValues.push(scope[k]);
});
}
// Put the JS-scipt to be executed.
argNames.push(html._js);
// Execute JS-script via function with arguments.
Function.apply(undefined, argNames).apply(undefined, argValues);
}
function defineConstant(obj, name, value) {
Object.defineProperty(obj, name, {
value,
writable: false
});
}
}
function compilePage(html, model, viewData, scope, isDebugMode, done) {
try {
compilePageSync(html, model, viewData, scope, isDebugMode);
return html.__renderLayout(done);
}
catch (exc) {
done(exc);
}
}
module.exports = function (opts) {
opts = opts || {};
const dbg = require('../core/dbg/debugger');
const debugMode = dbg.isDebugMode;
const isBrowser = dbg.isBrowser;
const log = opts.log || { debug: () => { } };
log.debug(`Parser debug mode is '${!!debugMode}'.`);
const HtmlString = require('./HtmlString');
const htmlEncode = require('./libs/js-htmlencode');
////////////////////
/// Html class
////////////////////
function Html(args) {
this._vm = null;
if (debugMode && !isBrowser) {
this._vm = require('vm');
this._sandbox = Object.create(null);
this._vm.createContext(this._sandbox);
}
// function (process,...){...}() prevents [this] to exist for the 'vm.runInNewContext()' method
this._js = `
'use strict';
(function (process, window, global, module, require, compilePage, compilePageSync, navigator, undefined) {
delete Html._js;
delete Html._vm;
delete Html._sandbox;
${args.js}
}).call();`;
// User section.
if (debugMode)
this.__dbg = { viewName: args.filePath, template: args.template, pos: [] }
this.$ =
this.layout = null;
// Private
let sectionName = null;
let sections = args.parsedSections;
this.__val = function (i) {
return args.jsValues.getAt(i);
};
this.__renderLayout = (done) => {
if (!this.layout) // if the layout is not defined..
return Promise.resolve().then(() => done(null, args.html)), null;
// looking for the `Layout`..
args.er.isLayout = true; // the crutch
args.findPartial(this.layout, args.filePath, args.er, (err, result) => {
args.er.isLayout = false;
if (err) return done(err);
let compileOpt = {
scope: args.scope,
template: result.data,
filePath: result.filePath,
model: args.model,
bodyHtml: args.html,
findPartial: args.findPartial,
findPartialSync: args.findPartialSync,
parsedSections: args.parsedSections,
partialsCache: args.partialsCache,
viewData: args.viewData
};
compile(compileOpt, done);
});
};
this.__sec = function (name) { // in section
if (!sectionName) {
sectionName = name;
}
else if (sectionName === name) {
sections[sectionName][args.filePath].compiled = true;
sectionName = null;
}
else {
throw new Error(`Unexpected section name = '${name}'.`); // Cannot be tested via user-inputs.
}
};
this.raw = function (val) { // render
if (!isVisibleValue(val)) // 'undefined' can be passed when `Html.raw()` is used by user in the view, in this case it will be wrapped into `Html.ecnode()` anyway an it will call `Html.raw` passing 'undefined' to it.
return;
if (sectionName) {
let sec = sections[sectionName][args.filePath];
if (!sec.compiled) // it could have been compiled already if it's defined in a partial view which is rendred more than once
sec.html += val;
}
else {
args.html += val;
}
};
this.encode = function (val) {
var encoded = this.getEncoded(val);
this.raw(encoded);
};
this.getEncoded = function (val) {
if (!isVisibleValue(val))
return '';
if (typeof val === "number" || val instanceof Number || val instanceof HtmlString)
return val;
if (String.is(val))
return htmlEncode(val);
return htmlEncode(val.toString());
};
this.body = function () {
return new HtmlString(args.bodyHtml);
};
this.section = function (name, required) {
if (!args.filePath)
throw new Error("'args.filePath' is not set.");
let secGroup = sections[name];
if (secGroup) {
if (secGroup.renderedBy)
throw args.er.sectionsAlreadyRendered(name, secGroup.renderedBy, args.filePath); // TESTME:
let html = '';
for (var key in secGroup) {
if (secGroup.hasOwnProperty(key)) {
let sec = secGroup[key];
if (!sec.compiled)
throw args.er.sectionIsNotCompiled(name, args.filePath); // [#3.2]
html += sec.html;
}
}
secGroup.renderedBy = args.filePath;
return new HtmlString(html);
}
else {
if (required)
throw args.er.sectionIsNotFound(name, args.filePath); // [#3.3]
}
return '';
};
this.getPartial = function (viewName, viewModel) {
let compileOpt = {
scope: args.scope,
model: viewModel === undefined ? args.model : viewModel, // if is not set explicitly, set default (parent) model
findPartial: args.findPartial,
findPartialSync: args.findPartialSync,
sections,
parsedSections: args.parsedSections,
partialsCache: args.partialsCache,
viewData: args.viewData
};
// Read file and complie to JS.
let partial = args.findPartialSync(viewName, args.filePath, args.er, args.partialsCache);
compileOpt.template = partial.data;
compileOpt.filePath = partial.filePath;
if (partial.js) { // if it's taken from cache
compileOpt.js = partial.js;
compileOpt.jsValues = partial.jsValues;
}
let { html, precompiled } = compileSync(compileOpt);
partial.js = precompiled.js; // put to cache
partial.jsValues = precompiled.jsValues; // put to cache
return html;
};
this.partial = function (viewName, viewModel) {
var partialHtml = this.getPartial(viewName, viewModel);
this.raw(partialHtml)
};
}
class Block {
constructor(type, name) {
this.type = type;
if (name)
this.name = name;
this.text = '';
}
append(ch) {
this.text += ch;
//this.text += (ch === '"') ? '\\"' : ch;
}
toScript(jsValues) {
return toScript(this, jsValues);
}
}
function isVisibleValue(val) {
return (val != null && val !== '');
}
function toScript(block, jsValues) {
if (block.type === blockType.section) {
let secMarker = `\r\nHtml.__sec("${block.name}");`;
let script = secMarker;
for (let n = 0; n < block.blocks.length; n++) {
let sectionBlock = block.blocks[n];
script += toScript(sectionBlock, jsValues);
}
script += secMarker;
return script;
}
else {
let i;
switch (block.type) {
case blockType.html:
i = jsValues.enq(block.text);
return "\r\nHtml.raw(Html.__val(" + i + "));";
case blockType.expr:
i = jsValues.enq(block.text);
let code = `Html.encode(eval(Html.__val(${i})));`;
return debugMode ? setDbg(code, block) : "\r\n" + code;
case blockType.code:
return debugMode ? setDbg(block.text, block) : "\r\n" + block.text;
default:
throw new Error(`Unexpected block type = "${blockType}".`);
}
}
throw new Error(`Unexpected code behaviour, block type = "${blockType}".`);
}
function setDbg(code, block) {
return `
Html.__dbg.pos = { start:${block.posStart}, end: ${block.posEnd} };
${code}
Html.__dbg.pos = null;`;
}
class Queue {
constructor() {
this._items = [];
}
enq(item) {
//if (opts.debug) log.debug(item);
return this._items.push(item) - 1;
}
getAt(i) {
if (opts.debug) {
let item = this._items[i];
//log.debug(item);
return item;
}
else {
return this._items[i];
}
}
}
const _sectionKeyword = "section";
//const _functionKeyword = "function";
const blockType = { none: 0, html: 1, code: 2, expr: 3, section: 4 };
const ErrorsFactory = require('./errors/errors');
const voidTags = "area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr".toUpperCase().split("|").map(s => s.trim());
////////////////
// PARSER //
////////////////
class Parser {
constructor(args) {
args.filePath = args.filePath || "js-script";
let linesBaseNumber = (debugMode && opts.express) ? 0 : 1; // in debug-mode the file-path of a template is added as a very first line comment
this.args = args;
this.er = new ErrorsFactory({ filename: args.filePath, jshtml: args.template }, linesBaseNumber);
}
compile(done) {
log.debug();
let errorFactory = this.er;
try {
var htmlObj = this.getHtml({}, done);
}
catch (exc) {
return error(exc);
}
compilePage(htmlObj, this.args.model, this.args.viewData, this.args.scope, debugMode, (err, html) => {
if (err)
return error(err, htmlObj.__dbg);
try {
this.checkSections();
}
catch (exc) {
return error(exc, htmlObj.__dbg);
}
return done(null, html);
});
function error(err, dbg) {
err.__dbg = dbg;
var parserError = toParserError(err, errorFactory);
return Promise.resolve().then(() => done(parserError)), null;
}
}
compileSync() {
try {
log.debug();
var htmlArgs = {};
var html = this.getHtml(htmlArgs);
compilePageSync(html, this.args.model, this.args.viewData, this.args.scope, debugMode);
this.checkSections();
}
catch (exc) {
exc.__dbg = html && html.__dbg;
throw toParserError(exc, this.er);
}
return { html: htmlArgs.html, precompiled: { js: htmlArgs.js, jsValues: htmlArgs.jsValues } };
}
getHtml(htmlArgs) {
log.debug(this.args.filePath);
// extract scope..
var model = this.args.model;
if (model && model.$) {
this.args.scope = model.$;
delete model.$;
}
this.args.parsedSections = this.args.parsedSections || {};
this.args.viewData = this.args.viewData || this.args.ViewData || {};
this.args.partialsCache = this.args.partialsCache || {};
let js = this.args.js;
let jsValues = this.args.jsValues;
let template = this.args.template;
if (!js) {
var isString = String.is(template);
if (!isString)
throw new Error(ErrorsFactory.templateShouldBeString);
this.text = template;
this.line = '', this.lineNum = 0, this.pos = 0, this.padding = '';
this.inSection = false;
this.blocks = [];
this.parseHtml(this.blocks);
jsValues = new Queue();
var scripts = this.blocks.map(b => b.toScript(jsValues));
js = scripts.join("");
}
Object.assign(htmlArgs, {
html: '',
jsValues,
js,
template,
er: this.er
});
Object.assign(htmlArgs, this.args);
var html = new Html(htmlArgs);
return html;
}
// Check if all sections have been rendered.
checkSections() {
if (!this.args.root)
return;
let sections = this.args.parsedSections;
for (var key in sections) {
if (sections.hasOwnProperty(key)) {
let secGroup = sections[key];
if (!secGroup.renderedBy) {
let sec = secGroup[Object.keys(secGroup)[0]]; // just any section from the group
throw this.er.sectionNeverRendered(key, sec.filePath);
}
}
}
}
parseHtml(blocks, outerWaitTag) {
log.debug();
const docTypeName = "!DOCTYPE";
const textQuotes = `'"\``;
var quotes = [];
const tagKinds = { open: 0, close: 1, selfclose: 2 };
var openTags = [];
var tag = '', lineLastLiteral = '', lastLiteral = '';
var block = this.newBlock(blockType.html, blocks);
let stop = false, inComments = false;
let inJs = "script".equal(outerWaitTag, true);
var lastCh = '';
for (var ch = this.pickChar(); ch; ch = this.pickChar()) {
let isSpace = Char.isWhiteSpace(ch);
let nextCh = this.pickNextChar();
let inQuotes = (quotes.length > 0);
if (inComments) {
if (ch === '-') {
if (!tag || tag === '-')
tag += ch;
else
tag = '';
}
else if (ch === '>') {
if (tag === '--')
inComments = false;
tag = '';
}
else {
tag = '';
}
}
else if (ch === '@') {
if (nextCh === '@') { // checking for '@@' that means just text '@'
ch = this.fetchChar(); // skip the next '@'
nextCh = this.pickNextChar();
}
else {
this.fetchChar();
this.parseCode(blocks);
if (tag === '<' || tag === '</')
tag = '';
block = this.newBlock(blockType.html, blocks);
continue;
}
}
else if (inQuotes) {
if (tag) tag += ch;
if (textQuotes.indexOf(ch) !== -1) { // it could be closing text qoutes
if (quotes.length && quotes[quotes.length - 1] === ch) {
quotes.pop(); // collasping quotes..
inQuotes = false;
}
}
}
else if ((tag || inJs) && textQuotes.indexOf(ch) !== -1) { // it could be opening text qoutes within a tag's attributes or a JS-block
quotes.push(ch);
inQuotes = true;
if (tag) tag += ch;
}
else if (ch === '-') {
if (tag.length > 1) { // at least '<!'
if (lastCh === '!') {
tag += ch;
}
else if (tag.startsWith("!-", 1)) {
tag = '';
inComments = true;
}
}
else {
tag = '';
}
}
else if (ch === '<') {
tag = ch; // if '<' occurs more than once, all the previous ones are considered as a plain text by default
}
else if (ch === '/') {
if (tag) {
if (tag[tag.length - 1] === '/') { // tag should be at least '<a'
tag = ''; // it's just a text (not a tag)
}
else {
tag += ch;
}
}
}
else if (ch === '>') {
if (tag) {
if (tag.length === 1 || tag.length === 2 && lastCh === '/' || tag.startsWith(docTypeName, 1)) { // tag should be at least '<a' or '<a/'
tag = ''; // it was just text
}
else {
tag += ch;
let tagName = getTagName(tag);
let tagKind = tag.startsWith("</")
?
tagKinds.close
:
tag.endsWith("/>") || voidTags.includes(tagName.toUpperCase())
?
tagKinds.selfclose
:
tagKinds.open;
if (tagKind === tagKinds.close) {
let openTag = openTags.pop();
if (openTag) { // if we have an open tag we must close it before we can go back to the caller method
if (openTag.name.toUpperCase() !== tagName.toUpperCase())
throw this.er.missingMatchingStartTag(tag, this.lineNum, this.linePos() - tag.length + 1); // tested by "Invalid-HTML 1+, 2+, 7"
// else they are neitralizing each other..
if ("script".equal(tagName, true))
inJs = false;
}
else if (outerWaitTag && outerWaitTag === tagName) {
this.stepBack(blocks, tag.length - 1);
break;
}
else {
throw this.er.missingMatchingStartTag(tag, this.lineNum, this.linePos() - tag.length + 1); // tested by "Invalid-HTML 4", "Code 22"
}
}
else if (tagKind === tagKinds.open) {
inJs = "script".equal(tagName, true);
openTags.push({ tag: tag, name: tagName, lineNum: this.lineNum, linePos: this.linePos() - tag.length + 1 });
}
else {
// just do nothing (self-close tag)
}
tag = '';
}
}
}
else if (isSpace) {
if (tag) { // within a tag
if (lastCh === '<' || lastCh === '/') // '<' or // '</' or '<tag/'
tag = ''; // reset tag (it was just a text)
else
tag += ch;
}
}
else if (!openTags.length && ch === '}' && (lastLiteral === '>'/* || !block.text*/)) { // the close curly bracket can follow only a tag (not just a text)
this.stepBack(blocks, 0);
stop = true;
break; // return back to the callee code-block..
}
else { // any other character
if (tag)
tag += ch; // tag's insides
}
if (isSpace) {
if (ch === '\n') {
lineLastLiteral = '';
this.flushPadding(blocks);
block.append(ch);
}
else { // it's a true- space or tab
if (lineLastLiteral) // it's not the beginning of the current line
block.append(ch);
else // it is the beginning of the line
this.padding += ch; // we still don't know whether this line is going to be a code or HTML
}
}
else {
this.flushPadding(blocks);
block.append(ch);
lastLiteral = lineLastLiteral = ch;
}
lastCh = ch;
this.fetchChar();
}
if (openTags.length) {
let openTag = openTags[openTags.length - 1];
throw this.er.missingMatchingEndTag(openTag.tag, openTag.lineNum, openTag.linePos); // tested by "Invalid-HTML 3"
}
if (!stop)
this.flushPadding(blocks);
this.removeEmptyBlock();
}
parseHtmlInsideCode(blocks) {
log.debug();
const textQuotes = '\'"';
var quotes = [];
var tag = '', openTag = '', openTagName = '', lineLastLiteral = '';
let openTagLineNum, openTagPos;
var block = this.newBlock(blockType.html, blocks);
var lastCh = '';
let stop = false, inComments = false, inJs = false;
for (var ch = this.pickChar(); !stop && ch; ch = ch && this.pickChar()) {
var nextCh = this.pickNextChar();
let isSpace = Char.isWhiteSpace(ch);
if (inComments) {
if (!tag) {
if (ch === '-')
tag = ch;
}
else if (tag.length === 1) {
if (ch === '-')
tag += ch;
}
else if (ch === '>') {
tag = '';
inComments = false;
}
}
else if (ch === '@') {
if (String.isWhiteSpace(block.text)) {
// In contrast to a base-HTML-block, here it can only start with an HTML-tag.
throw this.er.unexpectedCharacter(ch, this.lineNum, this.linePos(), this.line); // Cannot be tested.
}
if (this.pickNextChar() === '@') { // checking for '@@' that means just text '@'
ch = this.fetchChar(); // skip the next '@'
}
else if (openTagName || tag) { // it must be an expression somewhere inside HTML
this.fetchChar(); // skip current '@'
this.parseCode(blocks);
if (tag && (tag === '<' || tag === '</'))
tag += '@' + blocks[blocks.length - 1].text + this.padding; // just to be considered as a tag later (for the case of '<@tag>')
block = this.newBlock(blockType.html, blocks);
continue;
}
else {
throw this.er.unexpectedAtCharacter(this.lineNum, this.linePos()); // [Section 0]
}
}
else if (quotes.length) { // In Quotes..
if (tag) tag += ch;
// if in ".." (it's possible only inside the first tag or between tags)
if (textQuotes.indexOf(ch) !== -1) { // it could be the closing text qoutes
if (quotes[quotes.length - 1] === ch) {
quotes.pop(); // collasping quotes..
}
}
}
else if ((tag || inJs) && textQuotes.indexOf(ch) !== -1) { // Open Quotes..
if (tag) tag += ch;
quotes.push(ch);
}
else if (ch === '-') {
if (tag.length > 1) { // at least '<!'
if (lastCh === '!') {
tag += ch;
}
else if (tag.startsWith("!-", 1)) {
tag = '';
inComments = true;
}
}
else {
tag = '';
}
}
else if (ch === '<') {
if (tag)
throw this.er.unexpectedCharacter(ch, this.lineNum, this.line.length, this.line + ch); // tested by "Invalid-HTML 8"
if (openTagName) { // it should be a close-tag or another nested html-block
if (nextCh !== '/') { // it should be the next nested block of HTML which should be parsed with the normal `parseHtml` method.
processInnerHtml.call(this);
continue;
}
}
// ELSE it must be the begining an open-tag or a self-close, however it will be a new one on the same deep-level
tag = ch;
}
else if (ch === '/') {
// So, it must be..
if (tag) {
if (nextCh === '/') { // it can be only considered as html, only as a text-fragment, so parse it as the next level of html..
// '<//' or '<a //>', smarter than MS-RAZOR :)
processInnerHtml.call(this);
continue;
}
// closing- or self-closing tag ..
// '<' or `<a` at least
tag += ch;
}
else {
processInnerHtml.call(this);
continue;
}
}
else if (ch === '>') {
if (tag) {
tag += ch;
if (openTagName) {
if (tag.length > 2) { // it's a close-tag, at least `</a`
let tagName = getTagName(tag);
if (openTagName.toUpperCase() !== tagName.toUpperCase())
throw this.er.missingMatchingStartTag(tag, this.lineNum, this.linePos() - tag.length + 1); // tested by "Code 22"
openTag = openTagName = ''; // open-tag is closed
if ("script".equal(tagName, true))
inJs = false;
}
}
else {
let tagName = getTagName(tag);
if (tag[tag.length - 2] === '/' || voidTags.includes(tagName.toUpperCase())) {
// it's a self-close tag... nothing to do
}
else if (tag.length > 2) { // it's an open-tag, at least `<a>`
if (tag[1] === '/') // it's a close-tag, unexpected..
throw this.er.missingMatchingStartTag(tag, this.lineNum, this.linePos() - tag.length + 1); // tested by "Invalid-HTML 5"
inJs = "script".equal(tagName, true);
openTag = tag;
openTagName = tagName;
openTagPos = this.linePos() - tag.length + 1;
openTagLineNum = this.lineNum;
}
else
throw this.er.tagNameExpected(this.lineNum, this.linePos()); // tested by "Code 28"
}
tag = ''; // reset it & go on..
}
}
else if (isSpace) {
if (tag) { // within a tag
if (lastCh === '<' || lastCh === '/') // '<' or '</' or '<tag/'
throw this.er.tagNameExpected(this.lineNum, this.linePos()); // tests: "Code 33", "Code 34"
else
tag += ch;
}
}
else { // any other character
if (tag) {
tag += ch;
}
else if (openTagName) { // even if it is '}' it will be considered as a plain text
processInnerHtml.call(this);
continue;
}
else {
// it should be returned back to code-block
//this.stepBack(); // step back before `<` literal
ch = '';
stop = true;
}
}
if (ch) {
if (isSpace) {
if (ch === '\n') {
lineLastLiteral = '';
this.flushPadding(blocks);// flash padding buffer in case this whole line contains only whitespaces ..
block.append(ch);
}
else { // it's a true- space or tab
if (lineLastLiteral) // it's not the beginning of the current line
block.append(ch);
else // it is the beginning of the line
this.padding += ch; // we still don't know whether this line is going to be a code or HTML
}
}
else {
this.flushPadding(blocks);
block.append(ch);
lineLastLiteral = ch;
}
lastCh = ch[ch.length - 1];
}
!stop && this.fetchChar();
}
if (openTagName)
throw this.er.missingMatchingEndTag(openTag, openTagLineNum, openTagPos); // tested by "Invalid-HTML 6", "Code 20", "Code 31"
if (!stop)
this.flushPadding(blocks);
function processInnerHtml() {
this.stepBack(blocks, tag.length);
this.parseHtml(blocks, openTagName);
block = this.newBlock(blockType.html, blocks);
tag = lastCh = lineLastLiteral = '';
}
}
parseCode(blocks) {
log.debug();
var ch = this.pickChar();
if (!ch)
throw Error(this.er.endOfFileFoundAfterAtSign(this.lineNum, this.linePos())); // tests: "Code 39"
if (ch === '{') {
this.parseJsBlock(blocks);
}
else if (canExpressionStartWith(ch)) {
this.parseJsExpression(blocks);
}
else {
throw this.er.notValidStartOfCodeBlock(ch, this.lineNum, this.linePos()); // tests: "Code 40"
}
}
parseJsExpression(blocks) {
log.debug();
const startScopes = '([';
const endScopes = ')]';
const textQuotes = '\'"`/';
var waits = [];
var wait = null;
var firstScope = null;
let lastCh = '';
let padding = this.padding;
this.padding = '';
let block = this.newBlock(blockType.expr, blocks);
var checkForBlockCode = false;
let inText = false;
let operatorName = '';
for (var ch = this.pickChar(); ch; ch = this.pickChar()) { // pick or fetch ??
if (checkForBlockCode) {
if (Char.isWhiteSpace(ch)) {
this.padding += ch;
}
else if (ch === '{') {
block.type = blockType.code;
return this.parseJsBlock(blocks, block, operatorName);
}
else {
break;
}
}
else if (inText) {
if (textQuotes.indexOf(ch) !== -1) { // it's some sort of text qoutes
if (ch === wait) {
wait = waits.pop(); // collasping quotes..
inText = false;
}
}
}
else { // if not (inText)
let pos = startScopes.indexOf(ch);
// IF it's a start-scope literal
if (pos !== -1) { // ch === '(' || ch === '['
if (!firstScope) {
wait = firstScope = endScopes[pos];
operatorName = block.text.trim();
}
else {
if (wait)
waits.push(wait);
wait = endScopes[pos];
}
}
else if (wait) {
if (endScopes.indexOf(ch) !== -1) {
if (wait === ch) {
wait = waits.pop(); // collasping scope..
checkForBlockCode = (!wait && ch === firstScope && ch !== ']'); // can continue with "[1,2,3].toString()"
}
else {
throw this.er.invalidExpressionChar(ch, this.lineNum, this.pos, this.line); // Tests: "Code 41".
}
}
else if (textQuotes.indexOf(ch) !== -1 && (ch !== '/' || lastCh !== '<')) { // it's some sort of text qoutes (exclude the case when ch='/' and it's a tag start '</')
wait && waits.push(wait);
wait = ch;
inText = true; // put on waits-stack
}
}
else if (block.text) {
let nextCh = this.pickNextChar();
if (Char.isWhiteSpace(lastCh) && !Char.isWhiteSpace(ch)) {
let op = block.text.trim();
if (!['function', 'class', 'try'].some(e => e == op))
break; // [Code 63]: <span>@year is a leap year.</span>
}
if (!canExpressionEndWith(ch)) {
if (Char.isWhiteSpace(ch) || ch === '{') {
if (checkForSection.call(this))
return;
else if (ch === '{') {
let op = block.text.trim();
if (['do', 'try'].some(e => e == op)) {
operatorName = op;
checkForBlockCode = true;
continue;
}
break;
}
}
else if (ch === '.') { // @Model.text
if (!nextCh || !canExpressionEndWith(nextCh))
break;
}
else {
break;
}
}
}
}
if (Char.isWhiteSpace(ch)) {
if (!checkForBlockCode)
this.padding += ch;
}
else {
if (!checkForBlockCode)
this.flushPadding(blocks);
block.append(ch);
}
lastCh = ch;
this.fetchChar();
}
if (wait)
throw this.er.expressionMissingEnd('@' + block.text, wait, this.lineNum, this.linePos()); // Tests: "Code 42".
if (!block.text)
throw this.er.invalidExpressionChar(ch, this.lineNum, this.linePos(), this.line); // Seems to be impossible.
flushDeferredPadding(blocks); // there is no sense to put padding to the expression text since it will be lost while evaluating
function flushDeferredPadding(blocks) {
if (!padding)
return;
let prevBlock = blocks[blocks.length - 2];
prevBlock.text += padding;
}
function checkForSection() {
let keyword = block.text.trim();
if (keyword === _sectionKeyword) {
this.blocks.pop();
this.parseSection();
return true;
}
return false;
}
}
parseJsBlock(blocks, block, operatorName) {
log.debug();
const startScopes = '{([';
const endScopes = '})]';
const textQuotes = '\'"`/';
var lastCh = '', lastLiteral = '', lineLastLiteral = '';
var waits = [];
var wait = null;
var firstScope = null;
var stop = false;
let skipCh = true;
let inText = false;
let hasOperator = !!block;
block = block || this.newBlock(blockType.code, blocks);
let firstLine = this.line, firstLineNum = this.lineNum, trackFirstLine = true;
let waitOperator = null, waitAcc = '', operatorExpectScope;
for (var ch = this.pickChar(); !stop && ch; ch = this.pickChar()) { // pick or fetch ??
if (trackFirstLine) {
trackFirstLine = (ch !== '\n');
if (trackFirstLine)
firstLine += ch;
}
skipCh = false;
if (waitOperator && ch !== '<' && ch !== '}' && ch !== operatorExpectScope) {
if (!Char.isWhiteSpace(ch)) {
waitAcc += ch;
if (waitOperator.startsWith(waitAcc)) {
if (waitOperator === waitAcc) {
operatorName = waitOperator;
waitOperator = null;
if (["while", "catch", "if"].some(e => waitAcc === e)) {
operatorExpectScope = '(';
}
else if ("finally" === waitAcc) {
operatorExpectScope = '{';
}
else if ("else" === waitAcc) {
operatorExpectScope = '{';
waitOperator = 'if';
waitAcc = '';
}
}
}
else {
waitOperator = null; // outer html (end of code block)
this.stepBack(blocks, waitAcc.length - 1); // [Code 66]
break;
}
}
else if (waitAcc) { // there shouldn't be any spaces within the 'waitOperator'
if (waitOperator === "while")
throw this.er.wordExpected(waitOperator, this.lineNum, this.linePos() - waitAcc.length); // [Code 59]
this.stepBack(blocks, waitAcc.length);
break;
}
}
else if (inText) {
if (textQuotes.indexOf(ch) !== -1) { // it's some sort of text qoutes
if (ch === wait) {
wait = waits.pop(); // collasping quotes..
inText = false;
}
}
}
else { // if not (inText)
if (!firstScope && ch !== '{')
throw this.er.characterExpected('{', this.lineNum, this.linePos());
if (operatorExpectScope && !Char.isWhiteSpace(ch) && ch !== operatorExpectScope) {
if (!waitOperator)
throw this.er.characterExpectedAfter(operatorExpectScope, this.lineNum, this.linePos(), operatorName); // [Code 58, Code 66.1, Code 67.1]
}
let pos = startScopes.indexOf(ch);
// IF it's a start-scope literal
if (pos !== -1) {
if (!firstScope) {
wait = firstScope = endScopes[pos];
skipCh = !hasOperator; // skip the outer {} of the code-block
}
else {
if (wait) waits.push(wait);
wait = endScopes[pos];
}
if (operatorExpectScope == ch) {
//firstScope = wait;
operatorExpectScope = null;
waitOperator = null;
}
}
else if (wait) {
if (endScopes.indexOf(ch) !== -1) { // IF it's an end-scope literal
if (wait === ch) {
wait = waits.pop(); // collasping scope..
if (/*!wait && */(operatorName !== "if" || ch === firstScope)) {
if (ch === '}') { // the last & closing scope..)
switch (operatorName) {
case "try":
waitOperator = "catch";
break;
case "catch":
waitOperator = "finally";
break;
case "if":
waitOperator = "else";
//firstScope = null;
break;
case "do":
waitOperator = "while";
//firstScope = null; Don't do this for 'while' - it shouldn't expect the '{' char after that.
break;
default:
waitOperator = null;
}
operatorName = null;
}
if (!wait) {
waitAcc = '';
stop = !(waitOperator || operatorName);
skipCh = (ch === '}') && !hasOperator;// skip the outer {} of the code-block
}
}
}
else {
throw this.er.invalidExpressionChar(ch, this.lineNum, this.linePos(), this.line); // Tests: "Code 43".
}
}
else if (textQuotes.indexOf(ch) !== -1) { // it's some sort of text qoutes
wait && waits.push(wait);
wait = ch;
inText = true; // put on waits-stack
}
else if (ch === '@'/* && (!lastLiteral || Char.isWhiteSpace(lastLiteral))*/) {
throw this.er.unexpectedAtCharacter(this.lineNum, this.linePos(), this.line); // [Invalid-HTML 9], [Section 1]
}
else if (ch === '<') {
// ':' for `switch/case:`
if (['', '{', '}', ';', ':'].some((c) => c === lastLiteral)) {
this.stepBack(blocks, 0);
this.parseHtmlInsideCode(blocks);
block = this.newBlock(blockType.code, blocks);
waitOperator = null;
continue;
}
}
}
else if (!Char.isWhiteSpace(ch)) {
break;
}
}
if (skipCh) {
this.padding = '';
}
else {
let isSpace = Char.isWhiteSpace(ch);
if (isSpace) {
if (ch === '\n') {
lineLastLiteral = '';
this.flushPadding(blocks); // flash padding buffer in case this whole line contains only whitespaces ..
block.append(ch);
}
else { // it's a true- space or tab
if (lineLastLiteral) // it's not the b