cocholate

4.0.0 • Public • Published

cocholate

"Why cocholate? Because it goes well with vanilla." -- cocholate's PR.

cocholate is a small library for DOM manipulation. It's meant to be small, easily understandable and fast.

Current status of the project

The current version of cocholate, v4.0.0, is considered to be stable and complete. Suggestions and patches are welcome. Besides bug fixes, there are no future changes planned.

cocholate is part of the ustack, a set of libraries to build web applications which aims to be fully understandable by those who use it.

Installation

The dependencies of cocholate are two:

cocholate is written in Javascript. You can use it in the browser by sourcing the dependencies and the main file:

<script src="dale.js"></script>
<script src="teishi.js"></script>
<script src="cocholate.js"></script>

Or you can use these links to the latest version - courtesy of jsDelivr.

<script src="https://cdn.jsdelivr.net/gh/fpereiro/dale@3199cebc19ec639abf242fd8788481b65c7dc3a3/dale.js"></script>
<script src="https://cdn.jsdelivr.net/gh/fpereiro/teishi@31a9cf552dbaee79fb1c2b7d12c6fad20f987983/teishi.js"></script>
<script src="https://cdn.jsdelivr.net/gh/fpereiro/cocholate@47a37cabfc0684091d6ff1d01f4c300f24ed11c1/cocholate.js"></script>

cocholate is exclusively a client-side library. Still, you can find it in npm: npm install cocholate

Browser compatibility has been tested in the following browsers:

  • Google Chrome 15 and above.
  • Mozilla Firefox 3 and above.
  • Safari 4 and above.
  • Internet Explorer 6 and above.
  • Microsoft Edge 14 and above.
  • Opera 10.6 and above.
  • Yandex 14.12 and above.

The author wishes to thank Browserstack for providing tools to test cross-browser compatibility.

Loading cocholate

As soon as you include cocholate, it will be available on window.c.

var c = window.c;

A couple notes regarding polyfills:

  • If cocholate detects that the DOM method insertAdjacentHTML is not defined, cocholate will set it (this will only happen in Firefox 7 and below and Safari 3 and below).
  • Because cocholate uses teishi, the indexOf method for arrays will also be set (this will happen only in Firefox 1, Edge 12 and below and Internet Explorer 8 and below).

Selectors

c is the main function of the library. It takes a selector and an optional fun.

Let's go with the simplest case:

// This code will return an array with all the divs in the document.
c ('div');
// This code will return an array with all the elements that have the class .nav.
c ('.nav');

Whenever you pass a string as a selector and no other arguments, cocholate simply uses the native document.querySelectorAll, with the sole difference that returns an array instead of a NodeList.

A very important exception is when you pass a string selector that targets an id, such as #hola or div#hola, you won't get an array - instead, you'll get either the element itself, or undefined:

// This code will return the div with id `hola`
c ('#hola');

// This code will do the same thing.
c ('#hola');

// This code, however, won't do the same thing, though it means the same thing.
c ('body #hola');

// This code will return an array, since it targets the children of an element.
c ('#hola p');

If you invoke c with the string 'body' as the selector, you will receive only the body itself as the result, instead of an array containing the body.

c ('body') === document.body // this line will be true

Note: in old browsers that do not support querySelectorAll (Firefox 3 and below, Internet Explorer 7 and below), cocholate provides a limited variety of selectors, with the following shapes: TAG, #ID, .CLASS, TAG#ID and TAG.CLASS. In these old browsers, if you use a selector that does not conform to these specific forms, cocholate will print an error and return false; in particular, the following characters are forbidden in selectors: ,>[].

If instead of searching from all elements you want to search within a specific element, instead of a string selector you can use an object with the form {selector: SELECTOR, from: FROM}, where SELECTOR is the string selector and FROM is an DOM element. For example:

// This will return all divs with class `hello` from the body
c ({selector: 'div', from: c ('body')});

// This will return all paragraphs with class `hello` from a div with id `hello`
c ({selector: 'div', from: document.getElementById ('hello')});

// This is equivalent to the last thing we did
c ({selector: 'div', from: c ('div#hello')});

If you want to use the logical operations and, not and or, you can do so by using a selector that's an array where the first element is either ':and', ':or' or ':not'.

// This code will return an array with all the elements that are `div` or `p`
c ([':or', 'div', 'p']);

// This code will return an empty array (because there are no elements that can be simultaneously a `div` and a `p`).
c ([':and', 'div', 'p']);

// This code will return an array with all the elements that are neither `div` or `p`.
c ([':not', 'div', 'p']);

You can also nest the selectors to an arbitrary degree, as long as the first element of each nested array selector is one of ':and', ':or' or ':not':

// This code will return an array with all the elements that are `div` or `p` and are contained inside `body`.
c ([':and', 'body *', [':or', 'div', 'p']]);

A subtle point: when you pass multiple elements to a ':not' selector, it is actually equivalent to using the ':or' selector. For example, [':not', 'div', 'p'] is equivalent to writing [':not', [':or', 'div', 'p']]. The reason for this disambiguation is that and and or are operations on two operands, where not is an unary operation. The choice of or instead of and reflects what I believe is the most intuitive and common usage of not for multiple operands.

// These two calls will return the same result.
c ([':not', 'div', 'p']);
c ([':not', [':or', 'div', 'p']]);

If you want to perform an operation on a certain DOM element, you can directly pass it to c.

// These two calls are equivalent.
c ('#hello');
c (document.getElementById ('hello'));

Note that in this case c will return a single result, instead of an array of results, because the DOM element is only one.

fun

Besides returning an array of DOM elements, we will want to do some operations on them. To do this, we can pass a second argument to c, which is a function that will be executed for every element that matched the selector. The results will be collected on an array that's then returned.

For example, the following call will return an array with the ids of all the divs in the document.

c ('div', function (e) {
   return e.getAttribute ('id');
});

All the following DOM functions are implemented as underlying calls to c, passing its specific logic as the second argument to c.

DOM functions

All the functions presented in this section take a selector as its first element and execute its logic for each of the elements matching the selector.

c.empty

c.empty removes all the DOM elements within the elements matched by the selector. In other words, it completely gets rid of all the DOM elements nested inside of the matching elements. This function has no meaningful return value. If an invalid selector was passed to this function, an error will be printed.

c.fill

c.fill takes html (an HTML string) as its second argument and then fills it with the provided HTML string. This function has no meaningful return value. If an invalid selector was passed to this function, an error will be printed.

c.place

c.place takes where as its second argument and html as its third. where can be one of 'beforeBegin', 'afterBegin', 'beforeEnd', 'afterEnd', and html is an HTML string. This function has no meaningful return value. Its functionality is based on that of insertAdjacentHTML.

To explain what this function does, an example serves best. If you start with the following HTML:

<div id="vamo">
</div>

If you would execute the following function call:

c.place ('#vamo', 'beforeBegin', '<p>beforeBegin</p>');

You would end up with the following HTML.

<p>beforeBegin</p>
<div id="vamo">
</div>

If you then do the following three calls:

c.place ('#vamo', 'afterBegin', '<p>beforeBegin</p>');
c.place ('#vamo', 'beforeEnd', '<p>beforeEnd</p>');
c.place ('#vamo', 'afterEnd', '<p>afterEnd</p>');

You will get:

<p>beforeBegin</p>
<div id="vamo">
<p>afterBegin</p>
<p>beforeEnd</p>
</div>
<p>afterEnd</p>

c.get

c.get is useful for fetching attributes from elements. It takes attributes as its second argument (which can be undefined, a string or an array of strings, each of them representing an attribute name) and an optional boolean third parameter css which marks whether you want to get CSS properties instead of DOM ones.

For each of the matching elements, this function will return an object where the key is the attribute name and the corresponding value is the attribute value. All these objects are wrapped in an array (with the sole exception of a selector that targets an id).

For example, if you have the following HTML:

<p id="a" class="red"></p>
<p id="b" class="blue"></p>
<p id="c" class="green"></p>

And you run this code:

c.get ('p', 'class');

You'll get an array with three objects: [{class: 'red'}, {class: 'blue'}, {class: 'green'}].

If you run this code:

c.get ('p', ['id', 'class']);

You'll get an array with three objects but two properties each: [{id: 'a', class: 'red'}, {id: 'b', class: 'blue'}, {id: 'c', class: 'green'}].

As with c, if you pass a selector that targets the id of an element, you will get the attributes themselves without them being wrapped in an array:

c.get ('#a', 'class');  // will return {class: 'red'}
c.get ('p#a', 'class'); // will also return {class: 'red'}

Using the css attribute, you can obtain the CSS properties of an element. Consider this example:

<p style="color: red;"></p>
<p style="color: blue;"></p>
<p style="color: green;"></p>
c.get ('p', 'color', true);

If you run this code on the above HTML, you will obtain an array with three objects: [{color: 'red'}, {color: 'blue'}, {color: 'green'}].

Finally, either with normal attributes or CSS ones, if the attribute is not present, you will get null as its value. For example:

c.get ('p', 'name');         // will return `[{name: null}]`
c.get ('p', 'height', true); // will return `[{height: null}]`

If attributes is undefined, all the attributes will be returned, except those with falsy values (like null, '', false, 0 and false). If you want to bring all CSS attributes, you can explicitly pass undefined as a second argument; note that this will bring only the inline CSS attributes, and not the computed CSS values for the element.

c.set

This function is similar to c.get, except that it sets the attributes instead of getting them. This function has no meaningful return value.

This function takes a selector as first argument, and as second argument an object with all the properties you wish to set. An optional css flag is the third argument, in order to set inline CSS properties.

If you have the following HTML:

<p>
</p>
c.set ('p', {class: 'someclass'});

The HTML will look like this:

<p class="someclass">
</p>

To remove an attribute, just pass null as the attribute value. If you execute this code:

c.set ('body p', {class: null});

The HTML will go back to its original state:

<p>
</p>

If you pass a truthy third argument, you'll set/unset CSS properties instead.

c.set ('body p', {color: 'red'}, true);

Now the HTML will look like this:

<p style="color: red;">
</p>

To remove the style property, you can also use null as a value:

c.set ('body p', {color: null}, true);

Now the HTML will look like this:

<p style="">
</p>

By default, if the element whose attribute is being modified has an onchange event handler, the onchange event will be automatically triggered. For example:

<input id="hello">
<script>
   c ('#hello').onchange = function () {
      alert (this.value);
   }

   c.set ('#hello', {value: 2});
</script>

Because of the event handler that we assigned to #hello, we will see an alert with the value 2.

This also is the case with CSS events. For example, this call to c.set will also trigger the onchange event:

<input id="hello">
<script>
   c ('#hello').onchange = function () {
      alert (this.style.color);
   }

   c.set ('#hello', {color: 'lime'}, true);
</script>

If you want to override this behavior, you can simply pass a truthy fourth argument:

// for normal attributes
c.set ('#hello', {value: 2}, false, true);

// for CSS attributes
c.set ('#hello', {color: 'lime'}, true, true);

c.fire

This function creates an event and triggers it on the specified elements. It takes an eventType as its second argument, a string that determines which argument is fired (for example, 'click'). This function has no meaningful return value.

c.fire ('#button', 'click');

c.fire is useful for test scripts that simulate user interactions.

Non-DOM functions

Besides the six DOM functions, there are five more for a few things that are convenient to have around.

c.ready

A function that gets executed when the HTML page and all its resources (including stylesheets and scripts) have finished loading. Takes a single argument, fun, containing the code to be executed when this event happens.

This function is handy to prevent executing your application code before all scripts are loaded. This will happen automatically on most browsers if you place all your scripts at the bottom of the body - the exception is Internet Explorer 8 and below, which seem to run the scripts in parallel.

This function is also handy if you want to wait for all stylesheets to load before executing your script.

c.cookie

Quick & dirty cookie parsing. Takes an optional argument, cookie, a cookie string that will be parsed. If you don't pass any arguments, this function will read the cookie at document.cookie instead.

This function returns an object with keys/values, each of them pertaining to a property of the cookie.

If you pass false as the argument, c.cookie will delete all the cookies that are accessible to javascript - that is, those that don't have the HttpOnly directive/attribute.

c.ajax

This function can make ajax calls and provides a few conveniences.

It takes five arguments:

  • method: a string. Defaults to 'GET' if you pass a falsy argument.
  • path: a string with the target path.
  • headers: an object where every value is a string. Defaults to {} if you pass a falsy argument.
  • body: the body of the request. Defaults to '' if you pass a falsy argument.
  • callback: a function to be executed after the request is completed. Defaults to an empty function if you pass a falsy argument.

The conveniences provided are:

  • You can directly pass a FormData object, useful for multipart/form-data requests.
  • If you pass an array or object as body, the content-type header will be automatically set to application/json and the body will be stringified.
  • All ajax requests done through this function are asynchronous.
  • The function will synchronously return an object of the form {headers: ..., body: ..., xhr: <the request object>} (corresponding to the request data).
  • If the response has a code 200 or 304, the callback will receive null as its first argument and the following object as the second argument: {headers: {...}, body: ..., xhr: <the request object>}. If the Content-Type response header is application/json, the body will be parsed - if the body turns out to be invalid JSON, its value will be false.
  • If the code is not 200 or 304, the request object will be received as the first argument. The request object contains all the relevant information, including payloads and errors.

c.loadScript

This function requests a script and places it on the DOM, at the bottom of the body. It takes two arguments: src, the path to the javascript file; and an optional callback that is executed after the script is fetched. If callback is not passed, it will default to an empty function.

c.loadScript uses c.ajax to retrieve the script asynchronously and will return the result of its invocation to c.ajax - which will be false if src is invalid, and a request object otherwise.

If the script is successfully fetched, callback will receive two arguments (null and the request object); in case of error, it will receive the request object as its first argument.

c.test

This function allows to define and execute tests. It's meant as an ultra lightweight yet effective test runner. This function takes two arguments: tests, which is an array, and callback, which is an optional function to be executed when the test suite finishes running. Each of the elements contained by tests should also be an array. The three possible forms for each test array is:

  • [TAG, ACTION, CHECK]
  • [TAG, CHECK]
  • [] (this is a no-op, useful for running a test only if a condition is met)

TAG is a string which prints the name of the test being performed. CHECK is a synchronous function that performs a check; if the check is successful, the should return true - this will make c.test throw an error (unless you specify another behavior in callback). Any other value returned by CHECK (even undefined) will signify an error - in fact, the idea is that you return an error message whenever one of your checks fails.

ACTION is a potentially asynchronous function that performs an action. It will be executed before CHECK. If this function returns a value other than undefined, it will be considered synchronous and CHECK will be executed immediately afterwards. If you however wish to perform an async operation, you can do so and not return any value. When the async operation is done, use the next function passed as the first argument to ACTION, which is the callback. If you wish to wait n milliseconds before CHECK gets called, pass a non-negative integer to next - this is equivalent as writing setTimeout (next, <milliseconds>).

If your desired wait time after an ACTION should be at most a certain number of milliseconds, you can pass a second argument to next, which will signify that the CHECK function should be executed every n milliseconds up until the total wait time (that you specified in the first argument) is elapsed. For example, if you invoke next (1000, 10); after executing an ACTION, the CHECK function will be run every 10 milliseconds, for up to a second, until either the CHECK function succeeds or the time runs up. This is very useful since most of the time, for async operations, you don't know exactly how long they will take. This, however, requires that your CHECK function should be able to be run multiple times without detriment to the state of the test suite.

c.test will execute all tests in sequence and stop at the first error. It will print the TAG for each test about to be executed. If you have passed a callback, that function will receive either an error as its first argument or the number of milliseconds that the successful test run took to run as its second argument. If you don't provide a callback function, c.test will print either the error or success message to the console.

If c.test receives an invalid tests array, it will print an error and return false. Otherwise, the function will return undefined. Note that, whether the test suite fails or succeeds, c.test will return undefined - false only denotes invalid tests.

prod mode

cocholate's functions spend most of its running time (easily 80-90%) performing validations to their inputs. While validation is essential to shorten the debug cycle when developing, in certain cases you might want to turn it off to improve performance. This can be done by enabling prod mode. To do this, set c.prod to true.

The cost of turning off validation is that if there's an invalid invocation somewhere, an error will be thrown.

Source code

The complete source code is contained in cocholate.js. It is about 360 lines long.

Below is the annotated source.

/*
cocholate - v4.0.0

Written by Federico Pereiro (fpereiro@gmail.com) and released into the public domain.

Please refer to readme.md to read the annotated source.
*/

Setup

We wrap the entire file in a self-executing anonymous function. This practice is commonly named the javascript module pattern. The purpose of it is to wrap our code in a closure and hence avoid making the local variables we define here to be available outside of this module.

(function () {

If we're in node.js, we print an error and return undefined.

   if (typeof exports === 'object') return console.log ('cocholate only works in a browser!');

We require dale and teishi. Note that, in the browser, dale and teishi will be loaded as global variables.

   var dale   = window.dale;
   var teishi = window.teishi;

We create an alias to teishi.type, the function for finding out the type of an element. We do the same for teishi.clog, a function for printing logs that also returns false. We also do the same for teishi.inc, a function for checking whether a given element is contained in an array.

   var type = teishi.type, clog = teishi.clog, inc = teishi.inc;

Polyfill for insertAdjacentHTML

We will define a polyfill for insertAdjacentHTML, which will be necessary in old versions of Safari and Firefox. It is based on Eli Grey's polyfill.

We set the function only if it's not defined. The function takes two arguments, position and html.

   if (! document.createElement ('_').insertAdjacentHTML) HTMLElement.prototype.insertAdjacentHTML = function (position, html) {

We create a container element and then we set its innerHTML property to the html we received as a string. This will create all the desired DOM nodes inside container.

      var container = document.createElement ('div');
      container.innerHTML = html;

We now iterate the outermost elements inside container. By outermost, I mean that only those elements that are direct children of container will be iterated - whereas elements that are inside these top-level children will not be iterated.

We're, however, iterating the elements in a rather strange way. Instead of using a for loop or an equivalent functional construct, we're executing the same piece of code as long as container has one element. How can this work without setting us for an infinite loop? Let's see it in a minute.

      while (container.firstChild) {

If position is beforeBegin, we place container.firstChild before the element, using the insertBefore method on the element's parent.

Once we do this, container.firstChild (the first element we're positioning) will be in its desired position and not on container anymore. in this way, the next time the while loop runs, container.firstChild will be the second element we want to place - and if there's no second element, the loop will be finished.

         if      (position === 'beforeBegin') this.parentNode.insertBefore (container.firstChild, this);

If position is afterBegin, we place container.firstChild as the first children of the element using the insertBefore method on the element itself.

         else if (position === 'afterBegin')  this.insertBefore            (container.firstChild, this.firstChild);

If position is beforeEnd, we merely append container.firstChild to the element.

         else if (position === 'beforeEnd')   this.appendChild             (container.firstChild);

Finally, if position is afterEnd, we place container.firstChild just after the element using the insertBefore method on the parent.

         else                                 this.parentNode.insertBefore (container.firstChild, this.nextElementSibling)

There's nothing else to do, so we close the loop and the polyfill function.

      }
   }

Core

We define c, the main function of the library. Note we also attach it to window.c, so that it is globally available to other scripts. This function takes two arguments, selector and fun.

c, besides being a function, will serve as an object that collects the other functions of the library.

   var c = window.c = function (selector, fun) {

If prod mode is not enabled, we check that fun is a function or undefined. If it is neither, an error is printed and the function returns false.

Note we pass true as the fourth argument to teishi.stop. We will do this for every invocation of teishi.stop and teishi.v, to tell teishi not to validate our validation rules. This will yield a (very small) performance improvement.

      if (! c.prod && teishi.stop ('c', ['fun', fun, ['function', 'undefined'], 'oneOf'], undefined, true)) return false;

We create a local variable that will indicate whether the selector is actually a DOM node.

      var selectorIsNode = selector && selector.nodeName;

We define a local variable elements that will contain all the DOM elements to which selector refers. If the selector is itself a DOM node, we wrap it in an array. Otherwise, the search for all matching elements is done by c.find, a function which we'll see below.

      var elements = selectorIsNode ? [selector] : c.find (selector);

If c.find returns false, this means that the selector is invalid. In this case, c.find will have already printed an error message. We return false.

      if (elements === false) return false;

If we're here, the input is valid.

If fun was passed, we first collect all extra arguments passed to c into an array named args. This array will always exclude selector and fun. If no extra arguments were passed, args will be an empty array.

      if (fun) {
         var args = dale.go (arguments, function (v) {return v}).slice (2);

We iterate through elements, the DOM elements that match selector. For each of them, we apply them to fun, with each of them as the first argument and further arguments also passed. We collect the results of these function applications into an array and set it to elements.

This means that if fun is present, elements will contain the results of passing each of elements to fun - whereas if fun is absent, elements will contain the DOM elements themselves.

         elements = dale.go (elements, function (v) {
            return fun.apply (undefined, [v].concat (args));
         });
      }

If the selector a DOM node, or if it is the string 'body', or if it is of the form #ID or TAGNAME#ID, we return the first (and only) element of elements.

      if (selectorIsNode || selector === 'body' || (type (selector) === 'string' && selector.match (/^[a-z0-9]*#[^\s\[>,:]+$/))) return elements [0];

Otherwise, we return the entire array of elements. There's nothing else to do, so we close the function.

      return elements;
   }

c.nodeListToArray is a helper function that converts a NodeList into a plain array containing DOM elements. Whenever the browser returns a NodeList, we convert it into a plain array - this simplifies iteration, especially in old browsers. This function takes a single nodeList as its argument; since we always pass it a valid NodeList, we don't validate its input.

   c.nodeListToArray = function (nodeList) {

We define an output array.

      var output = [];

We iterate nodeList with a plain old for loop. We cannot use dale.go here since Safari 5.1 and below consider nodeList to be of type function.

      for (var i = 0; i < nodeList.length; i++) {

We push each of the elements to output.

         output.push (nodeList [i]);
      }

We return output and close the function.

      return output;
   }

We define c.setop, a helper function that performs set operations (and, or and not). This function takes an operation (and|or|not) and two arrays of DOM elements. Each of these sets are compared according to the given operation.

   c.setop = function (operation, set1, set2) {

If the operation is and, we go through the first set and create a new array filtering out those elements from the first set that are not present on the second set. We return this filtered first set, which represents an intersection of both sets.

      if (operation === 'and') return dale.fil (set1, undefined, function (v) {
         if (inc (set2, v)) return v;
      });

We copy the first set onto a new array output, since we don't want to modify it.

      var output = set1.slice ();

If the operation is or, we iterate the elements of the second set. Each of the elements of the second set that are not on the first set will be pushed onto output. or represents an union of both sets.

      if (operation === 'or') {
         dale.go (set2, function (v) {
            if (! inc (output, v)) output.push (v);
         });
      }

If we're here, the operation is not. In this case, we want to substract the second set from the first.

      else {

If the first set is empty, we put in output all the elements from the document.

         if (output.length === 0) output = c.nodeListToArray (document.getElementsByTagName ('*'));

We remove from output all the elements that are present in the second set.

         dale.go (set2, function (v) {
            var index = output.indexOf (v);
            if (index > -1) output.splice (index, 1);
         });
      }

We return output and close the function.

      return output;
   }

c.find is a function that resolves a cocholate selector into a set of DOM elements. It is used by the main (c) function to find elements. It takes a single argument, selector.

   c.find = function (selector) {

We get the type of selector.

      var selectorType = type (selector);

selector must be either an array, a string or an object.

      if (! c.prod && teishi.stop ('cocholate', [
         ['selector', selector, ['array', 'string', 'object'], 'oneOf'],

If selector is an array, its first element must be a string with a colon plus one of the operations and, or and not.

         function () {return [
            [selectorType === 'array',  ['first element of array selector', selector [0], [':and', ':or', ':not'], 'oneOf', teishi.test.equal]],

If selector is an object, its keys must be selector and from. selector.selector must be an array or string.

            [selectorType === 'object', [
               ['selector keys', dale.keys (selector), ['selector', 'from'], 'eachOf', teishi.test.equal],
               ['selector.selector', selector.selector, ['array', 'string'], 'oneOf'],

Now we validate selector.from. We expect it to be a DOM element. If the browser supports querySelectorAll, we check that the DOM element is valid by testing whether the querySelectorAll element exists for the given selector.from.

An implementation note: we write this last validation rule as a function and not an array because Internet Explorer 8 and below throw a strange error when placing DOM elements within a teishi array rule.

               function () {
                  if (type (selector.from) !== 'object' || (document.querySelectorAll && ! selector.from.querySelectorAll)) return clog ('c.find', 'selector.from passed to cocholate must be a DOM element.');
                  return true;
               }

If any of the validations fail, we print an error and return false.

            ]]
         ]}
      ], undefined, true)) return false;

First we'll cover the cases where selector is either a string or an object.

      if (selectorType !== 'array') {

If the browser supports querySelectorAll (which should happen for any of the browsers we support except Firefox 3 and below and Internet Explorer 7 and below) and selector is a string, we merely invoke document.querySelectorAll on it, convert the NodeList into an array, and return it.

      if (document.querySelectorAll && selectorType === 'string') return c.nodeListToArray (document.querySelectorAll (selector));

If selector is an object, we invoke querySelectorAll on selector.from (which is a DOM element) and we use selector.selector as the selector. We also convert the NodeList into an array and return it.

         if (document.querySelectorAll && selectorType === 'object') return c.nodeListToArray (selector.from.querySelectorAll (selector.selector));

If we're here, we're still dealing with a string or object selector, but on either Firefox 3 and below or Internet Explorer 7 and below. This is where it gets fun. In this section, we'll write code to provide limited selector support to these old browsers.

We define a variable from that will be the context for selecting DOM elements. It will be selector.from (if defined) or document (if selector is a string).

         var from = selector.from ? selector.from : document;

If selector is an object, we reassign it to selector.selector.

         selector = selectorType === 'string' ? selector : selector.selector;

We are going to provide limited support for selectors; namely, we will only support selectors of these shapes: TAG, TAG#ID, TAG.CLASS, #ID, .CLASS. Note that we also support *, since it's possible to pass a wildcard to document.getElementsByTagName (which means that all elements will be selected).

If selector doesn't conform to any of these shapes, we will print an error and return false. We make sure to forbid the characters ,, >, [ and ] since those have special meaning on modern DOM selectors.

         if (selector !== '*' && ! selector.match (/^[a-z0-9]*(#|\.)?[^,>\[\]]+$/i)) return clog ('The selector ' + selector + ' is not supported in IE <= 7 or Firefox <= 3.');

If we're here, selector is supported. We will now determine what's the criterium for selecting elements; if there's a # in the selector, it will be by id; if there's a ., it will be by class. If there's neither, we'll set it to undefined (in which case it means that we will select elements by tag).

         var criterium = selector.match ('#') ? 'id' : (selector.match (/\./) ? 'class' : undefined);

We split selector by either # or ..

         selector = selector.split (/#|\./);

We define tag, a variable that will indicate whether we filter our elements to belong to a certain tag name. If selector was split in two (because there's either a hashtag or a dot), we'll set tag to its first element; if it was not split in two (which means absence of both hashtag and dot), we'll also set tag to its first element. If selector has length 1 and there's a class or hashtag present, then tag will be undefined (because selector will only contain class or id information).

Note that, if present, we convert tag to uppercase since browsers expect it to be uppercase.

         var tag = (selector.length === 2 || ! criterium) ? selector [0].toUpperCase () : undefined;

We invoke getElementsByTagName on from; if a specific tag is required, we pass it as an argument to this function; otherwise, we pass a wildcard to get all the elements. Now, if selector.from was passed, we want only the child elements of from to be selected; if selector.from is absent, then we'll select elements from all the elements in the document.

The invocation to getElementsByTagName returns a NodeList. We convert it to an array of DOM elements with c.nodeListToArray. Once we have this array of elements, we iterate them, filtering out those for which the iterating function returns undefined. In other words, the function we're about to define (which takes one node at a time), will determine whether the iterated element is selected.

         return dale.fil (c.nodeListToArray (from.getElementsByTagName (tag || '*')), undefined, function (node) {

If we're selecting elements by class and this element's class doesn't match it, we ignore the element. Note we split node.className (if it exists) by whitespace into an array of classes, and make sure that the class we're looking for is one of the elements of that array.

            if (criterium === 'class' && ! inc ((node.className || '').split (/\s/), teishi.last (selector))) return;

If we're selecting an element by id and the element's id doesn't match the id we're looking for, we ignore the element.

            if (criterium === 'id'    && node.id !== teishi.last (selector)) return;

If we're here, we will return the element since it matches the required criteria.

            return node;

We close the iteration function and also this block, since there's nothing left to do.

         });
      }

If we're here, selector is an array. This means that selector contains logical operations (and/or/not). We will start by extracting operation, which is the logical operation that is the first element of the selector array. We'll also define output, an array where we'll collect the matching elements.

      var operation = selector.shift (), output = [];

We iterate selector, which contains a number of selectors that could be themselves arrays, objects or strings. We will stop whenever any of these iterations returns false.

The approach we take is to iterate the selectors and apply logical operations onto the output set incrementally as we iterate through the selectors.

      dale.stop (selector, false, function (v, k) {

We do a recursive call to c.find passing it the selector we're iterating. We collect the result of the recursive call in a local variable elements.

         var elements = c.find (v);

If the recursive call to c.find is false, it means the selector is invalid. We set output to false and return false, which will stop the iteration.

         if (elements === false) return output = false;

If we're on the first selector and the operation is either and or or, we set output to elements. This initializes output, since before this operation it was empty.

         if (k === 0 && operation !== ':not') output = elements;

If we're not on the first selector, or we're using the not criterium, we apply the logical operation (through c.setop) with output (the already selected elements) and elements (the new elements to be taken into consideration).

         else                                 output = c.setop (operation.replace (':', ''), output, elements);

We finish iterating the elements.

      });

If any of the selectors was false, output will also be false. If all the selectors were valid, output will contain the selected elements. We return it and close the function.

      return output;
   }

DOM functions

We now start with the first of our DOM functions. All the DOM functions internally use the c function and the other helper functions we have seen. Let's start with c.empty, which deletes all the child elements within the selected elements. This function takes a single argument, a selector.

   c.empty = function (selector) {

We call c with selector and a function as arguments. This function will be executed for each of the elements that match selector.

      c (selector, function (element) {

We merely set the element's innerHTML to an empty string.

         element.innerHTML = '';

There's nothing else to do, so we close the iteration function, the invocation to c and the function. Note that we don't return any values.

         });
      });
   }

We now define c.fill, which takes selector and html as arguments.

   c.fill = function (selector, html) {

If html is not a string, the function will print an error message and return false.

      if (! c.prod && teishi.stop ('c.fill', ['html', html, 'string'], undefined, true)) return false;

We iterate the elements matched by selector (through a call to c) and set their innerHTML property to html.

      c (selector, function (element) {
         element.innerHTML = html;

There's nothing else to do, so we close the function. Note that we don't return any values.

      });
   }

We now define c.place, a function that takes three arguments: selector, where and html.

   c.place = function (selector, where, html) {

We make sure that where is one of four strings: beforeBegin|afterBegin|beforeEnd|afterEnd, and that html is a string. If either of these conditions is not fulfilled, we print an error message and return false.

      if (! c.prod && teishi.stop ('c.place', [
         ['where', where, ['beforeBegin', 'afterBegin', 'beforeEnd', 'afterEnd'], 'oneOf', teishi.test.equal],
         ['html', html, 'string']
      ], undefined, true)) return false;

For each of the elements matching selector, we apply insertAdjacentHTML with where and html as its arguments.

      c (selector, function (element) {
         element.insertAdjacentHTML (where, html);

There's nothing else to do, so we close the function. Note that we don't return any values.

      });
   }

We now define c.get, which takes three arguments: selector, attributes and css. The last argument is a flag (presumably boolean), that will determine whether we're referring to CSS attributes or not.

   c.get  = function (selector, attributes, css) {

If attributes is not string, undefined, nor an array, we print an error and return false.

      if (! c.prod && teishi.stop ('c.get', ['attributes', attributes, ['string', 'array', 'undefined'], 'oneOf'], undefined, true)) return false;

We define an array ignoredValues with attribute values which we will ignore. This is only necessary for iterating style attributes in Internet Explorer 8 and element attributes in Internet Explorer 7 and below.

      var ignoredValues = [null, '', false, 0, "false"];

We iterate the elements matched by selector and apply the following function to each of them. Note that we will return an array containing the output of this function for each of the elements.

      return c (selector, function (element) {

If attributes is not undefined, we iterate it - if attributes is a string, this will be equivalent to having a single attribute. We will create an object with the attributes and return it.

         if (attributes !== undefined) return dale.obj (attributes, function (v) {

If the css flag is enabled, we'll access element.style [v] (which contains the CSS attribute). If the attribute is not present, we will consider it to be null.

            if (css) return [v, element.style [v] || null];

If the css flag is disabled, we will instead return the element's attribute (accessed through getAttribute).

            else     return [v, element.getAttribute (v)];
         });

If we're here, attributes is undefined, which means we want all the element's attributes. If css is falsy, we want the actual attributes (as opposed to the style attributes) of the element. We iterate element.attributes. Note that we start with a base object with the element's class or the element className (if class is absent) - this is only for the benefit of Internet Explorer 7 and below.

         if (! css) return dale.obj (element.attributes, (element ['class'] || element.className) ? {'class': element ['class'] || element.className} : {}, function (v, k) {

If the attribute is truthy, if its nodeName is truthy, and its nodeValue is not one of the values we are ignoring, we return them both. Checking whether the attribute is truthy is only necessary in Internet Explorer 7 and below; many browsers, however, require us to check whether nodeName is truthy, otherwise undefined attributes will be returned.

            if (v && v.nodeName && ! inc (ignoredValues, v.nodeValue)) return [v.nodeName, v.nodeValue];
         });

If we're here, we want all inline CSS attributes for the element. In all supported browsers except for Internet Explorer 8, element.style has a length property that we will use to iterate the style object. In Internet Explorer 8 and below, however, we're forced to iterate all the keys of the object.

         return dale.obj (element.style.length ? dale.times (element.style.length, 0) : dale.keys (element.style), function (k) {

If element.style.length is supported, we simply return the corresponding key and value of the style object.

            if (element.style.length) return [element.style [k], element.style [element.style [k]]];

For Internet Explorer 8 and below, we return the key and value but only if the value is not one of the ignoredValues.

            if (! inc (ignoredValues, element.style [k])) return [k, element.style [k]];
         });

There's nothing else to do, so we close the iterating function, the invocation to c and the function itself.

      });
   }

We now define c.set. It is similar to c.get, but instead of returning attributes, it sets them. It takes four arguments, selector, attributes, css and notrigger - the last two are flags.

   c.set  = function (selector, attributes, css, notrigger) {

We now validate the input. attributes must be an object.

      if (! c.prod && teishi.stop ('c.set', [
         ['attributes', attributes, 'object'],

Every attribute key must start with a ASCII letter, underscore or colon, and must follow with zero or more of the following:

  • A letter.
  • An underscore.
  • A colon.
  • A digit.
  • A period.
  • A dash.
  • Any Unicode character with a code point of 129 (0080 in hexadecimal) or above - these include all extended ASCII characters (the top half of the set) and every non-ASCII character.

This arcana was kindly provided by this article. The regex below was taken from the article and modified to add the permitted Unicode characters.

         [
            ['attribute keys', 'start with an ASCII letter, underscore or colon, and be followed by letters, digits, underscores, colons, periods, dashes, extended ASCII characters, or any non-ASCII characters.'],
            dale.keys (attributes),
            /^[a-zA-Z_:][a-zA-Z_:0-9.\-\u0080-\uffff]*$/,
            'each', teishi.test.match
         ],

The attribute values must be either integers, floats, strings or null.

         ['attribute values', attributes, ['integer', 'float', 'string', 'null'], 'eachOf']

If any of these conditions is not met, an error will be printed and the function will return false.

      ], undefined, true)) return false;

For each of the elements that are matched by selector, we will invoke the following function.

      c (selector, function (element) {

For each of the elements, we iterate attributes.

         dale.go (attributes, function (v, k) {

If we're setting a css attribute, we set the attribute within element.style. Note that if its desired value is null, we set the attribute to an empty string.

            if       (css)        element.style [k] = v === null ? '' : v;

If we're instead setting an HTML attribute, we use either removeAttribute or setAttribute, depending on whether the desired value is null or not.

            else if  (v === null) element.removeAttribute (k);
            else                  element.setAttribute    (k, v);
         });

If the notrigger flag is absent, we fire a change event on the element through c.fire, which is defined later. This means that by default c.set will trigger a change event for those elements that it matches.

         if (! notrigger) c.fire (element, 'change');

There's nothing else to do, so we close the function. Note that we don't return any values.

      });
   }

We now define c.fire, the last DOM function. For each matched element, this function creates an event and dispatches it to the element. This function takes two arguments: selector and eventType.

   c.fire = function (selector, eventType) {

If eventType is not a string, we print an error and return false.

      if (! c.prod && teishi.stop ('c.fire', ['event type', eventType, 'string'], undefined, true)) return false;

For each of the elements that are matched by selector, we will invoke the following function.

      c (selector, function (element) {

We define a local variable ev to hold the event we are about to create.

         var ev;

We first try to create the event using the Event constructor, which should work for most browsers. Note we pass eventType to the constructor so the event is of the desired type.

         try {
            ev = new Event (eventType);
         }

If the event constructor is not supported, we use either the createEvent method or the createEventObject method. The latter method is only for Internet 8 and below.

         catch (error) {
            ev = document.createEvent ? document.createEvent ('Event') : document.createEventObject ();

In all browsers that don't support the constructor and aren't Internet Explorer, we invoke the initEvent method and pass to it eventType. We pass extra false arguments since they're required in old versions of Firefox.

            if (document.createEvent) ev.initEvent (eventType, false, false);
         }

If the browser supports the dispatchEvent method, (which goes for all the browsers we support except for Internet Explorer 8 and below), we will invoke it, passing the event to it.

         if (element.dispatchEvent) return element.dispatchEvent (ev);

If the browser doesn't support fireEvent, there's no available method with which to fire the event. In this case, we print an error and return false.

         if (! element.fireEvent) return clog ('c.fire error', 'Unfortunately, this browser supports neither EventTarget.dispatchEvent nor element.fireEvent.');

For Internet Explorer 8 and below, we instead invoke fireEvent. Note that we pass both eventType and the event itself. Also notice we prepend 'on' to the eventType, so that (for example), click becomes onclick.

We wrap this statement into a try block because some the combination of some events and node elements throws an error in Internet Explorer 8 and below - for example, a change event on a <div>.

         try {
            element.fireEvent ('on' + eventType, ev);
         }

If fireEvent throws an error, we detect whether there's a handler for the event. If there is, we execute it. While we could pass ev as an argument to it, arguments seem to be ignored altogether and are not received by the event handlers.

         catch (error) {
            if (element ['on' + eventType]) element ['on' + eventType] ();
         }

There's nothing else to do, so we close the function. Note that we don't return any values.

      });
   }

Non-DOM functions

Our first non-DOM function is c.ready, which takes a single function that will be run when the the HTML page, all scripts and all stylesheets have been loaded.

   c.ready = function (fun) {

If document.addEventListener is present (which is the case on most browsers), we'll attach fun to it when the load event is triggered. We pass a false third argument (useCapture) because Firefox 5 and Opera 11.5 and below require it.

      if (window.addEventListener) return window.addEventListener ('load', fun, false);

If we're on Internet Explorer 8 and below, we instead use window.attachEvent and bind it to the onload event.

      if (window.attachEvent)      return window.attachEvent      ('onload', fun);

If we're on very old browsers (Safari 6 and below, old versions of Chrome & Firefox for Android), neither of these two methods will be present. Instead, we we will run a function every 10 milliseconds until the document is ready.

Once the document is complete, fun will be run and the function running every 10 milliseconds will stop being called.

      var interval = setInterval (function () {
         if (document.readyState === 'complete') fun () || clearInterval (interval);
      }, 10);

We close the function.

   }

We define c.cookie, a function to read and delete cookies. It takes an optional cookie argument.

   c.cookie = function (cookie) {

If cookie is false, we will delete all cookies from the client. Note that this only will happen for those cookies within the domain on which the page is being served, since a given domain can only access its own cookies.

      if (cookie === false) {

We take document.cookie, a string which contains all the stored cookies we can access from the current domain. We split it by semicolons, ignoring any whitespace after the semicolons.

         return dale.go (document.cookie.split (/;\s*/), function (v) {

For each of the cookies, we take the name of the cookie (the text before the = sign), overwrite its value with an empty string and then set its expires property to the present moment. This will make the browser delete the cookie immediately. The approach was taken from here.

            document.cookie = v.replace (/^ +/, '').replace (/=.*/, '=;expires=' + new Date ().toUTCString ())

We return the deleted cookie.

            return v;

We close the loop and the block; all the deleted cookies will be returned inside an array.

         });
      }

If we're here, we're reading cookies instad of deleting them. If cookie is absent, we will read document.cookie instead. We split it by semicolons, ignoring any whitespace after the semicolons.

After iterating each cookie we will return an object where each key is the cookie name and each value is the cookie value.

      return dale.obj ((cookie || document.cookie).split (/;\s*/), function (v) {

If the cookie is empty, we ignore it.

         if (v === '') return;

We split the cookie by the = sign.

         v = v.split ('=');

We extract the name and the value into variables.

         var name = v [0];
         var value = v.slice (1).join ('=');

We return name as the key and value as the value for the cookie. There's nothing else to do, so we close the iteration and the function.

         return [name, value];
      });
   }

We define now c.ajax, a function for performing asynchronous calls, therefore rending web applications (as opposed to mere web pages) truly possible.

This function takes five arguments: method, path, headers, body and callback.

   c.ajax = function (method, path, headers, body, callback) {

If method is not present, we will set it to GET.

      method   = method   || 'GET';

If headers is not present, we will set it to an empty string.

      headers  = headers  || {};

If body is not present, we will set it to an empty string.

      body     = body     || '';

If callback is not present, we will set it to an empty function.

      callback = callback || function () {};

Notice we don't set path to anything if it's absent, since no sensible default can be assumed.

We make sure that method and path are strings, that headers is an object and that callback is a function. If any of these conditions is not met, we print an error and return false.

      if (! c.prod && teishi.stop ('c.ajax', [
         ['method',   method,   'string'],
         ['path',     path,     'string'],
         ['headers',  headers,  'object'],
         ['callback', callback, 'function']
      ], undefined, true)) return false;

We initialize the XMLHttpRequest object, which will be present in most browsers. In Internet Explorer 5 and 6, XMLHttpRequest is absent, but we can use ActiveXObject instead.

      var r = window.XMLHttpRequest ? new XMLHttpRequest () : new ActiveXObject ('Microsoft.XMLHTTP');

We initialize the request, passing it the uppercased method, path and a truthy third argument to indicate that we want the request to be asynchronous.

      r.open (method.toUpperCase (), path, true);

If body is not a FormData object and it is instead a plain array or an object, we do two things:

      if (teishi.complex (body) && type (body, true) !== 'formdata') {
  1. Set the content-type header to application/json, unless it's already present in headers.
         headers ['content-type'] = headers ['content-type'] || 'application/json';
  1. We set body to its stringified value.
         body = teishi.str (body);
      }

We set all the headers through setRequestHeader.

      dale.go (headers, function (v, k) {
         r.setRequestHeader (k, v);
      });

We set an event handler that will be fired when the request goes from one phase to the next.

      r.onreadystatechange = function () {

If readyState does not equal to 4, the request is not yet complete, so we don't do anything.

         if (r.readyState !== 4) return;

If the response status is neither 200 nor 304, we consider the request to have failed. In this case, we invoke callback with the request as its first argument, to indicate that an error has happened.

         if (r.status !== 200 && r.status !== 304) return callback (r);

We define a variable json that we will use to detect whether the response was of type json.

         var json;

We define a variable res that will hold the response object. We set its xhr key to the actual response object.

         var res = {
            xhr: r,

We take the response headers (which are a string), split them by newlines and build a headers object by iterating them. In Firefox 18 and below, the response headers don't contain carriage returns (\r), so we make them optional in the regex we pass to split. For each header:

            headers: dale.obj (r.getAllResponseHeaders ().split (/\r?\n/), function (header) {

If header is an empty string, we ignore it.

               if (header === '') return;

We create two variables: name, to hold the name of the header; and value, to hold the contents of the header after the name, a colon and one or more whitespace. The name will be all the text before the first colon.

               var name = header.match (/^[^:]+/) [0], value = header.replace (name, '').replace (/:\s+/, '');

If the content-type header matches the application/json MIME type, we set json to true.

               if (name.match (/^content-type/i) && value.match (/application\/json/i)) json = true;

We return name and value to place them within headers. This concludes the iteration. We also close headers.

               return [name, value];
            })
         };

We set res.body to the responseText property of the request; if the response was JSON, we parse the responseText.

         res.body = json ? teishi.parse (r.responseText) : r.responseText;

We invoke callback with a null first argument and res as its second. This concludes the handler.

         callback (null, res);
      }

We submit the request.

      r.send (body);

We synchronously return an object with headers (the request headers), body (the body sent with the request) and xhr (the request object itself). This concludes the function.

      return {headers: headers, body: body, xhr: r};
   }

We define c.loadScript, a function that loads an external script. It takes two arguments, src (the path to the script) and callback (the function that is executed after the operation is complete).

   c.loadScript = function (src, callback) {

If callback is falsy, we set it to an empty function.

      callback = callback || function () {};

We perform a GET request through c.ajax, passing src as the path. Note we return the result of the invocation to c.ajax, so that the request object is available outside of the function invocation.

      return c.ajax ('get', src, {}, '', function (error, data) {

If there was an error, we pass it to callback.

         if (error) return callback (error);

If we're here, the request was successful. We create a script element.

         var script = document.createElement ('script');

We set the text of script to the body of the response. We do this within a try/catch block since Internet Explorer 8 and below don't support the first method. The code was adapted from this snippet.

         try {
            script.appendChild (document.createTextNode (data.body));
         }
         catch (error) {
            script.text = data.body;
         }

We append script to the body.

         document.body.appendChild (script);

We invoke callback with null and data as its arguments, to indicate success. There's nothing else to do, so we close the ajax request and the function.

         callback (null, data);
      });
   }

We define c.test, a function to execute a test suite on the browser. This function takes two argument: tests and callback.

   c.test = function (tests, callback) {

tests must be an array and each of its elements must also be an array.

      if (! c.prod && teishi.stop ('c.test', [
         ['tests', tests, 'array'],
         ['tests', tests, 'array', 'each'],

We iterate each of the tests. If a test is an empty array, we don't apply any further validation rules on it, since it represents a no-op. Otherwise, we proceed with its validation.

         dale.go (tests, function (test, k) {return test.length === 0 ? [] : [

Each test must have a length of either two or three.

            ['test length', test.length, {min: 2, max: 3}, teishi.test.range],

The first element of each test must be a string, which is the tag.

            ['test #' + (k + 1) + ' tag', test [0], 'string'],

If the test has length 2, we expect its second element to be a function (the check function). If it has length 3, we expect its second element (the action function) and its third element (the check function) to be functions.

            test.length === 2 ? ['test #' + (k + 1) + ' check', test [1], 'function'] : [
               ['test #' + (k + 1) + ' action', test [1], 'function'],
               ['test #' + (k + 1) + ' check',  test [2], 'function']
            ]
         ]}),

callback must be either undefined or a function.

         ['callback', callback, ['function', 'undefined'], 'oneOf']

If any of these checks fails, an error will be printed and c.test will return false.

      ], undefined, true)) return false;

If callback is not defined, we initialize it to a function that will either throw an error (if one is received as its first argument) or that prints a success message when the test suite finishes its execution.

      callback = callback || function (error, time) {
         if (error) throw new Error ('c.test: Test failed: ' + error.test + '; result: ' + error.result);
         clog ('c.test', 'All tests finished successfully (' + (teishi.time () - start) + ' ms)');
      }

We define two variables: start, to mark the beginning time of the test suite; and runNext, a function that will run one test at a time. runNext takes an index k as its sole argument. We now proceed to define this function, which is the engine of c.test.

      var start = teishi.time (), runNext = function (k) {

We select the k-th test based on the argument received by runNext and place it in a variable test.

         var test = tests [k];

If there're no tests left, we invoke callback with a null first argument (to signify the absence of an error) and the total execution time for the entire test suite as its second argument. Note that we place a return before this invocation, to do no further actions if this is the case.

         if (! test)            return callback (null, teishi.time () - start);

If the current test has no elements, it is a no-op. We invoke runNext with k + 1 so that we can run the next test. Note that we place a return before this invocation, to do no further actions if this is the case.

         if (test.length === 0) return runNext (k + 1);

We define a function check, which will be a wrapper around the check function specified in the last argument of the current test.

This function will take two optional arguments: retry, which indicates whether the check should be repeated if it were to fail; and interval, the result of a setInterval call that will repeatedly invoke the check function, which should be cleared when necessary.

         var check = function (retry, interval) {

We execute the check function (which will be the second or third element, depending on how many elements are contained by test) and store its result in a variable result.

            var result = test [test.length - 1] ();

If interval was passed, this means that there's a setInterval function invoking the check function periodically. If this is the case, and either result is true (which means that the check was successful) or retry is not set (which means we should stop retrying the check), we clear the interval so it stops executing.

            if (interval && (result === true || ! retry)) clearInterval (interval);

If result is true, we invoke runNext with k + 1, to run the next test. Note we return on this line, to avoid performing any further actions.

            if (result === true) return runNext (k + 1);

If we're here, result is not true, which means that the check has failed. If the retry flag is not set, we invoke the callback with an error of the form {test: TAG, result: result}.

            if (! retry) callback ({test: test [0], result: result});

This concludes our wrapper around the check function provided in the test. Notice that if retry was set and the test fails, nothing will be done - in this case, there will be a setInterval function invoking this function again later.

         }

We now print a message containing the tag for the current test.

         clog ('c.test', 'Running test:', test [0]);

If there's no action function, we execute check directly and exit runNext.

         if (test.length === 2) return check ();

If there's an action function, we invoke it passing to it another function as its first argument. This function is the next function, which will be optionally invoked by action to continue the chain of tests in case it performs an asynchronous operation. This function can receive two arguments: wait, an integer telling us how many milliseconds to wait until performing the check (or a series of checks) - and ms, a positive integer telling us to repeat check every n milliseconds until either wait elapses or the check is successful.

         if (test [1] (function (wait, ms) {

If the first argument passed to next is undefined, next will invoke check and return. This is useful for immediately checking for a condition after an asynchronous action has been performed.

            if (wait === undefined) return check ();

If we are here, wait is not undefined. We validate that wait is an integer equal or larger than 0 and throw an error otherwise.

            if (type (wait) !== 'integer' || wait < 0) throw new Error ('c.test: `wait` parameter must be undefined, zero or a positive integer but instead is ' + wait);

If wait is set but ms is not, we invoke check through setTimeout, using wait as the second parameter to setTimeout. This will run the check function once after (approximately) wait milliseconds.

            if (ms === undefined)   return setTimeout (check, wait);

If we are here, ms is not undefined. We validate that wait is an larger than 0 and throw an error otherwise.

            if (type (ms) !== 'integer'   || ms < 1)   throw new Error ('c.test: `ms` parameter must be undefined or a positive integer but instead is ' + ms);

We define until, a timestamp that will indicate until when we should execute the check function should it keep failing. We also set interval to the output of a setInterval invocation.

            var until = teishi.time () + wait, interval = setInterval (function () {

Within the function we pass to setInterval, we will invoke check with two parameters: retry, which will be true if the current time is less or equal than until; and interval, the interval function that should be cleared if either the check is successful or time runs out; you might recall we execute clearInterval on this argument on the check function we defined earlier.

               check (teishi.time () <= until, interval);

We execute the interval function every ms milliseconds.

            }, ms);

We close the invocation to action. If action returns anything except undefined, we invoke check directly. Otherwise, we'll let action invoke check on its own.

         }) !== undefined) check ();

We close runNext.

      }

We invoke runNext passing an index of 0 (to start at the first test). This concludes the function.

      runNext (0);
   }

License

cocholate is written by Federico Pereiro (fpereiro@gmail.com) and released into the public domain.

Versions

Current Tags

  • Version
    Downloads (Last 7 Days)
    • Tag
  • 4.0.0
    0
    • latest

Version History

Package Sidebar

Install

npm i cocholate

Weekly Downloads

0

Version

4.0.0

License

Public Domain

Unpacked Size

118 kB

Total Files

4

Last publish

Collaborators

  • fpereiro