diff --git a/Rakefile b/Rakefile index 19b08d8..c522986 100755 --- a/Rakefile +++ b/Rakefile @@ -10,29 +10,41 @@ PROTOTYPE_TEST_UNIT_DIR = File.join(PROTOTYPE_TEST_DIR, 'unit') PROTOTYPE_TMP_DIR = File.join(PROTOTYPE_TEST_UNIT_DIR, 'tmp') PROTOTYPE_VERSION = '1.6.0.3' +$:.unshift File.join(PROTOTYPE_ROOT, 'lib') + task :default => [:dist, :dist_helper, :package, :clean_package_source] desc "Builds the distribution." task :dist do - $:.unshift File.join(PROTOTYPE_ROOT, 'lib') require 'protodoc' - - Dir.chdir(PROTOTYPE_SRC_DIR) do - File.open(File.join(PROTOTYPE_DIST_DIR, 'prototype.js'), 'w+') do |dist| - dist << Protodoc::Preprocessor.new('prototype.js') - end + + File.open(File.join(PROTOTYPE_DIST_DIR, 'prototype.js'), 'w+') do |dist| + source = File.join(PROTOTYPE_SRC_DIR, 'prototype.js') + dist << Protodoc::Preprocessor.new(source, :strip_documentation => true) + end +end + +desc "Builds the documentation." +task :doc do + require 'protodoc' + require 'pdoc' + + Tempfile.open("prototype-doc") do |temp| + source = File.join(PROTOTYPE_SRC_DIR, 'prototype.js') + temp << Protodoc::Preprocessor.new(source, :strip_documentation => false) + temp.flush + rm_rf PROTOTYPE_DOC_DIR + PDoc::Runner.new(temp.path, :output => PROTOTYPE_DOC_DIR).run end end desc "Builds the updating helper." task :dist_helper do - $:.unshift File.join(PROTOTYPE_ROOT, 'lib') require 'protodoc' - - Dir.chdir(File.join(PROTOTYPE_ROOT, 'ext', 'update_helper')) do - File.open(File.join(PROTOTYPE_DIST_DIR, 'prototype_update_helper.js'), 'w+') do |dist| - dist << Protodoc::Preprocessor.new('prototype_update_helper.js') - end + + File.open(File.join(PROTOTYPE_DIST_DIR, 'prototype_update_helper.js'), 'w+') do |dist| + source = File.join(PROTOTYPE_ROOT, 'ext', 'update_helper', 'prototype_update_helper.js') + dist << Protodoc::Preprocessor.new(source) end end diff --git a/lib/protodoc.rb b/lib/protodoc.rb index 8695707..37e3661 100644 --- a/lib/protodoc.rb +++ b/lib/protodoc.rb @@ -8,27 +8,43 @@ class String def strip_whitespace_at_line_ends lines.map {|line| line.gsub(/\s+$/, '')} * $/ end + + def strip_pdoc_comments + gsub %r{\s*/\*\*.*?\*\*/}m, "\n" + end end module Protodoc module Environment def include(*filenames) - filenames.map {|filename| Preprocessor.new(filename).to_s}.join("\n") + filenames.map do |filename| + Preprocessor.new(expand_path(filename), @options).result + end.join("\n") end end class Preprocessor include Environment - def initialize(filename) + def initialize(filename, options = { }) filename = File.join(filename.split('/')) @filename = File.expand_path(filename) @template = ERB.new(IO.read(@filename), nil, '%') + @options = options end - def to_s - @template.result(binding).strip_whitespace_at_line_ends + def expand_path(filename) + File.join(File.dirname(@filename), filename) end + + def result + result = @template.result(binding) + result = result.strip_whitespace_at_line_ends + result = result.strip_pdoc_comments if @options[:strip_documentation] + result + end + + alias_method :to_s, :result end end diff --git a/src/lang/array.js b/src/lang/array.js index 9597ac1..5144aa1 100644 --- a/src/lang/array.js +++ b/src/lang/array.js @@ -1,3 +1,11 @@ +/** section: lang, alias of: Array.from + * $A(iterable) -> Array + * + * Accepts an array-like collection (anything with numeric indices) and returns + * its equivalent as an actual Array object. + * This method is a convenience alias of [[Array.from]], but is the preferred way + * of casting to an Array. + **/ function $A(iterable) { if (!iterable) return []; if (iterable.toArray) return iterable.toArray(); @@ -21,6 +29,14 @@ if (Prototype.Browser.WebKit) { }; } +/** section: lang + * $w(string) -> Array + * - string (String): A string with zero or more spaces. + * + * Splits a string into an array, treating all whitespace as delimiters. + * + * Equivalent to Ruby's `%w{foo bar}` or Perl's `qw(foo bar)`. +**/ function $w(string) { if (!Object.isString(string)) return []; string = string.strip(); @@ -29,6 +45,9 @@ function $w(string) { Array.from = $A; +/** section: lang + * Array +**/ (function() { var arrayProto = Array.prototype, slice = arrayProto.slice, @@ -40,25 +59,49 @@ Array.from = $A; } if (!_each) _each = each; + /** + * Array#clear() -> Array + * Empties an array. + **/ function clear() { this.length = 0; return this; } + /** + * Array#first() -> ? + * Returns the array's first item. + **/ function first() { return this[0]; } + /** + * Array#last() -> ? + * Returns the array's last item. + **/ function last() { return this[this.length - 1]; } + /** + * Array#compact() -> Array + * Trims the array of `null`, `undefined`, or other "falsy" values. + **/ function compact() { return this.select(function(value) { return value != null; }); } + /** + * Array#flatten() -> Array + * Returns a “flat” (one-dimensional) version of the array. + * + * Nested arrays are recursively injected “inline”. This can prove very + * useful when handling the results of a recursive collection algorithm, + * for instance. + **/ function flatten() { return this.inject([], function(array, value) { if (Object.isArray(value)) @@ -68,6 +111,13 @@ Array.from = $A; }); } + /** + * Array#without(value...) -> Array + * - value (?): A value to exclude. + * + * Produces a new version of the array that does not contain any of the + * specified values. + **/ function without() { var values = slice.call(arguments, 0); return this.select(function(value) { @@ -75,14 +125,34 @@ Array.from = $A; }); } + /** + * Array#reverse([inline = false]) -> Array + * - inline (Boolean): Whether to modify the array in place. If `false`, + * clones the original array first. + * + * Returns the reversed version of the array. + **/ function reverse(inline) { return (inline !== false ? this : this.toArray())._reverse(); } + /** + * Array#reduce() -> Array + * Reduces arrays: one-element arrays are turned into their unique item, + * while multiple-element arrays are returned untouched. + **/ function reduce() { return this.length > 1 ? this : this[0]; } + /** + * Array#uniq([sorted = false]) -> Array + * - sorted (Boolean): Whether the array has already been sorted. If `true`, + * a less-costly algorithm will be used. + * + * Produces a duplicate-free version of an array. If no duplicates are + * found, the original array is returned. + **/ function uniq(sorted) { return this.inject([], function(array, value, index) { if (0 == index || (sorted ? array.last() != value : !array.include(value))) @@ -91,24 +161,53 @@ Array.from = $A; }); } + /** + * Array#intersect(array) -> Array + * - array (Array): A collection of values. + * + * Returns an array containing every item that is shared between the two + * given arrays. + **/ function intersect(array) { return this.uniq().findAll(function(item) { return array.detect(function(value) { return item === value }); }); } + /** alias of: Array#toArray + * Array#clone() -> Array + * + * Returns a duplicate of the array, leaving the original array intact. + **/ function clone() { return slice.call(this, 0); } + /** related to: Enumerable#size + * Array#size() -> Number + * Returns the size of the array. + * + * This is just a local optimization of the mixed-in [[Enumerable#size]] + * which avoids array cloning and uses the array’s native length property. + **/ function size() { return this.length; } + /** related to: Object.inspect + * Array#inspect() -> String + * + * Returns the debug-oriented string representation of an array. + **/ function inspect() { return '[' + this.map(Object.inspect).join(', ') + ']'; } + /** related to: Object.toJSON + * Array#toJSON() -> String + * + * Returns a JSON string representation of the array. + **/ function toJSON() { var results = []; this.each(function(object) { @@ -118,6 +217,15 @@ Array.from = $A; return '[' + results.join(', ') + ']'; } + /** + * Array#indexOf(item[, offset = 0]) -> Number + * - item (?): A value that may or may not be in the array. + * - offset (Number): The number of initial items to skip before beginning the + * search. + * + * Returns the position of the first occurrence of `item` within the array — or + * `-1` if `item` doesn’t exist in the array. + **/ function indexOf(item, i) { i || (i = 0); var length = this.length; @@ -127,12 +235,26 @@ Array.from = $A; return -1; } + /** related to: Array#indexOf + * Array#lastIndexOf(item[, offset]) -> Number + * - item (?): A value that may or may not be in the array. + * - offset (Number): The number of items at the end to skip before beginning + * the search. + * + * Returns the position of the last occurrence of `item` within the array — or + * `-1` if `item` doesn’t exist in the array. + **/ function lastIndexOf(item, i) { i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; var n = this.slice(0, i).reverse().indexOf(item); return (n < 0) ? n : i - n - 1; } + /** + * Array#concat(args...) -> Array + * + * TODO: Array#concat + **/ function concat() { var array = slice.call(this, 0), item; for (var i = 0, length = arguments.length; i < length; i++) { diff --git a/src/lang/class.js b/src/lang/class.js index ab72b29..0e3bd4a 100644 --- a/src/lang/class.js +++ b/src/lang/class.js @@ -1,5 +1,35 @@ /* Based on Alex Arnell's inheritance implementation. */ + +/** section: lang + * Class +**/ var Class = (function() { + /** + * Class.create([superclass][, methods...]) -> Class + * - superclass (Class): The optional superclass to inherit methods from. + * - methods (Object): An object whose properties will be "mixed-in" to the + * new class. Any number of mixins can be added; later mixins take + * precedence. + * + * Creates a class. + * + * Class.create returns a function that, when called, will fire its own + * `initialize` method. + * + * `Class.create` accepts two kinds of arguments. If the first argument is + * a `Class`, it's treated as the new class's superclass, and all its + * methods are inherited. Otherwise, any arguments passed are treated as + * objects, and their methods are copied over as instance methods of the new + * class. Later arguments take precedence over earlier arguments. + * + * If a subclass overrides an instance method declared in a superclass, the + * subclass's method can still access the original method. To do so, declare + * the subclass's method as normal, but insert `$super` as the first + * argument. This makes `$super` available as a method for use within the + * function. + * + * To extend a class after it has been defined, use [[Class#addMethods]]. + **/ function create() { var parent = null, properties = $A(arguments); if (Object.isFunction(properties[0])) @@ -30,6 +60,23 @@ var Class = (function() { return klass; } + /** + * Class#addMethods(methods) -> Class + * - methods (Object): The methods to add to the class. + * + * Adds methods to an existing class. + * + * `Class#addMethods` is a method available on classes that have been + * defined with `Class.create`. It can be used to add new instance methods + * to that class, or overwrite existing methods, after the class has been + * defined. + * + * New methods propagate down the inheritance chain. If the class has + * subclasses, those subclasses will receive the new methods — even in the + * context of `$super` calls. The new methods also propagate to instances of + * the class and of all its subclasses, even those that have already been + * instantiated. + **/ function addMethods(source) { var ancestor = this.superclass && this.superclass.prototype; var properties = Object.keys(source); diff --git a/src/lang/date.js b/src/lang/date.js index 0e478c6..f0c67d1 100644 --- a/src/lang/date.js +++ b/src/lang/date.js @@ -1,3 +1,12 @@ +/** section: lang + * Date +**/ + +/** + * Date#toJSON -> String + * + * TODO: Date#toJSON +**/ Date.prototype.toJSON = function() { return '"' + this.getUTCFullYear() + '-' + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + diff --git a/src/lang/enumerable.js b/src/lang/enumerable.js index 138e897..3cb908c 100644 --- a/src/lang/enumerable.js +++ b/src/lang/enumerable.js @@ -1,6 +1,19 @@ +/** section: lang + * mixin Enumerable +**/ + var $break = { }; var Enumerable = (function() { + /** + * Enumerable#each(iterator[, context]) -> Enumerable + * - iterator (Function): A `Function` that expects an item in the + * collection as the first argument and a numerical index as the second. + * - context (Object): The scope in which to call `iterator`. Affects what + * the keyword `this` means inside `iterator`. + * + * Calls `iterator` for each item in the collection. + **/ function each(iterator, context) { var index = 0; try { @@ -13,6 +26,12 @@ var Enumerable = (function() { return this; } + /** + * Enumerable#eachSlice(number[, iterator = Prototype.K[, context]]) -> Enumerable + * + * TODO: Enumerable#eachSlice + * http://prototypejs.org/api/enumerable#method-eachslice + **/ function eachSlice(number, iterator, context) { var index = -number, slices = [], array = this.toArray(); if (number < 1) return array; @@ -21,6 +40,12 @@ var Enumerable = (function() { return slices.collect(iterator, context); } + /** + * Enumerable#all([iterator = Prototype.K[, context]]) -> Boolean + * + * Determines whether all the elements are boolean-equivalent to `true`, + * either directly or through computation by the provided iterator. + **/ function all(iterator, context) { iterator = iterator || Prototype.K; var result = true; @@ -31,6 +56,12 @@ var Enumerable = (function() { return result; } + /** + * Enumerable#any([iterator = Prototype.K[, context]]) -> Boolean + * + * Determines whether at least one element is boolean-equivalent to `true`, + * either directly or through computation by the provided iterator. + **/ function any(iterator, context) { iterator = iterator || Prototype.K; var result = false; @@ -41,6 +72,12 @@ var Enumerable = (function() { return result; } + /** alias of: Enumerable#map + * Enumerable#collect([iterator = Prototype.K[, context]]) -> Array + * + * Returns the results of applying the iterator to each element. + * Aliased as [[Enumerable#map]]. + **/ function collect(iterator, context) { iterator = iterator || Prototype.K; var results = []; @@ -50,6 +87,12 @@ var Enumerable = (function() { return results; } + /** alias of: Enumerable#find + * Enumerable#detect(iterator[, context]) -> firstElement | undefined + * + * Finds the first element for which the iterator returns a “truthy” value. + * Aliased by the [[Enumerable#find]] method. + **/ function detect(iterator, context) { var result; this.each(function(value, index) { @@ -61,6 +104,12 @@ var Enumerable = (function() { return result; } + /** alias of: select + * Enumerable#findAll(iterator[, context]) -> Array + * + * Returns all the elements for which the iterator returned “truthy” value. + * Aliased as [[Enumerable#select]]. + **/ function findAll(iterator, context) { var results = []; this.each(function(value, index) { @@ -70,6 +119,12 @@ var Enumerable = (function() { return results; } + /** + * Enumerable#grep(regex[, iterator = Prototype.K[, context]]) -> Array + * + * Returns all the elements that match the filter. If an iterator is provided, + * it is used to produce the returned value for each selected element. + **/ function grep(filter, iterator, context) { iterator = iterator || Prototype.K; var results = []; @@ -84,6 +139,12 @@ var Enumerable = (function() { return results; } + /** alias of: Enumerable#member + * Enumerable#include(object) -> Boolean + * + * Determines whether a given object is in the Enumerable or not, + * based on the `==` comparison operator. Aliased as [[Enumerable#member]]. + **/ function include(object) { if (Object.isFunction(this.indexOf)) if (this.indexOf(object) != -1) return true; @@ -98,6 +159,12 @@ var Enumerable = (function() { return found; } + /** + * Enumerable#inGroupsOf(size[, filler = null]) -> [group...] + * + * Groups items in fixed-size chunks, using a specific value to fill up + * the last chunk if necessary. + **/ function inGroupsOf(number, fillWith) { fillWith = Object.isUndefined(fillWith) ? null : fillWith; return this.eachSlice(number, function(slice) { @@ -106,6 +173,13 @@ var Enumerable = (function() { }); } + /** + * Enumerable#inject(accumulator, iterator[, context]) -> accumulatedValue + * + * Incrementally builds a result value based on the successive results + * of the iterator. + * This can be used for array construction, numerical sums/averages, etc. + **/ function inject(memo, iterator, context) { this.each(function(value, index) { memo = iterator.call(context, memo, value, index); @@ -113,6 +187,12 @@ var Enumerable = (function() { return memo; } + /** + * Enumerable#invoke(methodName[, arg...]) -> Array + * + * Invokes the same method, with the same arguments, for all items in a collection. + * Returns the results of the method calls. + **/ function invoke(method) { var args = $A(arguments).slice(1); return this.map(function(value) { @@ -120,6 +200,14 @@ var Enumerable = (function() { }); } + /** + * Enumerable#max([iterator = Prototype.K[, context]]) -> maxValue + * + * Returns the maximum element (or element-based computation), or undefined if + * the enumeration is empty. + * Elements are either compared directly, or by first applying the iterator + * and comparing returned values. + **/ function max(iterator, context) { iterator = iterator || Prototype.K; var result; @@ -131,6 +219,14 @@ var Enumerable = (function() { return result; } + /** + * Enumerable#min([iterator = Prototype.K[, context]]) -> minValue + * + * Returns the minimum element (or element-based computation), or undefined if + * the enumeration is empty. + * Elements are either compared directly, or by first applying the iterator + * and comparing returned values. + **/ function min(iterator, context) { iterator = iterator || Prototype.K; var result; @@ -142,6 +238,14 @@ var Enumerable = (function() { return result; } + /** + * Enumerable#partition([iterator = Prototype.K[, context]]) -> [TrueArray, FalseArray] + * + * Partitions the elements in two groups: those regarded as true, and those + * considered false. + * By default, regular JavaScript boolean equivalence is used, but an iterator + * can be provided, that computes a boolean representation of the elements. + **/ function partition(iterator, context) { iterator = iterator || Prototype.K; var trues = [], falses = []; @@ -152,6 +256,12 @@ var Enumerable = (function() { return [trues, falses]; } + /** + * Enumerable#pluck(propertyName) -> Array + * + * Optimization for a common use-case of collect: fetching the same property + * for all the elements. Returns the property values. + **/ function pluck(property) { var results = []; this.each(function(value) { @@ -160,6 +270,11 @@ var Enumerable = (function() { return results; } + /** + * Enumerable#reject(iterator[, context]) -> Array + * + * Returns all the elements for which the iterator returned a “falsy” value. + **/ function reject(iterator, context) { var results = []; this.each(function(value, index) { @@ -169,6 +284,12 @@ var Enumerable = (function() { return results; } + /** + * Enumerable#sortBy(iterator[, context]) -> Array + * + * Provides a custom-sorted view of the elements based on the criteria computed, + * for each element, by the iterator. + **/ function sortBy(iterator, context) { return this.map(function(value, index) { return { @@ -181,10 +302,24 @@ var Enumerable = (function() { }).pluck('value'); } + /** alias of: Enumerable#entries + * Enumerable#toArray() -> Array + * + * Returns an Array representation of the enumeration. + * Aliased as [[Enumerable#entries]]. + **/ function toArray() { return this.map(); } + /** + * Enumerable#zip(sequence...[, iterator = Prototype.K]) -> Array + * + * Zips together (think of the zip on a pair of trousers) 2+ sequences, + * providing an array of tuples. + * Each tuple contains one value per original sequence. + * Tuples can be converted to something else by applying the optional iterator on them. + **/ function zip() { var iterator = Prototype.K, args = $A(arguments); if (Object.isFunction(args.last())) @@ -196,10 +331,20 @@ var Enumerable = (function() { }); } + /** + * Enumerable#size() -> Number + * + * Returns the size of the enumeration. + **/ function size() { return this.toArray().length; } + /** related to: Object.inspect + * Enumerable#inspect() -> String + * + * TODO: Enumerable#inspect + **/ function inspect() { return '#'; } diff --git a/src/lang/function.js b/src/lang/function.js index e73a01e..435c02a 100644 --- a/src/lang/function.js +++ b/src/lang/function.js @@ -1,3 +1,6 @@ +/** section: lang + * Function +**/ Object.extend(Function.prototype, (function() { var slice = Array.prototype.slice; @@ -12,6 +15,13 @@ Object.extend(Function.prototype, (function() { return update(array, args); } + /** + * Function#argumentNames() -> Array + * + * Reads the argument names as stated in the function definition and returns + * the values as an array of strings (or an empty array if the function is + * defined without parameters). + **/ function argumentNames() { var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1] .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '') @@ -19,6 +29,13 @@ Object.extend(Function.prototype, (function() { return names.length == 1 && !names[0] ? [] : names; } + /** + * Function#bind(object[, args...]) -> Function + * - object (Object): The object to bind to. + * + * Wraps the function in another, locking its execution scope to an object + * specified by `object`. + **/ function bind(context) { if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this; var __method = this, args = slice.call(arguments, 1); @@ -28,6 +45,14 @@ Object.extend(Function.prototype, (function() { } } + /** related to: Function#bind + * Function#bindAsEventListener(object[, args...]) -> Function + * - object (Object): The object to bind to. + * + * An event-specific variant of [[Function#bind]] which ensures the function + * will recieve the current event object as the first argument when + * executing. + **/ function bindAsEventListener(context) { var __method = this, args = slice.call(arguments, 1); return function(event) { @@ -36,6 +61,15 @@ Object.extend(Function.prototype, (function() { } } + /** + * Function#curry(args...) -> Function + * Partially applies the function, returning a function with one or more + * arguments already “filled in.” + * + * Function#curry works just like [[Function#bind]] without the initial + * scope argument. Use the latter if you need to partially apply a function + * _and_ modify its execution scope at the same time. + **/ function curry() { if (!arguments.length) return this; var __method = this, args = slice.call(arguments, 0); @@ -45,6 +79,19 @@ Object.extend(Function.prototype, (function() { } } + /** + * Function#delay(seconds[, args...]) -> Number + * - seconds (Number): How long to wait before calling the function. + * + * Schedules the function to run after the specified amount of time, passing + * any arguments given. + * + * Behaves much like `window.setTimeout`. Returns an integer ID that can be + * used to clear the timeout with `window.clearTimeout` before it runs. + * + * To schedule a function to run as soon as the interpreter is idle, use + * [[Function#defer]]. + **/ function delay(timeout) { var __method = this, args = slice.call(arguments, 1); timeout = timeout * 1000 @@ -53,11 +100,33 @@ Object.extend(Function.prototype, (function() { }, timeout); } + /** + * Function#defer(args...) -> Number + * Schedules the function to run as soon as the interpreter is idle. + * + * A “deferred” function will not run immediately; rather, it will run as soon + * as the interpreter’s call stack is empty. + * + * Behaves much like `window.setTimeout` with a delay set to `0`. Returns an + * ID that can be used to clear the timeout with `window.clearTimeout` before + * it runs. + **/ function defer() { var args = update([0.01], arguments); return this.delay.apply(this, args); } + /** + * Function#wrap(wrapperFunction) -> Function + * - wrapperFunction (Function): The function to act as a wrapper. + * + * Returns a function “wrapped” around the original function. + * + * `Function#wrap` distills the essence of aspect-oriented programming into + * a single method, letting you easily build on existing functions by + * specifying before and after behavior, transforming the return value, or + * even preventing the original function from being called. + **/ function wrap(wrapper) { var __method = this; return function() { @@ -66,6 +135,13 @@ Object.extend(Function.prototype, (function() { } } + /** + * Function#methodize() -> Function + * Wraps the function inside another function that, at call time, pushes + * `this` to the original function as the first argument. + * + * Used to define both a generic method and an instance method. + **/ function methodize() { if (this._methodized) return this._methodized; var __method = this; diff --git a/src/lang/hash.js b/src/lang/hash.js index 928a236..0a275b0 100644 --- a/src/lang/hash.js +++ b/src/lang/hash.js @@ -1,8 +1,23 @@ +/** section: lang + * $H([object]) -> Hash + * + * Creates a Hash (which is synonymous to “map” or “associative array” for our purposes). + * A convenience wrapper around the Hash constructor, with a safeguard that lets you pass + * an existing Hash object and get it back untouched (instead of uselessly cloning it). + **/ function $H(object) { return new Hash(object); }; +/** section: lang + * class Hash +**/ var Hash = Class.create(Enumerable, (function() { + /** + * new Hash([object]) + * + * TODO: new Hash + **/ function initialize(object) { this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); } @@ -16,34 +31,69 @@ var Hash = Class.create(Enumerable, (function() { } } + /** + * Hash#set(key, value) -> value + * + * Sets the hash’s `key` property to value and returns value. + **/ function set(key, value) { return this._object[key] = value; } + /** + * Hash#get(key) -> value + * + * Returns the value of the hash’s `key` property. + **/ function get(key) { // simulating poorly supported hasOwnProperty if (this._object[key] !== Object.prototype[key]) return this._object[key]; } + /** + * Hash#unset(key) -> value + * + * Deletes the hash’s `key` property and returns its value. + **/ function unset(key) { var value = this._object[key]; delete this._object[key]; return value; } + /** + * Hash#toObject() -> Object + * + * Returns a cloned, vanilla object. + **/ function toObject() { return Object.clone(this._object); } + /** + * Hash#keys() -> [String...] + * + * Provides an Array of keys (that is, property names) for the hash. + **/ function keys() { return this.pluck('key'); } + /** + * Hash#values() -> Array + * + * Collect the values of a hash and returns them in an array. + **/ function values() { return this.pluck('value'); } + /** + * Hash#index(value) -> String + * + * TODO: Hash#index + **/ function index(value) { var match = this.detect(function(pair) { return pair.value === value; @@ -51,10 +101,23 @@ var Hash = Class.create(Enumerable, (function() { return match && match.key; } + /** + * Hash#merge(object) -> Hash + * + * Merges `object` to hash and returns the result of that merge. + * Prior to v1.6.0: This was destructive (object's values were added to hash). + * Since v1.6.0: This is no longer destructive (hash is cloned before the operation). + **/ function merge(object) { return this.clone().update(object); } + /** + * Hash#update(object) -> Hash + * + * Updates hash with the key/value pairs of `object`. + * The original hash will be modified. + **/ function update(object) { return new Hash(object).inject(this, function(result, pair) { result.set(pair.key, pair.value); @@ -62,11 +125,21 @@ var Hash = Class.create(Enumerable, (function() { }); } + /** + * Hash#toQueryPair(key, value) -> String + * + * TODO: Hash#toQueryPair + **/ function toQueryPair(key, value) { if (Object.isUndefined(value)) return key; return key + '=' + encodeURIComponent(String.interpret(value)); } + /** related to: String#toQueryParams + * Hash#toQueryString() -> String + * + * Turns a hash into its URL-encoded query string representation. + **/ function toQueryString() { return this.inject([], function(results, pair) { var key = encodeURIComponent(pair.key), values = pair.value; @@ -79,16 +152,31 @@ var Hash = Class.create(Enumerable, (function() { }).join('&'); } + /** related to: Object.inspect + * Hash#inspect() -> String + * + * Returns the debug-oriented string representation of the hash. + **/ function inspect() { return '#'; } + /** related to: Object.toJSON + * Hash#toJSON() -> String + * + * Returns a JSON string. + **/ function toJSON() { return Object.toJSON(this.toObject()); } + /** + * Hash#clone() -> newHash + * + * Returns a clone of hash. + **/ function clone() { return new Hash(this); } diff --git a/src/lang/number.js b/src/lang/number.js index 8fea757..6d3c780 100644 --- a/src/lang/number.js +++ b/src/lang/number.js @@ -1,38 +1,91 @@ +/** section: lang + * Number +**/ Object.extend(Number.prototype, (function() { + /** + * Number#toColorPart() -> String + * + * Produces a 2-digit hexadecimal representation of the number + * (which is therefore assumed to be in the [0..255] range). + * Useful for composing CSS color strings. + **/ function toColorPart() { return this.toPaddedString(2, 16); } + /** + * Number#succ() -> Number + * + * Returns the successor of the current Number, as defined by current + 1. + * Used to make numbers compatible with ObjectRange. + **/ function succ() { return this + 1; } + /** + * Number#times(iterator) -> Number + * + * Encapsulates a regular [0..n[ loop, Ruby-style. + **/ function times(iterator, context) { $R(0, this, true).each(iterator, context); return this; } + /** + * Number#toPaddedString(length[, radix]) -> String + * + * Converts the number into a string padded with 0s so that the string’s length + * is at least equal to `length`. + * Takes an optional `radix` argument which specifies the base to use for conversion. + **/ function toPaddedString(length, radix) { var string = this.toString(radix || 10); return '0'.times(length - string.length) + string; } + /** related to: Object.toJSON + * Number#toJSON() -> String + * + * Returns a JSON string. + **/ function toJSON() { return isFinite(this) ? this.toString() : 'null'; } + /** + * Number#abs() -> Number + * + * Returns the absolute value of the number. + **/ function abs() { return Math.abs(this); } + /** + * Number#round() -> Number + * + * Rounds the number to the nearest integer. + **/ function round() { return Math.round(this); } + /** + * Number#ceil() -> Number + * + * Returns the smallest integer greater than or equal to the number. + **/ function ceil() { return Math.ceil(this); } + /** + * Number#floor() -> Number + * + * Returns the largest integer less than or equal to the number. + **/ function floor() { return Math.floor(this); } diff --git a/src/lang/object.js b/src/lang/object.js index f99004d..7ff1c8d 100644 --- a/src/lang/object.js +++ b/src/lang/object.js @@ -1,3 +1,6 @@ +/** section: lang + * Object +**/ (function() { function getClass(object) { @@ -5,12 +8,36 @@ .match(/^\[object\s(.*)\]$/)[1]; } + /** + * Object.extend(destination, source) -> Object + * - destination (Object): The object to receive the new properties. + * - source (Object): The object whose properties will be duplicated. + * + * Copies all properties from the source to the destination object. Returns + * the destination object. + **/ function extend(destination, source) { for (var property in source) destination[property] = source[property]; return destination; } + /** + * Object.inspect(object) -> String + * - object (Object): The item to be inspected. + * + * Returns the debug-oriented string representation of the object. + * + * `undefined` and `null` are represented as such. + * + * Other types are checked for a `inspect` method. If there is one, it is + * used; otherwise, it reverts to the `toString` method. + * + * Prototype provides `inspect` methods for many types, both built-in and + * library-defined — among them `String`, `Array`, `Enumerable` and `Hash`. + * These attempt to provide useful string representations (from a + * developer’s standpoint) for their respective types. + **/ function inspect(object) { try { if (isUndefined(object)) return 'undefined'; @@ -22,6 +49,19 @@ } } + /** + * Object.toJSON(object) -> String + * - object (Object): The object to be serialized. + * + * Returns a JSON string. + * + * `undefined` and `function` types have no JSON representation. `boolean` + * and `null` are coerced to strings. + * + * For other types, `Object.toJSON` looks for a `toJSON` method on `object`. + * If there is one, it is used; otherwise the object is treated like a + * generic `Object`. + **/ function toJSON(object) { var type = typeof object; switch (type) { @@ -45,14 +85,53 @@ return '{' + results.join(', ') + '}'; } + /** + * Object.toQueryString(object) -> String + * object (Object): The object whose property/value pairs will be converted. + * + * Turns an object into its URL-encoded query string representation. + * + * This is a form of serialization, and is mostly useful to provide complex + * parameter sets for stuff such as objects in the Ajax namespace (e.g. + * [[Ajax.Request]]). + * + * Undefined-value pairs will be serialized as if empty-valued. Array-valued + * pairs will get serialized with one name/value pair per array element. All + * values get URI-encoded using JavaScript’s native `encodeURIComponent` + * function. + * + * The order of pairs in the serialized form is not guaranteed (and mostly + * irrelevant anyway) — except for array-based parts, which are serialized + * in array order. + **/ function toQueryString(object) { return $H(object).toQueryString(); } + /** + * Object.toHTML(object) -> String + * - object (Object): The object to convert to HTML. + * + * Converts the object to its HTML representation. + * + * Returns the return value of `object`’s `toHTML` method if it exists; else + * runs `object` through [[String.interpret]]. + **/ function toHTML(object) { return object && object.toHTML ? object.toHTML() : String.interpret(object); } + /** + * Object.keys(object) -> Array + * - object (Object): The object to pull keys from. + * + * Returns an array of the object's property names. + * + * Note that the order of the resulting array is browser-dependent — it + * relies on the `for…in` loop, for which the ECMAScript spec does not + * prescribe an enumeration order. Sort the resulting array if you wish to + * normalize the order of the object keys. + **/ function keys(object) { var results = []; for (var property in object) @@ -60,6 +139,19 @@ return results; } + /** + * Object.values(object) -> Array + * - object (Object): The object to pull values from. + * + * Returns an array of the object's values. + * + * Note that the order of the resulting array is browser-dependent — it + * relies on the `for…in` loop, for which the ECMAScript spec does not + * prescribe an enumeration order. + * + * Also, remember that while property _names_ are unique, property _values_ + * have no such constraint. + **/ function values(object) { var results = []; for (var property in object) @@ -67,34 +159,89 @@ return results; } + /** + * Object.clone(object) -> Object + * - object (Object): The object to clone. + * + * Duplicates the passed object. + * + * Copies all the original's key/value pairs onto an empty object. + * + * Do note that this is a _shallow_ copy, not a _deep_ copy. Nested objects + * will retain their references. + **/ function clone(object) { return extend({ }, object); } + /** + * Object.isElement(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is a DOM node of type 1; `false` otherwise. + **/ function isElement(object) { return !!(object && object.nodeType == 1); } + /** + * Object.isArray(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is an array; false otherwise. + **/ function isArray(object) { return getClass(object) === "Array"; } + + /** + * Object.isHash(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is an instance of the [[Hash]] class; `false` + * otherwise. + **/ function isHash(object) { return object instanceof Hash; } + /** + * Object.isFunction(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is of type `function`; `false` otherwise. + **/ function isFunction(object) { return typeof object === "function"; } + /** + * Object.isString(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is of type `string`; `false` otherwise. + **/ function isString(object) { return getClass(object) === "String"; } + /** + * Object.isNumber(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is of type `number`; `false` otherwise. + **/ function isNumber(object) { return getClass(object) === "Number"; } + /** + * Object.isUndefined(object) -> Boolean + * - object (Object): The object to test. + * + * Returns `true` if `object` is of type `string`; `false` otherwise. + **/ function isUndefined(object) { return typeof object === "undefined"; } diff --git a/src/lang/periodical_executer.js b/src/lang/periodical_executer.js index a8fd241..cb753bb 100644 --- a/src/lang/periodical_executer.js +++ b/src/lang/periodical_executer.js @@ -1,4 +1,26 @@ +/** section: lang + * class PeriodicalExecuter +**/ var PeriodicalExecuter = Class.create({ + /** + * new PeriodicalExecuter(callback, frequency) + * - callback (Function): the function to be executed at each interval. + * - frequency (Number): the amount of time, in sections, to wait in between + * callbacks. + * + * Creates an object that oversees the calling of a particular function via + * `window.setInterval`. + * + * The only notable advantage provided by `PeriodicalExecuter` is that it + * shields you against multiple parallel executions of the `callback` + * function, should it take longer than the given interval to execute (it + * maintains an internal “running” flag, which is shielded against + * exceptions in the callback function). + * + * This is especially useful if you use one to interact with the user at + * given intervals (e.g. use a prompt or confirm call): this will avoid + * multiple message boxes all waiting to be actioned. + **/ initialize: function(callback, frequency) { this.callback = callback; this.frequency = frequency; @@ -15,6 +37,11 @@ var PeriodicalExecuter = Class.create({ this.callback(this); }, + /** + * PeriodicalExecuter#stop() -> undefined + * + * Stops the periodical executer (there will be no further triggers). + **/ stop: function() { if (!this.timer) return; clearInterval(this.timer); diff --git a/src/lang/range.js b/src/lang/range.js index f29ecde..c92638f 100644 --- a/src/lang/range.js +++ b/src/lang/range.js @@ -1,8 +1,24 @@ +/** section: lang + * class ObjectRange +**/ + +/** + * $R(start, end[, exclusive = false]) -> ObjectRange + * + * Creates a new ObjectRange object. + * This method is a convenience wrapper around the ObjectRange constructor, + * but $R is the preferred alias. +**/ function $R(start, end, exclusive) { return new ObjectRange(start, end, exclusive); } var ObjectRange = Class.create(Enumerable, (function() { + /** + * new ObjectRange(start, end[, exclusive = false]) + * + * TODO: new ObjectRange + **/ function initialize(start, end, exclusive) { this.start = start; this.end = end; @@ -17,6 +33,11 @@ var ObjectRange = Class.create(Enumerable, (function() { } } + /** + * ObjectRange#include(value) -> Boolean + * + * Determines whether the value is included in the range. + **/ function include(value) { if (value < this.start) return false; diff --git a/src/lang/regexp.js b/src/lang/regexp.js index c507db4..d63b6c5 100644 --- a/src/lang/regexp.js +++ b/src/lang/regexp.js @@ -1,5 +1,23 @@ +/** section: lang + * RegExp +**/ + +/** alias of: RegExp#test + * RegExp#match(str) -> Boolean + * + * Return true if string matches the regular expression, false otherwise. + **/ RegExp.prototype.match = RegExp.prototype.test; +/** + * RegExp.escape(str) -> String + * - str (String): A string intended to be used in a `RegExp` constructor. + * + * Escapes any characters in the string that have special meaning in a + * regular expression. + * + * Use before passing a string into the `RegExp` constructor. +**/ RegExp.escape = function(str) { return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); }; diff --git a/src/lang/string.js b/src/lang/string.js index 587c944..844d6d9 100644 --- a/src/lang/string.js +++ b/src/lang/string.js @@ -1,4 +1,12 @@ +/** section: lang + * String +**/ Object.extend(String, { + /** + * String.interpret(value) -> String + * + * TODO: String.interpret + **/ interpret: function(value) { return value == null ? '' : String(value); }, @@ -20,6 +28,13 @@ Object.extend(String.prototype, (function() { return function(match) { return template.evaluate(match) }; } + /** + * String#gsub(pattern, replacement) -> String + * + * Returns the string with every occurence of a given pattern replaced by either + * a regular string, the returned value of a function or a [[Template]] string. + * The pattern can be a string or a regular expression. + **/ function gsub(pattern, replacement) { var result = '', source = this, match; replacement = prepareReplacement(replacement); @@ -36,6 +51,13 @@ Object.extend(String.prototype, (function() { return result; } + /** + * String#sub(pattern, replacement[, count = 1]) -> String + * + * Returns a string with the first count occurrences of pattern replaced by either + * a regular string, the returned value of a function or a [[Template]] string. + * The pattern can be a string or a regular expression. + **/ function sub(pattern, replacement, count) { replacement = prepareReplacement(replacement); count = Object.isUndefined(count) ? 1 : count; @@ -46,11 +68,24 @@ Object.extend(String.prototype, (function() { }); } + /** related to: String#gsub + * String#scan(pattern, iterator) -> String + * + * Allows iterating over every occurrence of the given pattern (which can be a + * string or a regular expression). + * Returns the original string. + **/ function scan(pattern, iterator) { this.gsub(pattern, iterator); return String(this); } + /** + * String#truncate([length = 30[, suffix = '...']]) -> String + * + * Truncates a string to given `length` and appends `suffix` to it (indicating + * that it is only an excerpt). + **/ function truncate(length, truncation) { length = length || 30; truncation = Object.isUndefined(truncation) ? '...' : truncation; @@ -58,18 +93,39 @@ Object.extend(String.prototype, (function() { this.slice(0, length - truncation.length) + truncation : String(this); } + /** + * String#strip() -> String + * + * Strips all leading and trailing whitespace from a string. + **/ function strip() { return this.replace(/^\s+/, '').replace(/\s+$/, ''); } + /** + * String#stripTags() -> String + * + * Strips a string of any HTML tag. + **/ function stripTags() { return this.replace(/<\/?[^>]+>/gi, ''); } + /** + * String#stripScripts() -> String + * + * Strips a string of anything that looks like an HTML script block. + **/ function stripScripts() { return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); } + /** + * String#extractScripts() -> Array + * + * Exctracts the content of any script block present in the string and returns + * them as an array of strings. + **/ function extractScripts() { var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); @@ -78,15 +134,31 @@ Object.extend(String.prototype, (function() { }); } + /** + * String#evalScripts() -> Array + * + * Evaluates the content of any script block present in the string. + * Returns an array containing the value returned by each script. + **/ function evalScripts() { return this.extractScripts().map(function(script) { return eval(script) }); } + /** + * String#escapeHTML() -> String + * + * Converts HTML special characters to their entity equivalents. + **/ function escapeHTML() { escapeHTML.text.data = this; return escapeHTML.div.innerHTML; } + /** related to: String#escapeHTML + * String#unescapeHTML() -> String + * + * Strips tags and converts the entity forms of special HTML characters to their normal form. + **/ function unescapeHTML() { var div = new Element('div'); div.innerHTML = this.stripTags(); @@ -95,6 +167,11 @@ Object.extend(String.prototype, (function() { div.childNodes[0].nodeValue) : ''; } + /** alias of: String#parseQuery, related to: Hash#toQueryString + * String#toQueryParams([separator = '&']) -> Object + * + * Parses a URI-like query string and returns an object composed of parameter/value pairs. + **/ function toQueryParams(separator) { var match = this.strip().match(/([^?#]*)(#.*)?$/); if (!match) return { }; @@ -115,19 +192,42 @@ Object.extend(String.prototype, (function() { }); } + /** + * String#toArray() -> Array + * + * Splits the string character-by-character and returns an array with the result. + **/ function toArray() { return this.split(''); } + /** + * String#succ() -> String + * + * Used internally by ObjectRange. + * Converts the last character of the string to the following character in the + * Unicode alphabet. + **/ function succ() { return this.slice(0, this.length - 1) + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); } + /** + * String#times(count) -> String + * + * Concatenates the string `count` times. + **/ function times(count) { return count < 1 ? '' : new Array(count + 1).join(this); } + /** + * String#camelize() -> String + * + * Converts a string separated by dashes into a camelCase equivalent. + * For instance, 'foo-bar' would be converted to 'fooBar'. + **/ function camelize() { var parts = this.split('-'), len = parts.length; if (len == 1) return parts[0]; @@ -142,18 +242,39 @@ Object.extend(String.prototype, (function() { return camelized; } + /** + * String#capitalize() -> String + * + * Capitalizes the first letter of a string and downcases all the others. + **/ function capitalize() { return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); } + /** + * String#underscore() -> String + * + * Converts a camelized string into a series of words separated by an underscore ("_"). + **/ function underscore() { return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); } + /** + * String#dasherize() -> String + * + * Replaces every instance of the underscore character ("_") by a dash ("-"). + **/ function dasherize() { return this.gsub(/_/,'-'); } + /** related to: Object.inspect + * String#inspect([useDoubleQuotes = false]) -> String + * + * Returns a debug-oriented version of the string (i.e. wrapped in single or + * double quotes, with backslashes and quotes escaped). + **/ function inspect(useDoubleQuotes) { var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { var character = String.specialChar[match[0]]; @@ -163,14 +284,31 @@ Object.extend(String.prototype, (function() { return "'" + escapedString.replace(/'/g, '\\\'') + "'"; } + /** related to: Object.toJSON + * String#toJSON() -> String + * + * Returns a JSON string. + **/ function toJSON() { return this.inspect(true); } + /** + * String#unfilterJSON([filter = Prototype.JSONFilter]) -> String + * + * Strips comment delimiters around Ajax JSON or JavaScript responses. + * This security method is called internally. + **/ function unfilterJSON(filter) { return this.sub(filter || Prototype.JSONFilter, '#{1}'); } + /** + * String#isJSON() -> Boolean + * + * Check if the string is valid JSON by the use of regular expressions. + * This security method is called internally. + **/ function isJSON() { var str = this; if (str.blank()) return false; @@ -178,6 +316,13 @@ Object.extend(String.prototype, (function() { return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); } + /** + * String#evalJSON([sanitize = false]) -> object + * + * Evaluates the JSON in the string and returns the resulting object. + * If the optional sanitize parameter is set to true, the string is checked for + * possible malicious attempts and eval is not called if one is detected. + **/ function evalJSON(sanitize) { var json = this.unfilterJSON(); try { @@ -186,27 +331,57 @@ Object.extend(String.prototype, (function() { throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); } + /** + * String#include(substring) -> Boolean + * + * Check if the string contains a substring. + **/ function include(pattern) { return this.indexOf(pattern) > -1; } + /** + * String#startsWith(substring) -> Boolean + * + * Checks if the string starts with substring. + **/ function startsWith(pattern) { return this.indexOf(pattern) === 0; } + /** + * String#endsWith(substring) -> Boolean + * + * Checks if the string ends with substring. + **/ function endsWith(pattern) { var d = this.length - pattern.length; return d >= 0 && this.lastIndexOf(pattern) === d; } + /** + * String#empty() -> Boolean + * + * Checks if the string is empty. + **/ function empty() { return this == ''; } + /** + * String#blank() -> Boolean + * + * Check if the string is 'blank', meaning either empty or containing only whitespace. + **/ function blank() { return /^\s*$/.test(this); } + /** + * String#interpolate(object[, pattern]) -> String + * + * Treats the string as a Template and fills it with object’s properties. + **/ function interpolate(object, pattern) { return new Template(this, pattern).evaluate(object); } diff --git a/src/lang/template.js b/src/lang/template.js index adeb070..6e8b743 100644 --- a/src/lang/template.js +++ b/src/lang/template.js @@ -1,9 +1,23 @@ +/** section: lang + * class Template +**/ var Template = Class.create({ + /** + * new Template(template[, pattern = Template.Pattern]) + * + * TODO: new Template + **/ initialize: function(template, pattern) { this.template = template.toString(); this.pattern = pattern || Template.Pattern; }, + /** + * Template#evaluate(object) -> String + * + * Applies the template to given `object`’s data, producing a formatted string + * with symbols replaced by corresponding object’s properties. + **/ evaluate: function(object) { if (Object.isFunction(object.toTemplateReplacements)) object = object.toTemplateReplacements(); diff --git a/src/prototype.js b/src/prototype.js index 871d742..e5a760f 100644 --- a/src/prototype.js +++ b/src/prototype.js @@ -35,6 +35,21 @@ if (Prototype.Browser.MobileSafari) var Abstract = { }; +/** + * == lang == + * Language extensions. +**/ + +/** section: lang + * Try +**/ + +/** + * Try.these(function...) -> ? + * - function (Function): A function that may throw an exception. + * Accepts an arbitrary number of functions and returns the result of the + * first one that doesn't throw an error. + **/ var Try = { these: function() { var returnValue;