+
+
+EOF
+ PDoc.run({
+ :source_files => Dir[File.join('src', 'prototype', '**', '*.js')],
+ :destination => DOC_DIR,
+ :index_page => 'README.markdown',
+ :syntax_highlighter => syntax_highlighter,
+ :markdown_parser => :bluecloth,
+ :src_code_text => "View source on GitHub →",
+ :src_code_href => proc { |obj|
+ "https://github.com/sstephenson/prototype/blob/#{hash}/#{obj.file}#L#{obj.line_number}"
+ },
+ :pretty_urls => false,
+ :bust_cache => false,
+ :name => 'Prototype JavaScript Framework',
+ :short_name => 'Prototype',
+ :home_url => 'http://prototypejs.org',
+ :version => PrototypeHelper::VERSION,
+ :index_header => index_header,
+ :footer => 'This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.',
+ :assets => 'doc_assets'
+ })
+ end
+
+ def self.require_package(name)
+ begin
+ require name
+ rescue LoadError
+ puts "You need the #{name} package. Try installing it with:\n"
+ puts " $ gem install #{name}"
+ exit
+ end
+ end
+
+ def self.require_phantomjs
+ cmd = IS_WINDOWS ? "phantomjs.cmd -v" : "phantomjs -v > /dev/null 2>&1"
+ success = system(cmd)
+ if !success
+ puts "\nYou need phantomjs installed to run this task. Find out how at:"
+ puts " http://phantomjs.org/download.html"
+ exit
+ end
+ end
+
+ def self.syntax_highlighter
+ if ENV['SYNTAX_HIGHLIGHTER']
+ highlighter = ENV['SYNTAX_HIGHLIGHTER'].to_sym
+ require_highlighter(highlighter, true)
+ return highlighter
+ end
+
+ SYNTAX_HIGHLIGHTERS.detect { |n| require_highlighter(n) }
+ end
+
+ def self.require_highlighter(name, verbose=false)
+ case name
+ when :pygments
+ success = system("pygmentize -V > /dev/null")
+ if !success && verbose
+ puts "\nYou asked to use Pygments, but I can't find the 'pygmentize' binary."
+ puts "To install, visit:\n"
+ puts " http://pygments.org/docs/installation/\n\n"
+ exit
+ end
+ return success # (we have pygments)
+ when :coderay
+ begin
+ require 'coderay'
+ rescue LoadError
+ if verbose
+ puts "\nYou asked to use CodeRay, but I can't find the 'coderay' gem. Just run:\n\n"
+ puts " $ gem install coderay"
+ puts "\nand you should be all set.\n\n"
+ exit
+ end
+ return false
+ end
+ return true # (we have CodeRay)
+ when :none
+ return true
+ else
+ puts "\nYou asked to use a syntax highlighter I don't recognize."
+ puts "Valid options: #{SYNTAX_HIGHLIGHTERS.join(', ')}\n\n"
+ exit
+ end
+ end
+
+ def self.require_sprockets
+ require_submodule('Sprockets', 'sprockets')
+ end
+
+ def self.require_pdoc
+ require_submodule('PDoc', 'pdoc')
+ end
+
+ def self.require_unittest_js
+ require_submodule('UnittestJS', 'unittest_js')
+ end
+
+ def self.require_caja_builder
+ require_submodule('CajaBuilder', 'caja_builder')
+ end
+
+ def self.get_selector_engine(name)
+ return if !name
+ # If the submodule exists, we should use it.
+ submodule_path = File.join(ROOT_DIR, "vendor", name)
+ return submodule_path if File.exist?(File.join(submodule_path, "repository", ".git"))
+ return submodule_path if name === "legacy_selector"
+
+ # If it doesn't exist, we should fetch it.
+ get_submodule('the required selector engine', "#{name}/repository")
+ unless File.exist?(submodule_path)
+ puts "The selector engine you required isn't available at vendor/#{name}.\n\n"
+ exit
+ end
+ end
+
+ def self.get_submodule(name, path)
+ require_git
+ puts "\nYou seem to be missing #{name}. Obtaining it via git...\n\n"
-def sprocketize(path, source, destination = source)
- begin
- require "sprockets"
- rescue LoadError => e
- puts "\nYou'll need Sprockets to build Prototype. Just run:\n\n"
+ Kernel.system("git submodule init")
+ return true if Kernel.system("git submodule update vendor/#{path}")
+ # If we got this far, something went wrong.
+ puts "\nLooks like it didn't work. Try it manually:\n\n"
puts " $ git submodule init"
- puts " $ git submodule update"
- puts "\nand you should be all set.\n\n"
- end
-
- secretary = Sprockets::Secretary.new(
- :root => File.join(PROTOTYPE_ROOT, path),
- :load_path => [PROTOTYPE_SRC_DIR],
- :source_files => [source]
- )
-
- secretary.concatenation.save_to(File.join(PROTOTYPE_DIST_DIR, destination))
+ puts " $ git submodule update vendor/#{path}"
+ false
+ end
+
+ def self.require_submodule(name, path)
+ begin
+ full_path = File.join(PrototypeHelper::ROOT_DIR, 'vendor', path, 'lib', path)
+ # We need to require the explicit version in the submodule.
+ require full_path
+ rescue LoadError => e
+ # Wait until we notice that a submodule is missing before we bother the
+ # user about installing git. (Maybe they brought all the files over
+ # from a different machine.)
+ missing_file = e.message.sub('no such file to load -- ', '').sub('cannot load such file -- ', '')
+ if missing_file == full_path
+ # Missing a git submodule.
+ retry if get_submodule(name, path)
+ else
+ # Missing a gem.
+ puts "\nIt looks like #{name} is missing the '#{missing_file}' gem. Just run:\n\n"
+ puts " $ gem install #{missing_file}"
+ puts "\nand you should be all set.\n\n"
+ end
+ exit
+ end
+ end
+
+ def self.current_head
+ `git show-ref --hash HEAD`.chomp[0..6]
+ end
end
+task :default => [:dist, :dist_helper, :package, :clean_package_source]
+
desc "Builds the distribution."
task :dist do
- sprocketize("src", "prototype.js")
+ PrototypeHelper.sprocketize(
+ :path => 'src',
+ :source => 'prototype.js',
+ :selector_engine => ENV['SELECTOR_ENGINE'] || PrototypeHelper::DEFAULT_SELECTOR_ENGINE
+ )
end
namespace :doc do
desc "Builds the documentation."
task :build => [:require] do
- require 'protodoc'
- require 'tempfile'
-
- 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
-
+ PrototypeHelper.build_doc_for(ENV['SECTION'] ? "#{ENV['SECTION']}.js" : 'prototype.js')
+ end
+
task :require do
- lib = 'vendor/pdoc/lib/pdoc'
- unless File.exists?(lib)
- puts "\nYou'll need PDoc to generate the documentation. Just run:\n\n"
- puts " $ git submodule init"
- puts " $ git submodule update"
- puts "\nand you should be all set.\n\n"
- end
- require lib
+ PrototypeHelper.require_pdoc
end
end
@@ -70,12 +264,12 @@ task :doc => ['doc:build']
desc "Builds the updating helper."
task :dist_helper do
- sprocketize("ext/update_helper", "prototype_update_helper.js")
+ PrototypeHelper.sprocketize(:path => 'ext/update_helper', :source => 'prototype_update_helper.js')
end
-Rake::PackageTask.new('prototype', PROTOTYPE_VERSION) do |package|
+Rake::PackageTask.new('prototype', PrototypeHelper::VERSION) do |package|
package.need_tar_gz = true
- package.package_dir = PROTOTYPE_PKG_DIR
+ package.package_dir = PrototypeHelper::PKG_DIR
package.package_files.include(
'[A-Z]*',
'dist/prototype.js',
@@ -86,100 +280,40 @@ Rake::PackageTask.new('prototype', PROTOTYPE_VERSION) do |package|
end
task :clean_package_source do
- rm_rf File.join(PROTOTYPE_PKG_DIR, "prototype-#{PROTOTYPE_VERSION}")
+ rm_rf File.join(PrototypeHelper::PKG_DIR, "prototype-#{PrototypeHelper::VERSION}")
end
-task :test => ['test:build', 'test:run']
+task :test => ['test:require', 'test:start']
namespace :test do
- desc 'Runs all the JavaScript unit tests and collects the results'
- task :run => [:require] do
- testcases = ENV['TESTCASES']
- browsers_to_test = ENV['BROWSERS'] && ENV['BROWSERS'].split(',')
- tests_to_run = ENV['TESTS'] && ENV['TESTS'].split(',')
- runner = UnittestJS::WEBrickRunner::Runner.new(:test_dir => PROTOTYPE_TMP_DIR)
-
- Dir[File.join(PROTOTYPE_TMP_DIR, '*_test.html')].each do |file|
- file = File.basename(file)
- test = file.sub('_test.html', '')
- unless tests_to_run && !tests_to_run.include?(test)
- runner.add_test(file, testcases)
- end
- end
-
- UnittestJS::Browser::SUPPORTED.each do |browser|
- unless browsers_to_test && !browsers_to_test.include?(browser)
- runner.add_browser(browser.to_sym)
- end
- end
-
- trap('INT') { runner.teardown; exit }
- runner.run
- end
-
- task :build => [:clean, :dist] do
- builder = UnittestJS::Builder::SuiteBuilder.new({
- :input_dir => PROTOTYPE_TEST_UNIT_DIR,
- :assets_dir => PROTOTYPE_DIST_DIR
- })
- selected_tests = (ENV['TESTS'] || '').split(',')
- builder.collect(*selected_tests)
- builder.render
- end
-
- task :clean => [:require] do
- UnittestJS::Builder.empty_dir!(PROTOTYPE_TMP_DIR)
+ desc 'Starts the test server.'
+ task :start => [:require] do
+ path_to_app = File.join(PrototypeHelper::ROOT_DIR, 'test', 'unit', 'server.rb')
+ require path_to_app
+
+ puts "Starting unit test server..."
+ puts "Unit tests available at | ', ' |
Kiwi, banana and apple.
' + * + * Relying on the `toString()` method: + * + * $('fruits').update(123); + * // -> Element + * $('fruits').innerHTML; + * // -> '123' + * + * Finally, you can do some pretty funky stuff by defining your own + * `toString()` method on your custom objects: + * + * var Fruit = Class.create({ + * initialize: function(fruit){ + * this.fruit = fruit; + * }, + * toString: function(){ + * return 'I am a fruit and my name is "' + this.fruit + '".'; + * } + * }); + * var apple = new Fruit('apple'); + * + * $('fruits').update(apple); + * $('fruits').innerHTML; + * // -> 'I am a fruit and my name is "apple".' + **/ + function update(element, content) { + element = $(element); + + // Purge the element's existing contents of all storage keys and + // event listeners, since said content will be replaced no matter + // what. + var descendants = element.getElementsByTagName('*'), + i = descendants.length; + while (i--) purgeElement(descendants[i]); + + if (content && content.toElement) + content = content.toElement(); + + if (Object.isElement(content)) + return element.update().insert(content); + + + content = Object.toHTML(content); + var tagName = element.tagName.toUpperCase(); + + if (ANY_INNERHTML_BUGGY) { + if (tagName in INSERTION_TRANSLATIONS.tags) { + while (element.firstChild) + element.removeChild(element.firstChild); + + var nodes = getContentFromAnonymousElement(tagName, content.stripScripts()); + for (var i = 0, node; node = nodes[i]; i++) + element.appendChild(node); + + } else { + element.innerHTML = content.stripScripts(); + } + } else { + element.innerHTML = content.stripScripts(); + } + + content.evalScripts.bind(content).defer(); + return element; + } + + /** + * Element.replace(@element[, newContent]) -> Element + * + * Replaces `element` _itself_ with `newContent` and returns `element`. + * + * Keep in mind that this method returns the element that has just been + * removed — not the element that took its place. + * + * `newContent` can be either plain text, an HTML snippet or any JavaScript + * object which has a `toString()` method. + * + * If `newContent` contains any `'); + * // -> Element (ul#favorite) and prints "removed!" in an alert dialog. + * + * $('fruits').innerHTML; + * // -> 'Melon, oranges and grapes.
' + * + * With plain text: + * + * $('still-first').replace('Melon, oranges and grapes.'); + * // -> Element (p#still-first) + * + * $('fruits').innerHTML; + * // -> 'Melon, oranges and grapes.' + * + * Finally, relying on the `toString()` method: + * + * $('fruits').replace(123); + * // -> Element + * + * $('food').innerHTML; + * // -> '123' + * + * ##### Warning + * + * Using [[Element.replace]] as an instance method (e.g., + * `$('foo').replace('Bar
')`) causes errors in Opera 9 when used on + * `input` elements. The `replace` property is reserved on `input` elements + * as part of [Web Forms 2](http://www.whatwg.org/specs/web-forms/current-work/). + * As a workaround, use the generic version instead + * (`Element.replace('foo', 'Bar
')`). + * + **/ + function replace(element, content) { + element = $(element); + + if (content && content.toElement) { + content = content.toElement(); + } else if (!Object.isElement(content)) { + content = Object.toHTML(content); + var range = element.ownerDocument.createRange(); + range.selectNode(element); + content.evalScripts.bind(content).defer(); + content = range.createContextualFragment(content.stripScripts()); + } + + element.parentNode.replaceChild(content, element); + return element; + } + + var INSERTION_TRANSLATIONS = { + before: function(element, node) { + element.parentNode.insertBefore(node, element); + }, + top: function(element, node) { + element.insertBefore(node, element.firstChild); + }, + bottom: function(element, node) { + element.appendChild(node); + }, + after: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); + }, + + tags: { + TABLE: ['| ', ' |
+ *
+ *
+ *
+ * | Name | + *Default | + *Description | + *
|---|---|---|
setLeft |
+ * true |
+ * Clones source's left CSS property onto element. |
+ *
setTop |
+ * true |
+ * Clones source's top CSS property onto element. |
+ *
setWidth |
+ * true |
+ * Clones source's width onto element. |
+ *
setHeight |
+ * true |
+ * Clones source's width onto element. |
+ *
offsetLeft |
+ * 0 |
+ * Number by which to offset element's left CSS property. |
+ *
offsetTop |
+ * 0 |
+ * Number by which to offset element's top CSS property. |
+ *
'
+ *
+ * Lastly, you can pass [[String#gsub]] a [[Template]] string in which you can also access
+ * the returned value of the `match()` method using the ruby inspired notation: `#{0}`
+ * for the first element of the array, `#{1}` for the second one, and so on.
+ * So our last example could be easily re-written as:
+ *
+ * markdown.gsub(/!\[(.*?)\]\((.*?)\)/, '
'
+ *
+ * If you need an equivalent to [[String#gsub]] but without global match set on, try [[String#sub]].
+ *
+ * ##### Note
+ *
+ * Do _not_ use the `"g"` flag on the regex as this will create an infinite loop.
+ **/
+ function gsub(pattern, replacement) {
+ var result = '', source = this, match;
+ replacement = prepareReplacement(replacement);
+
+ if (Object.isString(pattern))
+ pattern = RegExp.escape(pattern);
+
+ if (!(pattern.length || isNonEmptyRegExp(pattern))) {
+ replacement = replacement('');
+ return replacement + source.split('').join(replacement) + replacement;
+ }
+
+ while (source.length > 0) {
+ match = source.match(pattern)
+ if (match && match[0].length > 0) {
+ result += source.slice(0, match.index);
+ result += String.interpret(replacement(match));
+ source = source.slice(match.index + match[0].length);
+ } else {
+ result += source, source = '';
+ }
+ }
+ 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.
+ * `pattern` can be a string or a regular expression.
+ *
+ * Unlike [[String#gsub]], [[String#sub]] takes a third optional parameter which specifies
+ * the number of occurrences of the pattern which will be replaced.
+ * If not specified, it will default to 1.
+ *
+ * Apart from that, [[String#sub]] works just like [[String#gsub]].
+ * Please refer to it for a complete explanation.
+ *
+ * ##### Examples
+ *
+ * var fruits = 'apple pear orange';
+ *
+ * fruits.sub(' ', ', ');
+ * // -> 'apple, pear orange'
+ *
+ * fruits.sub(' ', ', ', 1);
+ * // -> 'apple, pear orange'
+ *
+ * fruits.sub(' ', ', ', 2);
+ * // -> 'apple, pear, orange'
+ *
+ * fruits.sub(/\w+/, function(match){ return match[0].capitalize() + ',' }, 2);
+ * // -> 'Apple, Pear, orange'
+ *
+ * var markdown = ' ';
+ *
+ * markdown.sub(/!\[(.*?)\]\((.*?)\)/, function(match) {
+ * return '
'
+ *
+ * markdown.sub(/!\[(.*?)\]\((.*?)\)/, '
'
+ *
+ * ##### Note
+ *
+ * Do _not_ use the `"g"` flag on the regex as this will create an infinite loop.
+ **/
+ function sub(pattern, replacement, count) {
+ replacement = prepareReplacement(replacement);
+ count = Object.isUndefined(count) ? 1 : count;
+
+ return this.gsub(pattern, function(match) {
+ if (--count < 0) return match[0];
+ return replacement(match);
+ });
+ }
+
+ /** 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.
+ *
+ * Internally just calls [[String#gsub]] passing it `pattern` and `iterator` as arguments.
+ *
+ * ##### Examples
+ *
+ * 'apple, pear & orange'.scan(/\w+/, alert);
+ * // -> 'apple pear & orange' (and displays 'apple', 'pear' and 'orange' in three successive alert dialogs)
+ *
+ * Can be used to populate an array:
+ *
+ * var fruits = [];
+ * 'apple, pear & orange'.scan(/\w+/, function(match) { fruits.push(match[0]) });
+ * fruits.inspect()
+ * // -> ['apple', 'pear', 'orange']
+ *
+ * or even to work on the DOM:
+ *
+ * 'failure-message, success-message & spinner'.scan(/(\w|-)+/, Element.toggle)
+ * // -> 'failure-message, success-message & spinner' (and toggles the visibility of each DOM element)
+ *
+ * ##### Note
+ *
+ * Do _not_ use the `"g"` flag on the regex as this will create an infinite loop.
+ **/
+ 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).
+ *
+ * ##### Examples
+ *
+ * 'A random sentence whose length exceeds 30 characters.'.truncate();
+ * // -> 'A random sentence whose len...'
+ *
+ * 'Some random text'.truncate();
+ * // -> 'Some random text.'
+ *
+ * 'Some random text'.truncate(10);
+ * // -> 'Some ra...'
+ *
+ * 'Some random text'.truncate(10, ' [...]');
+ * // -> 'Some [...]'
+ **/
+ function truncate(length, truncation) {
+ length = length || 30;
+ truncation = Object.isUndefined(truncation) ? '...' : truncation;
+ return this.length > length ?
+ this.slice(0, length - truncation.length) + truncation : String(this);
+ }
+
+ /**
+ * String#strip() -> String
+ *
+ * Strips all leading and trailing whitespace from a string.
+ *
+ * ##### Example
+ *
+ * ' hello world! '.strip();
+ * // -> 'hello world!'
+ **/
+ function strip() {
+ return this.replace(/^\s+/, '').replace(/\s+$/, '');
+ }
+
+ /**
+ * String#stripTags() -> String
+ *
+ * Strips a string of any HTML tags.
+ *
+ * Note that [[String#stripTags]] will only strip HTML 4.01 tags — like
+ * `div`, `span`, and `abbr`. It _will not_ strip namespace-prefixed tags
+ * such as `h:table` or `xsl:template`.
+ *
+ * Watch out for `'.stripTags();
+ * // -> 'a linkalert("hello world!");'
+ *
+ * 'a link'.stripScripts().stripTags();
+ * // -> 'a link'
+ **/
+ function stripTags() {
+ return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?(\/)?>|<\/\w+>/gi, '');
+ }
+
+ /**
+ * String#stripScripts() -> String
+ *
+ * Strips a string of things that look like HTML script blocks.
+ *
+ * ##### Example
+ *
+ * "This is a test.End of test
".stripScripts(); + * // => "This is a test.End of test
" + * + * ##### Caveat User + * + * Note that the processing [[String#stripScripts]] does is good enough for + * most purposes, but you cannot rely on it for security purposes. If you're + * processing end-user-supplied content, [[String#stripScripts]] is probably + * not sufficiently robust to prevent hack attacks. + **/ + function stripScripts() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + } + + /** + * String#extractScripts() -> Array + * + * Extracts the content of any `'.extractScripts(); + * // -> ['2 + 2'] + * + * ''.extractScripts(); + * // -> ['2 + 2', 'alert("hello world!")'] + * + * ##### Notes + * + * To evaluate the scripts later on, you can use the following: + * + * var myScripts = ''.extractScripts(); + * // -> ['2 + 2', 'alert("hello world!")'] + * + * var myReturnedValues = myScripts.map(function(script) { + * return eval(script); + * }); + * // -> [4, undefined] (and displays 'hello world!' in the alert dialog) + **/ + function extractScripts() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'), + matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + var matchMimeType = new RegExp(Prototype.ExecutableScriptFragment, 'im'); + var matchTypeAttribute = /type=/i; + + var results = []; + (this.match(matchAll) || []).each(function(scriptTag) { + var match = scriptTag.match(matchOne); + var attributes = match[1]; + if (attributes !== '') { + // If the script has a `type` attribute, make sure it has a + // JavaScript MIME-type. If not, ignore it. + attributes = attributes.strip(); + var hasTypeAttribute = (matchTypeAttribute).test(attributes); + var hasMimeType = (matchMimeType).test(attributes); + if (hasTypeAttribute && !hasMimeType) return; + } + results.push(match ? match[2] : ''); + }); + + return results; + } + + /** + * String#evalScripts() -> Array + * + * Evaluates the content of any inline `'.evalScripts(); + * // -> [4] + * + * ''.evalScripts(); + * // -> [4, undefined] (and displays 'hello world!' in the alert dialog) + * + * ##### About `evalScripts`, `var`s, and defining functions + * + * [[String#evalScripts]] evaluates script blocks, but this **does not** mean + * they are evaluated in the global scope. They aren't, they're evaluated in + * the scope of the [[String#evalScripts]] method. This has important + * ramifications for your scripts: + * + * * Anything in your script declared with the `var` keyword will be + * discarded momentarily after evaluation, and will be invisible to any + * other scope. + * * If any ` - + - + - +Scope test - scope of the handler should be this element
- +Event object test - should be present as a first argument
- +Hijack link test (preventDefault)
- + - +Mouse click:
- + - +Context menu event (tries to prevent default)
- +Event.element() test
- +Event.currentTarget test
- + - +Event.findElement() test
- + - +Stop propagation test (bubbling)
Keyup test - focus on the textarea and type
bindAsEventListener() test
Object.inspect(event) test
mouseenter test
Add unload events
- + + +