(function($) {
    var PREFIX = 'data-',
        PATTERN = /^data-(.*)$/;

    function dataset(name, value) {
        if (value !== undefined) {
            // dataset(name, value): set the NAME attribute to VALUE.
            return this.attr(PREFIX + name, value);
        }

        switch (typeof name) {
        case 'string':
            // dataset(name): get the value of the NAME attribute.
            return this.attr(PREFIX + name);
        case 'object':
            // dataset(items): set the values of all (name, value) items.
            return set_items.call(this, name);
        case 'undefined':
            // dataset(): return a mapping of (name, value) items for the
            // first element.
            return get_items.call(this);
        default:
            throw 'dataset: invalid argument ' + name;
        }
    }

    function get_items() {
        return this.foldAttr(function(index, attr, result) {
            var match = PATTERN.exec(this.name);
            if (match) result[match[1]] = this.value;
        });
    }

    function set_items(items) {
        for (var key in items) {
            this.attr(PREFIX + key, items[key]);
        }
        return this;
    }

    function remove(name) {
        if (typeof name == 'string') {
            // Remove a single attribute;
            return this.removeAttr(PREFIX + name);
        }
        return remove_names(name);
    }

    function remove_names(obj) {
        var idx,
            length = obj && obj.length;

        // For any object, remove attributes named by the keys.
        if (length === undefined) {
            for (idx in obj) {
                this.removeAttr(PREFIX + idx);
            }
        }
        // For an array, remove attributes named by the values.
        else {
            for (idx = 0; idx < length; idx++) {
                this.removeAttr(PREFIX + obj[idx]);
            }
        }

        return this;
    }

    $.fn.dataset = dataset;
    $.fn.removeDataset = remove_names;
})(jQuery);

(function($) {
    function each_attr(proc) {
        if (this.length > 0) {
            $.each(this[0].attributes, proc);
        }
        return this;
    }

    function fold_attr(proc, acc) {
        return fold((this.length > 0) && this[0].attributes, proc, acc);
    }

    /*
      * A left-fold operator. The behavior is the same as $.each(),
      * but the callback is called with the accumulator as the third
      * argument.  The default accumulator is an empty object.
      */
    function fold(object, proc, acc) {
        var length = object && object.length;

        // The default accumulator is an empty object.
        if (acc === undefined) acc = {};

        // Returning an empty accumulator when OBJECT is "false"
        // makes FOLD more composable.
        if (!object) return acc;

        // Check to see if OBJECT is an array.
        if (length !== undefined) {
            for (var i = 0, value = object[i];
            (i < length) && (proc.call(value, i, value, acc) !== false);
            value = object[++i])
            {}
        }
        // Object is a map of (name, value) items.
        else {
            for (var name in object) {
                if (proc.call(object[name], name, object[name], acc) === false) break;
            }
        }

        return acc;
    }

    function fold_jquery(proc, acc) {
        if (acc === undefined) acc = [];
        return fold(this, proc, acc);
    }

    $.fn.eachAttr = each_attr;
    $.fn.foldAttr = fold_attr;
    $.fn.fold = fold_jquery;
    $.fold = fold;
})(jQuery);
