I was bored over the last couple of days and wrote up a string interpolation library for JavaScript.
I'm very pleased with its functionality, it passes it 79 tests cross browser and the comments and README seem fine too.
My main concern here are the regular expressions. I'm not a real pro in that area so I suspect there could be some enhancement to them.
Another thing is readability of the code as well as how good the comments are. I'd like to have "unclear" sections of the code pointed out so I can improve the naming / comments.
The library brings with it the Formatter
factory and the FormatError
.
Basic usage
> format = Formatter()
> format('Good {} Sir {}.', 'morning', 'Lancelot')
'Good morning Sir Lancelot.'
> format('Good {time} Sir {name}.', 'morning', 'Lancelot')
'Good morning Sir Lancelot.'
> format('Good {0} Sir {1}.', ['morning', 'Lancelot'])
'Good morning Sir Lancelot.'
Source
(function(undefined) {
'use strict';
// Formatter factory
function Formatter(formats) {
function f() {
return format(f.formats, arguments);
}
f.formats = formats || {};
return f;
}
// Default formatters
Formatter.formats = {
repeat: repeat,
join: function(value, str) {
return value.join(str || ', ');
},
upper: function(value) {
return value.toUpperCase();
},
lower: function(value) {
return value.toLowerCase();
},
lpad: function(value, length, str) {
return pad(value, length, str, 'l');
},
rpad: function(value, length, str) {
return pad(value, length, str, 'r');
},
pad: function(value, length, str) {
return pad(value, length, str);
},
surround: function(value, left, right) {
return left + value + (right || left);
},
hex: function(value, lead) {
return (lead ? '0x' : '') + value.toString(16);
},
bin: function(value, lead) {
return (lead ? '0b' : '') + value.toString(2);
}
};
function repeat(value, count) {
return new Array((count || 0) + 1).join(value || ' ');
}
function pad(value, length, str, mode) {
value = '' + value;
str = str || ' ';
var len = length - value.length;
if (len < 0) {
return value;
} else if (mode === 'l') {
return repeat(str, len) + value;
} else if (mode === 'r') {
return value + repeat(str, len);
} else {
return repeat(str, len - ~~(len / 2))
+ value
+ repeat(str, ~~(len / 2));
}
}
// match {} placholders like {0}, {name}, {} and the inner "{{foo}}"
// {} can be escaped with \
var replaceExp = /([^\\]|^)\{([^\{\}]*[^\\^\}]|)\}/g,
// match things like: foo[0].test["test"]['test]
accessExp = /^\.?([^\.\[]+)|\[((-?\d+)|('|")(.*?[^\\])\4)\]/,
// match :foo and :foo(.*?)
formatExp = /\:([a-zA-Z]+)(\((.*?)\))?(\:|$)/,
// match arguments: "test", 12, -12, 'test', true, false
// strings can contain escaped characters like \"
argumentsExp = /^(,|^)\s*?((true|false|(-?\d+))|('|")(.*?([^\\]|\5))\5)/;
// Main formatting function
function format(formatters, args) {
// Setup magic!
var string = args[0],
first = args[1],
argsLength = args.length - 2,
type = first != null ? {}.toString.call(first).slice(8, -1) : '',
arrayLength = first ? first.length - 1 : 0,
autoIndex = 0;
function replace(value, pre, form) {
// Extract formatters
var formats = [], format = null, id = form;
while (format = form.match(formatExp)) {
if (formats.length === 0) {
id = form.substring(0, format.index);
}
form = form.substring(format[0].length - 1);
formats.push(format);
}
// In case of a valid number use it for indexing
var num = (isNaN(+id) || id === '') ? null : +id;
// Handle objects
if (type === 'Object' && id !== '') {
// Handle plain keys
if (id.indexOf('.') === -1 && id.indexOf('[') === -1) {
if (first[id] !== undefined) {
value = first[id];
// fall back to obj.toString()
} else {
value = args[1 + autoIndex];
}
// Access properties
} else {
value = getProperty(first, id);
}
// Handle given array indexes
} else if (type === 'Array' && num !== null) {
value = first[num >= 0 ? num : arrayLength + num];
// Handle given arguments indexes
} else if (num !== null) {
value = args[1 + (num >= 0 ? num : argsLength + num)];
// Handle automatic arguments indexes
} else {
value = args[1 + autoIndex];
}
autoIndex++;
// Apply formats
while (format = formats.shift()) {
var method = (formatters[format[1]] ? formatters : Formatter.formats)[format[1]];
if (method) {
value = method.apply(undefined,
getArguments(value, format[3] || ''));
} else {
throw new FormatError(
replace, 'Undefined formatter "{}".', format[1]
);
}
}
return pre + value;
}
return string.replace(replaceExp, replace);
}
// Get a specific peoperty of an object based on a accessor string
function getProperty(obj, id) {
var m, pos = 0;
while (m = id.substring(pos).match(accessExp)) {
// .name / [0] / ["test"]
var prop = m[1] || (m[3] ? +m[3] : m[5].replace('\\' + m[4], m[4]));
if (obj === undefined) {
throw new FormatError(
getProperty,
'Cannot access property "{}" of undefined.', prop
);
} else {
obj = obj[prop];
}
pos += m[0].length;
}
return obj;
}
// Convert a string like:
// true, false, -1, 34, 'foo', "bla\" foo"
//
// Into a list of arguments:
// [true, false, -1, 34, 'foo', 'bla" foo']
function getArguments(value, string) {
var m, pos = 0, args = [value];
while (m = string.substring(pos).match(argumentsExp)) {
// number
args.push(m[4] ? +m[4]
// boolean
: (m[3] ? m[3] === 'true'
// string
: m[6].replace('\\' + m[5], m[5])));
pos += m[0].length;
}
return args;
}
// Formatting error type
function FormatError(func, msg, value) {
this.name = 'FormatError';
this.message = format(Formatter.formats, [msg, value]);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, func);
}
}
FormatError.prototype = new Error();
// Exports
var exp = typeof window === 'undefined' ? exports : window;
exp.Formatter = Formatter;
exp.FormatError = FormatError;
})();