Skip to content

Instantly share code, notes, and snippets.

@spiralx
Last active August 11, 2016 12:04
Show Gist options
  • Save spiralx/6404947 to your computer and use it in GitHub Desktop.
Save spiralx/6404947 to your computer and use it in GitHub Desktop.
A small-ish module for doing string interpolation, variable substitution and simple transforms for when you don't want a full-blown templating system
<!DOCTYPE html>
<html>
<head>
<title>String format() function demo</title>
</head>
<body>
<h1>String format() function demo</h1>
<div>
<p>Module format not loaded</p>
</div>
<script src="format.js"></script>
<script>
window.onload = function() {
var url = 'https://gist.github.com/spiralx/6404947';
document.querySelector('div').innerHTML =
format('<p>Loaded <b>format</b> module (<a href="{0}">See on GitHub</a>)</p>', url) +
format('<p>location: host="{host}", path="{pathname}"</p>', location);
}
</script>
</body>
</html>
var format = require('./format');
console.info(
format('Running Node.js v{versions.node} (v8: v{versions.v8}) on {platform}/{arch}, argv: "{argv[0]}"', process)
);
;(function(name, definition) {
var moduleObj = definition();
// AMD Module
if (typeof define === 'function') {
define(moduleObj);
}
// CommonJS Module
else if (typeof module !== 'undefined' && module.exports) {
module.exports = moduleObj;
}
// Assign to the global object (window)
else {
this[name] = moduleObj;
}
})('format', function() {
'use strict';
/**
* Formatting helper functions accepted by `format()`
*/
const format_funcs = {
t: s => String(s).trim(),
l: s => String(s).toLowerCase(),
u: s => String(s).toUpperCase(),
j: (o, long) => typeof o === 'object' ? JSON.stringify(o, null, long ? 2: 0) : o
};
/** Apply an array of functions one after the other */
const apply_all = (value, funcs) => funcs.reduce((cur, fn) => fn(cur) || '', value);
/** Get all regex matches as an array of match results */
const match_all = (re, str) => { let o = [], m; while (m = re.exec(str)) { o.push(m); } return o; };
/** Get the internal [[Class]] for an object */
const classof = v => Object.prototype.toString.call(v).replace(/^\[object\s(.*)\]$/, '$1').toLowerCase();
/** Dump an array of funcs to the console */
const dump_funcs = function(funcs) {
if (!Array.isArray(funcs)) {
console.warn(funcs);
}
else {
console.table(funcs.map(fn => fn.toString()));
}
return funcs;
};
const isobj = v => classof(v) === 'object';
/** Parses strings like 'o.x[5]' into an array of functions */
const parse_paths = function(ps) {
return match_all(/(?:^|\.)([_$\w]+)|\[(-?\d+)\]/gi, ps).map(function(m) {
let [, k, i] = m;
if (k) {
return o => isobj(o) && o.hasOwnProperty(k) ? o[k] : '';
}
else {
i = parseInt(i);
return o => Array.isArray(o) ? o.slice(i)[0] : '';
}
});
};
/** Parses strings like 'j:true,u' into an array of functions */
const parse_helpers = function(hs) {
if (!hs) {
return [];
}
return hs.split(';').map(function(s) {
let [name, argstr] = s.split(':');
if (!format_funcs[name]) {
return null;
}
if (!argstr) {
return format_funcs[name];
}
let args = argstr.split(',').map(vs => {
switch (vs) {
case 'true': return true;
case 'false': return false;
}
return isNaN(vs) ? vs : parseInt(vs);
});
return s => format_funcs[name](s, ...args);
});
};
/**
* Simple string substution function.
*
* fmt('x={0}, y={1}', 12, 4) -> 'x=12, y=4'
* fmt('x={x}, y={y}', { x: 12, y: 4 }) -> 'x=12, y=4'
* fmt('x={x}, y={{moo}}', { x: 12, y: 4 }) -> 'x=12, y={moo}'
* fmt('{x}: {y.thing}', { x: 'foo', y: { thing: 'bar' }}) -> 'foo: bar'
* fmt('{x}: {y.a[1]}', { x: 'foo', y: { thing: 'bar', a: [6, 7] }}) -> 'foo: 7'
* fmt('{0[2]}, {0[-2]}', [{ x: 12, y: 4 }, 7, 120, 777, 999]) -> '120, 777'
* fmt('{0[-5].y}', [{ x: 12, y: 4 }, 7, 120, 777, 999]) -> '4'
* fmt('{a[-5].x}', {a: [{ x: 12, y: 4 }, 7, 120, 777, 999]}) -> '12'
*
* @param {String} format
* @param {Object|Object+} data
* @return {String}
*/
function format(formatString, data) {
data = arguments.length == 2 && typeof data === "object" && !Array.isArray(data)
? data
: [].slice.call(arguments, 1);
// console.log('data = %s', JSON.stringify(data));
return formatString
.replace(/\{\{/g, String.fromCharCode(0))
.replace(/\}\}/g, String.fromCharCode(1))
.replace(/\{([_$a-z][_$\w]*)(?:!([^}]+))?\}/g, function(m, p, h) {
try {
let path = parse_paths(p), helpers = parse_helpers(h);
// console.log('m = "%s"\npath = %s\nhelpers = %s', m, dump_funcs(path), dump_funcs(helpers));
return String(apply_all(apply_all(data, path), helpers));
}
catch (ex) {
return m;
}
})
.replace(/\x00/g, "{")
.replace(/\x01/g, "}");
}
// e.g. format('x = {x}, y = "{y}", o: {o!j:true}', { x: 12, y: ' mooo! ', o: { x: 12, y: ' mooo! ' } });
return format;
});
;(function(name, definition) {
var moduleObj = definition();
// AMD Module
if (typeof define === 'function') {
define(moduleObj);
}
// CommonJS Module
else if (typeof module !== 'undefined' && module.exports) {
module.exports = moduleObj;
}
// Assign to the global object (window)
else {
this[name] = moduleObj;
}
})('format', function() {
'use strict';
/**
* Simple string substution function.
*
* fmt('x={0}, y={1}', 12, 4) -> 'x=12, y=4'
* fmt('x={x}, y={y}', { x: 12, y: 4 }) -> 'x=12, y=4'
* fmt('x={x}, y={{moo}}', { x: 12, y: 4 }) -> 'x=12, y={moo}'
* fmt('{x}: {y.thing}', { x: 'foo', y: { thing: 'bar' }}) -> 'foo: bar'
* fmt('{x}: {y.a[1]}', { x: 'foo', y: { thing: 'bar', a: [6, 7] }}) -> 'foo: 7'
* fmt('{0[2]}, {0[-2]}', [{ x: 12, y: 4 }, 7, 120, 777, 999]) -> '120, 777'
* fmt('{0[-5].y}', [{ x: 12, y: 4 }, 7, 120, 777, 999]) -> '4'
* fmt('{a[-5].x}', {a: [{ x: 12, y: 4 }, 7, 120, 777, 999]}) -> '12'
*
* @param {String} format
* @param {Object|Object+} data
* @return {String}
*/
function format(formatString, data) {
data = arguments.length == 2 && typeof data === "object" && !Array.isArray(data)
? data
: [].slice.call(arguments, 1);
return formatString
.replace(/\{\{/g, String.fromCharCode(0))
.replace(/\}\}/g, String.fromCharCode(1))
.replace(/\{([^}]+)\}/g, function(match, path) {
try {
var p = path.replace(/\[(-?\w+)\]/g, '.$1').split('.');
//console.log('path="%s" (%s), data=%s', path, p.toSource(), data.toSource());
return String(p.reduce(function(o, n) {
return o.slice && !isNaN(n) ? o.slice(n).shift() : o[n];
}, data));
}
catch (ex) {
return match;
}
})
.replace(/\x00/g, "{")
.replace(/\x01/g, "}");
}
return format;
});
@spiralx
Copy link
Author

spiralx commented Mar 21, 2014

Updated fmt() to handle object "paths" rather than just simple index/property names.

Assume the abstract operation traverse(data, path_item):

  • if data.slice exists and isNan(path_item) is false, return data.slice(path_item).shift()
  • otherwise return data[path_item]

Given a path p, we generate a list of path items by replacing each [number] with .number and then splitting on .. Array.reduce is then used against this list and the data object to traverse the path and retrieve the result. Any exceptions are caught and no replacement is done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment