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]; }