Last active
August 11, 2016 12:04
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var format = require('./format'); | |
console.info( | |
format('Running Node.js v{versions.node} (v8: v{versions.v8}) on {platform}/{arch}, argv: "{argv[0]}"', process) | |
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;(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; | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;(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; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Updated
fmt()
to handle object "paths" rather than just simple index/property names.Assume the abstract operation
traverse(data, path_item)
:data.slice
exists andisNan(path_item)
isfalse
, returndata.slice(path_item).shift()
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.