mirror of
https://github.com/mgerb/mywebsite
synced 2026-01-12 10:52:47 +00:00
379 lines
10 KiB
JavaScript
379 lines
10 KiB
JavaScript
var lookup = require('racer/lib/path').lookup
|
|
, trimLeading = require('html-util').trimLeading;
|
|
|
|
exports.wrapRemainder = wrapRemainder;
|
|
exports.extractPlaceholder = extractPlaceholder;
|
|
exports.pathFnArgs = pathFnArgs;
|
|
exports.ctxPath = ctxPath;
|
|
exports.getValue = getValue;
|
|
exports.dataValue = dataValue;
|
|
exports.setBoundFn = setBoundFn;
|
|
exports.patchCtx = patchCtx;
|
|
|
|
function wrapRemainder(tagName, remainder) {
|
|
if (!remainder) return false;
|
|
return !(new RegExp('^<\/' + tagName, 'i')).test(remainder);
|
|
}
|
|
|
|
var openPlaceholder = /^([\s\S]*?)(\{{1,3})\s*([\s\S]*)/
|
|
, aliasContent = /^([\s\S]*)\s+as\s+:(\S+)\s*$/
|
|
, blockContent = /^([\#\/]?)(else\sif|if|else|unless|each|with|unescaped)?\s*([\s\S]*?)\s*$/
|
|
, closeMap = { 1: '}', 2: '}}' }
|
|
function extractPlaceholder(text) {
|
|
var match = openPlaceholder.exec(text);
|
|
if (!match) return;
|
|
var pre = match[1]
|
|
, open = match[2]
|
|
, remainder = match[3]
|
|
, openLen = open.length
|
|
, bound = openLen === 1
|
|
, end = matchBraces(remainder, openLen, 0, '{', '}')
|
|
, endInner = end - openLen
|
|
, inner = remainder.slice(0, endInner)
|
|
, post = remainder.slice(end)
|
|
, alias, hash, type, name, escaped;
|
|
|
|
if (/["{[]/.test(inner)) {
|
|
// Make sure that we didn't accidentally match a JSON literal
|
|
try {
|
|
JSON.parse(open + inner + closeMap[openLen]);
|
|
return;
|
|
} catch (e) {}
|
|
}
|
|
|
|
match = aliasContent.exec(inner);
|
|
if (match) {
|
|
inner = match[1];
|
|
alias = match[2];
|
|
}
|
|
|
|
match = blockContent.exec(inner)
|
|
if (!match) return;
|
|
hash = match[1];
|
|
type = match[2];
|
|
name = match[3];
|
|
|
|
escaped = true;
|
|
if (type === 'unescaped') {
|
|
escaped = false;
|
|
type = '';
|
|
}
|
|
if (bound) name = name.replace(/\bthis\b/, '.');
|
|
return {
|
|
pre: pre
|
|
, post: post
|
|
, bound: bound
|
|
, alias: alias
|
|
, hash: hash
|
|
, type: type
|
|
, name: name
|
|
, escaped: escaped
|
|
, source: text
|
|
};
|
|
}
|
|
|
|
function matchBraces(text, num, i, openChar, closeChar) {
|
|
var close, hasClose, hasOpen, open;
|
|
i++;
|
|
while (num) {
|
|
close = text.indexOf(closeChar, i);
|
|
open = text.indexOf(openChar, i);
|
|
hasClose = ~close;
|
|
hasOpen = ~open;
|
|
if (hasClose && (!hasOpen || (close < open))) {
|
|
i = close + 1;
|
|
num--;
|
|
continue;
|
|
} else if (hasOpen) {
|
|
i = open + 1;
|
|
num++;
|
|
continue;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
return i;
|
|
}
|
|
|
|
var fnCall = /^([^(]+)\s*\(\s*([\s\S]*?)\s*\)\s*$/
|
|
, argSeparator = /\s*([,(])\s*/g
|
|
, notSeparator = /[^,\s]/g
|
|
, notPathArg = /(?:^['"\d\-[{])|(?:^null$)|(?:^true$)|(?:^false$)/;
|
|
|
|
function fnArgs(inner) {
|
|
var args = []
|
|
, lastIndex = 0
|
|
, match, end, last;
|
|
while (match = argSeparator.exec(inner)) {
|
|
if (match[1] === '(') {
|
|
end = matchBraces(inner, 1, argSeparator.lastIndex, '(', ')');
|
|
args.push(inner.slice(lastIndex, end));
|
|
notSeparator.lastIndex = end;
|
|
lastIndex = argSeparator.lastIndex =
|
|
notSeparator.test(inner) ? notSeparator.lastIndex - 1 : end;
|
|
continue;
|
|
}
|
|
args.push(inner.slice(lastIndex, match.index));
|
|
lastIndex = argSeparator.lastIndex;
|
|
}
|
|
last = inner.slice(lastIndex);
|
|
if (last) args.push(last);
|
|
return args;
|
|
}
|
|
|
|
function fnCallError(name) {
|
|
throw new Error('malformed view function call: ' + name);
|
|
}
|
|
|
|
function fnArgValue(view, ctx, model, name, arg) {
|
|
var literal = literalValue(arg)
|
|
, argIds, path, pathId;
|
|
if (literal === undefined) {
|
|
argIds = ctx.hasOwnProperty('$fnArgIds') ?
|
|
ctx.$fnArgIds : (ctx.$fnArgIds = {});
|
|
if (pathId = argIds[arg]) {
|
|
path = model.__pathMap.paths[pathId];
|
|
} else {
|
|
path = ctxPath(view, ctx, arg);
|
|
argIds[arg] = model.__pathMap.id(path);
|
|
}
|
|
return dataValue(view, ctx, model, path);
|
|
}
|
|
return literal;
|
|
}
|
|
|
|
function fnValue(view, ctx, model, name) {
|
|
var match = fnCall.exec(name) || fnCallError(name)
|
|
, fnName = match[1]
|
|
, args = fnArgs(match[2])
|
|
, fn, fnName, i;
|
|
for (i = args.length; i--;) {
|
|
args[i] = fnArgValue(view, ctx, model, name, args[i]);
|
|
}
|
|
if (!(fn = view.getFns[fnName])) {
|
|
throw new Error('view function "' + fnName + '" not found for call: ' + name);
|
|
}
|
|
return fn.apply({view: view, ctx: ctx, model: model}, args);
|
|
}
|
|
|
|
function pathFnArgs(name, paths) {
|
|
var match = fnCall.exec(name) || fnCallError(name)
|
|
, args = fnArgs(match[2])
|
|
, i, arg;
|
|
if (paths == null) paths = [];
|
|
for (i = args.length; i--;) {
|
|
arg = args[i];
|
|
if (notPathArg.test(arg)) continue;
|
|
if (~arg.indexOf('(')) {
|
|
pathFnArgs(arg, paths);
|
|
continue;
|
|
}
|
|
paths.push(arg);
|
|
}
|
|
return paths;
|
|
}
|
|
|
|
var indexPlaceholder = /\$#/g;
|
|
|
|
function relativePath(ctx, i, remainder, noReplace) {
|
|
var meta = ctx.$paths[i - 1] || []
|
|
, base = meta[0]
|
|
, name = base + remainder
|
|
, offset, indices, index, placeholders
|
|
|
|
// Replace `$#` segments in a path with the proper indicies
|
|
if (!noReplace && (placeholders = name.match(indexPlaceholder))) {
|
|
indices = ctx.$indices;
|
|
index = placeholders.length + indices.length - meta[1] - 1;
|
|
name = name.replace(indexPlaceholder, function() {
|
|
return indices[--index];
|
|
});
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
function macroName(view, ctx, name) {
|
|
if (name.charAt(0) !== '@') return;
|
|
|
|
var macroCtx = ctx.$macroCtx
|
|
, segments = name.slice(1).split('.')
|
|
, base = segments.shift().toLowerCase()
|
|
, remainder = segments.join('.')
|
|
, value = lookup(base, macroCtx)
|
|
, matchName = value && value.$matchName
|
|
if (matchName) {
|
|
if (!remainder) return value;
|
|
return {$matchName: matchName + '.' + remainder};
|
|
}
|
|
return remainder ? base + '.' + remainder : base;
|
|
}
|
|
|
|
function ctxPath(view, ctx, name, noReplace) {
|
|
var macroPath = macroName(view, ctx, name);
|
|
if (macroPath && macroPath.$matchName) name = macroPath.$matchName;
|
|
|
|
var firstChar = name.charAt(0)
|
|
, i, aliasName, remainder
|
|
|
|
// Resolve path aliases
|
|
if (firstChar === ':') {
|
|
if (~(i = name.search(/[.[]/))) {
|
|
aliasName = name.slice(1, i);
|
|
remainder = name.slice(i);
|
|
} else {
|
|
aliasName = name.slice(1);
|
|
remainder = '';
|
|
}
|
|
i = ctx.$paths.length - ctx.$aliases[aliasName];
|
|
if (i !== i) throw new Error('Cannot find alias for ' + aliasName);
|
|
|
|
name = relativePath(ctx, i, remainder, noReplace);
|
|
|
|
// Resolve relative paths
|
|
} else if (firstChar === '.') {
|
|
i = 0;
|
|
while (name.charAt(i) === '.') {
|
|
i++;
|
|
}
|
|
remainder = i === name.length ? '' : name.slice(i - 1);
|
|
|
|
name = relativePath(ctx, i, remainder, noReplace);
|
|
}
|
|
|
|
// Perform path interpolation
|
|
// TODO: This should nest properly and currently is only one level deep
|
|
// TODO: This should also set up bindings
|
|
return name.replace(/\[([^\]]+)\]/g, function(match, property, offset) {
|
|
var segment = getValue(view, ctx, view.model, property);
|
|
if (offset === 0 || name.charAt(offset - 1) === '.') return segment;
|
|
return '.' + segment;
|
|
});
|
|
}
|
|
|
|
function escapeValue(value, escape) {
|
|
return escape ? escape(value) : value;
|
|
}
|
|
|
|
function literalValue(value) {
|
|
if (value === 'null') return null;
|
|
if (value === 'true') return true;
|
|
if (value === 'false') return false;
|
|
var firstChar = value.charAt(0)
|
|
, match;
|
|
if (firstChar === "'") {
|
|
match = /^'(.*)'$/.exec(value) || fnCallError(value);
|
|
return match[1];
|
|
}
|
|
if (firstChar === '"') {
|
|
match = /^"(.*)"$/.exec(value) || fnCallError(value);
|
|
return match[1];
|
|
}
|
|
if (/^[\d\-]/.test(firstChar) && !isNaN(value)) {
|
|
return +value;
|
|
}
|
|
if (firstChar === '[' || firstChar === '{') {
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch (e) {}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function getValue(view, ctx, model, name, escape, forceEscape) {
|
|
var literal = literalValue(name)
|
|
if (literal === undefined) {
|
|
return dataValue(view, ctx, model, name, escape, forceEscape);
|
|
}
|
|
return literal;
|
|
}
|
|
|
|
function dataValue(view, ctx, model, name, escape, forceEscape) {
|
|
var macroPath, path, value;
|
|
if (~name.indexOf('(')) {
|
|
value = fnValue(view, ctx, model, name);
|
|
return escapeValue(value, escape);
|
|
}
|
|
path = ctxPath(view, ctx, name);
|
|
macroPath = macroName(view, ctx, path);
|
|
if (macroPath) {
|
|
if (macroPath.$matchName) {
|
|
path = macroPath.$matchName;
|
|
} else {
|
|
value = lookup(macroPath, ctx.$macroCtx);
|
|
if (typeof value === 'function') {
|
|
if (value.unescaped && !forceEscape) return value(ctx, model);
|
|
value = value(ctx, model);
|
|
}
|
|
return escapeValue(value, escape);
|
|
}
|
|
}
|
|
value = lookup(path, ctx);
|
|
if (value !== void 0) return escapeValue(value, escape);
|
|
value = model.get(path);
|
|
value = value !== void 0 ? value : model[path];
|
|
return escapeValue(value, escape);
|
|
}
|
|
|
|
function setBoundFn(view, ctx, model, name, value) {
|
|
var match = fnCall.exec(name) || fnCallError(name)
|
|
, fnName = match[1]
|
|
, args = fnArgs(match[2])
|
|
, get = view.getFns[fnName]
|
|
, set = view.setFns[fnName]
|
|
, numInputs = set && set.length - 1
|
|
, arg, i, inputs, out, key, path, len;
|
|
|
|
if (!(get && set)) {
|
|
throw new Error('view function "' + fnName + '" setter not found for binding to: ' + name);
|
|
}
|
|
|
|
if (numInputs) {
|
|
inputs = [value];
|
|
i = 0;
|
|
while (i < numInputs) {
|
|
inputs.push(fnArgValue(view, ctx, model, name, args[i++]));
|
|
}
|
|
out = set.apply(null, inputs);
|
|
} else {
|
|
out = set(value);
|
|
}
|
|
if (!out) return;
|
|
|
|
for (key in out) {
|
|
value = out[key];
|
|
arg = args[key];
|
|
if (~arg.indexOf('(')) {
|
|
setBoundFn(view, ctx, model, arg, value);
|
|
continue;
|
|
}
|
|
if (value === void 0 || notPathArg.test(arg)) continue;
|
|
path = ctxPath(view, ctx, arg);
|
|
if (model.get(path) === value) continue;
|
|
model.set(path, value);
|
|
}
|
|
}
|
|
|
|
function patchCtx(ctx, triggerPath) {
|
|
var meta, path;
|
|
if (!(triggerPath && (meta = ctx.$paths[0]) && (path = meta[0]))) return;
|
|
|
|
var segments = path.split('.')
|
|
, triggerSegments = triggerPath.replace(/\*$/, '').split('.')
|
|
, indices = ctx.$indices.slice()
|
|
, index = indices.length
|
|
, i, len, segment, triggerSegment, n;
|
|
for (i = 0, len = segments.length; i < len; i++) {
|
|
segment = segments[i];
|
|
triggerSegment = triggerSegments[i];
|
|
// `(n = +triggerSegment) === n` will be false only if segment is NaN
|
|
if (segment === '$#' && (n = +triggerSegment) === n) {
|
|
indices[--index] = n;
|
|
} else if (segment !== triggerSegment) {
|
|
break;
|
|
}
|
|
}
|
|
ctx.$indices = indices;
|
|
ctx.$index = indices[0];
|
|
}
|