CODE TEST

The Tiny Function Library

Introduction

This small library is meant to provide some handy functions which are missing in DOM manipulating frameworks like jQuery or others.

And it doesn't want to be another armed-to-teeth full featured framework (might also be hard-to-learn).

Its goal is providing a minimalist building foundation for modern web pages.

import()

By default, Tiny will not inject global functions and object methods used in below examples into global namespace (the window object).

To use those short function names, pleas call tiny.import() ahead of all calls.


           // register global function names for tiny methods
           tiny.import();
        

A typical initialization code with Tiny sequence looks like this:


           tiny.import();                        // import functions & methods
           tiny.output('error');                // output errors only in production environment
           _storage.keyPrefix = 'my_app';        // set storage key prefix

           var lang = _storage('lang');          // get user language setting
           _lang.strings = LANG_STRINGS;         // bind translation data
           _lang.set(lang);                      // load correspond language strings

           app.start();                          // start your app script
        

help()

Show a structural overview of tiny namespace. This function will also show a list of injected global functions and prototype methods.


           // show tiny definitions in console
           tiny.help();
        

Core Functions

_type()

Get detailed object type. This function is slow on Objects, don't use it in performance critical operations.

EXAMPLES


            ASSERT('undefined',      _type() == 'undefined');
            ASSERT('null',           _type(null) == 'null');
            ASSERT('number',         _type(123) == 'number');
            ASSERT('string',         _type('test') == 'string');
            ASSERT('function',       _type(function(){}) == 'function');
            ASSERT('object',         _type({test: true}) == 'object');
            ASSERT('RegExp',         _type(/\s\t\n/) == 'RegExp');
            ASSERT('Date',           _type(new Date()) == 'Date');
            ASSERT('Array',          _type([1,2,3]) == 'Array');
            ASSERT('Arguments',      _type(arguments) == 'Arguments');
            ASSERT('Window',         _type(window) == 'Window');
            ASSERT('HTMLCollection', _type(document.forms) == 'HTMLCollection');
            ASSERT('NodeList',       _type(document.querySelectorAll('.run-code')) == 'NodeList');
        

_each()

A handy replacement to the for() and for...in loop.

The syntax looks like Array.forEach() method, but you can also use it on Array, Object, Function, String, Number and some Array-like objects:

And you can continue or break with a return value.

The number of steps was determined by the array length before the loop starts. It won't change no matter you add items to or remove items from the array.


            _each(object, [start,] callback [,this_arg]) : any
            // object    : Array, Object, String or Number to loop
            // start     : you can set the start index of the loop (Array-like only)
            // callback  : callback function for the loop
            // this_arg  : set the value of 'this' inside the scope

            // extended method for Array and String
            Array._each([start,] callback [,this_arg]) : any
            String._each([start,] callback [,this_arg]) : any

            function callback(value, index, array) {
                return;              // continue to next item in array_or_object
                return any_value;    // break out, and _each() will return any_value
            }
            

NOTES

Always use for() on Arrays in performance critical operations.

Below is a test result on an Array:


        for()               109.114ms      // best performance
        _each()             792.093ms      // implement with for() loop 
        Array.forEach()     2052.318ms
        for...of            1501.836ms
        for...in            9032.005ms
        

For looping through Object, Map or Set, if you can use for..of feature of ECMAScript 6 in your project, the performance can be 50 times faster. And this feature allows you using continue and break to control code flow.

Below is a test result on an Object:


        Object.keys() + for()       1064.989ms    // not sure why this is slow
        Object.keys().forEach()     145.356ms     // fast but no flow control
        _each()                     822.088ms     // implement with for...in loop
        for...in                    802.236ms
        for...of                    213.247ms     // best choice - just a little slower than forEach()
        

EXAMPLES


            var arr = [];

            // ==> Number
            // value: 1 -> 6, index: 0 -> 5
            _each(6, function (value, index) {
                arr[index] = value;
            });
            ASSERT('_each() on number', arr.join() === '1,2,3,4,5,6');

            // ==> String
            // value: a -> c, index: 1 -> 2
            'abc'._each(1, function (value) {
                arr.push(value);
            });
            ASSERT('_each() on string', arr.join() === '1,2,3,4,5,6,b,c');

            // ==> Array
            _each(arr, function (value, index, array) {
                if (value < 4) return;       // continue
                if (value > 5) return true;  // break
                array[index] = value * 10;
            });
            ASSERT('_each() on array', arr.join() === '1,2,3,40,50,6,b,c' );

            // return a value
            var chr = arr._each(function (value, index, array) {
                if (typeof value !== 'number')
                    return value;    // return first non-number value
            });
            ASSERT('Array._each() & return value', chr === 'b' );

            // ==> Object
            var list = { action: 'test', animal: 'duck' };
            var my_string = '';
            _each(list, 'action', function (value, label) {
                my_string += 'This is a ' + value + '. ';
            });
            ASSERT('_each() on object', my_string === 'This is a test. This is a duck. ');

            // ==> Elements
            var elems = document.querySelectorAll('h3');
            var count = 0;
            _each(elems, function (elem, index) {
                if(elem.tagName != 'H3') count += index;
            });
            ASSERT('_each() on NodeList', count === 0 );

            // ==> Arguments
            // this code block is called as func(elem, 'this', 'is', 'a', 'test');
            count = 0;
            _each(arguments, function (arg, index) {
                if(arg == 'test') count = index;
            });
            ASSERT('_each() on arguments', count === 4 );
        

ERROR TESTS


            // A TypeError will be thrown on invalid parameter types
            // Detailed information can be found in console
            try{
                _each([1, 2, 3], 'error test 2');
                FAIL('_each() paramter 2 error');
            }catch(err){
                ASSERT('_each() paramter 2 error', err instanceof TypeError );
                _info(err.message)
            }
            

_extend()

Extend an object with new properties. This function does not do deep extension.

And Javscript preserved keywords can not be used or overwritten.


            _extend(target, extensions[, overwrite]) : object
            // target     : the target Object or Function you want to extend
            // extensions : an Object contains extensions that will be applied to target
            // overwrite  : whether overwrite existing properties, default is true
        

EXAMPLES


            var point = { x: 1, y: 2 };
            var point_3d = _extend(point, {z: 3});
            ASSERT('_extend()', FLAT(point_3d) === '{"x":1,"y":2,"z":3}' );

            // x will be overwrite
            var point_4d = _extend(point_3d, {x: 10, t: 4});
            ASSERT('_extend() overwrite', FLAT(point_4d) === '{"x":10,"y":2,"z":3,"t":4}' );

            // x & y will not be overwrite
            point_4d = _extend(point_3d, {x: 100, y: 100}, false);
            ASSERT('_extend() no overwrite', FLAT(point_4d) === '{"x":10,"y":2,"z":3,"t":4}' );
        

ERROR TESTS


            // A TypeError will be thrown on invalid parameter types
            // Detailed information can be found in console
            try{
                _extend('error test 1');
                FAIL('_extend() parameter 1 error');
            }catch(err){
                ASSERT('_extend() parameter 1 error', err instanceof TypeError );
            }
            try{
                _extend({}, 'error test 2');
                FAIL('_extend() parameter 2 error');
            }catch(err){
                ASSERT('_extend() parameter 2 error', err instanceof TypeError );
                _info(err.message)
            }
            

hash()

A fast hash function for generating 32-bit hash number from a string.

Currently it's using the Murmur2 algorithm. This algorithm is designed for uniqueness not security. Don't use it for password hashing.

EXAMPLES


            
            ASSERT('1', tiny.hash('This is a test') === 466082811);

            // you can apply an integer seed
            ASSERT('1', tiny.hash('This is a test', 5330) === 3689166302);

            // kown Murmur2 collisions
            ASSERT('collision 1', tiny.hash('costarring') !== tiny.hash('liquid'));

        

Console Functions

_log() ... _error()

Tiny provides several shorthands to console methods: _log(), _group(), _timer(), _info(), _warn() and _error().

You might use these functions just like the original methods of console. These are not wrapper functions, and will not show the wrapper function name & location in the console output.

By default, output of _log() & _dir() is turned off. Don't bother end users with those logs.

You can easily turn the console log on or off by calling tiny.output('all') in develop or production environments.

EXAMPLES


            // enable _log() output when you need them
            tiny.output('all');

            // use them as you would like the console.xxx equivalents
            _log('Test for _log()', '----------------------------');
            _info('Test for _info()', null );
            _warn('Test for _warn()', 'Nothing will happen');
            _error('Test for _error()', '----------------------------');
        

_inspect()

The _inspect function is a helper function for debug purpose.

It will return a formatted JSON string version of given object for inspection.

Functions and special objects might lose in this process.


            _inspect(obj [, key_filter] [, log]) : string
        

EXAMPLES


            var obj = {
                id: 123,
                view: { loaded: true,
                        date: new Date(1467190047725) },
                method: function(){}
            };

            var result = _inspect( obj, ['method'], false );
            var expected_result = JSON.stringify({
                "id": 123,
                "view": {
                    "loaded": true,
                    "date": "2016-06-29T08:47:27.725Z"
                }
            }, null, 4);
            // the result should looks like expected_result
            ASSERT('_inspect()', result == expected_result);
        

_group()

Wrapper for console.group()/groupCollapsed()/groupEnd().

EXAMPLES


            // call with a name to start a group
            _group('test group');
            // log something
            _info('test', 123)
            // call again to end a group
            _group();

            // collapsed group - first parameter is false
            _group(false, 'test collapsed group');
            _info('test', 456)
            _group();
        

_timer()

Timing function using window.performance.now(). It returns a value on end

EXAMPLES


            // call with an id to record start time
            _timer('test');
            // call again to output to console - last timer name can be ommitted
            _timer();

            // start another
            _timer('test delayed');
            setTimeout(function(){
                // call with false as second parameter to get the value
                ASSERT('time', _timer('test delayed') > 199);
            }, 200);
        

Message System

This tiny library includes a basic pseudo message system. Messages are more like remote function calls.

In order to distinguish from the DOM Event system, it is named _message.

Though it's not real asynchronize message delivery, it should be enough for most use cases on browser client.

_message.register()

Register messages.

All messages must be registered before using. Duplicate registration will result an error.


            _message.register([namespace,] array_of_message_name_strings) : _message
        

_message.listen()

listen to a message.

There is no unlisten() method, since you can easily drop a message in your message handler by checking internal flag (and you definitely will have an internal flag in such case).


            _message.listen(message_name, handler) : _message

            function handler(msg_name, param_1, param_2 ...){ ... }
        

_message.post()

Post a message to listeners.


            _message.post(msg_name, param_1, param_2 ...) : _message
        

_message.postDelayed()

Post a message to listeners after a specified delay in milliseconds.

If the same message is triggered again, the delay time will be reset. This behavior is suit for implement Type-n-Search like features.


            _message.postDelayed(delay_time, msg_name, param_1, param_2 ...) : _message
        

All these methods will return the _message object. You can chain methods if required.

EXAMPLES


            var list = [];

            // define the message handler
            function put_in_list(){
                var args = Array.prototype.slice.call(arguments);
                list = list.concat(args);
            }

            // register messages
            _message.register('global-msg')
            _message.register('list::add')
            _message.register(
                'list', [
                    'remove',
                    'error' ]
            )

            // listen to the message
            _message
                .listen('list::add', put_in_list)
                .listen('list::error', function(){ FAIL('_message.post()') });

            // post a message
            _message.post('list::add', 'me', 123);
            // check result
            ASSERT( '_message.post()', FLAT(list) == '["me",123]' );

            // post a delayed message
            _message.postDelayed(50, 'list::add', 'you', 456);
            // check result after 100 milliseconds
            setTimeout(function(){
                ASSERT( '_message.postDelayed()', FLAT(list) == '["me",123,"you",456]' );
            }, 100);
        

ERROR TESTS


            // A TypeError will be thrown on invalid parameter types
            // Detailed information can be found in console
            try{
                _message.register(1, []);
                FAIL('_message.register() parameter 1 error');
            }catch(err){
                ASSERT('_message.register() parameter 1 error', err instanceof TypeError );
                _info(err.message)
            }
            try{
                _message.register('ns', 2);
                FAIL('_message.register() parameter 2 error');
            }catch(err){
                ASSERT('_message.register() parameter 2 error', err instanceof TypeError );
                _info(err.message)
            }

            try{
                _message.listen(['error test', 1]);
                FAIL('_message.listen() parameter 1 error');
            }catch(err){
                ASSERT('_message.listen() parameter 1 error', err instanceof TypeError );
                _info(err.message)
            }
            try{
                _message.listen('my_msg', 'error test');
                FAIL('_message.listen() parameter 2 error');
            }catch(err){
                ASSERT('_message.listen() parameter 2 error', err instanceof TypeError );
                _info(err.message)
            }

            try{
                _message.post(['error test'], null );
                FAIL('_message.post() parameter 1 error');
            }catch(err){
                ASSERT('_message.post() parameter 1 error', err instanceof TypeError );
                _info(err.message)
            }

            try{
                _message.postDelayed(['error test 1'], null , null );
                FAIL('_message.postDelayed() parameter 1 error');
            }catch(err){
                ASSERT('_message.postDelayed() parameter 1 error', err instanceof TypeError );
                _info(err.message)
            }
            try{
                _message.postDelayed(100, ['error test'], null );
                FAIL('_message.postDelayed() parameter 2 error');
            }catch(err){
                ASSERT('_message.postDelayed() parameter 2 error', err instanceof TypeError );
                _info(err.message)
            }
        

Router System

Routes are used to indicate a stateful view by a persistent URL.

With them you can access or share content directly with an URL, go back & forth with the browser buttons.

They should not be used for temporary view like a popup dialog or represent an action to execute.

This is why the router system of Tiny is this simple.

And for simplicity and compatibility, this router system uses window.location.hash (#string) for routing, instead of the fancy History API introduced in HTML5.

And Tiny provides a predefined View Manager for fast implement of views.

_route.watch()

Watch a certain route.

There is no unwatch() method, since you can easily drop a call in your route handler by checking internal flag (and you definitely will have an internal flag in such case).

The route parameter can be a string or a RegExp object.


            _route.watch(route, handler) : _route
            
            function handler(current_route, param_object){ ... }
        

This method will return _route object, you can chain methods if needed.

The route parameter can be:

The handler function will be called when:

This behavior can make view switching easier.

The current_route parameter of the callback function will be current hash string without starting '#'.

And the param_object will be true or an Object contains paramters mateched in URL. Like:


            // route match
            -> handler('current/route', true);

            // route match and has parameters
            -> handler('current/route/noname/123', {name: 'noname', id: '123'});
        

If watched route was matched last time but not this time, the param_object will be false.


            // route not match
            -> handler('current/route', false);
        

_route.check()

Check a route string or window.location.hash for route matching. Returns true if match found.

If you want Tiny to check automatically on window.location.hash changes, please use _route.on().


            _route.check([route_string]) : boolean
            // route_string : if not given, window.location.hash will be checked
        

EXAMPLES


            var data = [];
            function save_data(route, params){
                data.push(params);
            }

            _route
                .watch('/', function(route, param){ data.push(param ? 1 : 0) })
                .watch('/list', save_data)
                .watch('list/id:{id}', save_data);
            
            // invoke root rule only
            _route.check('');
            ASSERT('_route.check() root', FLAT(data) === '[1]');

            // '/' no match, a false parameter will be sent
            // and match '/list' without parameter
            var result = _route.check('/list/a');
            ASSERT('_route.check() true', result === true);
            ASSERT('data', FLAT(data) === '[1,0,true]');

            // no match, but '/list' matched previously, a false parameter will be sent
            result = _route.check('/id:123');
            ASSERT('_route.check() false', result === false);
            ASSERT('data add false', FLAT(data) === '[1,0,true,false]');

            // no match again, nothing should happen
            result = _route.check('lis');
            ASSERT('_route.check() false again', result === false);
            ASSERT('data no change', FLAT(data) === '[1,0,true,false]');

            // match '/list/id:{id}' and get parameters
            result = _route.check('word/list/id:123');
            ASSERT('_route.check() true again', result === true);
            ASSERT('data change', FLAT(data) === '[1,0,true,false,{"id":"123"}]');

            data = [];
            // match '/list' and '/list/id:{id}'
            result = _route.check('/list/id:456');
            ASSERT('_route.check() must true again', result === true);
            ASSERT('data change again', FLAT(data) === '[true,{"id":"456"}]');

            data = [];
            // no match becuase the section string is 'my_list', not 'list'
            // and previously matched routes will receive false
            result = _route.check('my_list/id:789');
            ASSERT('_route.check() shoule be false', result === false);
            ASSERT('data add 2 false', FLAT(data) === '[false,false]');
        

_route.on/off()

Start or stop monitoring window.onhashchange event and check current window.location.hash immediately.


            _route.on() : _route
            _route.off() : _route
        

_route.append()

Append sections to route string.


            _route.append(string_or_array[, trigger]) : _route
            // string_or_array : a string or an array of items to append
            // trigger         : whether trigger route change event, default is true
        

_route.remove()

Remove route string from first occurance of given string.

Slashes matters:


            _route.remove(section[, trigger]) : _route
            // section : from which the route string will be removed
        

EXAMPLES


            var data = [];
            function save_data(route, params){
                data.push(params);
            }

            _route
                .watch('/', function(){ data.push('/') })
                .watch('/books', save_data)
                .watch('books/id:{id}', save_data)
                .watch('books/{id}/{page}', save_data);
            
            // set a test route - no event triggered
            _route.set('/books', false);
            ASSERT('_route.set() no match', FLAT(data) === '[]');

            // start monitoring window.location.hash changes
            _route.on();
            // got a match immediately
            ASSERT('_route.on() match', FLAT(data) === '[true]');

            // add something to current route
            _route.append('id:123');
            ASSERT('_route.get()', _route.get() === '/books/id:123');
            // match '/books' and '/books/id:{id}' with the parameter
            ASSERT('_route.append()', FLAT(data) === '[true,true,{"id":"123"}]');

            // remove the 'id:123' section without trigger the event
            _route.remove('/id:', false);
            ASSERT('_route.get() after cut', _route.get() === '/books');
            // no change to the data
            ASSERT('_route.remove()', FLAT(data) === '[true,true,{"id":"123"}]');

            data = [];
            // send an array to _route.add()
            _route.append([123, 456]);
            ASSERT('_route.append() array', _route.get() === '/books/123/456');
            // '/books' match, 'books/id:{id}' no match, 'books/{id}/{page}' match
            ASSERT('_route.append() result', FLAT(data) === '[true,false,{"id":"123","page":"456"}]');

            // turn off event monitoring
            _route.off();
            // set a new route
            _route.set('my/books/id:789');
            ASSERT('_route.set() result', _route.get() === 'my/books/id:789');
            // no change to the data
            ASSERT('_route.set() array result', FLAT(data) === '[true,false,{"id":"123","page":"456"}]');

            data = [];
            // check window.location.hash
            _route.check();
            // '/books' no match, 'books/id:{id}' match, 'books/{id}/{page}' no match
            ASSERT('_route.on() again', FLAT(data) === '[false,{"id":"789"},false]');
            
            data = [];
            // move to root
            _route.set('/');
            _route.check();
            // '/' match, 'books/id:{id}' no match
            ASSERT('_route.set() to root', FLAT(data) === '["/",false]');
        

ERROR TESTS


            // A TypeError will be thrown on invalid parameter types
            // Detailed information can be found in console
            try{
                _route.watch(null);
                FAIL('_route.watch() paramter 1 error');
            }catch(err){
                ASSERT('_route.watch() paramter 1 error', err instanceof TypeError );
            }
            try{
                _route.watch('/test', null);
                FAIL('_route.watch() paramter 2 error');
            }catch(err){
                ASSERT('_route.watch() paramter 2 error', err instanceof TypeError );
            }

            try{
                _route.check(123);
                FAIL('_route.check() paramter 1 error');
            }catch(err){
                ASSERT('_route.check() paramter 1 error', err instanceof TypeError );
            }

            try{
                _route.append(null);
                FAIL('_route.append() paramter 1 error');
            }catch(err){
                ASSERT('_route.append() paramter 1 error', err instanceof TypeError );
            }

            try{
                _route.remove(null);
                FAIL('_route.remove() paramter 1 error');
            }catch(err){
                ASSERT('_route.remove() paramter 1 error', err instanceof TypeError );
            }
            

DOM Manipulation

_q() and _q1()

A tiny jQuery like function solely relies on document.querySelector(). It just provide some basic functions in order to keep lean. And we shouldn't reinvent the wheel when there are plenty good ones.

Note: It might have problems under IE8 since many CSS3 selectors are not supported and IE8 has some namespace problems on selectors.


Local Storage

_storage()

A handy function for accessing window.localStorage.

Integer, Boolean, Array, Date & Object types will be automatically converted on access.

Note: All local pages might share a same localStorage store in some browsers. You can use keyPrefix to avoid collisions.


            // filter keys by prefix, default is ''
            tiny.storage.keyPrefix = 'your_prefix';

            _storage()            // return all values as an Object
            _storage(key)         // get value by key
            _storage(key, value)  // set value by key
            _storage(key, null)   // delete item of given key
            _storage(null, null)  // delete all contents

            // or send an object for batch operations
            _storage({
                key1: value1,
                key2: null,
                ...
            })
        

EXAMPLES


            // clean up for test
            _storage('tiny_lib_test', null);

            // write a value
            _storage('test', false);
            ASSERT('_storage() no prefix', _storage('test') === false);

            // set a prefix
            tiny.storage.keyPrefix = 'tiny_lib';

            // should not be able to read from previous key
            ASSERT('_storage() with prefix', _storage('test') === undefined);

            // write value - with prefix
            _storage('test', true);
            _storage('num', 123);
            _storage('array', [4, 5, 6]);
            _storage('date', new Date(1467190047725));
            _storage('object', {x:1, y:2});

            // read - with prefix
            ASSERT('_storage() 1', _storage('test') === true);
            ASSERT('_storage() 2', _storage('num') === 123);
            ASSERT('_storage() 3', FLAT(_storage('array')) === '[4,5,6]');
            ASSERT('_storage() 4', FLAT(_storage('date')) === '"2016-06-29T08:47:27.725Z"');
            ASSERT('_storage() 5', FLAT(_storage('object')) === '{"x":1,"y":2}');

            // read all - with prefix
            var data = _storage();
            ASSERT('_storage() all 1', data.test === true);
            ASSERT('_storage() all 2', data.num === 123);
            ASSERT('_storage() all 3', FLAT(data.array) === '[4,5,6]');
            ASSERT('_storage() all 4', FLAT(data.date) === '"2016-06-29T08:47:27.725Z"');
            ASSERT('_storage() all 5', FLAT(data.object) === '{"x":1,"y":2}');

            // delete - with prefix
            _storage('date', null);
            ASSERT('_storage() delete', _storage('date') === undefined);

            // batch operation
            _storage({
                bool: false,    // add
                num: 789,       // modify
                array: null     // delete
            });
            ASSERT('_storage() batch 1', _storage('bool') === false);
            ASSERT('_storage() batch 2', _storage('num') === 789);
            ASSERT('_storage() batch 3', _storage('array') === undefined);

            // delete all - with prefix
            _storage(null, null);
            ASSERT('_storage() delete all', FLAT(_storage()) === '{}');

            tiny.storage.keyPrefix = '';
            // previous value with no prefix should still there
            ASSERT('_storage() no prefix value', _storage('test') === false);
        

ERROR TESTS


            // A TypeError will be thrown on invalid parameter types
            // Detailed information can be found in console
            try{
                _storage(123);
                FAIL('_storage() paramter 1 error');
            }catch(err){
                ASSERT('_storage() paramter 1 error', err instanceof TypeError );
            }
        

Internationalize

Tiny provides a set of simple functions to make i18n easier.

_lang()

Get a string from language string set.

Undefined strings will be collected in a list _lang.missing in runtime, this will make finding missing language strings easier.


            _lang(str [,lang_code])
            // str       : the string to lookup in current language string set
            // lang_code : the language code
        

        

The language strings should be assign to _lang.strings. You can modify it whatever you want. But the default section set in _lang.code must exist.


            // current languague code - default is 'en'
            // please use 
            _lang.code = 'en';

            // assign set object
            _lang.strings = {

                // default = 'en'
                "en" : {
                    '_name' : 'English',
                    "hello" : "Hello!"
                },

                // Simplified Chinese - 'zh-cn'
                "zh-cn" : {

                    '_name' : '简体中文',
                    'language' : '=> _name',   // refer to '_name', must starts with '=> '

                    // these options will be set to format functions on _lang.set() call if exist
                    '_decimalDelimiter' : '.',
                    '_thousandsDelimiter' : ' ',
                    '_currencyFormat' : '¥ [.00]',
                    '_dateFormat' : 'datetime',
                    '_dateNames' : {
                        day: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
                        dayAbbr: ['日', '一', '二', '三', '四', '五', '六'],
                        month: ['一月', '二月', '三月', '四月', '五月', '六月',
                                '七月', '八月', '九月', '十月', '十一月', '十二月'],
                        monthAbbr: ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'],
                        ap: ['上午', '下午']
                    },

                    // here goes your language strings
                    'hello' : "你好!"
                }
            };


        

_lang.set()

Get a string from language string set.


            _lang.set([lang_code])
            // lang_code : the language code
        

Format Template

_format()

Simply yet powerful enough format function with template support.

This function support both normal html templates and the shorthand templates.

Templates are parsed in a one-time loop over the string. In favor of performance, no RegExp is used.

Expanded shorthand templates will be cached in memory or their original <script> tags for performance.


            _format(template, data_object);
            // template    : the template string
            // data_object : an object contains data you want to fill into the template

            String._format(data_object);

            // format html template string
            _format('<span class="date-tag">Today is {date}</span>', {date: new Date()});
            // format shorthand template string (starts with '...')
            _format('span.date-tag :Today is {date}', new Date());
            // get template for HTML tag with id 'date-tag-template'
            '#date-tag-template'._format(obj_date);

            /*
            >> Template Token format for value fill-in
                {index}      : refer to data_object[index] item by index
                {key}        : refer to data_object[key], the token will be kept if undefined
                {*key}       : optional token, fill with '' if undefined
                {key|format} : set format string for data_object[key]
                {}           : fill with whole object, format with default style
                {|format}    : fill with whole object with given 'format' string
                {[any text]} : contents inside brackets will be output directly
            If a token's value is undefined, the token will be kept untouched.
            
            >> Special Formats
                {key|!html} : don't encode special html chars
                {$key}      : refer to a language string by calling _t(key)
                {key|5}     : output first 5 chars from beginning
                {key|5.}    : output first 5 chars and add ... at end
                {key|-5}    : output last 5 chars from ending
                {key|-5.}   : output last 5 chars and add ... at beginning
            
            >> Conditional Block
                {?key}        : Show block if data_object[key] is not empty or false, 
                                and if it is an Array, loop throught it to build a list
                    {subkey}  : Tokens inside block will be filled with data_object[key][subkey]
                {/?key}
                
                {!key}           : Show block if data_object[key] is empty or false
                    {other_key}	 : Tokens inside block will be filled with data_object[other_key]
                {/!key}
                
            >> Reference to template
                {#template-id}   : refer to a template block inside html file with id

            >> Shorthand Template Syntax
                ...          : always starts with three dots mark
                tag          : start a tag with tag name, DIV can be ommitted
                #id          : mark start of an ID
                .classname   : mark start of a class
                [attr=val]   : mark an attribute
                :text        : all string between this mark to next newline char will be treated as tag content
                >            : indicate an inline nested indent (won't work after ':' mark)
            
            */
        

EXAMPLES - SIMPLE FORMAT


            // use whole data object and format with 'DD'
            var result = _format('Today is {|DD}.', new Date(1118102950753));
            ASSERT('date', result === 'Today is Tuesday.');
            
            // cut long string
            var str = 'This is a very very loooooooong string';
            result = 'It says: "{|14.}"'._format(str);
            ASSERT('cut end', result === 'It says: "This is a very..."');
            result = 'It says: "{|-14.}"'._format(str);
            ASSERT('cut start', result === 'It says: "...ooooong string"');

            // output text and keep { } inside
            result = '{[Output {token} text\n with new line]}'._format({token: 123});
            ASSERT('{[output text]}', result === 'Output {token} text\n with new line');

            var my_items = {
                book: { price: 1299.99,
                        name: "my book" }
            };
            // refer to a sub key of data object
            result = 'The price is {book.price|,}'._format(my_items);
            ASSERT('sub key', result === 'The price is 1,299.99');

            var arr = [10, 20, 30];
            // array index - starts at 0
            result = 'Refer to {1}'._format(arr);
            ASSERT('array index', result === 'Refer to 20');

            // special chars will be escaped
            result = 'Click {}'._format('<a href="#">HERE</a>');
            ASSERT('escape html', result === 'Click &lt;a href=&quot;#&quot;&gt;HERE&lt;/a&gt;');
            // don't escape
            result = 'Click {|!html}'._format('<a href="#">HERE</a>');
            ASSERT('keep html', result === 'Click <a href="#">HERE</a>');
        

EXAMPLES - SHORTHAND TEMPLATE


            var data = {
                id: 123456,
                title: 'Plan A & B',
                tasks: 7,
                people: 9
            };

            // shorthand template is short and clear
            var template = '... li #{id} .item > .title :{title}';
            var result = template._format(data);
            ASSERT('inline shorthand 1',
                result ===
                '<li id="123456" class="item">\n' +
                '  <div class="title">Plan A &amp; B</div>\n' +
                '</li>\n');
                
            template = '...' +
                'li #{id} .item :{[\n' +
                '  Output {token} text\n' +
                '  with new line]}\n' +
                '  b :Though this might mess up';
            result = template._format(data);
            ASSERT('inline shorthand 2',
                result ===
                '<li id="123456" class="item">\n' +
                '  Output {token} text\n' +
                '  with new line\n' +
                '  <b>Though this might mess up</b>\n' +
                '</li>\n');

            // complex shorthand template
            //   indents('\t' or ' ') and end-of-lines('\n') are important
            //   you can choose to use tabs or spaces, mixing them might result chaos
            template = '...' +
                'li.item[data={*data}][ref={url}]\n'+
                '  .title :{title}\n' +
                '  .tags :{tasks} TASKS <i>/</i> {people} PEOPLE\n';

            // format without data - returns template
            result = template._format();
            ASSERT('shorthand -> html template',
                result ===
                '<li class="item" data="{*data}" ref="{url}">\n' +
                '  <div class="title">{title}</div>\n' +
                '  <div class="tags">{tasks} TASKS <i>/</i> {people} PEOPLE</div>\n' +
                '</li>\n');

            // fill in data
            // undefined value's token will be kept untouched unless it's optional (start with *)
            result = result._format(data);
            ASSERT('html template -> output',
                result ===
                '<li class="item" data="" ref="{url}">\n' +
                '  <div class="title">Plan A &amp; B</div>\n' +
                '  <div class="tags">7 TASKS <i>/</i> 9 PEOPLE</div>\n' +
                '</li>\n');
        

EXAMPLES - TEMPLATES INSIDE HTML FILE


            <!--
            ****** TEMPLATES IN HTML FILE ******
            Templates must be placed inside <script> tags to keep indents, tags and invisible.
            And it's better to place them after the body tag to avoid being translated by
            the _lang.translate() function before usage.
            -->

            <!-- Normal HTML Template -->
            <script type="text/x-template" id="tpl-circular-ref">
                <div class="content">
                    <h2>
                        <a href="#/project/{id}">
                            <img src="img/project_{logo}.png"/>
                            Plan List of {project}
                        </a>
                    </h2>
                    <ul class="plan-list">
                        {?plans}
                            {#tpl-circular-ref} <!-- Circular Reference to self => ReferenceError -->
                        {/?plans}
                    </ul>
                </div>
            </script>

            <!-- Shorthand Equilavent of above -->
            <!--
                Indents are important in shorthand templates
                a tab is count as one indent, and don't mix tabs and spaces
                and conditional {} tokens should start with : to output as text
            -->
            <script type="text/x-template" id="tpl-sh-block">
            ...
            .content
                h2
                    a [href=#/project/{id}]
                        img [src=img/project_{logo}.png]
                        :Plan List of {project}
                    :{!done}Project in Progress{/!done}
                ul .plan-list
                    :{?plans}
                        :{#tpl-sh-li}
                    :{/?plans}
            </script>

            <!-- List item template -->
            <script type="text/x-template" id="tpl-sh-li">
            ...
            li .item [id={id}]
                .title :{title}
                .tags :{tasks} TASKS <i>/</i> {people} PEOPLE
            </script>
        

        /****** Javascript File ******/

        // returns expanded HTML template string if no data object is given
        var result = _format('#tpl-sh-block');
        ASSERT('sh -> html',
            result ===
            '<div class="content">\n'+
            '    <h2>\n'+
            '        <a href="#/project/{id}">\n'+
            '            <img src="img/project_{logo}.png"/>\n'+
            '            Plan List of {project}\n'+
            '        </a>\n'+
            '        {!done}Project in Progress{/!done}\n'+
            '    </h2>\n'+
            '    <ul class="plan-list">\n'+
            '        {?plans}\n'+
            '            {#tpl-sh-li}\n'+
            '        {/?plans}\n'+
            '    </ul>\n'+
            '</div>\n');
        
        // and the expanded template should be cached inside the original tag
        var cache = document.getElementById('tpl-sh-block').innerHTML;
        ASSERT('cached', cache == result);

        // prepare data object
        var data = {
            id: 789,
            project: 'Tiny Project',
            logo: 'panda',
            plans: [
                {id: 1, title: 'Sprint 1', tasks: 2, people: 5},
                {id: 2, title: 'Sprint 2', tasks: 15, people: 3},
            ]
        };

        // fill data into the template
        result = result._format(data);
        ASSERT('output',
            result ==
            '<div class="content">\n' +
            '    <h2>\n' +
            '        <a href="#/project/789">\n' +
            '            <img src="img/project_panda.png"/>\n' +
            '            Plan List of Tiny Project\n' +
            '        </a>\n' +
            '\n'+ // {!done}
            'Project in Progress\n' + // {/!done}
            '    </h2>\n' +
            '    <ul class="plan-list">\n' +
            '\n'+ // {?plans}
            '<li class="item" id="1">\n' +
            '    <div class="title">Sprint 1</div>\n' +
            '    <div class="tags">2 TASKS <i>/</i> 5 PEOPLE</div>\n' +
            '</li>\n' +
            '<li class="item" id="2">\n' +
            '    <div class="title">Sprint 2</div>\n' +
            '    <div class="tags">15 TASKS <i>/</i> 3 PEOPLE</div>\n' +
            '</li>\n' +
            '\n' +  // {/?plans}
            '    </ul>\n' +
            '</div>\n');
        

ERROR TESTS


            <!-- HTML CONTENT FOR ERROR TESTS -->

            <!-- Missing close '}' => SyntaxError -->
            <script type="text/x-template" id="tpl-missing-mustache">
                <div class="{state">{title} </div>
            </script>
            <!-- Missing close ']}' => SyntaxError -->
            <script type="text/x-template" id="tpl-missing-bracket">
                <div class="{state}">{[Use {token} to
                replace} </div>
            </script>
            <!-- Missing close '{/?token}' => SyntaxError -->
            <script type="text/x-template" id="tpl-missing-close-token">
                {?has_state}
                <div class="{state}">
                    {?has_state}
                    {[Use {token} to replace]}
                    {/?has_state}
                </div>
                {/?has_token}  <!-- wrong close token -->
            </script>
        

            // Detailed error information can be found in console

            // TypeError : invalid parameter types
            try{
                _format({obj: "obj"});
                FAIL('_format() paramter error');
            }catch(err){
                ASSERT('_format() paramter error', err instanceof TypeError );
            }
            // TypeError : data object is not an object
            try{
                _format('{key}', 'test');
                FAIL('_format() invalid data object');
            }catch(err){
                ASSERT('_format() invalid data object', err instanceof TypeError );
            }
            
            // ReferenceError : element of given id is not found
            try{
                _format('#test-id-here');
                FAIL('_format() id not found');
            }catch(err){
                ASSERT('_format() id not found', err instanceof ReferenceError );
            }

            // ReferenceError : circular reference is detected
            try{
                _format('#tpl-circular-ref');
                FAIL('_format() circular reference');
            }catch(err){
                ASSERT('_format() circular reference', err instanceof ReferenceError );
            }

            // SyntaxError : '\n' inside token
            try{
                '{test\nnew_line}'._format({});
                FAIL('_format() \\n inside');
            }catch(err){
                ASSERT('_format() \\n inside', err instanceof SyntaxError );
            }

            // SyntaxError : a close '}' is missing
            try{
                _format('#tpl-missing-mustache', {});
                FAIL('_format() missing }');
            }catch(err){
                ASSERT('_format() missing }', err instanceof SyntaxError );
            }

            // SyntaxError : a close ']}' is missing
            try{
                _format('#tpl-missing-bracket', {});
                FAIL('_format() missing ]}');
            }catch(err){
                ASSERT('_format() missing ]}', err instanceof SyntaxError );
            }

            // SyntaxError : a close '{/?token}' is missing
            try{
                _format('#tpl-missing-close-token', {});
                FAIL('_format() missing clsoe {/?token}');
            }catch(err){
                ASSERT('_format() missing close {/?token}}', err instanceof SyntaxError );
            }
        

_htmlSafe()

Make a string HTML-safe.


            _htmlSafe(str [, keep_spaces]);

            String._htmlSafe([keep_spaces]);
        

EXAMPLES


            var txt = 'task:\n  >> done';

            ASSERT('1', _htmlSafe(txt) === 'task:<br/>  &gt;&gt; done');
            ASSERT('2', txt._htmlSafe() === 'task:<br/>  &gt;&gt; done');

            // keep white spaces in html
            ASSERT('3', _htmlSafe(txt, true) === 'task:<br/>&nbsp;&nbsp;&gt;&gt;&nbsp;done');
            ASSERT('4', txt._htmlSafe(true) === 'task:<br/>&nbsp;&nbsp;&gt;&gt;&nbsp;done');
        

ERROR TESTS


            // A TypeError will be thrown on invalid parameter types
            // Detailed information can be found in console
            try{
                _htmlSafe(123);
                FAIL('_htmlSafe() paramter 1 error');
            }catch(err){
                ASSERT('_htmlSafe() paramter 1 error', err instanceof TypeError );
            }
        

_formatNumber()

Number format function.


            _formatNumber(num[, format]);
            
            Number._format([format]);

            // you can set the default curreny format
            _format.currencyFormat = '$[,.00]';
            
            // and default delimiter characters
            _format.decimalDelimiter = '.';
            _format.thousandsDelimiter = ',';

            // FORMAT STRING CODES:
            // '[]'   = indicate format token, '$ [.].00' => '$ 99.00'
            // ','    = add thousand delimiter, hex will add ' ' every 4 digits
            // '0000' = pad number to given width with '0'
            // '.00'  = set decimal place by number of '0'
            // '$'    = use default currency format
            // '%'    = convert to percent, decimal only
            // 'x'    = lowercase hex
            // 'X'    = uppercase HEX
        

EXAMPLES


            var num = 123456.789;

            ASSERT('1', num._format() == '123456.789');
            ASSERT('2', num._format('Round to [.]') == 'Round to 123457');
            ASSERT('3', num._format('.00') == '123456.79');
            ASSERT('4', num._format(',') == '123,456.789');
            ASSERT('5', num._format('[,.00]') == '123,456.79');
            ASSERT('6', num._format('[,.00%] * 100') == '12,345,678.90% * 100');
            ASSERT('7', num._format('$') == '$123,456.79');

            // pad zero
            num = 12345.678;
            ASSERT('pad 1', num._format('00000000') == '00012345.678');
            ASSERT('pad 2', num._format('00000000.') == '00012346');
            ASSERT('pad 3', num._format('Padded: [,00000000.00]') == 'Padded: 00,012,345.68');

            // change delimiters & currency format
            _format.decimalDelimiter = ',';
            _format.thousandsDelimiter = ' ';
            _format.currencyFormat = '[,.00] €';

            ASSERT('currency', num._format('$') == '12 345,68 €');
            
            // hex - won't affect by above settings
            num = 123456.789;
            ASSERT('hex 1', num._format('x') == '1e240.c9fbe76c9');
            ASSERT('hex 2', num._format('X.') == '1E240');
            ASSERT('hex 3', num._format('X.00') == '1E240.C9');
            ASSERT('hex 4', num._format('X0000000000,.') == '00 0001 E240');
            
        

ERROR TESTS


            // A TypeError will be thrown on invalid parameter types
            // Detailed information can be found in console
            try{
                _formatNumber('123');
                FAIL('_formatNumber() paramter 1 error');
            }catch(err){
                ASSERT('_formatNumber() paramter 1 error', err instanceof TypeError );
            }
            try{
                _formatNumber(123, 456);
                FAIL('_formatNumber() paramter 2 error');
            }catch(err){
                ASSERT('_formatNumber() paramter 2 error', err instanceof TypeError );
            }
        

_formatDate()

Date format function.


            // you can use localized date names by replacing this data object
            _format.dateNames = {
                day: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
                dayAbbr: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
                month: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
                    'August', 'September', 'October', 'November', 'December'],
                monthAbbr: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
                ap: ['AM', 'PM']
            };

            // and set default format
            _format.defaultDateFormat = 'datetime';
            // 'datetime' => '2005-06-07 08:09:10'
            // 'date'     => '2005-06-07' // yyyy-MM-dd
            // 'time'     => '08:09:10'   // HH:mm:ss
            // 'iso'      => '2005-06-07T00:09:10.753Z' // ISO 8601

            // call the function
            _formatDate(date[, format]);

            // or use extended method
            Date._format([format]);

            // FORMAT STRING CODES:
            // '[]'   = indicate format token, 'yyyy[-M-d]' => 'yyyy-6-7'
            // 'yyyy' = 2009,  'yy' = 09,    'y' = 9         // Year
            // 'M'    = 6,     'MM' = 06                     // Numeric month
            // 'MMM'  = Jun,   'MMMM' = June                 // Month name
            // 'd'    = 7,     'dd' = 07                     // Day of the month
            // 'D'    = Tue,   'DD' = Tuesday                // Day of the week
            // 'h'    = 8,     'hh' = 08                     // 12 Hour clock
            // 'H'    = 8,     'HH' = 08                     // 24 Hour clock 
            // 'm'    = 9,     'mm' = 09                     // Minutes
            // 's'    = 10,    'ss' = 10,     'sss' = 753    // Seconds & Milliseconds
            // 'z'    = +08,   'zz' = +0800,  'ZZ' = +08:00  // Timezone
            // 't'    = AM,    // AM / PM
        

EXAMPLES


            var d = new Date(1118102950753);
            ASSERT('0', d._format() === '2005-06-07 08:09:10');
            ASSERT('1', d._format('datetime') === '2005-06-07 08:09:10');
            ASSERT('2', d._format('date') == '2005-06-07');
            ASSERT('3', d._format('time') == '08:09:10');
            ASSERT('4', d._format('iso') == '2005-06-07T00:09:10.753Z');
            ASSERT('5', d._format('D, d MMM yyyy H:m:s') == 'Tue, 7 Jun 2005 8:9:10');

            // also accept date number from Date.getTime()
            // text inside [] will be kept
            ASSERT('[] token',  _formatDate(1118102950753, 'Today is [DD]') == 'Today is Tuesday');
            
            // mix token and text
            ASSERT('mix', d._format('[D, d MMM yyyy H:m:s] GMT[zz]') == 'Tue, 7 Jun 2005 8:9:10 GMT+0800');
        

ERROR TESTS


            // A TypeError will be thrown on invalid parameter types
            // Detailed information can be found in console
            try{
                _formatDate('123');
                FAIL('_formatDate() paramter 1 error');
            }catch(err){
                ASSERT('_formatDate() paramter 1 error', err instanceof TypeError );
            }
            try{
                _formatDate(new Date(), 456);
                FAIL('_formatDate() paramter 2 error');
            }catch(err){
                ASSERT('_formatDate() paramter 2 error', err instanceof TypeError );
            }