diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..77362d8078 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.pyc +third_party/closure/goog/deps.js +shaka-player.compiled.js +shaka-player.compiled.debug.js +shaka-player.compiled.debug.map +docs/api +docs/reference diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000000..65c4ca6e07 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,16 @@ +# This is the official list of shaka-player authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. +# +# Names should be added to this file with one of the following patterns: +# +# For individual contributors: +# Name +# +# For corporate contributors: +# Organization +# See examples below or python fnmatch module documentation for more information. +# +# Please keep the list sorted. + +Google Inc. <*@google.com> diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..5b4c4d2f76 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,58 @@ +## 1.0 (2014-12-19) + +First public release. + +Bugfixes: + - Text tracks are no longer ignored in MPD manifests. + - Adaptation decisions are now quicker and more reliable. + - (This bug was more noticeable on faster internet connections.) + - Playback no longer gets "stuck" on certain content. + - Playback no longer gets "stuck" after certain seek patterns. + - Player get/select/enable methods can now be called without a video source. + - A \ tag's "videoWidth"/"videoHeight" attributes now update + correctly on Chrome >= 40. + - Manual adaptation while paused no longer unpauses the video. + - Credentials can now be used on cross-domain license requests. + - Range headers are no longer sent for all segment requests. + - (This fixes issues with IIS.) + - A missing declaration of getVideoPlaybackQuality() has been added. + - The compiled code no longer pollutes the global namespace. + - DASH manifests using \ are now parsed correctly. + - Formatting has been fixed in the "Shaka Player Development" tutorial. + +Features: + - The Player is now reusable. You can call load() multiple times without + calling destroy(). + - The JS linter is now included in sources, fixing compatibility issues + between versions. + - The test suite now includes playback integration tests. + - The Player has been updated to support the 01 Dec 2014 draft of the EME + specification. + - The loader in load.js no longer makes assumptions about app.js. You can + now use load.js to bootstrap other applications. + - The test app now uses less screen real estate. + - All custom events have been documented, and a new tutorial has been added + to demonstrate how they can be used. + - The Player now has a support-check API to determine if the browser has all + necessary features for playback. + - Sample code in the tutorials is now marked up to highlight changes from the + previous sample. + - Code coverage in unit tests has been increased. + - Flakiness in unit tests has been reduced. + - DASH manifests using \ without a segment index or segment + timeline are now supported. + - The DASH "presentationTimeOffset" attribute is now supported. + +Broken Compatibility: + - ContentProtectionCallback no longer takes a "mimeType" argument. + - DrmSchemeInfo constructor no longer takes a "mimeType" argument. + - DrmSchemeInfo constructor's "initData" argument is now an object with + fields instead of a Uint8Array. + - DrmSchemeInfo now takes a "withCredentials" argument. + - lib.js has been renamed to shaka-player.compiled.js. + + +## 0.1b (2014-11-21) + +Private beta release. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..e8ee028500 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,58 @@ +# How to contribute # + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + + +## Contributor License Agreement ## + +Contributions to any Google project must be accompanied by a Contributor +License Agreement. This is not a copyright **assignment**, it simply gives +Google permission to use and redistribute your contributions as part of the +project. + + * If you are an individual writing original source code and you're sure you + own the intellectual property, then you'll need to sign an [individual + CLA][]. + + * If you work for a company that wants to allow you to contribute your work, + then you'll need to sign a [corporate CLA][]. + +You generally only need to submit a CLA once, so if you've already submitted +one (even if it was for a different project), you probably don't need to do it +again. + +[individual CLA]: https://developers.google.com/open-source/cla/individual +[corporate CLA]: https://developers.google.com/open-source/cla/corporate + +Once your CLA is submitted (or if you already submitted one for +another Google project), make a commit adding yourself to the +[AUTHORS][] and [CONTRIBUTORS][] files. This commit can be part +of your first [pull request][]. + +[AUTHORS]: AUTHORS +[CONTRIBUTORS]: CONTRIBUTORS + + +## Submitting a patch ## + + 1. It's generally best to start by opening a new issue describing the bug or + feature you're intending to fix. Even if you think it's relatively minor, + it's helpful to know what people are working on. Mention in the initial + issue that you are planning to work on that bug or feature so that it can + be assigned to you. + + 1. Follow the normal process of [forking][] the project, and setup a new + branch to work in. It's important that each group of changes be done in + separate branches in order to ensure that a pull request only includes the + commits related to that bug or feature. + + 1. Do your best to have [well-formed commit messages][] for each change. + This provides consistency throughout the project, and ensures that commit + messages are able to be formatted properly by various git tools. + + 1. Finally, push the commits to your fork and submit a [pull request][]. + +[forking]: https://help.github.com/articles/fork-a-repo +[well-formed commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html +[pull request]: https://help.github.com/articles/creating-a-pull-request diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000000..dd226b72d4 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,28 @@ +# People who have agreed to one of the CLAs and can contribute patches. +# The AUTHORS file lists the copyright holders; this file +# lists people. For example, Google employees are listed here +# but not in AUTHORS, because Google holds the copyright. +# +# Names should be added to this file only after verifying that +# the individual or the individual's organization has agreed to +# the appropriate Contributor License Agreement, found here: +# +# https://developers.google.com/open-source/cla/individual +# https://developers.google.com/open-source/cla/corporate +# +# The agreement for individuals can be filled out on the web. +# +# When adding J Random Contributor's name to this file, +# either J's name or J's organization's name should be +# added to the AUTHORS file, depending on whether the +# individual or corporate CLA was used. +# +# Names should be added to this file as: +# Name +# +# Please keep the list sorted. + +Joey Parrish +Natalie Harris +Timothy Drews +Vasanth Polipelli diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..bdd53e34e6 --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# Shaka Player # + +The Shaka Player is a JavaScript library which implements a [DASH][] client. +It relies on [HTML5 video][], [MediaSource Extensions][], and [Encrypted Media +Extensions][] for playback. + +A generic DASH client can be difficult to implement, and the DASH standard does +not always align well with the new browser APIs that DASH clients are built on. +Our goal is to reduce this friction and make it easier to adopt these emerging +web standards for streaming, without falling back to plugins. + +We support both ISO BMFF (MP4) and WebM files (even in the same manifest), +WebVTT for subtitles and captions, both clear and encrypted content, and +multiple audio and subtitle languages (even in the same manifest). +And best of all, it's free! + +[DASH]: http://dashif.org/ +[HTML5 video]: http://www.html5rocks.com/en/tutorials/video/basics/ +[MediaSource Extensions]: http://w3c.github.io/media-source/ +[Encrypted Media Extensions]: https://w3c.github.io/encrypted-media/ + + +## Dependencies ## + +Most of the tools you need to work on the Shaka Player are included in the +sources, including the [Closure Compiler][], [gjslint][], [JSDoc][], and +[Jasmine][]. + +[Closure Compiler]: https://developers.google.com/closure/compiler/ +[gjslint]: https://developers.google.com/closure/utilities/docs/linter_howto +[JSDoc]: http://usejsdoc.org/ +[Jasmine]: http://jasmine.github.io/2.1/introduction.html + + +## Documentation ## + +We have detailed documentation which is generated from the sources using JSDoc. +A pre-rendered version of this documentation is available on the web at +http://shaka-player-demo.appspot.com/docs/index.html . This will be updated +with each release, but you can generate the same docs yourself at any time: +```Shell +./build/docs.sh +``` + +If you are new to the project, we recommend you start by browsing the docs, +in particular [the tutorials][]. This landing page is very brief, and only +covers the most basic information about the project. + +[the tutorials]: http://shaka-player-demo.appspot.com/docs/tutorial-player.html + + +## Getting Sources ## + +Up-to-date sources can be obtained from http://github.com/google/shaka-player . + + +## Building ## + +The development process is documented in more detail [in our generated docs][], +but in short, you can build the library by running: +```Shell +./build/all.sh +``` + +[in our generated docs]: http://shaka-player-demo.appspot.com/docs/tutorial-dev.html + + +## Running ## + +The library comes with a test app that can be used to tinker with all of the +library's basic functionality. The test app (index.html and app.js in the +sources) is meant to be used by making the source folder available to a local +web server and pointing your browser at it. + +A hosted version of the test app is also available at +http://shaka-player-demo.appspot.com/ for your convenience. + + +## Updating ## + +Simply pull new sources from github and enjoy! +```Shell +git pull --rebase +``` + + +## Design Overview ## + +The main entities you care about are [shaka.player.Player][], +[shaka.player.DashVideoSource][], and [shaka.player.DrmSchemeInfo][]. +In short, you construct a player and give it a \ tag, then you +construct a DASH video source and give it a manifest URL and an optional DRM +callback. Your DRM callback returns DrmSchemeInfo objects to describe your +DRM setup. You load this video source into the player to begin playback. + +The player handles high-level playback and DRM, while the video source deals +with streaming and all of the low-level parts of adaptive playback. The DRM +scheme info is an explicit set of parameters for DRM, and contains everything +the library can't glean from a DASH manifest. + +More detailed information and walkthroughs with fully-functional sample code +can be found in [the tutorials][]. + +[shaka.player.Player]: http://shaka-player-demo.appspot.com/docs/shaka.player.Player.html +[shaka.player.DashVideoSource]: http://shaka-player-demo.appspot.com/docs/shaka.player.DashVideoSource.html +[shaka.player.DrmSchemeInfo]: http://shaka-player-demo.appspot.com/docs/shaka.player.DrmSchemeInfo.html +[the tutorials]: http://shaka-player-demo.appspot.com/docs/tutorial-player.html + + +## Contributing ## + +If you have improvements or fixes, we would love to have your contributions. +Please read CONTRIBUTIONS.md for more information on the process we would like +contributors to follow. + diff --git a/app.js b/app.js new file mode 100644 index 0000000000..1fa4ef1fc1 --- /dev/null +++ b/app.js @@ -0,0 +1,548 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements the application layer of the test application. + */ + + +/** @class */ +var app = function() {}; + + +/** + * The video element owned by the app. + * + * @private {HTMLVideoElement} + */ +app.video_ = null; + + +/** + * The video resolution debug element owned by the app. + * + * @private {Element} + */ +app.videoResDebug_ = null; + + +/** + * True if the aspect ratio has been set for this playback. + * + * @private {boolean} + */ +app.aspectRatioSet_ = false; + + +/** + * The player object owned by the app. + * + * @private {shaka.player.Player} + */ +app.player_ = null; + + +/** + * True if polyfills have been installed. + * + * @private {boolean} + */ +app.polyfillsInstalled_ = false; + + +/** + * Initializes the application. + */ +app.init = function() { + // Set default values. + document.getElementById('forcePrefixed').checked = false; + document.getElementById('preferredLanguage').value = 'en-US'; + + document.getElementById('licenseServerUrlInput').value = + 'assets/test_license.json'; + document.getElementById('mediaUrlInput').value = 'assets/bear-av-enc.mp4'; + document.getElementById('subtitlesUrlInput').value = 'assets/test_subs.vtt'; + + document.getElementById('mpdList').value = + 'assets/car_cenc-20120827-manifest.mpd'; + + app.video_ = + /** @type {!HTMLVideoElement} */ (document.getElementById('video')); + app.videoResDebug_ = document.getElementById('videoResDebug'); + window.setInterval(app.updateVideoSize_, 50); + + var fields = location.search.split('?').pop(); + fields = fields ? fields.split(';') : []; + var params = {}; + for (var i = 0; i < fields.length; ++i) { + var kv = fields[i].split('='); + params[kv[0]] = kv[1]; + } + + if ('prefixed' in params) { + document.getElementById('forcePrefixed').checked = true; + } + if ('lang' in params) { + document.getElementById('preferredLanguage').value = params['lang']; + } + if ('nocenc' in params) { + document.getElementById('mpdList').value = + 'assets/car-20120827-manifest.mpd'; + } + if ('vp9' in params) { + document.getElementById('mpdList').value = + 'assets/feelings_vp9-20130806-manifest.mpd'; + } + if ('tng' in params) { + document.getElementById('mpdList').value = + 'assets/angel_one.mpd'; + } + if ('debug' in params && shaka.log) { + shaka.log.setLevel(shaka.log.Level.DEBUG); + } + if ('v' in params && shaka.log) { + shaka.log.setLevel(shaka.log.Level.V1); + } + + app.onMpdChange(); + + if ('dash' in params) { + document.getElementById('streamTypeList').value = 'dash'; + app.onStreamTypeChange(); + app.loadStream(); + } else if ('http' in params) { + document.getElementById('streamTypeList').value = 'http'; + app.onStreamTypeChange(); + app.loadStream(); + } +}; + + +/** + * Called when the stream type is changed. + */ +app.onStreamTypeChange = function() { + var type = document.getElementById('streamTypeList').value; + var on; + var off; + + if (type == 'http') { + on = document.getElementsByClassName('http'); + off = document.getElementsByClassName('dash'); + } else { + on = document.getElementsByClassName('dash'); + off = document.getElementsByClassName('http'); + } + + for (var i = 0; i < on.length; ++i) { + on[i].style.display = 'table-row'; + } + for (var i = 0; i < off.length; ++i) { + off[i].style.display = 'none'; + } +}; + + +/** + * Called when a new MPD is selected. + */ +app.onMpdChange = function() { + document.getElementById('manifestUrlInput').value = + document.getElementById('mpdList').value; +}; + + +/** + * Called when the custom MPD field is used. + */ +app.onMpdCustom = function() { + document.getElementById('mpdList').value = ''; +}; + + +/** + * Called when a new video track is selected. + */ +app.onVideoChange = function() { + var id = document.getElementById('videoTracks').value; + document.getElementById('adaptationEnabled').checked = false; + app.onAdaptationChange(); + app.player_.selectVideoTrack(id); +}; + + +/** + * Called when adaptation is enabled or disabled. + */ +app.onAdaptationChange = function() { + var enabled = document.getElementById('adaptationEnabled').checked; + if (app.player_) { + app.player_.enableAdaptation(enabled); + } +}; + + +/** + * Called when a new audio track is selected. + */ +app.onAudioChange = function() { + var id = document.getElementById('audioTracks').value; + app.player_.selectAudioTrack(id); +}; + + +/** + * Called when a new text track is selected or its enabled state is changed. + */ +app.onTextChange = function() { + var id = document.getElementById('textTracks').value; + var enabled = document.getElementById('textEnabled').checked; + app.player_.selectTextTrack(id); + app.player_.enableTextTrack(enabled); +}; + + +/** + * A very lazy demo function to cycle through audio tracks. + */ +app.cycleAudio = function() { + var intervalId = window.setInterval(function() { + // On EOF, the video goes into a paused state. + if (app.video_.paused) { + window.clearInterval(intervalId); + return; + } + + var audioTracks = document.getElementById('audioTracks'); + var option = audioTracks.selectedOptions[0]; + option = option.nextElementSibling || audioTracks.firstElementChild; + audioTracks.value = option.value; + app.onAudioChange(); + }, 3000); +}; + + +/** + * Loads whatever stream type is selected. + */ +app.loadStream = function() { + var type = document.getElementById('streamTypeList').value; + if (type == 'http') { + app.loadHttpStream(); + } else { + app.loadDashStream(); + } +}; + + +/** + * Loads an http stream. + */ +app.loadHttpStream = function() { + if (!app.player_) { + app.installPolyfills_(); + app.initPlayer_(); + } + + var mediaUrl = document.getElementById('mediaUrlInput').value; + var keySystem = document.getElementById('keySystemList').value; + var licenseServerUrl = document.getElementById('licenseServerUrlInput').value; + var subtitlesUrl = document.getElementById('subtitlesUrlInput').value; + var drmSchemeInfo = null; + if (keySystem) { + drmSchemeInfo = new shaka.player.DrmSchemeInfo( + keySystem, false, licenseServerUrl, false, null, null); + } + + app.load_(new shaka.player.HttpVideoSource(mediaUrl, subtitlesUrl, + drmSchemeInfo)); +}; + + +/** + * Loads a dash stream. + */ +app.loadDashStream = function() { + if (!app.player_) { + app.installPolyfills_(); + app.initPlayer_(); + } + + var mediaUrl = document.getElementById('manifestUrlInput').value; + + app.load_( + new shaka.player.DashVideoSource( + mediaUrl, + app.interpretContentProtection_)); +}; + + +/** + * Exceptions thrown in 'then' handlers are not seen until catch. + * Promises can therefore mask what would otherwise be uncaught exceptions. + * As a utility to work around this, wrap the function in setTimeout so that + * it is called outside of the Promise's 'then' handler. + * + * @param {function(...)} fn + * @return {function(...)} + * @private + */ +app.breakOutOfPromise_ = function(fn) { + return window.setTimeout.bind(window, fn, 0); +}; + + +/** + * Loads the given video source into the player. + * @param {!shaka.player.IVideoSource} videoSource + * @private + */ +app.load_ = function(videoSource) { + console.assert(app.player_ != null); + + var preferredLanguage = document.getElementById('preferredLanguage').value; + app.player_.setPreferredLanguage(preferredLanguage); + + app.player_.load(videoSource).then(app.breakOutOfPromise_( + function() { + app.displayMetadata_(); + }) + ).catch(function() {}); // Error already handled through error event. +}; + + +/** + * Displays player metadata on the page. + * @private + */ +app.displayMetadata_ = function() { + console.assert(app.player_ != null); + app.aspectRatioSet_ = false; + + // Populate video tracks. + var videoTracksList = document.getElementById('videoTracks'); + while (videoTracksList.firstChild) { + videoTracksList.removeChild(videoTracksList.firstChild); + } + var videoTracks = app.player_.getVideoTracks(); + videoTracks.sort(shaka.player.VideoTrack.compare); + for (var i = 0; i < videoTracks.length; ++i) { + var track = videoTracks[i]; + var item = document.createElement('option'); + item.textContent = track.width + 'x' + track.height + ', ' + + track.bandwidth + ' bits/s'; + item.value = track.id; + item.selected = track.active; + videoTracksList.appendChild(item); + } + + // Populate audio tracks. + var audioTracksList = document.getElementById('audioTracks'); + while (audioTracksList.firstChild) { + audioTracksList.removeChild(audioTracksList.firstChild); + } + var audioTracks = app.player_.getAudioTracks(); + audioTracks.sort(shaka.player.AudioTrack.compare); + for (var i = 0; i < audioTracks.length; ++i) { + var track = audioTracks[i]; + var item = document.createElement('option'); + item.textContent = 'language: ' + track.lang + ', ' + + track.bandwidth + ' bits/s'; + item.value = track.id; + item.selected = track.active; + audioTracksList.appendChild(item); + } + + // Populate text tracks. + var textTracksList = document.getElementById('textTracks'); + while (textTracksList.firstChild) { + textTracksList.removeChild(textTracksList.firstChild); + } + var textTracks = app.player_.getTextTracks(); + textTracks.sort(shaka.player.TextTrack.compare); + for (var i = 0; i < textTracks.length; ++i) { + var track = textTracks[i]; + var item = document.createElement('option'); + item.textContent = 'language: ' + track.lang; + item.value = track.id; + item.selected = track.active; + if (track.enabled) { + document.getElementById('textEnabled').checked = true; + } + textTracksList.appendChild(item); + } +}; + + +/** + * Requests fullscreen mode. + */ +app.requestFullscreen = function() { + if (app.player_) { + app.player_.requestFullscreen(); + } +}; + + +/** + * Update video resolution information. + * @private + */ +app.updateVideoSize_ = function() { + if (app.aspectRatioSet_ == false) { + var aspect = app.video_.videoWidth / app.video_.videoHeight; + if (aspect) { + // Round off common aspect ratios. + if (Math.abs(aspect - (16 / 9)) < 0.01) { + aspect = 16 / 9; + } else if (Math.abs(aspect - (4 / 3)) < 0.01) { + aspect = 4 / 3; + } + + // Resize the video tag to match the aspect ratio of the media. + var h = 576; + var w = h * aspect; + app.video_.width = w.toString(); + app.video_.height = h.toString(); + + app.aspectRatioSet_ = true; + } + } + + app.videoResDebug_.innerText = + app.video_.videoWidth + ' x ' + app.video_.videoHeight; +}; + + +/** + * Installs the polyfills if the have not yet been installed. + * @private + */ +app.installPolyfills_ = function() { + if (app.polyfillsInstalled_) + return; + + var forcePrefixedElement = document.getElementById('forcePrefixed'); + var forcePrefixed = forcePrefixedElement.checked; + + // Once the setting is applied, it cannot be changed. + forcePrefixedElement.disabled = true; + forcePrefixedElement.title = 'EME choice locked in for this browser session.'; + + if (forcePrefixed) { + window['MediaKeys'] = null; + window['MediaKeySession'] = null; + HTMLMediaElement.prototype['setMediaKeys'] = null; + Navigator.prototype['requestMediaKeySystemAccess'] = null; + } + + shaka.polyfill.Fullscreen.install(); + shaka.polyfill.MediaKeys.install(); + shaka.polyfill.VideoPlaybackQuality.install(); + + app.polyfillsInstalled_ = true; +}; + + +/** + * Initializes the Player instance. + * If the Player instance already exists then it is reinitialized. + * @private + */ +app.initPlayer_ = function() { + console.assert(app.player_ == null); + if (app.player_) { + return; + } + + app.player_ = + new shaka.player.Player(/** @type {!HTMLVideoElement} */ (app.video_)); + app.player_.addEventListener('error', app.onPlayerError_, false); + app.player_.addEventListener('adaptation', app.displayMetadata_, false); + + // Load the adaptation setting. + app.onAdaptationChange(); +}; + + +/** + * Called when the player generates an error. + * @param {!Event} event + * @private + */ +app.onPlayerError_ = function(event) { + console.error('Player error', event); +}; + + +/** + * Called to interpret ContentProtection elements from the MPD. + * @param {!shaka.dash.mpd.ContentProtection} contentProtection The MPD element. + * @return {shaka.player.DrmSchemeInfo} or null if the element is not + * understood by this application. + * @private + */ +app.interpretContentProtection_ = function(contentProtection) { + var StringUtils = shaka.util.StringUtils; + + var override = document.getElementById('wvLicenseServerUrlInput'); + if (override.value) { + // The user is using the test app's UI to override the MPD. + // This is useful to test external MPDs when no mapping is known in + // advance. + return new shaka.player.DrmSchemeInfo( + 'com.widevine.alpha', true, override.value, false, null, null); + } + + if (contentProtection.schemeIdUri == 'com.youtube.clearkey') { + // This is the scheme used by YouTube's MediaSource demo. + var child = contentProtection.children[0]; + var keyid = StringUtils.fromHex(child.getAttribute('keyid')); + var key = StringUtils.fromHex(child.getAttribute('key')); + var keyObj = { + kty: 'oct', + kid: StringUtils.toBase64(keyid, false), + k: StringUtils.toBase64(key, false) + }; + var jwkSet = {keys: [keyObj]}; + var license = JSON.stringify(jwkSet); + var initData = { + initData: StringUtils.toUint8Array(keyid), + initDataType: 'cenc' + }; + var licenseServerUrl = 'data:application/json;base64,' + + StringUtils.toBase64(license); + return new shaka.player.DrmSchemeInfo( + 'org.w3.clearkey', false, licenseServerUrl, false, initData, null); + } + + if (contentProtection.schemeIdUri == + 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed') { + // This is the UUID which represents Widevine in the edash-packager. + var licenseServerUrl = 'http://widevine-proxy.appspot.com/proxy'; + return new shaka.player.DrmSchemeInfo( + 'com.widevine.alpha', true, licenseServerUrl, false, null, null); + } + + console.warn('Unrecognized scheme: ' + contentProtection.schemeIdUri); + return null; +}; + + +if (document.readyState == 'complete' || + document.readyState == 'interactive') { + app.init(); +} else { + document.addEventListener('DOMContentLoaded', app.init); +} diff --git a/assets/.htaccess b/assets/.htaccess new file mode 100644 index 0000000000..733ba0edb0 --- /dev/null +++ b/assets/.htaccess @@ -0,0 +1,4 @@ +AddType text/vtt .vtt +AddCharset utf-8 .vtt +Header set Access-Control-Allow-Origin "*" +Header set Access-Control-Allow-Headers "Range" diff --git a/assets/angel_one.mpd b/assets/angel_one.mpd new file mode 100644 index 0000000000..c85bf0c10b --- /dev/null +++ b/assets/angel_one.mpd @@ -0,0 +1,92 @@ + + + http://shaka-player-demo.appspot.com/assets/angel_one/ + + + + a-en.webm + + + + + + + + a-de.webm + + + + + + + + a-es.webm + + + + + + + + a-fr.webm + + + + + + + + a-it.webm + + + + + + + + v-768x576-1M.webm + + + + + + v-768x576-500k.webm + + + + + + v-576x432-250k.webm + + + + + + v-432x324-125k.webm + + + + + + + + subs/s-en.vtt + + + + + subs/s-fr.vtt + + + + + subs/s-pt-BR.vtt + + + + + subs/s-el.vtt + + + + diff --git a/assets/angel_one.sidx b/assets/angel_one.sidx new file mode 100644 index 0000000000..4d6988e1b6 Binary files /dev/null and b/assets/angel_one.sidx differ diff --git a/assets/bear-av-enc.mp4 b/assets/bear-av-enc.mp4 new file mode 100644 index 0000000000..2054ae0739 Binary files /dev/null and b/assets/bear-av-enc.mp4 differ diff --git a/assets/car-20120827-87.sidx b/assets/car-20120827-87.sidx new file mode 100644 index 0000000000..6dc8e97773 Binary files /dev/null and b/assets/car-20120827-87.sidx differ diff --git a/assets/car-20120827-manifest.mpd b/assets/car-20120827-manifest.mpd new file mode 100644 index 0000000000..27278f8e4e --- /dev/null +++ b/assets/car-20120827-manifest.mpd @@ -0,0 +1,65 @@ + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/car-20120827-89.mp4 + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/car-20120827-88.mp4 + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/car-20120827-87.mp4 + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/car-20120827-86.mp4 + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/car-20120827-85.mp4 + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/car-20120827-160.mp4 + + + + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/car-20120827-8c.mp4 + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/car-20120827-8d.mp4 + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/car-20120827-8b.mp4 + + + + + + + diff --git a/assets/car_cenc-20120827-manifest.mpd b/assets/car_cenc-20120827-manifest.mpd new file mode 100644 index 0000000000..57fedb9977 --- /dev/null +++ b/assets/car_cenc-20120827-manifest.mpd @@ -0,0 +1,66 @@ + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/car_cenc-20120827-manifest.mpd + + + + + + + + car_cenc-20120827-89.mp4 + + + + + + car_cenc-20120827-88.mp4 + + + + + + car_cenc-20120827-87.mp4 + + + + + + car_cenc-20120827-86.mp4 + + + + + + car_cenc-20120827-85.mp4 + + + + + + + + + + + + car_cenc-20120827-8c.mp4 + + + + + + car_cenc-20120827-8d.mp4 + + + + + + car_cenc-20120827-8b.mp4 + + + + + + + diff --git a/assets/feelings_audio_only-20130806-manifest.mpd b/assets/feelings_audio_only-20130806-manifest.mpd new file mode 100644 index 0000000000..8c52da87c1 --- /dev/null +++ b/assets/feelings_audio_only-20130806-manifest.mpd @@ -0,0 +1,21 @@ + + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/feelings_vp9-20130806-171.webm + + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/feelings_vp9-20130806-172.webm + + + + + + + diff --git a/assets/feelings_vp9-20130806-171.webm.cues b/assets/feelings_vp9-20130806-171.webm.cues new file mode 100644 index 0000000000..52e50e5347 Binary files /dev/null and b/assets/feelings_vp9-20130806-171.webm.cues differ diff --git a/assets/feelings_vp9-20130806-171.webm.headers b/assets/feelings_vp9-20130806-171.webm.headers new file mode 100644 index 0000000000..db0ba61fd7 Binary files /dev/null and b/assets/feelings_vp9-20130806-171.webm.headers differ diff --git a/assets/feelings_vp9-20130806-manifest.mpd b/assets/feelings_vp9-20130806-manifest.mpd new file mode 100644 index 0000000000..97cd59d39b --- /dev/null +++ b/assets/feelings_vp9-20130806-manifest.mpd @@ -0,0 +1,59 @@ + + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/feelings_vp9-20130806-171.webm + + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/feelings_vp9-20130806-172.webm + + + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/feelings_vp9-20130806-242.webm + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/feelings_vp9-20130806-243.webm + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/feelings_vp9-20130806-244.webm + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/feelings_vp9-20130806-245.webm + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/feelings_vp9-20130806-246.webm + + + + + + http://yt-dash-mse-test.commondatastorage.googleapis.com/media/feelings_vp9-20130806-247.webm + + + + + + + diff --git a/assets/generate_jwkset.py b/assets/generate_jwkset.py new file mode 100755 index 0000000000..638ef60dcc --- /dev/null +++ b/assets/generate_jwkset.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import binascii +import sys + +#TEST_KEY_ID = "0123456789012345" +#TEST_KEY = "ebdd62f16814d27b68ef122afce4ae3c" + +key_id = base64.b64encode(sys.argv[1]).rstrip("=") +key = base64.b64encode(binascii.unhexlify(sys.argv[2])).rstrip("=") + +jwk = '{"kty":"oct","kid":"%s","k":"%s"}' % (key_id, key) +jwk_set = '{"keys":[%s]}' % jwk +sys.stdout.write(jwk_set) diff --git a/assets/poster.jpg b/assets/poster.jpg new file mode 100644 index 0000000000..611fabf76e Binary files /dev/null and b/assets/poster.jpg differ diff --git a/assets/poster.license b/assets/poster.license new file mode 100644 index 0000000000..d7bbbda9aa --- /dev/null +++ b/assets/poster.license @@ -0,0 +1,7 @@ +Shaka poster image from: + http://en.wikipedia.org/wiki/Shaka_sign#mediaviewer/File:Gesture_raised_fist_with_thumb_and_pinky_lifted.jpg +Author: + http://en.wikipedia.org/wiki/User:Jeremykemp +License: + Creative Commons Share-Alike 3.0 + http://creativecommons.org/licenses/by-sa/3.0/ diff --git a/assets/test_license.json b/assets/test_license.json new file mode 100644 index 0000000000..d9ef94b883 --- /dev/null +++ b/assets/test_license.json @@ -0,0 +1 @@ +{"keys":[{"kty":"oct","kid":"MDEyMzQ1Njc4OTAxMjM0NQ","k":"691i8WgU0nto7xIq/OSuPA"}]} \ No newline at end of file diff --git a/assets/test_subs.vtt b/assets/test_subs.vtt new file mode 100644 index 0000000000..dd2b68e8b3 --- /dev/null +++ b/assets/test_subs.vtt @@ -0,0 +1,5 @@ +WEBVTT FILE + +1 +00:00:00.500 --> 00:00:02.000 D:vertical A:start +The Web is always changing diff --git a/build/all.sh b/build/all.sh new file mode 100755 index 0000000000..4faaa5344f --- /dev/null +++ b/build/all.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +dir=$(dirname $0)/.. + +set -e + +"$dir"/build/gendeps.sh +"$dir"/build/build.sh +"$dir"/build/lint.sh diff --git a/build/audit_todo.sh b/build/audit_todo.sh new file mode 100755 index 0000000000..a924414d5b --- /dev/null +++ b/build/audit_todo.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +dir=$(dirname $0)/.. + +set -e + +cd "$dir" + +# Find TODO and FIXME comments that are not annotated with a story ID. +grep --color=auto -Pr '(TODO|FIXME)(?!.*story)' \ + lib/ \ + app.js \ + load.js \ + index.* \ + spec/ \ + spec_runner.html \ + support.html + diff --git a/build/build.sh b/build/build.sh new file mode 100755 index 0000000000..8f0ef0be22 --- /dev/null +++ b/build/build.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +dir=$(dirname $0)/.. +. "$dir"/build/lib.sh + +set -e + +# This was the old name. +rm -f "$dir"/lib.js{,.map} + +# These are the new names. +rm -f "$dir"/shaka-player.compiled.debug.{js,map} +rm -f "$dir"/shaka-player.compiled.js + +# Compile once with app.js so that it gets checked. Don't keep the output. +(library_sources_0; closure_sources_0) | compile_0 \ + --summary_detail_level 3 "$dir"/app.js > /dev/null +# NOTE: --js_output_file /dev/null results in a non-zero return value and +# stops execution of this script. + +# Compile without app.js and output the minified library only. +(library_sources_0; closure_sources_0) | compile_0 \ + --create_source_map "$dir"/shaka-player.compiled.debug.map \ + --js_output_file "$dir"/shaka-player.compiled.debug.js + +# Fork the non-debug version before appending debug info. +cp "$dir"/shaka-player.compiled{.debug,}.js + +# Add a special source-mapping comment so that Chrome and Firefox can map line +# and character numbers from the compiled library back to the original source +# locations. +echo "//# sourceMappingURL=shaka-player.compiled.debug.map" >> \ + "$dir"/shaka-player.compiled.debug.js + diff --git a/build/check.sh b/build/check.sh new file mode 100755 index 0000000000..a8d9278406 --- /dev/null +++ b/build/check.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +dir=$(dirname $0)/.. + +set -e + +"$dir"/build/build.sh +"$dir"/build/lint.sh + +echo "Compiled binary is $(cat "$dir"/shaka-player.compiled.js | wc -c) bytes." diff --git a/build/docs.sh b/build/docs.sh new file mode 100755 index 0000000000..4dd29da2bb --- /dev/null +++ b/build/docs.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +dir=$(dirname $0)/.. +. "$dir"/build/lib.sh + +set -e + +cd "$dir" +rm -rf docs/api +./third_party/jsdoc/jsdoc -c jsdoc.conf.json -R docs/mainpage.md diff --git a/build/gendeps.sh b/build/gendeps.sh new file mode 100755 index 0000000000..c58fde587d --- /dev/null +++ b/build/gendeps.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +dir=$(dirname $0)/.. + +set -e + +cd "$dir" +python third_party/closure/deps/depswriter.py \ + --root_with_prefix="lib ../../../lib" \ + --root_with_prefix="third_party/closure ../../../third_party/closure" \ + > third_party/closure/goog/deps.js diff --git a/build/lib.sh b/build/lib.sh new file mode 100644 index 0000000000..4eac7acdce --- /dev/null +++ b/build/lib.sh @@ -0,0 +1,88 @@ +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +dir=$(dirname $0)/.. + +closure_opts=" + --language_in ECMASCRIPT5 + --language_out ECMASCRIPT5 + + --jscomp_error=accessControls + --jscomp_error=ambiguousFunctionDecl + --jscomp_error=checkDebuggerStatement + --jscomp_error=checkRegExp + --jscomp_error=checkTypes + --jscomp_error=checkVars + --jscomp_error=const + --jscomp_error=constantProperty + --jscomp_error=deprecated + --jscomp_error=duplicate + --jscomp_error=es5Strict + --jscomp_error=externsValidation + --jscomp_error=fileoverviewTags + --jscomp_error=globalThis + --jscomp_error=internetExplorerChecks + --jscomp_error=invalidCasts + --jscomp_error=missingProperties + --jscomp_error=nonStandardJsDocs + --jscomp_error=strictModuleDepCheck + --jscomp_error=suspiciousCode + --jscomp_error=undefinedNames + --jscomp_error=undefinedVars + --jscomp_error=unknownDefines + --jscomp_error=uselessCode + --jscomp_error=visibility + + --extra_annotation_name=listens + + -O ADVANCED + --generate_exports + --output_wrapper='(function(){%output%}.bind(window))()' + + -D COMPILED=true + -D goog.DEBUG=false + -D goog.STRICT_MODE_COMPATIBLE=true + -D goog.ENABLE_DEBUG_LOADER=false + -D shaka.asserts.ENABLE_ASSERTS=false + -D shaka.log.MAX_LOG_LEVEL=0 +" + +set -e + +function library_sources_0() { + find \ + "$dir"/lib \ + "$dir"/externs \ + -name '*.js' -print0 +} + +function closure_sources_0() { + find \ + "$dir"/third_party/closure \ + -name '*.js' -print0 +} + +function compile_0() { + xargs -0 java -jar "$dir"/third_party/closure/compiler.jar $closure_opts "$@" +} + +function lint_0() { + # Allow JSDoc3 tags not normally recognized by the linter, but be strict + # otherwise. + jsdoc3_tags=static,summary,namespace,event,description,property,fires,listens + xargs -0 "$dir"/third_party/gjslint/gjslint \ + --custom_jsdoc_tags $jsdoc3_tags \ + --strict "$@" 1>&2 +} + diff --git a/build/lint.sh b/build/lint.sh new file mode 100755 index 0000000000..14e6e01d05 --- /dev/null +++ b/build/lint.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +dir=$(dirname $0)/.. +. "$dir"/build/lib.sh + +set -e + +library_sources_0 | lint_0 "$dir"/app.js diff --git a/docs/mainpage.md b/docs/mainpage.md new file mode 100644 index 0000000000..54bbfcf385 --- /dev/null +++ b/docs/mainpage.md @@ -0,0 +1,28 @@ +## Welcome to the Shaka Player documentation! + +The Shaka Player library is meant be compiled before deployment. The compiled +library will only have some symbols exported. Others will be inaccessible from +the compiled bundle. + +Because of this, these API docs can be filtered to show you what is and isn't +accessible. The combo box in the top-right corner of the page lets you select +different views of the library. You can choose the "exported", "public", or +"everything" view. + +"Exported" means everything which is available outside the compiled library. +In all modes, exported symbols are shown in red. + +"Public" means everything which is public in the sources. This is used in the +sense of [public/protected/private](http://goo.gl/jg5iKD) in object-oriented +programming languages. + +"Everything" shows all symbols, even private ones. + +Whatever view you choose will be stored by the browser across page loads and +sessions, so you should not have to keep setting it as you browse the docs. + + +## What Next? + +If you're not sure where to go next, try the Tutorials on the right. Enjoy! + diff --git a/docs/reference.html b/docs/reference.html new file mode 100644 index 0000000000..8e45bdcea9 --- /dev/null +++ b/docs/reference.html @@ -0,0 +1,38 @@ + + + + + External reference docs + + +

+ These documents are copyrighted by their respective authors. + Links are provided for convenience. +

+ + + diff --git a/externs/fullscreen.js b/externs/fullscreen.js new file mode 100644 index 0000000000..f0a30b2c50 --- /dev/null +++ b/externs/fullscreen.js @@ -0,0 +1,21 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Externs for prefixed fullscreen methods. + * @externs + */ + + +HTMLMediaElement.prototype.mozRequestFullscreen = function() {}; diff --git a/externs/htmltrackelement.js b/externs/htmltrackelement.js new file mode 100644 index 0000000000..5c73c24d84 --- /dev/null +++ b/externs/htmltrackelement.js @@ -0,0 +1,50 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview HTMLTrackElement externs. + * @externs + */ + + + +/** + * @constructor + * @extends {HTMLElement} + */ +function HTMLTrackElement() {} + + +/** @type {string} */ +HTMLTrackElement.prototype.kind; + + +/** @type {string} */ +HTMLTrackElement.prototype.src; + + +/** @type {string} */ +HTMLTrackElement.prototype.srclang; + + +/** @type {string} */ +HTMLTrackElement.prototype.label; + + +/** @type {boolean} */ +HTMLTrackElement.prototype.default; + + +/** @const {TextTrack} */ +HTMLTrackElement.prototype.track; diff --git a/externs/jwk_set.js b/externs/jwk_set.js new file mode 100644 index 0000000000..df0fd5244f --- /dev/null +++ b/externs/jwk_set.js @@ -0,0 +1,53 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Externs for JWK set. + * @externs + */ + + + +/** + * A JSON Web Key set. + * + * @constructor + * @struct + */ +function JWKSet() { + /** @type {Array.} */ + this.keys = []; +} + + + +/** + * A JSON Web Key. + * + * @constructor + * @struct + */ +function JWK() { + /** + * A key in hex. + * @type {string} + */ + this.k = ''; + + /** + * A key ID in hex. + * @type {string} + */ + this.kid = ''; +} diff --git a/externs/mediakeys.js b/externs/mediakeys.js new file mode 100644 index 0000000000..120f05b260 --- /dev/null +++ b/externs/mediakeys.js @@ -0,0 +1,237 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview MediaKey externs. + * Based on the Dec 1, 2014 draft of the EME spec. + * @externs + */ + + +/** @typedef {ArrayBufferView|ArrayBuffer} */ +var BufferSource; + + +/** @typedef {!Object.} */ +var MediaKeyStatuses; + + +/** + * @param {string} keySystem + * @param {Array.=} opt_supportedConfigurations + * @return {!Promise.} + */ +Navigator.prototype.requestMediaKeySystemAccess = + function(keySystem, opt_supportedConfigurations) {}; + + +/** + * @type {MediaKeys} + * @const + */ +HTMLMediaElement.prototype.mediaKeys; + + +/** + * NOTE: Not yet implemented as of Chrome 40. Type may be incorrect. + * @type {string} + * @const + */ +HTMLMediaElement.prototype.waitingFor; + + +/** + * @param {MediaKeys} mediaKeys + * @return {!Promise} + */ +HTMLMediaElement.prototype.setMediaKeys = function(mediaKeys) {}; + + + +/** @interface */ +function MediaKeySystemAccess() {} + + +/** @return {!Promise.} */ +MediaKeySystemAccess.prototype.createMediaKeys = function() {}; + + +/** + * NOTE: Not yet implemented as of Chrome 40. Type may be incorrect. + * @return {Object} + */ +MediaKeySystemAccess.prototype.getConfiguration = function() {}; + + +/** @type {string} */ +MediaKeySystemAccess.prototype.keySystem; + + + +/** @interface */ +function MediaKeys() {} + + +/** + * @param {string=} opt_sessionType + * @return {!MediaKeySession} + * @throws {TypeError} if opt_sessionType is invalid. + */ +MediaKeys.prototype.createSession = function(opt_sessionType) {}; + + +/** + * @param {BufferSource} serverCertificate + * @return {!Promise} + */ +MediaKeys.prototype.setServerCertificate = function(serverCertificate) {}; + + + +/** + * @interface + * @extends {EventTarget} + */ +function MediaKeySession() {} + + +/** + * @type {string} + * @const + */ +MediaKeySession.prototype.sessionId; + + +/** + * @type {number} + * @const + */ +MediaKeySession.prototype.expiration; + + +/** + * @type {!Promise} + * @const + */ +MediaKeySession.prototype.closed; + + +/** + * NOTE: Not yet implemented as of Chrome 40. Type may be incorrect. + * @type {!MediaKeyStatuses} + * @const + */ +MediaKeySession.prototype.keyStatuses; + + +/** + * @param {string} initDataType + * @param {BufferSource} initData + * @return {!Promise} + */ +MediaKeySession.prototype.generateRequest = function(initDataType, initData) {}; + + +/** + * @param {string} sessionId + * @return {!Promise.}} + */ +MediaKeySession.prototype.load = function(sessionId) {}; + + +/** + * @param {BufferSource} response + * @return {!Promise} + */ +MediaKeySession.prototype.update = function(response) {}; + + +/** + * @return {!Promise} + */ +MediaKeySession.prototype.close = function() {}; + + +/** @return {!Promise} */ +MediaKeySession.prototype.remove = function() {}; + + +/** @override */ +MediaKeySession.prototype.addEventListener = + function(type, listener, useCapture) {}; + + +/** @override */ +MediaKeySession.prototype.removeEventListener = + function(type, listener, useCapture) {}; + + +/** @override */ +MediaKeySession.prototype.dispatchEvent = function(evt) {}; + + + +/** + * @constructor + * @param {string} type + * @param {Object=} opt_eventInitDict + * @extends {Event} + */ +function MediaKeyMessageEvent(type, opt_eventInitDict) {} + + +/** + * @type {string} + * @const + */ +MediaKeyMessageEvent.prototype.messageType; + + +/** + * @type {!ArrayBuffer} + * @const + */ +MediaKeyMessageEvent.prototype.message; + + +/** + * @type {!MediaKeySession} + * @const + */ +MediaKeyMessageEvent.prototype.target; + + + +/** + * @constructor + * @param {string} type + * @param {Object=} opt_eventInitDict + * @extends {Event} + */ +function MediaEncryptedEvent(type, opt_eventInitDict) {} + + +/** + * @type {string} + * @const + */ +MediaEncryptedEvent.prototype.initDataType; + + +/** + * @type {ArrayBuffer} + * @const + */ +MediaEncryptedEvent.prototype.initData; + diff --git a/externs/prefixed_eme.js b/externs/prefixed_eme.js new file mode 100644 index 0000000000..261631e0c3 --- /dev/null +++ b/externs/prefixed_eme.js @@ -0,0 +1,133 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Externs for prefixed EME v0.1b. + * @externs + */ + + +/** + * @param {string} keySystem + * @param {Uint8Array} key + * @param {Uint8Array} keyId + * @param {string} sessionId + */ +HTMLMediaElement.prototype.webkitAddKey = + function(keySystem, key, keyId, sessionId) {}; + + +/** + * @param {string} keySystem + * @param {string} sessionId + */ +HTMLMediaElement.prototype.webkitCancelKeyRequest = + function(keySystem, sessionId) {}; + + +/** + * @param {string} keySystem + * @param {!Uint8Array} initData + */ +HTMLMediaElement.prototype.webkitGenerateKeyRequest = + function(keySystem, initData) {}; + + +/** + * @param {string} mimeType + * @param {string=} opt_keySystem + * @return {string} '', 'maybe', or 'probably' + */ +HTMLVideoElement.prototype.canPlayType = + function(mimeType, opt_keySystem) {}; + + + +/** + * @constructor + * @param {string} type + * @param {Object=} opt_eventInitDict + * @extends {Event} + */ +function MediaKeyEvent(type, opt_eventInitDict) {} + + +/** + * @type {string} + * @const + */ +MediaKeyEvent.prototype.keySystem; + + +/** + * @type {string} + * @const + */ +MediaKeyEvent.prototype.sessionId; + + +/** + * @type {Uint8Array} + * @const + */ +MediaKeyEvent.prototype.initData; + + +/** + * @type {Uint8Array} + * @const + */ +MediaKeyEvent.prototype.message; + + +/** + * @type {string} + * @const + */ +MediaKeyEvent.prototype.defaultURL; + + +/** + * @type {MediaKeyError} + * @const + */ +MediaKeyEvent.prototype.errorCode; + + +/** + * @type {number} + * @const + */ +MediaKeyEvent.prototype.systemCode; + + +/** + * @type {!HTMLMediaElement} + * @const + */ +MediaKeyEvent.prototype.target; + + + +/** @constructor */ +function MediaKeyError() {} + + +/** @type {number} */ +MediaKeyError.prototype.code; + + +/** @type {number} */ +MediaKeyError.prototype.systemCode; + diff --git a/externs/promise.js b/externs/promise.js new file mode 100644 index 0000000000..f882d45caf --- /dev/null +++ b/externs/promise.js @@ -0,0 +1,26 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Promise externs missing from closure. + * @externs + */ + + +/** + * @param {function(!Error)} fn + * @return {!Promise} + */ +Promise.prototype.catch = function(fn) {}; + diff --git a/externs/videoplaybackquality.js b/externs/videoplaybackquality.js new file mode 100644 index 0000000000..d6df912dfa --- /dev/null +++ b/externs/videoplaybackquality.js @@ -0,0 +1,50 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Externs for VideoPlaybackQuality. + * @externs + */ + + + +/** + * @constructor + */ +function VideoPlaybackQuality() {} + + +/** @type {number} */ +VideoPlaybackQuality.prototype.creationTime; + + +/** @type {number} */ +VideoPlaybackQuality.prototype.corruptedVideoFrames; + + +/** @type {number} */ +VideoPlaybackQuality.prototype.droppedVideoFrames; + + +/** @type {number} */ +VideoPlaybackQuality.prototype.totalFrameDelay; + + +/** @type {number} */ +VideoPlaybackQuality.prototype.totalVideoFrames; + + +/** @return {VideoPlaybackQuality} */ +HTMLVideoElement.prototype.getVideoPlaybackQuality = function() {}; + diff --git a/index.css b/index.css new file mode 100644 index 0000000000..49c234c3c3 --- /dev/null +++ b/index.css @@ -0,0 +1,43 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +table { + border: 3px solid navy; + border-collapse: collapse; +} + +td { + padding: 2px; +} + +td.heading { + font-weight: bold; + text-decoration: underline; + font-size: 120%; +} + +tr { + padding: 2px; +} + +#preferredLanguage { + width: 50px; +} + +.http { + display: none; +} + diff --git a/index.html b/index.html new file mode 100644 index 0000000000..036d955252 --- /dev/null +++ b/index.html @@ -0,0 +1,158 @@ + + + + + + Shaka Player Test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Stream Setup
Force prefixed EME?
Stream type: + +
Media URL:
Key system: + +
License server URL:
Subtitles URL:
Preferred language:
Test manifest: + +
Custom manifest URL: + +
+ Custom WV license server URL: + [?] + + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Stream info
Available video tracks: + Enable adaptation? + +
Available audio tracks:
Available text tracks: + Enable subs? + +
Active resolution: + +
+ +
+ +
+ +
+ + diff --git a/jsdoc.conf.json b/jsdoc.conf.json new file mode 100644 index 0000000000..b089eec37f --- /dev/null +++ b/jsdoc.conf.json @@ -0,0 +1,16 @@ +{ + "tags": { + /* This reverses the preferred order of tag dictionaries so that + * closure's version of tag interpretation is used over jsdoc's. */ + "dictionaries": [ "closure", "jsdoc" ] + }, + "source": { + "include": [ "lib" ] + }, + "opts": { + "private": true, + "recurse": true, + "tutorials": "tutorials", + "destination": "docs/api" + } +} diff --git a/lib/dash/abr_manager.js b/lib/dash/abr_manager.js new file mode 100644 index 0000000000..7b5e2c4203 --- /dev/null +++ b/lib/dash/abr_manager.js @@ -0,0 +1,265 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a manager for adaptive bitrate streaming. + */ + +goog.provide('shaka.dash.AbrManager'); + +goog.require('shaka.log'); +goog.require('shaka.player.AudioTrack'); +goog.require('shaka.player.IVideoSource'); +goog.require('shaka.player.VideoTrack'); +goog.require('shaka.util.EventManager'); +goog.require('shaka.util.IBandwidthEstimator'); + + + +/** + * Creates an AbrManager. AbrManager listens for bandwidth events and makes + * decisions about which stream should be used at any given time. It can be + * queried for the initial stream to use when starting playback, and it will + * make active stream changes during playback (if enabled). + * + * @param {!shaka.util.IBandwidthEstimator} estimator + * @param {!shaka.player.IVideoSource} videoSource + * + * @listens shaka.util.IBandwidthEstimator.BandwidthEvent + * + * @struct + * @constructor + */ +shaka.dash.AbrManager = function(estimator, videoSource) { + /** @private {!shaka.util.IBandwidthEstimator} */ + this.estimator_ = estimator; + + /** @private {!shaka.player.IVideoSource} */ + this.videoSource_ = videoSource; + + /** @private {!shaka.util.EventManager} */ + this.eventManager_ = new shaka.util.EventManager(); + + /** + * The timestamp after which we are allowed to adapt, in seconds. + * @private {number} + */ + this.nextAdaptationTime_ = (Date.now() / 1000) + + shaka.dash.AbrManager.FIRST_SWITCH_INTERVAL_; + + /** @private {boolean} */ + this.enabled_ = true; + + this.eventManager_.listen(this.estimator_, 'bandwidth', + this.onBandwidth_.bind(this)); +}; + + +/** + * The minimum amount of time that must pass before the first switch, in + * seconds. This gives the bandwidth estimator time to get some real data + * before changing anything. + * + * @private + * @const {number} + */ +shaka.dash.AbrManager.FIRST_SWITCH_INTERVAL_ = 4.0; + + +/** + * The minimum amount of time that must pass between switches, in seconds. + * This keeps us from changing too often and annoying the user. + * + * @private + * @const {number} + */ +shaka.dash.AbrManager.MIN_SWITCH_INTERVAL_ = 8.0; + + +/** + * The minimum amount of time that must pass between bandwidth evaluations, in + * seconds. This keeps us from checking for adaptation opportunities too often. + * + * @private + * @const {number} + */ +shaka.dash.AbrManager.MIN_EVAL_INTERVAL_ = 3.0; + + +/** + * The fraction of the estimated bandwidth which we should try to use when + * upgrading. + * + * @private + * @const {number} + */ +shaka.dash.AbrManager.BANDWIDTH_UPGRADE_TARGET_ = 0.85; + + +/** + * The fraction of the estimated bandwidth we should downgrade to avoid + * exceeding. + * + * @private + * @const {number} + */ +shaka.dash.AbrManager.BANDWIDTH_DOWNGRADE_TARGET_ = 0.95; + + +/** + * Destroy the AbrManager. + * + * @suppress {checkTypes} to set otherwise non-nullable types to null. + */ +shaka.dash.AbrManager.prototype.destroy = function() { + this.eventManager_.destroy(); + + this.eventManager_ = null; + this.estimator_ = null; + this.videoSource_ = null; +}; + + +/** + * Enable or disable the AbrManager. It is enabled by default when created. + * + * @param {boolean} enabled + */ +shaka.dash.AbrManager.prototype.enable = function(enabled) { + this.enabled_ = enabled; +}; + + +/** + * Decide on an initial video track to use. Called before playback begins. + * + * @return {?number} The chosen video track ID or null if there are no video + * tracks to choose. + */ +shaka.dash.AbrManager.prototype.getInitialVideoTrackId = function() { + var chosen = this.chooseVideoTrack_(); + return chosen ? chosen.id : null; +}; + + +/** + * Find the active track in the list. + * + * @param {!Array.} trackList + * @return {T} + * + * @template T + * @private + */ +shaka.dash.AbrManager.findActiveTrack_ = function(trackList) { + for (var i = 0; i < trackList.length; ++i) { + if (trackList[i].active) { + return trackList[i]; + } + } + + return null; +}; + + +/** + * Handles bandwidth update events and makes adaptation decisions. + * + * @param {!Event} event + * @private + */ +shaka.dash.AbrManager.prototype.onBandwidth_ = function(event) { + if (!this.enabled_) { + return; + } + + // Alias. + var AbrManager = shaka.dash.AbrManager; + + var now = Date.now() / 1000.0; + if (now < this.nextAdaptationTime_) { + return; + } + + var chosen = this.chooseVideoTrack_(); + + if (chosen) { + if (chosen.active) { + // We are already using the correct video track. + this.nextAdaptationTime_ = now + AbrManager.MIN_EVAL_INTERVAL_; + return; + } + + shaka.log.info('Video adaptation:', chosen); + this.videoSource_.selectVideoTrack(chosen.id, false); + } + + this.nextAdaptationTime_ = now + AbrManager.MIN_SWITCH_INTERVAL_; +}; + + +/** + * Choose a video track based on current bandwidth conditions. + * + * @return {shaka.player.VideoTrack} The chosen video track or null if there + * are no video tracks to choose. + * @private + */ +shaka.dash.AbrManager.prototype.chooseVideoTrack_ = function() { + // Alias. + var AbrManager = shaka.dash.AbrManager; + + var videoTracks = this.videoSource_.getVideoTracks(); + if (videoTracks.length == 0) { + return null; + } + + videoTracks.sort(shaka.player.VideoTrack.compare); + + var activeAudioTrack = + AbrManager.findActiveTrack_(this.videoSource_.getAudioTracks()); + var audioBandwidth = activeAudioTrack ? activeAudioTrack.bandwidth : 0; + + var bandwidth = this.estimator_.getBandwidth(); + + // Start by assuming that we will use the first track. + var chosen = videoTracks[0]; + + for (var i = 0; i < videoTracks.length; ++i) { + var track = videoTracks[i]; + var nextTrack = (i + 1 < videoTracks.length) ? + videoTracks[i + 1] : + { bandwidth: Number.POSITIVE_INFINITY }; + + // Ignore any track which is missing bandwidth info. + if (!track.bandwidth) continue; + + var minBandwidth = (track.bandwidth + audioBandwidth) / + AbrManager.BANDWIDTH_DOWNGRADE_TARGET_; + var maxBandwidth = (nextTrack.bandwidth + audioBandwidth) / + AbrManager.BANDWIDTH_UPGRADE_TARGET_; + shaka.log.v2('Bandwidth ranges:', + ((track.bandwidth + audioBandwidth) / 1e6).toFixed(3), + (minBandwidth / 1e6).toFixed(3), + (maxBandwidth / 1e6).toFixed(3)); + + if (bandwidth >= minBandwidth && bandwidth <= maxBandwidth) { + chosen = track; + if (chosen.active) break; + } + } + + return chosen; +}; + diff --git a/lib/dash/dash_stream.js b/lib/dash/dash_stream.js new file mode 100644 index 0000000000..d153c6775b --- /dev/null +++ b/lib/dash/dash_stream.js @@ -0,0 +1,789 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a DASH stream. + */ + +goog.provide('shaka.dash.DashStream'); + +goog.require('shaka.asserts'); +goog.require('shaka.dash.IDashStream'); +goog.require('shaka.dash.ISegmentIndexParser'); +goog.require('shaka.dash.IsobmffSegmentIndexParser'); +goog.require('shaka.dash.SegmentIndex'); +goog.require('shaka.dash.SourceBufferManager'); +goog.require('shaka.dash.WebmSegmentIndexParser'); +goog.require('shaka.dash.mpd'); +goog.require('shaka.log'); +goog.require('shaka.timer'); +goog.require('shaka.util.FakeEvent'); +goog.require('shaka.util.FakeEventTarget'); +goog.require('shaka.util.IBandwidthEstimator'); +goog.require('shaka.util.RangeRequest'); +goog.require('shaka.util.TypedBind'); + + +/** + * @event shaka.dash.DashStream.AdaptationEvent + * @description Fired when video or audio tracks change. + * Bubbles up through the Player. + * @property {string} type 'adaptation' + * @property {boolean} bubbles true + * @property {string} contentType 'video' or 'audio' + * @property {?{width: number, height: number}} size The resolution chosen, if + * the stream is a video stream. + * @property {number} bandwidth The stream's bandwidth requirement in bits per + * second. + * @export + */ +/** + * @event shaka.dash.DashStream.EndedEvent + * @description Fired when the stream ends. + * @property {string} type 'ended' + * @property {boolean} bubbles false + */ + + + +/** + * Creates a DashStream. A DashStream is an active representation. + * @param {!shaka.util.FakeEventTarget} parent The parent for event bubbling. + * @param {!HTMLVideoElement} video The video element. + * @param {!MediaSource} mediaSource The SourceBuffer's MediaSource parent. + * @param {!SourceBuffer} sourceBuffer The SourceBuffer. It's assumed that + * |sourceBuffer| has the same mime type as |representation|. + * @param {shaka.util.IBandwidthEstimator} estimator A bandwidth estimator to + * attach to all DASH data requests. + * + * @fires shaka.dash.DashStream.AdaptationEvent + * @fires shaka.dash.DashStream.EndedEvent + * @fires shaka.player.Player.ErrorEvent + * + * @struct + * @constructor + * @implements {shaka.dash.IDashStream} + * @extends {shaka.util.FakeEventTarget} + */ +shaka.dash.DashStream = + function(parent, video, mediaSource, sourceBuffer, estimator) { + shaka.util.FakeEventTarget.call(this, parent); + + /** @private {!HTMLVideoElement} */ + this.video_ = video; + + /** @private {!SourceBuffer} */ + this.sourceBuffer_ = sourceBuffer; + + /** @private {!shaka.dash.SourceBufferManager} */ + this.sbm_ = + new shaka.dash.SourceBufferManager(mediaSource, sourceBuffer, estimator); + + /** @private {shaka.util.IBandwidthEstimator} */ + this.estimator_ = estimator; + + /** @private {shaka.dash.mpd.Representation} */ + this.representation_ = null; + + /** @private {shaka.dash.SegmentIndex} */ + this.segmentIndex_ = null; + + /** @private {?function()} */ + this.nextSwitch_ = null; + + /** @private {?number} */ + this.updateTimerId_ = null; + + /** @private {shaka.dash.DashStream.State_} */ + this.state_ = shaka.dash.DashStream.State_.IDLE; + + /** @private {string} */ + this.type_ = ''; +}; +goog.inherits(shaka.dash.DashStream, shaka.util.FakeEventTarget); + + +/** + * @enum + * @private + */ +shaka.dash.DashStream.State_ = { + // The stream has not started yet. + IDLE: 0, + + // The stream is starting. + INITIALIZING: 1, + + // The stream is fetching metadata for the new representation and is still + // updating using the old representation. + SWITCHING: 2, + + // The stream has stopped updating using the old representation and is + // splicing in segments into the source buffer from the new representation. + SPLICING: 3, + + // The stream is updating by periodically appending segments into the + // source buffer. + UPDATING: 4, + + // The stream has ended. + ENDED: 5 +}; + + +/** + * The number of seconds of data we try to keep in buffer after initiating + * playback. Mpd.minBufferTime, if greater, will override this. + * + * @private {number} + * @const + */ +shaka.dash.DashStream.BUFFER_SIZE_SECONDS_ = 15.0; + + +/** + * The number of seconds of old data we keep in buffer when switching + * representations to avoid buffering during the switch operation. + * Mpd.minBufferTime, if greater, will override this. + * + * @private {number} + * @const + */ +shaka.dash.DashStream.SWITCH_BUFFER_SIZE_SECONDS_ = 5.0; + + +/** + * The duration of a single frame at 20 FPS. We use this value to force the + * video element to start showing new content after an immediate switch. + * + * @private + * @const {number} + */ +shaka.dash.DashStream.SINGLE_FRAME_SECONDS_ = 0.05; + + +/** + * @override + * @suppress {checkTypes} to set otherwise non-nullable types to null. + */ +shaka.dash.DashStream.prototype.destroy = function() { + this.state_ = null; + + this.cancelUpdateTimer_(); + + this.nextSwitch_ = null; + this.segmentIndex_ = null; + this.representation_ = null; + this.estimator_ = null; + + this.sbm_.destroy(); + this.sbm_ = null; + + this.sourceBuffer_ = null; + this.video_ = null; + + this.parent = null; +}; + + +/** @override */ +shaka.dash.DashStream.prototype.getRepresentation = function() { + return this.representation_; +}; + + +/** @override */ +shaka.dash.DashStream.prototype.hasEnded = function() { + return this.state_ == shaka.dash.DashStream.State_.ENDED; +}; + + +/** @override */ +shaka.dash.DashStream.prototype.start = function(representation) { + if (!this.validateRepresentation_(representation)) { + return; + } + + shaka.asserts.assert(this.state_ == shaka.dash.DashStream.State_.IDLE); + if (this.state_ != shaka.dash.DashStream.State_.IDLE) { + shaka.log.error('Cannot start stream: stream has already been started.'); + return; + } + + shaka.log.info('Starting stream for', representation); + + this.representation_ = representation; + this.type_ = representation.mimeType.split('/')[0]; + this.segmentIndex_ = null; + this.state_ = shaka.dash.DashStream.State_.INITIALIZING; + + // Request all segment metadata in parallel. + var async = this.requestAllSegmentMetadata_(representation); + + Promise.all(async).then(shaka.util.TypedBind(this, + /** @param {!Array} results */ + function(results) { + var segmentIndexData = results[0]; + var initSegmentData = results[1]; + + // Create/get SegmentIndex. + if (representation.segmentBase) { + shaka.asserts.assert(segmentIndexData); + this.segmentIndex_ = this.createSegmentIndex_( + representation, segmentIndexData, initSegmentData); + if (!this.segmentIndex_) { + var error = new Error('Failed to create SegmentIndex.'); + error.type = 'dash'; + return Promise.reject(error); + } + } else { + this.segmentIndex_ = /** @type {shaka.dash.SegmentIndex} */ ( + representation.segmentList.userData); + shaka.asserts.assert(this.segmentIndex_); + } + + // Set the SourceBuffer's timestamp offset to handle non-zero earliest + // presentation times. + var firstReference = this.segmentIndex_.getReference(0); + if (firstReference && firstReference.startTime >= 1.0) { + // If the start time is less than one then assume any offset is + // because the segment index uses DTS (decoding timestamps). + this.sourceBuffer_.timestampOffset = -firstReference.startTime; + shaka.log.v1('timestampOffset', -firstReference.startTime); + } + + var initialBufferTime = representation.minBufferTime; + var segmentRange = this.segmentIndex_.getRangeForInterval( + this.getCurrentTime_(), initialBufferTime); + if (!segmentRange) { + return Promise.reject(new Error('No segments available.')); + } + shaka.log.v1('Fetching segment range', this.type_, + segmentRange.references); + return this.sbm_.fetch(segmentRange, initSegmentData); + }) + ).then(shaka.util.TypedBind(this, + function() { + this.fireAdaptationEvent_(representation); + this.switchRepresentationOrUpdate_(); + }) + ).catch(shaka.util.TypedBind(this, + /** @param {!Error} error */ + function(error) { + if (error.type != 'aborted') { + this.state_ = shaka.dash.DashStream.State_.IDLE; + var event = shaka.util.FakeEvent.createErrorEvent(error); + this.dispatchEvent(event); + } + }) + ); +}; + + +/** @override */ +shaka.dash.DashStream.prototype.switch = function(representation, immediate) { + shaka.timer.begin('switch'); + shaka.timer.begin('switch logic'); + if (!this.validateRepresentation_(representation)) { + // Error has already been dispatched. + return; + } + + // Alias. + var DashStream = shaka.dash.DashStream; + + // We cannot switch representations if the stream has not been started. + shaka.asserts.assert(this.state_ != DashStream.State_.IDLE); + if (this.state_ == DashStream.State_.IDLE) { + shaka.log.error( + 'Cannot switch representation: stream has not been started.'); + return; + } + + // We cannot switch representations if we are initializing or already + // switching representations. + if (this.state_ == DashStream.State_.INITIALIZING || + this.state_ == DashStream.State_.SWITCHING || + this.state_ == DashStream.State_.SPLICING) { + shaka.log.info('Waiting to switch representations...'); + this.nextSwitch_ = this.switch.bind(this, representation, immediate); + return; + } + + if (representation == this.representation_) { + shaka.log.info('Ignoring switch.'); + // Nothing to do. If this was a deferred switch, the update loop is not + // running. So kick off an update to be safe. + this.onUpdate_(); + return; + } + + shaka.log.info('Switching representations to', representation); + + if (immediate && representation.height && + representation.height != this.representation_.height) { + var check = (function(video) { + if (video.videoHeight == representation.height) { + shaka.timer.end('switch'); + shaka.timer.diff('switch', 'switch logic'); + } else { + window.setTimeout(check, 50); + } + }).bind(null, this.video_); + check(); + } + + this.state_ = DashStream.State_.SWITCHING; + + // Request all segment metadata in parallel. + var async = this.requestAllSegmentMetadata_(representation); + + // If it's an immediate switch, pause the video and cancel updates until the + // switch is complete. + var previouslyPaused = this.video_.paused; + if (immediate) { + this.video_.pause(); + this.cancelUpdateTimer_(); + async.push(this.sbm_.abort()); + } + + // Save intermediate results so that we do not have to nest promises. + var initSegmentData; + + Promise.all(async).then(shaka.util.TypedBind(this, + /** @param {!Array} results */ + function(results) { + var segmentIndexData = results[0]; + initSegmentData = results[1]; + + // Create/get SegmentIndex. + if (representation.segmentBase) { + shaka.asserts.assert(segmentIndexData); + this.segmentIndex_ = this.createSegmentIndex_( + representation, segmentIndexData, initSegmentData); + if (!this.segmentIndex_) { + var error = new Error('Failed to create SegmentIndex.'); + error.type = 'dash'; + return Promise.reject(error); + } + } else { + this.segmentIndex_ = /** @type {shaka.dash.SegmentIndex} */ ( + representation.segmentList.userData); + shaka.asserts.assert(this.segmentIndex_); + } + + this.representation_ = representation; + this.type_ = representation.mimeType.split('/')[0]; + this.state_ = DashStream.State_.SPLICING; + + this.sbm_.reset(); + + this.fireAdaptationEvent_(representation); + + // Stop updating and abort |sbm_|'s current operation. This will reject + // |sbm_|'s current promise. + this.cancelUpdateTimer_(); + return this.sbm_.abort(); + }) + ).then(shaka.util.TypedBind(this, + function() { + var currentTime = this.getCurrentTime_(); + var bufferTime = Math.max(representation.minBufferTime, + DashStream.BUFFER_SIZE_SECONDS_); + // Fetch new segments to meet the buffering requirement and replace + // what's currently in buffer. + var segmentRange = this.segmentIndex_.getRangeForInterval( + this.getCurrentTime_(), + bufferTime); + if (!segmentRange) { + return Promise.reject(new Error('No segments available.')); + } + shaka.log.v1('Fetching segment range', this.type_, + segmentRange.references); + return this.sbm_.fetch(segmentRange, initSegmentData); + }) + ).then(shaka.util.TypedBind(this, + function() { + if (immediate) { + // Force the video to start presenting the new segment(s). + this.video_.currentTime -= DashStream.SINGLE_FRAME_SECONDS_; + if (!previouslyPaused) { + this.video_.play(); + } + } + shaka.timer.end('switch logic'); + this.switchRepresentationOrUpdate_(); + }) + ).catch(shaka.util.TypedBind(this, + /** @param {!Error} error */ + function(error) { + if (error.type != 'aborted') { + var event = shaka.util.FakeEvent.createErrorEvent(error); + this.dispatchEvent(event); + + // Try to recover. + this.state_ = DashStream.State_.UPDATING; + this.onUpdate_(); + } + }) + ); +}; + + +/** + * Fires a shaka.dash.DashStream.AdaptationEvent for a given representation. + * + * @param {shaka.dash.mpd.Representation} representation + * @private + */ +shaka.dash.DashStream.prototype.fireAdaptationEvent_ = + function(representation) { + var contentType = representation.mimeType.split('/')[0]; + var size = (contentType != 'video') ? null : { + 'width': representation.width, + 'height': representation.height + }; + var event = shaka.util.FakeEvent.create({ + 'type': 'adaptation', + 'bubbles': true, + 'contentType': contentType, + 'size': size, + 'bandwidth': representation.bandwidth + }); + this.dispatchEvent(event); +}; + + +/** + * Validates the given |representation|. If |representation| is valid then + * return true; otherwise, dispatch an error event and return false. + * + * @param {shaka.dash.mpd.Representation} representation + * @return {boolean} + * + * @private + */ +shaka.dash.DashStream.prototype.validateRepresentation_ = function( + representation) { + var hasSegmentBase = representation.segmentBase && + representation.segmentBase.representationIndex && + representation.segmentBase.representationIndex.range && + representation.segmentBase.mediaUrl; + + var hasSegmentList = representation.segmentList && + representation.segmentList.userData; + + if (!hasSegmentBase && !hasSegmentList) { + var error = new Error('Missing critical segment information.'); + error.type = 'mpd'; + + var event = shaka.util.FakeEvent.createErrorEvent(error); + this.dispatchEvent(event); + + return false; + } + + return true; +}; + + +/** + * Calls |nextSwitch_| if it's non-null; otherwise, calls onUpdate_(). + * @private + */ +shaka.dash.DashStream.prototype.switchRepresentationOrUpdate_ = function() { + // Alias. + var DashStream = shaka.dash.DashStream; + + shaka.asserts.assert(this.state_ == DashStream.State_.INITIALIZING || + this.state_ == DashStream.State_.SPLICING); + + + // Note that |state_| must be set to UPDATING before switchRepresentation_() + // is called. + this.state_ = DashStream.State_.UPDATING; + + if (this.nextSwitch_) { + shaka.log.info('Processing deferred switch...'); + var f = this.nextSwitch_; + this.nextSwitch_ = null; + f(); + } else { + this.onUpdate_(); + } +}; + + +/** @override */ +shaka.dash.DashStream.prototype.resync = function() { + // Alias. + var DashStream = shaka.dash.DashStream; + + shaka.asserts.assert(this.state_ != DashStream.State_.IDLE); + if (this.state_ == DashStream.State_.IDLE) { + shaka.log.error('Cannot resync stream: stream has not been initialized.'); + return; + } + + if (this.state_ == DashStream.State_.INITIALIZING || + this.state_ == DashStream.State_.SWITCHING || + this.state_ == DashStream.State_.SPLICING) { + // Since the stream is initializing or switching it will be resynchronized + // after the first call to onUpdate_(). + return; + } + + // Stop updating and abort |sbm_|'s current operation. This will reject + // |sbm_|'s current promise. + this.cancelUpdateTimer_(); + this.sbm_.abort().then(shaka.util.TypedBind(this, + function() { + this.state_ = DashStream.State_.UPDATING; + this.onUpdate_(); + }) + ); +}; + + +/** @override */ +shaka.dash.DashStream.prototype.setEnabled = function(enabled) { + // NOP, not supported for audio and video streams. +}; + + +/** @override */ +shaka.dash.DashStream.prototype.getEnabled = function() { + return true; +}; + + +/** + * Requests all segment metadata for the given representation. + * @param {!shaka.dash.mpd.Representation} representation + * @return {!Array.|!Promise.>} The first result + * contains the segment index data or null. The second result contains the + * initialization segment data or null. + * @private + */ +shaka.dash.DashStream.prototype.requestAllSegmentMetadata_ = function( + representation) { + var async; + + if (representation.segmentBase) { + return [ + this.requestSegmentMetadata_( + representation.segmentBase.representationIndex), + this.requestSegmentMetadata_( + representation.segmentBase.initialization)]; + } else { + return [ + Promise.resolve(null), + this.requestSegmentMetadata_( + representation.segmentList.initialization)]; + } +}; + + +/** + * Requests either a segment index or an initialization segment. + * @param {shaka.dash.mpd.RepresentationIndex| + * shaka.dash.mpd.Initialization} urlTypeObject + * @return {!Promise.|!Promise.} + * @private + */ +shaka.dash.DashStream.prototype.requestSegmentMetadata_ = function( + urlTypeObject) { + if (!urlTypeObject || !urlTypeObject.url) { + shaka.log.debug('No metadata to fetch.'); + return Promise.resolve(null); + } + + var range = urlTypeObject.range; + if (!range) { + range = { begin: 0, end: null }; + } + + var urlString = urlTypeObject.url.toString(); + var request = new shaka.util.RangeRequest(urlString, range.begin, range.end); + request.estimator = this.estimator_; + return request.send(); +}; + + +/** + * Creates a SegmentIndex. |representation| must contain a SegmentBase. + * @param {shaka.dash.mpd.Representation} representation + * @param {!ArrayBuffer} segmentIndexData The segment index data. + * @param {ArrayBuffer} initSegmentData The initialization segment data. + * @return {shaka.dash.SegmentIndex} + * @private + */ +shaka.dash.DashStream.prototype.createSegmentIndex_ = function( + representation, segmentIndexData, initSegmentData) { + shaka.asserts.assert(representation.segmentBase); + shaka.asserts.assert(representation.segmentBase.mediaUrl); + + /** @type {shaka.dash.ISegmentIndexParser} */ + var indexParser = null; + + if (representation.mimeType.indexOf('mp4') >= 0) { + indexParser = new shaka.dash.IsobmffSegmentIndexParser( + /** @type {!goog.Uri} */ (representation.segmentBase.mediaUrl)); + } else if (representation.mimeType.indexOf('webm') >= 0) { + if (!initSegmentData) { + shaka.log.error('Cannot create segment index: initialization segment ' + + 'required for WebM.'); + return null; + } + indexParser = new shaka.dash.WebmSegmentIndexParser( + /** @type {!goog.Uri} */ (representation.segmentBase.mediaUrl)); + } else { + shaka.log.error('Cannot create segment index: unsupported mime type.'); + return null; + } + shaka.asserts.assert(indexParser); + + var initSegmentDataView = + initSegmentData ? new DataView(initSegmentData) : null; + var segmentIndexDataView = new DataView(segmentIndexData); + var indexOffset = representation.segmentBase.representationIndex.range.begin; + + var references = + indexParser.parse(initSegmentDataView, segmentIndexDataView, indexOffset); + + if (!references) { + shaka.log.error('Cannot create segment index: failed to parse references.'); + return null; + } + + return new shaka.dash.SegmentIndex(references); +}; + + +/** + * Update callback. + * @private + */ +shaka.dash.DashStream.prototype.onUpdate_ = function() { + // Alias. + var DashStream = shaka.dash.DashStream; + + shaka.asserts.assert(this.representation_); + shaka.asserts.assert(this.segmentIndex_); + shaka.asserts.assert(this.state_ == DashStream.State_.SWITCHING || + this.state_ == DashStream.State_.UPDATING); + + // Avoid stacking timeouts. + this.cancelUpdateTimer_(); + + // Get the SegmentReference index and actual SegmentReference (if one exists) + // for the next unbuffered time range. + var currentTime = this.getCurrentTime_(); + var referenceIndex = this.findNextNeededIndex_(currentTime); + var reference = this.segmentIndex_.getReference(referenceIndex); + + if (!reference) { + // EOF. + shaka.log.info('EOF for ' + this.representation_.mimeType + ' stream.'); + this.state_ = DashStream.State_.ENDED; + + // Dispatch a non-bubbling event. Let the VideoSource handle it. + var event = shaka.util.FakeEvent.create({ type: 'ended' }); + this.dispatchEvent(event); + + return; + } + + var bufferingGoal = Math.max(this.representation_.minBufferTime, + DashStream.BUFFER_SIZE_SECONDS_); + var bufferedAhead = reference.startTime - currentTime; + if (bufferedAhead >= bufferingGoal) { + // We don't need to make a request right now, so check again in a second. + this.updateTimerId_ = window.setTimeout(this.onUpdate_.bind(this), 1000); + return; + } + + // Fetch and append the next segment. Only fetch a single segment, because + // fetching multiple segments could cause a buffering event when utilization + // of available bandwidth is high. If we are behind our buffering goal by + // more than one segment, we should still be able to catch up by requesting + // single segments so long as we are using an appropriate representation. + + // This operation may be interrupted by switchRepresentation_(). + shaka.log.v1('Fetching segment', this.type_, reference); + + var fetch = this.sbm_.fetch(new shaka.dash.SegmentRange([reference])); + fetch.then(shaka.util.TypedBind(this, + function() { + shaka.log.v1('Added segment', referenceIndex); + this.onUpdate_(); + }) + ).catch(shaka.util.TypedBind(this, + /** @param {!Error} error */ + function(error) { + // The fetch operation may be aborted while switching representations. + if (error.type != 'aborted') { + var event = shaka.util.FakeEvent.createErrorEvent(error); + this.dispatchEvent(event); + } + }) + ); +}; + + +/** + * Returns the index of the SegmentReference corresponding to the first + * unbuffered segment starting at |time|. + * + * @param {number} time + * @return {number} + * + * @private + */ +shaka.dash.DashStream.prototype.findNextNeededIndex_ = function(time) { + shaka.asserts.assert(this.segmentIndex_); + + var index = this.segmentIndex_.findReferenceIndex(time); + while (index >= 0 && index < this.segmentIndex_.getNumReferences()) { + if (!this.sbm_.isBuffered(index)) { + break; + } + index++; + } + + return index; +}; + + +/** + * Cancels the update timer if it is running. + * @private + */ +shaka.dash.DashStream.prototype.cancelUpdateTimer_ = function() { + if (this.updateTimerId_) { + window.clearTimeout(this.updateTimerId_); + this.updateTimerId_ = null; + } +}; + + +/** + * Gets the video's current time, offset by the earliest presentation time. + * @return {number} + * @private + */ +shaka.dash.DashStream.prototype.getCurrentTime_ = function() { + return this.video_.currentTime - this.sourceBuffer_.timestampOffset; +}; + diff --git a/lib/dash/dash_text_stream.js b/lib/dash/dash_text_stream.js new file mode 100644 index 0000000000..683abc6fc2 --- /dev/null +++ b/lib/dash/dash_text_stream.js @@ -0,0 +1,132 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a DASH stream for text tracks. + */ + +goog.provide('shaka.dash.DashTextStream'); + +goog.require('shaka.dash.IDashStream'); +goog.require('shaka.log'); +goog.require('shaka.util.FakeEventTarget'); + + + +/** + * Creates a DashTextStream. A DashTextStream is a DashStream work-alike for + * text tracks. + * + * @param {!shaka.util.FakeEventTarget} parent The parent for event bubbling. + * @param {!HTMLVideoElement} video The video element. + * @struct + * @constructor + * @implements {shaka.dash.IDashStream} + * @extends {shaka.util.FakeEventTarget} + */ +shaka.dash.DashTextStream = function(parent, video) { + shaka.util.FakeEventTarget.call(this, parent); + + /** @private {!HTMLVideoElement} */ + this.video_ = video; + + /** @private {shaka.dash.mpd.Representation} */ + this.representation_ = null; + + /** @private {HTMLTrackElement} */ + this.track_ = null; +}; +goog.inherits(shaka.dash.DashTextStream, shaka.util.FakeEventTarget); + + +/** + * @override + * @suppress {checkTypes} to set otherwise non-nullable types to null. + */ +shaka.dash.DashTextStream.prototype.destroy = function() { + if (this.track_) { + this.video_.removeChild(this.track_); + } + + this.track_ = null; + this.representation_ = null; + this.video_ = null; + this.parent = null; +}; + + +/** @override */ +shaka.dash.DashTextStream.prototype.getRepresentation = function() { + return this.representation_; +}; + + +/** @override */ +shaka.dash.DashTextStream.prototype.hasEnded = function() { + return true; +}; + + +/** @override */ +shaka.dash.DashTextStream.prototype.start = function(representation) { + this.representation_ = representation; + shaka.log.info('Starting stream for', this.representation_); + + // NOTE: Simply changing the src attribute of an existing track may result + // in both the old and new subtitles appearing simultaneously. To be safe, + // remove the old track and create a new one. + if (this.track_) { + // NOTE: When the current track is enabled, and we change tracks and + // immediately disable the new one, the new one seems to end up enabled + // anyway. To solve this, we disable the current track before removing. + this.setEnabled(false); + this.video_.removeChild(this.track_); + } + + this.track_ = /** @type {HTMLTrackElement} */ + (document.createElement('track')); + this.video_.appendChild(this.track_); + + var url = this.representation_.baseUrl.toString(); + this.track_.src = url; + + // NOTE: mode must be set after appending to the DOM. + this.setEnabled(false); +}; + + +/** @override */ +shaka.dash.DashTextStream.prototype.switch = + function(representation, immediate) { + this.start(representation); +}; + + +/** @override */ +shaka.dash.DashTextStream.prototype.resync = function() { + // NOP +}; + + +/** @override */ +shaka.dash.DashTextStream.prototype.setEnabled = function(enabled) { + this.track_.track.mode = enabled ? 'showing' : 'disabled'; +}; + + +/** @override */ +shaka.dash.DashTextStream.prototype.getEnabled = function() { + return this.track_.track.mode == 'showing'; +}; + diff --git a/lib/dash/i_dash_stream.js b/lib/dash/i_dash_stream.js new file mode 100644 index 0000000000..b1de0d36b6 --- /dev/null +++ b/lib/dash/i_dash_stream.js @@ -0,0 +1,87 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview DASH stream interface. + */ + +goog.provide('shaka.dash.IDashStream'); + +goog.require('shaka.dash.mpd'); + + + +/** + * An IDashStream is an active representation. + * + * @interface + * @extends {EventTarget} + */ +shaka.dash.IDashStream = function() {}; + + +/** + * Destroys the DashStream. + */ +shaka.dash.IDashStream.prototype.destroy = function() {}; + + +/** @return {shaka.dash.mpd.Representation} */ +shaka.dash.IDashStream.prototype.getRepresentation = function() {}; + + +/** @return {boolean} */ +shaka.dash.IDashStream.prototype.hasEnded = function() {}; + + +/** + * Start processing the stream. This should only be called once. + * An 'ended' event will be fired on EOF. + * + * @param {!shaka.dash.mpd.Representation} representation The representation. + */ +shaka.dash.IDashStream.prototype.start = function(representation) {}; + + +/** + * Switch the stream to use the given |representation|. The stream must + * already be started. + * + * @param {!shaka.dash.mpd.Representation} representation The representation. + * @param {boolean} immediate If true, switch as soon as possible. Otherwise, + * switch when convenient. + */ +shaka.dash.IDashStream.prototype.switch = + function(representation, immediate) {}; + + +/** + * Resync the stream with the video's currentTime. Called on seeking. + */ +shaka.dash.IDashStream.prototype.resync = function() {}; + + +/** + * Enable or disable the stream. Not supported for all stream types. + * + * @param {boolean} enabled + */ +shaka.dash.IDashStream.prototype.setEnabled = function(enabled) {}; + + +/** + * @return {boolean} true if the stream is enabled. + */ +shaka.dash.IDashStream.prototype.getEnabled = function() {}; + diff --git a/lib/dash/i_segment_index_parser.js b/lib/dash/i_segment_index_parser.js new file mode 100644 index 0000000000..0613d97a92 --- /dev/null +++ b/lib/dash/i_segment_index_parser.js @@ -0,0 +1,46 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview An interface for a generic segment index parser. + */ + +goog.provide('shaka.dash.ISegmentIndexParser'); + +goog.require('shaka.dash.SegmentReference'); + + + +/** + * An interface for a generic segment index parser. + * + * @interface + */ +shaka.dash.ISegmentIndexParser = function() {}; + + +/** + * Parses a segment index into segment references. + * + * @param {DataView} initSegmentData The initialization segment, or null if not + * available. Some parsers may require this. + * @param {!DataView} indexData The segment index bytes. + * @param {number} indexOffset The byte offset of the segmentIndex in the + * container. + * @return {Array.} The segment references, or + * null if an error occurred + */ +shaka.dash.ISegmentIndexParser.prototype.parse = + function(initSegmentData, indexData, indexOffset) {}; + diff --git a/lib/dash/isobmff_segment_index_parser.js b/lib/dash/isobmff_segment_index_parser.js new file mode 100644 index 0000000000..7a03048e56 --- /dev/null +++ b/lib/dash/isobmff_segment_index_parser.js @@ -0,0 +1,177 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Parses a segment index from an ISO BMFF SIDX structure. + */ + +goog.provide('shaka.dash.IsobmffSegmentIndexParser'); + +goog.require('shaka.dash.ISegmentIndexParser'); +goog.require('shaka.dash.SegmentReference'); +goog.require('shaka.log'); +goog.require('shaka.util.DataViewReader'); + + + +/** + * A parser for ISO BMFF SIDX structures. + * + * @param {!goog.Uri} mediaUrl The location of the segments, i.e., all parsed + * SegmentReferences are assumed to be reteivable from |mediaUrl|. + * + * @constructor + * @implements {shaka.dash.ISegmentIndexParser} + */ +shaka.dash.IsobmffSegmentIndexParser = function(mediaUrl) { + /** @private {!goog.Uri} */ + this.mediaUrl_ = mediaUrl; +}; + + +/** @override */ +shaka.dash.IsobmffSegmentIndexParser.prototype.parse = + function(initSegmentData, indexData, indexOffset) { + var references = null; + + try { + references = this.parseInternal_(indexData, indexOffset); + } catch (exception) { + if (!(exception instanceof RangeError)) { + throw exception; + } + } + + return references; +}; + + +/** + * Indicates the SIDX box structure. It is equal to the string 'sidx' as a + * 32-bit unsigned integer. + * @const {number} + */ +shaka.dash.IsobmffSegmentIndexParser.SIDX_INDICATOR = 0x73696478; + + +/** + * Parses the segment index from an ISO BMFF SIDX structure. + * @param {!DataView} dataView The ISO BMFF SIDX data. + * @param {number} sidxOffset The byte offset of the SIDX in the container. + * @return {Array.} The segment references, or + * null if an error occurred. + * @throws {RangeError} + * @private + * @see ISO/IEC 14496-12:2012 section 4.2 and 8.16.3 + */ +shaka.dash.IsobmffSegmentIndexParser.prototype.parseInternal_ = function( + dataView, sidxOffset) { + var reader = new shaka.util.DataViewReader( + dataView, + shaka.util.DataViewReader.Endianness.BIG_ENDIAN); + + /** @type {!Array.} */ + var references = []; + + // A SIDX structure is contained within a FullBox structure, which itself is + // contained within a Box structure. + + // Parse the Box structure. + var boxSize = reader.readUint32(); + var boxType = reader.readUint32(); + + if (boxType != shaka.dash.IsobmffSegmentIndexParser.SIDX_INDICATOR) { + shaka.log.error('Invalid box type, expected "sidx".'); + return null; + } + + if (boxSize == 1) { + boxSize = reader.readUint64(); + } + + // Parse the FullBox structure. + var version = reader.readUint8(); + + // Skip flags (24 bits) + reader.skip(3); + + // Parse the SIDX structure. + // Skip reference_ID (32 bits). + reader.skip(4); + + var timescale = reader.readUint32(); + shaka.asserts.assert(timescale != 0); + if (timescale == 0) { + shaka.log.error('Invalid timescale.'); + return null; + } + + var earliestPresentationTime; + var firstOffset; + + if (version == 0) { + earliestPresentationTime = reader.readUint32(); + firstOffset = reader.readUint32(); + } else { + earliestPresentationTime = reader.readUint64(); + firstOffset = reader.readUint64(); + } + + // Skip reserved (16 bits). + reader.skip(2); + + // Add references. + var referenceCount = reader.readUint16(); + var unscaledStartTime = earliestPresentationTime; + var startByte = sidxOffset + boxSize + firstOffset; + + for (var i = 0; i < referenceCount; i++) { + // |chunk| is 1 bit for |referenceType|, and 31 bits for |referenceSize|. + var chunk = reader.readUint32(); + var referenceType = (chunk & 0x80000000) >>> 31; + var referenceSize = chunk & 0x7FFFFFFF; + + var subsegmentDuration = reader.readUint32(); + + // |chunk| is 1 bit for |startsWithSap|, 3 bits for |sapType|, and 28 bits + // for |sapDelta|. + // TODO(story 1891508): Handle stream access point (SAP)? + chunk = reader.readUint32(); + var startsWithSap = (chunk & 0x80000000) >>> 31; + var sapType = (chunk & 0x70000000) >>> 28; + var sapDelta = chunk & 0x0FFFFFFF; + + // If |referenceType| is 1 then the reference is to another SIDX. + // We do not support this. + if (referenceType == 1) { + shaka.log.error('Heirarchical SIDXs are not supported.'); + return null; + } + + references.push( + new shaka.dash.SegmentReference( + i, + unscaledStartTime / timescale, + (unscaledStartTime + subsegmentDuration) / timescale, + startByte, + startByte + referenceSize - 1, + this.mediaUrl_)); + + unscaledStartTime += subsegmentDuration; + startByte += referenceSize; + } + + return references; +}; + diff --git a/lib/dash/mpd_parser.js b/lib/dash/mpd_parser.js new file mode 100644 index 0000000000..54e217c412 --- /dev/null +++ b/lib/dash/mpd_parser.js @@ -0,0 +1,1282 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a media presentation description object. + */ + +goog.provide('shaka.dash.mpd'); + +goog.require('goog.Uri'); +goog.require('shaka.log'); +goog.require('shaka.util.DataViewReader'); +goog.require('shaka.util.LanguageUtils'); +goog.require('shaka.util.Pssh'); +goog.require('shaka.util.StringUtils'); + + +/** + * Creates an Mpd. The MPD XML text is parsed into a tree structure. + * + * If a tag that should only exist once exists more than once, then all + * instances of that tag are ignored; for example, if a "Representation" tag + * contains more than one "SegmentBase" tag, then every "SegmentBase" tag + * contained in that "Representation" tag is ignored. + * + * Numbers, times, and byte ranges are set to null if they cannot be parsed. + * + * @param {string} source The MPD XML text. + * @param {string} url The MPD URL for relative BaseURL resolution. + * @return {shaka.dash.mpd.Mpd} + * + * @see ISO/IEC 23009-1 + */ +shaka.dash.mpd.parseMpd = function(source, url) { + var parser = new DOMParser(); + var xml = parser.parseFromString(source, 'text/xml'); + + if (!xml) { + shaka.log.error('Failed to parse MPD XML.'); + return null; + } + + // Reset the unique IDs so that IDs are predictable no matter how many MPDs + // are parsed in this browser session. + shaka.dash.mpd.nextUniqueId_ = 0; + + // Construct a virtual parent for the MPD to use in resolving relative URLs. + var parent = { baseUrl: new goog.Uri(url) }; + + return shaka.dash.mpd.parseChild_(parent, xml, shaka.dash.mpd.Mpd); +}; + + +/** @private {number} */ +shaka.dash.mpd.nextUniqueId_ = 0; + + +/** + * @private {number} + * @const + */ +shaka.dash.mpd.DEFAULT_MIN_BUFFER_TIME_ = 5.0; + + + +/** @constructor */ +shaka.dash.mpd.Mpd = function() { + /** @type {?string} */ + this.id = null; + + /** @type {?string} */ + this.type = null; + + /** @type {goog.Uri} */ + this.baseUrl = null; + + /** + * The duration in seconds. + * @type {?number} + */ + this.duration = null; + + /** + * Time in seconds that should be buffered before playback begins, to assure + * uninterrupted playback. + * @type {number} + */ + this.minBufferTime = shaka.dash.mpd.DEFAULT_MIN_BUFFER_TIME_; + + /** @type {!Array.} */ + this.periods = []; +}; + + + +/** @constructor */ +shaka.dash.mpd.Period = function() { + /** @type {?string} */ + this.id = null; + + /** + * Never seen on the Period itself, but inherited from Mpd for convenience. + * @see Mpd.minBufferTime + * @type {number} + */ + this.minBufferTime = shaka.dash.mpd.DEFAULT_MIN_BUFFER_TIME_; + + /** + * The start time in seconds. + * @type {?number} + */ + this.start = null; + + /** + * The duration in seconds. + * @type {?number} + */ + this.duration = null; + + /** @type {goog.Uri} */ + this.baseUrl = null; + + /** @type {shaka.dash.mpd.SegmentBase} */ + this.segmentBase = null; + + /** @type {shaka.dash.mpd.SegmentList} */ + this.segmentList = null; + + /** @type {shaka.dash.mpd.SegmentTemplate} */ + this.segmentTemplate = null; + + /** @type {!Array.} */ + this.adaptationSets = []; +}; + + + +/** @constructor */ +shaka.dash.mpd.AdaptationSet = function() { + /** @type {?string} */ + this.id = null; + + /** + * Never seen on the AdaptationSet itself, but inherited from Mpd for + * convenience. + * @see Mpd.minBufferTime + * @type {number} + */ + this.minBufferTime = shaka.dash.mpd.DEFAULT_MIN_BUFFER_TIME_; + + /** + * The language. + * @type {?string} + * @see IETF RFC 5646 + * @see ISO 639 + */ + this.lang = null; + + /** + * Should be 'video' or 'audio', not a MIME type. + * If not specified, will be inferred from the MIME type. + * @type {?string} + */ + this.contentType = null; + + /** @type {?number} */ + this.width = null; + + /** @type {?number} */ + this.height = null; + + /** + * If not specified, will be inferred from the first representation. + * @type {?string} + */ + this.mimeType = null; + + /** @type {?string} */ + this.codecs = null; + + /** @type {goog.Uri} */ + this.baseUrl = null; + + /** @type {shaka.dash.mpd.SegmentBase} */ + this.segmentBase = null; + + /** @type {shaka.dash.mpd.SegmentList} */ + this.segmentList = null; + + /** @type {shaka.dash.mpd.SegmentTemplate} */ + this.segmentTemplate = null; + + /** @type {!Array.} */ + this.contentProtections = []; + + /** @type {*} */ + this.userData = null; + + /** @type {!Array.} */ + this.representations = []; +}; + + + +/** @constructor */ +shaka.dash.mpd.ContentComponent = function() { + /** @type {?string} */ + this.id = null; + + /** + * The language. + * @type {?string} + * @see IETF RFC 5646 + * @see ISO 639 + */ + this.lang = null; + + /** + * Should be 'video' or 'audio', not a MIME type. + * @type {?string} + */ + this.contentType = null; +}; + + + +/** @constructor */ +shaka.dash.mpd.Representation = function() { + /** @type {?string} */ + this.id = null; + + /** + * Never seen on the Representation itself, but inherited from AdapationSet + * for convenience. + * @see AdaptationSet.lang + * @type {?string} + */ + this.lang = null; + + /** + * Never seen on the Representation itself, but inherited from Mpd for + * convenience. + * @see Mpd.minBufferTime + * @type {number} + */ + this.minBufferTime = shaka.dash.mpd.DEFAULT_MIN_BUFFER_TIME_; + + /** + * Bandwidth required, in bits per second, to assure uninterrupted playback, + * assuming that Mpd.minBufferTime seconds of video are in buffer before + * playback begins. + * @type {?number} + */ + this.bandwidth = null; + + /** @type {?number} */ + this.width = null; + + /** @type {?number} */ + this.height = null; + + /** @type {?string} */ + this.mimeType = null; + + /** @type {?string} */ + this.codecs = null; + + /** @type {goog.Uri} */ + this.baseUrl = null; + + /** @type {shaka.dash.mpd.SegmentBase} */ + this.segmentBase = null; + + /** @type {shaka.dash.mpd.SegmentList} */ + this.segmentList = null; + + /** @type {shaka.dash.mpd.SegmentTemplate} */ + this.segmentTemplate = null; + + /** @type {!Array.} */ + this.contentProtections = []; + + /** @type {*} */ + this.userData = null; + + /** + * A unique ID independent of |id| or other attributes. + * @type {number} + */ + this.uniqueId = ++shaka.dash.mpd.nextUniqueId_; +}; + + + +/** @constructor */ +shaka.dash.mpd.ContentProtection = function() { + /** + * @type {?string} + * @expose + */ + this.schemeIdUri = null; + + /** @type {?string} */ + this.value = null; + + /** @type {!Array.} */ + this.children = []; + + /** + * @type {shaka.dash.mpd.CencPssh} + * @expose + */ + this.pssh = null; +}; + + + +/** @constructor */ +shaka.dash.mpd.CencPssh = function() { + /** + * @type {Uint8Array} + * @expose + */ + this.psshBox = null; + + /** + * @type {shaka.util.Pssh} + * @expose + */ + this.parsedPssh = null; +}; + + + +/** @constructor */ +shaka.dash.mpd.BaseUrl = function() { + /** @type {?string} */ + this.url = null; +}; + + + +/** @constructor */ +shaka.dash.mpd.SegmentBase = function() { + /** + * This not an actual XML attribute of SegmentBase. It is inherited from the + * SegmentBase's parent Representation. + * @type {goog.Uri} + */ + this.baseUrl = null; + + /** + * This is not an actual XML attribute of SegmentBase. It is either inherited + * from the SegmentBase's parent Representation or generated from a + * SegmentTemplate. + * @type {goog.Uri} + */ + this.mediaUrl = null; + + /** @type {shaka.dash.mpd.Range} */ + this.indexRange = null; + + /** @type {shaka.dash.mpd.RepresentationIndex} */ + this.representationIndex = null; + + /** @type {shaka.dash.mpd.Initialization} */ + this.initialization = null; +}; + + + +/** @constructor */ +shaka.dash.mpd.RepresentationIndex = function() { + /** @type {goog.Uri} */ + this.url = null; + + /** + * Inherits the value of SegmentBase.indexRange if not specified. + * @type {shaka.dash.mpd.Range} + */ + this.range = null; +}; + + + +/** @constructor */ +shaka.dash.mpd.Initialization = function() { + /** @type {goog.Uri} */ + this.url = null; + + /** @type {shaka.dash.mpd.Range} */ + this.range = null; +}; + + + +/** @constructor */ +shaka.dash.mpd.SegmentList = function() { + /** + * This not an actual XML attribute of SegmentList. It is inherited from the + * SegmentList's parent Representation. + * @type {goog.Uri} + */ + this.baseUrl = null; + + /** @type {number} */ + this.timescale = 1; + + /** @type {number} */ + this.presentationTimeOffset = 0; + + /** @type {?number} */ + this.segmentDuration = null; + + /** @type {number} */ + this.firstSegmentNumber = 1; + + /** @type {shaka.dash.mpd.Initialization} */ + this.initialization = null; + + /** @type {!Array.} */ + this.segmentUrls = []; + + /** @type {*} */ + this.userData = null; +}; + + + +/** @constructor */ +shaka.dash.mpd.SegmentUrl = function() { + /** @type {goog.Uri} */ + this.mediaUrl = null; + + /** @type {shaka.dash.mpd.Range} */ + this.mediaRange = null; + + /** + * This is not an actual XML attribute. It is either left null or generated + * from a SegmentTemplate. + * @type {?number} + */ + this.startTime = null; + + /** + * This is not an actual XML attribute. It is either left null or generated + * from a SegmentTemplate. + * @type {?number} + */ + this.duration = null; +}; + + + +/** @constructor */ +shaka.dash.mpd.SegmentTemplate = function() { + /** @type {number} */ + this.timescale = 1; + + /** @type {number} */ + this.presentationTimeOffset = 0; + + /** @type {?number} */ + this.segmentDuration = null; + + /** @type {number} */ + this.firstSegmentNumber = 1; + + /** @type {?string} */ + this.mediaUrlTemplate = null; + + /** @type {?string} */ + this.indexUrlTemplate = null; + + /** @type {?string} */ + this.initializationUrlTemplate = null; + + /** @type {shaka.dash.mpd.SegmentTimeline} */ + this.timeline = null; +}; + + + +/** @constructor */ +shaka.dash.mpd.SegmentTimeline = function() { + /** @type {!Array.} */ + this.timePoints = []; +}; + + + +/** @constructor */ +shaka.dash.mpd.SegmentTimePoint = function() { + /** @type {?number} */ + this.startTime = null; + + /** @type {?number} */ + this.duration = null; + + /** @type {?number} */ + this.repeat = null; +}; + + + +/** + * Creates a Range. + * @param {number} begin The beginning of the range. + * @param {number} end The end of the range. + * @constructor + */ +shaka.dash.mpd.Range = function(begin, end) { + /** @type {number} */ + this.begin = begin; + + /** @type {number} */ + this.end = end; +}; + + +// MPD tag names -------------------------------------------------------------- + + +/** + * @const {string} + * @expose all TAG_NAME properties so that they do not get stripped during + * advanced compilation. + */ +shaka.dash.mpd.Mpd.TAG_NAME = 'MPD'; + + +/** @const {string} */ +shaka.dash.mpd.Period.TAG_NAME = 'Period'; + + +/** @const {string} */ +shaka.dash.mpd.AdaptationSet.TAG_NAME = 'AdaptationSet'; + + +/** @const {string} */ +shaka.dash.mpd.ContentComponent.TAG_NAME = 'ContentComponent'; + + +/** @const {string} */ +shaka.dash.mpd.Representation.TAG_NAME = 'Representation'; + + +/** @const {string} */ +shaka.dash.mpd.ContentProtection.TAG_NAME = 'ContentProtection'; + + +/** @const {string} */ +shaka.dash.mpd.CencPssh.TAG_NAME = 'cenc:pssh'; + + +/** @const {string} */ +shaka.dash.mpd.BaseUrl.TAG_NAME = 'BaseURL'; + + +/** @const {string} */ +shaka.dash.mpd.SegmentBase.TAG_NAME = 'SegmentBase'; + + +/** @const {string} */ +shaka.dash.mpd.RepresentationIndex.TAG_NAME = 'RepresentationIndex'; + + +/** @const {string} */ +shaka.dash.mpd.Initialization.TAG_NAME = 'Initialization'; + + +/** @const {string} */ +shaka.dash.mpd.SegmentList.TAG_NAME = 'SegmentList'; + + +/** @const {string} */ +shaka.dash.mpd.SegmentUrl.TAG_NAME = 'SegmentURL'; + + +/** @const {string} */ +shaka.dash.mpd.SegmentTemplate.TAG_NAME = 'SegmentTemplate'; + + +/** @const {string} */ +shaka.dash.mpd.SegmentTimeline.TAG_NAME = 'SegmentTimeline'; + + +/** @const {string} */ +shaka.dash.mpd.SegmentTimePoint.TAG_NAME = 'S'; + + +// MPD tag parsing functions -------------------------------------------------- + + +/** + * Parses an "MPD" tag. + * @param {!Object} parent A virtual parent tag containing a BaseURL which + * refers to the MPD resource itself. + * @param {!Node} elem The MPD XML element. + */ +shaka.dash.mpd.Mpd.prototype.parse = function(parent, elem) { + var mpd = shaka.dash.mpd; + + // Parse attributes. + this.id = mpd.parseAttr_(elem, 'id', mpd.parseString_); + this.type = mpd.parseAttr_(elem, 'type', mpd.parseString_); + this.duration = + mpd.parseAttr_(elem, 'mediaPresentationDuration', mpd.parseDuration_); + this.minBufferTime = + mpd.parseAttr_(elem, 'minBufferTime', mpd.parseDuration_) || + mpd.DEFAULT_MIN_BUFFER_TIME_; + + // Parse simple child elements. + var baseUrl = mpd.parseChild_(this, elem, mpd.BaseUrl); + this.baseUrl = mpd.resolveUrl_(parent.baseUrl, baseUrl ? baseUrl.url : null); + + // Parse hierarchical children. + this.periods = mpd.parseChildren_(this, elem, mpd.Period); +}; + + +/** + * Parses a "Period" tag. + * @param {!shaka.dash.mpd.Mpd} parent The parent Mpd. + * @param {!Node} elem The Period XML element. + */ +shaka.dash.mpd.Period.prototype.parse = function(parent, elem) { + var mpd = shaka.dash.mpd; + + // Parse attributes. + this.id = mpd.parseAttr_(elem, 'id', mpd.parseString_); + this.start = mpd.parseAttr_(elem, 'start', mpd.parseDuration_); + this.duration = mpd.parseAttr_(elem, 'duration', mpd.parseDuration_); + + // Never seen on this element itself, but inherited for convenience. + this.minBufferTime = parent.minBufferTime; + + // Parse simple child elements. + var baseUrl = mpd.parseChild_(this, elem, mpd.BaseUrl); + this.baseUrl = mpd.resolveUrl_(parent.baseUrl, baseUrl ? baseUrl.url : null); + + // Parse hierarchical children. + this.segmentBase = mpd.parseChild_(this, elem, mpd.SegmentBase); + this.segmentList = mpd.parseChild_(this, elem, mpd.SegmentList); + this.segmentTemplate = mpd.parseChild_(this, elem, mpd.SegmentTemplate); + + this.adaptationSets = mpd.parseChildren_(this, elem, mpd.AdaptationSet); +}; + + +/** + * Parses an "AdaptationSet" tag. + * @param {!shaka.dash.mpd.Period} parent The parent Period. + * @param {!Node} elem The AdaptationSet XML element. + */ +shaka.dash.mpd.AdaptationSet.prototype.parse = function(parent, elem) { + var mpd = shaka.dash.mpd; + + // Parse children which provide properties of the AdaptationSet. + var contentComponent = mpd.parseChild_(this, elem, mpd.ContentComponent) || + {}; + + // Parse attributes. + this.id = mpd.parseAttr_(elem, 'id', mpd.parseString_); + this.lang = mpd.parseAttr_(elem, 'lang', mpd.parseString_) || + contentComponent.lang; + this.contentType = mpd.parseAttr_(elem, 'contentType', mpd.parseString_) || + contentComponent.contentType; + this.width = mpd.parseAttr_(elem, 'width', mpd.parsePositiveInt_); + this.height = mpd.parseAttr_(elem, 'height', mpd.parsePositiveInt_); + this.mimeType = mpd.parseAttr_(elem, 'mimeType', mpd.parseString_); + this.codecs = mpd.parseAttr_(elem, 'codecs', mpd.parseString_); + + // Normalize the language tag. + if (this.lang) this.lang = shaka.util.LanguageUtils.normalize(this.lang); + + // Never seen on this element itself, but inherited for convenience. + this.minBufferTime = parent.minBufferTime; + + // Parse simple child elements. + var baseUrl = mpd.parseChild_(this, elem, mpd.BaseUrl); + this.baseUrl = mpd.resolveUrl_(parent.baseUrl, baseUrl ? baseUrl.url : null); + + this.contentProtections = + mpd.parseChildren_(this, elem, mpd.ContentProtection); + + if (!this.contentType && this.mimeType) { + // Infer contentType from mimeType. This must be done before parsing any + // child Representations, as Representation inherits contentType. + this.contentType = this.mimeType.split('/')[0]; + } + + // Parse hierarchical children. + this.segmentBase = mpd.parseChild_(this, elem, mpd.SegmentBase) || + parent.segmentBase; + this.segmentList = mpd.parseChild_(this, elem, mpd.SegmentList) || + parent.segmentList; + this.segmentTemplate = mpd.parseChild_(this, elem, mpd.SegmentTemplate) || + parent.segmentTemplate; + + this.representations = mpd.parseChildren_(this, elem, mpd.Representation); + + if (!this.mimeType && this.representations.length) { + // Infer mimeType from children. MpdProcessor will deal with the case + // where Representations have inconsistent mimeTypes. + this.mimeType = this.representations[0].mimeType; + + if (!this.contentType && this.mimeType) { + this.contentType = this.mimeType.split('/')[0]; + } + } +}; + + +/** + * Parses a "ContentComponent" tag. + * @param {!shaka.dash.mpd.AdaptationSet} parent The parent AdaptationSet. + * @param {!Node} elem The ContentComponent XML element. + */ +shaka.dash.mpd.ContentComponent.prototype.parse = function(parent, elem) { + var mpd = shaka.dash.mpd; + + // Parse attributes. + this.id = mpd.parseAttr_(elem, 'id', mpd.parseString_); + this.lang = mpd.parseAttr_(elem, 'lang', mpd.parseString_); + this.contentType = mpd.parseAttr_(elem, 'contentType', mpd.parseString_); + + // Normalize the language tag. + if (this.lang) this.lang = shaka.util.LanguageUtils.normalize(this.lang); +}; + + +/** + * Parses a "Representation" tag. + * @param {!shaka.dash.mpd.AdaptationSet} parent The parent AdaptationSet. + * @param {!Node} elem The Representation XML element. + */ +shaka.dash.mpd.Representation.prototype.parse = function(parent, elem) { + var mpd = shaka.dash.mpd; + + // Parse attributes. + this.id = mpd.parseAttr_(elem, 'id', mpd.parseString_); + this.bandwidth = mpd.parseAttr_(elem, 'bandwidth', mpd.parsePositiveInt_); + this.width = + mpd.parseAttr_(elem, 'width', mpd.parsePositiveInt_) || parent.width; + this.height = + mpd.parseAttr_(elem, 'height', mpd.parsePositiveInt_) || parent.height; + this.mimeType = + mpd.parseAttr_(elem, 'mimeType', mpd.parseString_) || parent.mimeType; + this.codecs = + mpd.parseAttr_(elem, 'codecs', mpd.parseString_) || parent.codecs; + + // Never seen on this element itself, but inherited for convenience. + this.lang = parent.lang; + this.minBufferTime = parent.minBufferTime; + + // Parse simple child elements. + var baseUrl = mpd.parseChild_(this, elem, mpd.BaseUrl); + this.baseUrl = mpd.resolveUrl_(parent.baseUrl, baseUrl ? baseUrl.url : null); + + this.contentProtections = + mpd.parseChildren_(this, elem, mpd.ContentProtection); + + // Parse hierarchical children. + this.segmentBase = mpd.parseChild_(this, elem, mpd.SegmentBase) || + parent.segmentBase; + this.segmentList = mpd.parseChild_(this, elem, mpd.SegmentList) || + parent.segmentList; + this.segmentTemplate = mpd.parseChild_(this, elem, mpd.SegmentTemplate) || + parent.segmentTemplate; + + if (this.contentProtections.length == 0) { + this.contentProtections = parent.contentProtections; + } +}; + + +/** + * Parses a "ContentProtection" tag. + * @param {*} parent The parent object. + * @param {!Node} elem The ContentProtection XML element. + */ +shaka.dash.mpd.ContentProtection.prototype.parse = function(parent, elem) { + var mpd = shaka.dash.mpd; + + // Parse attributes. + this.schemeIdUri = mpd.parseAttr_(elem, 'schemeIdUri', mpd.parseString_); + this.value = mpd.parseAttr_(elem, 'value', mpd.parseString_); + + // Parse simple child elements. + this.pssh = mpd.parseChild_(this, elem, mpd.CencPssh); + + // NOTE: A given ContentProtection tag could contain anything, and a scheme + // could be application-specific. Therefore we must capture whatever it + // contains, and let the application choose a scheme and map it to a key + // system. + this.children = elem.children; +}; + + +/** + * Parse a "cenc:pssh" tag. + * @param {*} parent The parent object. + * @param {!Node} elem The CencPssh XML element. + */ +shaka.dash.mpd.CencPssh.prototype.parse = function(parent, elem) { + var mpd = shaka.dash.mpd; + var StringUtils = shaka.util.StringUtils; + + var contents = mpd.getContents_(elem); + if (!contents) { + return; + } + + this.psshBox = StringUtils.toUint8Array(StringUtils.fromBase64(contents)); + + try { + this.parsedPssh = new shaka.util.Pssh(this.psshBox); + } catch (exception) { + if (!(exception instanceof RangeError)) { + throw exception; + } + } +}; + + +/** + * Parses a "BaseURL" tag. + * @param {*} parent The parent object. + * @param {!Node} elem The BaseURL XML element. + */ +shaka.dash.mpd.BaseUrl.prototype.parse = function(parent, elem) { + this.url = shaka.dash.mpd.getContents_(elem); +}; + + +/** + * Parses a "SegmentBase" tag. + * @param {*} parent The parent object. + * @param {!Node} elem The SegmentBase XML element. + */ +shaka.dash.mpd.SegmentBase.prototype.parse = function(parent, elem) { + var mpd = shaka.dash.mpd; + + this.baseUrl = parent.baseUrl; + this.mediaUrl = parent.baseUrl; + + // Parse attributes. + this.indexRange = mpd.parseAttr_(elem, 'indexRange', mpd.parseRange_); + + // Parse simple child elements. + this.representationIndex = + mpd.parseChild_(this, elem, mpd.RepresentationIndex); + + this.initialization = mpd.parseChild_(this, elem, mpd.Initialization); + + if (this.representationIndex) { + if (!this.representationIndex.range) { + this.representationIndex.range = this.indexRange; + } + } else { + // Normalize the SegmentBase by creating a default RepresentationIndex. + this.representationIndex = new shaka.dash.mpd.RepresentationIndex(); + this.representationIndex.url = this.baseUrl; + this.representationIndex.range = this.indexRange; + } +}; + + +/** + * Parses a "RepresentationIndex" tag. + * @param {!shaka.dash.mpd.SegmentBase} parent The parent SegmentBase. + * @param {!Node} elem The RepresentationIndex XML element. + */ +shaka.dash.mpd.RepresentationIndex.prototype.parse = function( + parent, elem) { + var mpd = shaka.dash.mpd; + + // Parse attributes. + var url = mpd.parseAttr_(elem, 'sourceURL', mpd.parseString_); + this.url = mpd.resolveUrl_(parent.baseUrl, url); + + this.range = mpd.parseAttr_(elem, 'range', mpd.parseRange_); +}; + + +/** + * Parses an "Initialization" tag. + * @param {!shaka.dash.mpd.SegmentBase|!shaka.dash.mpd.SegmentList} parent + * The parent SegmentBase or parent SegmentList. + * @param {!Node} elem The Initialization XML element. + */ +shaka.dash.mpd.Initialization.prototype.parse = function(parent, elem) { + var mpd = shaka.dash.mpd; + + // Parse attributes. + var url = mpd.parseAttr_(elem, 'sourceURL', mpd.parseString_); + this.url = mpd.resolveUrl_(parent.baseUrl, url); + + this.range = mpd.parseAttr_(elem, 'range', mpd.parseRange_); +}; + + +/** + * Parses a "SegmentList" tag. + * @param {*} parent The parent object. + * @param {!Node} elem The SegmentList XML element. + */ +shaka.dash.mpd.SegmentList.prototype.parse = function(parent, elem) { + var mpd = shaka.dash.mpd; + + this.baseUrl = parent.baseUrl; + + // Parse attributes. + this.timescale = + mpd.parseAttr_(elem, 'timescale', mpd.parsePositiveInt_) || 1; + + this.presentationTimeOffset = mpd.parseAttr_( + elem, 'presentationTimeOffset', mpd.parseNonNegativeInt_) || 0; + + this.segmentDuration = + mpd.parseAttr_(elem, 'duration', mpd.parseNonNegativeInt_); + + this.firstSegmentNumber = + mpd.parseAttr_(elem, 'startNumber', mpd.parsePositiveInt_) || 1; + + // Parse simple children + this.initialization = mpd.parseChild_(this, elem, mpd.Initialization); + this.segmentUrls = mpd.parseChildren_(this, elem, mpd.SegmentUrl); +}; + + +/** + * Parses a "SegmentUrl" tag. + * @param {!shaka.dash.mpd.SegmentList} parent The parent SegmentList. + * @param {!Node} elem The SegmentUrl XML element. + */ +shaka.dash.mpd.SegmentUrl.prototype.parse = function(parent, elem) { + var mpd = shaka.dash.mpd; + + // Parse attributes. + var url = mpd.parseAttr_(elem, 'media', mpd.parseString_); + this.mediaUrl = mpd.resolveUrl_(parent.baseUrl, url); + + this.mediaRange = mpd.parseAttr_(elem, 'mediaRange', mpd.parseRange_); +}; + + +/** + * Parses a "SegmentTemplate" tag. + * @param {*} parent The parent object. + * @param {!Node} elem The SegmentTemplate XML element. + */ +shaka.dash.mpd.SegmentTemplate.prototype.parse = function(parent, elem) { + var mpd = shaka.dash.mpd; + + // Parse attributes. + this.timescale = + mpd.parseAttr_(elem, 'timescale', mpd.parsePositiveInt_) || 1; + + this.presentationTimeOffset = mpd.parseAttr_( + elem, 'presentationTimeOffset', mpd.parseNonNegativeInt_) || 0; + + this.segmentDuration = + mpd.parseAttr_(elem, 'duration', mpd.parseNonNegativeInt_); + + this.firstSegmentNumber = + mpd.parseAttr_(elem, 'startNumber', mpd.parsePositiveInt_) || 1; + + this.mediaUrlTemplate = mpd.parseAttr_(elem, 'media', mpd.parseString_); + this.indexUrlTemplate = mpd.parseAttr_(elem, 'index', mpd.parseString_); + this.initializationUrlTemplate = + mpd.parseAttr_(elem, 'initialization', mpd.parseString_); + + // Parse hierarchical children. + this.timeline = mpd.parseChild_(this, elem, mpd.SegmentTimeline); +}; + + +/** + * Parses a "SegmentTimeline" tag. + * @param {!shaka.dash.mpd.SegmentTemplate} parent The parent SegmentTemplate. + * @param {!Node} elem The SegmentTimeline XML element. + */ +shaka.dash.mpd.SegmentTimeline.prototype.parse = function(parent, elem) { + var mpd = shaka.dash.mpd; + + this.timePoints = mpd.parseChildren_(this, elem, mpd.SegmentTimePoint); +}; + + +/** + * Parses an "S" tag. + * @param {!shaka.dash.mpd.SegmentTimeline} parent The parent SegmentTimeline. + * @param {!Node} elem The SegmentTimePoint XML element. + */ +shaka.dash.mpd.SegmentTimePoint.prototype.parse = function(parent, elem) { + var mpd = shaka.dash.mpd; + + // Parse attributes. + this.startTime = mpd.parseAttr_(elem, 't', mpd.parseNonNegativeInt_); + this.duration = mpd.parseAttr_(elem, 'd', mpd.parseNonNegativeInt_); + this.repeat = mpd.parseAttr_(elem, 'r', mpd.parseNonNegativeInt_); +}; + + +// MPD parsing utility functions ---------------------------------------------- + + +/** + * Resolves |urlString| relative to |baseUrl|. + * @param {goog.Uri} baseUrl + * @param {?string} urlString + * @return {goog.Uri} + * @private + */ +shaka.dash.mpd.resolveUrl_ = function(baseUrl, urlString) { + var url = urlString ? new goog.Uri(urlString) : null; + + if (baseUrl) { + return url ? baseUrl.resolve(url) : baseUrl; + } else { + return url; + } +}; + + +/** + * Parses a child XML element. + * @param {*} parent The parsed parent object. + * @param {!Node} elem The parent XML element. + * @param {function(new:T)} constructor The constructor of the parsed + * child XML element. The constructor must define the attribute "TAG_NAME". + * @return {T} The parsed child XML element on success, or null if a child + * XML element does not exist with the given tag name OR if there exists + * more than one child XML element with the given tag name OR if the child + * XML element could not be parsed. + * @template T + * @private + */ +shaka.dash.mpd.parseChild_ = function(parent, elem, constructor) { + var childElement = null; + + for (var i = 0; i < elem.children.length; i++) { + if (elem.children[i].tagName != constructor.TAG_NAME) { + continue; + } + if (childElement) { + return null; + } + childElement = elem.children[i]; + } + + if (!childElement) { + return null; + } + + var parsedChild = new constructor(); + parsedChild.parse.call(parsedChild, parent, childElement); + return parsedChild; +}; + + +/** + * Parses an array of child XML elements. + * @param {*} parent The parsed parent object. + * @param {!Node} elem The parent XML element. + * @param {function(new:T)} constructor The constructor of each parsed child + * XML element. The constructor must define the attribute "TAG_NAME". + * @return {!Array.} The parsed child XML elements. + * @template T + * @private + */ +shaka.dash.mpd.parseChildren_ = function(parent, elem, constructor) { + var parsedChildren = []; + + for (var i = 0; i < elem.children.length; i++) { + if (elem.children[i].tagName != constructor.TAG_NAME) { + continue; + } + var parsedChild = new constructor(); + parsedChild.parse.call(parsedChild, parent, elem.children[i]); + parsedChildren.push(parsedChild); + } + + return parsedChildren; +}; + + +/** + * Gets an array of child XML elements by tag name, without parsing them. + * @param {!Node} elem The parent XML element. + * @param {string} tagName The tag name to filter by. + * @return {!Array.} The child XML elements. + * @private + */ +shaka.dash.mpd.getChildren_ = function(elem, tagName) { + var children = []; + + for (var i = 0; i < elem.children.length; i++) { + if (elem.children[i].tagName != tagName) { + continue; + } + children.push(elem.children[i]); + } + + return children; +}; + + +/** + * Gets the text contents of a node. + * @param {!Node} elem The XML element. + * @return {?string} The text contents, or null if there are none. + * @private + */ +shaka.dash.mpd.getContents_ = function(elem) { + var contents = elem.firstChild; + if (contents.nodeType != Node.TEXT_NODE) { + return null; + } + + return contents.nodeValue; +}; + + +/** + * Parses an attribute by its name. + * @param {!Node} elem The XML element. + * @param {string} name The attribute name. + * @param {function(string): (T|null)} parseFunction A function to parse the + * attribute. + * @return {(T|null)} The parsed attribute on success, or null if the + * attribute does not exist OR could not be parsed. + * @template T + * @private + */ +shaka.dash.mpd.parseAttr_ = function(elem, name, parseFunction) { + return parseFunction(elem.getAttribute(name)); +}; + + +/** + * Parses an XML duration string. + * Note that months and years are not supported, nor are negative values. + * @param {string} durationString The duration string, e.g., "PT1H3M43.2S", + * which means 1 hour, 3 minutes, and 43.2 seconds. + * @return {?number} The parsed duration in seconds, or null if the duration + * string could not be parsed. + * @see http://www.datypic.com/sc/xsd/t-xsd_duration.html + * @private + */ +shaka.dash.mpd.parseDuration_ = function(durationString) { + if (!durationString) { + return null; + } + + var regex = + /^P(?:([0-9]*)D)?(?:T(?:([0-9]*)H)?(?:([0-9]*)M)?(?:([0-9.]*)S)?)?$/; + var matches = regex.exec(durationString); + + if (!matches) { + shaka.log.warning('Invalid duration string:', durationString); + return null; + } + + var duration = 0; + + var days = shaka.dash.mpd.parseNonNegativeInt_(matches[1]); + if (days) { + duration += 86400 * days; + } + + var hours = shaka.dash.mpd.parseNonNegativeInt_(matches[2]); + if (hours) { + duration += 3600 * hours; + } + + var minutes = shaka.dash.mpd.parseNonNegativeInt_(matches[3]); + if (minutes) { + duration += 60 * minutes; + } + + var seconds = shaka.dash.mpd.parseFloat_(matches[4]); + if (seconds) { + duration += seconds; + } + + return duration; +}; + + +/** + * Parses a range string. + * @param {string} rangeString The range string, e.g., "101-9213" + * @return {shaka.dash.mpd.Range} The parsed range, or null if the range string + * could not be parsed. + * @private + */ +shaka.dash.mpd.parseRange_ = function(rangeString) { + var matches = /([0-9]+)-([0-9]+)/.exec(rangeString); + + if (!matches) { + return null; + } + + var begin = shaka.dash.mpd.parseNonNegativeInt_(matches[1]); + if (begin == null) { + return null; + } + + var end = shaka.dash.mpd.parseNonNegativeInt_(matches[2]); + if (end == null) { + return null; + } + + return new shaka.dash.mpd.Range(begin, end); +}; + + +/** + * Parses a positive integer. + * @param {string} intString The integer string. + * @return {?number} The parsed positive integer on success; otherwise, + * return null. + * @private + */ +shaka.dash.mpd.parsePositiveInt_ = function(intString) { + var result = window.parseInt(intString, 10); + return (result > 0 ? result : null); +}; + + +/** + * Parses a non-negative integer. + * @param {string} intString The integer string. + * @return {?number} The parsed non-negative integer on success; otherwise, + * return null. + * @private + */ +shaka.dash.mpd.parseNonNegativeInt_ = function(intString) { + var result = window.parseInt(intString, 10); + return (result >= 0 ? result : null); +}; + + +/** + * Parses a floating point number. + * @param {string} floatString The floating point number string. + * @return {?number} The parsed floating point number, or null if the floating + * point number string could not be parsed. + * @private + */ +shaka.dash.mpd.parseFloat_ = function(floatString) { + var result = window.parseFloat(floatString); + return (!isNaN(result) ? result : null); +}; + + +/** + * A misnomer. Does no parsing, just returns the input string as-is. + * @param {string} inputString The inputString. + * @return {?string} The "parsed" string. The type is specified as nullable + * only to fit into the parseAttr_() template, but null will never be + * returned. + * @private + */ +shaka.dash.mpd.parseString_ = function(inputString) { + return inputString; +}; + diff --git a/lib/dash/mpd_processor.js b/lib/dash/mpd_processor.js new file mode 100644 index 0000000000..dfb9da6e94 --- /dev/null +++ b/lib/dash/mpd_processor.js @@ -0,0 +1,1560 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Processes, filters, and interprets an MPD. + */ + +goog.provide('shaka.dash.MpdProcessor'); + +goog.require('goog.Uri'); +goog.require('shaka.asserts'); +goog.require('shaka.dash.SegmentIndex'); +goog.require('shaka.dash.SegmentReference'); +goog.require('shaka.dash.mpd'); +goog.require('shaka.log'); +goog.require('shaka.player.DrmSchemeInfo'); +goog.require('shaka.player.Player'); +goog.require('shaka.util.ArrayUtils'); +goog.require('shaka.util.LanguageUtils'); +goog.require('shaka.util.MultiMap'); + + + +/** + * Set up an MPD processor. + * @param {shaka.player.DashVideoSource.ContentProtectionCallback} + * interpretContentProtection + * @constructor + * @struct + */ +shaka.dash.MpdProcessor = function(interpretContentProtection) { + /** @private {shaka.player.DashVideoSource.ContentProtectionCallback} */ + this.interpretContentProtection_ = interpretContentProtection; + + /** + * @private {!Array.} + */ + this.adaptationSetMapAndDrmSchemeByPeriod_ = []; +}; + + +/** + * Maps content types to collections of AdaptationSets. + * @typedef {!shaka.util.MultiMap.} + */ +shaka.dash.MpdProcessor.AdaptationSetMap; + + +/** + * @typedef {{adaptationSetMap: !shaka.dash.MpdProcessor.AdaptationSetMap, + * drmScheme: shaka.player.DrmSchemeInfo}} + */ +shaka.dash.MpdProcessor.AdaptationSetMapAndDrmScheme; + + +/** + * Determine the full MIME type of a Representation. + * + * @param {!shaka.dash.mpd.Representation} representation + * @return {string} + */ +shaka.dash.MpdProcessor.representationMimeType = function(representation) { + var type = representation.mimeType || ''; + if (representation.codecs) { + type += '; codecs="' + representation.codecs + '"'; + } + return type; +}; + + +/** + * Process the MPD. The MPD will be modified by having unsupported + * Representations and AdaptationSets removed, setting userData fields on + * Representations and AdaptationSets, and sorting Representations. + * + * @param {!shaka.dash.mpd.Mpd} mpd + */ +shaka.dash.MpdProcessor.prototype.process = function(mpd) { + // First, check that each Representation has only one of SegmentBase, + // SegmentList, or SegmentTemplate. + this.validateSegmentInfo_(mpd); + + // Next, generate concrete Representations from SegmentTemplates. + this.processSegmentTemplates_(mpd); + + // Next, fix up period start/duration and MPD duration attributes. + this.calculateDurations_(mpd); + + // Next, filter out any invalid Representations. + this.filterRepresentations_(mpd); + + // Next, build segment indexes for each SegmentList. + this.buildSegmentIndexes_(mpd); + + // Next, bubble up DRM scheme info to the AdaptationSet level. + this.bubbleUpDrmSchemes_(mpd); + + // Next, sort the Representations by bandwidth. + this.sortRepresentations_(mpd); + + // Finally, choose AdaptationSets for each period. + this.chooseAdaptationSets_(mpd); +}; + + +/** + * Get the number of processed periods. + * + * @return {number} + */ +shaka.dash.MpdProcessor.prototype.getNumPeriods = function() { + return this.adaptationSetMapAndDrmSchemeByPeriod_.length; +}; + + +/** + * Get the processed AdaptationSets for a given period. + * + * @param {number} periodIdx + * @param {string=} opt_type Optional content type. If left undefined then all + * AdaptationSets are returned for the given period. + * @return {!Array.} + */ +shaka.dash.MpdProcessor.prototype.getAdaptationSets = function( + periodIdx, opt_type) { + shaka.asserts.assert( + periodIdx >= 0 && + periodIdx < this.adaptationSetMapAndDrmSchemeByPeriod_.length); + + var tuple = this.adaptationSetMapAndDrmSchemeByPeriod_[periodIdx]; + if (!tuple) { + return []; + } + + return opt_type ? + tuple.adaptationSetMap.get(opt_type) || [] : + tuple.adaptationSetMap.getAll(); +}; + + +/** + * Get the common DRM scheme for a given period. + * + * @param {number} periodIdx + * @return {shaka.player.DrmSchemeInfo} + */ +shaka.dash.MpdProcessor.prototype.getDrmScheme = function(periodIdx) { + shaka.asserts.assert( + periodIdx >= 0 && + periodIdx < this.adaptationSetMapAndDrmSchemeByPeriod_.length); + + var tuple = this.adaptationSetMapAndDrmSchemeByPeriod_[periodIdx]; + if (!tuple) { + return null; + } + + return tuple.drmScheme; +}; + + +/** + * Select AdaptationSets for a given period. + * + * @param {number} periodIdx + * @param {string} preferredLang The preferred language. + * @return {!Array.} + */ +shaka.dash.MpdProcessor.prototype.selectAdaptationSets = function( + periodIdx, preferredLang) { + shaka.asserts.assert( + periodIdx >= 0 && + periodIdx < this.adaptationSetMapAndDrmSchemeByPeriod_.length); + + var tuple = this.adaptationSetMapAndDrmSchemeByPeriod_[periodIdx]; + if (!tuple) { + return []; + } + + var sets = []; + + // Add a video AdaptationSet. + var videoSets = tuple.adaptationSetMap.get('video'); + if (videoSets && videoSets.length > 0) { + shaka.asserts.assert(videoSets.length == 1); + sets.push(videoSets[0]); + } + + // Add an audio AdaptationSet. + var audioSets = tuple.adaptationSetMap.get('audio'); + if (audioSets && audioSets.length > 0) { + var favoredAudioSets = + this.filterSetsByLanguage_(audioSets, preferredLang); + + // If no matches were found, take the first audio set. + sets.push(favoredAudioSets.length > 0 ? favoredAudioSets[0] : audioSets[0]); + } + + // Add a text AdaptationSet. + var textSets = tuple.adaptationSetMap.get('text'); + if (textSets && textSets.length > 0) { + var favoredTextSets = + this.filterSetsByLanguage_(textSets, preferredLang); + + // If no matches were found, take the first subtitle set. + var textSet = favoredTextSets.length > 0 ? favoredTextSets[0] : + textSets[0]; + sets.push(textSet); + } + + return sets; +}; + + +/** + * Enforces restrictions on the video Representations can be used. + * Representations which exceed any of these restrictions will be removed. + * + * @param {!shaka.dash.mpd.Mpd} mpd + * @param {!shaka.player.DrmSchemeInfo.Restrictions} restrictions + */ +shaka.dash.MpdProcessor.prototype.enforceRestrictions = + function(mpd, restrictions) { + for (var i = 0; i < mpd.periods.length; ++i) { + var period = mpd.periods[i]; + + for (var j = 0; j < period.adaptationSets.length; ++j) { + var adaptationSet = period.adaptationSets[j]; + + for (var k = 0; k < adaptationSet.representations.length; ++k) { + var representation = adaptationSet.representations[k]; + var remove = false; + + if (restrictions.maxWidth && + representation.width > restrictions.maxWidth) { + remove = true; + } + + if (restrictions.maxHeight && + representation.height > restrictions.maxHeight) { + remove = true; + } + + if (remove) { + adaptationSet.representations.splice(k, 1); + --k; + } + } // for k + } // for j + } // for i +}; + + +/** + * Returns a list of sets matching the preferred language. + * + * @param {Array.} sets + * @param {string} preferredLang The preferred language. + * @return {!Array.} + * @private + */ +shaka.dash.MpdProcessor.prototype.filterSetsByLanguage_ = + function(sets, preferredLang) { + // Alias. + var LanguageUtils = shaka.util.LanguageUtils; + + if (sets && sets.length > 0) { + // Do a fuzzy match and stop on the lowest successful fuzz level. + var favoredSets; + for (var fuzz = LanguageUtils.MatchType.MIN; + fuzz <= LanguageUtils.MatchType.MAX; + ++fuzz) { + favoredSets = sets.filter( + function(set) { + var candidate = set.lang || ''; + return LanguageUtils.match(fuzz, preferredLang, candidate); + }); + if (favoredSets.length) { + return favoredSets; + } + } + } + return []; +}; + + +/** + * Calculate each Period's start and duration as well as the MPD's duration. + * + * @see ISO/IEC 23009-1:2014 section 5.3.2.1 + * + * @param {!shaka.dash.mpd.Mpd} mpd + * @private + */ +shaka.dash.MpdProcessor.prototype.calculateDurations_ = function(mpd) { + if (!mpd.periods.length) { + return; + } + + // "If the 'start' attribute is absent, and the Period element is the first + // in the MPD, and the MPD type is 'static', then the Period.start time shall + // be set to zero." + if (mpd.type == 'static' && mpd.periods[0].start == null) { + mpd.periods[0].start = 0; + } + + // If it's zero or truthy, it's set. This means null and NaN are not set. + var isSet = function(x) { return x == 0 || !!x; }; + + if (mpd.periods.length == 1 && + !isSet(mpd.periods[0].duration) && + isSet(mpd.duration)) { + // Assume the period's duration is equal to the MPD's + // 'mediaPresentationDuration' attribute. + mpd.periods[0].duration = mpd.duration; + } + + var totalDuration = 0; + + for (var i = 0; i < mpd.periods.length; ++i) { + var previousPeriod = mpd.periods[i - 1]; + var period = mpd.periods[i]; + + this.calculatePeriodDuration_(period); + shaka.log.debug('Period duration', period.duration); + + // "The Period extends until the Period.start of the next Period, or until + // the end of the Media Presentation in the case of the last Period." + var nextPeriod = mpd.periods[i + 1] || { start: mpd.duration }; + + // "If the 'start' attribute is absent, but the previous period contains a + // 'duration' attribute, the start time of the new Period is the sum of the + // start time of the previous period Period.start and the value of the + // attribute 'duration' of the previous Period." + if (!isSet(period.start) && + previousPeriod && isSet(previousPeriod.duration)) { + shaka.asserts.assert(isSet(previousPeriod.start)); + period.start = previousPeriod.start + previousPeriod.duration; + } + shaka.asserts.assert(isSet(period.start)); + + // "The difference between the start time of a Period and the start time + // of the following Period is the duration of the media content represented + // by this Period." + if (!isSet(period.duration) && isSet(nextPeriod.start)) { + period.duration = nextPeriod.start - period.start; + } + shaka.asserts.assert(isSet(period.duration)); + + totalDuration += period.duration; + } + shaka.asserts.assert(isSet(totalDuration)); + + var finalPeriod = mpd.periods[mpd.periods.length - 1]; + // "The Media Presentation Duration is provided either as the value of MPD + // 'mediaPresentationDuration' attribute if present, or as the sum of + // Period.start + Period.duration of the last Period." + if (!isSet(mpd.duration)) { + mpd.duration = finalPeriod.start + finalPeriod.duration; + } + shaka.asserts.assert(isSet(mpd.duration)); + shaka.asserts.assert(totalDuration == mpd.duration); +}; + + +/** + * Calculate |period|'s duration based upon its Representations. + * + * @param {!shaka.dash.mpd.Period} period + * + * @private + */ +shaka.dash.MpdProcessor.prototype.calculatePeriodDuration_ = function(period) { + if (period.duration) { + return; + } + + var maxDuration = 0; + + for (var i = 0; i < period.adaptationSets.length; ++i) { + var adaptationSet = period.adaptationSets[i]; + for (var j = 0; j < adaptationSet.representations.length; ++j) { + var representation = adaptationSet.representations[j]; + + if (!representation.segmentList) { + continue; + } + + var segmentListDuration = + this.calculateSegmentListDuration_(representation.segmentList); + + maxDuration = Math.max(maxDuration, segmentListDuration); + } + } + + period.duration = maxDuration; +}; + + +/** + * Calculates the duration of a SegmentList. + * @param {!shaka.dash.mpd.SegmentList} segmentList + * + * @return {number} The duration of |segmentList|. + * @private + */ +shaka.dash.MpdProcessor.prototype.calculateSegmentListDuration_ = function( + segmentList) { + if (segmentList.segmentDuration) { + return segmentList.segmentDuration / + segmentList.timescale * + segmentList.segmentUrls.length; + } + + var totalUnscaledDuration = 0; + + for (var i = 0; i < segmentList.segmentUrls.length; ++i) { + var segmentUrl = segmentList.segmentUrls[i]; + + shaka.asserts.assert(segmentUrl.duration); + totalUnscaledDuration += segmentUrl.duration; + } + + return totalUnscaledDuration / segmentList.timescale; +}; + + +/** + * Ensure that each Representation has only one of SegmentBase, SegmentList, + * or SegmentTemplate. + * + * @param {!shaka.dash.mpd.Mpd} mpd + * + * @private + */ +shaka.dash.MpdProcessor.prototype.validateSegmentInfo_ = function(mpd) { + for (var i = 0; i < mpd.periods.length; ++i) { + var period = mpd.periods[i]; + for (var j = 0; j < period.adaptationSets.length; ++j) { + var adaptationSet = period.adaptationSets[j]; + if (adaptationSet.contentType == 'text') continue; + + for (var k = 0; k < adaptationSet.representations.length; ++k) { + var representation = adaptationSet.representations[k]; + + var n = 0; + n += representation.segmentBase ? 1 : 0; + n += representation.segmentList ? 1 : 0; + n += representation.segmentTemplate ? 1 : 0; + + if (n == 0) { + shaka.log.warning( + 'Representation does not contain any segment information. ' + + 'Representation must contain one of SegmentBase, ' + + 'SegmentList, or SegmentTemplate.'); + adaptationSet.representations.splice(k, 1); + --k; + } else if (n != 1) { + shaka.log.warning( + 'Representation contains multiple segment information sources. ' + + 'Representation should only contain one of SegmentBase, ' + + 'SegmenstList, or SegmentTemplate.'); + if (representation.segmentBase) { + shaka.log.info('Using SegmentBase by default.'); + representation.segmentList = null; + representation.segmentTemplate = null; + } else if (representation.segmentList) { + shaka.log.info('Using SegmentList by default.'); + representation.segmentTemplate = null; + } else { + shaka.asserts.unreachable(); + } + } + } // for k + } + } +}; + + +/** + * Generates either a SegmentBase or SegmentList for each Representation that + * uses a SegmentTemplate. + * + * @see ISO/IEC 23009-1:2014 section 5.3.9.4 + * + * @param {!shaka.dash.mpd.Mpd} mpd + * @private + */ +shaka.dash.MpdProcessor.prototype.processSegmentTemplates_ = function(mpd) { + for (var i = 0; i < mpd.periods.length; ++i) { + var period = mpd.periods[i]; + for (var j = 0; j < period.adaptationSets.length; ++j) { + var adaptationSet = period.adaptationSets[j]; + for (var k = 0; k < adaptationSet.representations.length; ++k) { + var representation = adaptationSet.representations[k]; + + if (!representation.segmentTemplate) { + continue; + } + + var segmentTemplate = representation.segmentTemplate; + + // Prefer an explicit segment index URL, then a segment timeline, and + // then a segment duration. + if (segmentTemplate.indexUrlTemplate) { + if (segmentTemplate.timeline) { + shaka.log.warning( + 'Ignoring segment timeline because an explicit segment index ' + + 'URL was provided for the SegmentTemplate.'); + } + if (segmentTemplate.duration) { + shaka.log.warning( + 'Ignoring segment duration because an explicit segment index ' + + 'URL was provided for the SegmentTemplate.'); + } + this.generateSegmentBase_(representation); + if (!representation.segmentBase) { + // An error has already been logged. + adaptationSet.representations.splice(k, 1); + --k; + } + } else if (segmentTemplate.timeline) { + if (segmentTemplate.segmentDuration) { + shaka.log.warning( + 'Ignoring segment duration because a segment timeline was ' + + 'provided for the SegmentTemplate.'); + } + this.generateSegmentListFromTimeline_(representation); + if (!representation.segmentList) { + // An error has already been logged. + adaptationSet.representations.splice(k, 1); + --k; + } + } else if (segmentTemplate.segmentDuration) { + if (period.duration) { + this.generateSegmentListFromDuration_( + representation, + period.duration); + if (!representation.segmentList) { + // An error has already been logged. + adaptationSet.representations.splice(k, 1); + --k; + } + } else { + shaka.log.error( + 'SegmentTemplate provides a segment duration but the ' + + 'Period\'s duration is unknown.'); + adaptationSet.representations.splice(k, 1); + --k; + } + } else { + shaka.log.error( + 'SegmentTemplate does not provide a segment timeline, a ' + + 'segment duration, or an explicit index URL template.'); + adaptationSet.representations.splice(k, 1); + --k; + } + } // for k + } + } +}; + + +/** + * Generates a SegmentBase from a SegmentTemplate. + * Sets |representation.segmentBase| on success. + * + * @param {!shaka.dash.mpd.Representation} representation + * + * @private + */ +shaka.dash.MpdProcessor.prototype.generateSegmentBase_ = function( + representation) { + shaka.asserts.assert(representation.segmentBase == null); + shaka.asserts.assert(representation.segmentList == null); + shaka.asserts.assert(representation.segmentTemplate); + shaka.asserts.assert(representation.segmentTemplate.indexUrlTemplate); + + var segmentTemplate = representation.segmentTemplate; + var segmentBase = new shaka.dash.mpd.SegmentBase(); + + segmentBase.representationIndex = + this.generateRepresentationIndex_(representation); + if (!segmentBase.representationIndex) { + // An error has already been logged. + return; + } + + segmentBase.initialization = this.generateInitialization_(representation); + + // Generate the media URL. Since there is no SegmentTimeline there is only + // one media URL, so just map $Number$ to 1 and $Time$ to 0. + var mediaUrl; + if (segmentTemplate.mediaUrlTemplate) { + var filledUrlTemplate = this.fillUrlTemplate_( + segmentTemplate.mediaUrlTemplate, + representation.id, + 1, + representation.bandwidth, + 0); + + if (!filledUrlTemplate) { + // An error has already been logged. + return; + } + + mediaUrl = representation.baseUrl ? + representation.baseUrl.resolve(filledUrlTemplate) : + filledUrlTemplate; + } else { + // Fallback to the Representation's URL. + mediaUrl = representation.baseUrl; + } + + segmentBase.mediaUrl = mediaUrl; + representation.segmentBase = segmentBase; +}; + + +/** + * Generates a SegmentList from a SegmentTemplate which has a segment timeline. + * Sets |representation.segmentList| on success. + * + * @param {!shaka.dash.mpd.Representation} representation + * + * @private + */ +shaka.dash.MpdProcessor.prototype.generateSegmentListFromTimeline_ = function( + representation) { + shaka.asserts.assert(representation.segmentBase == null); + shaka.asserts.assert(representation.segmentList == null); + shaka.asserts.assert(representation.segmentTemplate); + shaka.asserts.assert(representation.segmentTemplate.timeline); + + var segmentTemplate = representation.segmentTemplate; + if (!segmentTemplate.mediaUrlTemplate) { + shaka.log.error('SegmentTemplate provided without a media URL template.'); + return; + } + + var segmentList = new shaka.dash.mpd.SegmentList(); + + // Note: do not copy |segmentDuration| since the segments may have different + // lengths. + segmentList.timescale = segmentTemplate.timescale; + segmentList.presentationTimeOffset = segmentTemplate.presentationTimeOffset; + segmentList.firstSegmentNumber = segmentTemplate.firstSegmentNumber; + segmentList.initialization = this.generateInitialization_(representation); + segmentList.segmentUrls = []; + + // Generate SegmentUrls. + var timePoints = segmentTemplate.timeline.timePoints; + + // The current segment number. + var segmentNumber = 1; + var lastEndTime = -1; + + for (var i = 0; i < timePoints.length; ++i) { + var repeat = timePoints[i].repeat || 0; + for (var j = 0; j <= repeat; ++j) { + if (!timePoints[i].duration) { + shaka.log.warning( + 'SegmentTimeline "S" element does not have a duration.', + timePoints[i]); + return; + } + + // Compute the time-point's true unscaled start time. + var startTime; + if (timePoints[i].startTime && j == 0) { + startTime = timePoints[i].startTime; + } else { + if (i == 0 && j == 0) { + startTime = 0; + } else { + startTime = lastEndTime; + } + } + + shaka.asserts.assert(startTime >= 0); + + if (lastEndTime >= 0 && startTime > lastEndTime) { + // The start of the current segment may begin before the end of the + // last segment, but there should not be a gap between them. + shaka.log.warning( + 'SegmentTimeline "S" element does not have a valid start time. ' + + 'There is no segment information for the time range ' + + lastEndTime + ' to ' + startTime + '.', + timePoints[i]); + return; + } + + lastEndTime = startTime + timePoints[i].duration; + + // Generate the media URL. + shaka.asserts.assert(segmentTemplate.mediaUrlTemplate); + var filledUrlTemplate = this.fillUrlTemplate_( + segmentTemplate.mediaUrlTemplate, + representation.id, + segmentNumber - 1 + segmentTemplate.firstSegmentNumber, + representation.bandwidth, + startTime); + + if (!filledUrlTemplate) { + // An error has already been logged. + return; + } + + var mediaUrl = representation.baseUrl ? + representation.baseUrl.resolve(filledUrlTemplate) : + filledUrlTemplate; + + // Create the SegmentUrl. + var segmentUrl = new shaka.dash.mpd.SegmentUrl(); + segmentUrl.mediaUrl = mediaUrl; + segmentUrl.startTime = startTime; + segmentUrl.duration = timePoints[i].duration; + + segmentList.segmentUrls.push(segmentUrl); + + ++segmentNumber; + } // for j + } + + representation.segmentList = segmentList; +}; + + +/** + * Generates a SegmentList from a SegmentTemplate which has a segment duration. + * Sets |representation.segmentList| on success. + * + * @param {!shaka.dash.mpd.Representation} representation + * @param {number} periodDuration + * + * @private + */ +shaka.dash.MpdProcessor.prototype.generateSegmentListFromDuration_ = function( + representation, periodDuration) { + shaka.asserts.assert(representation.segmentBase == null); + shaka.asserts.assert(representation.segmentList == null); + shaka.asserts.assert(representation.segmentTemplate); + shaka.asserts.assert(representation.segmentTemplate.segmentDuration); + + var segmentTemplate = representation.segmentTemplate; + if (!segmentTemplate.mediaUrlTemplate) { + shaka.log.error('SegmentTemplate provided without a media URL template.'); + return; + } + + var segmentList = new shaka.dash.mpd.SegmentList(); + + // Note: do not copy |segmentDuration| since the segments may have different + // lengths. + segmentList.timescale = segmentTemplate.timescale; + segmentList.presentationTimeOffset = segmentTemplate.presentationTimeOffset; + segmentList.firstSegmentNumber = segmentTemplate.firstSegmentNumber; + segmentList.initialization = this.generateInitialization_(representation); + segmentList.segmentUrls = []; + + // The current segment number. + var segmentNumber = 1; + var startTime = 0; + + while ((startTime / segmentList.timescale) < periodDuration) { + // Generate the media URL. + shaka.asserts.assert(segmentTemplate.mediaUrlTemplate); + var filledUrlTemplate = this.fillUrlTemplate_( + segmentTemplate.mediaUrlTemplate, + representation.id, + segmentNumber - 1 + segmentTemplate.firstSegmentNumber, + representation.bandwidth, + startTime); + + if (!filledUrlTemplate) { + // An error has already been logged. + return; + } + + var mediaUrl = representation.baseUrl ? + representation.baseUrl.resolve(filledUrlTemplate) : + filledUrlTemplate; + + // Create the SegmentUrl. + var segmentUrl = new shaka.dash.mpd.SegmentUrl(); + segmentUrl.mediaUrl = mediaUrl; + segmentUrl.startTime = startTime; + segmentUrl.duration = segmentTemplate.segmentDuration; + + segmentList.segmentUrls.push(segmentUrl); + + ++segmentNumber; + startTime += segmentTemplate.segmentDuration; + } + + representation.segmentList = segmentList; +}; + + +/** + * Generates a RepresentationIndex from a SegmentTemplate. + * + * @param {!shaka.dash.mpd.Representation} representation + * + * @return {shaka.dash.mpd.RepresentationIndex} A RepresentationIndex on + * success, null if no index URL template exists or an error occurred. + * @private + */ +shaka.dash.MpdProcessor.prototype.generateRepresentationIndex_ = function( + representation) { + shaka.asserts.assert(representation.segmentTemplate); + + var segmentTemplate = representation.segmentTemplate; + if (!segmentTemplate.indexUrlTemplate) { + return null; + } + + var representationIndex = new shaka.dash.mpd.RepresentationIndex(); + + // $Number$ and $Time$ cannot be present in an index URL template. + var filledUrlTemplate = this.fillUrlTemplate_( + segmentTemplate.indexUrlTemplate, + representation.id, + null, + representation.bandwidth, + null); + + if (!filledUrlTemplate) { + // An error has already been logged. + return null; + } + + if (representation.baseUrl && filledUrlTemplate) { + representationIndex.url = + representation.baseUrl.resolve(filledUrlTemplate); + } else { + representationIndex.url = filledUrlTemplate; + } + + return representationIndex; +}; + + +/** + * Generates an Initialization from a SegmentTemplate. + * + * @param {!shaka.dash.mpd.Representation} representation + * + * @return {shaka.dash.mpd.Initialization} An Initialization on success, null + * if no initialization URL template exists or an error occurred. + * @private + */ +shaka.dash.MpdProcessor.prototype.generateInitialization_ = function( + representation) { + shaka.asserts.assert(representation.segmentTemplate); + + var segmentTemplate = representation.segmentTemplate; + if (!segmentTemplate.initializationUrlTemplate) { + // This is not an error: the segments may be self initializing. + return null; + } + + var initialization = new shaka.dash.mpd.Initialization(); + + // $Number$ and $Time$ cannot be present in an initialization URL template. + var filledUrlTemplate = this.fillUrlTemplate_( + segmentTemplate.initializationUrlTemplate, + representation.id, + null, + representation.bandwidth, + null); + + if (!filledUrlTemplate) { + // An error has already been logged. + return null; + } + + if (representation.baseUrl && filledUrlTemplate) { + initialization.url = + representation.baseUrl.resolve(filledUrlTemplate); + } else { + initialization.url = filledUrlTemplate; + } + + return initialization; +}; + + +/** + * Fills a SegmentTemplate URL template. + * + * @see ISO/IEC 23009-1:2014 section 5.3.9.4.4 + * + * @param {string} urlTemplate + * @param {?string} representationId + * @param {?number} number + * @param {?number} bandwidth + * @param {?number} time + * + * @return {goog.Uri} A URL on success; null if the resulting URL contains + * illegal characters. + * @private + */ +shaka.dash.MpdProcessor.prototype.fillUrlTemplate_ = function( + urlTemplate, representationId, number, bandwidth, time) { + /** @type {!Object.} */ + var valueTable = { + 'RepresentationID': representationId, + 'Number': number, + 'Bandwidth': bandwidth, + 'Time': time + }; + + var re = /\$(RepresentationID|Number|Bandwidth|Time)?(?:%0([0-9]+)d)?\$/g; + var url = urlTemplate.replace(re, function(match, name, widthString) { + if (match == '$$') { + return '$'; + } + + var value = valueTable[name]; + shaka.asserts.assert(value !== undefined); + + // Note that |value| may be 0 or ''. + if (value == null) { + shaka.log.warning( + 'URL template does not have an available substitution for ' + + 'identifier ' + '"' + name + '".'); + return match; + } + + if (name == 'RepresentationID' && widthString) { + shaka.log.warning( + 'URL template should not contain a width specifier for identifier ' + + '"RepresentationID".'); + widthString = undefined; + } + + var valueString = value.toString(); + + // Create padding string. + var width = window.parseInt(widthString, 10) || 1; + var paddingSize = Math.max(0, width - valueString.length); + var padding = (new Array(paddingSize + 1)).join('0'); + + return padding + valueString; + }); + + // The URL might contain illegal characters (e.g., '%'). + try { + return new goog.Uri(url); + } catch (exception) { + if (exception instanceof URIError) { + shaka.log.warning('URL template contains an illegal character.'); + return null; + } + throw exception; + } +}; + + +/** + * Builds a SegmentIndex for each SegmentList. + * Clears each SegmentList's SegmentUrls. + * + * @param {!shaka.dash.mpd.Mpd} mpd + * + * @private + */ +shaka.dash.MpdProcessor.prototype.buildSegmentIndexes_ = function(mpd) { + for (var i = 0; i < mpd.periods.length; ++i) { + var period = mpd.periods[i]; + for (var j = 0; j < period.adaptationSets.length; ++j) { + var adaptationSet = period.adaptationSets[j]; + for (var k = 0; k < adaptationSet.representations.length; ++k) { + var representation = adaptationSet.representations[k]; + + if (!representation.segmentList) { + continue; + } + + var segmentList = representation.segmentList; + + var segmentIndex = this.createSegmentIndex_(segmentList); + if (!segmentIndex) { + // An error has already been logged. + adaptationSet.representations.splice(k, 1); + --k; + } + + // There could be hundreds of SegmentUrls; no need to keep them around. + segmentList.segmentUrls = []; + + segmentList.userData = segmentIndex; + } // for k + } + } +}; + + +/** + * Creates a SegmentIndex from a SegmentList. + * + * @param {!shaka.dash.mpd.SegmentList} segmentList + * + * @return {shaka.dash.SegmentIndex} A SegmentIndex on success; otherwise, + * return null. + * @private + */ +shaka.dash.MpdProcessor.prototype.createSegmentIndex_ = function(segmentList) { + var timescale = segmentList.timescale; + var presentationTimeOffset = segmentList.presentationTimeOffset; + var firstSegmentNumber = segmentList.firstSegmentNumber; + var segmentDuration = segmentList.segmentDuration; + + /** @type {!Array.} */ + var references = []; + + for (var i = 0; i < segmentList.segmentUrls.length; ++i) { + var segmentUrl = segmentList.segmentUrls[i]; + + /** @type {number} */ + var startTime = 0; + + /** @type {?number} */ + var endTime = null; + + /** @type {number} */ + var startByte = 0; + + /** @type {?number} */ + var endByte = null; + + // Note that |startTime| may be 0. + if (segmentUrl.startTime != null) { + shaka.asserts.assert(segmentUrl.mediaRange == null); + shaka.asserts.assert(segmentUrl.duration); + + if (i > 0 && segmentList.segmentUrls[i - 1].startTime != null) { + // Sanity check: there should not be a gap between the end of the last + // segment and the start of the current segment. + var lastTime = segmentList.segmentUrls[i - 1].startTime; + var lastDuration = segmentList.segmentUrls[i - 1].duration; + shaka.asserts.assert(lastTime + lastDuration >= segmentUrl.startTime); + } + + startTime = (presentationTimeOffset + segmentUrl.startTime) / timescale; + endTime = startTime + (segmentUrl.duration / timescale); + } else { + shaka.asserts.assert(segmentUrl.duration == null); + + if (!segmentDuration) { + shaka.log.warning( + 'SegmentList does not contain an explicit segment duration.', + segmentList); + return null; + } + + if (i == 0) { + startTime = presentationTimeOffset / timescale; + } else { + var lastTime = references[i - 1].startTime; + startTime = lastTime + (segmentDuration / timescale); + } + + endTime = startTime + (segmentDuration / timescale); + + if (segmentUrl.mediaRange) { + startByte = segmentUrl.mediaRange.begin; + endByte = segmentUrl.mediaRange.end; + } + } + + shaka.asserts.assert(segmentUrl.mediaUrl); + references.push( + new shaka.dash.SegmentReference( + i, + startTime, + endTime, + startByte, + endByte, + /** @type {!goog.Uri} */ (segmentUrl.mediaUrl))); + } + + return new shaka.dash.SegmentIndex(references); +}; + + +/** + * Remove the Representations from the given MPD that have inconsistent mime + * types, that specify unsupported types, or that specify unsupported DRM + * schemes. + * + * @param {!shaka.dash.mpd.Mpd} mpd + * @private + */ +shaka.dash.MpdProcessor.prototype.filterRepresentations_ = + function(mpd) { + for (var i = 0; i < mpd.periods.length; ++i) { + var period = mpd.periods[i]; + for (var j = 0; j < period.adaptationSets.length; ++j) { + var adaptationSet = period.adaptationSets[j]; + this.removeUnsupportedRepresentations_(adaptationSet); + this.removeInconsistentRepresentations_(adaptationSet); + if (adaptationSet.representations.length == 0) { + // Drop any AdaptationSet in which all Representations have been + // filtered out. An error has already been logged. + period.adaptationSets.splice(j, 1); + --j; + } + } + } +}; + + +/** + * Remove the Representations from the given AdaptationSet that have a + * different mime type than the mime type of the first Representation of the + * given AdaptationSet. + * + * @param {!shaka.dash.mpd.AdaptationSet} adaptationSet + * @private + */ +shaka.dash.MpdProcessor.prototype.removeInconsistentRepresentations_ = + function(adaptationSet) { + var desiredMimeType = null; + for (var i = 0; i < adaptationSet.representations.length; ++i) { + var representation = adaptationSet.representations[i]; + var mimeType = representation.mimeType || ''; + if (!desiredMimeType) { + desiredMimeType = mimeType; + } else if (mimeType != desiredMimeType) { + shaka.log.warning( + 'Representation has an inconsistent mime type.', + adaptationSet.representations[i]); + adaptationSet.representations.splice(i, 1); + --i; + } + } +}; + + +/** + * Remove the Representations from the given AdaptationSet that have + * unsupported types or that only use unsupported DRM schemes. + * + * @param {!shaka.dash.mpd.AdaptationSet} adaptationSet + * @private + */ +shaka.dash.MpdProcessor.prototype.removeUnsupportedRepresentations_ = + function(adaptationSet) { + var Player = shaka.player.Player; + + for (var i = 0; i < adaptationSet.representations.length; ++i) { + var representation = adaptationSet.representations[i]; + var type = shaka.dash.MpdProcessor.representationMimeType(representation); + + // Check which schemes the application understands. + var approvedSchemes = this.getApprovedSchemes_(representation); + // Filter through those now to find only the ones which use key systems + // and MIME types the browser supports. + var supportedSchemes = []; + var numSupported = 0; + for (var j = 0; j < approvedSchemes.length; ++j) { + var scheme = approvedSchemes[j]; + if (Player.isTypeSupported(scheme.keySystem, type)) { + supportedSchemes.push(scheme); + ++numSupported; + } + } + + // Drop any encrypted Representation whose MIME types and DRM schemes + // can't be supported by the browser. + if (numSupported == 0) { + shaka.log.warning( + 'Representation has an unsupported mime type and DRM ' + + 'scheme combination.', + adaptationSet.representations[i]); + adaptationSet.representations.splice(i, 1); + --i; + continue; + } + + // Store the list of schemes for this Representation. + representation.userData = supportedSchemes; + } +}; + + +/** + * Get all application-approved DRM schemes for a representation. + * + * @param {!shaka.dash.mpd.Representation} representation + * @return {!Array.} A list of application-approved + * schemes. A dummy scheme structure will be used for unencrypted content. + * This dummy scheme will have an empty string for |keySystem| and is used + * to simplify calculations later. + * @private + */ +shaka.dash.MpdProcessor.prototype.getApprovedSchemes_ = + function(representation) { + var approvedSchemes = []; + if (representation.contentProtections.length == 0) { + // Return a single item which indicates that the content is unencrypted. + approvedSchemes.push(shaka.player.DrmSchemeInfo.createUnencrypted()); + } else if (this.interpretContentProtection_) { + for (var i = 0; i < representation.contentProtections.length; ++i) { + var contentProtection = representation.contentProtections[i]; + var schemeInfo = this.interpretContentProtection_(contentProtection); + if (schemeInfo) { + approvedSchemes.push(schemeInfo); + } + } + } + return approvedSchemes; +}; + + +/** + * Populate each AdaptationSet with the DRM schemes common to all of its + * Representations. This is because we cannot change DRM schemes during + * playback, so we can only consider the schemes common to all of the + * Representations within an AdaptationSet. + * + * @param {!shaka.dash.mpd.Mpd} mpd + * @private + */ +shaka.dash.MpdProcessor.prototype.bubbleUpDrmSchemes_ = function(mpd) { + for (var i = 0; i < mpd.periods.length; ++i) { + var period = mpd.periods[i]; + for (var j = 0; j < period.adaptationSets.length; ++j) { + var adaptationSet = period.adaptationSets[j]; + this.bubbleUpDrmSchemesInAdaptationSet_(adaptationSet); + } + } +}; + + +/** + * Populate the given AdaptationSet with the DRM schemes common to all of its + * Representations. + * + * @param {!shaka.dash.mpd.AdaptationSet} adaptationSet + * @private + */ +shaka.dash.MpdProcessor.prototype.bubbleUpDrmSchemesInAdaptationSet_ = + function(adaptationSet) { + // Alias. + var DrmSchemeInfo = shaka.player.DrmSchemeInfo; + + // Start by building a map of all DRM schemes from the representations. + /** @type {!Object.} */ + var schemeMap = {}; + + for (var i = 0; i < adaptationSet.representations.length; ++i) { + var representation = adaptationSet.representations[i]; + var drmSchemes = /** @type {!Array.} */ ( + representation.userData); + + // Collect all unique keys. The same key may appear more than once in + // drmSchemes, in which case it should only be counted once. + var keyMap = {}; + for (var j = 0; j < drmSchemes.length; ++j) { + var drmScheme = drmSchemes[j]; + var key = drmScheme.key(); + keyMap[key] = drmScheme; + } + + for (var key in keyMap) { + if (!schemeMap.hasOwnProperty(key)) { + schemeMap[key] = {drmScheme: keyMap[key], count: 1}; + } else { + schemeMap[key].count++; + } + } + } + + // Find the key systems which appear in all Representations. + var numRepresentations = adaptationSet.representations.length; + var commonDrmSchemes = []; + for (var key in schemeMap) { + var entry = schemeMap[key]; + if (entry.count == numRepresentations) { + // This scheme is common to all representations. + commonDrmSchemes.push(entry.drmScheme); + } + } + + // Store the list of schemes for this AdaptationSet. + adaptationSet.userData = commonDrmSchemes; +}; + + +/** + * Sort Representations by bandwidth within each AdaptationSet in the MPD. + * + * @param {!shaka.dash.mpd.Mpd} mpd + * @private + */ +shaka.dash.MpdProcessor.prototype.sortRepresentations_ = function(mpd) { + for (var i = 0; i < mpd.periods.length; ++i) { + var period = mpd.periods[i]; + for (var j = 0; j < period.adaptationSets.length; ++j) { + var adaptationSet = period.adaptationSets[j]; + adaptationSet.representations.sort( + shaka.dash.MpdProcessor.compareByBandwidth_); + } + } +}; + + +/** + * @param {!shaka.dash.mpd.Representation} rep1 + * @param {!shaka.dash.mpd.Representation} rep2 + * @return {number} + * @private + */ +shaka.dash.MpdProcessor.compareByBandwidth_ = function(rep1, rep2) { + var b1 = rep1.bandwidth || Number.MAX_VALUE; + var b2 = rep2.bandwidth || Number.MAX_VALUE; + + if (b1 < b2) { + return -1; + } else if (b1 > b2) { + return 1; + } + + return 0; +}; + + +/** + * Choose AdaptationSets for each period. + * + * @param {!shaka.dash.mpd.Mpd} mpd + * @private + */ +shaka.dash.MpdProcessor.prototype.chooseAdaptationSets_ = function(mpd) { + this.adaptationSetMapAndDrmSchemeByPeriod_ = []; + for (var i = 0; i < mpd.periods.length; ++i) { + var period = mpd.periods[i]; + this.adaptationSetMapAndDrmSchemeByPeriod_.push( + this.chooseAdaptationSetsForPeriod_(period)); + } +}; + + +/** + * Choose AdaptationSets for a given period. + * + * @param {!shaka.dash.mpd.Period} period + * @return {?shaka.dash.MpdProcessor.AdaptationSetMapAndDrmScheme} + * @private + */ +shaka.dash.MpdProcessor.prototype.chooseAdaptationSetsForPeriod_ = + function(period) { + /** @type {!shaka.dash.MpdProcessor.AdaptationSetMap} */ + var byType = new shaka.util.MultiMap(); + + /** @type {!shaka.util.MultiMap.} */ + var byKeySystem = new shaka.util.MultiMap(); + + // Build multi-maps by both type and key system. + for (var i = 0; i < period.adaptationSets.length; ++i) { + var adaptationSet = period.adaptationSets[i]; + var type = adaptationSet.contentType || ''; + byType.push(type, adaptationSet); + + var drmSchemes = /** @type {!Array.} */ ( + adaptationSet.userData); + for (var j = 0; j < drmSchemes.length; ++j) { + var drmScheme = drmSchemes[j]; + byKeySystem.push(drmScheme.keySystem, adaptationSet); + } + } + + // For each desired type, make a list of key systems which can supply it. + // Keep track of the intersection of all of these lists. + var desiredTypes = ['audio', 'video', 'text']; + var intersection = null; + var allKeySystems = byKeySystem.keys(); + for (var i = 0; i < desiredTypes.length; ++i) { + var type = desiredTypes[i]; + var adaptationSets = byType.get(type); + if (!adaptationSets) { + // There is no such type available, so ignore it and move on. + shaka.log.warning('No AdaptationSets available for ' + type); + continue; + } + + var keySystems = this.buildKeySystemList_(adaptationSets, allKeySystems); + if (!intersection) { + intersection = keySystems; + } else { + intersection = shaka.util.ArrayUtils.intersect(intersection, keySystems); + } + } + + if (!intersection) { + // There are no key systems which can provide all desired types. + return null; + } + + // Any of the key systems in |intersection| is suitable. + var keySystem = intersection[0]; + + // But if unencrypted for everything is an option, prefer that. + if (intersection.indexOf('') >= 0) { + keySystem = ''; + } + + var tuple = this.chooseAdaptationSetsByKeySystem_(byType.getAll(), keySystem); + + tuple.adaptationSetMap = + this.chooseAdaptationSetsByMimeType_(tuple.adaptationSetMap); + + return tuple; +}; + + +/** + * Build a list of key systems which appear in the list of adaptation sets. + * If there is an unencrypted adaptation set, all key systems will appear in + * the output. This allows an unencrypted source to mix in with all other key + * systems. + * + * @param {!Array.} adaptationSets + * @param {!Array.} allKeySystems + * @return {!Array.} + * @private + */ +shaka.dash.MpdProcessor.prototype.buildKeySystemList_ = + function(adaptationSets, allKeySystems) { + /** @type {!Object.} */ + var keySystemMap = {}; + + for (var i = 0; i < adaptationSets.length; ++i) { + var adaptationSet = adaptationSets[i]; + var drmSchemes = /** @type {!Array.} */ ( + adaptationSet.userData); + for (var j = 0; j < drmSchemes.length; ++j) { + var drmScheme = drmSchemes[j]; + keySystemMap[drmScheme.keySystem] = null; + } + } + + if (keySystemMap.hasOwnProperty('')) { + // There is an unencrypted set in the list, so this list can match with + // any key system. + return allKeySystems; + } + + return shaka.util.ArrayUtils.fromObjectKeys(keySystemMap); +}; + + +/** + * Get the AdaptationSets that support the given key system, and get those + * AdaptationSets' common DRM scheme. + * + * @param {!Array.} adaptationSets + * @param {string} keySystem + * @return {shaka.dash.MpdProcessor.AdaptationSetMapAndDrmScheme} + * @private + */ +shaka.dash.MpdProcessor.prototype.chooseAdaptationSetsByKeySystem_ = + function(adaptationSets, keySystem) { + /** + * The AdaptationSets that support |keySystem|. + * @type {!shaka.util.MultiMap.} + */ + var allowableAdaptationSetMap = new shaka.util.MultiMap(); + + /** + * The DRM scheme shared by |allowableAdaptationSetMap|. + * @type {shaka.player.DrmSchemeInfo} + */ + var commonDrmScheme = null; + + for (var i = 0; i < adaptationSets.length; ++i) { + var adaptationSet = adaptationSets[i]; + var drmSchemes = /** @type {!Array.} */ ( + adaptationSet.userData); + for (var j = 0; j < drmSchemes.length; ++j) { + var drmScheme = drmSchemes[j]; + + // Unencrypted mixes with everything, so the empty keySystem is okay. + if (drmScheme.keySystem != keySystem && drmScheme.keySystem != '') + continue; + + shaka.asserts.assert(adaptationSet.contentType != null); + var type = /** @type {string} */ (adaptationSet.contentType); + allowableAdaptationSetMap.push(type, adaptationSet); + + if (!commonDrmScheme || !commonDrmScheme.keySystem) { + commonDrmScheme = drmScheme; + } else if (drmScheme.keySystem) { + commonDrmScheme = + shaka.player.DrmSchemeInfo.combine(commonDrmScheme, drmScheme); + } + } + } + + return { + adaptationSetMap: allowableAdaptationSetMap, + drmScheme: commonDrmScheme + }; +}; + + +/** + * Choose a single video AdaptationSet and a collection of audio AdaptationSets + * that each have the same mime type. It's assumed that within an + * AdaptationSet, each Representation has the same mime type as the first + * Representation within that AdaptationSet. + * + * @param {!shaka.dash.MpdProcessor.AdaptationSetMap} byType + * @return {!shaka.dash.MpdProcessor.AdaptationSetMap} + * @private + */ +shaka.dash.MpdProcessor.prototype.chooseAdaptationSetsByMimeType_ = + function(byType) { + var allowableAdaptationSetMap = new shaka.util.MultiMap(); + + // Add one video AdaptationSet. + var videoSets = byType.get('video'); + if (videoSets) { + allowableAdaptationSetMap.push('video', videoSets[0]); + } + + // Add audio AdaptationSets. + var audioSets = byType.get('audio'); + if (audioSets) { + for (var i = 0; i < audioSets.length; ++i) { + if (audioSets[i].mimeType == audioSets[0].mimeType) { + allowableAdaptationSetMap.push('audio', audioSets[i]); + } + } + } + + // Add text AdaptationSets. + var textSets = byType.get('text'); + if (textSets) { + for (var i = 0; i < textSets.length; ++i) { + allowableAdaptationSetMap.push('text', textSets[i]); + } + } + + return allowableAdaptationSetMap; +}; + diff --git a/lib/dash/mpd_request.js b/lib/dash/mpd_request.js new file mode 100644 index 0000000000..fd15d87fc7 --- /dev/null +++ b/lib/dash/mpd_request.js @@ -0,0 +1,62 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements an MPD request. + */ + +goog.provide('shaka.dash.MpdRequest'); + +goog.require('shaka.dash.mpd'); +goog.require('shaka.util.AjaxRequest'); + + + +/** + * Creates an MpdRequest. + * + * @param {string} url The URL. + * + * @struct + * @constructor + * @extends {shaka.util.AjaxRequest} + */ +shaka.dash.MpdRequest = function(url) { + shaka.util.AjaxRequest.call(this, url); + this.parameters.responseType = 'text'; +}; +goog.inherits(shaka.dash.MpdRequest, shaka.util.AjaxRequest); + + +/** + * Sends the MPD request. + * @return {!Promise.} + */ +shaka.dash.MpdRequest.prototype.send = function() { + var url = this.url; + return this.sendInternal().then( + /** @param {!XMLHttpRequest} xhr */ + function(xhr) { + var mpd = shaka.dash.mpd.parseMpd(xhr.responseText, url); + if (mpd) { + return Promise.resolve(mpd); + } + + var error = new Error('MPD parse failure.'); + error.type = 'mpd'; + return Promise.reject(error); + } + ); +}; + diff --git a/lib/dash/segment_index.js b/lib/dash/segment_index.js new file mode 100644 index 0000000000..929d85ff21 --- /dev/null +++ b/lib/dash/segment_index.js @@ -0,0 +1,118 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a segment index. + */ + +goog.provide('shaka.dash.SegmentIndex'); + +goog.require('shaka.asserts'); +goog.require('shaka.dash.SegmentRange'); +goog.require('shaka.dash.SegmentReference'); + + + +/** + * Creates a SegmentIndex. + * + * @param {!Array.} references Sorted by time in + * ascending order. + * @constructor + */ +shaka.dash.SegmentIndex = function(references) { + /** @private {!Array.} */ + this.references_ = references; +}; + + +/** + * Gets the number of SegmentReferences. + * + * @return {number} + */ +shaka.dash.SegmentIndex.prototype.getNumReferences = function() { + return this.references_.length; +}; + + +/** + * Gets the SegmentReference at the given index. + * + * @param {number} index + * @return {shaka.dash.SegmentReference} The SegmentReference or null if |index| + * is out of range. + */ +shaka.dash.SegmentIndex.prototype.getReference = function(index) { + if (index < 0 || index >= this.references_.length) { + return null; + } + + return this.references_[index]; +}; + + +/** + * Gets the SegmentRange that contains the timestamps |startTime| and + * |startTime| + |duration|. + * + * @param {number} startTime The starting time in seconds. + * @param {number} duration The interval's duration in seconds. + * @return {shaka.dash.SegmentRange} The SegmentRange or null if there are no + * segments. + */ +shaka.dash.SegmentIndex.prototype.getRangeForInterval = + function(startTime, duration) { + var beginIndex = this.findReferenceIndex(startTime); + if (beginIndex < 0) { + return null; + } + + /** @type {!Array.} */ + var references = []; + + for (var i = beginIndex; i < this.references_.length; i++) { + references.push(this.references_[i]); + var endTime = this.references_[i].endTime; + // Note: the end time is exclusive! + if (!endTime || (endTime > startTime + duration)) { + break; + } + } + + return new shaka.dash.SegmentRange(references); +}; + + +/** + * Gets the index of the SegmentReference for the specified time. + * + * @param {number} time The time in seconds. + * @return {number} The index of the SegmentReference for the specified time. + * 0 is returned if |time| is less than the first segment's time. The + * index of the last SegmentReference is returned if |time| is greater than + * the last segment's time. -1 is returned if there are no segments. + */ +shaka.dash.SegmentIndex.prototype.findReferenceIndex = function(time) { + for (var i = 0; i < this.references_.length; i++) { + if (this.references_[i].startTime > time) { + return i ? i - 1 : 0; + } + } + + // |time| is greater than the |startTime| field of all references, or there + // are no references. + return this.references_.length - 1; +}; + diff --git a/lib/dash/segment_range.js b/lib/dash/segment_range.js new file mode 100644 index 0000000000..5614bd772b --- /dev/null +++ b/lib/dash/segment_range.js @@ -0,0 +1,56 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a SegmentRange, which represents a set of one or + * more contiguous segments. + */ + +goog.provide('shaka.dash.SegmentRange'); + +goog.require('shaka.asserts'); +goog.require('shaka.dash.SegmentReference'); + + + +/** + * Creates a SegmentRange, which represents a set of one or more contiguous + * segments. + * + * @param {!Array.} references A set of + * references to contiguous segments. There must be at least one reference. + * + * @constructor + * @struct + */ +shaka.dash.SegmentRange = function(references) { + shaka.asserts.assert(references.length > 0); + + /** @type {!Array.} */ + this.references = references; +}; + + +/** + * Gets the time range. + * + * @return {{begin: number, end: ?number}} + */ +shaka.dash.SegmentRange.prototype.getTimeRange = function() { + return { + begin: this.references[0].startTime, + end: this.references[this.references.length - 1].endTime + }; +}; + diff --git a/lib/dash/segment_reference.js b/lib/dash/segment_reference.js new file mode 100644 index 0000000000..2a0480ffab --- /dev/null +++ b/lib/dash/segment_reference.js @@ -0,0 +1,100 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a segment reference structure. + */ + +goog.provide('shaka.dash.SegmentReference'); + +goog.require('goog.Uri'); + + + +/** + * Creates a SegmentReference, which is a reference to some media segment. + * + * @param {number} index The segment's number, starting at 0, within the stream. + * @param {number} startTime The time, in seconds, that the segment begins. + * @param {?number} endTime The time, in seconds, that the segment ends. The + * segment ends immediately before this time. A null value indicates that + * the segment continues to the end of the stream. + * @param {number} startByte The position of the segment's first byte. + * @param {?number} endByte The position of the segment's last byte, inclusive. + * A null value indicates that the segment continues to the end of the + * file located at |url|. + * @param {!goog.Uri} url The segment's location. + * @constructor + * @struct + */ +shaka.dash.SegmentReference = function( + index, startTime, endTime, startByte, endByte, url) { + /** + * The segment's number, starting at 0, within the stream. + * @const {number} + */ + this.index = index; + + /** + * The time, in seconds, that the segment begins. + * @const {number} + */ + this.startTime = startTime; + + /** + * The time, in seconds, that the segment ends. The segment ends immediately + * before this time. A null value indicates that the segment continues to the + * end of the stream. + * @const {?number} + */ + this.endTime = endTime; + + /** + * The position of the segment's first byte. + * @const {number} + */ + this.startByte = startByte; + + /** + * The position of the segment's last byte, inclusive. A null value indicates + * that the segment continues to the end of the file located at |url|. + * @const {?number} + */ + this.endByte = endByte; + + /** + * The segment's location. + * @const {!goog.Uri} + */ + this.url = url; +}; + + +/** + * Compares two SegmentReference objects by their start time. + * @param {!shaka.dash.SegmentReference} ref1 + * @param {!shaka.dash.SegmentReference} ref2 + * @return {number} + * @export + */ +shaka.dash.SegmentReference.compare = function(ref1, ref2) { + if (ref1.startTime < ref2.startTime) { + return -1; + } else if (ref1.startTime > ref2.startTime) { + return 1; + } + + return 0; +}; + diff --git a/lib/dash/source_buffer_manager.js b/lib/dash/source_buffer_manager.js new file mode 100644 index 0000000000..1e99f4490f --- /dev/null +++ b/lib/dash/source_buffer_manager.js @@ -0,0 +1,546 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Manages a SourceBuffer an provides an enhanced interface + * based on Promises. + */ + +goog.provide('shaka.dash.SourceBufferManager'); + +goog.require('shaka.asserts'); +goog.require('shaka.dash.SegmentRange'); +goog.require('shaka.util.EventManager'); +goog.require('shaka.util.IBandwidthEstimator'); +goog.require('shaka.util.PublicPromise'); +goog.require('shaka.util.RangeRequest'); + + + +/** + * Creates a SourceBufferManager (SBM). + * + * The SBM manages access to a SourceBuffer object through a fetch operation + * and a clear operation. It also maintains a "virtual source buffer" to keep + * track of which segments have been appended to the actual underlying source + * buffer. The SBM uses this virtual source buffer because it cannot rely on + * the browser to tell it what is in the underlying SourceBuffer because the + * segment index may use PTS (presentation timestamps) and the browser may use + * DTS (decoding timestamps) or vice-versa. + * + * @param {!MediaSource} mediaSource The SourceBuffer's parent MediaSource. + * @param {!SourceBuffer} sourceBuffer + * @param {shaka.util.IBandwidthEstimator} estimator A bandwidth estimator to + * attach to all requests. + * @struct + * @constructor + */ +shaka.dash.SourceBufferManager = function( + mediaSource, sourceBuffer, estimator) { + /** @private {!MediaSource} */ + this.mediaSource_ = mediaSource; + + /** @private {!SourceBuffer} */ + this.sourceBuffer_ = sourceBuffer; + + /** @private {shaka.util.IBandwidthEstimator} */ + this.estimator_ = estimator; + + /** @private {!shaka.util.EventManager} */ + this.eventManager_ = new shaka.util.EventManager(); + + /** + * An array that indicates which segments are buffered. + * @private {!Array.} + */ + this.buffered_ = []; + + /** @private {shaka.dash.SourceBufferManager.State_} */ + this.state_ = shaka.dash.SourceBufferManager.State_.IDLE; + + /** @private {Promise} */ + this.promise_ = null; + + /** @private {Promise} */ + this.abortPromise_ = null; + + /** + * The current SegmentReferences being fetched or appended. + * @private {!Array.} + */ + this.references_ = []; + + /** + * The current request while fetching. + * @private {shaka.util.RangeRequest} + */ + this.request_ = null; + + /** + * The current segment data being fetched or appended. + * @private {!Array.} + */ + this.segments_ = []; + + this.eventManager_.listen( + this.sourceBuffer_, + 'updateend', + this.onSourceBufferUpdateEnd_.bind(this)); +}; + + +/** + * SBM states. + * @enum + * @private + */ +shaka.dash.SourceBufferManager.State_ = { + IDLE: 0, + REQUESTING: 1, + APPENDING: 2, + CLEARING: 3, + ABORTING: 4 +}; + + +/** + * Destroys the SourceBufferManager. + * @suppress {checkTypes} to set otherwise non-nullable types to null. + */ +shaka.dash.SourceBufferManager.prototype.destroy = function() { + this.abort(); + + this.state_ = null; + this.segments_ = null; + this.request_ = null; + this.references_ = null; + this.abortPromise_ = null; + this.promise_ = null; + + this.eventManager_.destroy(); + this.eventManager_ = null; + + this.buffered_ = null; + + this.sourceBuffer_ = null; + this.mediaSource_ = null; +}; + + +/** + * Checks if the given segment is buffered. + * + * @param {number} index The segment's index. + * @return {boolean} True if the segment is buffered. + */ +shaka.dash.SourceBufferManager.prototype.isBuffered = function(index) { + return this.buffered_[index] == true; +}; + + +/** + * Fetches the segments specified by the given SegmentRange and appends the + * retrieved segment data to the underlying SourceBuffer. This cannot be called + * if another operation is in progress. + * + * @param {!shaka.dash.SegmentRange} segmentRange + * @param {!ArrayBuffer=} opt_initSegment Optional initialization segment that + * will be appended to the underlying SourceBuffer before the retrieved + * segment data. + * + * @return {!Promise} + */ +shaka.dash.SourceBufferManager.prototype.fetch = function( + segmentRange, opt_initSegment) { + shaka.log.v1('fetch'); + + // Alias. + var SBM = shaka.dash.SourceBufferManager; + + // Check state. + shaka.asserts.assert(this.state_ == SBM.State_.IDLE); + if (this.state_ != SBM.State_.IDLE) { + var error = new Error('Cannot fetch: previous operation not complete.'); + error.type = 'dash'; + return Promise.reject(error); + } + + shaka.asserts.assert(this.promise_ == null); + shaka.asserts.assert(this.references_.length == 0); + shaka.asserts.assert(this.request_ == null); + shaka.asserts.assert(this.segments_.length == 0); + + this.state_ = SBM.State_.REQUESTING; + this.promise_ = new shaka.util.PublicPromise(); + this.references_ = segmentRange.references; + + if (opt_initSegment) { + this.segments_.push(opt_initSegment); + } + + // If the segments are all located at the same URL then only a single request + // is required. + var singleLocation = true; + + var firstUrl = this.references_[0].url.toString(); + for (var i = 1; i < this.references_.length; ++i) { + if (this.references_[i].url.toString() != firstUrl) { + singleLocation = false; + break; + } + } + + // Send the request. If this.abort() is called before |this.request_|'s + // promise is resolved then |this.request_|'s promise will be rejected via a + // call to this.request_.abort(). + var p = singleLocation ? + this.fetchFromSingleUrl_() : + this.fetchFromMultipleUrls_(); + + p.then(shaka.util.TypedBind(this, + function() { + shaka.log.debug('Estimated bandwidth:', + (this.estimator_.getBandwidth() / 1e6).toFixed(2), 'Mbps'); + + this.sourceBuffer_.appendBuffer(this.segments_.shift()); + this.state_ = SBM.State_.APPENDING; + this.request_ = null; + }) + ).catch(shaka.util.TypedBind(this, + /** @param {!Error} error */ + function(error) { + if (error.type != 'aborted') { + this.rejectPromise_(error); + } + }) + ); + + return this.promise_; +}; + + +/** + * Returns a promise to fetch one or more segments from the same location. The + * promise will resolve once the request completes. This synchronously sets + * |request_| to the request in progress. + * + * @return {!Promise} + * @private + */ +shaka.dash.SourceBufferManager.prototype.fetchFromSingleUrl_ = function() { + shaka.log.v1('fetchFromSingleUrl_'); + shaka.asserts.assert(this.references_.length > 0); + shaka.asserts.assert(this.request_ == null); + + this.request_ = new shaka.util.RangeRequest( + this.references_[0].url.toString(), + this.references_[0].startByte, + this.references_[this.references_.length - 1].endByte); + + this.request_.estimator = this.estimator_; + + return this.request_.send().then(this.appendSegment_.bind(this)); +}; + + +/** + * Returns a promise to fetch multiple segments from different locations. The + * promise will resolve once the last request completes. This synchronously + * sets |request_| to the first request and then asynchronously sets |request_| + * to the request in progress. + * + * @return {!Promise} + * @private + */ +shaka.dash.SourceBufferManager.prototype.fetchFromMultipleUrls_ = function() { + shaka.log.v1('fetchFromMultipleUrls_'); + shaka.asserts.assert(this.references_.length > 0); + shaka.asserts.assert(this.request_ == null); + + /** + * Requests the segment specified by |reference|. + * @param {!shaka.dash.SegmentReference} reference + * @this {shaka.dash.SourceBufferManager} + * @return {!Promise.} + */ + var requestSegment = function(reference) { + this.request_ = new shaka.util.RangeRequest( + reference.url.toString(), + reference.startByte, + reference.endByte); + + this.request_.estimator = this.estimator_; + + return this.request_.send(); + }; + + // Request the first segment. + var p = shaka.util.TypedBind(this, requestSegment)(this.references_[0]); + + // Request the subsequent segments. + var appendSegment = this.appendSegment_.bind(this); + for (var i = 1; i < this.references_.length; ++i) { + var requestNextSegment = requestSegment.bind(this, this.references_[i]); + p = p.then(appendSegment).then(requestNextSegment); + } + + p = p.then(shaka.util.TypedBind(this, this.appendSegment_)); + + return p; +}; + + +/** + * Appends |data| to |segments_|. + * + * @param {!ArrayBuffer} data + * @return {!Promise} + * @private + */ +shaka.dash.SourceBufferManager.prototype.appendSegment_ = function(data) { + this.segments_.push(data); + return Promise.resolve(); +}; + + +/** + * Resets the virtual source buffer and clears all media from the underlying + * SourceBuffer. The returned promise will resolve immediately if there is no + * media within the underlying SourceBuffer. This cannot be called if another + * operation is in progress. + * + * @return {!Promise} + */ +shaka.dash.SourceBufferManager.prototype.clear = function() { + shaka.log.v1('clear'); + + // Alias. + var SBM = shaka.dash.SourceBufferManager; + + // Check state. + shaka.asserts.assert(this.state_ == SBM.State_.IDLE); + if (this.state_ != SBM.State_.IDLE) { + var error = new Error('Cannot clear: previous operation not complete.'); + error.type = 'dash'; + return Promise.reject(error); + } + + shaka.asserts.assert(this.promise_ == null); + shaka.asserts.assert(this.references_.length == 0); + shaka.asserts.assert(this.request_ == null); + shaka.asserts.assert(this.segments_.length == 0); + + if (this.sourceBuffer_.buffered.length == 0) { + shaka.log.v1('Nothing to clear.'); + shaka.asserts.assert(this.buffered_.length == 0); + return Promise.resolve(); + } + + try { + // This will trigger an 'updateend' event. + this.sourceBuffer_.remove(0, Number.POSITIVE_INFINITY); + } catch (exception) { + shaka.log.debug('Failed to clear buffer:', exception); + return Promise.reject(exception); + } + + // Clear |buffered_| immediately since any buffered segments will be + // gone soon. + this.buffered_ = []; + + this.state_ = SBM.State_.CLEARING; + this.promise_ = new shaka.util.PublicPromise(); + + return this.promise_; +}; + + +/** + * Resets the virtual source buffer without removing any media from the + * underlying SourceBuffer. This can be called at any time. + */ +shaka.dash.SourceBufferManager.prototype.reset = function() { + this.buffered_ = []; +}; + + +/** + * Aborts the current operation if one exists. This should not be called + * if the current operation is an abort operation. The returned promise + * will never be rejected. + * + * @return {!Promise} + */ +shaka.dash.SourceBufferManager.prototype.abort = function() { + shaka.log.v1('abort'); + + // Alias. + var SBM = shaka.dash.SourceBufferManager; + + shaka.asserts.assert(this.abortPromise_ == null); + shaka.asserts.assert(this.state_ != SBM.State_.ABORTING); + + switch (this.state_) { + case SBM.State_.IDLE: + return Promise.resolve(); + case SBM.State_.REQUESTING: + shaka.log.info('Aborting request...'); + shaka.asserts.assert(this.request_); + this.state_ = SBM.State_.ABORTING; + + // We do not need to wait for |request_| to completely stop. It is + // enough to know that no SourceBuffer operations are in progress when + // the abort promise is resolved. + + // Create a new promise where resolveAbortPromise_() will look for it. + this.abortPromise_ = new shaka.util.PublicPromise(); + // Keep a local reference since resolveAbortPromise_() will nullify it. + var p = this.abortPromise_; + // Abort the request. + this.request_.abort(); + // Reject the original promise and resolve the abort promise. + this.resolveAbortPromise_(); + // Return the local reference to the abort promise. + return p; + case SBM.State_.APPENDING: + case SBM.State_.CLEARING: + shaka.log.info('Aborting append/clear...'); + this.state_ = SBM.State_.ABORTING; + this.abortPromise_ = new shaka.util.PublicPromise(); + // If |mediaSource_| is open and aborting will not cause an exception, + // call abort() on |sourceBuffer_|. This will trigger an 'updateend' + // event if updating (e.g., appending or removing). + if (this.mediaSource_.readyState == 'open') { + this.sourceBuffer_.abort(); + } + shaka.asserts.assert(this.sourceBuffer_.updating == false); + return this.abortPromise_; + case SBM.State_.ABORTING: + // This case should not happen, but handle it just in case it occurs in + // production. + shaka.log.error('Already aborting!'); + shaka.asserts.assert(this.abortPromise_); + return /** @type {!Promise} */ (this.abortPromise_); + } + + shaka.asserts.unreachable(); +}; + + +/** + * |sourceBuffer_|'s 'updateend' callback. + * + * @param {!Event} event + * + * @private + */ +shaka.dash.SourceBufferManager.prototype.onSourceBufferUpdateEnd_ = + function(event) { + shaka.log.v1('onSourceBufferUpdateEnd_'); + + // Alias. + var SBM = shaka.dash.SourceBufferManager; + + shaka.asserts.assert(this.sourceBuffer_.updating == false); + shaka.asserts.assert(this.state_ == SBM.State_.APPENDING || + this.state_ == SBM.State_.CLEARING || + this.state_ == SBM.State_.ABORTING); + shaka.asserts.assert(this.promise_); + shaka.asserts.assert(this.request_ == null); + + switch (this.state_) { + case SBM.State_.APPENDING: + // A segment has been appended so update |buffered_|. + shaka.asserts.assert(this.references_.length > 0); + + if (this.segments_.length > 0) { + // Append the next segment. + try { + this.sourceBuffer_.appendBuffer(this.segments_.shift()); + } catch (exception) { + shaka.log.debug('Failed to append buffer:', exception); + this.rejectPromise_(exception); + } + return; + } + + // Update |buffered_|. Note that if we abort an append then there may be + // segments in the underlying source buffer that are not indicated in + // |buffered_|. However, this should not cause any harm. + for (var i = 0; i < this.references_.length; ++i) { + var r = this.references_[i]; + this.buffered_[r.index] = true; + // To solve bug #18597156, where buffered ranges manifest a gap after + // seeking. All data after the gap is unusable, so always treat the + // next segment as one you don't have. If you don't seek, or if you + // only seek forward, this has no effect. + this.buffered_[r.index + 1] = false; + } + this.references_ = []; + + // Fall-through. + case SBM.State_.CLEARING: + this.state_ = SBM.State_.IDLE; + this.promise_.resolve(); + this.promise_ = null; + break; + case SBM.State_.ABORTING: + this.resolveAbortPromise_(); + break; + default: + shaka.asserts.unreachable(); + } +}; + + +/** + * Resolves |abortPromise_|, and then calls rejectPromise_(). + * + * @private + */ +shaka.dash.SourceBufferManager.prototype.resolveAbortPromise_ = function() { + shaka.log.v1('resolveAbortPromise_'); + shaka.asserts.assert(this.abortPromise_); + + this.abortPromise_.resolve(); + this.abortPromise_ = null; + + var error = new Error('Current operation aborted.'); + error.type = 'aborted'; + + this.rejectPromise_(error); +}; + + +/** + * Rejects |promise_| and puts the SBM into the IDLE state. + * + * @param {!Error} error + * + * @private + */ +shaka.dash.SourceBufferManager.prototype.rejectPromise_ = function(error) { + shaka.log.v1('rejectPromise_'); + shaka.asserts.assert(this.promise_); + shaka.asserts.assert(this.abortPromise_ == null); + + this.promise_.reject(error); + + this.state_ = shaka.dash.SourceBufferManager.State_.IDLE; + this.promise_ = null; + this.references_ = []; + this.request_ = null; + this.segments_ = []; +}; + diff --git a/lib/dash/webm_segment_index_parser.js b/lib/dash/webm_segment_index_parser.js new file mode 100644 index 0000000000..37479342d2 --- /dev/null +++ b/lib/dash/webm_segment_index_parser.js @@ -0,0 +1,346 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a WebM segment index parser. + */ + +goog.provide('shaka.dash.WebmSegmentIndexParser'); + +goog.require('shaka.asserts'); +goog.require('shaka.dash.ISegmentIndexParser'); +goog.require('shaka.dash.SegmentReference'); +goog.require('shaka.log'); +goog.require('shaka.util.EbmlElement'); +goog.require('shaka.util.EbmlParser'); + + + +/** + * A parser for WebM index data. + * + * @param {!goog.Uri} mediaUrl The location of the segments, i.e., all parsed + * SegmentReferences are assumed to be retrievable from |mediaUrl|. + * + * @constructor + * @implements {shaka.dash.ISegmentIndexParser} + */ +shaka.dash.WebmSegmentIndexParser = function(mediaUrl) { + /** @private {!goog.Uri} */ + this.mediaUrl_ = mediaUrl; +}; + + +/** @override */ +shaka.dash.WebmSegmentIndexParser.prototype.parse = + function(initSegmentData, indexData, indexOffset) { + var references = null; + + shaka.asserts.assert(initSegmentData); + try { + references = this.parseInternal_( + /** @type {!DataView} */ (initSegmentData), indexData); + } catch (exception) { + if (!(exception instanceof RangeError)) { + throw exception; + } + } + + return references; +}; + + +/** @const {number} */ +shaka.dash.WebmSegmentIndexParser.EBML_ID = 0x1a45dfa3; + + +/** @const {number} */ +shaka.dash.WebmSegmentIndexParser.SEGMENT_ID = 0x18538067; + + +/** @const {number} */ +shaka.dash.WebmSegmentIndexParser.INFO_ID = 0x1549a966; + + +/** @const {number} */ +shaka.dash.WebmSegmentIndexParser.TIMECODE_SCALE_ID = 0x2ad7b1; + + +/** @const {number} */ +shaka.dash.WebmSegmentIndexParser.CUES_ID = 0x1c53bb6b; + + +/** @const {number} */ +shaka.dash.WebmSegmentIndexParser.CUE_POINT_ID = 0xbb; + + +/** @const {number} */ +shaka.dash.WebmSegmentIndexParser.CUE_TIME_ID = 0xb3; + + +/** @const {number} */ +shaka.dash.WebmSegmentIndexParser.CUE_TRACK_POSITIONS_ID = 0xb7; + + +/** @const {number} */ +shaka.dash.WebmSegmentIndexParser.CUE_CLUSTER_POSITION = 0xf1; + + +/** + * Parses a segment index from a WebM container. + * @param {!DataView} webmData The WebM container data. + * @param {!DataView} cuesData The WebM container's "Cueing Data" section. + * @return {Array.} The segment references, or + * null if an error occurred + * @throws {RangeError} + * @see http://www.matroska.org/technical/specs/index.html + * @see http://www.webmproject.org/docs/container/ + * @private + */ +shaka.dash.WebmSegmentIndexParser.prototype.parseInternal_ = function( + webmData, cuesData) { + var tuple = this.parseWebmContainer_(webmData); + if (!tuple) { + return null; + } + + var parser = new shaka.util.EbmlParser(cuesData); + var cuesElement = parser.parseElement(); + if (cuesElement.id != shaka.dash.WebmSegmentIndexParser.CUES_ID) { + shaka.log.error('CuesElement does not exist.'); + return null; + } + + return this.parseCues_(cuesElement, tuple.segmentOffset, tuple.timecodeScale); +}; + + +/** + * Parses a WebM container to get the segment's offset and timecode scale. + * @param {!DataView} webmData + * @return {?{segmentOffset: number, timecodeScale: number}} The segment's + * offset in bytes, and the segment's timecode scale in seconds. + * @private + */ +shaka.dash.WebmSegmentIndexParser.prototype.parseWebmContainer_ = function( + webmData) { + var parser = new shaka.util.EbmlParser(webmData); + + // Check that the WebM container data starts with the EBML header, but + // skip its contents. + var ebmlElement = parser.parseElement(); + if (ebmlElement.id != shaka.dash.WebmSegmentIndexParser.EBML_ID) { + shaka.log.error('EBML element does not exist.'); + return null; + } + + var segmentElement = parser.parseElement(); + if (segmentElement.id != shaka.dash.WebmSegmentIndexParser.SEGMENT_ID) { + shaka.log.error('Segment element does not exist.'); + return null; + } + + // This value is used as the initial offset to the first referenced segment. + var segmentOffset = segmentElement.getOffset(); + + // Parse the Segment element to get the segment's timecode scale. + var timecodeScale = this.parseSegment_(segmentElement); + if (!timecodeScale) { + return null; + } + + return { segmentOffset: segmentOffset, timecodeScale: timecodeScale }; +}; + + +/** + * Parses a WebM Info element to get the segment's timecode scale. + * @param {!shaka.util.EbmlElement} segmentElement + * @return {?number} The segment's timecode scale in seconds, or null if an + * error occurred. + * @private + */ +shaka.dash.WebmSegmentIndexParser.prototype.parseSegment_ = function( + segmentElement) { + var parser = segmentElement.createParser(); + + // Find the Info element. + var infoElement = null; + while (parser.hasMoreData()) { + var elem = parser.parseElement(); + if (elem.id != shaka.dash.WebmSegmentIndexParser.INFO_ID) { + continue; + } + + infoElement = elem; + + break; + } + + if (!infoElement) { + shaka.log.error('Info element does not exist.'); + return null; + } + + return this.parseInfo_(infoElement); +}; + + +/** + * Parses a WebM Info element to get the segment's timecode scale. + * @param {!shaka.util.EbmlElement} infoElement + * @return {number} The segment's timecode scale in seconds. + * @private + */ +shaka.dash.WebmSegmentIndexParser.prototype.parseInfo_ = function(infoElement) { + var parser = infoElement.createParser(); + + // The timecode scale factor in units of [nanoseconds / T], where [T] are the + // units used to express all other time values in the WebM container. By + // default it's assumed that [T] == [milliseconds]. + var timecodeScaleNanoseconds = 1000000; + + while (parser.hasMoreData()) { + var elem = parser.parseElement(); + if (elem.id != shaka.dash.WebmSegmentIndexParser.TIMECODE_SCALE_ID) { + continue; + } + + timecodeScaleNanoseconds = elem.getUint(); + + break; + } + + // The timecode scale factor in units of [seconds / T]. + var timecodeScale = timecodeScaleNanoseconds / 1000000000; + + return timecodeScale; +}; + + +/** + * Parses a WebM CuesElement. + * @param {!shaka.util.EbmlElement} cuesElement + * @param {number} segmentOffset + * @param {number} timecodeScale + * @return {Array.} The segment references. + * @private + */ +shaka.dash.WebmSegmentIndexParser.prototype.parseCues_ = function( + cuesElement, segmentOffset, timecodeScale) { + var parser = cuesElement.createParser(); + + /** @type {Array.} */ + var references = []; + + var lastTime = -1; + var lastOffset = -1; + var index = 0; + + while (parser.hasMoreData()) { + var elem = parser.parseElement(); + if (elem.id != shaka.dash.WebmSegmentIndexParser.CUE_POINT_ID) { + continue; + } + + var tuple = this.parseCuePoint_(elem); + if (!tuple) { + continue; + } + + var currentTime = timecodeScale * tuple.unscaledTime; + var currentOffset = segmentOffset + tuple.relativeOffset; + + if (lastTime >= 0) { + shaka.asserts.assert(lastOffset >= 0); + + references.push( + new shaka.dash.SegmentReference( + index, + lastTime, + currentTime, + lastOffset, + currentOffset - 1, + this.mediaUrl_)); + + ++index; + } + + lastTime = currentTime; + lastOffset = currentOffset; + } + + if (lastTime >= 0) { + shaka.asserts.assert(lastOffset >= 0); + + references.push( + new shaka.dash.SegmentReference( + index, + lastTime, + null, + lastOffset, + null, + this.mediaUrl_)); + } + + return references; +}; + + +/** + * Parses a WebM CuePointElement to get an "unadjusted" segment reference. + * @param {shaka.util.EbmlElement} cuePointElement + * @return {?{unscaledTime: number, relativeOffset: number}} The referenced + * segment's start time in units of [T] (see parseInfo_()), and the + * referenced segment's offset in bytes, relative to a WebM Segment + * element. + * @private + */ +shaka.dash.WebmSegmentIndexParser.prototype.parseCuePoint_ = function( + cuePointElement) { + var parser = cuePointElement.createParser(); + + // Parse CueTime element. + var cueTimeElement = parser.parseElement(); + if (cueTimeElement.id != shaka.dash.WebmSegmentIndexParser.CUE_TIME_ID) { + shaka.log.warning('CueTime element does not exist.'); + return null; + } + var unscaledTime = cueTimeElement.getUint(); + + // Parse CueTrackPositions element. + var cueTrackPositionsElement = parser.parseElement(); + if (cueTrackPositionsElement.id != + shaka.dash.WebmSegmentIndexParser.CUE_TRACK_POSITIONS_ID) { + shaka.log.warning('CueTrackPositions element does not exist.'); + return null; + } + + var cueTrackParser = cueTrackPositionsElement.createParser(); + var relativeOffset = 0; + + while (cueTrackParser.hasMoreData()) { + var elem = cueTrackParser.parseElement(); + if (elem.id != shaka.dash.WebmSegmentIndexParser.CUE_CLUSTER_POSITION) { + continue; + } + + relativeOffset = elem.getUint(); + + break; + } + + return { unscaledTime: unscaledTime, relativeOffset: relativeOffset }; +}; + diff --git a/lib/debug/asserts.js b/lib/debug/asserts.js new file mode 100644 index 0000000000..b0cc401022 --- /dev/null +++ b/lib/debug/asserts.js @@ -0,0 +1,57 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements assert functions which can be compiled out. + */ + +goog.provide('shaka.asserts'); + + +/** + * @namespace shaka.asserts + * @summary An assertion framework which is compiled out for deployment. + */ + + +/** + * @define {boolean} true to enable asserts, false otherwise. + */ +goog.define('shaka.asserts.ENABLE_ASSERTS', goog.DEBUG); + + +/** @type {function()|function(*, string=)} */ +shaka.asserts.assert = function() {}; + + +/** @type {function()} */ +shaka.asserts.notImplemented = function() {}; + + +/** @type {function()} */ +shaka.asserts.unreachable = function() {}; + + +// Install assert functions. +if (shaka.asserts.ENABLE_ASSERTS) { + shaka.asserts.assert = + console.assert.bind(console); + + shaka.asserts.notImplemented = + console.assert.bind(console, 0 == 1, 'Not implemented.'); + + shaka.asserts.unreachable = + console.assert.bind(console, 0 == 1, 'Unreachable reached.'); +} + diff --git a/lib/debug/log.js b/lib/debug/log.js new file mode 100644 index 0000000000..7751813cc7 --- /dev/null +++ b/lib/debug/log.js @@ -0,0 +1,121 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements log functions which can be compiled out. + */ + +goog.provide('shaka.log'); + + +/** + * @namespace shaka.log + * @summary A console logging framework which is compiled out for deployment. + */ + + +/** + * Log levels. + * @enum {number} + */ +shaka.log.Level = { + NONE: 0, + ERROR: 1, + WARNING: 2, + INFO: 3, + DEBUG: 4, + V1: 5, + V2: 6 +}; + + +/** + * @define {number} the maximum log level. + */ +goog.define('shaka.log.MAX_LOG_LEVEL', 3); + + +/** @type {function(*, ...[*])} */ +shaka.log.error = function() {}; + + +/** @type {function(*, ...[*])} */ +shaka.log.warning = function() {}; + + +/** @type {function(*, ...[*])} */ +shaka.log.info = function() {}; + + +/** @type {function(*, ...[*])} */ +shaka.log.debug = function() {}; + + +/** @type {function(*, ...[*])} */ +shaka.log.v1 = function() {}; + + +/** @type {function(*, ...[*])} */ +shaka.log.v2 = function() {}; + + +if (!COMPILED) { + /** + * Change the log level. Useful for debugging in uncompiled mode. + * + * @param {number} level + */ + shaka.log.setLevel = function(level) { + var nop = function() {}; + var log = shaka.log; + var Level = shaka.log.Level; + + log.error = (level >= Level.ERROR) ? console.error.bind(console) : nop; + log.warning = (level >= Level.WARNING) ? console.warn.bind(console) : nop; + log.info = (level >= Level.INFO) ? console.info.bind(console) : nop; + log.debug = (level >= Level.DEBUG) ? console.log.bind(console) : nop; + log.v1 = (level >= Level.V1) ? console.debug.bind(console) : nop; + log.v2 = (level >= Level.V2) ? console.debug.bind(console) : nop; + }; +} + + +// Although these bindings are redundant with setLevel() above, refactoring to +// call a method here makes it so that the log messages themselves cannot be +// compiled out. + +if (shaka.log.MAX_LOG_LEVEL >= shaka.log.Level.ERROR) { + shaka.log.error = console.error.bind(console); +} + +if (shaka.log.MAX_LOG_LEVEL >= shaka.log.Level.WARNING) { + shaka.log.warning = console.warn.bind(console); +} + +if (shaka.log.MAX_LOG_LEVEL >= shaka.log.Level.INFO) { + shaka.log.info = console.info.bind(console); +} + +if (shaka.log.MAX_LOG_LEVEL >= shaka.log.Level.DEBUG) { + shaka.log.debug = console.log.bind(console); +} + +if (shaka.log.MAX_LOG_LEVEL >= shaka.log.Level.V1) { + shaka.log.v1 = console.debug.bind(console); +} + +if (shaka.log.MAX_LOG_LEVEL >= shaka.log.Level.V2) { + shaka.log.v2 = console.debug.bind(console); +} + diff --git a/lib/debug/timer.js b/lib/debug/timer.js new file mode 100644 index 0000000000..6c222efe1f --- /dev/null +++ b/lib/debug/timer.js @@ -0,0 +1,109 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements performance timers. + */ + +goog.provide('shaka.timer'); + +goog.require('shaka.log'); + + +/** + * @namespace shaka.timer + * @summary A performance timing framework. + * Used in both debugging and production builds. + */ + + +/** + * Begins a timer. + * + * @param {string} name + */ +shaka.timer.begin = function(name) { + shaka.timer.timers_[name] = { + begin: shaka.timer.now_(), + end: NaN + }; +}; + + +/** + * End a timer and log (debug) the elapsed time. + * Does nothing if the timer has not begun. + * + * @param {string} name + */ +shaka.timer.end = function(name) { + var record = shaka.timer.timers_[name]; + if (!record) { + return; + } + + record.end = shaka.timer.now_(); + var diff = record.end - record.begin; + shaka.log.debug(name + ': ' + diff.toFixed(3) + 'ms'); +}; + + +/** + * Log (debug) the diff between two completed timers and return it. + * Does nothing if either timer has not begun. + * + * @param {string} name1 + * @param {string} name2 + * @return {number} The diff between the two, or NaN if they are not both + * completed. + */ +shaka.timer.diff = function(name1, name2) { + var t1 = shaka.timer.get(name1); + var t2 = shaka.timer.get(name2); + if (!t1 || !t2) { + return NaN; + } + var diff = t1 - t2; + var name = name1 + ' - ' + name2; + shaka.log.debug(name + ': ' + diff.toFixed(3) + 'ms'); + return diff; +}; + + +/** + * Query a timer. + * + * @param {string} name + * @return {number} The elapsed time in milliseconds, if the timer is complete. + * Returns NaN if the timer doesn't exist or hasn't ended yet. + */ +shaka.timer.get = function(name) { + var record = shaka.timer.timers_[name]; + if (!record || !record.end) { + return NaN; + } + + return record.end - record.begin; +}; + + +/** @private {function():number} */ +shaka.timer.now_ = window.performance ? + window.performance.now.bind(window.performance) : + Date.now; + + +/** @private {!Object.} */ +shaka.timer.timers_ = {}; + diff --git a/lib/player/audio_track.js b/lib/player/audio_track.js new file mode 100644 index 0000000000..f9d577914a --- /dev/null +++ b/lib/player/audio_track.js @@ -0,0 +1,87 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview AudioTrack class. + */ + +goog.provide('shaka.player.AudioTrack'); + + + +/** + * Creates a new AudioTrack. + * @param {number} id + * @param {?number} bandwidth + * @param {?string} lang + * @constructor + */ +shaka.player.AudioTrack = function(id, bandwidth, lang) { + /** + * A unique ID for the track. + * + * @type {number} + * @expose + */ + this.id = id; + + /** + * The bandwidth required in bits per second. + * + * @type {number} + * @expose + */ + this.bandwidth = bandwidth || 0; + + /** + * The track's language, a BCP 47 language tag. + * + * @type {string} + * @expose + */ + this.lang = lang || 'unknown'; + + /** + * True if this is currently the active track. + * + * @type {boolean} + * @expose + */ + this.active = false; +}; + + +/** + * Compares two AudioTrack objects: first by language, and then by bandwidth. + * @param {!shaka.player.AudioTrack} audioTrack1 + * @param {!shaka.player.AudioTrack} audioTrack2 + * @return {number} + * @export + */ +shaka.player.AudioTrack.compare = function(audioTrack1, audioTrack2) { + if (audioTrack1.lang < audioTrack2.lang) { + return -1; + } else if (audioTrack1.lang > audioTrack2.lang) { + return 1; + } + + if (audioTrack1.bandwidth < audioTrack2.bandwidth) { + return -1; + } else if (audioTrack1.bandwidth > audioTrack2.bandwidth) { + return 1; + } + + return 0; +}; + diff --git a/lib/player/dash_video_source.js b/lib/player/dash_video_source.js new file mode 100644 index 0000000000..a7b60dc8de --- /dev/null +++ b/lib/player/dash_video_source.js @@ -0,0 +1,650 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a DASH video source. + */ + +goog.provide('shaka.player.DashVideoSource'); + +goog.require('shaka.asserts'); +goog.require('shaka.dash.AbrManager'); +goog.require('shaka.dash.DashStream'); +goog.require('shaka.dash.DashTextStream'); +goog.require('shaka.dash.IDashStream'); +goog.require('shaka.dash.MpdProcessor'); +goog.require('shaka.dash.MpdRequest'); +goog.require('shaka.dash.mpd'); +goog.require('shaka.log'); +goog.require('shaka.player.AudioTrack'); +goog.require('shaka.player.DrmSchemeInfo'); +goog.require('shaka.player.IVideoSource'); +goog.require('shaka.player.VideoTrack'); +goog.require('shaka.util.ArrayUtils'); +goog.require('shaka.util.EWMABandwidthEstimator'); +goog.require('shaka.util.EventManager'); +goog.require('shaka.util.FakeEventTarget'); +goog.require('shaka.util.IBandwidthEstimator'); +goog.require('shaka.util.LanguageUtils'); +goog.require('shaka.util.PublicPromise'); +goog.require('shaka.util.TypedBind'); + + + +/** + * Creates a DashVideoSource. + * @param {string} mpdUrl The MPD URL. + * @param {shaka.player.DashVideoSource.ContentProtectionCallback} + * interpretContentProtection A callback to interpret the ContentProtection + * elements in the DASH MPD. + * + * @listens shaka.dash.DashStream.EndedEvent + * @listens shaka.util.IBandwidthEstimator.BandwidthEvent + * + * @struct + * @constructor + * @implements {shaka.player.IVideoSource} + * @extends {shaka.util.FakeEventTarget} + * @export + */ +shaka.player.DashVideoSource = function(mpdUrl, interpretContentProtection) { + shaka.util.FakeEventTarget.call(this, null); + + /** @private {shaka.dash.mpd.Mpd}. */ + this.mpd_ = null; + + /** @private {string} */ + this.mpdUrl_ = mpdUrl; + + /** @private {!shaka.dash.MpdProcessor} */ + this.processor_ = new shaka.dash.MpdProcessor(interpretContentProtection); + + /** @private {!MediaSource} */ + this.mediaSource_ = new MediaSource(); + + /** @private {HTMLVideoElement} */ + this.video_ = null; + + /** + * The active DASH streams. + * @private {!Object.} + */ + this.streamsByType_ = {}; + + /** @private {!shaka.util.EventManager} */ + this.eventManager_ = new shaka.util.EventManager(); + + /** @private {!shaka.util.PublicPromise} */ + this.attachPromise_ = new shaka.util.PublicPromise(); + + /** @private {string} */ + this.lang_ = ''; + + /** @private {!shaka.util.IBandwidthEstimator} */ + this.estimator_ = new shaka.util.EWMABandwidthEstimator(); + // TODO(story 1925894): Seed the estimator with data from the previous + // playback in the same browser session, unless that data is more than 1 + // hour old. + + /** @private {shaka.player.Stats} */ + this.stats_ = null; + + /** @private {!shaka.dash.AbrManager} */ + this.abrManager_ = new shaka.dash.AbrManager(this.estimator_, this); +}; +goog.inherits(shaka.player.DashVideoSource, shaka.util.FakeEventTarget); + + +/** + * A callback to the application to interpret DASH ContentProtection elements. + * These elements can contain almost anything and can be highly application- + * specific, so they cannot (in general) be interpreted by the library. + * + * The first parameter is the ContentProtection element. + * The callback should return a DrmSchemeInfo object if the ContentProtection + * element is understood by the application, or null otherwise. + * + * @typedef {function(!shaka.dash.mpd.ContentProtection): + * shaka.player.DrmSchemeInfo} + * @expose + */ +shaka.player.DashVideoSource.ContentProtectionCallback; + + +/** + * Destroys the DashVideoSource. + * @suppress {checkTypes} to set otherwise non-nullable types to null. + */ +shaka.player.DashVideoSource.prototype.destroy = function() { + this.eventManager_.destroy(); + this.eventManager_ = null; + + this.destroyStreams_(); + this.streamsByType_ = null; + + this.video_ = null; + this.mediaSource_ = null; + this.processor_ = null; + this.mpdUrl_ = null; + this.mpd_ = null; + this.parent = null; + this.attachPromise_ = null; + this.estimator_ = null; +}; + + +/** @override */ +shaka.player.DashVideoSource.prototype.attach = function(player, video) { + this.parent = player; + this.video_ = video; + this.stats_ = player.getStats(); + + // The "sourceopen" event fires after setting the video element's "src" + // attribute. + this.eventManager_.listen( + this.mediaSource_, + 'sourceopen', + this.onMediaSourceOpen_.bind(this)); + + this.eventManager_.listen( + this.video_, + 'seeking', + this.onSeeking_.bind(this)); + + this.eventManager_.listen( + this.estimator_, + 'bandwidth', + this.onBandwidth_.bind(this)); + + // When re-using a video tag in Chrome, mediaKeys can get cleared by Chrome + // when src is set for the second (or subsequent) time. This feels like a + // bug in Chrome. + + // To work around this, back up the old value and ensure that it is set again + // before the attach promise is resolved. This fixes bug #18614098. + var backupMediaKeys = this.video_.mediaKeys; + this.video_.src = window.URL.createObjectURL(this.mediaSource_); + var restorePromise = this.video_.setMediaKeys(backupMediaKeys); + + // Return a promise which encompasses both attach and the restoration of + // mediaKeys. + return Promise.all([this.attachPromise_, restorePromise]); +}; + + +/** @override */ +shaka.player.DashVideoSource.prototype.getDrmSchemeInfo = function() { + if (this.processor_.getNumPeriods() == 0) { + return null; + } + + // TODO(story 1890046): Support multiple periods. + var drmScheme = this.processor_.getDrmScheme(0); + + // Externally unencrypted is signalled by null. + return (drmScheme && drmScheme.keySystem) ? drmScheme : null; +}; + + +/** @override */ +shaka.player.DashVideoSource.prototype.load = function(preferredLanguage) { + this.lang_ = preferredLanguage; + + var mpdRequest = new shaka.dash.MpdRequest(this.mpdUrl_); + return mpdRequest.send().then(shaka.util.TypedBind(this, + /** @param {!shaka.dash.mpd.Mpd} mpd */ + function(mpd) { + if (mpd.periods.length == 0) { + var error = new Error('Unplayable MPD: no periods.'); + error.type = 'mpd'; + return Promise.reject(error); + } + + this.processor_.process(mpd); + this.mpd_ = mpd; + + // TODO(story 1890046): Support multiple periods. + if ((this.processor_.getNumPeriods() == 0) || + (this.processor_.getAdaptationSets(0).length == 0)) { + var error = new Error( + 'This content cannot be displayed on this browser/platform.'); + error.type = 'mpd'; + return Promise.reject(error); + } + + return Promise.resolve(); + }) + ); +}; + + +/** @override */ +shaka.player.DashVideoSource.prototype.getVideoTracks = function() { + if (this.processor_.getNumPeriods() == 0) { + return []; + } + + var stream = this.streamsByType_['video']; + var activeRepresentation = stream ? stream.getRepresentation() : null; + var activeId = activeRepresentation ? activeRepresentation.uniqueId : 0; + + /** @type {!Array.} */ + var tracks = []; + + // TODO(story 1890046): Support multiple periods. + var adaptationSets = this.processor_.getAdaptationSets(0, 'video'); + + for (var i = 0; i < adaptationSets.length; ++i) { + var adaptationSet = adaptationSets[i]; + + for (var j = 0; j < adaptationSet.representations.length; ++j) { + var representation = adaptationSet.representations[j]; + + var id = representation.uniqueId; + var bandwidth = representation.bandwidth; + var width = representation.width; + var height = representation.height; + + var videoTrack = + new shaka.player.VideoTrack(id, bandwidth, width, height); + if (id == activeId) { + videoTrack.active = true; + } + tracks.push(videoTrack); + } + } + + return tracks; +}; + + +/** @override */ +shaka.player.DashVideoSource.prototype.getAudioTracks = function() { + if (this.processor_.getNumPeriods() == 0) { + return []; + } + + var stream = this.streamsByType_['audio']; + var activeRepresentation = stream ? stream.getRepresentation() : null; + var activeId = activeRepresentation ? activeRepresentation.uniqueId : 0; + + /** @type {!Array.} */ + var tracks = []; + + // TODO(story 1890046): Support multiple periods. + var adaptationSets = this.processor_.getAdaptationSets(0, 'audio'); + + for (var i = 0; i < adaptationSets.length; ++i) { + var adaptationSet = adaptationSets[i]; + var lang = adaptationSet.lang; + + for (var j = 0; j < adaptationSet.representations.length; ++j) { + var representation = adaptationSet.representations[j]; + + var id = representation.uniqueId; + var bandwidth = representation.bandwidth; + + var audioTrack = new shaka.player.AudioTrack(id, bandwidth, lang); + if (id == activeId) { + audioTrack.active = true; + } + tracks.push(audioTrack); + } + } + + return tracks; +}; + + +/** @override */ +shaka.player.DashVideoSource.prototype.getTextTracks = function() { + if (this.processor_.getNumPeriods() == 0) { + return []; + } + + var stream = this.streamsByType_['text']; + var activeRepresentation = stream ? stream.getRepresentation() : null; + var activeId = activeRepresentation ? activeRepresentation.uniqueId : 0; + + /** @type {!Array.} */ + var tracks = []; + + // TODO(story 1890046): Support multiple periods. + var adaptationSets = this.processor_.getAdaptationSets(0, 'text'); + + for (var i = 0; i < adaptationSets.length; ++i) { + var adaptationSet = adaptationSets[i]; + var lang = adaptationSet.lang; + + for (var j = 0; j < adaptationSet.representations.length; ++j) { + var representation = adaptationSet.representations[j]; + + var id = representation.uniqueId; + var textTrack = new shaka.player.TextTrack(id, lang); + if (id == activeId) { + textTrack.active = true; + shaka.asserts.assert(stream != null); + textTrack.enabled = stream.getEnabled(); + } + tracks.push(textTrack); + } + } + + return tracks; +}; + + +/** @override */ +shaka.player.DashVideoSource.prototype.getResumeThreshold = function() { + return this.mpd_.minBufferTime; +}; + + +/** @override */ +shaka.player.DashVideoSource.prototype.selectVideoTrack = + function(id, immediate) { + return this.selectTrack_('video', id, immediate); +}; + + +/** @override */ +shaka.player.DashVideoSource.prototype.selectAudioTrack = + function(id, immediate) { + return this.selectTrack_('audio', id, immediate); +}; + + +/** @override */ +shaka.player.DashVideoSource.prototype.selectTextTrack = + function(id, immediate) { + return this.selectTrack_('text', id, immediate); +}; + + +/** @override */ +shaka.player.DashVideoSource.prototype.enableTextTrack = function(enabled) { + var textStream = this.streamsByType_['text']; + if (textStream) { + textStream.setEnabled(enabled); + } +}; + + +/** @override */ +shaka.player.DashVideoSource.prototype.enableAdaptation = function(enabled) { + this.abrManager_.enable(enabled); +}; + + +/** @override */ +shaka.player.DashVideoSource.prototype.setRestrictions = + function(restrictions) { + shaka.asserts.assert(this.mpd_); + if (this.mpd_) { + this.processor_.enforceRestrictions(this.mpd_, restrictions); + } +}; + + +/** + * Select a track by ID. + * + * @param {string} type The type of track to change, such as 'video', 'audio', + * or 'text'. + * @param {number} id The |uniqueId| field of the desired representation. + * @param {boolean} immediate If true, switch immediately. + * + * @return {boolean} True if the specified track was found. + * @private + */ +shaka.player.DashVideoSource.prototype.selectTrack_ = + function(type, id, immediate) { + if (this.processor_.getNumPeriods() == 0) { + return false; + } + if (!this.streamsByType_[type]) { + return false; + } + + // TODO(story 1890046): Support multiple periods. + var adaptationSets = this.processor_.getAdaptationSets(0, type); + + for (var i = 0; i < adaptationSets.length; ++i) { + var adaptationSet = adaptationSets[i]; + for (var j = 0; j < adaptationSet.representations.length; ++j) { + var representation = adaptationSet.representations[j]; + if (representation.uniqueId == id) { + this.stats_.logRepresentationChange(representation); + this.streamsByType_[type].switch(representation, immediate); + return true; + } + } + } + + return false; +}; + + +/** + * MediaSource callback. + * + * @param {!Event} event The MediaSource event. + * @private + */ +shaka.player.DashVideoSource.prototype.onMediaSourceOpen_ = + function(event) { + shaka.asserts.assert(this.mpd_ != null); + shaka.asserts.assert(this.mediaSource_.sourceBuffers.length == 0); + shaka.asserts.assert(this.mpd_.periods.length != 0); + + this.eventManager_.unlisten(this.mediaSource_, 'sourceopen'); + + // TODO(story 1890046): Support multiple periods. + var duration = this.mpd_.periods[0].duration || this.mpd_.duration; + shaka.asserts.assert(duration != null); + this.mediaSource_.duration = /** @type {number} */ (duration); + + shaka.asserts.assert(this.processor_.getNumPeriods() > 0); + var adaptationSets = this.processor_.selectAdaptationSets(0, this.lang_); + + /** @type {!Object.} */ + var representationsByType = {}; + + // Create DASH streams. + for (var i = 0; i < adaptationSets.length; ++i) { + var adaptationSet = adaptationSets[i]; + var contentType = adaptationSet.contentType || ''; + + // Start by assuming we will use the first Representation. + var representation = adaptationSet.representations[0]; + if (contentType == 'video') { + // Ask AbrManager which video Representation to start with. + var trackId = this.abrManager_.getInitialVideoTrackId(); + shaka.asserts.assert(trackId != null); + var found = false; + for (var j = 0; j < adaptationSet.representations.length; ++j) { + representation = adaptationSet.representations[j]; + if (representation.uniqueId == trackId) { + found = true; + break; + } + } + shaka.asserts.assert(found); + } else if (contentType == 'audio') { + // In lieu of audio adaptation, choose the middle representation from + // the audio adaptation set. If we have high, medium, and low quality + // audio, this is medium. If we only have high and low, this is high. + var length = adaptationSet.representations.length; + var index = Math.floor(length / 2); + representation = adaptationSet.representations[index]; + } + + // Log the initial representation choice. + this.stats_.logRepresentationChange(representation); + + var mimeType = + shaka.dash.MpdProcessor.representationMimeType(representation); + var stream = (contentType == 'text') ? this.createTextStream_() : + this.createStream_(mimeType); + if (!stream) { + // An error has already been dispatched and the promise rejected. + return; + } + this.streamsByType_[contentType] = stream; + representationsByType[contentType] = representation; + } + + // Start DASH streams. + for (var type in this.streamsByType_) { + this.eventManager_.listen( + this.streamsByType_[type], + 'ended', + this.onStreamEnded_.bind(this)); + var representation = representationsByType[type]; + this.streamsByType_[type].start(representation); + } + + // Assume subs will be needed. + var subsNeeded = true; + + // If there is an audio track, and the language matches the user's + // preference, then subtitles are not needed. + var audioRepresentation = representationsByType['audio']; + if (audioRepresentation) { + // If the MPD did not specify a language, assume it is the right one. + // This means that content creators who omit language because they serve a + // monolingual demographic will not have annoyed users who have to disable + // subtitles every single time they play a video. + var lang = audioRepresentation.lang || this.lang_; + + // Alias. + var LanguageUtils = shaka.util.LanguageUtils; + if (LanguageUtils.match(LanguageUtils.MatchType.MAX, this.lang_, lang)) { + // It's a match, so subs are not needed. + subsNeeded = false; + } + } + + // Enable the subtitle display by default iff the subs are needed. + this.enableTextTrack(subsNeeded); + + this.attachPromise_.resolve(); +}; + + +/** + * Creates a DashStream object. + * + * @param {string} mimeType + * @return {shaka.dash.DashStream} or null on failure. + * @private + */ +shaka.player.DashVideoSource.prototype.createStream_ = function(mimeType) { + // Create source buffer. + var buf; + try { + buf = this.mediaSource_.addSourceBuffer(mimeType); + shaka.asserts.assert(buf != null); + } catch (exception) { + this.destroyStreams_(); + var error = new Error('Failed to create DASH stream for ' + mimeType + '.'); + error.type = 'dash'; + error.exception = exception; + this.attachPromise_.reject(error); + return null; + } + + // Create stream. + return new shaka.dash.DashStream( + this, + /** @type {!HTMLVideoElement} */ (this.video_), + this.mediaSource_, + /** @type {!SourceBuffer} */ (buf), + this.estimator_); +}; + + +/** + * Creates a DashTextStream object. + * + * @return {!shaka.dash.DashTextStream} + * @private + */ +shaka.player.DashVideoSource.prototype.createTextStream_ = function() { + var video = /** @type {!HTMLVideoElement} */ (this.video_); + return new shaka.dash.DashTextStream(this, video); +}; + + +/** + * Destroy all streams. + * + * @private + */ +shaka.player.DashVideoSource.prototype.destroyStreams_ = function() { + for (var type in this.streamsByType_) { + this.streamsByType_[type].destroy(); + } + this.streamsByType_ = {}; +}; + + +/** + * DashStream EOF callback. + * + * @param {!Event} event + * @private + */ +shaka.player.DashVideoSource.prototype.onStreamEnded_ = function(event) { + shaka.log.v1('onStreamEnded_', event); + + // Check the state, otherwise this throws an exception. + if (this.mediaSource_.readyState == 'open') { + for (var type in this.streamsByType_) { + if (!this.streamsByType_[type].hasEnded()) { + // Not all streams have ended, so ignore. + return; + } + } + + // All streams have ended, so signal EOF to the |mediaSource_|. + this.mediaSource_.endOfStream(); + } +}; + + +/** + * Video seeking callback. + * + * @param {!Event} event + * @private + */ +shaka.player.DashVideoSource.prototype.onSeeking_ = function(event) { + // Resync each stream to the new timestamp. + for (var type in this.streamsByType_) { + this.streamsByType_[type].resync(); + } +}; + + +/** + * Bandwidth statistics update callback. + * + * @param {!Event} event + * @private + */ +shaka.player.DashVideoSource.prototype.onBandwidth_ = function(event) { + this.stats_.logBandwidth(this.estimator_.getBandwidth()); +}; + diff --git a/lib/player/drm_scheme_info.js b/lib/player/drm_scheme_info.js new file mode 100644 index 0000000000..8ee2f789c4 --- /dev/null +++ b/lib/player/drm_scheme_info.js @@ -0,0 +1,171 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Defines DRM scheme information. + */ + +goog.provide('shaka.player.DrmSchemeInfo'); + +goog.require('shaka.asserts'); +goog.require('shaka.util.StringUtils'); + + + +/** + * An object which is an interpreted form of a ContentProtection object. + * + * @param {string} keySystem The key system, e.g., "com.widevine.alpha". + * @param {boolean} suppressMultipleEncryptedEvents If true, only the first + * 'encrypted' event will be used. This is desired for some applications. + * @param {string} licenseServerUrl The license server URL. + * @param {boolean} withCredentials True if the request should include cookies + * when sent cross-domain. See http://goo.gl/pzY9F7 for more information. + * @param {?{initData: !Uint8Array, initDataType: string}} initData If non-null, + * this overrides the initData from EME 'encrypted' events in the Player. + * @param {?shaka.player.DrmSchemeInfo.LicensePostProcessor} + * licensePostProcessor An optional post-processor for license responses. + * @constructor + * @struct + * @export + */ +shaka.player.DrmSchemeInfo = + function(keySystem, suppressMultipleEncryptedEvents, licenseServerUrl, + withCredentials, initData, licensePostProcessor) { + /** @type {string} */ + this.keySystem = keySystem; + + /** @type {boolean} */ + this.suppressMultipleEncryptedEvents = suppressMultipleEncryptedEvents; + + /** @type {string} */ + this.licenseServerUrl = licenseServerUrl; + + /** @type {boolean} */ + this.withCredentials = withCredentials; + + /** @type {!Array.<{initData: !Uint8Array, initDataType: string}>} */ + this.initDatas = []; + + /** @type {?shaka.player.DrmSchemeInfo.LicensePostProcessor} */ + this.licensePostProcessor = licensePostProcessor; + + if (initData) { + this.initDatas.push(initData); + } +}; + + +/** + * A callback which does application-specific post-processing on license + * responses before they are passed to the CDM. + * + * Any restrictions on playback can be set by the callback on the Restrictions + * object passed to it. + * + * Returns the raw license after application-specific headers have been removed. + * + * @typedef {function(!Uint8Array, + !shaka.player.DrmSchemeInfo.Restrictions):!Uint8Array} + */ +shaka.player.DrmSchemeInfo.LicensePostProcessor; + + + +/** + * A set of basic restrictions on playback. + * + * The video source will not adapt to a video track which exceeds any + * limitations set here. + * + * @constructor + * @struct + */ +shaka.player.DrmSchemeInfo.Restrictions = function() { + /** + * If set, specifies a maximum height for video tracks. + * + * @type {?number} + * @expose + */ + this.maxHeight = null; + + /** + * If set, specifies a maximum width for video tracks. + * + * @type {?number} + * @expose + */ + this.maxWidth = null; +}; + + +/** + * Return a DrmSchemeInfo object for unencrypted contents. + * @return {!shaka.player.DrmSchemeInfo} + * @export + */ +shaka.player.DrmSchemeInfo.createUnencrypted = function() { + return new shaka.player.DrmSchemeInfo('', false, '', false, null, null); +}; + + +/** + * Combine two DrmSchemeInfos. Their |keySystem|, + * |suppressMultipleEncryptedEvents|, and |withCredentials| members must match. + * + * The |licenseServerUrl| and |licensePostProcessor| of the combined + * DrmSchemeInfo will be taken from |a|. + * + * @param {!shaka.player.DrmSchemeInfo} a + * @param {!shaka.player.DrmSchemeInfo} b + * @return {!shaka.player.DrmSchemeInfo} + * @export + */ +shaka.player.DrmSchemeInfo.combine = function(a, b) { + shaka.asserts.assert(a.keySystem == b.keySystem, 'key system mismatch'); + shaka.asserts.assert(a.suppressMultipleEncryptedEvents == + b.suppressMultipleEncryptedEvents, + 'suppress mismatch'); + shaka.asserts.assert(a.withCredentials == b.withCredentials, + 'credentials mismatch'); + + var c = new shaka.player.DrmSchemeInfo( + a.keySystem, a.suppressMultipleEncryptedEvents, a.licenseServerUrl, + a.withCredentials, null, a.licensePostProcessor); + + var initDatas = a.initDatas.concat(b.initDatas); + + /** + * @param {{initData: !Uint8Array, initDataType: string}} o + * @return {string} + */ + var initDataKey = function(o) { + return shaka.util.StringUtils.uint8ArrayKey(o.initData); + }; + c.initDatas = shaka.util.ArrayUtils.removeDuplicates(initDatas, initDataKey); + + return c; +}; + + +/** + * Generate a key for this DrmSchemeInfo. + * If two DrmSchemeInfos are equal, they should generate the same key. + * @return {string} + */ +shaka.player.DrmSchemeInfo.prototype.key = function() { + return JSON.stringify(this); +}; + diff --git a/lib/player/http_video_source.js b/lib/player/http_video_source.js new file mode 100644 index 0000000000..35f94aa45d --- /dev/null +++ b/lib/player/http_video_source.js @@ -0,0 +1,170 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements an HTTP video source. + */ + +goog.provide('shaka.player.HttpVideoSource'); + +goog.require('shaka.player.DrmSchemeInfo'); +goog.require('shaka.player.IVideoSource'); +goog.require('shaka.util.FakeEventTarget'); + + + +/** + * Creates an HttpVideoSource. + * @param {string} mediaUrl The media URL. + * @param {string} textUrl The text URL, or empty string if no subtitles. + * @param {shaka.player.DrmSchemeInfo} drmSchemeInfo Description of the DRM + * scheme, or null for non-encrypted sources. + * @struct + * @constructor + * @implements {shaka.player.IVideoSource} + * @extends {shaka.util.FakeEventTarget} + * @export + */ +shaka.player.HttpVideoSource = function(mediaUrl, textUrl, drmSchemeInfo) { + shaka.util.FakeEventTarget.call(this, null); + + /** @private {string} */ + this.mediaUrl_ = mediaUrl; + + /** @private {string} */ + this.textUrl_ = textUrl; + + /** @private {shaka.player.DrmSchemeInfo} */ + this.drmSchemeInfo_ = drmSchemeInfo; + + /** @private {HTMLTrackElement} */ + this.textTrack_ = null; +}; +goog.inherits(shaka.player.HttpVideoSource, shaka.util.FakeEventTarget); + + +/** @override */ +shaka.player.HttpVideoSource.prototype.destroy = function() { + if (this.textTrack_) { + this.textTrack_.parentElement.removeChild(this.textTrack_); + this.textTrack_ = null; + } + + this.drmSchemeInfo_ = null; + this.parent = null; +}; + + +/** @override */ +shaka.player.HttpVideoSource.prototype.attach = function(player, video) { + this.parent = player; + + // This fixes bug #18614098. See comments in DashVideoSource.attach for more + // details. + var backupMediaKeys = video.mediaKeys; + video.src = this.mediaUrl_; + var restorePromise = video.setMediaKeys(backupMediaKeys); + + if (this.textUrl_) { + this.textTrack_ = /** @type {HTMLTrackElement} */ + (document.createElement('track')); + this.textTrack_.src = this.textUrl_; + video.appendChild(this.textTrack_); + // NOTE: mode must be set after appending to the DOM. + this.textTrack_.track.mode = 'showing'; + } + + return restorePromise; +}; + + +/** @override */ +shaka.player.HttpVideoSource.prototype.getDrmSchemeInfo = function() { + return this.drmSchemeInfo_; +}; + + +/** @override */ +shaka.player.HttpVideoSource.prototype.load = function(preferredLanguage) { + return Promise.resolve(); +}; + + +/** @override */ +shaka.player.HttpVideoSource.prototype.getVideoTracks = function() { + return []; +}; + + +/** @override */ +shaka.player.HttpVideoSource.prototype.getAudioTracks = function() { + return []; +}; + + +/** @override */ +shaka.player.HttpVideoSource.prototype.getTextTracks = function() { + return []; +}; + + +/** @override */ +shaka.player.HttpVideoSource.prototype.getResumeThreshold = function() { + return 5.0; +}; + + +/** @override */ +shaka.player.HttpVideoSource.prototype.selectVideoTrack = + function(id, immediate) { + return false; +}; + + +/** @override */ +shaka.player.HttpVideoSource.prototype.selectAudioTrack = + function(id, immediate) { + return false; +}; + + +/** @override */ +shaka.player.HttpVideoSource.prototype.selectTextTrack = + function(id, immediate) { + return false; +}; + + +/** @override */ +shaka.player.HttpVideoSource.prototype.enableTextTrack = function(enabled) { + if (!this.textTrack_) { + return; + } + + this.textTrack_.track.mode = enabled ? 'showing' : 'disabled'; +}; + + +/** @override */ +shaka.player.HttpVideoSource.prototype.enableAdaptation = function(enabled) { + // nop +}; + + +/** @override */ +shaka.player.HttpVideoSource.prototype.setRestrictions = + function(restrictions) { + // nop +}; + diff --git a/lib/player/i_video_source.js b/lib/player/i_video_source.js new file mode 100644 index 0000000000..e3fa7b5bd7 --- /dev/null +++ b/lib/player/i_video_source.js @@ -0,0 +1,169 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Defines the IVideoSource interface. + */ + +goog.provide('shaka.player.IVideoSource'); + +goog.require('shaka.player.AudioTrack'); +goog.require('shaka.player.DrmSchemeInfo'); +goog.require('shaka.player.Player'); +goog.require('shaka.player.TextTrack'); +goog.require('shaka.player.VideoTrack'); + + + +/** + * @interface + * @extends {EventTarget} + */ +shaka.player.IVideoSource = function() {}; + + +/** + * Destroys the video source. + */ +shaka.player.IVideoSource.prototype.destroy = function() {}; + + +/** + * Attaches the video source to the specified video element. + * This allows the Player to avoid setting the video's src attribute until it + * is ready. Should not be called until after the load() Promise is resolved. + * + * @param {shaka.player.Player} player The associated Player, used for event + * bubbling and stats. + * @param {!HTMLVideoElement} video The video element. + * @return {!Promise} + */ +shaka.player.IVideoSource.prototype.attach = function(player, video) {}; + + +/** + * Returns an object containing everything you need to know about the DRM + * scheme. If null, indicates that the source is not encrypted. + * Should not be called until after the load() Promise is resolved. + * @return {shaka.player.DrmSchemeInfo} + */ +shaka.player.IVideoSource.prototype.getDrmSchemeInfo = function() {}; + + +/** + * Load any intermediate source material (manifest, etc.) + * + * @param {string} preferredLanguage The user's preferred language tag. + * @see IETF RFC 5646 + * @see ISO 639 + * @return {!Promise} + */ +shaka.player.IVideoSource.prototype.load = function(preferredLanguage) {}; + + +/** + * Gets the available video tracks. + * + * @return {!Array.} + */ +shaka.player.IVideoSource.prototype.getVideoTracks = function() {}; + + +/** + * Gets the available audio tracks. + * + * @return {!Array.} + */ +shaka.player.IVideoSource.prototype.getAudioTracks = function() {}; + + +/** + * Gets the available text tracks. + * + * @return {!Array.} + */ +shaka.player.IVideoSource.prototype.getTextTracks = function() {}; + + +/** + * Gets the number of seconds of data needed to resume after buffering. + * + * @return {number} + */ +shaka.player.IVideoSource.prototype.getResumeThreshold = function() {}; + + +/** + * Select a video track by ID. + * + * @param {number} id The |id| field of the desired VideoTrack object. + * @param {boolean} immediate If true, switch immediately. Otherwise, switch + * when convenient. + * + * @return {boolean} True if the specified VideoTrack was found. + */ +shaka.player.IVideoSource.prototype.selectVideoTrack = + function(id, immediate) {}; + + +/** + * Select an audio track by ID. + * + * @param {number} id The |id| field of the desired AudioTrack object. + * @param {boolean} immediate If true, switch immediately. Otherwise, switch + * when convenient. + * + * @return {boolean} True if the specified AudioTrack was found. + */ +shaka.player.IVideoSource.prototype.selectAudioTrack = + function(id, immediate) {}; + + +/** + * Select a text track by ID. + * + * @param {number} id The |id| field of the desired TextTrack object. + * @param {boolean} immediate If true, switch immediately. Otherwise, switch + * when convenient. + * + * @return {boolean} True if the specified TextTrack was found. + */ +shaka.player.IVideoSource.prototype.selectTextTrack = + function(id, immediate) {}; + + +/** + * Enable or disable the text track. + * + * @param {boolean} enabled + */ +shaka.player.IVideoSource.prototype.enableTextTrack = function(enabled) {}; + + +/** + * Enable or disable bitrate adaptation. + * + * @param {boolean} enabled + */ +shaka.player.IVideoSource.prototype.enableAdaptation = function(enabled) {}; + + +/** + * Sets restrictions on the video tracks which can be selected. Tracks which + * exceed any of these restrictions will be ignored. + * + * @param {!shaka.player.DrmSchemeInfo.Restrictions} restrictions + */ +shaka.player.IVideoSource.prototype.setRestrictions = function(restrictions) {}; + diff --git a/lib/player/player.js b/lib/player/player.js new file mode 100644 index 0000000000..0df0120e40 --- /dev/null +++ b/lib/player/player.js @@ -0,0 +1,968 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements the player. + */ + +goog.provide('shaka.player.Player'); + +goog.require('shaka.asserts'); +goog.require('shaka.log'); +goog.require('shaka.player.AudioTrack'); +goog.require('shaka.player.Stats'); +goog.require('shaka.player.TextTrack'); +goog.require('shaka.player.VideoTrack'); +goog.require('shaka.timer'); +goog.require('shaka.util.EventManager'); +goog.require('shaka.util.FakeEvent'); +goog.require('shaka.util.FakeEventTarget'); +goog.require('shaka.util.LanguageUtils'); +goog.require('shaka.util.LicenseRequest'); +goog.require('shaka.util.StringUtils'); + + +/** + * @event shaka.player.Player.ErrorEvent + * @description Fired when a playback error occurs. + * Bubbles up through the Player. + * @property {string} type 'error' + * @property {boolean} bubbles true + * @property {!Error} detail An object which contains details on the error. + * @export + */ + + + +/** + * Creates a Player. + * + * @param {!HTMLVideoElement} video The video element. + * + * @fires shaka.player.Player.ErrorEvent + * @fires shaka.dash.DashStream.AdaptationEvent + * + * @constructor + * @extends {shaka.util.FakeEventTarget} + * @export + */ +shaka.player.Player = function(video) { + shaka.util.FakeEventTarget.call(this, null); + + /** + * The video element. + * @private {!HTMLVideoElement} + */ + this.video_ = video; + + /** + * The video source object. + * @private {shaka.player.IVideoSource} + */ + this.videoSource_ = null; + + /** + * The MediaKeys object, non-null if using the OO EME API, null otherwise. + * @private {MediaKeys} + */ + this.mediaKeys_ = null; + + /** @private {!shaka.util.EventManager} */ + this.eventManager_ = new shaka.util.EventManager(); + + /** @private {!Object.} */ + this.requestGenerated_ = {}; + + /** @private {!Array.} */ + this.fakeEncryptedEvents_ = []; + + /** @private {!Array.} */ + this.sessions_ = []; + + /** @private {string} */ + this.lang_ = 'en'; + + /** @private {?number} */ + this.rewindTimer_ = null; + + /** @private {?number} */ + this.watchdogTimer_ = null; + + /** @private {boolean} */ + this.buffering_ = false; + + /** @private {!shaka.player.Stats} */ + this.stats_ = new shaka.player.Stats; + + /** @private {boolean} */ + this.adaptationEnabled_ = true; +}; +goog.inherits(shaka.player.Player, shaka.util.FakeEventTarget); + + +/** + * @const {string} + * @export + */ +shaka.player.Player.version = 'v1.0'; + + +/** + * Determines if the browser has all of the necessary APIs to support the Shaka + * Player. This check may not pass if polyfills have not been installed. + * + * @return {boolean} + * @export + */ +shaka.player.Player.isBrowserSupported = function() { + return true && + // MSE is needed for adaptive streaming. + !!window.MediaSource && + // EME is needed for protected content. + !!window.MediaKeys && + // Indicates recent EME APIs. + !!window.navigator && + !!window.navigator.requestMediaKeySystemAccess && + // Promises are used frequently for asynchronous operations. + !!window.Promise && + // Playback quality metrics used by Player.getStats(). + !!HTMLVideoElement.prototype.getVideoPlaybackQuality && + // Fullscreen API. + !!HTMLMediaElement.prototype.requestFullscreen && + // Node.children is used by mpd_parser.js, and body is a Node instance. + !!document.body.children; +}; + + +/** + * Determines if the specified codec is supported with the given key system. + * + * @param {string} keySystem The key system. Use the empty string for + * unencrypted content. + * @param {string} mimeType A media MIME type, possibly including codec info. + * + * @return {boolean} true if the codec is supported by the key system, + * false otherwise. + * @export + */ +shaka.player.Player.isTypeSupported = function(keySystem, mimeType) { + var supported; + + // TODO(story 1922598): Although Chrome reports support for mp4a.40.5, it + // fails to decode some such content. These are low-quality streams anyway, + // so disable support for them until a solution can be found. + if (mimeType.indexOf('mp4a.40.5') >= 0) { + return false; + } + + if (mimeType == 'text/vtt') { + supported = !!window.VTTCue; + } else { + supported = MediaSource.isTypeSupported(mimeType); + + if (supported && keySystem) { + // Strip off the codec info, if any, leaving just a basic MIME type. + var basicType = mimeType.split(';')[0]; + // TODO: isTypeSupported deprecated + supported = MediaKeys.isTypeSupported(keySystem, basicType); + } + } + + shaka.log.info(keySystem, '+', mimeType, + supported ? 'is' : 'is not', 'supported'); + return supported; +}; + + +/** + * Destroys the player. + * @suppress {checkTypes} to set otherwise non-nullable types to null. + * @export + */ +shaka.player.Player.prototype.destroy = function() { + this.unload().catch(function() {}); + + this.eventManager_.destroy(); + this.eventManager_ = null; + + this.video_ = null; +}; + + +/** + * Stop playback and unload the current video source. Makes the player ready + * for reuse. Also resets any statistics gathered. + * + * MediaKeys must be unloaded asynchronously, but all other resources are + * removed synchronously. + * + * @return {!Promise} A promise, resolved when MediaKeys is removed. + * @export + */ +shaka.player.Player.prototype.unload = function() { + // Stop playback. + this.video_.pause(); + + // Stop listening for events and timers. + this.eventManager_.removeAll(); + this.cancelWatchdogTimer_(); + this.cancelRewindTimer_(); + + // Release all EME resources. + for (var i = 0; i < this.sessions_.length; ++i) { + this.sessions_[i].close(); + } + this.sessions_ = []; + this.fakeEncryptedEvents_ = []; + this.mediaKeys_ = null; + + // Remove the video source. + this.video_.src = ''; + this.video_.load(); + var p = this.video_.setMediaKeys(null); + if (this.videoSource_) { + this.videoSource_.destroy(); + this.videoSource_ = null; + } + + // Reset state. + this.buffering_ = false; + this.requestGenerated_ = {}; + this.stats_ = new shaka.player.Stats; + + return p; +}; + + +/** + * Loads the specified video source and starts playback. If a video source has + * already been loaded, this calls unload() for you before loading the new + * source. + * + * @param {!shaka.player.IVideoSource} videoSource The IVideoSource object. The + * Player takes ownership of |videoSource|. + * @return {!Promise} + * @export + */ +shaka.player.Player.prototype.load = function(videoSource) { + var p = this.videoSource_ ? this.unload() : Promise.resolve(); + shaka.asserts.assert(this.videoSource_ == null); + + if (this.video_.autoplay) { + shaka.timer.begin('load'); + this.eventManager_.listen(this.video_, 'timeupdate', + this.onFirstTimestamp_.bind(this)); + } + + // Sync adaptation setting, which could have been set before this source was + // loaded. + videoSource.enableAdaptation(this.adaptationEnabled_); + + return p.then(shaka.util.TypedBind(this, + function() { + return videoSource.load(this.lang_); + }) + ).then(shaka.util.TypedBind(this, + function() { + this.videoSource_ = videoSource; + return this.initializeDrmScheme_(); + }) + ).then(shaka.util.TypedBind(this, + function() { + this.setVideoEventListeners_(); + return this.videoSource_.attach(this, this.video_); + }) + ).then(shaka.util.TypedBind(this, + function() { + // Dispatch any fake 'encrypted' events we might have created. + for (var i = 0; i < this.fakeEncryptedEvents_.length; ++i) { + this.onEncrypted_(this.fakeEncryptedEvents_[i]); + } + return Promise.resolve(); + }) + ).catch(shaka.util.TypedBind(this, + /** @param {!Error} error */ + function(error) { + // We own the source now, so we must clean it up. + // We may not have set the source on this, so call destroy on the local + // var instead. + videoSource.destroy(); + + // Since we may have set the source on this, set it to null. + this.videoSource_ = null; + + // Even though we return a rejected promise, we still want to dispatch + // an error event to ensure that the application is aware of all errors + // from the player. + var event = shaka.util.FakeEvent.createErrorEvent(error); + this.dispatchEvent(event); + + return Promise.reject(error); + }) + ); +}; + + +/** + * Initializes the DRM scheme. This function sets |mediaKeys_|. + * @return {!Promise} + * @private + */ +shaka.player.Player.prototype.initializeDrmScheme_ = function() { + shaka.asserts.assert(this.mediaKeys_ == null); + shaka.asserts.assert(this.video_.mediaKeys == null); + + // TODO(story 2544736): Support multiple DASH periods with different schemes? + var drmScheme = this.videoSource_.getDrmSchemeInfo(); + if (!drmScheme) { + shaka.log.info('No encryption.'); + return Promise.resolve(); + } + + var p = navigator.requestMediaKeySystemAccess(drmScheme.keySystem); + return p.then(shaka.util.TypedBind(this, + /** @param {!MediaKeySystemAccess} mediaKeySystemAccess */ + function(mediaKeySystemAccess) { + return mediaKeySystemAccess.createMediaKeys(); + }) + ).then(shaka.util.TypedBind(this, + /** @param {!MediaKeys} mediaKeys */ + function(mediaKeys) { + this.mediaKeys_ = mediaKeys; + return this.video_.setMediaKeys(this.mediaKeys_); + }) + ).then(shaka.util.TypedBind(this, + function() { + shaka.asserts.assert(this.video_.mediaKeys); + shaka.asserts.assert(this.video_.mediaKeys == this.mediaKeys_); + this.generateFakeEncryptedEvents_(drmScheme); + + // Explicit init data for any one stream is sufficient to suppress + // 'encrypted' events for all streams. + if (this.fakeEncryptedEvents_.length == 0) { + this.eventManager_.listen( + this.video_, + 'encrypted', + /** @type {shaka.util.EventManager.ListenerType} */( + this.onEncrypted_.bind(this))); + } + }) + ); +}; + + +/** + * Generate any fake 'encrypted' events for the given DRM scheme and store them + * in |fakeEncryptedEvents_|. + * + * @param {shaka.player.DrmSchemeInfo} drmScheme + * @private + */ +shaka.player.Player.prototype.generateFakeEncryptedEvents_ = + function(drmScheme) { + this.fakeEncryptedEvents_ = []; + + for (var i = 0; i < drmScheme.initDatas.length; ++i) { + var initData = drmScheme.initDatas[i]; + + // This DRM scheme has init data information which should override that + // found in the actual stream. Therefore, we fake an 'encrypted' event + // and ignore the actual 'encrypted' events from the browser. + var event = /** @type {!MediaEncryptedEvent} */ ({ + type: 'encrypted', + initDataType: initData.initDataType, + initData: initData.initData + }); + + // The video hasn't been attached yet, so we can't fire these until later. + this.fakeEncryptedEvents_.push(event); + } +}; + + +/** + * Sets the video's event listeners. + * + * @private + */ +shaka.player.Player.prototype.setVideoEventListeners_ = function() { + // TODO(story 1891509): Connect these events to the UI. + this.eventManager_.listen(this.video_, 'play', this.onPlay_.bind(this)); + this.eventManager_.listen(this.video_, 'playing', this.onPlaying_.bind(this)); + this.eventManager_.listen(this.video_, 'seeking', this.onSeeking_.bind(this)); + this.eventManager_.listen(this.video_, 'pause', this.onPause_.bind(this)); + this.eventManager_.listen(this.video_, 'ended', this.onEnded_.bind(this)); +}; + + +/** + * EME 'encrypted' event handler. + * + * @param {!MediaEncryptedEvent} event The EME 'encrypted' event. + * @private + */ +shaka.player.Player.prototype.onEncrypted_ = function(event) { + // Suppress duplicate init data. + shaka.asserts.assert(event.initData); + var initData = new Uint8Array(event.initData); + var initDataKey = shaka.util.StringUtils.uint8ArrayKey(initData); + + var drmScheme = this.videoSource_.getDrmSchemeInfo(); + if (drmScheme.suppressMultipleEncryptedEvents) { + // In this scheme, all 'encrypted' events are equivalent. + // Never create more than one session. + initDataKey = 'first'; + } + + if (this.requestGenerated_[initDataKey]) { + return; + } + + shaka.log.info('onEncrypted_', initData, event); + + var session = this.mediaKeys_.createSession(); + this.sessions_.push(session); + + this.eventManager_.listen( + session, 'message', /** @type {shaka.util.EventManager.ListenerType} */( + this.onSessionMessage_.bind(this))); + + var p = session.generateRequest(event.initDataType, event.initData); + p.then(shaka.util.TypedBind(this, + function() { + this.requestGenerated_[initDataKey] = true; + }) + ).catch(shaka.util.TypedBind(this, + /** @param {!Error} error */ + function(error) { + var event = shaka.util.FakeEvent.createErrorEvent(error); + this.dispatchEvent(event); + }) + ); +}; + + +/** + * EME key-message handler. + * + * @param {!MediaKeyMessageEvent} event The EME message event. + * @private + */ +shaka.player.Player.prototype.onSessionMessage_ = function(event) { + shaka.log.info('onSessionMessage_', event); + var drmScheme = this.videoSource_.getDrmSchemeInfo(); + this.requestLicense_(event.target, drmScheme.licenseServerUrl, event.message, + drmScheme.withCredentials, + drmScheme.licensePostProcessor); +}; + + +/** + * Requests a license. + * + * @param {!MediaKeySession} session An EME session object. + * @param {string} licenseServerUrl The license server URL. + * @param {!ArrayBuffer} licenseRequestBody The license request's body. + * @param {boolean} withCredentials True if the request should include cookies + * when sent cross-domain. See http://goo.gl/pzY9F7 for more information. + * @param {?shaka.player.DrmSchemeInfo.LicensePostProcessor} postProcessor The + * post-processor for the license, if any. + * + * @private + */ +shaka.player.Player.prototype.requestLicense_ = + function(session, licenseServerUrl, licenseRequestBody, withCredentials, + postProcessor) { + shaka.log.info( + 'requestLicense_', session, licenseServerUrl, licenseRequestBody); + + var licenseRequest = new shaka.util.LicenseRequest( + licenseServerUrl, licenseRequestBody, withCredentials); + + licenseRequest.send().then(shaka.util.TypedBind(this, + /** @param {!Uint8Array} response */ + function(response) { + shaka.log.info('onLicenseSuccess_', session); + if (postProcessor) { + var restrictions = new shaka.player.DrmSchemeInfo.Restrictions(); + response = postProcessor(response, restrictions); + this.videoSource_.setRestrictions(restrictions); + } + + return session.update(response); + }) + ).then( + function() { + shaka.log.info('onSessionReady_', session); + } + ).catch(shaka.util.TypedBind(this, + /** @param {!Error} error */ + function(error) { + error.session = session; + var event = shaka.util.FakeEvent.createErrorEvent(error); + this.dispatchEvent(event); + }) + ); +}; + + +/** + * Time update event handler. Will be removed once the first update is seen. + * + * @param {!Event} event + * @private + */ +shaka.player.Player.prototype.onFirstTimestamp_ = function(event) { + shaka.timer.end('load'); + this.stats_.logPlaybackLatency(shaka.timer.get('load')); + this.eventManager_.unlisten(this.video_, 'timeupdate'); +}; + + +/** + * Video play event handler. + * + * @param {!Event} event + * @private + */ +shaka.player.Player.prototype.onPlay_ = function(event) { + shaka.log.debug('onPlay_', event); +}; + + +/** + * Video playing event handler. Fires any time the video starts playing. + * + * @param {!Event} event + * @private + */ +shaka.player.Player.prototype.onPlaying_ = function(event) { + shaka.log.debug('onPlaying_', event); + shaka.timer.begin('playing'); + + this.cancelWatchdogTimer_(); + + this.watchdogTimer_ = + window.setTimeout(this.onWatchdogTimer_.bind(this), 100); +}; + + +/** + * Video seeking event handler. + * + * @param {!Event} event + * @private + */ +shaka.player.Player.prototype.onSeeking_ = function(event) { + shaka.log.debug('onSeeking_', event); + + this.cancelWatchdogTimer_(); + this.buffering_ = false; +}; + + +/** + * Video pause event handler. Fires any time the video stops for any reason, + * including before a 'seeking' or 'ended' event. + * + * @param {!Event} event + * @private + */ +shaka.player.Player.prototype.onPause_ = function(event) { + shaka.log.debug('onPause_', event); + shaka.timer.end('playing'); + this.stats_.logPlayTime(shaka.timer.get('playing')); +}; + + +/** + * Video end event handler. + * + * @param {!Event} event + * @private + */ +shaka.player.Player.prototype.onEnded_ = function(event) { + shaka.log.debug('onEnded_', event, this.getStats()); + this.cancelWatchdogTimer_(); +}; + + +/** + * Gets updated stats about the player. + * + * @return {!shaka.player.Stats} + * @export + */ +shaka.player.Player.prototype.getStats = function() { + if (!this.video_.paused) { + // Update play time, which is still progressing. + shaka.timer.end('playing'); + this.stats_.logPlayTime(shaka.timer.get('playing')); + shaka.timer.begin('playing'); + } + this.stats_.updateVideoStats(this.video_); + return this.stats_; +}; + + +/** + * Gets the current video resolution. Returns null if the current video + * resolution could not be determined. + * + * @return {?{width: number, height: number}} + * @export + */ +shaka.player.Player.prototype.getCurrentResolution = function() { + var width = this.video_.videoWidth; + var height = this.video_.videoHeight; + + if (width && height) { + return { width: width, height: height }; + } else { + return null; + } +}; + + +/** + * Gets the available video tracks. + * + * @return {!Array.} + * @export + */ +shaka.player.Player.prototype.getVideoTracks = function() { + if (!this.videoSource_) return []; + return this.videoSource_.getVideoTracks(); +}; + + +/** + * Gets the available audio tracks. + * + * @return {!Array.} + * @export + */ +shaka.player.Player.prototype.getAudioTracks = function() { + if (!this.videoSource_) return []; + return this.videoSource_.getAudioTracks(); +}; + + +/** + * Gets the available text tracks. + * + * @return {!Array.} + * @export + */ +shaka.player.Player.prototype.getTextTracks = function() { + if (!this.videoSource_) return []; + return this.videoSource_.getTextTracks(); +}; + + +/** + * Select a video track by ID. This can interfere with automatic adaptation, + * so you should call {@link shaka.player.Player#enableAdaptation}(false) if + * you intend to switch to manual video track selection. + * + * @param {number} id The |id| field of the desired VideoTrack object. + * + * @return {boolean} True if the specified VideoTrack was found. + * @export + */ +shaka.player.Player.prototype.selectVideoTrack = function(id) { + if (!this.videoSource_) return false; + return this.videoSource_.selectVideoTrack(id, true); +}; + + +/** + * Select an audio track by ID. + * + * @param {number} id The |id| field of the desired AudioTrack object. + * + * @return {boolean} True if the specified AudioTrack was found. + * @export + */ +shaka.player.Player.prototype.selectAudioTrack = function(id) { + if (!this.videoSource_) return false; + return this.videoSource_.selectAudioTrack(id, false); +}; + + +/** + * Select a text track by ID. + * + * @param {number} id The |id| field of the desired TextTrack object. + * + * @return {boolean} True if the specified TextTrack was found. + * @export + */ +shaka.player.Player.prototype.selectTextTrack = function(id) { + if (!this.videoSource_) return false; + return this.videoSource_.selectTextTrack(id, false); +}; + + +/** + * Enable or disable the text track. + * + * @param {boolean} enabled + * @export + */ +shaka.player.Player.prototype.enableTextTrack = function(enabled) { + if (!this.videoSource_) return; + this.videoSource_.enableTextTrack(enabled); +}; + + +/** + * Enable or disable automatic bitrate adaptation. + * + * @param {boolean} enabled + * @export + */ +shaka.player.Player.prototype.enableAdaptation = function(enabled) { + this.adaptationEnabled_ = enabled; + if (this.videoSource_) { + this.videoSource_.enableAdaptation(enabled); + } +}; + + +/** + * @return {number} Current playback time in seconds. + * @export + */ +shaka.player.Player.prototype.getCurrentTime = function() { + return this.video_.currentTime; +}; + + +/** + * @return {number} Video duration in seconds. + * @export + */ +shaka.player.Player.prototype.getDuration = function() { + return this.video_.duration; +}; + + +/** + * @return {boolean} True if the video is muted. + * @export + */ +shaka.player.Player.prototype.getMuted = function() { + return this.video_.muted; +}; + + +/** + * @return {number} The video volume, between 0 and 1. + * @export + */ +shaka.player.Player.prototype.getVolume = function() { + return this.video_.volume; +}; + + +/** + * Play the video. Will reset the playback rate to 1.0 as well. + * @export + */ +shaka.player.Player.prototype.play = function() { + this.setPlaybackRate(1.0); + this.video_.play(); +}; + + +/** + * Pause the video. + * @export + */ +shaka.player.Player.prototype.pause = function() { + this.video_.pause(); +}; + + +/** + * Make the video go full-screen. + * For security reasons, only works from an event handler for user input. + * @export + */ +shaka.player.Player.prototype.requestFullscreen = function() { + this.video_.requestFullscreen(); +}; + + +/** + * @param {number} seconds The desired playback position in seconds. + * @export + */ +shaka.player.Player.prototype.seek = function(seconds) { + this.video_.currentTime = seconds; +}; + + +/** + * @param {boolean} on True to mute the video, false to unmute the video. + * @export + */ +shaka.player.Player.prototype.setMuted = function(on) { + this.video_.muted = on; +}; + + +/** + * @param {number} level The video volume, between 0 and 1. + * @export + */ +shaka.player.Player.prototype.setVolume = function(level) { + this.video_.volume = level; +}; + + +/** + * @param {string} lang The user's preferred language tag. + * If not set, defaults to 'en'. + * @see IETF RFC 5646 + * @see ISO 639 + * @export + */ +shaka.player.Player.prototype.setPreferredLanguage = function(lang) { + // Normalize the language tag. + this.lang_ = shaka.util.LanguageUtils.normalize(lang); +}; + + +/** + * @param {number} rate The playback rate. + * Negative values will rewind the video. + * Positive values less than 1.0 will trigger slow-motion playback. + * Positive values greater than 1.0 will trigger fast-forward. + * 0.0 is invalid and will be ignored. + * Some UAs will not play audio at rates less than 0.25 or 0.5 or greater + * than 4.0 or 5.0, but this behavior is not specified. + * No audio will be played while rewinding. + * @export + */ +shaka.player.Player.prototype.setPlaybackRate = function(rate) { + shaka.asserts.assert(rate != 0); + if (rate == 0) { + return; + } + + // Cancel any rewind we might be in the middle of. + this.cancelRewindTimer_(); + + if (rate > 0) { + // Slow-mo or fast-forward are handled natively by the UA. + this.video_.playbackRate = rate; + } else { + // Rewind is not supported by any UA to date (2014), so we fake it. + this.video_.playbackRate = 0; + this.onRewindTimer_(rate); + } +}; + + +/** + * Cancels the rewind timer, if any. + * @private + */ +shaka.player.Player.prototype.cancelRewindTimer_ = function() { + if (this.rewindTimer_) { + window.clearTimeout(this.rewindTimer_); + this.rewindTimer_ = null; + } +}; + + +/** + * Cancels the watchdog timer, if any. + * @private + */ +shaka.player.Player.prototype.cancelWatchdogTimer_ = function() { + if (this.watchdogTimer_) { + window.clearTimeout(this.watchdogTimer_); + this.watchdogTimer_ = null; + } +}; + + +/** + * Called on a recurring timer to simulate rewind. + * @param {number} rate + * @private + */ +shaka.player.Player.prototype.onRewindTimer_ = function(rate) { + shaka.asserts.assert(rate < 0); + // For a rate of -1.0, we move the playhead back by 0.1s every 0.1s (100ms). + this.video_.currentTime += 0.1 * rate; + this.rewindTimer_ = + window.setTimeout(this.onRewindTimer_.bind(this, rate), 100); +}; + + +/** + * Called on a recurring timer to detect buffering events. + * @private + */ +shaka.player.Player.prototype.onWatchdogTimer_ = function() { + this.watchdogTimer_ = + window.setTimeout(this.onWatchdogTimer_.bind(this), 100); + + // Because we cancel this onSeeking_ and re-enable it onPlaying_. + shaka.asserts.assert(!this.video_.seeking, 'should not be seeking'); + + var buffered = this.video_.buffered; + // Counter-intuitively, the play head can advance audio-only while video is + // buffering. |buffered| will show the intersection of buffered ranges for + // both audio and video, so this is an accurate way to sense that we are + // buffering. The 'stalled', 'waiting', and 'suspended' events do not work + // for this purpose as of Chrome 38. + var bufferEnd = buffered.length ? buffered.end(buffered.length - 1) : 0; + var underflow = this.video_.currentTime - bufferEnd; + + if (!this.buffering_) { + if (underflow > shaka.player.Player.UNDERFLOW_THRESHOLD_) { + this.buffering_ = true; + this.video_.pause(); + this.stats_.logBufferingEvent(); + shaka.timer.begin('buffering'); + shaka.log.debug('Buffering...'); + } + } else { + var resumeThreshold = this.videoSource_.getResumeThreshold(); + shaka.asserts.assert(resumeThreshold > 0); + if (underflow < -resumeThreshold) { + shaka.log.debug('Buffering complete.'); + shaka.timer.end('buffering'); + this.stats_.logBufferingTime(shaka.timer.get('buffering')); + this.buffering_ = false; + this.video_.play(); + } + } +}; + + +/** + * The threshold for underflow, in seconds. If the play head is outside the + * buffered range by this much, we will consider the player to be out of data. + * + * @private {number} + * @const + */ +shaka.player.Player.UNDERFLOW_THRESHOLD_ = 0.050; + diff --git a/lib/player/stats.js b/lib/player/stats.js new file mode 100644 index 0000000000..5b1c57eda7 --- /dev/null +++ b/lib/player/stats.js @@ -0,0 +1,266 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a player stats object. + */ + +goog.provide('shaka.player.Stats'); + +goog.require('shaka.asserts'); +goog.require('shaka.dash.mpd'); + + + +/** + * Creates a Stats object. + * + * @constructor + * @struct + */ +shaka.player.Stats = function() { + /** + * @type {shaka.player.Stats.RepresentationStats} + * @expose + */ + this.representation = null; + + /** + * Number of frames decoded. NaN if not available. + * + * @type {number} + * @expose + */ + this.decodedFrames = NaN; + + /** + * Number of frames dropped. NaN if not available. + * + * @type {number} + * @expose + */ + this.droppedFrames = NaN; + + /** + * Estimated bandwidth in bits per second. + * + * @type {number} + * @expose + */ + this.estimatedBandwidth = 0; + + /** + * Time in playback state in seconds. + * + * @type {number} + * @expose + */ + this.playTime = 0; + + /** + * Time in buffering state in seconds. + * + * @type {number} + * @expose + */ + this.bufferingTime = 0; + + /** + * Playback latency in seconds. NaN if autoplay is not used. + * + * @type {number} + * @expose + */ + this.playbackLatency = NaN; + + /** + * Buffering history. Each number is a timestamp when the player entered a + * buffering state. + * + * @type {!Array.} + * @expose + */ + this.bufferingHistory = []; + + /** + * Bandwidth history. Each timestamped value is a bandwidth measurement, in + * bits per second. + * + * @type {!Array.>} + * @expose + */ + this.bandwidthHistory = []; + + /** + * Representation history. Each timestamped value is a representation chosen + * by the player. + * + * @type {!Array.>} + * @expose + */ + this.representationHistory = []; +}; + + +/** + * Updates video stats from the video tag. + * + * @param {HTMLVideoElement} video + */ +shaka.player.Stats.prototype.updateVideoStats = function(video) { + // Quality metrics may not be supported in all browsers yet. + var quality = video.getVideoPlaybackQuality(); + if (quality) { + this.decodedFrames = quality.totalVideoFrames; + this.droppedFrames = quality.droppedVideoFrames; + } +}; + + +/** + * Logs a buffering event. + */ +shaka.player.Stats.prototype.logBufferingEvent = function() { + this.bufferingHistory.push(Date.now() / 1000.0); +}; + + +/** + * Logs play time. + * + * @param {number} t Milliseconds the player has been in a playback state. + */ +shaka.player.Stats.prototype.logPlayTime = function(t) { + this.playTime += t / 1000.0; +}; + + +/** + * Logs buffering time. + * + * @param {number} t Milliseconds the player has been in a buffering state. + */ +shaka.player.Stats.prototype.logBufferingTime = function(t) { + this.bufferingTime += t / 1000.0; +}; + + +/** + * Logs a representation change. + * + * @param {!shaka.dash.mpd.Representation} representation + */ +shaka.player.Stats.prototype.logRepresentationChange = + function(representation) { + this.representation = + new shaka.player.Stats.RepresentationStats(representation); + this.representationHistory.push( + new shaka.player.Stats.TimedValue(this.representation)); +}; + + +/** + * Logs bandwidth stats. + * + * @param {number} bandwidth in bits per second. + */ +shaka.player.Stats.prototype.logBandwidth = function(bandwidth) { + this.estimatedBandwidth = bandwidth; + this.bandwidthHistory.push( + new shaka.player.Stats.TimedValue(bandwidth)); +}; + + +/** + * Logs playback latency. + * + * @param {number} latency in milliseconds. + */ +shaka.player.Stats.prototype.logPlaybackLatency = function(latency) { + this.playbackLatency = latency / 1000.0; +}; + + + +/** + * A collection of video representation stats. + * + * @param {!shaka.dash.mpd.Representation} representation + * + * @constructor + * @struct + */ +shaka.player.Stats.RepresentationStats = function(representation) { + /** + * Representation width in pixels. + * + * @type {?number} + * @expose + */ + this.videoWidth = representation.width; + + /** + * Representation height in pixels. + * + * @type {?number} + * @expose + */ + this.videoHeight = representation.height; + + /** + * Representation MIME type. + * + * @type {?string} + * @expose + */ + this.videoMimeType = representation.mimeType; + + /** + * Representation bandwidth requirement in bits per second. + * + * @type {?number} + * @expose + */ + this.videoBandwidth = representation.bandwidth; +}; + + + +/** + * A value associated with a timestamp. + * + * @param {T} value + * + * @template T + * @constructor + * @struct + */ +shaka.player.Stats.TimedValue = function(value) { + /** + * Seconds since 1970. + * + * @type {number} + * @const + * @expose + */ + this.timestamp = Date.now() / 1000.0; + + /** + * @const {T} + * @expose + */ + this.value = value; +}; + diff --git a/lib/player/text_track.js b/lib/player/text_track.js new file mode 100644 index 0000000000..2d826dc859 --- /dev/null +++ b/lib/player/text_track.js @@ -0,0 +1,80 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview TextTrack class. + */ + +goog.provide('shaka.player.TextTrack'); + + + +/** + * Creates a new TextTrack. + * @param {number} id + * @param {?string} lang + * @constructor + */ +shaka.player.TextTrack = function(id, lang) { + /** + * A unique ID for the track. + * + * @type {number} + * @expose + */ + this.id = id; + + /** + * The track's language, a BCP 47 language tag. + * + * @type {string} + * @expose + */ + this.lang = lang || 'unknown'; + + /** + * True if this is currently the active track. + * + * @type {boolean} + * @expose + */ + this.active = false; + + /** + * True if this track is currently being displayed. + * + * @type {boolean} + * @expose + */ + this.enabled = false; +}; + + +/** + * Compares two TextTrack objects by language. + * @param {!shaka.player.TextTrack} textTrack1 + * @param {!shaka.player.TextTrack} textTrack2 + * @return {number} + * @export + */ +shaka.player.TextTrack.compare = function(textTrack1, textTrack2) { + if (textTrack1.lang < textTrack2.lang) { + return -1; + } else if (textTrack1.lang > textTrack2.lang) { + return 1; + } + + return 0; +}; + diff --git a/lib/player/video_track.js b/lib/player/video_track.js new file mode 100644 index 0000000000..9fa48a70ac --- /dev/null +++ b/lib/player/video_track.js @@ -0,0 +1,99 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview VideoTrack class. + */ + +goog.provide('shaka.player.VideoTrack'); + + + +/** + * Creates a new VideoTrack. + * @param {number} id + * @param {?number} bandwidth + * @param {?number} width + * @param {?number} height + * @constructor + */ +shaka.player.VideoTrack = function(id, bandwidth, width, height) { + /** + * A unique ID for the track. + * + * @type {number} + * @expose + */ + this.id = id; + + /** + * The bandwidth required in bits per second. + * + * @type {number} + * @expose + */ + this.bandwidth = bandwidth || 0; + + /** + * The track's width in pixels. + * + * @type {number} + * @expose + */ + this.width = width || 0; + + /** + * The track's height in pixels. + * + * @type {number} + * @expose + */ + this.height = height || 0; + + /** + * True if this is currently the active track. + * + * @type {boolean} + * @expose + */ + this.active = false; +}; + + +/** + * Compares two VideoTrack objects: first by resolution, and then by bandwidth. + * @param {!shaka.player.VideoTrack} videoTrack1 + * @param {!shaka.player.VideoTrack} videoTrack2 + * @return {number} + * @export + */ +shaka.player.VideoTrack.compare = function(videoTrack1, videoTrack2) { + var resolution1 = videoTrack1.width * videoTrack1.height; + var resolution2 = videoTrack2.width * videoTrack2.height; + + if (resolution1 < resolution2) { + return -1; + } else if (resolution1 > resolution2) { + return 1; + } + + if (videoTrack1.bandwidth < videoTrack2.bandwidth) { + return -1; + } else if (videoTrack1.bandwidth > videoTrack2.bandwidth) { + return 1; + } + + return 0; +}; + diff --git a/lib/polyfill/fullscreen.js b/lib/polyfill/fullscreen.js new file mode 100644 index 0000000000..5f91e4df85 --- /dev/null +++ b/lib/polyfill/fullscreen.js @@ -0,0 +1,46 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview A polyfill to unify fullscreen APIs. + * + * @see http://enwp.org/polyfill + */ + +goog.provide('shaka.polyfill.Fullscreen'); + + +/** + * @namespace shaka.polyfill.Fullscreen + * @export + * + * @summary A polyfill to unify fullscreen APIs across browsers. + * Many browsers have a prefixed fullscreen method on HTMLMediaElement. + * See {@link http://goo.gl/n7TYl0 Using fullscreen mode} on MDN for more + * information. + */ + + +/** + * Install the polyfill if needed. + * @export + */ +shaka.polyfill.Fullscreen.install = function() { + var proto = HTMLMediaElement.prototype; + proto.requestFullscreen = proto.requestFullscreen || + proto.mozRequestFullscreen || + proto.msRequestFullscreen || + proto.webkitRequestFullscreen; +}; + diff --git a/lib/polyfill/mediakeys.js b/lib/polyfill/mediakeys.js new file mode 100644 index 0000000000..1f6752a315 --- /dev/null +++ b/lib/polyfill/mediakeys.js @@ -0,0 +1,58 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview A polyfill to unify EME APIs across browser versions. + * + * @see http://enwp.org/polyfill + */ + +goog.provide('shaka.polyfill.MediaKeys'); + +goog.require('shaka.log'); +goog.require('shaka.polyfill.PatchedMediaKeys.nop'); +goog.require('shaka.polyfill.PatchedMediaKeys.v01b'); + + +/** + * @namespace shaka.polyfill.MediaKeys + * @export + * + * @summary A polyfill to unify EME APIs across browser versions. + * + * The {@link https://w3c.github.io/encrypted-media/ EME spec} is still a + * work-in-progress. As such, we need to provide a consistent API to the Shaka + * Player. Until the spec is completely stable, the API provided by this + * polyfill may lag behind the latest spec developments. + */ + + +/** + * Install the polyfill if needed. + * @export + */ +shaka.polyfill.MediaKeys.install = function() { + shaka.log.debug('MediaKeys.install'); + + if (HTMLMediaElement.prototype.webkitGenerateKeyRequest) { + shaka.log.info('Using prefixed EME v0.1b.'); + shaka.polyfill.PatchedMediaKeys.v01b.install(); + } else if (Navigator.prototype.requestMediaKeySystemAccess) { + shaka.log.info('Using native EME as-is.'); + } else { + shaka.log.info('EME not available.'); + shaka.polyfill.PatchedMediaKeys.nop.install(); + } +}; + diff --git a/lib/polyfill/patchedmediakeys_nop.js b/lib/polyfill/patchedmediakeys_nop.js new file mode 100644 index 0000000000..f261828955 --- /dev/null +++ b/lib/polyfill/patchedmediakeys_nop.js @@ -0,0 +1,105 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview A polyfill to stub out {@link http://goo.gl/sgJHNN EME draft + * 01 December 2014} on browsers without EME. All methods will fail. + * + * @see http://enwp.org/polyfill + */ + +goog.provide('shaka.polyfill.PatchedMediaKeys.nop'); + +goog.require('shaka.asserts'); +goog.require('shaka.log'); + + +/** + * Install the polyfill. + * @export + */ +shaka.polyfill.PatchedMediaKeys.nop.install = function() { + shaka.log.debug('PatchedMediaKeys.nop.install'); + + // Alias. + var nop = shaka.polyfill.PatchedMediaKeys.nop; + + // Install patches. + Navigator.prototype.requestMediaKeySystemAccess = + nop.requestMediaKeySystemAccess; + // Work around read-only declarations for these properties by using strings: + HTMLMediaElement.prototype['mediaKeys'] = null; + HTMLMediaElement.prototype['waitingFor'] = ''; + HTMLMediaElement.prototype.setMediaKeys = nop.setMediaKeys; + window.MediaKeys = nop.MediaKeys; + // TODO: isTypeSupported deprecated + window.MediaKeys.isTypeSupported = nop.MediaKeys.isTypeSupported; +}; + + +/** + * An implementation of Navigator.prototype.requestMediaKeySystemAccess. + * Retrieve a MediaKeySystemAccess object. + * + * @this {!Navigator} + * @param {string} keySystem + * @param {Array.=} opt_supportedConfigurations + * @return {!Promise.} + */ +shaka.polyfill.PatchedMediaKeys.nop.requestMediaKeySystemAccess = + function(keySystem, opt_supportedConfigurations) { + shaka.log.debug('PatchedMediaKeys.nop.requestMediaKeySystemAccess'); + shaka.asserts.assert(this instanceof Navigator); + + return Promise.reject(new Error( + 'The key system specified is not supported.')); +}; + + +/** + * An implementation of HTMLMediaElement.prototype.setMediaKeys. + * Attach a MediaKeys object to the media element. + * + * @this {!HTMLMediaElement} + * @param {MediaKeys} mediaKeys + * @return {!Promise} + */ +shaka.polyfill.PatchedMediaKeys.nop.setMediaKeys = function(mediaKeys) { + shaka.log.debug('PatchedMediaKeys.nop.setMediaKeys'); + shaka.asserts.assert(this instanceof HTMLMediaElement); + + if (mediaKeys == null) { + return Promise.resolve(); + } + + return Promise.reject(new Error('MediaKeys not supported.')); +}; + + +/** + * An unusable constructor for MediaKeys. Hosts isTypeSupported. + */ +shaka.polyfill.PatchedMediaKeys.nop.MediaKeys = function() { + throw new TypeError('Illegal constructor.'); +}; + + +/** @override */ +shaka.polyfill.PatchedMediaKeys.nop.MediaKeys.isTypeSupported = + function(keySystem, mimeType) { + // TODO: isTypeSupported deprecated + shaka.log.debug('PatchedMediaKeys.nop.isTypeSupported'); + return false; +}; + diff --git a/lib/polyfill/patchedmediakeys_v01b.js b/lib/polyfill/patchedmediakeys_v01b.js new file mode 100644 index 0000000000..473dc430f0 --- /dev/null +++ b/lib/polyfill/patchedmediakeys_v01b.js @@ -0,0 +1,724 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview A polyfill to implement {@link http://goo.gl/sgJHNN EME draft + * 01 December 2014} on top of {@link http://goo.gl/FSpoAo EME v0.1b}. + * + * @see http://enwp.org/polyfill + */ + +goog.provide('shaka.polyfill.PatchedMediaKeys.v01b'); + +goog.require('shaka.asserts'); +goog.require('shaka.log'); +goog.require('shaka.util.EventManager'); +goog.require('shaka.util.FakeEvent'); +goog.require('shaka.util.FakeEventTarget'); +goog.require('shaka.util.PublicPromise'); +goog.require('shaka.util.StringUtils'); + + +/** + * Install the polyfill. + * @export + */ +shaka.polyfill.PatchedMediaKeys.v01b.install = function() { + shaka.log.debug('v01b.install'); + + shaka.asserts.assert(HTMLMediaElement.prototype.webkitGenerateKeyRequest); + + // Alias. + var v01b = shaka.polyfill.PatchedMediaKeys.v01b; + + // Install patches. + Navigator.prototype.requestMediaKeySystemAccess = + v01b.requestMediaKeySystemAccess; + // Work around read-only declarations for these properties by using strings: + HTMLMediaElement.prototype['mediaKeys'] = null; + HTMLMediaElement.prototype['waitingFor'] = ''; + HTMLMediaElement.prototype.setMediaKeys = v01b.setMediaKeys; + window.MediaKeys = v01b.MediaKeys; + // TODO: isTypeSupported deprecated + window.MediaKeys.isTypeSupported = v01b.MediaKeys.isTypeSupported; +}; + + +/** + * An implementation of Navigator.prototype.requestMediaKeySystemAccess. + * Retrieve a MediaKeySystemAccess object. + * + * @this {!Navigator} + * @param {string} keySystem + * @param {Array.=} opt_supportedConfigurations + * @return {!Promise.} + */ +shaka.polyfill.PatchedMediaKeys.v01b.requestMediaKeySystemAccess = + function(keySystem, opt_supportedConfigurations) { + shaka.log.debug('v01b.requestMediaKeySystemAccess'); + shaka.asserts.assert(this instanceof Navigator); + + // TODO(story 1954733): handle opt_supportedConfigurations. + + // Alias. + var v01b = shaka.polyfill.PatchedMediaKeys.v01b; + try { + var access = new v01b.MediaKeySystemAccess(keySystem); + return Promise.resolve(/** @type {!MediaKeySystemAccess} */ (access)); + } catch (exception) { + return Promise.reject(exception); + } +}; + + +/** + * An implementation of HTMLMediaElement.prototype.setMediaKeys. + * Attach a MediaKeys object to the media element. + * + * @this {!HTMLMediaElement} + * @param {MediaKeys} mediaKeys + * @return {!Promise} + */ +shaka.polyfill.PatchedMediaKeys.v01b.setMediaKeys = function(mediaKeys) { + shaka.log.debug('v01b.setMediaKeys'); + shaka.asserts.assert(this instanceof HTMLMediaElement); + + // Alias. + var v01b = shaka.polyfill.PatchedMediaKeys.v01b; + + var newMediaKeys = + /** @type {shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys} */ ( + mediaKeys); + var oldMediaKeys = + /** @type {shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys} */ ( + this.mediaKeys); + + if (oldMediaKeys && oldMediaKeys != newMediaKeys) { + shaka.asserts.assert(oldMediaKeys instanceof v01b.MediaKeys); + // Have the old MediaKeys stop listening to events on the video tag. + oldMediaKeys.setMedia(null); + } + + delete this['mediaKeys']; // in case there is an existing getter + this['mediaKeys'] = mediaKeys; // work around read-only declaration + + if (newMediaKeys) { + shaka.asserts.assert(newMediaKeys instanceof v01b.MediaKeys); + newMediaKeys.setMedia(this); + } + + return Promise.resolve(); +}; + + +/** + * For some of this polyfill's implementation, we need to query a video element. + * But for some embedded systems, it is memory-expensive to create multiple + * video elements. Therefore, we check the document to see if we can borrow one + * to query before we fall back to creating one temporarily. + * + * @return {!HTMLVideoElement} + * @protected + */ +shaka.polyfill.PatchedMediaKeys.v01b.getVideoElement = function() { + var videos = document.getElementsByTagName('video'); + /** @type {!HTMLVideoElement} */ + var tmpVideo = videos.length ? videos[0] : document.createElement('video'); + return tmpVideo; +}; + + + +/** + * An implementation of MediaKeySystemAccess. + * + * @constructor + * @param {string} keySystem + * @implements {MediaKeySystemAccess} + * @throws {Error} if the key system is not supported. + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySystemAccess = + function(keySystem) { + shaka.log.debug('v01b.MediaKeySystemAccess'); + + /** @type {string} */ + this.keySystem = keySystem; + + /** @private {string} */ + this.internalKeySystem_ = keySystem; + + if (keySystem == 'org.w3.clearkey') { + // Clearkey's string must be prefixed in v0.1b. + this.internalKeySystem_ = 'webkit-org.w3.clearkey'; + } + + var tmpVideo = shaka.polyfill.PatchedMediaKeys.v01b.getVideoElement(); + // v0.1b tests for key system availability with an extra argument on + // canPlayType. This, however, requires you to check video types in order to + // check for key systems. So we check all types we expect might succeed in + // Chrome. + var knownGoodTypes = ['video/mp4', 'video/webm']; + for (var i = 0; i < knownGoodTypes.length; ++i) { + if (tmpVideo.canPlayType(knownGoodTypes[i], this.internalKeySystem_)) { + return; + } + } + + // The key system did not report as playable with any known-good video types. + throw Error('The key system specified is not supported.'); +}; + + +/** @override */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySystemAccess.prototype. + createMediaKeys = function() { + shaka.log.debug('v01b.MediaKeySystemAccess.createMediaKeys'); + + // Alias. + var v01b = shaka.polyfill.PatchedMediaKeys.v01b; + var mediaKeys = new v01b.MediaKeys(this.internalKeySystem_); + return Promise.resolve(/** @type {!MediaKeys} */ (mediaKeys)); +}; + + +/** @override */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySystemAccess.prototype. + getConfiguration = function() { + shaka.log.debug('v01b.MediaKeySystemAccess.getConfiguration'); + // TODO: getConfiguration unsupported + return null; +}; + + + +/** + * An implementation of MediaKeys. + * + * @constructor + * @param {string} keySystem + * @implements {MediaKeys} + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys = function(keySystem) { + shaka.log.debug('v01b.MediaKeys'); + + /** @private {string} */ + this.keySystem_ = keySystem; + + /** @private {HTMLMediaElement} */ + this.media_ = null; + + /** @private {!shaka.util.EventManager} */ + this.eventManager_ = new shaka.util.EventManager(); + + /** + * @private {!Array.} + */ + this.newSessions_ = []; + + /** + * @private {!Object.} + */ + this.sessionMap_ = {}; +}; + + +/** @override */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.isTypeSupported = + function(keySystem, mimeType) { + // TODO: isTypeSupported deprecated + shaka.log.debug('v01b.MediaKeys.isTypeSupported'); + + var tmpVideo = shaka.polyfill.PatchedMediaKeys.v01b.getVideoElement(); + + if (keySystem == 'org.w3.clearkey') { + // Clearkey's string must be prefixed in v0.1b. + keySystem = 'webkit-org.w3.clearkey'; + } + + return !!tmpVideo.canPlayType(mimeType, keySystem); +}; + + +/** + * @param {HTMLMediaElement} media + * @protected + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.setMedia = + function(media) { + this.media_ = media; + + // Remove any old listeners. + this.eventManager_.removeAll(); + + if (media) { + // Intercept and translate these prefixed EME events. + this.eventManager_.listen(media, 'webkitneedkey', + /** @type {shaka.util.EventManager.ListenerType} */ ( + this.onWebkitNeedKey_.bind(this))); + + this.eventManager_.listen(media, 'webkitkeymessage', + /** @type {shaka.util.EventManager.ListenerType} */ ( + this.onWebkitKeyMessage_.bind(this))); + + this.eventManager_.listen(media, 'webkitkeyadded', + /** @type {shaka.util.EventManager.ListenerType} */ ( + this.onWebkitKeyAdded_.bind(this))); + + this.eventManager_.listen(media, 'webkitkeyerror', + /** @type {shaka.util.EventManager.ListenerType} */ ( + this.onWebkitKeyError_.bind(this))); + } +}; + + +/** @override */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.createSession = + function(opt_sessionType) { + shaka.log.debug('v01b.MediaKeys.createSession'); + + var sessionType = opt_sessionType || 'temporary'; + if (sessionType != 'temporary' && sessionType != 'persistent') { + throw new TypeError('Session type ' + opt_sessionType + + ' is unsupported on this platform.'); + } + + // Alias. + var v01b = shaka.polyfill.PatchedMediaKeys.v01b; + + shaka.asserts.assert(this.media_); + var media = /** @type {!HTMLMediaElement} */ (this.media_); + + var session = new v01b.MediaKeySession(media, this.keySystem_, sessionType); + this.newSessions_.push(session); + return session; +}; + + +/** @override */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.setServerCertificate = + function(serverCertificate) { + shaka.log.debug('v01b.MediaKeys.setServerCertificate'); + + // There is no equivalent in v0.1b, so return failure. + return Promise.reject(new Error( + 'setServerCertificate not supported on this platform.')); +}; + + +/** + * @param {!MediaKeyEvent} event + * @private + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.onWebkitNeedKey_ = + function(event) { + shaka.log.debug('v01b.onWebkitNeedKey_', event); + shaka.asserts.assert(this.media_); + + var event2 = shaka.util.FakeEvent.create({ + type: 'encrypted', + initDataType: 'cenc', // not used by v0.1b EME, but given a valid value + initData: event.initData + }); + + this.media_.dispatchEvent(event2); +}; + + +/** + * @param {!MediaKeyEvent} event + * @private + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.onWebkitKeyMessage_ = + function(event) { + shaka.log.debug('v01b.onWebkitKeyMessage_', event); + + var session = this.findSession_(event.sessionId); + shaka.asserts.assert(session); + if (!session) { + shaka.log.error('Session not found', event.sessionId); + return; + } + + var isNew = isNaN(session.expiration); + + var event2 = shaka.util.FakeEvent.create({ + type: 'message', + messageType: isNew ? 'licenserequest' : 'licenserenewal', + message: event.message + }); + + session.generated(); + session.dispatchEvent(event2); +}; + + +/** + * @param {!MediaKeyEvent} event + * @private + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.onWebkitKeyAdded_ = + function(event) { + shaka.log.debug('v01b.onWebkitKeyAdded_', event); + + var session = this.findSession_(event.sessionId); + shaka.asserts.assert(session); + if (session) { + session.ready(); + } +}; + + +/** + * @param {!MediaKeyEvent} event + * @private + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.onWebkitKeyError_ = + function(event) { + shaka.log.debug('v01b.onWebkitKeyError_', event); + + var session = this.findSession_(event.sessionId); + shaka.asserts.assert(session); + if (session) { + session.handleError(event); + } +}; + + +/** + * @param {string} sessionId + * @return {shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession} + * @private + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeys.prototype.findSession_ = + function(sessionId) { + var session = this.sessionMap_[sessionId]; + if (session) { + shaka.log.debug('v01b.MediaKeys.findSession_', session); + return session; + } + + session = this.newSessions_.shift(); + if (session) { + session.sessionId = sessionId; + this.sessionMap_[sessionId] = session; + shaka.log.debug('v01b.MediaKeys.findSession_', session); + return session; + } + + return null; +}; + + + +/** + * An implementation of MediaKeySession. + * + * @param {!HTMLMediaElement} media + * @param {string} keySystem + * @param {string} sessionType + * + * @constructor + * @implements {MediaKeySession} + * @extends {shaka.util.FakeEventTarget} + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession = + function(media, keySystem, sessionType) { + shaka.log.debug('v01b.MediaKeySession'); + shaka.util.FakeEventTarget.call(this, null); + + /** @private {!HTMLMediaElement} */ + this.media_ = media; + + /** @private {shaka.util.PublicPromise} */ + this.generatePromise_ = null; + + /** @private {shaka.util.PublicPromise} */ + this.updatePromise_ = null; + + /** @private {string} */ + this.keySystem_ = keySystem; + + /** @private {string} */ + this.type_ = sessionType; + + /** @type {string} */ + this.sessionId = ''; + + /** @type {number} */ + this.expiration = NaN; + + /** @type {!shaka.util.PublicPromise} */ + this.closed = new shaka.util.PublicPromise(); + + /** @type {!MediaKeyStatuses} */ + this.keyStatuses = {}; + // TODO: key status and 'keyschange' events unsupported +}; +goog.inherits(shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession, + shaka.util.FakeEventTarget); + + +/** + * Signals that the license request has been generated. This resolves the + * 'generateRequest' promise. + * + * @protected + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.generated = + function() { + shaka.log.debug('v01b.MediaKeySession.generated'); + + if (this.generatePromise_) { + this.generatePromise_.resolve(); + this.generatePromise_ = null; + } +}; + + +/** + * Signals that the session is 'ready', which is the terminology used in older + * versions of EME. The new signal is to resolve the 'update' promise. This + * translates between the two. + * + * @protected + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.ready = + function() { + shaka.log.debug('v01b.MediaKeySession.ready'); + + // There is no expiration info in v0.1b, but we want to signal, at least + // internally, that the session is no longer new. This allows us to set + // the messageType attribute of 'message' events. + this.expiration = Number.POSITIVE_INFINITY; + // TODO: key status and 'keyschange' events unsupported + + if (this.updatePromise_) { + this.updatePromise_.resolve(); + } + this.updatePromise_ = null; +}; + + +/** + * Either rejects a promise, or dispatches an error event, as appropriate. + * + * @param {!MediaKeyEvent} event + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.handleError = + function(event) { + shaka.log.debug('v01b.MediaKeySession.handleError', event); + + // This does not match the DOMException we get in current WD EME, but it will + // at least provide some information which can be used to look into the + // problem. + var error = new Error('EME v0.1b key error'); + error.errorCode = event.errorCode; + error.errorCode.systemCode = event.systemCode; + + // The presence or absence of sessionId indicates whether this corresponds to + // generateRequest() or update(). + if (!event.sessionId && this.generatePromise_) { + error.method = 'generateRequest'; + this.generatePromise_.reject(error); + this.generatePromise_ = null; + } else if (event.sessionId && this.updatePromise_) { + error.method = 'update'; + this.updatePromise_.reject(error); + this.updatePromise_ = null; + } +}; + + +/** + * An internal version of generateRequest which defers new calls while old ones + * are in progress. + * + * @param {!shaka.util.PublicPromise} promise The promise associated with this + * call. + * @param {BufferSource} initData + * @param {?string} offlineSessionId + * @private + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.generate_ = + function(promise, initData, offlineSessionId) { + if (this.generatePromise_) { + // We already have a generate in-progress, so defer this one until after + // the old one is resolved. Execute this whether the original one succeeds + // or fails. + this.generatePromise_.then( + this.generate_.bind(this, promise, initData, offlineSessionId) + ).catch( + this.generate_.bind(this, promise, initData, offlineSessionId) + ); + return; + } + + this.generatePromise_ = promise; + try { + /** @type {Uint8Array} */ + var mangledInitData; + + if (this.type_ == 'persistent') { + var StringUtils = shaka.util.StringUtils; + if (!offlineSessionId) { + // Persisting the initial license. + // Prefix the init data with a tag to indicate persistence. + var u8InitData = new Uint8Array(initData); + mangledInitData = StringUtils.toUint8Array( + 'PERSISTENT|' + StringUtils.fromUint8Array(u8InitData)); + } else { + // Loading a stored license. + // Prefix the init data (which is really a session ID) with a tag to + // indicate that we are loading a persisted session. + mangledInitData = StringUtils.toUint8Array( + 'LOAD_SESSION|' + offlineSessionId); + } + } else { + // Streaming. + shaka.asserts.assert(this.type_ == 'temporary'); + shaka.asserts.assert(!offlineSessionId); + mangledInitData = new Uint8Array(initData); + } + + shaka.asserts.assert(mangledInitData); + this.media_.webkitGenerateKeyRequest(this.keySystem_, mangledInitData); + } catch (exception) { + // Reject the promise. + this.generatePromise_.reject(exception); + this.generatePromise_ = null; + } +}; + + +/** + * An internal version of update which defers new calls while old ones are in + * progress. + * + * @param {!shaka.util.PublicPromise} promise The promise associated with this + * call. + * @param {BufferSource} response + * @private + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.update_ = + function(promise, response) { + if (this.updatePromise_) { + // We already have an update in-progress, so defer this one until after the + // old one is resolved. Execute this whether the original one succeeds or + // fails. + this.updatePromise_.then( + this.update_.bind(this, promise, response) + ).catch( + this.update_.bind(this, promise, response) + ); + return; + } + + this.updatePromise_ = promise; + + var key; + var keyId; + + if (this.keySystem_ == 'webkit-org.w3.clearkey') { + // The current EME version of clearkey wants a structured JSON response. + // The v0.1b version wants just a raw key. Parse the JSON response and + // extract the key and key ID. + var StringUtils = shaka.util.StringUtils; + var licenseString = StringUtils.fromUint8Array(new Uint8Array(response)); + var jwkSet = /** @type {JWKSet} */ (JSON.parse(licenseString)); + key = StringUtils.toUint8Array(StringUtils.fromBase64(jwkSet.keys[0].k)); + keyId = StringUtils.toUint8Array( + StringUtils.fromBase64(jwkSet.keys[0].kid)); + } else { + // The key ID is not required. + key = new Uint8Array(response); + keyId = null; + } + + try { + this.media_.webkitAddKey(this.keySystem_, key, keyId, this.sessionId); + } catch (exception) { + // Reject the promise. + this.updatePromise_.reject(exception); + this.updatePromise_ = null; + } +}; + + +/** @override */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.generateRequest = + function(initDataType, initData) { + shaka.log.debug('v01b.MediaKeySession.generateRequest'); + var nextGeneratePromise = new shaka.util.PublicPromise(); + this.generate_(nextGeneratePromise, initData, null); + return nextGeneratePromise; +}; + + +/** @override */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.load = + function(sessionId) { + shaka.log.debug('v01b.MediaKeySession.load'); + if (this.type_ == 'persistent') { + var nextGeneratePromise = new shaka.util.PublicPromise(); + this.generate_(nextGeneratePromise, null, sessionId); + return nextGeneratePromise; + } else { + return Promise.reject(new Error('The session type is not "persistent".')); + } +}; + + +/** @override */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.update = + function(response) { + shaka.log.debug('v01b.MediaKeySession.update', response); + shaka.asserts.assert(this.sessionId); + + var nextUpdatePromise = new shaka.util.PublicPromise(); + this.update_(nextUpdatePromise, response); + return nextUpdatePromise; +}; + + +/** @override */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.close = + function() { + shaka.log.debug('v01b.MediaKeySession.close'); + shaka.asserts.assert(this.sessionId); + + if (this.type_ != 'persistent') { + // This will remove a persistent session, but it's also the only way to + // free CDM resources on v0.1b. + this.media_.webkitCancelKeyRequest(this.keySystem_, this.sessionId); + } + + // Resolve the 'closed' promise and return it. + this.closed.resolve(); + return this.closed; +}; + + +/** @override */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.remove = + function() { + shaka.log.debug('v01b.MediaKeySession.remove'); + + if (this.type_ != 'persistent') { + return Promise.reject(new Error('Not a persistent session.')); + } + + return this.close(); +}; + diff --git a/lib/polyfill/videoplaybackquality.js b/lib/polyfill/videoplaybackquality.js new file mode 100644 index 0000000000..b2c746b7ad --- /dev/null +++ b/lib/polyfill/videoplaybackquality.js @@ -0,0 +1,66 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview A polyfill to provide MSE VideoPlaybackQuality metrics with a + * single API. + * + * @see http://enwp.org/polyfill + */ + +goog.provide('shaka.polyfill.VideoPlaybackQuality'); + + +/** + * @namespace shaka.polyfill.VideoPlaybackQuality + * @export + * + * @summary A polyfill to provide MSE VideoPlaybackQuality metrics. + * Many browsers do not yet provide this API, and Chrome currently provides + * similar data through individual prefixed attributes on HTMLVideoElement. + */ + + +/** + * Install the polyfill if needed. + * @export + */ +shaka.polyfill.VideoPlaybackQuality.install = function() { + var proto = HTMLVideoElement.prototype; + if (proto.getVideoPlaybackQuality) { + // No polyfill needed. + return; + } + + /** + * @this {HTMLVideoElement} + * @return {VideoPlaybackQuality} + */ + proto.getVideoPlaybackQuality = function() { + if (!('webkitDroppedFrameCount' in this)) { + // No stats available. + return null; + } + + return /** @type {VideoPlaybackQuality} */ ({ + 'corruptedVideoFrames': 0, + 'droppedVideoFrames': this.webkitDroppedFrameCount, + 'totalVideoFrames': this.webkitDecodedFrameCount, + // Not provided by this polyfill: + 'creationTime': null, + 'totalFrameDelay': null + }); + }; +}; + diff --git a/lib/util/ajax_request.js b/lib/util/ajax_request.js new file mode 100644 index 0000000000..44c5c8f43f --- /dev/null +++ b/lib/util/ajax_request.js @@ -0,0 +1,387 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements an asynchronous HTTP request. + */ + +goog.provide('shaka.util.AjaxRequest'); + +goog.require('goog.Uri'); +goog.require('shaka.asserts'); +goog.require('shaka.log'); +goog.require('shaka.util.IBandwidthEstimator'); +goog.require('shaka.util.PublicPromise'); +goog.require('shaka.util.StringUtils'); + + + +/** + * Creates an AjaxRequest. An AjaxRequest manages retries automatically. + * + * @param {string} url The URL to request. + * + * @struct + * @constructor + */ +shaka.util.AjaxRequest = function(url) { + /** + * The request URL. + * @protected {string} + */ + this.url = url; + + /** + * A collection of parameters which an instance of a subclass may wish to + * override. + * @protected {!shaka.util.AjaxRequest.Parameters} + */ + this.parameters = new shaka.util.AjaxRequest.Parameters(); + + /** + * The number of times the request has been attempted. + * @private {number} + */ + this.attempts_ = 0; + + /** + * A timestamp in milliseconds when the request began. + * @private {number} + */ + this.startTime_ = 0; + + /** + * The delay, in milliseconds, before the next retry. + * @private {number} + */ + this.retryDelayMs_ = 0; + + /** + * The last used delay. This is used in unit tests only. + * @private {number} + */ + this.lastDelayMs_ = 0; + + /** @private {XMLHttpRequest} */ + this.xhr_ = null; + + /** + * Resolved when the request is completed successfully. + * Rejected if it cannot be completed. + * @private {shaka.util.PublicPromise.} + */ + this.promise_ = new shaka.util.PublicPromise(); + + /** + * @type {shaka.util.IBandwidthEstimator} + */ + this.estimator = null; +}; + + + +/** + * A collection of parameters which an instance of a subclass may wish to + * override. + * + * @struct + * @constructor + */ +shaka.util.AjaxRequest.Parameters = function() { + /** + * The request body, if desired. + * @type {ArrayBuffer} + */ + this.body = null; + + /** + * The maximum number of times the request should be attempted. + * @type {number} + */ + this.maxAttempts = 1; + + /** + * The delay before the first retry, in milliseconds. + * @type {number} + */ + this.baseRetryDelayMs = 1000; + + /** + * The multiplier for successive retry delays. + * @type {number} + */ + this.retryBackoffFactor = 2.0; + + /** + * The maximum amount of fuzz to apply to each retry delay. + * For example, 0.5 means "between 50% below and 50% above the retry delay." + * @type {number} + */ + this.retryFuzzFactor = 0.5; + + /** + * The request timeout, in milliseconds. Zero means "unlimited". + * @type {number} + */ + this.requestTimeoutMs = 0; + + /** + * The HTTP request method, such as 'GET' or 'POST'. + * @type {string} + */ + this.method = 'GET'; + + /** + * The response type, corresponding to XMLHttpRequest.responseType. + * @type {string} + */ + this.responseType = 'arraybuffer'; + + /** + * A dictionary of request headers. + * @type {!Object.} + */ + this.requestHeaders = {}; + + /** + * Make requests with credentials. This will allow cookies in cross-site + * requests. + * @see http://goo.gl/YBRKPe + * @type {boolean} + */ + this.withCredentials = false; +}; + + +/** + * Destroys the AJAX request. + * This happens automatically after the internal promise is resolved or + * rejected. + * + * @private + */ +shaka.util.AjaxRequest.prototype.destroy_ = function() { + this.cleanupRequest_(); + this.parameters.body = null; + this.promise_ = null; + this.estimator = null; +}; + + +/** + * Remove |xhr_|'s references to bound functions, and set |xhr_| to null. + * + * @private + */ +shaka.util.AjaxRequest.prototype.cleanupRequest_ = function() { + if (this.xhr_) { + this.xhr_.onload = null; + this.xhr_.onerror = null; + } + this.xhr_ = null; +}; + + +/** + * Sends the request. Called by subclasses. + * + * @return {Promise.} + * + * @protected + */ +shaka.util.AjaxRequest.prototype.sendInternal = function() { + shaka.asserts.assert(this.xhr_ == null); + if (this.xhr_) { + // The request is already in-progress, so there's nothing to do. + return this.promise_; + } + + // We can't request from data URIs, so handle it separately. + if (this.url.lastIndexOf('data:', 0) == 0) { + return this.handleDataUri_(); + } + + this.attempts_++; + this.startTime_ = Date.now(); + + if (!this.retryDelayMs_) { + // First try. Lock in the retry delay. + this.retryDelayMs_ = this.parameters.baseRetryDelayMs; + } + + this.xhr_ = new XMLHttpRequest(); + + var url = this.url; + if (this.estimator) { + // NOTE: Cached responses ruin bandwidth estimation and can cause wildly + // inappropriate adaptation decisions. Since we cannot detect that a + // response was cached after the fact, we add a cache-busting parameter to + // the request to avoid caching. There are other methods, but they do not + // work cross-origin without control over both client and server. + var modifiedUri = new goog.Uri(url); + modifiedUri.getQueryData().add('_', Date.now()); + url = modifiedUri.toString(); + } + + this.xhr_.open(this.parameters.method, url, true); + this.xhr_.responseType = this.parameters.responseType; + this.xhr_.timeout = this.parameters.requestTimeoutMs; + this.xhr_.withCredentials = this.parameters.withCredentials; + + this.xhr_.onload = this.onLoad_.bind(this); + this.xhr_.onerror = this.onError_.bind(this); + + for (var k in this.parameters.requestHeaders) { + this.xhr_.setRequestHeader(k, this.parameters.requestHeaders[k]); + } + this.xhr_.send(this.parameters.body); + + return this.promise_; +}; + + +/** + * Handles a data URI. + * This method does not modify |this|'s state. + * + * @return {!Promise} + * + * @private + */ +shaka.util.AjaxRequest.prototype.handleDataUri_ = function() { + // Alias. + var StringUtils = shaka.util.StringUtils; + + // Fake the data URI request. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs + var path = this.url.split(':')[1]; + var optionalTypeAndRest = path.split(';'); + var rest = optionalTypeAndRest.pop(); + var optionalEncodingAndData = rest.split(','); + var data = optionalEncodingAndData.pop(); + var optionalEncoding = optionalEncodingAndData.pop(); + + if (optionalEncoding == 'base64') { + data = StringUtils.fromBase64(data); + } else { + data = window.decodeURIComponent(data); + } + + if (this.parameters.responseType == 'arraybuffer') { + data = StringUtils.toUint8Array(data).buffer; + } + + // We can't set the response field of an XHR, although we can make a + // hacky object that will still look like an XHR. + var xhr = /** @type {!XMLHttpRequest} */ ( + JSON.parse(JSON.stringify(new XMLHttpRequest()))); + xhr.response = data; + xhr.responseText = data.toString(); + + var promise = this.promise_; + promise.resolve(xhr); + this.destroy_(); + return promise; +}; + + +/** + * Aborts an in-progress request. + * If a request is not in-progress then this function does nothing. + */ +shaka.util.AjaxRequest.prototype.abort = function() { + if (!this.xhr_ || this.xhr_.readyState == XMLHttpRequest.DONE) { + return; + } + shaka.asserts.assert(this.xhr_.readyState != 0); + + this.xhr_.abort(); + + var error = new Error('Request aborted.'); + error.type = 'aborted'; + error.status = null; + error.xhr = this.xhr_; + + this.promise_.reject(error); + this.destroy_(); +}; + + +/** + * Handles a "load" event. + * + * @param {!ProgressEvent} event The ProgressEvent from the request. + * + * @private + */ +shaka.util.AjaxRequest.prototype.onLoad_ = function(event) { + shaka.asserts.assert(event.target == this.xhr_); + + if (this.estimator) { + this.estimator.sample(Date.now() - this.startTime_, event.loaded); + } + + if (this.xhr_.status >= 200 && this.xhr_.status <= 299) { + // All 2xx HTTP codes are success cases. + this.promise_.resolve(this.xhr_); + this.destroy_(); + } else if (this.attempts_ < this.parameters.maxAttempts) { + this.cleanupRequest_(); + + var sendAgain = this.sendInternal.bind(this); + + // Fuzz the delay to avoid tons of clients hitting the server at once + // after it recovers from whatever is causing it to fail. + var negToPosOne = (Math.random() * 2.0) - 1.0; + var negToPosFuzzFactor = negToPosOne * this.parameters.retryFuzzFactor; + var fuzzedDelay = this.retryDelayMs_ * (1.0 + negToPosFuzzFactor); + window.setTimeout(sendAgain, fuzzedDelay); + + // Store the fuzzed delay to make testing retries feasible. + this.lastDelayMs_ = fuzzedDelay; + + // Back off the next delay. + this.retryDelayMs_ *= this.parameters.retryBackoffFactor; + } else { + var error = new Error('Network failure.'); + error.type = 'net'; + error.status = this.xhr_.status; + error.xhr = this.xhr_; + + this.promise_.reject(error); + this.destroy_(); + } +}; + + +/** + * Handles an "error" event. + * + * @param {!ProgressEvent} event The ProgressEvent from this.xhr_. + * + * @private + */ +shaka.util.AjaxRequest.prototype.onError_ = function(event) { + // Do not try again since an "error" event is usually unrecoverable. + shaka.asserts.assert(event.target == this.xhr_); + + var error = new Error('Network failure.'); + error.type = 'net'; + error.status = this.xhr_.status; + error.xhr = this.xhr_; + + this.promise_.reject(error); + this.destroy_(); +}; + diff --git a/lib/util/array_utils.js b/lib/util/array_utils.js new file mode 100644 index 0000000000..2ae650c0a3 --- /dev/null +++ b/lib/util/array_utils.js @@ -0,0 +1,93 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Array utility functions. + */ + +goog.provide('shaka.util.ArrayUtils'); + + +/** + * @namespace shaka.util.ArrayUtils + * @summary Array utility functions. + */ + + +/** + * Intersect two arrays. Assumes that the arrays are already sorted. + * @param {!Array} a + * @param {!Array} b + * @return {!Array} The intersection of a and b. + * @throws {TypeError} if the array contents cannot be sorted or compared. + */ +shaka.util.ArrayUtils.intersect = function(a, b) { + var ai = 0; + var bi = 0; + var result = []; + while (ai < a.length && bi < b.length) { + if (a[ai] < b[bi]) { + ++ai; + } else if (a[ai] > b[bi]) { + ++bi; + } else if (a[ai] == b[bi]) { + result.push(a[ai]); + ++ai; + ++bi; + } else { + throw new TypeError('Cannot intersect non-sortable values!'); + } + } + return result; +}; + + +/** + * Create an array from object keys. + * @param {!Object} object + * @return {!Array} A sorted list of the keys. + */ +shaka.util.ArrayUtils.fromObjectKeys = function(object) { + var result = []; + for (var k in object) { + result.push(k); + } + result.sort(); + return result; +}; + + +/** + * Remove duplicate entries from an array. + * @param {!Array.} array + * @param {function(T): string=} opt_keyFn An optional function which takes an + * array item and converts it into a key. Use this if your array items + * cannot be used as an index into an Object. + * @return {!Array.} A sorted list of the keys. + * @template T + */ +shaka.util.ArrayUtils.removeDuplicates = function(array, opt_keyFn) { + var set = {}; + for (var i = 0; i < array.length; ++i) { + var key = opt_keyFn ? opt_keyFn(array[i]) : array[i].toString(); + set[key] = array[i]; + } + + var result = []; + for (var k in set) { + result.push(set[k]); + } + return result; +}; + diff --git a/lib/util/data_view_reader.js b/lib/util/data_view_reader.js new file mode 100644 index 0000000000..3c568ca4ee --- /dev/null +++ b/lib/util/data_view_reader.js @@ -0,0 +1,167 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a data view reader, which abstracts a DataView + * object. + */ + +goog.provide('shaka.util.DataViewReader'); + +goog.require('shaka.asserts'); + + + +/** + * Creates a DataViewReader + * + * @param {!DataView} dataView The DataView. + * @param {shaka.util.DataViewReader.Endianness} endianness The endianness. + * @constructor + */ +shaka.util.DataViewReader = function(dataView, endianness) { + /** @private {!DataView} */ + this.dataView_ = dataView; + + /** @private {boolean} */ + this.littleEndian_ = + endianness == shaka.util.DataViewReader.Endianness.LITTLE_ENDIAN; + + /** @private {number} */ + this.position_ = 0; +}; + + +/** + * Endianness. + * @enum {number} + */ +shaka.util.DataViewReader.Endianness = { + BIG_ENDIAN: 0, + LITTLE_ENDIAN: 1 +}; + + +/** + * @return {boolean} True if the reader has more data, false otherwise. + */ +shaka.util.DataViewReader.prototype.hasMoreData = function() { + return this.position_ < this.dataView_.byteLength; +}; + + +/** + * Gets the current byte position. + * @return {number} + */ +shaka.util.DataViewReader.prototype.getPosition = function() { + return this.position_; +}; + + +/** + * Reads an unsigned 8 bit integer, and advances the reader. + * @return {number} The integer. + * @throws {RangeError} when reading past the end of the data view. + */ +shaka.util.DataViewReader.prototype.readUint8 = function() { + var value = this.dataView_.getUint8(this.position_); + this.position_ += 1; + return value; +}; + + +/** + * Reads an unsigned 16 bit integer, and advances the reader. + * @return {number} The integer. + * @throws {RangeError} when reading past the end of the data view. + */ +shaka.util.DataViewReader.prototype.readUint16 = function() { + var value = this.dataView_.getUint16(this.position_, this.littleEndian_); + this.position_ += 2; + return value; +}; + + +/** + * Reads an unsigned 32 bit integer, and advances the reader. + * @return {number} The integer. + * @throws {RangeError} when reading past the end of the data view. + */ +shaka.util.DataViewReader.prototype.readUint32 = function() { + var value = this.dataView_.getUint32(this.position_, this.littleEndian_); + this.position_ += 4; + return value; +}; + + +/** + * Reads an unsigned 64 bit integer, and advances the reader. + * @return {number} The integer. + * @throws {RangeError} when reading past the end of the data view or when + * reading an integer too large to store accurately in JavaScript. + */ +shaka.util.DataViewReader.prototype.readUint64 = function() { + var low, high; + + if (this.littleEndian_) { + low = this.dataView_.getUint32(this.position_, true); + high = this.dataView_.getUint32(this.position_ + 4, true); + } else { + high = this.dataView_.getUint32(this.position_, false); + low = this.dataView_.getUint32(this.position_ + 4, false); + } + + if (high > 0x1FFFFF) { + throw new RangeError('DataViewReader: Overflow reading 64-bit value.'); + } + + this.position_ += 8; + + // NOTE: This is subtle, but in JavaScript you can't shift left by 32 and get + // the full range of 53-bit values possible. You must multiply by 2^32. + return (high * Math.pow(2, 32)) + low; +}; + + +/** + * Reads the specified number of raw bytes. + * @param {number} bytes The number of bytes to read. + * @return {!Uint8Array} + * @throws {RangeError} when reading past the end of the data view. + */ +shaka.util.DataViewReader.prototype.readBytes = function(bytes) { + shaka.asserts.assert(bytes > 0); + if (this.position_ + bytes > this.dataView_.byteLength) { + throw new RangeError('DataViewReader: Read past end of DataView.'); + } + var value = new Uint8Array(this.dataView_.buffer, this.position_, bytes); + this.position_ += bytes; + return value; +}; + + +/** + * Skips the specified number of bytes. + * @param {number} bytes The number of bytes to skip. + * @throws {RangeError} when skipping past the end of the data view. + */ +shaka.util.DataViewReader.prototype.skip = function(bytes) { + shaka.asserts.assert(bytes > 0); + if (this.position_ + bytes > this.dataView_.byteLength) { + throw new RangeError('DataViewReader: Skip past end of DataView.'); + } + this.position_ += bytes; +}; + diff --git a/lib/util/ebml_parser.js b/lib/util/ebml_parser.js new file mode 100644 index 0000000000..ba6f47acfc --- /dev/null +++ b/lib/util/ebml_parser.js @@ -0,0 +1,298 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements an Extensible Binary Markup Language (EBML) parser. + */ + +goog.provide('shaka.util.EbmlElement'); +goog.provide('shaka.util.EbmlParser'); + + + +/** + * Creates an EbmlParser. + * @param {!DataView} dataView The EBML data. + * @constructor + */ +shaka.util.EbmlParser = function(dataView) { + /** @private {!DataView} */ + this.dataView_ = dataView; + + /** @private {!shaka.util.DataViewReader} */ + this.reader_ = new shaka.util.DataViewReader( + dataView, + shaka.util.DataViewReader.Endianness.BIG_ENDIAN); +}; + + +/** @const {!Array.} */ +shaka.util.EbmlParser.DYNAMIC_SIZES = [ + new Uint8Array([0xff]), + new Uint8Array([0x7f, 0xff]), + new Uint8Array([0x3f, 0xff, 0xff]), + new Uint8Array([0x1f, 0xff, 0xff, 0xff]), + new Uint8Array([0x0f, 0xff, 0xff, 0xff, 0xff]), + new Uint8Array([0x07, 0xff, 0xff, 0xff, 0xff, 0xff]), + new Uint8Array([0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), + new Uint8Array([0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])]; + + +/** + * @return {boolean} True if the parser has more data, false otherwise. + */ +shaka.util.EbmlParser.prototype.hasMoreData = function() { + return this.reader_.hasMoreData(); +}; + + +/** + * Parses an EBML element from the parser's current position, and advances + * the parser. + * @return {!shaka.util.EbmlElement} The EBML element. + * @throws {RangeError} + * @see http://matroska.org/technical/specs/rfc/index.html + */ +shaka.util.EbmlParser.prototype.parseElement = function() { + var id = this.parseId_(); + + // Parse the element's size. + var vint = this.parseVint_(); + if (shaka.util.EbmlParser.isDynamicSizeValue_(vint)) { + throw new RangeError( + 'EbmlParser: Element cannot contain dynamically sized data.'); + } + var size = shaka.util.EbmlParser.getVintValue_(vint); + + // Note that if the element's size is larger than the buffer then we are + // parsing a "partial element". This may occur if for example we are + // parsing the beginning of some WebM container data, but our buffer does + // not contain the entire WebM container data. + var elementSize = + this.reader_.getPosition() + size <= this.dataView_.byteLength ? + size : + this.dataView_.byteLength - this.reader_.getPosition(); + + var dataView = new DataView( + this.dataView_.buffer, + this.dataView_.byteOffset + this.reader_.getPosition(), elementSize); + + this.reader_.skip(elementSize); + + return new shaka.util.EbmlElement(id, dataView); +}; + + +/** + * Parses an EBML ID from the parser's current position, and advances the + * parser. + * @throws {RangeError} + * @return {number} The EBML ID. + * @private + */ +shaka.util.EbmlParser.prototype.parseId_ = function() { + var vint = this.parseVint_(); + + if (vint.length > 7) { + throw new RangeError('EbmlParser: EBML ID must be at most 7 bytes.'); + } + + var id = 0; + for (var i = 0; i < vint.length; i++) { + // Note that we cannot use << since |value| may exceed 32 bits. + id = (256 * id) + vint[i]; + } + + return id; +}; + + +/** + * Parses a variable sized integer from the parser's current position, and + * advances the parser. + * For example: + * 1 byte wide: 1xxx xxxx + * 2 bytes wide: 01xx xxxx xxxx xxxx + * 3 bytes wide: 001x xxxx xxxx xxxx xxxx xxxx + * @throws {RangeError} + * @return {!Uint8Array} The variable sized integer. + * @private + */ +shaka.util.EbmlParser.prototype.parseVint_ = function() { + var firstByte = this.reader_.readUint8(); + var numBytes; + + // Determine the byte width of the variable sized integer. + for (numBytes = 1; numBytes <= 8; numBytes++) { + var mask = 0x1 << (8 - numBytes); + if (firstByte & mask) { + break; + } + } + + if (numBytes > 8) { + throw new RangeError( + 'EbmlParser: Variable sized integer must fit within 8 bytes.'); + } + + var vint = new Uint8Array(numBytes); + vint[0] = firstByte; + + // Include the remaining bytes. + for (var i = 1; i < numBytes; i++) { + vint[i] = this.reader_.readUint8(); + } + + return vint; +}; + + +/** + * Gets the value of a variable sized integer. + * For example, the x's below are part of the vint's value. + * 7-bit value: 1xxx xxxx + * 14-bit value: 01xx xxxx xxxx xxxx + * 21-bit value: 001x xxxx xxxx xxxx xxxx xxxx + * @param {!Uint8Array} vint The variable sized integer. + * @throws {RangeError} + * @return {number} The value of the variable sized integer. + * @private + */ +shaka.util.EbmlParser.getVintValue_ = function(vint) { + // If |vint| is 8 bytes wide then we must ensure that it does not have more + // than 53 meaningful bits. For example, assume |vint| is 8 bytes wide, + // so it has the following structure, + // 0000 0001 | xxxx xxxx ... + // Thus, the the first 3 bits following the first byte of |vint| must be 0. + if ((vint.length == 8) && (vint[1] & 0xe0)) { + throw new RangeError( + 'EbmlParser: Variable sized integer value must be at most 53 bits.'); + } + + // Mask out the first few bits of |vint|'s first byte to get the most + // significant bits of |vint|'s value. If |vint| is 8 bytes wide then |value| + // will be set to 0. + var mask = 0x1 << (8 - vint.length); + var value = vint[0] & (mask - 1); + + // Add the remaining bytes. + for (var i = 1; i < vint.length; i++) { + // Note that we cannot use << since |value| may exceed 32 bits. + value = (256 * value) + vint[i]; + } + + return value; +}; + + +/** + * Checks if the given variable sized integer represents a dynamic size value. + * @param {!Uint8Array} vint The variable sized integer. + * @return {boolean} true if |vint| represents a dynamic size value, + * false otherwise. + * @private + */ +shaka.util.EbmlParser.isDynamicSizeValue_ = function(vint) { + var EbmlParser = shaka.util.EbmlParser; + var uint8ArrayEqual = shaka.util.StringUtils.uint8ArrayEqual; + + for (var i = 0; i < EbmlParser.DYNAMIC_SIZES.length; i++) { + if (uint8ArrayEqual(vint, EbmlParser.DYNAMIC_SIZES[i])) { + return true; + } + } + + return false; +}; + + + +/** + * Creates an EbmlElement. + * @param {number} id The ID. + * @param {!DataView} dataView The DataView. + * @constructor + */ +shaka.util.EbmlElement = function(id, dataView) { + /** @type {number} */ + this.id = id; + + /** @private {!DataView} */ + this.dataView_ = dataView; +}; + + +/** + * Gets the element's offset from the beginning of the buffer. + * @return {number} + */ +shaka.util.EbmlElement.prototype.getOffset = function() { + return this.dataView_.byteOffset; +}; + + +/** + * Interpret the element's data as a list of sub-elements. + * @throws {RangeError} + * @return {shaka.util.EbmlParser} A parser over the sub-elements. + */ +shaka.util.EbmlElement.prototype.createParser = function() { + return new shaka.util.EbmlParser(this.dataView_); +}; + + +/** + * Interpret the element's data as an unsigned integer. + * @throws {RangeError} + * @return {number} + */ +shaka.util.EbmlElement.prototype.getUint = function() { + if (this.dataView_.byteLength > 8) { + throw new RangeError('EbmlElement: Unsigned integer has too many bytes.'); + } + + // Ensure we have at most 53 meaningful bits. + if ((this.dataView_.byteLength == 8) && (this.dataView_.getUint8(0) & 0xe0)) { + throw new RangeError( + 'EbmlParser: Unsigned integer must be at most 53 bits.'); + } + + var value = 0; + + for (var i = 0; i < this.dataView_.byteLength; i++) { + var chunk = this.dataView_.getUint8(i); + value = (256 * value) + chunk; + } + + return value; +}; + + +/** + * Interpret the element's data as a floating point number (32 bits or 64 bits). + * 80-bit floating point numbers are not supported. + * @throws {RangeError} + * @return {number} + */ +shaka.util.EbmlElement.prototype.getFloat = function() { + if (this.dataView_.byteLength == 4) { + return this.dataView_.getFloat32(0); + } else if (this.dataView_.byteLength == 8) { + return this.dataView_.getFloat64(0); + } else { + throw new RangeError( + 'EbmlElement: floating point numbers must be 4 or 8 bytes.'); + } +}; + diff --git a/lib/util/event_manager.js b/lib/util/event_manager.js new file mode 100644 index 0000000000..ac0c67b494 --- /dev/null +++ b/lib/util/event_manager.js @@ -0,0 +1,136 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview EventManager class. + */ + +goog.provide('shaka.util.EventManager'); + +goog.require('shaka.asserts'); +goog.require('shaka.util.MultiMap'); + + + +/** + * Creates a new EventManager. An EventManager maintains a collection of "event + * bindings" between event targets and event listeners. + * @constructor + */ +shaka.util.EventManager = function() { + /** + * Maps an event type to an array of event bindings. + * @private {shaka.util.MultiMap.} + */ + this.bindingMap_ = new shaka.util.MultiMap(); +}; + + +/** + * @typedef {function(!Event)} + */ +shaka.util.EventManager.ListenerType; + + +/** + * Destroys the shaka.util.EventManager. All event listeners are detached. + */ +shaka.util.EventManager.prototype.destroy = function() { + this.removeAll(); + this.bindingMap_ = null; +}; + + +/** + * Attaches an event listener to an event target. + * @param {!EventTarget} target The event target. + * @param {string} type The event type. + * @param {shaka.util.EventManager.ListenerType} listener The event listener. + */ +shaka.util.EventManager.prototype.listen = function(target, type, listener) { + var binding = new shaka.util.EventManager.Binding_(target, type, listener); + this.bindingMap_.push(type, binding); +}; + + +/** + * Detaches an event listener from an event target. + * @param {!EventTarget} target The event target. + * @param {string} type The event type. + */ +shaka.util.EventManager.prototype.unlisten = function(target, type) { + var list = this.bindingMap_.get(type) || []; + + for (var i = 0; i < list.length; ++i) { + var binding = list[i]; + + if (binding.target == target) { + binding.unlisten(); + this.bindingMap_.remove(type, binding); + } + } +}; + + +/** + * Detaches all event listeners from all targets. + */ +shaka.util.EventManager.prototype.removeAll = function() { + var list = this.bindingMap_.getAll(); + + for (var i = 0; i < list.length; ++i) { + list[i].unlisten(); + } + + this.bindingMap_.clear(); +}; + + + +/** + * Creates a new Binding_ and attaches the event listener to the event target. + * @param {!EventTarget} target The event target. + * @param {string} type The event type. + * @param {shaka.util.EventManager.ListenerType} listener The event listener. + * @constructor + * @private + */ +shaka.util.EventManager.Binding_ = function(target, type, listener) { + /** @type {EventTarget} */ + this.target = target; + + /** @type {string} */ + this.type = type; + + /** @type {?shaka.util.EventManager.ListenerType} */ + this.listener = listener; + + this.target.addEventListener(type, listener, false); +}; + + +/** + * Detaches the event listener from the event target. This does nothing if the + * event listener is already detached. + */ +shaka.util.EventManager.Binding_.prototype.unlisten = function() { + if (!this.target) + return; + + this.target.removeEventListener(this.type, this.listener, false); + + this.target = null; + this.listener = null; +}; + diff --git a/lib/util/ewma.js b/lib/util/ewma.js new file mode 100644 index 0000000000..2f8b2ba0e3 --- /dev/null +++ b/lib/util/ewma.js @@ -0,0 +1,78 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Computes an exponentially-weighted moving average. + */ + +goog.provide('shaka.util.EWMA'); + +goog.require('shaka.asserts'); + + + +/** + * Computes an exponentionally-weighted moving average. + * + * @param {number} halfLife About half of the estimated value will be from the + * last |halfLife| samples by weight. + * @struct + * @constructor + */ +shaka.util.EWMA = function(halfLife) { + shaka.asserts.assert(halfLife > 0); + + /** + * Larger values of alpha expire historical data more slowly. + * @private {number} + */ + this.alpha_ = Math.exp(Math.log(0.5) / halfLife); + + /** @private {number} */ + this.estimate_ = 0; + + /** @private {number} */ + this.totalWeight_ = 0; +}; + + +/** + * Takes a sample. + * + * @param {number} weight + * @param {number} value + */ +shaka.util.EWMA.prototype.sample = function(weight, value) { + var adjAlpha = Math.pow(this.alpha_, weight); + this.estimate_ = value * (1 - adjAlpha) + adjAlpha * this.estimate_; + this.totalWeight_ += weight; +}; + + +/** + * @return {number} + */ +shaka.util.EWMA.prototype.getTotalWeight = function() { + return this.totalWeight_; +}; + + +/** + * @return {number} + */ +shaka.util.EWMA.prototype.getEstimate = function() { + var zeroFactor = 1 - Math.pow(this.alpha_, this.totalWeight_); + return this.estimate_ / zeroFactor; +}; + diff --git a/lib/util/ewma_bandwidth_estimator.js b/lib/util/ewma_bandwidth_estimator.js new file mode 100644 index 0000000000..0305c897eb --- /dev/null +++ b/lib/util/ewma_bandwidth_estimator.js @@ -0,0 +1,116 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Tracks bandwidth samples and estimates available bandwidth. + * Based on the minimum of two exponentially-weighted moving averages with + * different half-lives. + */ + +goog.provide('shaka.util.EWMABandwidthEstimator'); + +goog.require('shaka.util.EWMA'); +goog.require('shaka.util.FakeEvent'); +goog.require('shaka.util.FakeEventTarget'); +goog.require('shaka.util.IBandwidthEstimator'); + + + +/** + * Tracks bandwidth samples and estimates available bandwidth. + * + * @struct + * @constructor + * @extends {shaka.util.FakeEventTarget} + * @implements {shaka.util.IBandwidthEstimator} + */ +shaka.util.EWMABandwidthEstimator = function() { + shaka.util.FakeEventTarget.call(this, null); + + /** + * A fast-moving average. + * Half of the estimate is based on the last 3 seconds of sample history. + * @private {!shaka.util.EWMA} + */ + this.fast_ = new shaka.util.EWMA(3); + + /** + * A slow-moving average. + * Half of the estimate is based on the last 10 seconds of sample history. + * @private {!shaka.util.EWMA} + */ + this.slow_ = new shaka.util.EWMA(10); + + /** + * Prevents ultra-fast internal connections from causing crazy results. + * @private {number} + * @const + */ + this.minDelayMs_ = 50; + + /** + * Initial estimate used when there is not enough data. + * @private {number} + * @const + */ + this.defaultEstimate_ = 5e5; // 500kbps + + /** + * Minimum weight required to trust the estimate. + * @private {number} + * @const + */ + this.minWeight_ = 0.5; + + /** + * Minimum number of bytes, under which samples are discarded. + * @private {number} + * @const + */ + this.minBytes_ = 65536; +}; +goog.inherits(shaka.util.EWMABandwidthEstimator, shaka.util.FakeEventTarget); + + +/** @override */ +shaka.util.EWMABandwidthEstimator.prototype.sample = function(delayMs, bytes) { + if (bytes < this.minBytes_) { + return; + } + + delayMs = Math.max(delayMs, this.minDelayMs_); + + var bandwidth = 8000 * bytes / delayMs; + var weight = delayMs / 1000; + + this.fast_.sample(weight, bandwidth); + this.slow_.sample(weight, bandwidth); + + this.dispatchEvent(shaka.util.FakeEvent.create({ + 'type': 'bandwidth' + })); +}; + + +/** @override */ +shaka.util.EWMABandwidthEstimator.prototype.getBandwidth = function() { + if (this.fast_.getTotalWeight() < this.minWeight_) { + return this.defaultEstimate_; + } + + // Take the minimum of these two estimates. This should have the effect of + // adapting down quickly, but up more slowly. + return Math.min(this.fast_.getEstimate(), this.slow_.getEstimate()); +}; + diff --git a/lib/util/fake_event.js b/lib/util/fake_event.js new file mode 100644 index 0000000000..b3e5ac7f39 --- /dev/null +++ b/lib/util/fake_event.js @@ -0,0 +1,59 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview A utility to simplify the creation of fake events. + */ + +goog.provide('shaka.util.FakeEvent'); + + +/** + * @namespace shaka.util.FakeEvent + * @summary A utility to simplify the creation of fake events. + */ + + +/** + * Return an Event object based on the dictionary. + * The event should contain all of the same properties from the dict. + * @param {!Object} dict + * @return {!Event} + */ +shaka.util.FakeEvent.create = function(dict) { + var event = new CustomEvent(dict.type, { + detail: dict.detail, + bubbles: !!dict.bubbles + }); + // NOTE: This trick will not work in strict mode. + for (var key in dict) { + event[key] = dict[key]; + } + return event; +}; + + +/** + * Return an 'error' Event object based on an Error object. + * @param {!Error} error + * @return {!Event} + */ +shaka.util.FakeEvent.createErrorEvent = function(error) { + var event = new CustomEvent('error', { + detail: error, + bubbles: true + }); + return event; +}; + diff --git a/lib/util/fake_event_target.js b/lib/util/fake_event_target.js new file mode 100644 index 0000000000..6ccd07799b --- /dev/null +++ b/lib/util/fake_event_target.js @@ -0,0 +1,152 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview A utility base class which is a non-DOM work-alike for + * EventTarget. This simplifies the dispatch of events from custom classes. + */ + +goog.provide('shaka.util.FakeEventTarget'); + +goog.require('shaka.asserts'); +goog.require('shaka.util.MultiMap'); + + + +/** + * A work-alike for EventTarget. Only DOM elements may be true EventTargets, + * but this can be used as a base class to provide event dispatch to non-DOM + * classes. + * + * @param {shaka.util.FakeEventTarget} parent The parent for the purposes of + * event bubbling. Note that events on a FakeEventTarget can only bubble + * to other FakeEventTargets. + * @struct + * @constructor + * @implements {EventTarget} + */ +shaka.util.FakeEventTarget = function(parent) { + /** + * @private {!shaka.util.MultiMap.} + */ + this.listeners_ = new shaka.util.MultiMap(); + + /** @protected {shaka.util.FakeEventTarget} */ + this.parent = parent; +}; + + +/** + * These are the listener types defined in the closure extern for EventTarget. + * @typedef {EventListener|function(!Event):(boolean|undefined)} + */ +shaka.util.FakeEventTarget.ListenerType; + + +/** + * Add an event listener to this object. + * + * @param {string} type The event type to listen for. + * @param {shaka.util.FakeEventTarget.ListenerType} listener The callback or + * listener object to invoke. + * @param {boolean} capturing True to listen during the capturing phase, false + * to listen during the bubbling phase. Note that FakeEventTarget does not + * support the capturing phase from the standard event model. + * @override + */ +shaka.util.FakeEventTarget.prototype.addEventListener = + function(type, listener, capturing) { + // We don't support the capturing phase. + shaka.asserts.assert(!capturing); + if (!capturing) { + this.listeners_.push(type, listener); + } +}; + + +/** + * Remove an event listener from this object. + * + * @param {string} type The event type for which you wish to remove a listener. + * @param {shaka.util.FakeEventTarget.ListenerType} listener The callback or + * listener object to remove. + * @param {boolean} capturing True to remove a listener for the capturing phase, + * false to remove a listener for the bubbling phase. Note that + * FakeEventTarget does not support the capturing phase from the standard + * event model. + * @override + */ +shaka.util.FakeEventTarget.prototype.removeEventListener = + function(type, listener, capturing) { + // We don't support the capturing phase. + shaka.asserts.assert(!capturing); + if (!capturing) { + this.listeners_.remove(type, listener); + } +}; + + +/** + * Dispatch an event from this object. + * + * @param {!Event} event The event to be dispatched from this object. + * @return {boolean} True if the default action was prevented. + * @override + */ +shaka.util.FakeEventTarget.prototype.dispatchEvent = function(event) { + // NOTE: These tricks will not work in strict mode. + + // Because these are read-only properties, you must delete them first. + delete event.currentTarget; + delete event.srcElement; + delete event.target; + + // event.currentTarget will be set by recursiveDispatch_(). + event.srcElement = null; // Must be Element or null, so just use null. + event.target = this; + + return this.recursiveDispatch_(event); +}; + + +/** + * Dispatches an event recursively without changing its original target. + * + * @param {!Event} event + * @return {boolean} True if the default action was prevented. + * @private + */ +shaka.util.FakeEventTarget.prototype.recursiveDispatch_ = function(event) { + event.currentTarget = this; + + var list = this.listeners_.get(event.type) || []; + + for (var i = 0; i < list.length; ++i) { + var listener = list[i]; + if (listener.handleEvent) { + listener.handleEvent(event); + } else { + listener.call(this, event); + } + // NOTE: If needed, stopImmediatePropagation() would be checked here. + } + + // NOTE: If needed, stopPropagation() would be checked here. + if (this.parent && event.bubbles) { + this.parent.recursiveDispatch_(event); + } + + return event.defaultPrevented; +}; + diff --git a/lib/util/i_bandwidth_estimator.js b/lib/util/i_bandwidth_estimator.js new file mode 100644 index 0000000000..d3eb1c017a --- /dev/null +++ b/lib/util/i_bandwidth_estimator.js @@ -0,0 +1,58 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview An interface for tracking bandwidth samples and estimating + * available bandwidth. + */ + +goog.provide('shaka.util.IBandwidthEstimator'); + + +/** + * @event shaka.util.IBandwidthEstimator.BandwidthEvent + * @description Fired when a new bandwidth estimate is available. + * @property {string} type 'bandwidth' + * @property {boolean} bubbles false + */ + + + +/** + * Tracks bandwidth samples and estimates available bandwidth. + * + * @interface + * @extends {EventTarget} + */ +shaka.util.IBandwidthEstimator = function() {}; + + +/** + * Takes a bandwidth sample and dispatches a 'bandwidth' event. + * + * @fires shaka.util.IBandwidthEstimator.BandwidthEvent + * + * @param {number} delayMs The time it took to collect the sample, in ms. + * @param {number} bytes The number of bytes downloaded. + */ +shaka.util.IBandwidthEstimator.prototype.sample = function(delayMs, bytes) {}; + + +/** + * Get estimated bandwidth in bits per second. + * + * @return {number} + */ +shaka.util.IBandwidthEstimator.prototype.getBandwidth = function() {}; + diff --git a/lib/util/language_utils.js b/lib/util/language_utils.js new file mode 100644 index 0000000000..9a495d9a7b --- /dev/null +++ b/lib/util/language_utils.js @@ -0,0 +1,155 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Language utility functions. + */ + +goog.provide('shaka.util.LanguageUtils'); + +goog.require('shaka.asserts'); + + +/** + * @namespace shaka.util.LanguageUtils + * @summary A set of language utility functions. + */ + + +/** + * Compares two language tags as defined by RFC 5646 and ISO 639. The + * comparison takes sublanguages into account via the |fuzz| parameter. + * The caller is expected to normalize the inputs first. + * + * @see shaka.util.LanguageUtils.normalize() + * @see IETF RFC 5646 + * @see ISO 639 + * + * @param {shaka.util.LanguageUtils.MatchType} fuzz What kind of match is + * acceptable. + * @param {string} preference The user's preferred language tag. + * @param {string} candidate An available language tag. + * @return {boolean} + */ +shaka.util.LanguageUtils.match = function(fuzz, preference, candidate) { + // Alias. + var LanguageUtils = shaka.util.LanguageUtils; + + shaka.asserts.assert(preference == LanguageUtils.normalize(preference)); + shaka.asserts.assert(candidate == LanguageUtils.normalize(candidate)); + + if (candidate == preference) { + return true; + } + + if (fuzz >= shaka.util.LanguageUtils.MatchType.BASE_LANGUAGE_OKAY && + candidate == preference.split('-')[0]) { + return true; + } + + if (fuzz >= shaka.util.LanguageUtils.MatchType.OTHER_SUB_LANGUAGE_OKAY && + candidate.split('-')[0] == preference.split('-')[0]) { + return true; + } + + return false; +}; + + +/** + * A match type for fuzzy-matching logic. + * + * @enum {number} + */ +shaka.util.LanguageUtils.MatchType = { + /** Accepts an exact match. */ + EXACT: 0, + /** Accepts a less-specific version of the preferred sublanguage. */ + BASE_LANGUAGE_OKAY: 1, + /** Accepts a different sublanguage of the preferred base language. */ + OTHER_SUB_LANGUAGE_OKAY: 2, + + MIN: 0, MAX: 2 +}; + + +/** + * Normalize the language tag. + * + * RFC 5646 specifies that language tags are case insensitive and that the + * shortest representation of the base language should always be used. + * This will convert the tag to lower-case and map 3-letter codes (ISO 639-2) + * to 2-letter codes (ISO 639-1) whenever possible. + * + * @param {string} lang + * @return {string} + * + * @see IETF RFC 5646 + * @see ISO 639 + */ +shaka.util.LanguageUtils.normalize = function(lang) { + var fields = lang.toLowerCase().split('-'); + var base = fields[0]; + var replacement = shaka.util.LanguageUtils.isoMap_[base]; + if (replacement) { + fields[0] = replacement; + } + return fields.join('-'); +}; + + +/** + * A map from 3-letter language codes (ISO 639-2) to 2-letter language codes + * (ISO 639-1) for all languages which have both in the registry. + * + * @const {!Object.} + * @private + */ +shaka.util.LanguageUtils.isoMap_ = { + 'aar': 'aa', 'abk': 'ab', 'afr': 'af', 'aka': 'ak', 'alb': 'sq', 'amh': 'am', + 'ara': 'ar', 'arg': 'an', 'arm': 'hy', 'asm': 'as', 'ava': 'av', 'ave': 'ae', + 'aym': 'ay', 'aze': 'az', 'bak': 'ba', 'bam': 'bm', 'baq': 'eu', 'bel': 'be', + 'ben': 'bn', 'bih': 'bh', 'bis': 'bi', 'bod': 'bo', 'bos': 'bs', 'bre': 'br', + 'bul': 'bg', 'bur': 'my', 'cat': 'ca', 'ces': 'cs', 'cha': 'ch', 'che': 'ce', + 'chi': 'zh', 'chu': 'cu', 'chv': 'cv', 'cor': 'kw', 'cos': 'co', 'cre': 'cr', + 'cym': 'cy', 'cze': 'cs', 'dan': 'da', 'deu': 'de', 'div': 'dv', 'dut': 'nl', + 'dzo': 'dz', 'ell': 'el', 'eng': 'en', 'epo': 'eo', 'est': 'et', 'eus': 'eu', + 'ewe': 'ee', 'fao': 'fo', 'fas': 'fa', 'fij': 'fj', 'fin': 'fi', 'fra': 'fr', + 'fre': 'fr', 'fry': 'fy', 'ful': 'ff', 'geo': 'ka', 'ger': 'de', 'gla': 'gd', + 'gle': 'ga', 'glg': 'gl', 'glv': 'gv', 'gre': 'el', 'grn': 'gn', 'guj': 'gu', + 'hat': 'ht', 'hau': 'ha', 'heb': 'he', 'her': 'hz', 'hin': 'hi', 'hmo': 'ho', + 'hrv': 'hr', 'hun': 'hu', 'hye': 'hy', 'ibo': 'ig', 'ice': 'is', 'ido': 'io', + 'iii': 'ii', 'iku': 'iu', 'ile': 'ie', 'ina': 'ia', 'ind': 'id', 'ipk': 'ik', + 'isl': 'is', 'ita': 'it', 'jav': 'jv', 'jpn': 'ja', 'kal': 'kl', 'kan': 'kn', + 'kas': 'ks', 'kat': 'ka', 'kau': 'kr', 'kaz': 'kk', 'khm': 'km', 'kik': 'ki', + 'kin': 'rw', 'kir': 'ky', 'kom': 'kv', 'kon': 'kg', 'kor': 'ko', 'kua': 'kj', + 'kur': 'ku', 'lao': 'lo', 'lat': 'la', 'lav': 'lv', 'lim': 'li', 'lin': 'ln', + 'lit': 'lt', 'ltz': 'lb', 'lub': 'lu', 'lug': 'lg', 'mac': 'mk', 'mah': 'mh', + 'mal': 'ml', 'mao': 'mi', 'mar': 'mr', 'may': 'ms', 'mkd': 'mk', 'mlg': 'mg', + 'mlt': 'mt', 'mon': 'mn', 'mri': 'mi', 'msa': 'ms', 'mya': 'my', 'nau': 'na', + 'nav': 'nv', 'nbl': 'nr', 'nde': 'nd', 'ndo': 'ng', 'nep': 'ne', 'nld': 'nl', + 'nno': 'nn', 'nob': 'nb', 'nor': 'no', 'nya': 'ny', 'oci': 'oc', 'oji': 'oj', + 'ori': 'or', 'orm': 'om', 'oss': 'os', 'pan': 'pa', 'per': 'fa', 'pli': 'pi', + 'pol': 'pl', 'por': 'pt', 'pus': 'ps', 'que': 'qu', 'roh': 'rm', 'ron': 'ro', + 'rum': 'ro', 'run': 'rn', 'rus': 'ru', 'sag': 'sg', 'san': 'sa', 'sin': 'si', + 'slk': 'sk', 'slo': 'sk', 'slv': 'sl', 'sme': 'se', 'smo': 'sm', 'sna': 'sn', + 'snd': 'sd', 'som': 'so', 'sot': 'st', 'spa': 'es', 'sqi': 'sq', 'srd': 'sc', + 'srp': 'sr', 'ssw': 'ss', 'sun': 'su', 'swa': 'sw', 'swe': 'sv', 'tah': 'ty', + 'tam': 'ta', 'tat': 'tt', 'tel': 'te', 'tgk': 'tg', 'tgl': 'tl', 'tha': 'th', + 'tib': 'bo', 'tir': 'ti', 'ton': 'to', 'tsn': 'tn', 'tso': 'ts', 'tuk': 'tk', + 'tur': 'tr', 'twi': 'tw', 'uig': 'ug', 'ukr': 'uk', 'urd': 'ur', 'uzb': 'uz', + 'ven': 've', 'vie': 'vi', 'vol': 'vo', 'wel': 'cy', 'wln': 'wa', 'wol': 'wo', + 'xho': 'xh', 'yid': 'yi', 'yor': 'yo', 'zha': 'za', 'zho': 'zh', 'zul': 'zu' +}; + diff --git a/lib/util/license_request.js b/lib/util/license_request.js new file mode 100644 index 0000000000..edb17b5404 --- /dev/null +++ b/lib/util/license_request.js @@ -0,0 +1,62 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a license request. + */ + +goog.provide('shaka.util.LicenseRequest'); + +goog.require('shaka.util.AjaxRequest'); + + + +/** + * Creates a LicenseRequest. A LicenseRequest manages retries automatically. + * + * @param {string} serverUrl The URL of the license server. + * @param {!ArrayBuffer} requestBody The body of the license request. + * @param {boolean} withCredentials True if cookies should be sent in + * cross-domain license requests. If true, the browser will reject license + * responses which use the wildcard header "Access-Control-Allow-Origin: *". + * See http://goo.gl/pzY9F7 for more information. + * + * @struct + * @constructor + * @extends {shaka.util.AjaxRequest} + */ +shaka.util.LicenseRequest = function(serverUrl, requestBody, withCredentials) { + shaka.util.AjaxRequest.call(this, serverUrl); + this.parameters.body = requestBody; + this.parameters.method = 'POST'; + this.parameters.maxAttempts = 3; + this.parameters.withCredentials = withCredentials; +}; +goog.inherits(shaka.util.LicenseRequest, shaka.util.AjaxRequest); + + +/** + * Sends the license request. + * @return {!Promise.} + */ +shaka.util.LicenseRequest.prototype.send = function() { + return this.sendInternal().then( + /** @param {!XMLHttpRequest} xhr */ + function(xhr) { + var response = /** @type {ArrayBuffer} */ (xhr.response); + return Promise.resolve(new Uint8Array(response)); + } + ); +}; + diff --git a/lib/util/multi_map.js b/lib/util/multi_map.js new file mode 100644 index 0000000000..e11005bc14 --- /dev/null +++ b/lib/util/multi_map.js @@ -0,0 +1,109 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview A simple multimap template. + */ + +goog.provide('shaka.util.MultiMap'); + + + +/** + * @constructor + * @template T + */ +shaka.util.MultiMap = function() { + /** @private {!Object.>} */ + this.map_ = {}; +}; + + +/** + * Add a key, value pair to the map. + * @param {string} key + * @param {T} value + */ +shaka.util.MultiMap.prototype.push = function(key, value) { + if (this.map_.hasOwnProperty(key)) { + this.map_[key].push(value); + } else { + this.map_[key] = [value]; + } +}; + + +/** + * Get a list of values by key. + * @param {string} key + * @return {Array.} or null if no such key exists. + */ +shaka.util.MultiMap.prototype.get = function(key) { + var list = this.map_[key]; + // slice() clones the list so that it and the map can each be modified + // without affecting the other. + return list ? list.slice() : null; +}; + + +/** + * Get a list of all values. + * @return {!Array.} + */ +shaka.util.MultiMap.prototype.getAll = function() { + var list = []; + for (var key in this.map_) { + list.push.apply(list, this.map_[key]); + } + return list; +}; + + +/** + * Remove a specific value, if it exists. + * @param {string} key + * @param {T} value + */ +shaka.util.MultiMap.prototype.remove = function(key, value) { + var list = this.map_[key]; + if (!list) return; + for (var i = 0; i < list.length; ++i) { + if (list[i] == value) { + list.splice(i, 1); + --i; + } + } +}; + + +/** + * Get all keys from the multimap. + * @return {!Array.} + */ +shaka.util.MultiMap.prototype.keys = function() { + var result = []; + for (var key in this.map_) { + result.push(key); + } + return result; +}; + + +/** + * Clear all keys and values from the multimap. + */ +shaka.util.MultiMap.prototype.clear = function() { + this.map_ = {}; +}; + diff --git a/lib/util/pssh.js b/lib/util/pssh.js new file mode 100644 index 0000000000..8fd473631e --- /dev/null +++ b/lib/util/pssh.js @@ -0,0 +1,73 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a PSSH parser. + */ + +goog.provide('shaka.util.Pssh'); + +goog.require('shaka.util.DataViewReader'); +goog.require('shaka.util.StringUtils'); + + + +/** + * Parse a PSSH box and extract the system IDs. + * + * @param {!Uint8Array} psshBox + * @constructor + * @throws {RangeError} in the unlikely event that a PSSH box contains a size + * field over 53 bits. + */ +shaka.util.Pssh = function(psshBox) { + /** + * @type {!Array.} + * @expose + */ + this.systemIds = []; + + // Parse the PSSH box. + var reader = new shaka.util.DataViewReader( + new DataView(psshBox.buffer), + shaka.util.DataViewReader.Endianness.BIG_ENDIAN); + + // There could be multiple boxes concatenated together. + while (reader.hasMoreData()) { + var headerSize = 8; + var size = reader.readUint32(); + var type = reader.readUint32(); + if (size == 1) { + size = reader.readUint64(); + headerSize += 8; + } + + if (type != shaka.util.Pssh.BOX_TYPE) { + reader.skip(size - headerSize); + continue; + } + + var versionAndFlags = reader.readUint32(); + var systemId = shaka.util.StringUtils.fromUint8Array(reader.readBytes(16)); + var dataSize = reader.readUint32(); + reader.skip(dataSize); // Ignore the data section. + + this.systemIds.push(systemId); + } +}; + + +/** @const {number} */ +shaka.util.Pssh.BOX_TYPE = 0x70737368; + diff --git a/lib/util/public_promise.js b/lib/util/public_promise.js new file mode 100644 index 0000000000..bed13cf30c --- /dev/null +++ b/lib/util/public_promise.js @@ -0,0 +1,59 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview A utility to create Promises with convenient public resolve + * and reject methods. + */ + +goog.provide('shaka.util.PublicPromise'); + + + +/** + * @constructor + * @extends {Promise.} + * @template T + */ +shaka.util.PublicPromise = function() { + var resolvePromise; + var rejectPromise; + + // Promise.call causes an error. It seems that inheriting from a native + // Promise is not permitted by JavaScript interpreters. + + // The work-around is to construct a Promise object, modify it to look like + // the compiler's picture of PublicPromise, then return it. The caller of + // new PublicPromise will receive |promise| instead of |this|, and the + // compiler will be aware of the additional properties |resolve| and + // |reject|. + + var promise = new Promise(function(resolve, reject) { + resolvePromise = resolve; + rejectPromise = reject; + }); + + promise.resolve = resolvePromise; + promise.reject = rejectPromise; + return promise; +}; + + +/** @type {function(T=)} */ +shaka.util.PublicPromise.prototype.resolve; + + +/** @type {function(*)} */ +shaka.util.PublicPromise.prototype.reject; + diff --git a/lib/util/range_request.js b/lib/util/range_request.js new file mode 100644 index 0000000000..417f46ca4f --- /dev/null +++ b/lib/util/range_request.js @@ -0,0 +1,63 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a range request. + */ + +goog.provide('shaka.util.RangeRequest'); + +goog.require('shaka.asserts'); +goog.require('shaka.util.AjaxRequest'); + + + +/** + * Creates a RangeRequest. + * + * @param {string} url The URL. + * @param {number} begin The start byte. + * @param {?number} end The end byte, null to request to the end. + * + * @struct + * @constructor + * @extends {shaka.util.AjaxRequest} + */ +shaka.util.RangeRequest = function(url, begin, end) { + shaka.util.AjaxRequest.call(this, url); + + shaka.asserts.assert(begin !== undefined && begin !== null); + // Avoid adding the Range header if the range is actually the whole file. + if (begin || end) { + var rangeString = begin + '-' + (end != null ? end : ''); + this.parameters.requestHeaders['Range'] = 'bytes=' + rangeString; + } +}; +goog.inherits(shaka.util.RangeRequest, shaka.util.AjaxRequest); + + +/** + * Sends the range request. + * @return {!Promise.} + */ +shaka.util.RangeRequest.prototype.send = function() { + return this.sendInternal().then( + /** @param {!XMLHttpRequest} xhr */ + function(xhr) { + var data = /** @type {!ArrayBuffer} */ (xhr.response); + return Promise.resolve(data); + } + ); +}; + diff --git a/lib/util/string_utils.js b/lib/util/string_utils.js new file mode 100644 index 0000000000..9dabaae5ca --- /dev/null +++ b/lib/util/string_utils.js @@ -0,0 +1,133 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview String utility functions. + */ + +goog.provide('shaka.util.StringUtils'); + + +/** + * @namespace shaka.util.StringUtils + * @export + * @summary A set of string utility functions. + */ + + +/** + * Convert a string to a Uint8Array. + * + * @param {string} str The input string. + * @return {!Uint8Array} The output array. + * @export + */ +shaka.util.StringUtils.toUint8Array = function(str) { + var result = new Uint8Array(str.length); + for (var i = 0; i < str.length; i++) { + result[i] = str.charCodeAt(i); + } + return result; +}; + + +/** + * Convert a Uint8Array to a string. + * + * @param {Uint8Array} array The input array. + * @return {string} The output string. + * @export + */ +shaka.util.StringUtils.fromUint8Array = function(array) { + return String.fromCharCode.apply(null, array); +}; + + +/** + * Convert a raw string to a base-64 string. + * + * @param {string} str The raw string. + * @param {boolean=} opt_padding If true, pad the output with equals signs. + * Defaults to true. + * @return {string} The base-64 string. + * @export + */ +shaka.util.StringUtils.toBase64 = function(str, opt_padding) { + var base64 = window.btoa(str); + var padding = (opt_padding == undefined) ? true : opt_padding; + return padding ? base64 : base64.replace(/=*$/, ''); +}; + + +/** + * Convert a base-64 string to a raw string. + * + * @param {string} str The base-64 string. + * @return {string} The raw string. + * @export + */ +shaka.util.StringUtils.fromBase64 = function(str) { + return window.atob(str); +}; + + +/** + * Convert a hex string to a raw string. + * + * @param {string} str The hex string. + * @return {string} The output string. + * @export + */ +shaka.util.StringUtils.fromHex = function(str) { + var ints = []; + for (var i = 0; i < str.length; i += 2) { + ints.push(window.parseInt(str.substr(i, 2), 16)); + } + return String.fromCharCode.apply(null, ints); +}; + + +/** + * Compare two Uint8Arrays for equality. + * + * @param {Uint8Array} array1 + * @param {Uint8Array} array2 + * @return {boolean} + * @export + */ +shaka.util.StringUtils.uint8ArrayEqual = function(array1, array2) { + if (!array1 && !array2) return true; + if (!array1 || !array2) return false; + if (array1.length != array2.length) return false; + for (var i = 0; i < array1.length; ++i) { + if (array1[i] != array2[i]) return false; + } + return true; +}; + + +/** + * Convert a Uint8Array to a string which can be used as a key in a dictionary. + * @param {!Uint8Array} array + * @return {string} + * @export + */ +shaka.util.StringUtils.uint8ArrayKey = function(array) { + var tmp = []; + for (var i = 0; i < array.length; ++i) { + tmp.push(array[i]); + } + return tmp.join(','); +}; + diff --git a/lib/util/typed_bind.js b/lib/util/typed_bind.js new file mode 100644 index 0000000000..962ed7d65b --- /dev/null +++ b/lib/util/typed_bind.js @@ -0,0 +1,39 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview A bind wrapper which infers and preserves type information. + */ + +goog.provide('shaka.util.TypedBind'); + + +/** + * @namespace + * + * @summary A bind wrapper which infers and preserves type information in + * the closure compiler. Function.prototype.bind, in contrast, destroys the + * compiler's type information. As a trade-off, this interface limits the + * number of arguments to the bound function and does not permit partial + * binding. + * + * @param {CLASS} context + * @param {function(this:CLASS, A)} fn + * @return {function(A)} + * @template CLASS, A + */ +shaka.util.TypedBind = function(context, fn) { + return fn.bind(context); +}; + diff --git a/load.js b/load.js new file mode 100644 index 0000000000..36e4bbd9f2 --- /dev/null +++ b/load.js @@ -0,0 +1,57 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Loads the library. Chooses compiled or debug version of the + * library based on the presence or absence of the URL parameter "compiled". + * This dynamic loading process is not necessary in a production environment, + * but greatly simplifies the process of switching between compiled and + * uncompiled mode during development. + * + * This is used in the provided test app, but can also be used to load the + * uncompiled version of the library into your own application environment. + */ + +(function() { // anonymous namespace + // The sources may be in a different folder from the test app. + // Compute the base URL for all library sources. + var loaderSrc = document.currentScript.src; + var baseUrl = loaderSrc.split('/').slice(0, -1).join('/') + '/'; + + function loadScript(src) { + // This does not seem like it would be the best way to do this, but the + // timing is different than creating a new script element and appending + // it to the head element. This way, all script loading happens before + // DOMContentLoaded. This is also compatible with goog.require's loading + // mechanism, whereas appending an element to head isn't. + document.write(''); + } + + var params = location.search.split('?').pop(); + params = params ? params.split(';') : []; + + var compiledMode = (params.indexOf('compiled') >= 0); + + if (compiledMode) { + // This contains the entire library, compiled in debug mode. + loadScript('shaka-player.compiled.debug.js'); + } else { + // In non-compiled mode, loading the closure library is enough to bootstrap + // the system. goog.require will load additional dependencies at runtime. + loadScript('third_party/closure/goog/base.js'); + // This file contains goog.require calls for all exported classes. + loadScript('shaka-player.uncompiled.js'); + } +})(); // anonymous namespace + diff --git a/shaka-player.uncompiled.js b/shaka-player.uncompiled.js new file mode 100644 index 0000000000..82ae2a3f4a --- /dev/null +++ b/shaka-player.uncompiled.js @@ -0,0 +1,26 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Requires all exported classes from the library. + */ + +goog.require('shaka.player.DashVideoSource'); +goog.require('shaka.player.DrmSchemeInfo'); +goog.require('shaka.player.HttpVideoSource'); +goog.require('shaka.player.Player'); +goog.require('shaka.polyfill.Fullscreen'); +goog.require('shaka.polyfill.MediaKeys'); +goog.require('shaka.polyfill.VideoPlaybackQuality'); +goog.require('shaka.util.StringUtils'); diff --git a/spec/dash_video_source_spec.js b/spec/dash_video_source_spec.js new file mode 100644 index 0000000000..c7d5fc7747 --- /dev/null +++ b/spec/dash_video_source_spec.js @@ -0,0 +1,160 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview dash_video_source.js unit tests. + */ + +goog.require('shaka.dash.mpd'); +goog.require('shaka.player.AudioTrack'); +goog.require('shaka.player.DashVideoSource'); +goog.require('shaka.player.VideoTrack'); +goog.require('shaka.util.MultiMap'); + +describe('DashVideoSource', function() { + // TODO(story 2544737): Add additional unit tests. + it('is able to get audio and video tracks.', function() { + // |videoRepresentation1| and |videoRepresentation2| are part of + // |videoSet1| + var videoRepresentation1 = new shaka.dash.mpd.Representation(); + videoRepresentation1.mimeType = 'video/mp4'; + videoRepresentation1.bandwidth = 1000; + videoRepresentation1.width = 640; + videoRepresentation1.height = 380; + + var videoRepresentation2 = new shaka.dash.mpd.Representation(); + videoRepresentation2.mimeType = 'video/mp4'; + videoRepresentation2.bandwidth = 2000; + videoRepresentation2.width = 720; + videoRepresentation2.height = 480; + + // |audioRepresentation1| and |audioRepresentation2| are part of + // |audioSet1| + var audioRepresentation1 = new shaka.dash.mpd.Representation(); + audioRepresentation1.mimeType = 'audio/mp4'; + audioRepresentation1.bandwidth = 500; + + var audioRepresentation2 = new shaka.dash.mpd.Representation(); + audioRepresentation2.mimeType = 'audio/mp4'; + audioRepresentation2.bandwidth = 900; + + // |audioRepresentation3| and |audioRepresentation4| are part of + // |audioSet2| + var audioRepresentation3 = new shaka.dash.mpd.Representation(); + audioRepresentation3.mimeType = 'audio/mp4'; + audioRepresentation3.bandwidth = 500; + + var audioRepresentation4 = new shaka.dash.mpd.Representation(); + audioRepresentation4.mimeType = 'audio/mp4'; + audioRepresentation4.bandwidth = 800; + + var videoSet1 = new shaka.dash.mpd.AdaptationSet(); + videoSet1.contentType = 'video'; + videoSet1.representations = [videoRepresentation1, videoRepresentation2]; + + var audioSet1 = new shaka.dash.mpd.AdaptationSet(); + audioSet1.lang = 'en'; + audioSet1.contentType = 'audio'; + audioSet1.representations = [audioRepresentation1, audioRepresentation2]; + + var audioSet2 = new shaka.dash.mpd.AdaptationSet(); + audioSet2.lang = 'fr'; + audioSet2.contentType = 'audio'; + audioSet2.representations = [audioRepresentation3, audioRepresentation4]; + + var adaptationSetMap = new shaka.util.MultiMap(); + adaptationSetMap.push('video', videoSet1); + adaptationSetMap.push('audio', audioSet1); + adaptationSetMap.push('audio', audioSet2); + + var videoSource = createFakeDashVideoSource(adaptationSetMap); + + // Aliases. + var AudioTrack = shaka.player.AudioTrack; + var VideoTrack = shaka.player.VideoTrack; + + // Check the audio tracks. + var expectedAudioTracks = [ + new AudioTrack(3, 500, 'en'), + new AudioTrack(4, 900, 'en'), + new AudioTrack(5, 500, 'fr'), + new AudioTrack(6, 800, 'fr')]; + + var audioTracks = videoSource.getAudioTracks(); + expect(audioTracks).not.toBeNull(); + expect(audioTracks.length).toBe(expectedAudioTracks.length); + + expectedAudioTracks.sort(AudioTrack.compare); + audioTracks.sort(AudioTrack.compare); + + for (var i = 0; i < expectedAudioTracks.length; ++i) { + expect(audioTracks[i].id).toBe(expectedAudioTracks[i].id); + expect(AudioTrack.compare(audioTracks[i], + expectedAudioTracks[i])).toBe(0); + } + + // Check the video tracks. + var expectedVideoTracks = [ + new VideoTrack(1, 1000, 640, 380), + new VideoTrack(2, 2000, 720, 480)]; + + var videoTracks = videoSource.getVideoTracks(); + expect(videoTracks).not.toBeNull(); + expect(videoTracks.length).toBe(expectedVideoTracks.length); + + expectedVideoTracks.sort(VideoTrack.compare); + videoTracks.sort(VideoTrack.compare); + + for (var i = 0; i < expectedVideoTracks.length; ++i) { + expect(videoTracks[i].id).toBe(expectedVideoTracks[i].id); + expect(VideoTrack.compare(videoTracks[i], + expectedVideoTracks[i])).toBe(0); + } + }); + + // Creates a fake DashVideoSource object. This is a hack to test + // getAudioTracks() and getVideoTracks() without actually creating a real + // DashVideoSource, which would require setting up a bunch of mock objects. + // TODO(story 2544737): Use a real DashVideoSource object. + createFakeDashVideoSource = function(adaptationSetMap) { + var fakeMpdProcessor = { + getNumPeriods : function() { + return 1; + }, + getAdaptationSets : function(periodIdx, opt_type) { + console.assert(periodIdx == 0); + return opt_type ? + adaptationSetMap.get(opt_type) : + adaptationSetMap.getAll(); + } + }; + + fakeDashVideoSource = {}; + + fakeDashVideoSource.streamsByType_ = {}; + + fakeDashVideoSource.processor_ = fakeMpdProcessor; + + fakeDashVideoSource.getAudioTracks = + shaka.player.DashVideoSource.prototype.getAudioTracks.bind( + fakeDashVideoSource); + + fakeDashVideoSource.getVideoTracks = + shaka.player.DashVideoSource.prototype.getVideoTracks.bind( + fakeDashVideoSource); + + return fakeDashVideoSource; + }; +}); + diff --git a/spec/data_view_reader_spec.js b/spec/data_view_reader_spec.js new file mode 100644 index 0000000000..6aa3dcd3cf --- /dev/null +++ b/spec/data_view_reader_spec.js @@ -0,0 +1,253 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview data_view_reader.js unit tests. + */ + +goog.require('shaka.util.DataViewReader'); + +// TODO(story 2544737): Add tests that use a data view that views only a slice +// of an array. +describe('DataViewReader', function() { + // |data| as interpreted as a 64 bit integer must not be larger than 2^53-1. + // decimal digits. + var data = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); + // |data2| is small enough in little-endian to be read as a 64-bit number, + // and has the sign bit set on the first 6 bytes to prove that we don't + // return negative values. + var data2 = new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0xff, 0xff, 0x01, 0x00]); + + var bigEndianReader; + var littleEndianReader; + + beforeEach(function() { + bigEndianReader = new shaka.util.DataViewReader( + new DataView(data.buffer), + shaka.util.DataViewReader.Endianness.BIG_ENDIAN); + bigEndianReader2 = new shaka.util.DataViewReader( + new DataView(data2.buffer), + shaka.util.DataViewReader.Endianness.BIG_ENDIAN); + littleEndianReader = new shaka.util.DataViewReader( + new DataView(data.buffer), + shaka.util.DataViewReader.Endianness.LITTLE_ENDIAN); + littleEndianReader2 = new shaka.util.DataViewReader( + new DataView(data2.buffer), + shaka.util.DataViewReader.Endianness.LITTLE_ENDIAN); + }); + + it('is able to read a uint8 in big endian.', function() { + var value1 = bigEndianReader.readUint8(); + expect(value1).toBe(0x00); + + var value2 = bigEndianReader.readUint8(); + expect(value2).toBe(0x01); + + var value3 = bigEndianReader2.readUint8(); + expect(value3).toBe(0xde); + + var value4 = bigEndianReader2.readUint8(); + expect(value4).toBe(0xad); + }); + + it('is able to read a uint16 in big endian.', function() { + var value1 = bigEndianReader.readUint16(); + expect(value1).toBe(0x0001); + + var value2 = bigEndianReader.readUint16(); + expect(value2).toBe(0x0203); + + var value3 = bigEndianReader2.readUint16(); + expect(value3).toBe(0xdead); + + var value4 = bigEndianReader2.readUint16(); + expect(value4).toBe(0xbeef); + }); + + it('is able to read a uint32 in big endian.', function() { + var value1 = bigEndianReader.readUint32(); + expect(value1).toBe(0x00010203); + + var value2 = bigEndianReader.readUint32(); + expect(value2).toBe(0x04050607); + + var value3 = bigEndianReader2.readUint32(); + expect(value3).toBe(0xdeadbeef); + + var value4 = bigEndianReader2.readUint32(); + expect(value4).toBe(0xffff0100); + }); + + it('is able to read a uint64 in big endian.', function() { + var value = bigEndianReader.readUint64(); + expect(value).toBe(0x0001020304050607); + }); + + it('is able to read a uint8 in little endian.', function() { + var value1 = littleEndianReader.readUint8(); + expect(value1).toBe(0x00); + + var value2 = littleEndianReader.readUint8(); + expect(value2).toBe(0x01); + + var value3 = littleEndianReader2.readUint8(); + expect(value3).toBe(0xde); + + var value4 = littleEndianReader2.readUint8(); + expect(value4).toBe(0xad); + }); + + it('is able to read a uint16 in little endian.', function() { + var value1 = littleEndianReader.readUint16(); + expect(value1).toBe(0x0100); + + var value2 = littleEndianReader.readUint16(); + expect(value2).toBe(0x0302); + + var value3 = littleEndianReader2.readUint16(); + expect(value3).toBe(0xadde); + + var value4 = littleEndianReader2.readUint16(); + expect(value4).toBe(0xefbe); + }); + + it('is able to read a uint32 in little endian.', function() { + var value1 = littleEndianReader.readUint32(); + expect(value1).toBe(0x03020100); + + var value2 = littleEndianReader.readUint32(); + expect(value2).toBe(0x07060504); + + var value3 = littleEndianReader2.readUint32(); + expect(value3).toBe(0xefbeadde); + + var value4 = littleEndianReader2.readUint32(); + expect(value4).toBe(0x0001ffff); + }); + + it('is able to read a uint64 in little endian.', function() { + var value = littleEndianReader2.readUint64(); + expect(value).toBe(0x0001ffffefbeadde); + }); + + it('is able to skip bytes.', function() { + bigEndianReader.skip(1); + var value = bigEndianReader.readUint8(); + expect(value).toBe(0x01); + }); + + it('is able to determine the end of the data view.', function() { + bigEndianReader.skip(7); + expect(bigEndianReader.hasMoreData()).toBe(true); + + bigEndianReader.skip(1); + expect(bigEndianReader.hasMoreData()).toBe(false); + }); + + it('is able to get the byte position.', function() { + expect(bigEndianReader.getPosition()).toBe(0); + + bigEndianReader.skip(1); + expect(bigEndianReader.getPosition()).toBe(1); + + bigEndianReader.skip(7); + expect(bigEndianReader.getPosition()).toBe(8); + }); + + it('is able to detect an error when reading a uint8.', function() { + bigEndianReader.skip(7); + bigEndianReader.readUint8(); + + var exception = null; + + try { + bigEndianReader.readUint8(); + } catch (e) { + exception = e; + } + + expect(exception).not.toBeNull(); + expect(exception instanceof RangeError).toBe(true); + }); + + it('is able to detect an error when reading a uint16.', function() { + bigEndianReader.skip(7); + + var exception = null; + + try { + bigEndianReader.readUint16(); + } catch (e) { + exception = e; + } + + expect(exception).not.toBeNull(); + expect(exception instanceof RangeError).toBe(true); + }); + + it('is able to detect an error when reading a uint32.', function() { + bigEndianReader.skip(5); + + var exception = null; + + try { + bigEndianReader.readUint32(); + } catch (e) { + exception = e; + } + + expect(exception).not.toBeNull(); + expect(exception instanceof RangeError).toBe(true); + }); + + it('is able to detect an error when skipping bytes.', function() { + bigEndianReader.skip(8); + + var exception = null; + + try { + bigEndianReader.skip(1); + } catch (e) { + exception = e; + } + + expect(exception).not.toBeNull(); + expect(exception instanceof RangeError).toBe(true); + }); + + it('is able to detect an error when reading a large uint64.', function() { + var exception = null; + + try { + littleEndianReader.readUint64(); + } catch (e) { + exception = e; + } + + expect(exception).not.toBe(null); + expect(exception instanceof RangeError).toBe(true); + + exception = null; + + try { + bigEndianReader2.readUint64(); + } catch (e) { + exception = e; + } + + expect(exception).not.toBe(null); + expect(exception instanceof RangeError).toBe(true); + }); +}); + diff --git a/spec/ebml_parser_spec.js b/spec/ebml_parser_spec.js new file mode 100644 index 0000000000..3e78fae6fd --- /dev/null +++ b/spec/ebml_parser_spec.js @@ -0,0 +1,347 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview ebml_parser.js unit tests. + */ + +goog.require('shaka.util.EbmlParser'); + +describe('EbmlParser', function() { + beforeEach(function() { + jasmine.addMatchers(customMatchers); + }); + + it('is able to parse one element.', function() { + // Set ID to 0x1. + // Set size to 4 bytes. + // Set the data to [0x01, 0x02, 0x03, 0x04]. + var data = new Uint8Array([0x81, 0x84, 0x01, 0x02, 0x03, 0x04]); + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + + var elem = parser.parseElement(); + expect(elem.id).toBe(0x81); + expect(elem.dataView_.byteLength).toBe(4); + expect(elem.dataView_.getUint8(0)).toBe(0x01); + expect(elem.dataView_.getUint8(1)).toBe(0x02); + expect(elem.dataView_.getUint8(2)).toBe(0x03); + expect(elem.dataView_.getUint8(3)).toBe(0x04); + }); + + it('is able to parse two elements at the same level.', function() { + // For the first element: + // Set ID to 0x1. + // Set size to 4 bytes. + // Set the data to [0x01, 0x02, 0x03, 0x04]. + // For the second element: + // Set ID to 0x2. + // Set size to 4 bytes. + // Set the data to [0x09, 0x08, 0x07, 0x06]. + var data = new Uint8Array( + [0x81, 0x84, 0x01, 0x02, 0x03, 0x04, 0x82, 0x84, 0x09, 0x08, 0x07, 0x06]); + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + + var elem1 = parser.parseElement(); + expect(elem1.id).toBe(0x81); + expect(elem1.dataView_.byteLength).toBe(4); + expect(elem1.dataView_.getUint8(0)).toBe(0x01); + expect(elem1.dataView_.getUint8(1)).toBe(0x02); + expect(elem1.dataView_.getUint8(2)).toBe(0x03); + expect(elem1.dataView_.getUint8(3)).toBe(0x04); + + var elem2 = parser.parseElement(); + expect(elem2.id).toBe(0x82); + expect(elem2.dataView_.byteLength).toBe(4); + expect(elem2.dataView_.getUint8(0)).toBe(0x09); + expect(elem2.dataView_.getUint8(1)).toBe(0x08); + expect(elem2.dataView_.getUint8(2)).toBe(0x07); + expect(elem2.dataView_.getUint8(3)).toBe(0x06); + }); + + it('is able to detect a dynamic size value within an element.', function() { + var exception; + + try { + // Set ID to 0x1. + // Set size to a dynamic size value. + var data = new Uint8Array([0x81, 0xff]); + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + parser.parseElement(); + } catch (e) { + exception = e; + } + + expect(exception).not.toBe(undefined); + expect(exception instanceof RangeError).toBe(true); + }); + + it('is able to parse a 1 byte vint.', function() { + // 7-bit value: 1|100 0001 + var data = new Uint8Array([0xc1]); + + // Extract the variable sized integer from |data|. Note that since + // |data| contains exactly one variable sized integer, |vint| should be + // identical to |data|. + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + var vint = parser.parseVint_(); + + expect(shaka.util.EbmlParser.getVintValue_(data)).toBe(0x41); + expect(shaka.util.EbmlParser.getVintValue_(vint)).toBe(0x41); + }); + + it('is able to parse a 2 byte vint.', function() { + // 14-bit: 01|10 0001, 0001 1001 + var data = new Uint8Array([0x61, 0x19]); + + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + var vint = parser.parseVint_(); + + expect(shaka.util.EbmlParser.getVintValue_(data)).toBe(0x2119); + expect(shaka.util.EbmlParser.getVintValue_(vint)).toBe(0x2119); + }); + + it('is able to parse a 3 byte vint.', function() { + // 21-bit: 001|1 0001, 0010 0001, 0001 0011 + var data = new Uint8Array([0x31, 0x21, 0x13]); + + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + var vint = parser.parseVint_(); + + expect(shaka.util.EbmlParser.getVintValue_(data)).toBe(0x112113); + expect(shaka.util.EbmlParser.getVintValue_(vint)).toBe(0x112113); + }); + + it('is able to parse a 4 byte vint.', function() { + // 28-bit: 0001 | 1000, 0001 0001, 0001 0001, 0001 0101 + var data = new Uint8Array([0x18, 0x11, 0x11, 0x15]); + + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + var vint = parser.parseVint_(); + + expect(shaka.util.EbmlParser.getVintValue_(data)).toBe(0x8111115); + expect(shaka.util.EbmlParser.getVintValue_(vint)).toBe(0x8111115); + }); + + it('is able to parse a 5 byte vint.', function() { + // 35-bit: 0000 1|100, 0001 0001, 0001 0001, 0001 0001, 0001 1001 + var data = new Uint8Array([0x0c, 0x11, 0x11, 0x11, 0x19]); + + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + var vint = parser.parseVint_(); + + expect(shaka.util.EbmlParser.getVintValue_(data)).toBe(0x411111119); + expect(shaka.util.EbmlParser.getVintValue_(vint)).toBe(0x411111119); + }); + + it('is able to parse a 6 byte vint.', function() { + // 42-bit: 0000 01|10, 0001 0010, 0001 0001, 0001 0001, 0001 0001, + // 0001 1000 + var data = new Uint8Array([0x06, 0x12, 0x11, 0x11, 0x11, 0x18]); + + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + var vint = parser.parseVint_(); + + expect(shaka.util.EbmlParser.getVintValue_(data)).toBe(0x21211111118); + expect(shaka.util.EbmlParser.getVintValue_(vint)).toBe(0x21211111118); + }); + + it('is able to parse a 7 byte vint.', function() { + // 49-bit: 0000 001|1, 0001 0010, 0001 0001, 0001 0001, 0001 0001, + // 0001 0001, 1001 0001 + var data = new Uint8Array([0x03, 0x12, 0x11, 0x11, 0x11, 0x11, 0x91]); + + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + var vint = parser.parseVint_(); + + expect(shaka.util.EbmlParser.getVintValue_(data)).toBe(0x1121111111191); + expect(shaka.util.EbmlParser.getVintValue_(vint)).toBe(0x1121111111191); + }); + + it('is able to parse a 8 byte vint.', function() { + // 56-bit: 0000 0001 | 0001 0010, 0001 0100, 0001 1000, 0001 0001, + // 0001 0001, 0001 1001, 0011 0001 + var data = new Uint8Array([0x01, 0x12, 0x14, 0x18, 0x11, 0x11, 0x19, 0x31]); + + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + var vint = parser.parseVint_(); + + expect(shaka.util.EbmlParser.getVintValue_(data)).toBe(0x12141811111931); + expect(shaka.util.EbmlParser.getVintValue_(vint)).toBe(0x12141811111931); + }); + + it('is able to detect vints with too many bytes.', function() { + var exception = null; + + try { + // 63-bit: 0000 0000, 1|000 0001, 0001 0001, 0001 0001, 0001 0001, + // 0001 0001, 0001 0001, 0001 0001, + // 0001 0001 + var data = new Uint8Array( + [0x00, 0x81, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11]); + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + parser.parseVint_(); + } catch (e) { + exception = e; + } + + expect(exception).not.toBeNull(); + expect(exception instanceof RangeError).toBe(true); + }); + + it('is able to detect vint values with too many bits.', function() { + var exception = null; + + try { + // 56-bit: 0000 0001 | 1000 0001, 0001 0001, 0001 0001, 0001 0001, + // 0001 0001, 0001 0001, 0001 0001 + var data = new Uint8Array([0x01, 0x81, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11]); + shaka.util.EbmlParser.getVintValue_(data); + } catch (e) { + exception = e; + } + + expect(exception).not.toBeNull(); + expect(exception instanceof RangeError).toBe(true); + + exception = null; + + try { + // 56-bit: 0000 0001 | 0100 0001, 0001 0001, 0001 0001, 0001 0001, + // 0001 0001, 0001 0001, 0001 0001 + var data = new Uint8Array([0x01, 0x41, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11]); + shaka.util.EbmlParser.getVintValue_(data); + } catch (e) { + exception = e; + } + + expect(exception).not.toBeNull(); + expect(exception instanceof RangeError).toBe(true); + + exception = null; + + try { + // 56-bit: 0000 0001 | 0010 0001, 0001 0001, 0001 0001, 0001 0001, + // 0001 0001, 0001 0001, 0001 0001 + var data = new Uint8Array([0x01, 0x21, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11]); + shaka.util.EbmlParser.getVintValue_(data); + } catch (e) { + exception = e; + } + + expect(exception).not.toBeNull(); + expect(exception instanceof RangeError).toBe(true); + }); + + it('is able to detect the end of input while reading a vint.', function() { + // 14-bit: 01|10 0001, 0001 0001 + var data = new Uint8Array([0x61]); + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + + var exception = null; + + try { + var data = new Uint8Array( + [0x00, 0xc1, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11]); + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + parser.parseVint_(); + } catch (e) { + exception = e; + } + + expect(exception).not.toBeNull(); + expect(exception instanceof RangeError).toBe(true); + }); + + it('is able to parse a uint.', function() { + // Set ID to 0x1. + // Set size to 4 bytes. + // Set the data to [0x01, 0x02, 0x03, 0x04]. + var data = new Uint8Array([0x81, 0x84, 0x01, 0x02, 0x03, 0x04]); + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + + var elem = parser.parseElement(); + expect(elem.id).toBe(0x81); + expect(elem.getUint()).toBe(0x01020304); + }); + + it('is able to detect uints with too many bytes.', function() { + // Set ID to 0x1. + // Set size to 9 bytes. + // Set the data to [0x01, 0x02, 0x03, ..., 0x09]. + var data = new Uint8Array( + [0x81, 0x89, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09]); + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + + var elem = parser.parseElement(); + expect(elem.id).toBe(0x81); + + var exception = null; + + try { + elem.getUint(); + } catch (e) { + exception = e; + } + + expect(exception).not.toBeNull(); + expect(exception instanceof RangeError).toBe(true); + }); + + it('is able to detect uints with too many bits.', function() { + // Set ID to 0x1. + // Set size to 8 bytes. + // Set the data to [0x2f, 0xff, 0xff, ..., 0xff]. + var data = new Uint8Array( + [0x81, 0x88, 0x2f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]); + var parser = new shaka.util.EbmlParser(new DataView(data.buffer)); + + var elem = parser.parseElement(); + expect(elem.id).toBe(0x81); + + var exception = null; + + try { + elem.getUint(); + } catch (e) { + exception = e; + } + + expect(exception).not.toBeNull(); + expect(exception instanceof RangeError).toBe(true); + }); + + it('is able to determine a dynamic size value', function() { + var dynamicSizes = [ + new Uint8Array([0xff]), + new Uint8Array([0x7f, 0xff]), + new Uint8Array([0x3f, 0xff, 0xff]), + new Uint8Array([0x1f, 0xff, 0xff, 0xff]), + new Uint8Array([0x0f, 0xff, 0xff, 0xff, 0xff]), + new Uint8Array([0x07, 0xff, 0xff, 0xff, 0xff, 0xff]), + new Uint8Array([0x03, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]), + new Uint8Array([0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]) ]; + + var isDynamicSizeValue_ = shaka.util.EbmlParser.isDynamicSizeValue_; + + expect(isDynamicSizeValue_(dynamicSizes[0])).toBe(true); + expect(isDynamicSizeValue_(dynamicSizes[1])).toBe(true); + expect(isDynamicSizeValue_(dynamicSizes[2])).toBe(true); + expect(isDynamicSizeValue_(dynamicSizes[3])).toBe(true); + expect(isDynamicSizeValue_(dynamicSizes[4])).toBe(true); + expect(isDynamicSizeValue_(dynamicSizes[5])).toBe(true); + expect(isDynamicSizeValue_(dynamicSizes[6])).toBe(true); + expect(isDynamicSizeValue_(dynamicSizes[7])).toBe(true); + }); +}); + diff --git a/spec/event_manager_spec.js b/spec/event_manager_spec.js new file mode 100644 index 0000000000..29110d6b06 --- /dev/null +++ b/spec/event_manager_spec.js @@ -0,0 +1,156 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview event_manager.js unit tests. + */ + +goog.require('shaka.util.EventManager'); + +describe('EventManager', function() { + var eventManager; + + beforeEach(function() { + eventManager = new shaka.util.EventManager(); + }); + + afterEach(function() { + eventManager.destroy(); + }); + + it('can listen for an event.', function() { + var target = new FakeEventTarget(); + var listener = jasmine.createSpy('listener'); + + eventManager.listen(target, 'event', listener); + target.dispatchEvent('event'); + + expect(listener).toHaveBeenCalled(); + }); + + it('can listen for an event from mutiple targets.', function() { + var target1 = new FakeEventTarget(); + var target2 = new FakeEventTarget(); + + var listener1 = jasmine.createSpy('listener1'); + var listener2 = jasmine.createSpy('listener2'); + + eventManager.listen(target1, 'event', listener1); + eventManager.listen(target2, 'event', listener2); + + target1.dispatchEvent('event'); + target2.dispatchEvent('event'); + + expect(listener1).toHaveBeenCalled(); + expect(listener2).toHaveBeenCalled(); + }); + + it('can listen for multiple events.', function() { + var target = new FakeEventTarget(); + + var listener1 = jasmine.createSpy('listener1'); + var listener2 = jasmine.createSpy('listener2'); + + eventManager.listen(target, 'event1', listener1); + eventManager.listen(target, 'event2', listener2); + + target.dispatchEvent('event1'); + target.dispatchEvent('event2'); + + expect(listener1).toHaveBeenCalled(); + expect(listener2).toHaveBeenCalled(); + }); + + it('can listen for multiple events from mutiple targets.', function() { + var target1 = new FakeEventTarget(); + var target2 = new FakeEventTarget(); + + var listener1 = jasmine.createSpy('listener1'); + var listener2 = jasmine.createSpy('listener2'); + + eventManager.listen(target1, 'event1', listener1); + eventManager.listen(target2, 'event2', listener2); + + target1.dispatchEvent('event1'); + target2.dispatchEvent('event2'); + + expect(listener1).toHaveBeenCalled(); + expect(listener2).toHaveBeenCalled(); + }); + + it('can listen for an event with multiple listeners.', function() { + var target = new FakeEventTarget(); + + var listener1 = jasmine.createSpy('listener1'); + var listener2 = jasmine.createSpy('listener2'); + + eventManager.listen(target, 'event', listener1); + eventManager.listen(target, 'event', listener2); + + target.dispatchEvent('event'); + target.dispatchEvent('event'); + + expect(listener1).toHaveBeenCalled(); + expect(listener2).toHaveBeenCalled(); + }); + + it('can stop listening to an event.', function() { + var target = new FakeEventTarget(); + var listener = jasmine.createSpy('listener'); + + eventManager.listen(target, 'event', listener); + eventManager.unlisten(target, 'event'); + + target.dispatchEvent('event'); + + expect(listener).not.toHaveBeenCalled(); + }); + + it('can stop listening to multiple events.', function() { + var target = new FakeEventTarget(); + + var listener1 = jasmine.createSpy('listener1'); + var listener2 = jasmine.createSpy('listener2'); + + eventManager.listen(target, 'event1', listener1); + eventManager.listen(target, 'event2', listener2); + + eventManager.removeAll(target); + + target.dispatchEvent('event1'); + target.dispatchEvent('event2'); + + expect(listener1).not.toHaveBeenCalled(); + expect(listener2).not.toHaveBeenCalled(); + }); + + it('can stop listening for an event with multiple listeners.', function() { + var target = new FakeEventTarget(); + + var listener1 = jasmine.createSpy('listener1'); + var listener2 = jasmine.createSpy('listener2'); + + eventManager.listen(target, 'event', listener1); + eventManager.listen(target, 'event', listener2); + + eventManager.removeAll(target); + + target.dispatchEvent('event'); + target.dispatchEvent('event'); + + expect(listener1).not.toHaveBeenCalled(); + expect(listener2).not.toHaveBeenCalled(); + }); +}); + diff --git a/spec/fake_event_target.js b/spec/fake_event_target.js new file mode 100644 index 0000000000..1c488ee1f7 --- /dev/null +++ b/spec/fake_event_target.js @@ -0,0 +1,87 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements a concrete EventTarget class. + */ + + + +/** + * Creates a new FakeEventTarget. + * @constructor + */ +FakeEventTarget = function() { + /** @private {!Object.>} */ + this.listenerMap_ = {}; +}; + + +/** + * Adds an event listener. + * @param {string} type The event type. + * @param {function(!Object)} listener The event listener. + */ +FakeEventTarget.prototype.addEventListener = function(type, listener) { + var listeners = this.listenerMap_[type]; + if (listeners) { + if (listeners.indexOf(listener) == -1) { + listeners.push(listener); + } + } else { + this.listenerMap_[type] = [listener]; + } +}; + + +/** + * Removes an event listener. + * @param {string} type The event type. + * @param {function(!Object)} listener The event listener. + */ +FakeEventTarget.prototype.removeEventListener = function(type, listener) { + var listeners = this.listenerMap_[type]; + + if (!listeners) { + return; + } + + for (var i = 0; i < listeners.length; i++) { + if (listener == listeners[i]) { + listeners.splice(i, 1); + break; + } + } +}; + + +/** + * Dispatches an event. + * @param {string} type The event type. + * @param {Object=} opt_event Optional event object. + */ +FakeEventTarget.prototype.dispatchEvent = function(type, opt_event) { + var event = opt_event || {}; + + var listeners = this.listenerMap_[type]; + + if (!listeners) { + return; + } + + for (var i = 0; i < listeners.length; i++) { + listeners[i](event); + } +}; + diff --git a/spec/language_utils_spec.js b/spec/language_utils_spec.js new file mode 100644 index 0000000000..4ca079279a --- /dev/null +++ b/spec/language_utils_spec.js @@ -0,0 +1,64 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview language_utils.js unit tests. + */ + +goog.require('shaka.util.LanguageUtils'); + +describe('LanguageUtils', function() { + var LanguageUtils = shaka.util.LanguageUtils; + + describe('match with sublanguage preference', function() { + it('matches exactly at fuzz level 0', function() { + expect(LanguageUtils.match(0, 'en-us', 'en-us')).toBeTruthy(); + expect(LanguageUtils.match(0, 'en-us', 'en-gb')).toBeFalsy(); + expect(LanguageUtils.match(0, 'en-us', 'en')).toBeFalsy(); + }); + + it('accepts base languages at fuzz level 1', function() { + expect(LanguageUtils.match(1, 'en-us', 'en-us')).toBeTruthy(); + expect(LanguageUtils.match(1, 'en-us', 'en-gb')).toBeFalsy(); + expect(LanguageUtils.match(1, 'en-us', 'en')).toBeTruthy(); + }); + + it('accepts all related languages at fuzz level 2', function() { + expect(LanguageUtils.match(2, 'en-us', 'en-us')).toBeTruthy(); + expect(LanguageUtils.match(2, 'en-us', 'en-gb')).toBeTruthy(); + expect(LanguageUtils.match(2, 'en-us', 'en')).toBeTruthy(); + }); + }); + + describe('match with base language preference', function() { + it('matches exactly at fuzz level 0', function() { + expect(LanguageUtils.match(0, 'en', 'en-us')).toBeFalsy(); + expect(LanguageUtils.match(0, 'en', 'en-gb')).toBeFalsy(); + expect(LanguageUtils.match(0, 'en', 'en')).toBeTruthy(); + }); + + it('does not accept anything additional at fuzz level 1', function() { + expect(LanguageUtils.match(1, 'en', 'en-us')).toBeFalsy(); + expect(LanguageUtils.match(1, 'en', 'en-gb')).toBeFalsy(); + expect(LanguageUtils.match(1, 'en', 'en')).toBeTruthy(); + }); + + it('accepts all related languages at fuzz level 2', function() { + expect(LanguageUtils.match(2, 'en', 'en-us')).toBeTruthy(); + expect(LanguageUtils.match(2, 'en', 'en-gb')).toBeTruthy(); + expect(LanguageUtils.match(2, 'en', 'en')).toBeTruthy(); + }); + }); +}); + diff --git a/spec/license_request_spec.js b/spec/license_request_spec.js new file mode 100644 index 0000000000..4326609a99 --- /dev/null +++ b/spec/license_request_spec.js @@ -0,0 +1,165 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview license_request.js unit tests. + */ + +goog.require('shaka.util.LicenseRequest'); + +describe('LicenseRequest', function() { + const SERVER_URL = 'http://localhost/test_drm_url'; + const REQUEST_BODY = 'test_request_body'; + const FAKE_RESPONSE = new Uint8Array(['0', '1', '2', '3']); + var LicenseRequest = shaka.util.LicenseRequest; + + beforeEach(function() { + jasmine.addMatchers(customMatchers); + jasmine.clock().install(); + jasmine.Ajax.install(); + }); + + afterEach(function() { + jasmine.Ajax.uninstall(); + jasmine.clock().uninstall(); + }); + + it('is able to send successfully.', function(done) { + var license_request = new LicenseRequest(SERVER_URL, REQUEST_BODY); + + license_request.send().then(function(response) { + expect(response).toMatchUint8Array(FAKE_RESPONSE); + done(); + }).catch(function(error) { + fail(error); + done(); + }); + + var xhr = jasmine.Ajax.requests.mostRecent(); + mockXMLHttpRequestEventHandling(xhr); + + expect(xhr.url).toBe(SERVER_URL); + expect(xhr.responseType).toBe('arraybuffer'); + expect(xhr.method).toMatch(new RegExp('post', 'i')); + + xhr.fakeResponse({ + 'status': 200, + 'contentType': 'arraybuffer', + 'response': FAKE_RESPONSE.buffer + }); + }); + + it('is able to retry sending.', function(done) { + var license_request = new LicenseRequest(SERVER_URL, REQUEST_BODY); + + license_request.send().then(function(response) { + expect(response).toMatchUint8Array(FAKE_RESPONSE); + done(); + }).catch(function(error) { + fail(error); + done(); + }); + + // Make the first request fail. + var xhr = jasmine.Ajax.requests.mostRecent(); + mockXMLHttpRequestEventHandling(xhr); + + expect(xhr.url).toBe(SERVER_URL); + expect(xhr.responseType).toBe('arraybuffer'); + expect(xhr.method).toMatch(new RegExp('post', 'i')); + + xhr.fakeResponse({'status': 500}); + + jasmine.clock().tick(license_request.lastDelayMs_); + + // Make the second request succeed. + xhr = jasmine.Ajax.requests.mostRecent(); + mockXMLHttpRequestEventHandling(xhr); + + // Ensure it is a new request. + expect(xhr.status).toBe(null); + + expect(xhr.url).toBe(SERVER_URL); + expect(xhr.responseType).toBe('arraybuffer'); + expect(xhr.method).toMatch(new RegExp('post', 'i')); + + xhr.fakeResponse({ + 'status': 200, + 'contentType': 'arraybuffer', + 'response': FAKE_RESPONSE.buffer + }); + }); + + it('is able to handle repeated failures.', function(done) { + var license_request = new LicenseRequest(SERVER_URL, REQUEST_BODY); + + license_request.send().then(function(response) { + fail(new Error('Should not receive a response.')); + done(); + }).catch(function(error) { + expect(error.status).toBe(500); + done(); + }); + + for (var i = 0; i < license_request.parameters.maxAttempts; ++i) { + var xhr = jasmine.Ajax.requests.mostRecent(); + mockXMLHttpRequestEventHandling(xhr); + + // Ensure it is a new request. + expect(xhr.status).toBe(null); + + expect(xhr.url).toBe(SERVER_URL); + expect(xhr.responseType).toBe('arraybuffer'); + expect(xhr.method).toMatch(new RegExp('post', 'i')); + + xhr.fakeResponse({'status': 500}); + jasmine.clock().tick(license_request.lastDelayMs_); + } + }); + + it('parses data URIs with mime type and base64', function(done) { + checkDataUri('data:text/plain;base64,SGVsbG8sIGRhdGEh', + 'Hello, data!', done); + }); + + it('parses data URIs with no mime type and base64', function(done) { + checkDataUri('data:base64,SGVsbG8sIGRhdGEh', + 'Hello, data!', done); + }); + + it('parses data URIs with no mime type and no encoding', function(done) { + checkDataUri('data:Hello%2C%20data!', + 'Hello, data!', done); + }); + + it('parses data URIs with mime type and no encoding', function(done) { + checkDataUri('data:text/plain;Hello%2C%20data!', + 'Hello, data!', done); + }); + + function checkDataUri(uri, expectedData, done) { + var license_request = new LicenseRequest(uri, null); + + license_request.send().then(function(response) { + // Convert the Uint8Array back to string. + var data = String.fromCharCode.apply(null, response); + expect(data).toBe(expectedData); + done(); + }).catch(function(error) { + fail(error); + done(); + }); + } +}); + diff --git a/spec/mpd_processor_spec.js b/spec/mpd_processor_spec.js new file mode 100644 index 0000000000..96e0307fb5 --- /dev/null +++ b/spec/mpd_processor_spec.js @@ -0,0 +1,486 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview mpd_processor.js unit tests. + */ + +goog.require('shaka.dash.mpd'); +goog.require('shaka.dash.MpdProcessor'); + +describe('MpdProcessor', function() { + var parser; + + beforeEach(function() { + parser = new shaka.dash.MpdProcessor(null); + }); + + describe('validateSegmentInfo_()', function() { + var mpd = shaka.dash.mpd; + var m; + var p; + var as; + var r; + var sb; + var sl; + var st; + + beforeEach(function() { + m = new mpd.Mpd(); + p = new mpd.Period(); + as = new mpd.AdaptationSet(); + r = new mpd.Representation(); + sb = new mpd.SegmentBase(); + sl = new mpd.SegmentList(); + st = new mpd.SegmentTemplate(); + }); + + it('can handle a single SegmentBase.', function() { + r.segmentBase = sb; + as.representations.push(r); + p.adaptationSets.push(as); + m.periods.push(p); + + parser.validateSegmentInfo_(m); + expect(r.segmentBase).not.toBeNull(); + expect(r.segmentList).toBeNull(); + expect(r.segmentTemplate).toBeNull(); + }); + + it('can handle a single SegmentList.', function() { + r.segmentList = sl; + as.representations.push(r); + p.adaptationSets.push(as); + m.periods.push(p); + + parser.validateSegmentInfo_(m); + expect(r.segmentBase).toBeNull(); + expect(r.segmentList).not.toBeNull(); + expect(r.segmentTemplate).toBeNull(); + }); + + it('can handle a single SegmentTemplate.', function() { + r.segmentTemplate = st; + as.representations.push(r); + p.adaptationSets.push(as); + m.periods.push(p); + + parser.validateSegmentInfo_(m); + expect(r.segmentBase).toBeNull(); + expect(r.segmentList).toBeNull(); + expect(r.segmentTemplate).not.toBeNull(); + }); + + it('can handle a SegmentBase and a SegmentList.', function() { + r.segmentBase = sb; + r.segmentList = sl; + as.representations.push(r); + p.adaptationSets.push(as); + m.periods.push(p); + + // SegmentList should be removed. + parser.validateSegmentInfo_(m); + expect(r.segmentBase).not.toBeNull(); + expect(r.segmentList).toBeNull(); + expect(r.segmentTemplate).toBeNull(); + }); + + it('can handle a SegmentBase and a SegmentTemplate.', function() { + r.segmentBase = sb; + r.segmentTemplate = st; + as.representations.push(r); + p.adaptationSets.push(as); + m.periods.push(p); + + // SegmentTemplate should be removed. + parser.validateSegmentInfo_(m); + expect(r.segmentBase).not.toBeNull(); + expect(r.segmentList).toBeNull(); + expect(r.segmentTemplate).toBeNull(); + }); + + it('can handle a SegmentList and a SegmentTemplate.', function() { + r.segmentList = sl; + r.segmentTemplate = st; + as.representations.push(r); + p.adaptationSets.push(as); + m.periods.push(p); + + // SegmentTemplate should be removed. + parser.validateSegmentInfo_(m); + expect(r.segmentBase).toBeNull(); + expect(r.segmentList).not.toBeNull(); + expect(r.segmentTemplate).toBeNull(); + }); + + it('can handle a SegmentBase, a SegmentList, and a SegmentTemplate.', + function() { + r.segmentBase = sb; + r.segmentList = sl; + r.segmentTemplate = st; + as.representations.push(r); + p.adaptationSets.push(as); + m.periods.push(p); + + // SegmentList and SegmentTemplate should be removed. + parser.validateSegmentInfo_(m); + expect(r.segmentBase).not.toBeNull(); + expect(r.segmentList).toBeNull(); + expect(r.segmentTemplate).toBeNull(); + }); + + it('can handle no SegmentBase, SegmentList, or SegmentTemplate.', + function() { + as.representations.push(r); + p.adaptationSets.push(as); + m.periods.push(p); + + // The Representation should be removed. + parser.validateSegmentInfo_(m); + expect(as.representations.length).toBe(0); + }); + }); + + describe('processSegmentTemplates_()', function() { + var mpd = shaka.dash.mpd; + var m; + var p; + var as; + var r1; + var r2; + var st; + + beforeEach(function() { + m = new mpd.Mpd(); + p = new mpd.Period(); + as = new mpd.AdaptationSet(); + r1 = new mpd.Representation(); + r2 = new mpd.Representation(); + st = new mpd.SegmentTemplate(); + + r1.segmentTemplate = st; + r2.segmentTemplate = st; + + as.segmentTemplate = st; + as.representations.push(r1); + as.representations.push(r2); + + p.adaptationSets.push(as); + + m.periods.push(p); + }); + + it('can generate a SegmentBase from a SegmentTemplate.', function() { + st.mediaUrlTemplate = 'http://example.com/$Bandwidth$-media.mp4'; + st.indexUrlTemplate = 'http://example.com/$Bandwidth$-index.sidx'; + st.initializationUrlTemplate = 'http://example.com/$Bandwidth$-init.mp4'; + + r1.bandwidth = 250000; + r1.baseUrl = new mpd.BaseUrl(); + r1.baseUrl = new goog.Uri('http://example.com/'); + + r2.bandwidth = 500000; + r2.baseUrl = new mpd.BaseUrl(); + r2.baseUrl = new goog.Uri('http://example.com/'); + + parser.processSegmentTemplates_(m); + + // Check |r1|. + expect(r1.segmentBase).toBeTruthy(); + expect(r1.segmentList).toBeNull(); + + expect(r1.segmentBase.mediaUrl).toBeTruthy(); + expect(r1.segmentBase.mediaUrl.toString()) + .toBe('http://example.com/250000-media.mp4'); + + var ri1 = r1.segmentBase.representationIndex; + expect(ri1).toBeTruthy(); + + expect(ri1.url).toBeTruthy(); + expect(ri1.url.toString()).toBe('http://example.com/250000-index.sidx'); + expect(ri1.range).toBeNull(); + + var i1 = r1.segmentBase.initialization; + expect(i1.url).toBeTruthy(); + expect(i1.url.toString()).toBe('http://example.com/250000-init.mp4'); + expect(i1.range).toBeNull(); + + // Check |r2|. + expect(r2.segmentBase).toBeTruthy(); + expect(r2.segmentList).toBeNull(); + + expect(r2.segmentBase.mediaUrl).toBeTruthy(); + expect(r2.segmentBase.mediaUrl.toString()) + .toBe('http://example.com/500000-media.mp4'); + + var ri2 = r2.segmentBase.representationIndex; + expect(ri2).toBeTruthy(); + + expect(ri2.url).toBeTruthy(); + expect(ri2.url.toString()).toBe('http://example.com/500000-index.sidx'); + expect(ri2.range).toBeNull(); + + var i2 = r2.segmentBase.initialization; + expect(i2.url).toBeTruthy(); + expect(i2.url.toString()).toBe('http://example.com/500000-init.mp4'); + expect(i2.range).toBeNull(); + }); + + it('can generate a SegmentList from a SegmentTemplate.', function() { + var tp1 = new mpd.SegmentTimePoint(); + tp1.duration = 10; + tp1.repeat = 1; + + var tp2 = new mpd.SegmentTimePoint(); + tp2.duration = 20; + tp2.repeat = 0; + + var timeline = new mpd.SegmentTimeline(); + timeline.timePoints.push(tp1); + timeline.timePoints.push(tp2); + + st.timescale = 9000; + st.presentationTimeOffset = 0; + st.segmentDuration = null; + st.firstSegmentNumber = 1; + st.mediaUrlTemplate = '$Number$-$Time$-$Bandwidth$-media.mp4'; + st.initializationUrlTemplate = '$Bandwidth$-init.mp4'; + + st.timeline = timeline; + + r1.bandwidth = 250000; + r1.baseUrl = new mpd.BaseUrl(); + r1.baseUrl = new goog.Uri('http://example.com/'); + + r2.bandwidth = 500000; + r2.baseUrl = new mpd.BaseUrl(); + r2.baseUrl = new goog.Uri('http://example.com/'); + + parser.processSegmentTemplates_(m); + + // Check |r1|. + expect(r1.segmentBase).toBeNull(); + expect(r1.segmentList).toBeTruthy(); + + var sl1 = r1.segmentList; + expect(sl1.timescale).toBe(9000); + expect(sl1.presentationTimeOffset).toBe(0); + expect(sl1.segmentDuration).toBe(null); + expect(sl1.firstSegmentNumber).toBe(1); + + expect(sl1.initialization).toBeTruthy(); + expect(sl1.initialization.url).toBeTruthy(); + expect(sl1.initialization.url.toString()) + .toBe('http://example.com/250000-init.mp4'); + + expect(sl1.segmentUrls.length).toBe(3); + + expect(sl1.segmentUrls[0].mediaUrl).toBeTruthy(); + expect(sl1.segmentUrls[0].mediaUrl.toString()) + .toBe('http://example.com/1-0-250000-media.mp4'); + expect(sl1.segmentUrls[0].mediaRange).toBeNull(); + expect(sl1.segmentUrls[0].startTime).toBe(0); + expect(sl1.segmentUrls[0].duration).toBe(10); + + expect(sl1.segmentUrls[1].mediaUrl).toBeTruthy(); + expect(sl1.segmentUrls[1].mediaUrl.toString()) + .toBe('http://example.com/2-10-250000-media.mp4'); + expect(sl1.segmentUrls[1].mediaRange).toBeNull(); + expect(sl1.segmentUrls[1].startTime).toBe(10); + expect(sl1.segmentUrls[1].duration).toBe(10); + + expect(sl1.segmentUrls[2].mediaUrl).toBeTruthy(); + expect(sl1.segmentUrls[2].mediaUrl.toString()) + .toBe('http://example.com/3-20-250000-media.mp4'); + expect(sl1.segmentUrls[2].mediaRange).toBeNull(); + expect(sl1.segmentUrls[2].startTime).toBe(20); + expect(sl1.segmentUrls[2].duration).toBe(20); + + // Check |r2|. + expect(r2.segmentBase).toBeNull(); + expect(r2.segmentList).toBeTruthy(); + + var sl2 = r2.segmentList; + expect(sl2.timescale).toBe(9000); + expect(sl2.presentationTimeOffset).toBe(0); + expect(sl2.segmentDuration).toBe(null); + expect(sl2.firstSegmentNumber).toBe(1); + + expect(sl2.initialization).toBeTruthy(); + expect(sl2.initialization.url).toBeTruthy(); + expect(sl2.initialization.url.toString()) + .toBe('http://example.com/500000-init.mp4'); + + expect(sl2.segmentUrls.length).toBe(3); + + expect(sl2.segmentUrls[0].mediaUrl).toBeTruthy(); + expect(sl2.segmentUrls[0].mediaUrl.toString()) + .toBe('http://example.com/1-0-500000-media.mp4'); + expect(sl2.segmentUrls[0].mediaRange).toBeNull(); + expect(sl2.segmentUrls[0].startTime).toBe(0); + expect(sl2.segmentUrls[0].duration).toBe(10); + + expect(sl2.segmentUrls[1].mediaUrl).toBeTruthy(); + expect(sl2.segmentUrls[1].mediaUrl.toString()) + .toBe('http://example.com/2-10-500000-media.mp4'); + expect(sl2.segmentUrls[1].mediaRange).toBeNull(); + expect(sl2.segmentUrls[1].startTime).toBe(10); + expect(sl2.segmentUrls[1].duration).toBe(10); + + expect(sl2.segmentUrls[2].mediaUrl).toBeTruthy(); + expect(sl2.segmentUrls[2].mediaUrl.toString()) + .toBe('http://example.com/3-20-500000-media.mp4'); + expect(sl2.segmentUrls[2].mediaRange).toBeNull(); + expect(sl2.segmentUrls[2].startTime).toBe(20); + expect(sl2.segmentUrls[2].duration).toBe(20); + }); + }); + + describe('fillUrlTemplate_()', function() { + it('can handle a single RepresentationID identifier.', function() { + expect( + parser.fillUrlTemplate_( + '/example/$RepresentationID$.mp4', + 100, null, null, null).toString()).toBe('/example/100.mp4') + + // RepresentationID cannot use a width specifier. + expect( + parser.fillUrlTemplate_( + '/example/$RepresentationID%01d$.mp4', + 100, null, null, null).toString()).toBe('/example/100.mp4'); + + expect( + parser.fillUrlTemplate_( + '/example/$RepresentationID$.mp4', + null, null, null, null).toString()) + .toBe('/example/$RepresentationID$.mp4'); + }); + + it('can handle a single Number identifier.', function() { + expect( + parser.fillUrlTemplate_( + '/example/$Number$.mp4', + null, 100, null, null).toString()).toBe('/example/100.mp4') + + expect( + parser.fillUrlTemplate_( + '/example/$Number%05d$.mp4', + null, 100, null, null).toString()).toBe('/example/00100.mp4'); + + expect( + parser.fillUrlTemplate_( + '/example/$Number$.mp4', + null, null, null, null).toString()) + .toBe('/example/$Number$.mp4'); + }); + + it('can handle a single Bandwidth identifier.', function() { + expect( + parser.fillUrlTemplate_( + '/example/$Bandwidth$.mp4', + null, null, 100, null).toString()).toBe('/example/100.mp4') + + expect( + parser.fillUrlTemplate_( + '/example/$Bandwidth%05d$.mp4', + null, null, 100, null).toString()).toBe('/example/00100.mp4'); + + expect( + parser.fillUrlTemplate_( + '/example/$Bandwidth$.mp4', + null, null, null, null).toString()) + .toBe('/example/$Bandwidth$.mp4'); + }); + + it('can handle a single Time identifier.', function() { + expect( + parser.fillUrlTemplate_( + '/example/$Time$.mp4', + null, null, null, 100).toString()).toBe('/example/100.mp4') + + expect( + parser.fillUrlTemplate_( + '/example/$Time%05d$.mp4', + null, null, null, 100).toString()).toBe('/example/00100.mp4'); + + expect( + parser.fillUrlTemplate_( + '/example/$Time$.mp4', + null, null, null, null).toString()) + .toBe('/example/$Time$.mp4'); + }); + + it('can handle multiple identifiers.', function() { + expect( + parser.fillUrlTemplate_( + '/example/$RepresentationID$_$Number$_$Bandwidth$_$Time$.mp4', + 1, 2, 3, 4).toString()).toBe('/example/1_2_3_4.mp4') + + // No spaces. + expect( + parser.fillUrlTemplate_( + '/example/$RepresentationID$$Number$$Bandwidth$$Time$.mp4', + 1, 2, 3, 4).toString()).toBe('/example/1234.mp4') + + // Different order. + expect( + parser.fillUrlTemplate_( + '/example/$Bandwidth$_$Time$_$RepresentationID$_$Number$.mp4', + 1, 2, 3, 4).toString()).toBe('/example/3_4_1_2.mp4') + + // Single width. + expect( + parser.fillUrlTemplate_( + '$RepresentationID$_$Number%01d$_$Bandwidth%01d$_$Time%01d$', + 1, 2, 3, 400).toString()).toBe('1_2_3_400') + + // Different widths. + expect( + parser.fillUrlTemplate_( + '$RepresentationID$_$Number%02d$_$Bandwidth%02d$_$Time%02d$', + 1, 2, 3, 4).toString()).toBe('1_02_03_04') + + // Double $$. + expect( + parser.fillUrlTemplate_( + '$$/$RepresentationID$$$$Number$$$$Bandwidth$$$$Time$$$.$$', + 1, 2, 3, 4).toString()).toBe('$/1$2$3$4$.$') + }); + + it('can handle invalid identifiers.', function() { + expect( + parser.fillUrlTemplate_( + '/example/$Garbage$.mp4', + 1, 2, 3, 4).toString()).toBe('/example/$Garbage$.mp4'); + + expect( + parser.fillUrlTemplate_( + '/example/$RepresentationID%$', + 1, 2, 3, 4)).toBeNull(); + }); + + it('can handle partial identifiers.', function() { + expect( + parser.fillUrlTemplate_( + '/example/$Time.mp4', + 1, 2, 3, 4).toString()).toBe('/example/$Time.mp4'); + + expect( + parser.fillUrlTemplate_( + '/example/$Time%.mp4', + 1, 2, 3, 4)).toBeNull(); + }); + }); +}); + diff --git a/spec/mpd_spec.js b/spec/mpd_spec.js new file mode 100644 index 0000000000..225eb889ed --- /dev/null +++ b/spec/mpd_spec.js @@ -0,0 +1,789 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview mpd.js unit tests. + */ + +goog.require('shaka.dash.mpd'); + +describe('mpd', function() { + beforeEach(function() { + jasmine.addMatchers(customMatchers); + }); + + it('is able to parse an MPD time string.', function() { + var parseDuration = shaka.dash.mpd.parseDuration_; + + // Days only. + expect(parseDuration('P7D')).toBe(604800); + + // Hours only. + expect(parseDuration('PT1H')).toBe(3600); + + // Minutes only. + expect(parseDuration('PT1M')).toBe(60); + + // Seconds only (with no fractional part). + expect(parseDuration('PT1S')).toBe(1); + + // Seconds only (with no whole part). + expect(parseDuration('PT0.1S')).toBe(0.1); + expect(parseDuration('PT.1S')).toBe(0.1); + + // Seconds only (with whole part and fractional part). + expect(parseDuration('PT1.1S')).toBe(1.1); + + // Hours, and minutes. + expect(parseDuration('PT1H2M')).toBe(3720); + + // Hours, and seconds. + expect(parseDuration('PT1H2S')).toBe(3602); + expect(parseDuration('PT1H2.2S')).toBe(3602.2); + + // Minutes, and seconds. + expect(parseDuration('PT1M2S')).toBe(62); + expect(parseDuration('PT1M2.2S')).toBe(62.2); + + // Hours, minutes, and seconds. + expect(parseDuration('PT1H2M3S')).toBe(3723); + expect(parseDuration('PT1H2M3.3S')).toBe(3723.3); + + // Days, hours, minutes, and seconds. + expect(parseDuration('P1DT1H2M3S')).toBe(90123); + expect(parseDuration('P1DT1H2M3.3S')).toBe(90123.3); + + // Error cases. + expect(parseDuration('P1Sasdf')).toBeNull(); + expect(parseDuration('P1Y')).toBeNull(); + expect(parseDuration('P1YT1S')).toBeNull(); + expect(parseDuration('P1M')).toBeNull(); + expect(parseDuration('P1MT1S')).toBeNull(); + expect(parseDuration('P1M1D')).toBeNull(); + expect(parseDuration('P1M1DT1S')).toBeNull(); + expect(parseDuration('1H2M3S')).toBeNull(); + expect(parseDuration('123')).toBeNull(); + expect(parseDuration('abc')).toBeNull(); + expect(parseDuration('')).toBeNull(); + }); + + it('is able to parse an MPD range string.', function() { + var parseRange = shaka.dash.mpd.parseRange_; + var Range = shaka.dash.mpd.Range; + + expect(parseRange('0-0')).toMatchRange(new Range(0, 0)); + expect(parseRange('1-1')).toMatchRange(new Range(1, 1)); + expect(parseRange('1-50')).toMatchRange(new Range(1, 50)); + expect(parseRange('50-1')).toMatchRange(new Range(50, 1)); + + expect(parseRange('-1')).toBeNull(); + expect(parseRange('1-')).toBeNull(); + expect(parseRange('1')).toBeNull(); + expect(parseRange('-')).toBeNull(); + expect(parseRange('')).toBeNull(); + }); + + it('is able to parse basic MPD XML which has a SegmentBase.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' http://example.com', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + + var mpd = shaka.dash.mpd.parseMpd(source, ''); + expect(mpd).toBeTruthy(); + expect(mpd.periods.length).toBe(1); + + var period = mpd.periods[0]; + expect(period).toBeTruthy(); + expect(period.id).toBe('1'); + expect(period.duration).toBe(181.63); + expect(period.start).toBe(0); + expect(period.adaptationSets.length).toBe(1); + + var adaptationSet = period.adaptationSets[0]; + expect(adaptationSet).toBeTruthy(); + expect(adaptationSet.id).toBe('1'); + expect(adaptationSet.lang).toBe('en'); + expect(adaptationSet.contentType).toBe('video'); + expect(adaptationSet.representations.length).toBe(1); + + var representation = adaptationSet.representations[0]; + expect(representation).toBeTruthy(); + expect(representation.id).toBe('r1'); + expect(representation.codecs).toBe('mp4a.40.2'); + + var baseUrl = representation.baseUrl; + expect(baseUrl).toBeTruthy(); + expect(baseUrl.toString()).toBe('http://example.com'); + + var segmentBase = representation.segmentBase; + expect(segmentBase).toBeTruthy(); + expect(segmentBase.indexRange.begin).toBe(1000); + expect(segmentBase.indexRange.end).toBe(3540); + + var initialization = segmentBase.initialization; + expect(initialization).toBeTruthy(); + expect(initialization.range.begin).toBe(0); + expect(initialization.range.end).toBe(999); + }); + + it('is able to parse basic MPD XML which has a SegmentTemplate.', + function() { + var source = [ + '', + ' ', + ' ', + ' http://example.com', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + + var mpd = shaka.dash.mpd.parseMpd(source, ''); + expect(mpd).toBeTruthy(); + expect(mpd.periods.length).toBe(1); + + var period = mpd.periods[0]; + expect(period).toBeTruthy(); + expect(period.id).toBe('1'); + expect(period.duration).toBe(181.63); + expect(period.start).toBe(0); + expect(period.adaptationSets.length).toBe(1); + + var adaptationSet = period.adaptationSets[0]; + expect(adaptationSet).toBeTruthy(); + expect(adaptationSet.id).toBe('1'); + expect(adaptationSet.lang).toBe('en'); + expect(adaptationSet.contentType).toBe('video'); + expect(adaptationSet.representations.length).toBe(2); + + var baseUrl = adaptationSet.baseUrl; + expect(baseUrl).toBeTruthy(); + expect(baseUrl.toString()).toBe('http://example.com'); + + var segmentTemplate = adaptationSet.segmentTemplate; + expect(segmentTemplate).toBeTruthy(); + expect(segmentTemplate.timescale).toBe(9000); + expect(segmentTemplate.initializationUrlTemplate).toBe('$Bandwidth$/init.mp4'); + expect(segmentTemplate.mediaUrlTemplate).toBe('$Bandwidth$/frames.mp4'); + + var timeline = segmentTemplate.timeline; + expect(timeline).toBeTruthy(); + + var timePoints = timeline.timePoints; + expect(timePoints).toBeTruthy(); + expect(timePoints.length).toBe(1); + + var tp = timePoints[0]; + expect(tp.startTime).toBe(0); + expect(tp.duration).toBe(1000); + expect(tp.repeat).toBe(400); + + var representation = adaptationSet.representations[0]; + expect(representation).toBeTruthy(); + expect(representation.id).toBe('r1'); + expect(representation.bandwidth).toBe(250000); + expect(representation.codecs).toBe('mp4a.40.2'); + + representation = adaptationSet.representations[1]; + expect(representation).toBeTruthy(); + expect(representation.id).toBe('r2'); + expect(representation.bandwidth).toBe(500000); + expect(representation.codecs).toBe('mp4a.40.2'); + }); + + it('can inherit ContentComponent attributes in AdaptationSet.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + + var mpd = shaka.dash.mpd.parseMpd(source, ''); + expect(mpd).toBeTruthy(); + expect(mpd.periods.length).toBe(1); + + var period = mpd.periods[0]; + expect(period).toBeTruthy(); + expect(period.adaptationSets.length).toBe(1); + + var adaptationSet = period.adaptationSets[0]; + expect(adaptationSet).toBeTruthy(); + expect(adaptationSet.lang).toBe('en'); + expect(adaptationSet.contentType).toBe('video'); + }); + + it('can override ContentComponent attributes in AdaptationSet.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + + var mpd = shaka.dash.mpd.parseMpd(source, ''); + expect(mpd).toBeTruthy(); + expect(mpd.periods.length).toBe(1); + + var period = mpd.periods[0]; + expect(period).toBeTruthy(); + expect(period.adaptationSets.length).toBe(1); + + var adaptationSet = period.adaptationSets[0]; + expect(adaptationSet).toBeTruthy(); + expect(adaptationSet.lang).toBe('fr'); + expect(adaptationSet.contentType).toBe('audio'); + }); + + it('can inherit a SegmentBase from a Period.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkSegmentBase(source, 100, 200); + }); + + it('can inherit a SegmentBase from an AdaptationSet.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkSegmentBase(source, 100, 200); + + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkSegmentBase(source, 1000, 2000); + }); + + it('can override a SegmentBase from a Period.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkSegmentBase(source, 1000, 2000); + }); + + it('can override a SegmentBase from an AdaptationSet.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkSegmentBase(source, 1000, 2000); + + source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkSegmentBase(source, 10000, 20000); + }); + + /** + * Checks that a SegmentBase exists with the given index range. + * @param {string} source The XML source. + * @param {number} indexBegin + * @param {number} indexEnd + */ + var checkSegmentBase = function(source, indexBegin, indexEnd) { + var mpd = shaka.dash.mpd.parseMpd(source, ''); + expect(mpd).toBeTruthy(); + expect(mpd.periods.length).toBe(1); + + var period = mpd.periods[0]; + expect(period).toBeTruthy(); + expect(period.adaptationSets.length).toBe(1); + + var adaptationSet = period.adaptationSets[0]; + expect(adaptationSet).toBeTruthy(); + expect(adaptationSet.representations.length).toBe(1); + + var representation = adaptationSet.representations[0]; + expect(representation).toBeTruthy(); + + var segmentBase = representation.segmentBase; + expect(segmentBase).toBeTruthy(); + expect(segmentBase.indexRange.begin).toBe(indexBegin); + expect(segmentBase.indexRange.end).toBe(indexEnd); + }; + + it('can inherit a ContentProtection from an AdaptationSet.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkContentProtection(source, 'http://example.com'); + + source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkContentProtection(source, 'http://google.com'); + }); + + it('can override a ContentProtection from a Period.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkContentProtection(source, 'http://google.com'); + }); + + it('can override a ContentProtection from an AdaptationSet.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkContentProtection(source, 'http://google.com'); + + source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkContentProtection(source, 'http://youtube.com'); + }); + + /** + * Checks that a ContentProtection exists with the given schemeIdUri. + * @param {string} source The XML source. + * @param {string} schemeIdUri + */ + var checkContentProtection = function(source, schemeIdUri) { + var mpd = shaka.dash.mpd.parseMpd(source, ''); + expect(mpd).toBeTruthy(); + expect(mpd.periods.length).toBe(1); + + var period = mpd.periods[0]; + expect(period).toBeTruthy(); + expect(period.adaptationSets.length).toBe(1); + + var adaptationSet = period.adaptationSets[0]; + expect(adaptationSet).toBeTruthy(); + expect(adaptationSet.representations.length).toBe(1); + + var representation = adaptationSet.representations[0]; + expect(representation).toBeTruthy(); + expect(representation.contentProtections.length).toBeTruthy(); + + var foundMatch = false; + for (var i = 0; i < representation.contentProtections.length; ++i) { + var contentProtection = representation.contentProtections[i]; + expect(contentProtection).toBeTruthy(); + if (contentProtection.schemeIdUri == schemeIdUri) { + foundMatch = true; + } + } + expect(foundMatch).toBeTruthy(); + }; + + it('can inherit a "mimeType" attribute from an AdaptationSet.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkRepresentationMimeType(source, 'video/mp4'); + }); + + it('can override a "mimeType" attribute from an AdaptationSet.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkRepresentationMimeType(source, 'video/webm'); + }); + + /** + * @param {string} source The XML source. + * @param {string} mimeType + */ + var checkRepresentationMimeType = function(source, mimeType) { + var mpd = shaka.dash.mpd.parseMpd(source, ''); + expect(mpd).toBeTruthy(); + expect(mpd.periods.length).toBe(1); + + var period = mpd.periods[0]; + expect(period).toBeTruthy(); + expect(period.adaptationSets.length).toBe(1); + + var adaptationSet = period.adaptationSets[0]; + expect(adaptationSet).toBeTruthy(); + expect(adaptationSet.representations.length).toBe(1); + + var representation = adaptationSet.representations[0]; + expect(representation).toBeTruthy(); + expect(representation.mimeType).toBe(mimeType); + }; + + it('can infer a "contentType" attribute from "mimeType".', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + + var mpd = shaka.dash.mpd.parseMpd(source, ''); + expect(mpd).toBeTruthy(); + expect(mpd.periods.length).toBe(1); + + var period = mpd.periods[0]; + expect(period).toBeTruthy(); + expect(period.adaptationSets.length).toBe(1); + + var adaptationSet = period.adaptationSets[0]; + expect(adaptationSet).toBeTruthy(); + expect(adaptationSet.contentType).toBe('video'); + }); + + it('can infer a "mimeType" attribute from Representations.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + + var mpd = shaka.dash.mpd.parseMpd(source, ''); + expect(mpd).toBeTruthy(); + expect(mpd.periods.length).toBe(1); + + var period = mpd.periods[0]; + expect(period).toBeTruthy(); + expect(period.adaptationSets.length).toBe(1); + + var adaptationSet = period.adaptationSets[0]; + expect(adaptationSet).toBeTruthy(); + expect(adaptationSet.mimeType).toBe('video/mp4'); + expect(adaptationSet.contentType).toBe('video'); + }); + + it('can inherit a "codecs" attribute from an AdaptationSet.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkCodecs(source, 'vp8'); + }); + + it('can override a "codecs" attribute from an AdaptationSet.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + checkCodecs(source, 'vp9'); + }); + + /** + * @param {string} source The XML source. + * @param {string} codecs + */ + var checkCodecs = function(source, codecs) { + var mpd = shaka.dash.mpd.parseMpd(source, ''); + expect(mpd).toBeTruthy(); + expect(mpd.periods.length).toBe(1); + + var period = mpd.periods[0]; + expect(period).toBeTruthy(); + expect(period.adaptationSets.length).toBe(1); + + var adaptationSet = period.adaptationSets[0]; + expect(adaptationSet).toBeTruthy(); + expect(adaptationSet.representations.length).toBe(1); + + var representation = adaptationSet.representations[0]; + expect(representation).toBeTruthy(); + expect(representation.codecs).toBe(codecs); + }; + + it('can resolve relative and absolute URLs at every level.', function() { + var source = [ + '', + ' http://example.com/', + ' ', + ' Period1/', + ' ', + ' AdaptationSet1/', + ' ', + ' Representation1', + ' ', + ' ', + ' ', + ' ', + ' Period2', + ' ', + ' AdaptationSet2', + ' ', + ' Representation2', + ' ', + ' ', + ' ', + ' ', + ' /Period3/', + ' ', + ' /AdaptationSet3', + ' ', + ' ?Representation3', + ' ', + ' ', + ' #Representation4', + ' ', + ' ', + ' http://foo.bar/', + ' ', + ' ', + ' ', + ' http://foo.bar/multi/level', + ' ', + ' ?Representation5', + ' ', + ' ', + ' ', + ''].join('\n'); + + var mpd = shaka.dash.mpd.parseMpd(source, ''); + expect(mpd).toBeTruthy(); + expect(mpd.baseUrl.toString()).toBe('http://example.com/'); + expect(mpd.periods.length).toBe(3); + + var p = mpd.periods; + expect(p[0].baseUrl.toString()). + toBe('http://example.com/Period1/'); + expect(p[0].adaptationSets[0].baseUrl.toString()). + toBe('http://example.com/Period1/AdaptationSet1/'); + expect(p[0].adaptationSets[0].representations[0].baseUrl.toString()). + toBe('http://example.com/Period1/AdaptationSet1/Representation1'); + + expect(p[1].baseUrl.toString()). + toBe('http://example.com/Period2'); + expect(p[1].adaptationSets[0].baseUrl.toString()). + toBe('http://example.com/AdaptationSet2'); + expect(p[1].adaptationSets[0].representations[0].baseUrl.toString()). + toBe('http://example.com/Representation2'); + + expect(p[2].baseUrl.toString()). + toBe('http://example.com/Period3/'); + expect(p[2].adaptationSets[0].baseUrl.toString()). + toBe('http://example.com/AdaptationSet3'); + expect(p[2].adaptationSets[0].representations[0].baseUrl.toString()). + toBe('http://example.com/AdaptationSet3?Representation3'); + expect(p[2].adaptationSets[0].representations[1].baseUrl.toString()). + toBe('http://example.com/AdaptationSet3#Representation4'); + expect(p[2].adaptationSets[0].representations[2].baseUrl.toString()). + toBe('http://foo.bar/'); + + expect(p[2].adaptationSets[1].baseUrl.toString()). + toBe('http://foo.bar/multi/level'); + expect(p[2].adaptationSets[1].representations[0].baseUrl.toString()). + toBe('http://foo.bar/multi/level?Representation5'); + }); + + it('can resolve relative URLs across levels.', function() { + var source = [ + '', + ' sub/', + ' ', + ' ', + ' ', + ' 1.webm', + ' ', + ' ', + ' 2.webm', + ' ', + ' ', + ' ', + ''].join('\n'); + + var mpd = shaka.dash.mpd.parseMpd(source, ''); + expect(mpd).toBeTruthy(); + expect(mpd.baseUrl.toString()).toBe('sub/'); + expect(mpd.periods.length).toBe(1); + + var p = mpd.periods[0]; + expect(p.baseUrl.toString()).toBe('sub/'); + expect(p.adaptationSets.length).toBe(1); + + var as = p.adaptationSets[0]; + expect(as.baseUrl.toString()).toBe('sub/'); + expect(as.representations.length).toBe(2); + + var r = as.representations; + expect(r[0].baseUrl.toString()).toBe('sub/1.webm'); + expect(r[1].baseUrl.toString()).toBe('sub/2.webm'); + }); + + it('can resolve relative URLs with respect to the MPD URL.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' 1.webm', + ' ', + ' ', + ' ', + ''].join('\n'); + var mpdUrl = 'http://example.com/dash/test.mpd'; + + var mpd = shaka.dash.mpd.parseMpd(source, mpdUrl); + expect(mpd).toBeTruthy(); + expect(mpd.baseUrl.toString()).toBe(mpdUrl); + expect(mpd.periods.length).toBe(1); + + var p = mpd.periods[0]; + expect(p.baseUrl.toString()).toBe(mpdUrl); + expect(p.adaptationSets.length).toBe(1); + + var as = p.adaptationSets[0]; + expect(as.baseUrl.toString()).toBe(mpdUrl); + expect(as.representations.length).toBe(1); + + var r = as.representations[0]; + expect(r.baseUrl.toString()).toBe('http://example.com/dash/1.webm'); + }); + + it('can parse namespaced elements.', function() { + var source = [ + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ''].join('\n'); + + var mpd = shaka.dash.mpd.parseMpd(source, ''); + expect(mpd).toBeTruthy(); + }); +}); + diff --git a/spec/player_integration.js b/spec/player_integration.js new file mode 100644 index 0000000000..d34e1fe942 --- /dev/null +++ b/spec/player_integration.js @@ -0,0 +1,345 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Player integration tests. + */ + +goog.require('shaka.player.Player'); +goog.require('shaka.player.DashVideoSource'); +goog.require('shaka.polyfill.MediaKeys'); +goog.require('shaka.polyfill.VideoPlaybackQuality'); + +describe('Player', function() { + var originalAsserts; + var originalTimeout; + var video; + var player; + + function newSource(encrypted) { + var url = encrypted ? + 'assets/car_cenc-20120827-manifest.mpd' : + 'assets/car-20120827-manifest.mpd'; + return new shaka.player.DashVideoSource(url, interpretContentProtection); + } + + beforeAll(function() { + // Hijack assertions and convert failed assertions into failed tests. + assertsToFailures.install(); + + // Change the timeout. + originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; // ms + + // Install polyfills. + shaka.polyfill.MediaKeys.install(); + shaka.polyfill.VideoPlaybackQuality.install(); + + // Create a video tag. This will be visible so that long tests do not + // create the illusion of the test-runner being hung. + video = document.createElement('video'); + video.width = 600; + video.height = 400; + // Add it to the DOM. + document.body.appendChild(video); + }); + + beforeEach(function() { + // Create a new player. + player = new shaka.player.Player(video); + player.addEventListener('error', function(event) { + // Treat all player errors as test failures. + var error = event.detail; + fail(error); + }, false); + + // Create a new source. + source = newSource(false); + + // Disable automatic adaptation unless it is needed for a test. + // This makes test results more reproducible. + player.enableAdaptation(false); + }); + + afterEach(function() { + player.destroy(); + player = null; + }); + + afterAll(function() { + // Remove the video tag from the DOM. + document.body.removeChild(video); + + // Restore the timeout. + jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + + // Restore normal assertion behavior. + assertsToFailures.uninstall(); + }); + + // Returns the Id of the track for the intended track height. + // -1 if the target height is not found. + function getTrackIdForTargetHeight(tracks, targetHeight) { + for (var i = 0; i < tracks.length; i++) { + if (tracks[i].height == targetHeight) { + return tracks[i].id; + } + } + + return -1; + }; + + describe('load', function() { + // This covers basic player re-use. + it('can be used multiple times without EME', function(done) { + player.load(source).then(function() { + player.play(); + return delay(5.0); + }).then(function() { + source = newSource(false); + return player.load(source); + }).then(function() { + player.play(); + return delay(5.0); + }).then(function() { + expect(video.currentTime).toBeGreaterThan(0.0); + done(); + }).catch(function(error) { + fail(error); + done(); + }); + }); + + // This covers bug #18614098. A presumed bug in Chrome can cause mediaKeys + // to be unset on the second use of a video tag. + it('can be used multiple times with EME', function(done) { + source = newSource(true); + player.load(source).then(function() { + player.play(); + return delay(5.0); + }).then(function() { + source = newSource(true); + return player.load(source); + }).then(function() { + player.play(); + return delay(5.0); + }).then(function() { + expect(video.currentTime).toBeGreaterThan(0.0); + done(); + }).catch(function(error) { + fail(error); + done(); + }); + }); + }); + + describe('resize', function() { + // Tests video resizing at the time of initializing player. + it('can set resolution at time of initialization', function(done) { + player.load(source).then(function() { + var tracks = player.getVideoTracks(); + var trackId = getTrackIdForTargetHeight(tracks, 720); + player.selectVideoTrack(trackId); + player.play(); + return delay(10.0); + }).then(function() { + var currentResolution = player.getCurrentResolution(); + expect(currentResolution).not.toBe(null); + expect(currentResolution.height).toEqual(720); + expect(currentResolution.width).toEqual(1280); + done(); + }).catch(function(error) { + fail(error); + done(); + }); + }); + + // Tests player at different resolutions. + it('can be resized multiple times', function(done) { + player.load(source).then(function() { + var tracks = player.getVideoTracks(); + var trackId = getTrackIdForTargetHeight(tracks, 720); + player.selectVideoTrack(trackId); + player.play(); + return delay(10.0); + }).then(function() { + var currentResolution = player.getCurrentResolution(); + expect(currentResolution).not.toBe(null); + expect(currentResolution.height).toEqual(720); + expect(currentResolution.width).toEqual(1280); + return delay(2.0); + }).then(function() { + var tracks = player.getVideoTracks(); + var trackId = getTrackIdForTargetHeight(tracks, 360); + player.selectVideoTrack(trackId); + return delay(10.0); + }).then(function() { + var currentResolution = player.getCurrentResolution(); + expect(currentResolution).not.toBe(null); + expect(currentResolution.height).toEqual(360); + expect(currentResolution.width).toEqual(640); + done(); + }).catch(function(error) { + fail(error); + done(); + }); + }); + }); + + describe('player-controls', function() { + // Tests various player controls. + it('test play and pause video controls', function(done) { + var timeStamp; + player.load(source).then(function() { + player.play(); + return delay(8.0); + }).then(function() { + expect(video.currentTime).toBeGreaterThan(3.0); + player.pause(); + timeStamp = player.getCurrentTime(); + return delay(5.0); + }).then(function() { + expect(video.paused).toBe(true); + expect(video.currentTime).toEqual(timeStamp); + expect(video.currentTime).toEqual(player.getCurrentTime()); + timeStamp = player.getCurrentTime(); + player.play(); + return delay(5.0); + }).then(function() { + expect(video.paused).toBe(false); + expect(video.currentTime).toBeGreaterThan(timeStamp); + done(); + }).catch(function(error) { + fail(error); + done(); + }); + }); + + it('test volume control', function(done) { + var volume; + player.load(source).then(function() { + player.play(); + volume = player.getVolume(); + player.setVolume(0); + expect(player.getVolume()).toEqual(0); + player.setVolume(0.5); + expect(player.getVolume()).toEqual(0.5); + expect(video.volume).toEqual(0.5); + player.setMuted(true); + expect(player.getMuted()).toBe(true); + expect(video.muted).toBe(true); + player.setMuted(false); + expect(player.getMuted()).toBe(false); + expect(video.muted).toBe(false); + expect(player.getVolume()).toEqual(0.5); + expect(video.volume).toEqual(0.5); + done(); + }).catch(function(error) { + fail(error); + done(); + }); + }); + }); + + describe('seek', function() { + // This covers bug #18597152. Completely clearing the buffers after a seek + // can cause the media pipeline in Chrome to get stuck. This seemed to + // happen when certain seek intervals were used. + it('does not lock up on segment boundaries', function(done) { + player.load(source).then(function() { + player.play(); + return delay(1.0); // gets the player out of INIT state + }).then(function() { + player.seek(40.0); // <0.1s before end of segment N (5). + return delay(2.0); + }).then(function() { + player.seek(30.0); // <0.1s before end of segment N-2 (3). + return delay(5.0); + }).then(function() { + // Typically this bug manifests with seeking == true. + expect(video.seeking).toBe(false); + // Typically this bug manifests with readyState == HAVE_METADATA. + expect(video.readyState).not.toBe(HTMLVideoElement.HAVE_METADATA); + expect(video.readyState).not.toBe(HTMLVideoElement.HAVE_NOTHING); + // We can't expect to get all the way to 35.0 unless the seek is + // instantaneous. We use 32.0 because it leaves plenty of wiggle room + // for various delays (including network delay), and because in this + // particular bug, the video gets stuck at exactly the seek time (30). + expect(video.currentTime).toBeGreaterThan(32.0); + done(); + }).catch(function(error) { + fail(error); + done(); + }); + }); + + // This covers bug #18597156. Seeking around without removing any data + // from the buffers can cause the media pipeline in Chrome to manifest gaps + // in the buffered data ranges. Such a gap will move forward as data is + // replaced in buffer, but the gap will never close until the entire range + // has been replaced. It is therefore SourceBufferManager's job to work + // around this peculiar behavior from Chrome's SourceBuffer. If this is + // not done, playback gets "stuck" when the playhead enters such a gap. + it('does not create unclosable gaps in the buffer', function(done) { + player.load(source).then(function() { + player.play(); + return delay(1.0); + }).then(function() { + player.seek(33.0); + return delay(2.0); + }).then(function() { + player.seek(28.0); + return delay(10.0); + }).then(function() { + // We don't expect 38.0 because of the uncertainty of network and other + // delays. This is a safe number which will not cause false failures. + // When this bug manifests, the playhead typically gets stuck around + // 32.9, so we expect that 35.0 is a safe indication that the bug is + // not manifesting. + expect(video.currentTime).toBeGreaterThan(35.0); + done(); + }).catch(function(error) { + fail(error); + done(); + }); + }); + + it('can be used during stream switching', function(done) { + var videoStream; + var DashStream = shaka.dash.DashStream; + + player.load(source).then(function() { + player.play(); + return delay(2.0); + }).then(function() { + videoStream = source.streamsByType_['video']; + expect(videoStream.state_).toBe(DashStream.State_.UPDATING); + + var ok = player.selectVideoTrack(3); // 480p stream + expect(ok).toBe(true); + expect(videoStream.state_).toBe(DashStream.State_.SWITCHING); + + player.seek(30.0); + return delay(10.0); + }).then(function() { + expect(video.currentTime).toBeGreaterThan(35.0); + done(); + }).catch(function(error) { + fail(error); + done(); + }); + }); + }); +}); + diff --git a/spec/segment_index_spec.js b/spec/segment_index_spec.js new file mode 100644 index 0000000000..b738af9b94 --- /dev/null +++ b/spec/segment_index_spec.js @@ -0,0 +1,275 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview segment_index.js unit tests. + */ + +goog.require('shaka.dash.IsobmffSegmentIndexParser'); +goog.require('shaka.dash.SegmentIndex'); +goog.require('shaka.dash.SegmentReference'); +goog.require('shaka.dash.WebmSegmentIndexParser'); +goog.require('shaka.util.PublicPromise'); + +// TODO: Move IsobmffSegmentIndexParser and WebmSegmentIndexParser tests into +// their own respective files. +describe('SegmentIndex', function() { + var sidxData; + var sidxDataWithNonZeroStart; + var webmData; + var cuesData; + + var fetchArrayBuffer = function(url) { + var p = new shaka.util.PublicPromise(); + var xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.responseType = 'arraybuffer'; + xhr.onload = function(event) { p.resolve(xhr.response); }; + xhr.send(null); + return p; + }; + + var getSidxData = function() { + // Get the SIDX data if we haven't yet. + // This file contains just the SIDX, which was extracted from an actual MP4 + // file. + if (sidxData) return Promise.resolve(); + return fetchArrayBuffer('assets/car-20120827-87.sidx').then( + function(data) { sidxData = data; }); + }; + + var getSidxDataWithNonZeroStart = function() { + // Get the SIDX data with non-zero start if we haven't yet. + // This file contains just the SIDX, which was extracted from an actual MP4 + // file. It differs from the above SIDX in that it has a non-zero "earliest + // presentation time" field. + if (sidxDataWithNonZeroStart) return Promise.resolve(); + return fetchArrayBuffer('assets/angel_one.sidx').then( + function(data) { sidxDataWithNonZeroStart = data; }); + }; + + var getWebmData = function() { + // Get the WebM header data if we haven't yet. + if (webmData) return Promise.resolve(); + return fetchArrayBuffer('assets/feelings_vp9-20130806-171.webm.headers').then( + function(data) { webmData = data; }); + }; + + var getCuesData = function() { + // Get the WebM cues data if we haven't yet. + if (cuesData) return Promise.resolve(); + return fetchArrayBuffer('assets/feelings_vp9-20130806-171.webm.cues').then( + function(data) { cuesData = data; }); + }; + + beforeEach(function(done) { + var async = [ + getSidxData(), + getSidxDataWithNonZeroStart(), + getWebmData(), + getCuesData() + ]; + + Promise.all(async).then(function() { + done(); + }); + }); + + it('is able to parse an ISO BMFF segment index.', function() { + // The SIDX data was obtained from an MP4 file where the SIDX offset was + // 708. We use this value here since the expected offsets for parsing this + // SIDX are known. + var parser = new shaka.dash.IsobmffSegmentIndexParser(); + var references = parser.parse(null, new DataView(sidxData), 708); + expect(references).not.toBeNull(); + + // These values are rounded. + var expectedStartTimes = [ + 0.000, 5.005, 10.010, 15.015, 20.020, 25.025, 30.030, + 35.035, 40.040, 45.045, 50.050, 55.055, 60.060, 65.065, + 70.070, 75.075, 80.080, 85.085, 90.090, 95.095, 100.100, + 105.105, 110.110, 115.115, 120.120, 125.125, 130.130, 135.135, + 140.140, 145.145, 150.150, 155.155, 160.160, 165.165, 170.170, + 175.175, 180.180 ]; + + var expectedStartBytes = [ + 1184, 727647, 1450907, 2164185, 2605829, 3151190, 3854669, + 4574758, 5224472, 5931518, 6320466, 6801107, 7307570, 7697596, + 8336571, 8820074, 9268630, 9706572, 10137561, 10676341, 11384276, + 12089373, 12708006, 13111442, 13805201, 14361322, 14996946, 15676293, + 16273342, 16812658, 17465320, 18038404, 18634288, 18855907, 19386647, + 19580549, 19700059 ]; + + checkReferences(references, 0, expectedStartTimes, expectedStartBytes); + }); + + it('correctly handles a non-zero start time.', function() { + // The SIDX data was obtained from an MP4 file where the SIDX offset was + // 1322. We use this value here since the expected offsets for parsing this + // SIDX are known. + var parser = new shaka.dash.IsobmffSegmentIndexParser(); + var references = + parser.parse(null, new DataView(sidxDataWithNonZeroStart), 1322); + expect(references).not.toBeNull(); + + // These values are rounded. + var expectedStartTimes = [ + 0.040, 3.040, 6.040, 9.040, 11.780, 14.780, 17.460, + 20.460, 23.460, 26.460, 29.460, 32.460, 35.460, 38.460, + 41.460, 44.460, 47.460, 50.460, 52.860, 55.860 ]; + + var expectedStartBytes = [ + 1594, 1175673, 1417937, 1665835, 1973789, 2294769, 2490199, + 2671008, 2954930, 3371950, 3778589, 4073258, 4527374, 5033136, + 5532306, 5788871, 6025088, 6313961, 6642589, 6993868 ]; + + checkReferences(references, 0, expectedStartTimes, expectedStartBytes); + }); + + it('is able to parse a WebM segment index.', function() { + var parser = new shaka.dash.WebmSegmentIndexParser(); + var references = + parser.parse(new DataView(webmData), new DataView(cuesData), 0); + expect(references).not.toBeNull(); + + // These values are rounded. + var expectedStartTimes = [ + 0.000, 10.012, 20.026, 30.048, 40.067, 50.081, 60.100, + 70.111, 80.116, 90.133, 100.136, 110.137, 120.156, 130.159 ]; + + var expectedStartBytes = [ + 4687, 144903, 297659, 459957, 618640, 773028, 924089, + 1069119, 1226240, 1387394, 1545708, 1699983, 1859342, 2009816 ]; + + checkReferences(references, 0, expectedStartTimes, expectedStartBytes); + }); + + describe('getRangeForInterval()', function() { + var index; + + beforeEach(function() { + var parser = new shaka.dash.WebmSegmentIndexParser(); + var references = + parser.parse(new DataView(webmData), new DataView(cuesData), 0); + expect(references).not.toBeNull(); + + index = new shaka.dash.SegmentIndex(references); + }); + + it('can handle a regular interval.', function() { + var range = index.getRangeForInterval(31, 40); + + // These values are rounded. + var expectedStartTimes = [30.048, 40.067, 50.081, 60.100, 70.111]; + var expectedStartBytes = [459957, 618640, 773028, 924089, 1069119]; + + checkReferences( + range.references, 3, expectedStartTimes, expectedStartBytes); + }); + + it('can handle an interval starting at the first segment.', function() { + var range = index.getRangeForInterval(0, 40); + + // These values are rounded. + var expectedStartTimes = [0.000, 10.012, 20.026, 30.048]; + var expectedStartBytes = [4687, 144903, 297659, 459957]; + + checkReferences( + range.references, 0, expectedStartTimes, expectedStartBytes); + }); + + it('can handle an interval starting before the first segment', function() { + var range = index.getRangeForInterval(-10, 21); + + // These values are rounded. + var expectedStartTimes = [0.000, 10.012]; + var expectedStartBytes = [4687, 144903]; + + checkReferences( + range.references, 0, expectedStartTimes, expectedStartBytes); + }); + + it('can handle an interval starting at the last segment.', function() { + var range = index.getRangeForInterval(130.159, 10); + + // These values are rounded. + var expectedStartTimes = [130.159]; + var expectedStartBytes = [2009816]; + + checkReferences( + range.references, 13, expectedStartTimes, expectedStartBytes); + }); + + it('can handle an interval starting after the last segment.', function() { + var range = index.getRangeForInterval(150, 10); + + // These values are rounded. + var expectedStartTimes = [130.159]; + var expectedStartBytes = [2009816]; + + checkReferences( + range.references, 13, expectedStartTimes, expectedStartBytes); + }); + + it('can handle an interval ending with a null time.', function() { + var url = new goog.Uri('http://example.com'); + + var references = [ + new shaka.dash.SegmentReference(0, 0, 1, 0, 5, url), + new shaka.dash.SegmentReference(1, 1, 2, 6, 9, url), + new shaka.dash.SegmentReference(2, 2, null, 10, null, url) ]; + + var index2 = new shaka.dash.SegmentIndex(references); + var range = index2.getRangeForInterval(0, 2); + + var expectedStartTimes = [0, 1, 2]; + var expectedStartBytes = [0, 6, 10]; + + checkReferences( + range.references, 0, expectedStartTimes, expectedStartBytes); + + range = index2.getRangeForInterval(0, 3); + + expectedStartTimes = [0, 1, 2]; + expectedStartBytes = [0, 6, 10]; + + checkReferences( + range.references, 0, expectedStartTimes, expectedStartBytes); + }); + + it('can handle no segments.', function() { + index = new shaka.dash.SegmentIndex([]); + var range = index.getRangeForInterval(31, 40); + expect(range).toBeNull(); + }); + }); + + checkReferences = function( + references, expectedFirstIndex, expectedStartTimes, expectedStartBytes) { + console.assert(expectedStartTimes.length == expectedStartBytes.length); + expect(references.length).toBe(expectedStartTimes.length); + for (var i = 0; i < expectedStartTimes.length; i++) { + var ref = references[i]; + expect(ref.index).toBe(expectedFirstIndex + i); + expect(ref.startTime.toFixed(3)).toBe(expectedStartTimes[i].toFixed(3)); + expect(ref.startByte).toBe(expectedStartBytes[i]); + + if (i < expectedStartTimes.length - 1) { + expect(ref.endTime.toFixed(3)).toBe(expectedStartTimes[i + 1].toFixed(3)); + expect(ref.endByte).toBe(expectedStartBytes[i + 1] - 1); + } + } + } +}); + diff --git a/spec/util.js b/spec/util.js new file mode 100644 index 0000000000..f622be6b70 --- /dev/null +++ b/spec/util.js @@ -0,0 +1,234 @@ +/** + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Utility function for unit tests. + */ + +goog.require('shaka.asserts'); +goog.require('shaka.util.PublicPromise'); + + +var customMatchers = {}; + + +/** + * Creates a new Jasmine matcher object for comparing two Uint8Array objects. + * + * @param {Object} util + * @param {Object} customEqualityTesters + * + * @return {Object} A Jasmine matcher object. + */ +customMatchers.toMatchUint8Array = function(util, customEqualityTesters) { + var matcher = {}; + + matcher.compare = function(actual, opt_expected) { + var expected = opt_expected || new Uint8Array(); + + var result = {}; + + if (actual.length != expected.length) { + result.pass = false; + return result; + } + + for (var i = 0; i < expected.length; i++) { + if (actual[i] == expected[i]) + continue; + result.pass = false; + return result; + } + + result.pass = true; + return result; + }; + + return matcher; +}; + + +/** + * Creates a new Jasmine matcher object for comparing two range object. A range + * object is an object of type {{ start: number, end: number }}. + * + * @param {Object} util + * @param {Object} customEqualityTesters + * + * @return {Object} A Jasmine matcher object. + */ +customMatchers.toMatchRange = function(util, customEqualityTesters) { + var matcher = {}; + + matcher.compare = function(actual, opt_expected) { + var expected = opt_expected || { begin: 0, end: 0 }; + + var result = {}; + + if ((actual == null && expected != null) || + (actual != null && expected == null) || + (actual.begin != expected.begin) || (actual.end != expected.end)) { + result.pass = false; + return result; + } + + result.pass = true; + return result; + }; + + return matcher; +}; + + +/** + * Adds fake event handling support to a Jasmine FakeXMLHttpRequest object. + * + * @param {FakeXMLHttpRequest} xhr The FakeXMLHttpRequest object. + */ +function mockXMLHttpRequestEventHandling(xhr) { + // Jasmine's FakeXMLHttpRequest class uses the attribute "response" as a + // method to set the fake response. Our library uses it (correctly) to get + // the response itself. We "fix" Jasmine's overloaded abuse of this by + // renaming this method to "fakeResponse" and adding a shim to handle + // the "response" field. + // + // Since Jasmine ignores the setting of "response", we will map it to + // "responseText" here, and map it back again in the "onload" spy below. + // + // Note that in a real request, with "responseType" set to "arraybuffer", + // "responseText" throws DOMException. So our library does the right thing, + // and Jasmine's fake is deficient. + if (!xhr.fakeResponse) { + var originalResponseMethod = xhr.response; + console.assert(originalResponseMethod && originalResponseMethod.bind); + xhr.response = null; + xhr.fakeResponse = function(fields) { + if (fields.hasOwnProperty('response')) { + fields['responseText'] = fields['response']; + } + return originalResponseMethod.call(xhr, fields); + }; + } + + // Mock out onload(). + var onload = xhr.onload; + spyOn(xhr, 'onload').and.callFake(function() { + var fakeXMLHttpProgressEvent = { + 'target': xhr + }; + // After each load, overwrite "response" with "responseText". + xhr.response = xhr.responseText; + onload(fakeXMLHttpProgressEvent); + }); + + // Mock out onerror(). + var onerror = xhr.onerror; + spyOn(xhr, 'onerror').and.callFake(function() { + var fakeXMLHttpProgressEvent = { + 'target': xhr + }; + onerror(fakeXMLHttpProgressEvent); + }); +} + + +/** + * Returns a Promise which is resolved after the given delay. + * + * @param {number} seconds The delay in seconds. + */ +function delay(seconds) { + var p = new shaka.util.PublicPromise; + setTimeout(p.resolve, seconds * 1000.0); + return p; +} + + +/** + * Replace shaka.asserts and console.assert with a version which hooks into + * jasmine. This converts all failed assertions into failed tests. + */ +var assertsToFailures = { + uninstall: function() { + shaka.asserts = assertsToFailures.originalShakaAsserts_; + console.assert = assertsToFailures.originalConsoleAssert_; + }, + + install: function() { + assertsToFailures.originalShakaAsserts_ = shaka.asserts; + assertsToFailures.originalConsoleAssert_ = console.assert; + + var realAssert = console.assert.bind(console); + + var jasmineAssert = function(condition, opt_message) { + realAssert(condition, opt_message); + if (!condition) { + var message = opt_message || 'Assertion failed.'; + try { + throw new Error(message); + } catch (exception) { + fail(message); + } + } + }; + + shaka.asserts = { + assert: function(condition, opt_message) { + jasmineAssert(condition, opt_message); + }, + notImplemented: function() { + jasmineAssert(false, 'Not implemented.'); + }, + unreachable: function() { + jasmineAssert(false, 'Unreachable reached.'); + } + }; + + console.assert = jasmineAssert; + } +}; + + +/** + * Called to interpret ContentProtection elements from an MPD. + * @param {!shaka.dash.mpd.ContentProtection} contentProtection + * @return {shaka.player.DrmSchemeInfo} or null if the element is not supported. + */ +function interpretContentProtection(contentProtection) { + var StringUtils = shaka.util.StringUtils; + + // This is the only scheme used in integration tests at the moment. + if (contentProtection.schemeIdUri == 'com.youtube.clearkey') { + var child = contentProtection.children[0]; + var keyid = StringUtils.fromHex(child.getAttribute('keyid')); + var key = StringUtils.fromHex(child.getAttribute('key')); + var keyObj = { + kty: 'oct', + kid: StringUtils.toBase64(keyid, false), + k: StringUtils.toBase64(key, false) + }; + var jwkSet = {keys: [keyObj]}; + var license = JSON.stringify(jwkSet); + var initData = { + initData: StringUtils.toUint8Array(keyid), + initDataType: 'cenc' + }; + var licenseServerUrl = 'data:application/json;base64,' + + StringUtils.toBase64(license); + return new shaka.player.DrmSchemeInfo( + 'org.w3.clearkey', false, licenseServerUrl, false, initData, null); + } + + return null; +} diff --git a/spec_runner.html b/spec_runner.html new file mode 100644 index 0000000000..d76baa6869 --- /dev/null +++ b/spec_runner.html @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/support.html b/support.html new file mode 100644 index 0000000000..16647faa10 --- /dev/null +++ b/support.html @@ -0,0 +1,110 @@ + + + + + + + + Testing support... found: + + diff --git a/third_party/SUMMARY.txt b/third_party/SUMMARY.txt new file mode 100644 index 0000000000..92c768e7b3 --- /dev/null +++ b/third_party/SUMMARY.txt @@ -0,0 +1,44 @@ +closure/compiler.jar + The closure compiler, by Google. + Apache v2.0 license. + https://github.com/google/closure-compiler + +closure/goog + The closure library (modified), by Google. + Apache v2.0 license. + https://github.com/google/closure-library + +closure/deps + The closure dependency generator, part of the closure library, by Google. + Apache v2.0 license. + https://github.com/google/closure-library + +gjslint/closure_linter-2.3.13 + The closure JavaScript linter, by Google. + Apache v2.0 license. + https://code.google.com/p/closure-linter/ + +gjslint/python-gflags-2.0 + A python command-line parser, needed by the closure linter, by Google. + BSD-style license (3-clause). + https://code.google.com/p/python-gflags/ + +jasmine + The Jasmine JS testing framework, v2.1.3, by Pivotal Labs. + MIT license. + https://github.com/pivotal/jasmine + +jasmine/lib/jasmine-ajax-trunk + The Jasmine fake AJAX library, by Pivotal Labs. + MIT license. + https://github.com/pivotal/jasmine-ajax + +blanket_jasmine + Blanket JS coverage library (+ Jasmine adapter), v1.1.5, by Alex Seville. + MIT license. + https://github.com/alex-seville/blanket + +jsdoc + JSDoc 3 documentation generator (modified), by Michael Matthews. + Apache v2.0 license. + https://github.com/jsdoc3/jsdoc diff --git a/third_party/blanket_jasmine/LICENSE.txt b/third_party/blanket_jasmine/LICENSE.txt new file mode 100644 index 0000000000..52a244555e --- /dev/null +++ b/third_party/blanket_jasmine/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2013 Alex Seville + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/third_party/blanket_jasmine/blanket_jasmine.js b/third_party/blanket_jasmine/blanket_jasmine.js new file mode 100644 index 0000000000..72fcb7f267 --- /dev/null +++ b/third_party/blanket_jasmine/blanket_jasmine.js @@ -0,0 +1,5309 @@ +/*! blanket - v1.1.5 */ + +(function(define){ +/* + Copyright (C) 2013 Ariya Hidayat + Copyright (C) 2013 Thaddee Tyl + Copyright (C) 2013 Mathias Bynens + Copyright (C) 2012 Ariya Hidayat + Copyright (C) 2012 Mathias Bynens + Copyright (C) 2012 Joost-Wim Boekesteijn + Copyright (C) 2012 Kris Kowal + Copyright (C) 2012 Yusuke Suzuki + Copyright (C) 2012 Arpad Borsos + Copyright (C) 2011 Ariya Hidayat + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/*jslint bitwise:true plusplus:true */ +/*global esprima:true, define:true, exports:true, window: true, +throwErrorTolerant: true, +throwError: true, generateStatement: true, peek: true, +parseAssignmentExpression: true, parseBlock: true, parseExpression: true, +parseFunctionDeclaration: true, parseFunctionExpression: true, +parseFunctionSourceElements: true, parseVariableIdentifier: true, +parseLeftHandSideExpression: true, +parseUnaryExpression: true, +parseStatement: true, parseSourceElement: true */ + +(function (root, factory) { + 'use strict'; + + // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, + // Rhino, and plain browser loading. + + /* istanbul ignore next */ + if (typeof define === 'function' && define.amd) { + define(['exports'], factory); + } else if (typeof exports !== 'undefined') { + factory(exports); + } else { + factory((root.esprima = {})); + } +}(this, function (exports) { + 'use strict'; + + var Token, + TokenName, + FnExprTokens, + Syntax, + PropertyKind, + Messages, + Regex, + SyntaxTreeDelegate, + source, + strict, + index, + lineNumber, + lineStart, + length, + delegate, + lookahead, + state, + extra; + + Token = { + BooleanLiteral: 1, + EOF: 2, + Identifier: 3, + Keyword: 4, + NullLiteral: 5, + NumericLiteral: 6, + Punctuator: 7, + StringLiteral: 8, + RegularExpression: 9 + }; + + TokenName = {}; + TokenName[Token.BooleanLiteral] = 'Boolean'; + TokenName[Token.EOF] = ''; + TokenName[Token.Identifier] = 'Identifier'; + TokenName[Token.Keyword] = 'Keyword'; + TokenName[Token.NullLiteral] = 'Null'; + TokenName[Token.NumericLiteral] = 'Numeric'; + TokenName[Token.Punctuator] = 'Punctuator'; + TokenName[Token.StringLiteral] = 'String'; + TokenName[Token.RegularExpression] = 'RegularExpression'; + + // A function following one of those tokens is an expression. + FnExprTokens = ['(', '{', '[', 'in', 'typeof', 'instanceof', 'new', + 'return', 'case', 'delete', 'throw', 'void', + // assignment operators + '=', '+=', '-=', '*=', '/=', '%=', '<<=', '>>=', '>>>=', + '&=', '|=', '^=', ',', + // binary/unary operators + '+', '-', '*', '/', '%', '++', '--', '<<', '>>', '>>>', '&', + '|', '^', '!', '~', '&&', '||', '?', ':', '===', '==', '>=', + '<=', '<', '>', '!=', '!==']; + + Syntax = { + AssignmentExpression: 'AssignmentExpression', + ArrayExpression: 'ArrayExpression', + BlockStatement: 'BlockStatement', + BinaryExpression: 'BinaryExpression', + BreakStatement: 'BreakStatement', + CallExpression: 'CallExpression', + CatchClause: 'CatchClause', + ConditionalExpression: 'ConditionalExpression', + ContinueStatement: 'ContinueStatement', + DoWhileStatement: 'DoWhileStatement', + DebuggerStatement: 'DebuggerStatement', + EmptyStatement: 'EmptyStatement', + ExpressionStatement: 'ExpressionStatement', + ForStatement: 'ForStatement', + ForInStatement: 'ForInStatement', + FunctionDeclaration: 'FunctionDeclaration', + FunctionExpression: 'FunctionExpression', + Identifier: 'Identifier', + IfStatement: 'IfStatement', + Literal: 'Literal', + LabeledStatement: 'LabeledStatement', + LogicalExpression: 'LogicalExpression', + MemberExpression: 'MemberExpression', + NewExpression: 'NewExpression', + ObjectExpression: 'ObjectExpression', + Program: 'Program', + Property: 'Property', + ReturnStatement: 'ReturnStatement', + SequenceExpression: 'SequenceExpression', + SwitchStatement: 'SwitchStatement', + SwitchCase: 'SwitchCase', + ThisExpression: 'ThisExpression', + ThrowStatement: 'ThrowStatement', + TryStatement: 'TryStatement', + UnaryExpression: 'UnaryExpression', + UpdateExpression: 'UpdateExpression', + VariableDeclaration: 'VariableDeclaration', + VariableDeclarator: 'VariableDeclarator', + WhileStatement: 'WhileStatement', + WithStatement: 'WithStatement' + }; + + PropertyKind = { + Data: 1, + Get: 2, + Set: 4 + }; + + // Error messages should be identical to V8. + Messages = { + UnexpectedToken: 'Unexpected token %0', + UnexpectedNumber: 'Unexpected number', + UnexpectedString: 'Unexpected string', + UnexpectedIdentifier: 'Unexpected identifier', + UnexpectedReserved: 'Unexpected reserved word', + UnexpectedEOS: 'Unexpected end of input', + NewlineAfterThrow: 'Illegal newline after throw', + InvalidRegExp: 'Invalid regular expression', + UnterminatedRegExp: 'Invalid regular expression: missing /', + InvalidLHSInAssignment: 'Invalid left-hand side in assignment', + InvalidLHSInForIn: 'Invalid left-hand side in for-in', + MultipleDefaultsInSwitch: 'More than one default clause in switch statement', + NoCatchOrFinally: 'Missing catch or finally after try', + UnknownLabel: 'Undefined label \'%0\'', + Redeclaration: '%0 \'%1\' has already been declared', + IllegalContinue: 'Illegal continue statement', + IllegalBreak: 'Illegal break statement', + IllegalReturn: 'Illegal return statement', + StrictModeWith: 'Strict mode code may not include a with statement', + StrictCatchVariable: 'Catch variable may not be eval or arguments in strict mode', + StrictVarName: 'Variable name may not be eval or arguments in strict mode', + StrictParamName: 'Parameter name eval or arguments is not allowed in strict mode', + StrictParamDupe: 'Strict mode function may not have duplicate parameter names', + StrictFunctionName: 'Function name may not be eval or arguments in strict mode', + StrictOctalLiteral: 'Octal literals are not allowed in strict mode.', + StrictDelete: 'Delete of an unqualified identifier in strict mode.', + StrictDuplicateProperty: 'Duplicate data property in object literal not allowed in strict mode', + AccessorDataProperty: 'Object literal may not have data and accessor property with the same name', + AccessorGetSet: 'Object literal may not have multiple get/set accessors with the same name', + StrictLHSAssignment: 'Assignment to eval or arguments is not allowed in strict mode', + StrictLHSPostfix: 'Postfix increment/decrement may not have eval or arguments operand in strict mode', + StrictLHSPrefix: 'Prefix increment/decrement may not have eval or arguments operand in strict mode', + StrictReservedWord: 'Use of future reserved word in strict mode' + }; + + // See also tools/generate-unicode-regex.py. + Regex = { + NonAsciiIdentifierStart: new RegExp('[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F0\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]'), + NonAsciiIdentifierPart: new RegExp('[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0\u08A2-\u08AC\u08E4-\u08FE\u0900-\u0963\u0966-\u096F\u0971-\u0977\u0979-\u097F\u0981-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C01-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58\u0C59\u0C60-\u0C63\u0C66-\u0C6F\u0C82\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D02\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D60-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F0\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191C\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF6\u1D00-\u1DE6\u1DFC-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA697\uA69F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7B\uAA80-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE26\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]') + }; + + // Ensure the condition is true, otherwise throw an error. + // This is only to have a better contract semantic, i.e. another safety net + // to catch a logic error. The condition shall be fulfilled in normal case. + // Do NOT use this to enforce a certain condition on any user input. + + function assert(condition, message) { + /* istanbul ignore if */ + if (!condition) { + throw new Error('ASSERT: ' + message); + } + } + + function isDecimalDigit(ch) { + return (ch >= 48 && ch <= 57); // 0..9 + } + + function isHexDigit(ch) { + return '0123456789abcdefABCDEF'.indexOf(ch) >= 0; + } + + function isOctalDigit(ch) { + return '01234567'.indexOf(ch) >= 0; + } + + + // 7.2 White Space + + function isWhiteSpace(ch) { + return (ch === 0x20) || (ch === 0x09) || (ch === 0x0B) || (ch === 0x0C) || (ch === 0xA0) || + (ch >= 0x1680 && [0x1680, 0x180E, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000, 0xFEFF].indexOf(ch) >= 0); + } + + // 7.3 Line Terminators + + function isLineTerminator(ch) { + return (ch === 0x0A) || (ch === 0x0D) || (ch === 0x2028) || (ch === 0x2029); + } + + // 7.6 Identifier Names and Identifiers + + function isIdentifierStart(ch) { + return (ch === 0x24) || (ch === 0x5F) || // $ (dollar) and _ (underscore) + (ch >= 0x41 && ch <= 0x5A) || // A..Z + (ch >= 0x61 && ch <= 0x7A) || // a..z + (ch === 0x5C) || // \ (backslash) + ((ch >= 0x80) && Regex.NonAsciiIdentifierStart.test(String.fromCharCode(ch))); + } + + function isIdentifierPart(ch) { + return (ch === 0x24) || (ch === 0x5F) || // $ (dollar) and _ (underscore) + (ch >= 0x41 && ch <= 0x5A) || // A..Z + (ch >= 0x61 && ch <= 0x7A) || // a..z + (ch >= 0x30 && ch <= 0x39) || // 0..9 + (ch === 0x5C) || // \ (backslash) + ((ch >= 0x80) && Regex.NonAsciiIdentifierPart.test(String.fromCharCode(ch))); + } + + // 7.6.1.2 Future Reserved Words + + function isFutureReservedWord(id) { + switch (id) { + case 'class': + case 'enum': + case 'export': + case 'extends': + case 'import': + case 'super': + return true; + default: + return false; + } + } + + function isStrictModeReservedWord(id) { + switch (id) { + case 'implements': + case 'interface': + case 'package': + case 'private': + case 'protected': + case 'public': + case 'static': + case 'yield': + case 'let': + return true; + default: + return false; + } + } + + function isRestrictedWord(id) { + return id === 'eval' || id === 'arguments'; + } + + // 7.6.1.1 Keywords + + function isKeyword(id) { + if (strict && isStrictModeReservedWord(id)) { + return true; + } + + // 'const' is specialized as Keyword in V8. + // 'yield' and 'let' are for compatiblity with SpiderMonkey and ES.next. + // Some others are from future reserved words. + + switch (id.length) { + case 2: + return (id === 'if') || (id === 'in') || (id === 'do'); + case 3: + return (id === 'var') || (id === 'for') || (id === 'new') || + (id === 'try') || (id === 'let'); + case 4: + return (id === 'this') || (id === 'else') || (id === 'case') || + (id === 'void') || (id === 'with') || (id === 'enum'); + case 5: + return (id === 'while') || (id === 'break') || (id === 'catch') || + (id === 'throw') || (id === 'const') || (id === 'yield') || + (id === 'class') || (id === 'super'); + case 6: + return (id === 'return') || (id === 'typeof') || (id === 'delete') || + (id === 'switch') || (id === 'export') || (id === 'import'); + case 7: + return (id === 'default') || (id === 'finally') || (id === 'extends'); + case 8: + return (id === 'function') || (id === 'continue') || (id === 'debugger'); + case 10: + return (id === 'instanceof'); + default: + return false; + } + } + + // 7.4 Comments + + function addComment(type, value, start, end, loc) { + var comment, attacher; + + assert(typeof start === 'number', 'Comment must have valid position'); + + // Because the way the actual token is scanned, often the comments + // (if any) are skipped twice during the lexical analysis. + // Thus, we need to skip adding a comment if the comment array already + // handled it. + if (state.lastCommentStart >= start) { + return; + } + state.lastCommentStart = start; + + comment = { + type: type, + value: value + }; + if (extra.range) { + comment.range = [start, end]; + } + if (extra.loc) { + comment.loc = loc; + } + extra.comments.push(comment); + if (extra.attachComment) { + extra.leadingComments.push(comment); + extra.trailingComments.push(comment); + } + } + + function skipSingleLineComment(offset) { + var start, loc, ch, comment; + + start = index - offset; + loc = { + start: { + line: lineNumber, + column: index - lineStart - offset + } + }; + + while (index < length) { + ch = source.charCodeAt(index); + ++index; + if (isLineTerminator(ch)) { + if (extra.comments) { + comment = source.slice(start + offset, index - 1); + loc.end = { + line: lineNumber, + column: index - lineStart - 1 + }; + addComment('Line', comment, start, index - 1, loc); + } + if (ch === 13 && source.charCodeAt(index) === 10) { + ++index; + } + ++lineNumber; + lineStart = index; + return; + } + } + + if (extra.comments) { + comment = source.slice(start + offset, index); + loc.end = { + line: lineNumber, + column: index - lineStart + }; + addComment('Line', comment, start, index, loc); + } + } + + function skipMultiLineComment() { + var start, loc, ch, comment; + + if (extra.comments) { + start = index - 2; + loc = { + start: { + line: lineNumber, + column: index - lineStart - 2 + } + }; + } + + while (index < length) { + ch = source.charCodeAt(index); + if (isLineTerminator(ch)) { + if (ch === 0x0D && source.charCodeAt(index + 1) === 0x0A) { + ++index; + } + ++lineNumber; + ++index; + lineStart = index; + if (index >= length) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } else if (ch === 0x2A) { + // Block comment ends with '*/'. + if (source.charCodeAt(index + 1) === 0x2F) { + ++index; + ++index; + if (extra.comments) { + comment = source.slice(start + 2, index - 2); + loc.end = { + line: lineNumber, + column: index - lineStart + }; + addComment('Block', comment, start, index, loc); + } + return; + } + ++index; + } else { + ++index; + } + } + + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + function skipComment() { + var ch, start; + + start = (index === 0); + while (index < length) { + ch = source.charCodeAt(index); + + if (isWhiteSpace(ch)) { + ++index; + } else if (isLineTerminator(ch)) { + ++index; + if (ch === 0x0D && source.charCodeAt(index) === 0x0A) { + ++index; + } + ++lineNumber; + lineStart = index; + start = true; + } else if (ch === 0x2F) { // U+002F is '/' + ch = source.charCodeAt(index + 1); + if (ch === 0x2F) { + ++index; + ++index; + skipSingleLineComment(2); + start = true; + } else if (ch === 0x2A) { // U+002A is '*' + ++index; + ++index; + skipMultiLineComment(); + } else { + break; + } + } else if (start && ch === 0x2D) { // U+002D is '-' + // U+003E is '>' + if ((source.charCodeAt(index + 1) === 0x2D) && (source.charCodeAt(index + 2) === 0x3E)) { + // '-->' is a single-line comment + index += 3; + skipSingleLineComment(3); + } else { + break; + } + } else if (ch === 0x3C) { // U+003C is '<' + if (source.slice(index + 1, index + 4) === '!--') { + ++index; // `<` + ++index; // `!` + ++index; // `-` + ++index; // `-` + skipSingleLineComment(4); + } else { + break; + } + } else { + break; + } + } + } + + function scanHexEscape(prefix) { + var i, len, ch, code = 0; + + len = (prefix === 'u') ? 4 : 2; + for (i = 0; i < len; ++i) { + if (index < length && isHexDigit(source[index])) { + ch = source[index++]; + code = code * 16 + '0123456789abcdef'.indexOf(ch.toLowerCase()); + } else { + return ''; + } + } + return String.fromCharCode(code); + } + + function getEscapedIdentifier() { + var ch, id; + + ch = source.charCodeAt(index++); + id = String.fromCharCode(ch); + + // '\u' (U+005C, U+0075) denotes an escaped character. + if (ch === 0x5C) { + if (source.charCodeAt(index) !== 0x75) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + ++index; + ch = scanHexEscape('u'); + if (!ch || ch === '\\' || !isIdentifierStart(ch.charCodeAt(0))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + id = ch; + } + + while (index < length) { + ch = source.charCodeAt(index); + if (!isIdentifierPart(ch)) { + break; + } + ++index; + id += String.fromCharCode(ch); + + // '\u' (U+005C, U+0075) denotes an escaped character. + if (ch === 0x5C) { + id = id.substr(0, id.length - 1); + if (source.charCodeAt(index) !== 0x75) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + ++index; + ch = scanHexEscape('u'); + if (!ch || ch === '\\' || !isIdentifierPart(ch.charCodeAt(0))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + id += ch; + } + } + + return id; + } + + function getIdentifier() { + var start, ch; + + start = index++; + while (index < length) { + ch = source.charCodeAt(index); + if (ch === 0x5C) { + // Blackslash (U+005C) marks Unicode escape sequence. + index = start; + return getEscapedIdentifier(); + } + if (isIdentifierPart(ch)) { + ++index; + } else { + break; + } + } + + return source.slice(start, index); + } + + function scanIdentifier() { + var start, id, type; + + start = index; + + // Backslash (U+005C) starts an escaped character. + id = (source.charCodeAt(index) === 0x5C) ? getEscapedIdentifier() : getIdentifier(); + + // There is no keyword or literal with only one character. + // Thus, it must be an identifier. + if (id.length === 1) { + type = Token.Identifier; + } else if (isKeyword(id)) { + type = Token.Keyword; + } else if (id === 'null') { + type = Token.NullLiteral; + } else if (id === 'true' || id === 'false') { + type = Token.BooleanLiteral; + } else { + type = Token.Identifier; + } + + return { + type: type, + value: id, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index + }; + } + + + // 7.7 Punctuators + + function scanPunctuator() { + var start = index, + code = source.charCodeAt(index), + code2, + ch1 = source[index], + ch2, + ch3, + ch4; + + switch (code) { + + // Check for most common single-character punctuators. + case 0x2E: // . dot + case 0x28: // ( open bracket + case 0x29: // ) close bracket + case 0x3B: // ; semicolon + case 0x2C: // , comma + case 0x7B: // { open curly brace + case 0x7D: // } close curly brace + case 0x5B: // [ + case 0x5D: // ] + case 0x3A: // : + case 0x3F: // ? + case 0x7E: // ~ + ++index; + if (extra.tokenize) { + if (code === 0x28) { + extra.openParenToken = extra.tokens.length; + } else if (code === 0x7B) { + extra.openCurlyToken = extra.tokens.length; + } + } + return { + type: Token.Punctuator, + value: String.fromCharCode(code), + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index + }; + + default: + code2 = source.charCodeAt(index + 1); + + // '=' (U+003D) marks an assignment or comparison operator. + if (code2 === 0x3D) { + switch (code) { + case 0x2B: // + + case 0x2D: // - + case 0x2F: // / + case 0x3C: // < + case 0x3E: // > + case 0x5E: // ^ + case 0x7C: // | + case 0x25: // % + case 0x26: // & + case 0x2A: // * + index += 2; + return { + type: Token.Punctuator, + value: String.fromCharCode(code) + String.fromCharCode(code2), + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index + }; + + case 0x21: // ! + case 0x3D: // = + index += 2; + + // !== and === + if (source.charCodeAt(index) === 0x3D) { + ++index; + } + return { + type: Token.Punctuator, + value: source.slice(start, index), + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index + }; + } + } + } + + // 4-character punctuator: >>>= + + ch4 = source.substr(index, 4); + + if (ch4 === '>>>=') { + index += 4; + return { + type: Token.Punctuator, + value: ch4, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index + }; + } + + // 3-character punctuators: === !== >>> <<= >>= + + ch3 = ch4.substr(0, 3); + + if (ch3 === '>>>' || ch3 === '<<=' || ch3 === '>>=') { + index += 3; + return { + type: Token.Punctuator, + value: ch3, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index + }; + } + + // Other 2-character punctuators: ++ -- << >> && || + ch2 = ch3.substr(0, 2); + + if ((ch1 === ch2[1] && ('+-<>&|'.indexOf(ch1) >= 0)) || ch2 === '=>') { + index += 2; + return { + type: Token.Punctuator, + value: ch2, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index + }; + } + + // 1-character punctuators: < > = ! + - * % & | ^ / + if ('<>=!+-*%&|^/'.indexOf(ch1) >= 0) { + ++index; + return { + type: Token.Punctuator, + value: ch1, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index + }; + } + + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + // 7.8.3 Numeric Literals + + function scanHexLiteral(start) { + var number = ''; + + while (index < length) { + if (!isHexDigit(source[index])) { + break; + } + number += source[index++]; + } + + if (number.length === 0) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + if (isIdentifierStart(source.charCodeAt(index))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + return { + type: Token.NumericLiteral, + value: parseInt('0x' + number, 16), + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index + }; + } + + function scanOctalLiteral(start) { + var number = '0' + source[index++]; + while (index < length) { + if (!isOctalDigit(source[index])) { + break; + } + number += source[index++]; + } + + if (isIdentifierStart(source.charCodeAt(index)) || isDecimalDigit(source.charCodeAt(index))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + return { + type: Token.NumericLiteral, + value: parseInt(number, 8), + octal: true, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index + }; + } + + function scanNumericLiteral() { + var number, start, ch; + + ch = source[index]; + assert(isDecimalDigit(ch.charCodeAt(0)) || (ch === '.'), + 'Numeric literal must start with a decimal digit or a decimal point'); + + start = index; + number = ''; + if (ch !== '.') { + number = source[index++]; + ch = source[index]; + + // Hex number starts with '0x'. + // Octal number starts with '0'. + if (number === '0') { + if (ch === 'x' || ch === 'X') { + ++index; + return scanHexLiteral(start); + } + if (isOctalDigit(ch)) { + return scanOctalLiteral(start); + } + + // decimal number starts with '0' such as '09' is illegal. + if (ch && isDecimalDigit(ch.charCodeAt(0))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } + + while (isDecimalDigit(source.charCodeAt(index))) { + number += source[index++]; + } + ch = source[index]; + } + + if (ch === '.') { + number += source[index++]; + while (isDecimalDigit(source.charCodeAt(index))) { + number += source[index++]; + } + ch = source[index]; + } + + if (ch === 'e' || ch === 'E') { + number += source[index++]; + + ch = source[index]; + if (ch === '+' || ch === '-') { + number += source[index++]; + } + if (isDecimalDigit(source.charCodeAt(index))) { + while (isDecimalDigit(source.charCodeAt(index))) { + number += source[index++]; + } + } else { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } + + if (isIdentifierStart(source.charCodeAt(index))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + return { + type: Token.NumericLiteral, + value: parseFloat(number), + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index + }; + } + + // 7.8.4 String Literals + + function scanStringLiteral() { + var str = '', quote, start, ch, code, unescaped, restore, octal = false, startLineNumber, startLineStart; + startLineNumber = lineNumber; + startLineStart = lineStart; + + quote = source[index]; + assert((quote === '\'' || quote === '"'), + 'String literal must starts with a quote'); + + start = index; + ++index; + + while (index < length) { + ch = source[index++]; + + if (ch === quote) { + quote = ''; + break; + } else if (ch === '\\') { + ch = source[index++]; + if (!ch || !isLineTerminator(ch.charCodeAt(0))) { + switch (ch) { + case 'u': + case 'x': + restore = index; + unescaped = scanHexEscape(ch); + if (unescaped) { + str += unescaped; + } else { + index = restore; + str += ch; + } + break; + case 'n': + str += '\n'; + break; + case 'r': + str += '\r'; + break; + case 't': + str += '\t'; + break; + case 'b': + str += '\b'; + break; + case 'f': + str += '\f'; + break; + case 'v': + str += '\x0B'; + break; + + default: + if (isOctalDigit(ch)) { + code = '01234567'.indexOf(ch); + + // \0 is not octal escape sequence + if (code !== 0) { + octal = true; + } + + if (index < length && isOctalDigit(source[index])) { + octal = true; + code = code * 8 + '01234567'.indexOf(source[index++]); + + // 3 digits are only allowed when string starts + // with 0, 1, 2, 3 + if ('0123'.indexOf(ch) >= 0 && + index < length && + isOctalDigit(source[index])) { + code = code * 8 + '01234567'.indexOf(source[index++]); + } + } + str += String.fromCharCode(code); + } else { + str += ch; + } + break; + } + } else { + ++lineNumber; + if (ch === '\r' && source[index] === '\n') { + ++index; + } + lineStart = index; + } + } else if (isLineTerminator(ch.charCodeAt(0))) { + break; + } else { + str += ch; + } + } + + if (quote !== '') { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + return { + type: Token.StringLiteral, + value: str, + octal: octal, + startLineNumber: startLineNumber, + startLineStart: startLineStart, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index + }; + } + + function testRegExp(pattern, flags) { + var value; + try { + value = new RegExp(pattern, flags); + } catch (e) { + throwError({}, Messages.InvalidRegExp); + } + return value; + } + + function scanRegExpBody() { + var ch, str, classMarker, terminated, body; + + ch = source[index]; + assert(ch === '/', 'Regular expression literal must start with a slash'); + str = source[index++]; + + classMarker = false; + terminated = false; + while (index < length) { + ch = source[index++]; + str += ch; + if (ch === '\\') { + ch = source[index++]; + // ECMA-262 7.8.5 + if (isLineTerminator(ch.charCodeAt(0))) { + throwError({}, Messages.UnterminatedRegExp); + } + str += ch; + } else if (isLineTerminator(ch.charCodeAt(0))) { + throwError({}, Messages.UnterminatedRegExp); + } else if (classMarker) { + if (ch === ']') { + classMarker = false; + } + } else { + if (ch === '/') { + terminated = true; + break; + } else if (ch === '[') { + classMarker = true; + } + } + } + + if (!terminated) { + throwError({}, Messages.UnterminatedRegExp); + } + + // Exclude leading and trailing slash. + body = str.substr(1, str.length - 2); + return { + value: body, + literal: str + }; + } + + function scanRegExpFlags() { + var ch, str, flags, restore; + + str = ''; + flags = ''; + while (index < length) { + ch = source[index]; + if (!isIdentifierPart(ch.charCodeAt(0))) { + break; + } + + ++index; + if (ch === '\\' && index < length) { + ch = source[index]; + if (ch === 'u') { + ++index; + restore = index; + ch = scanHexEscape('u'); + if (ch) { + flags += ch; + for (str += '\\u'; restore < index; ++restore) { + str += source[restore]; + } + } else { + index = restore; + flags += 'u'; + str += '\\u'; + } + throwErrorTolerant({}, Messages.UnexpectedToken, 'ILLEGAL'); + } else { + str += '\\'; + throwErrorTolerant({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } else { + flags += ch; + str += ch; + } + } + + return { + value: flags, + literal: str + }; + } + + function scanRegExp() { + var start, body, flags, pattern, value; + + lookahead = null; + skipComment(); + start = index; + + body = scanRegExpBody(); + flags = scanRegExpFlags(); + value = testRegExp(body.value, flags.value); + + if (extra.tokenize) { + return { + type: Token.RegularExpression, + value: value, + lineNumber: lineNumber, + lineStart: lineStart, + start: start, + end: index + }; + } + + return { + literal: body.literal + flags.literal, + value: value, + start: start, + end: index + }; + } + + function collectRegex() { + var pos, loc, regex, token; + + skipComment(); + + pos = index; + loc = { + start: { + line: lineNumber, + column: index - lineStart + } + }; + + regex = scanRegExp(); + loc.end = { + line: lineNumber, + column: index - lineStart + }; + + /* istanbul ignore next */ + if (!extra.tokenize) { + // Pop the previous token, which is likely '/' or '/=' + if (extra.tokens.length > 0) { + token = extra.tokens[extra.tokens.length - 1]; + if (token.range[0] === pos && token.type === 'Punctuator') { + if (token.value === '/' || token.value === '/=') { + extra.tokens.pop(); + } + } + } + + extra.tokens.push({ + type: 'RegularExpression', + value: regex.literal, + range: [pos, index], + loc: loc + }); + } + + return regex; + } + + function isIdentifierName(token) { + return token.type === Token.Identifier || + token.type === Token.Keyword || + token.type === Token.BooleanLiteral || + token.type === Token.NullLiteral; + } + + function advanceSlash() { + var prevToken, + checkToken; + // Using the following algorithm: + // https://github.com/mozilla/sweet.js/wiki/design + prevToken = extra.tokens[extra.tokens.length - 1]; + if (!prevToken) { + // Nothing before that: it cannot be a division. + return collectRegex(); + } + if (prevToken.type === 'Punctuator') { + if (prevToken.value === ']') { + return scanPunctuator(); + } + if (prevToken.value === ')') { + checkToken = extra.tokens[extra.openParenToken - 1]; + if (checkToken && + checkToken.type === 'Keyword' && + (checkToken.value === 'if' || + checkToken.value === 'while' || + checkToken.value === 'for' || + checkToken.value === 'with')) { + return collectRegex(); + } + return scanPunctuator(); + } + if (prevToken.value === '}') { + // Dividing a function by anything makes little sense, + // but we have to check for that. + if (extra.tokens[extra.openCurlyToken - 3] && + extra.tokens[extra.openCurlyToken - 3].type === 'Keyword') { + // Anonymous function. + checkToken = extra.tokens[extra.openCurlyToken - 4]; + if (!checkToken) { + return scanPunctuator(); + } + } else if (extra.tokens[extra.openCurlyToken - 4] && + extra.tokens[extra.openCurlyToken - 4].type === 'Keyword') { + // Named function. + checkToken = extra.tokens[extra.openCurlyToken - 5]; + if (!checkToken) { + return collectRegex(); + } + } else { + return scanPunctuator(); + } + // checkToken determines whether the function is + // a declaration or an expression. + if (FnExprTokens.indexOf(checkToken.value) >= 0) { + // It is an expression. + return scanPunctuator(); + } + // It is a declaration. + return collectRegex(); + } + return collectRegex(); + } + if (prevToken.type === 'Keyword') { + return collectRegex(); + } + return scanPunctuator(); + } + + function advance() { + var ch; + + skipComment(); + + if (index >= length) { + return { + type: Token.EOF, + lineNumber: lineNumber, + lineStart: lineStart, + start: index, + end: index + }; + } + + ch = source.charCodeAt(index); + + if (isIdentifierStart(ch)) { + return scanIdentifier(); + } + + // Very common: ( and ) and ; + if (ch === 0x28 || ch === 0x29 || ch === 0x3B) { + return scanPunctuator(); + } + + // String literal starts with single quote (U+0027) or double quote (U+0022). + if (ch === 0x27 || ch === 0x22) { + return scanStringLiteral(); + } + + + // Dot (.) U+002E can also start a floating-point number, hence the need + // to check the next character. + if (ch === 0x2E) { + if (isDecimalDigit(source.charCodeAt(index + 1))) { + return scanNumericLiteral(); + } + return scanPunctuator(); + } + + if (isDecimalDigit(ch)) { + return scanNumericLiteral(); + } + + // Slash (/) U+002F can also start a regex. + if (extra.tokenize && ch === 0x2F) { + return advanceSlash(); + } + + return scanPunctuator(); + } + + function collectToken() { + var loc, token, range, value; + + skipComment(); + loc = { + start: { + line: lineNumber, + column: index - lineStart + } + }; + + token = advance(); + loc.end = { + line: lineNumber, + column: index - lineStart + }; + + if (token.type !== Token.EOF) { + value = source.slice(token.start, token.end); + extra.tokens.push({ + type: TokenName[token.type], + value: value, + range: [token.start, token.end], + loc: loc + }); + } + + return token; + } + + function lex() { + var token; + + token = lookahead; + index = token.end; + lineNumber = token.lineNumber; + lineStart = token.lineStart; + + lookahead = (typeof extra.tokens !== 'undefined') ? collectToken() : advance(); + + index = token.end; + lineNumber = token.lineNumber; + lineStart = token.lineStart; + + return token; + } + + function peek() { + var pos, line, start; + + pos = index; + line = lineNumber; + start = lineStart; + lookahead = (typeof extra.tokens !== 'undefined') ? collectToken() : advance(); + index = pos; + lineNumber = line; + lineStart = start; + } + + function Position(line, column) { + this.line = line; + this.column = column; + } + + function SourceLocation(startLine, startColumn, line, column) { + this.start = new Position(startLine, startColumn); + this.end = new Position(line, column); + } + + SyntaxTreeDelegate = { + + name: 'SyntaxTree', + + processComment: function (node) { + var lastChild, trailingComments; + + if (node.type === Syntax.Program) { + if (node.body.length > 0) { + return; + } + } + + if (extra.trailingComments.length > 0) { + if (extra.trailingComments[0].range[0] >= node.range[1]) { + trailingComments = extra.trailingComments; + extra.trailingComments = []; + } else { + extra.trailingComments.length = 0; + } + } else { + if (extra.bottomRightStack.length > 0 && + extra.bottomRightStack[extra.bottomRightStack.length - 1].trailingComments && + extra.bottomRightStack[extra.bottomRightStack.length - 1].trailingComments[0].range[0] >= node.range[1]) { + trailingComments = extra.bottomRightStack[extra.bottomRightStack.length - 1].trailingComments; + delete extra.bottomRightStack[extra.bottomRightStack.length - 1].trailingComments; + } + } + + // Eating the stack. + while (extra.bottomRightStack.length > 0 && extra.bottomRightStack[extra.bottomRightStack.length - 1].range[0] >= node.range[0]) { + lastChild = extra.bottomRightStack.pop(); + } + + if (lastChild) { + if (lastChild.leadingComments && lastChild.leadingComments[lastChild.leadingComments.length - 1].range[1] <= node.range[0]) { + node.leadingComments = lastChild.leadingComments; + delete lastChild.leadingComments; + } + } else if (extra.leadingComments.length > 0 && extra.leadingComments[extra.leadingComments.length - 1].range[1] <= node.range[0]) { + node.leadingComments = extra.leadingComments; + extra.leadingComments = []; + } + + + if (trailingComments) { + node.trailingComments = trailingComments; + } + + extra.bottomRightStack.push(node); + }, + + markEnd: function (node, startToken) { + if (extra.range) { + node.range = [startToken.start, index]; + } + if (extra.loc) { + node.loc = new SourceLocation( + startToken.startLineNumber === undefined ? startToken.lineNumber : startToken.startLineNumber, + startToken.start - (startToken.startLineStart === undefined ? startToken.lineStart : startToken.startLineStart), + lineNumber, + index - lineStart + ); + this.postProcess(node); + } + + if (extra.attachComment) { + this.processComment(node); + } + return node; + }, + + postProcess: function (node) { + if (extra.source) { + node.loc.source = extra.source; + } + return node; + }, + + createArrayExpression: function (elements) { + return { + type: Syntax.ArrayExpression, + elements: elements + }; + }, + + createAssignmentExpression: function (operator, left, right) { + return { + type: Syntax.AssignmentExpression, + operator: operator, + left: left, + right: right + }; + }, + + createBinaryExpression: function (operator, left, right) { + var type = (operator === '||' || operator === '&&') ? Syntax.LogicalExpression : + Syntax.BinaryExpression; + return { + type: type, + operator: operator, + left: left, + right: right + }; + }, + + createBlockStatement: function (body) { + return { + type: Syntax.BlockStatement, + body: body + }; + }, + + createBreakStatement: function (label) { + return { + type: Syntax.BreakStatement, + label: label + }; + }, + + createCallExpression: function (callee, args) { + return { + type: Syntax.CallExpression, + callee: callee, + 'arguments': args + }; + }, + + createCatchClause: function (param, body) { + return { + type: Syntax.CatchClause, + param: param, + body: body + }; + }, + + createConditionalExpression: function (test, consequent, alternate) { + return { + type: Syntax.ConditionalExpression, + test: test, + consequent: consequent, + alternate: alternate + }; + }, + + createContinueStatement: function (label) { + return { + type: Syntax.ContinueStatement, + label: label + }; + }, + + createDebuggerStatement: function () { + return { + type: Syntax.DebuggerStatement + }; + }, + + createDoWhileStatement: function (body, test) { + return { + type: Syntax.DoWhileStatement, + body: body, + test: test + }; + }, + + createEmptyStatement: function () { + return { + type: Syntax.EmptyStatement + }; + }, + + createExpressionStatement: function (expression) { + return { + type: Syntax.ExpressionStatement, + expression: expression + }; + }, + + createForStatement: function (init, test, update, body) { + return { + type: Syntax.ForStatement, + init: init, + test: test, + update: update, + body: body + }; + }, + + createForInStatement: function (left, right, body) { + return { + type: Syntax.ForInStatement, + left: left, + right: right, + body: body, + each: false + }; + }, + + createFunctionDeclaration: function (id, params, defaults, body) { + return { + type: Syntax.FunctionDeclaration, + id: id, + params: params, + defaults: defaults, + body: body, + rest: null, + generator: false, + expression: false + }; + }, + + createFunctionExpression: function (id, params, defaults, body) { + return { + type: Syntax.FunctionExpression, + id: id, + params: params, + defaults: defaults, + body: body, + rest: null, + generator: false, + expression: false + }; + }, + + createIdentifier: function (name) { + return { + type: Syntax.Identifier, + name: name + }; + }, + + createIfStatement: function (test, consequent, alternate) { + return { + type: Syntax.IfStatement, + test: test, + consequent: consequent, + alternate: alternate + }; + }, + + createLabeledStatement: function (label, body) { + return { + type: Syntax.LabeledStatement, + label: label, + body: body + }; + }, + + createLiteral: function (token) { + return { + type: Syntax.Literal, + value: token.value, + raw: source.slice(token.start, token.end) + }; + }, + + createMemberExpression: function (accessor, object, property) { + return { + type: Syntax.MemberExpression, + computed: accessor === '[', + object: object, + property: property + }; + }, + + createNewExpression: function (callee, args) { + return { + type: Syntax.NewExpression, + callee: callee, + 'arguments': args + }; + }, + + createObjectExpression: function (properties) { + return { + type: Syntax.ObjectExpression, + properties: properties + }; + }, + + createPostfixExpression: function (operator, argument) { + return { + type: Syntax.UpdateExpression, + operator: operator, + argument: argument, + prefix: false + }; + }, + + createProgram: function (body) { + return { + type: Syntax.Program, + body: body + }; + }, + + createProperty: function (kind, key, value) { + return { + type: Syntax.Property, + key: key, + value: value, + kind: kind + }; + }, + + createReturnStatement: function (argument) { + return { + type: Syntax.ReturnStatement, + argument: argument + }; + }, + + createSequenceExpression: function (expressions) { + return { + type: Syntax.SequenceExpression, + expressions: expressions + }; + }, + + createSwitchCase: function (test, consequent) { + return { + type: Syntax.SwitchCase, + test: test, + consequent: consequent + }; + }, + + createSwitchStatement: function (discriminant, cases) { + return { + type: Syntax.SwitchStatement, + discriminant: discriminant, + cases: cases + }; + }, + + createThisExpression: function () { + return { + type: Syntax.ThisExpression + }; + }, + + createThrowStatement: function (argument) { + return { + type: Syntax.ThrowStatement, + argument: argument + }; + }, + + createTryStatement: function (block, guardedHandlers, handlers, finalizer) { + return { + type: Syntax.TryStatement, + block: block, + guardedHandlers: guardedHandlers, + handlers: handlers, + finalizer: finalizer + }; + }, + + createUnaryExpression: function (operator, argument) { + if (operator === '++' || operator === '--') { + return { + type: Syntax.UpdateExpression, + operator: operator, + argument: argument, + prefix: true + }; + } + return { + type: Syntax.UnaryExpression, + operator: operator, + argument: argument, + prefix: true + }; + }, + + createVariableDeclaration: function (declarations, kind) { + return { + type: Syntax.VariableDeclaration, + declarations: declarations, + kind: kind + }; + }, + + createVariableDeclarator: function (id, init) { + return { + type: Syntax.VariableDeclarator, + id: id, + init: init + }; + }, + + createWhileStatement: function (test, body) { + return { + type: Syntax.WhileStatement, + test: test, + body: body + }; + }, + + createWithStatement: function (object, body) { + return { + type: Syntax.WithStatement, + object: object, + body: body + }; + } + }; + + // Return true if there is a line terminator before the next token. + + function peekLineTerminator() { + var pos, line, start, found; + + pos = index; + line = lineNumber; + start = lineStart; + skipComment(); + found = lineNumber !== line; + index = pos; + lineNumber = line; + lineStart = start; + + return found; + } + + // Throw an exception + + function throwError(token, messageFormat) { + var error, + args = Array.prototype.slice.call(arguments, 2), + msg = messageFormat.replace( + /%(\d)/g, + function (whole, index) { + assert(index < args.length, 'Message reference must be in range'); + return args[index]; + } + ); + + if (typeof token.lineNumber === 'number') { + error = new Error('Line ' + token.lineNumber + ': ' + msg); + error.index = token.start; + error.lineNumber = token.lineNumber; + error.column = token.start - lineStart + 1; + } else { + error = new Error('Line ' + lineNumber + ': ' + msg); + error.index = index; + error.lineNumber = lineNumber; + error.column = index - lineStart + 1; + } + + error.description = msg; + throw error; + } + + function throwErrorTolerant() { + try { + throwError.apply(null, arguments); + } catch (e) { + if (extra.errors) { + extra.errors.push(e); + } else { + throw e; + } + } + } + + + // Throw an exception because of the token. + + function throwUnexpected(token) { + if (token.type === Token.EOF) { + throwError(token, Messages.UnexpectedEOS); + } + + if (token.type === Token.NumericLiteral) { + throwError(token, Messages.UnexpectedNumber); + } + + if (token.type === Token.StringLiteral) { + throwError(token, Messages.UnexpectedString); + } + + if (token.type === Token.Identifier) { + throwError(token, Messages.UnexpectedIdentifier); + } + + if (token.type === Token.Keyword) { + if (isFutureReservedWord(token.value)) { + throwError(token, Messages.UnexpectedReserved); + } else if (strict && isStrictModeReservedWord(token.value)) { + throwErrorTolerant(token, Messages.StrictReservedWord); + return; + } + throwError(token, Messages.UnexpectedToken, token.value); + } + + // BooleanLiteral, NullLiteral, or Punctuator. + throwError(token, Messages.UnexpectedToken, token.value); + } + + // Expect the next token to match the specified punctuator. + // If not, an exception will be thrown. + + function expect(value) { + var token = lex(); + if (token.type !== Token.Punctuator || token.value !== value) { + throwUnexpected(token); + } + } + + // Expect the next token to match the specified keyword. + // If not, an exception will be thrown. + + function expectKeyword(keyword) { + var token = lex(); + if (token.type !== Token.Keyword || token.value !== keyword) { + throwUnexpected(token); + } + } + + // Return true if the next token matches the specified punctuator. + + function match(value) { + return lookahead.type === Token.Punctuator && lookahead.value === value; + } + + // Return true if the next token matches the specified keyword + + function matchKeyword(keyword) { + return lookahead.type === Token.Keyword && lookahead.value === keyword; + } + + // Return true if the next token is an assignment operator + + function matchAssign() { + var op; + + if (lookahead.type !== Token.Punctuator) { + return false; + } + op = lookahead.value; + return op === '=' || + op === '*=' || + op === '/=' || + op === '%=' || + op === '+=' || + op === '-=' || + op === '<<=' || + op === '>>=' || + op === '>>>=' || + op === '&=' || + op === '^=' || + op === '|='; + } + + function consumeSemicolon() { + var line; + + // Catch the very common case first: immediately a semicolon (U+003B). + if (source.charCodeAt(index) === 0x3B || match(';')) { + lex(); + return; + } + + line = lineNumber; + skipComment(); + if (lineNumber !== line) { + return; + } + + if (lookahead.type !== Token.EOF && !match('}')) { + throwUnexpected(lookahead); + } + } + + // Return true if provided expression is LeftHandSideExpression + + function isLeftHandSide(expr) { + return expr.type === Syntax.Identifier || expr.type === Syntax.MemberExpression; + } + + // 11.1.4 Array Initialiser + + function parseArrayInitialiser() { + var elements = [], startToken; + + startToken = lookahead; + expect('['); + + while (!match(']')) { + if (match(',')) { + lex(); + elements.push(null); + } else { + elements.push(parseAssignmentExpression()); + + if (!match(']')) { + expect(','); + } + } + } + + lex(); + + return delegate.markEnd(delegate.createArrayExpression(elements), startToken); + } + + // 11.1.5 Object Initialiser + + function parsePropertyFunction(param, first) { + var previousStrict, body, startToken; + + previousStrict = strict; + startToken = lookahead; + body = parseFunctionSourceElements(); + if (first && strict && isRestrictedWord(param[0].name)) { + throwErrorTolerant(first, Messages.StrictParamName); + } + strict = previousStrict; + return delegate.markEnd(delegate.createFunctionExpression(null, param, [], body), startToken); + } + + function parseObjectPropertyKey() { + var token, startToken; + + startToken = lookahead; + token = lex(); + + // Note: This function is called only from parseObjectProperty(), where + // EOF and Punctuator tokens are already filtered out. + + if (token.type === Token.StringLiteral || token.type === Token.NumericLiteral) { + if (strict && token.octal) { + throwErrorTolerant(token, Messages.StrictOctalLiteral); + } + return delegate.markEnd(delegate.createLiteral(token), startToken); + } + + return delegate.markEnd(delegate.createIdentifier(token.value), startToken); + } + + function parseObjectProperty() { + var token, key, id, value, param, startToken; + + token = lookahead; + startToken = lookahead; + + if (token.type === Token.Identifier) { + + id = parseObjectPropertyKey(); + + // Property Assignment: Getter and Setter. + + if (token.value === 'get' && !match(':')) { + key = parseObjectPropertyKey(); + expect('('); + expect(')'); + value = parsePropertyFunction([]); + return delegate.markEnd(delegate.createProperty('get', key, value), startToken); + } + if (token.value === 'set' && !match(':')) { + key = parseObjectPropertyKey(); + expect('('); + token = lookahead; + if (token.type !== Token.Identifier) { + expect(')'); + throwErrorTolerant(token, Messages.UnexpectedToken, token.value); + value = parsePropertyFunction([]); + } else { + param = [ parseVariableIdentifier() ]; + expect(')'); + value = parsePropertyFunction(param, token); + } + return delegate.markEnd(delegate.createProperty('set', key, value), startToken); + } + expect(':'); + value = parseAssignmentExpression(); + return delegate.markEnd(delegate.createProperty('init', id, value), startToken); + } + if (token.type === Token.EOF || token.type === Token.Punctuator) { + throwUnexpected(token); + } else { + key = parseObjectPropertyKey(); + expect(':'); + value = parseAssignmentExpression(); + return delegate.markEnd(delegate.createProperty('init', key, value), startToken); + } + } + + function parseObjectInitialiser() { + var properties = [], property, name, key, kind, map = {}, toString = String, startToken; + + startToken = lookahead; + + expect('{'); + + while (!match('}')) { + property = parseObjectProperty(); + + if (property.key.type === Syntax.Identifier) { + name = property.key.name; + } else { + name = toString(property.key.value); + } + kind = (property.kind === 'init') ? PropertyKind.Data : (property.kind === 'get') ? PropertyKind.Get : PropertyKind.Set; + + key = '$' + name; + if (Object.prototype.hasOwnProperty.call(map, key)) { + if (map[key] === PropertyKind.Data) { + if (strict && kind === PropertyKind.Data) { + throwErrorTolerant({}, Messages.StrictDuplicateProperty); + } else if (kind !== PropertyKind.Data) { + throwErrorTolerant({}, Messages.AccessorDataProperty); + } + } else { + if (kind === PropertyKind.Data) { + throwErrorTolerant({}, Messages.AccessorDataProperty); + } else if (map[key] & kind) { + throwErrorTolerant({}, Messages.AccessorGetSet); + } + } + map[key] |= kind; + } else { + map[key] = kind; + } + + properties.push(property); + + if (!match('}')) { + expect(','); + } + } + + expect('}'); + + return delegate.markEnd(delegate.createObjectExpression(properties), startToken); + } + + // 11.1.6 The Grouping Operator + + function parseGroupExpression() { + var expr; + + expect('('); + + expr = parseExpression(); + + expect(')'); + + return expr; + } + + + // 11.1 Primary Expressions + + function parsePrimaryExpression() { + var type, token, expr, startToken; + + if (match('(')) { + return parseGroupExpression(); + } + + if (match('[')) { + return parseArrayInitialiser(); + } + + if (match('{')) { + return parseObjectInitialiser(); + } + + type = lookahead.type; + startToken = lookahead; + + if (type === Token.Identifier) { + expr = delegate.createIdentifier(lex().value); + } else if (type === Token.StringLiteral || type === Token.NumericLiteral) { + if (strict && lookahead.octal) { + throwErrorTolerant(lookahead, Messages.StrictOctalLiteral); + } + expr = delegate.createLiteral(lex()); + } else if (type === Token.Keyword) { + if (matchKeyword('function')) { + return parseFunctionExpression(); + } + if (matchKeyword('this')) { + lex(); + expr = delegate.createThisExpression(); + } else { + throwUnexpected(lex()); + } + } else if (type === Token.BooleanLiteral) { + token = lex(); + token.value = (token.value === 'true'); + expr = delegate.createLiteral(token); + } else if (type === Token.NullLiteral) { + token = lex(); + token.value = null; + expr = delegate.createLiteral(token); + } else if (match('/') || match('/=')) { + if (typeof extra.tokens !== 'undefined') { + expr = delegate.createLiteral(collectRegex()); + } else { + expr = delegate.createLiteral(scanRegExp()); + } + peek(); + } else { + throwUnexpected(lex()); + } + + return delegate.markEnd(expr, startToken); + } + + // 11.2 Left-Hand-Side Expressions + + function parseArguments() { + var args = []; + + expect('('); + + if (!match(')')) { + while (index < length) { + args.push(parseAssignmentExpression()); + if (match(')')) { + break; + } + expect(','); + } + } + + expect(')'); + + return args; + } + + function parseNonComputedProperty() { + var token, startToken; + + startToken = lookahead; + token = lex(); + + if (!isIdentifierName(token)) { + throwUnexpected(token); + } + + return delegate.markEnd(delegate.createIdentifier(token.value), startToken); + } + + function parseNonComputedMember() { + expect('.'); + + return parseNonComputedProperty(); + } + + function parseComputedMember() { + var expr; + + expect('['); + + expr = parseExpression(); + + expect(']'); + + return expr; + } + + function parseNewExpression() { + var callee, args, startToken; + + startToken = lookahead; + expectKeyword('new'); + callee = parseLeftHandSideExpression(); + args = match('(') ? parseArguments() : []; + + return delegate.markEnd(delegate.createNewExpression(callee, args), startToken); + } + + function parseLeftHandSideExpressionAllowCall() { + var previousAllowIn, expr, args, property, startToken; + + startToken = lookahead; + + previousAllowIn = state.allowIn; + state.allowIn = true; + expr = matchKeyword('new') ? parseNewExpression() : parsePrimaryExpression(); + state.allowIn = previousAllowIn; + + for (;;) { + if (match('.')) { + property = parseNonComputedMember(); + expr = delegate.createMemberExpression('.', expr, property); + } else if (match('(')) { + args = parseArguments(); + expr = delegate.createCallExpression(expr, args); + } else if (match('[')) { + property = parseComputedMember(); + expr = delegate.createMemberExpression('[', expr, property); + } else { + break; + } + delegate.markEnd(expr, startToken); + } + + return expr; + } + + function parseLeftHandSideExpression() { + var previousAllowIn, expr, property, startToken; + + startToken = lookahead; + + previousAllowIn = state.allowIn; + expr = matchKeyword('new') ? parseNewExpression() : parsePrimaryExpression(); + state.allowIn = previousAllowIn; + + while (match('.') || match('[')) { + if (match('[')) { + property = parseComputedMember(); + expr = delegate.createMemberExpression('[', expr, property); + } else { + property = parseNonComputedMember(); + expr = delegate.createMemberExpression('.', expr, property); + } + delegate.markEnd(expr, startToken); + } + + return expr; + } + + // 11.3 Postfix Expressions + + function parsePostfixExpression() { + var expr, token, startToken = lookahead; + + expr = parseLeftHandSideExpressionAllowCall(); + + if (lookahead.type === Token.Punctuator) { + if ((match('++') || match('--')) && !peekLineTerminator()) { + // 11.3.1, 11.3.2 + if (strict && expr.type === Syntax.Identifier && isRestrictedWord(expr.name)) { + throwErrorTolerant({}, Messages.StrictLHSPostfix); + } + + if (!isLeftHandSide(expr)) { + throwErrorTolerant({}, Messages.InvalidLHSInAssignment); + } + + token = lex(); + expr = delegate.markEnd(delegate.createPostfixExpression(token.value, expr), startToken); + } + } + + return expr; + } + + // 11.4 Unary Operators + + function parseUnaryExpression() { + var token, expr, startToken; + + if (lookahead.type !== Token.Punctuator && lookahead.type !== Token.Keyword) { + expr = parsePostfixExpression(); + } else if (match('++') || match('--')) { + startToken = lookahead; + token = lex(); + expr = parseUnaryExpression(); + // 11.4.4, 11.4.5 + if (strict && expr.type === Syntax.Identifier && isRestrictedWord(expr.name)) { + throwErrorTolerant({}, Messages.StrictLHSPrefix); + } + + if (!isLeftHandSide(expr)) { + throwErrorTolerant({}, Messages.InvalidLHSInAssignment); + } + + expr = delegate.createUnaryExpression(token.value, expr); + expr = delegate.markEnd(expr, startToken); + } else if (match('+') || match('-') || match('~') || match('!')) { + startToken = lookahead; + token = lex(); + expr = parseUnaryExpression(); + expr = delegate.createUnaryExpression(token.value, expr); + expr = delegate.markEnd(expr, startToken); + } else if (matchKeyword('delete') || matchKeyword('void') || matchKeyword('typeof')) { + startToken = lookahead; + token = lex(); + expr = parseUnaryExpression(); + expr = delegate.createUnaryExpression(token.value, expr); + expr = delegate.markEnd(expr, startToken); + if (strict && expr.operator === 'delete' && expr.argument.type === Syntax.Identifier) { + throwErrorTolerant({}, Messages.StrictDelete); + } + } else { + expr = parsePostfixExpression(); + } + + return expr; + } + + function binaryPrecedence(token, allowIn) { + var prec = 0; + + if (token.type !== Token.Punctuator && token.type !== Token.Keyword) { + return 0; + } + + switch (token.value) { + case '||': + prec = 1; + break; + + case '&&': + prec = 2; + break; + + case '|': + prec = 3; + break; + + case '^': + prec = 4; + break; + + case '&': + prec = 5; + break; + + case '==': + case '!=': + case '===': + case '!==': + prec = 6; + break; + + case '<': + case '>': + case '<=': + case '>=': + case 'instanceof': + prec = 7; + break; + + case 'in': + prec = allowIn ? 7 : 0; + break; + + case '<<': + case '>>': + case '>>>': + prec = 8; + break; + + case '+': + case '-': + prec = 9; + break; + + case '*': + case '/': + case '%': + prec = 11; + break; + + default: + break; + } + + return prec; + } + + // 11.5 Multiplicative Operators + // 11.6 Additive Operators + // 11.7 Bitwise Shift Operators + // 11.8 Relational Operators + // 11.9 Equality Operators + // 11.10 Binary Bitwise Operators + // 11.11 Binary Logical Operators + + function parseBinaryExpression() { + var marker, markers, expr, token, prec, stack, right, operator, left, i; + + marker = lookahead; + left = parseUnaryExpression(); + + token = lookahead; + prec = binaryPrecedence(token, state.allowIn); + if (prec === 0) { + return left; + } + token.prec = prec; + lex(); + + markers = [marker, lookahead]; + right = parseUnaryExpression(); + + stack = [left, token, right]; + + while ((prec = binaryPrecedence(lookahead, state.allowIn)) > 0) { + + // Reduce: make a binary expression from the three topmost entries. + while ((stack.length > 2) && (prec <= stack[stack.length - 2].prec)) { + right = stack.pop(); + operator = stack.pop().value; + left = stack.pop(); + expr = delegate.createBinaryExpression(operator, left, right); + markers.pop(); + marker = markers[markers.length - 1]; + delegate.markEnd(expr, marker); + stack.push(expr); + } + + // Shift. + token = lex(); + token.prec = prec; + stack.push(token); + markers.push(lookahead); + expr = parseUnaryExpression(); + stack.push(expr); + } + + // Final reduce to clean-up the stack. + i = stack.length - 1; + expr = stack[i]; + markers.pop(); + while (i > 1) { + expr = delegate.createBinaryExpression(stack[i - 1].value, stack[i - 2], expr); + i -= 2; + marker = markers.pop(); + delegate.markEnd(expr, marker); + } + + return expr; + } + + + // 11.12 Conditional Operator + + function parseConditionalExpression() { + var expr, previousAllowIn, consequent, alternate, startToken; + + startToken = lookahead; + + expr = parseBinaryExpression(); + + if (match('?')) { + lex(); + previousAllowIn = state.allowIn; + state.allowIn = true; + consequent = parseAssignmentExpression(); + state.allowIn = previousAllowIn; + expect(':'); + alternate = parseAssignmentExpression(); + + expr = delegate.createConditionalExpression(expr, consequent, alternate); + delegate.markEnd(expr, startToken); + } + + return expr; + } + + // 11.13 Assignment Operators + + function parseAssignmentExpression() { + var token, left, right, node, startToken; + + token = lookahead; + startToken = lookahead; + + node = left = parseConditionalExpression(); + + if (matchAssign()) { + // LeftHandSideExpression + if (!isLeftHandSide(left)) { + throwErrorTolerant({}, Messages.InvalidLHSInAssignment); + } + + // 11.13.1 + if (strict && left.type === Syntax.Identifier && isRestrictedWord(left.name)) { + throwErrorTolerant(token, Messages.StrictLHSAssignment); + } + + token = lex(); + right = parseAssignmentExpression(); + node = delegate.markEnd(delegate.createAssignmentExpression(token.value, left, right), startToken); + } + + return node; + } + + // 11.14 Comma Operator + + function parseExpression() { + var expr, startToken = lookahead; + + expr = parseAssignmentExpression(); + + if (match(',')) { + expr = delegate.createSequenceExpression([ expr ]); + + while (index < length) { + if (!match(',')) { + break; + } + lex(); + expr.expressions.push(parseAssignmentExpression()); + } + + delegate.markEnd(expr, startToken); + } + + return expr; + } + + // 12.1 Block + + function parseStatementList() { + var list = [], + statement; + + while (index < length) { + if (match('}')) { + break; + } + statement = parseSourceElement(); + if (typeof statement === 'undefined') { + break; + } + list.push(statement); + } + + return list; + } + + function parseBlock() { + var block, startToken; + + startToken = lookahead; + expect('{'); + + block = parseStatementList(); + + expect('}'); + + return delegate.markEnd(delegate.createBlockStatement(block), startToken); + } + + // 12.2 Variable Statement + + function parseVariableIdentifier() { + var token, startToken; + + startToken = lookahead; + token = lex(); + + if (token.type !== Token.Identifier) { + throwUnexpected(token); + } + + return delegate.markEnd(delegate.createIdentifier(token.value), startToken); + } + + function parseVariableDeclaration(kind) { + var init = null, id, startToken; + + startToken = lookahead; + id = parseVariableIdentifier(); + + // 12.2.1 + if (strict && isRestrictedWord(id.name)) { + throwErrorTolerant({}, Messages.StrictVarName); + } + + if (kind === 'const') { + expect('='); + init = parseAssignmentExpression(); + } else if (match('=')) { + lex(); + init = parseAssignmentExpression(); + } + + return delegate.markEnd(delegate.createVariableDeclarator(id, init), startToken); + } + + function parseVariableDeclarationList(kind) { + var list = []; + + do { + list.push(parseVariableDeclaration(kind)); + if (!match(',')) { + break; + } + lex(); + } while (index < length); + + return list; + } + + function parseVariableStatement() { + var declarations; + + expectKeyword('var'); + + declarations = parseVariableDeclarationList(); + + consumeSemicolon(); + + return delegate.createVariableDeclaration(declarations, 'var'); + } + + // kind may be `const` or `let` + // Both are experimental and not in the specification yet. + // see http://wiki.ecmascript.org/doku.php?id=harmony:const + // and http://wiki.ecmascript.org/doku.php?id=harmony:let + function parseConstLetDeclaration(kind) { + var declarations, startToken; + + startToken = lookahead; + + expectKeyword(kind); + + declarations = parseVariableDeclarationList(kind); + + consumeSemicolon(); + + return delegate.markEnd(delegate.createVariableDeclaration(declarations, kind), startToken); + } + + // 12.3 Empty Statement + + function parseEmptyStatement() { + expect(';'); + return delegate.createEmptyStatement(); + } + + // 12.4 Expression Statement + + function parseExpressionStatement() { + var expr = parseExpression(); + consumeSemicolon(); + return delegate.createExpressionStatement(expr); + } + + // 12.5 If statement + + function parseIfStatement() { + var test, consequent, alternate; + + expectKeyword('if'); + + expect('('); + + test = parseExpression(); + + expect(')'); + + consequent = parseStatement(); + + if (matchKeyword('else')) { + lex(); + alternate = parseStatement(); + } else { + alternate = null; + } + + return delegate.createIfStatement(test, consequent, alternate); + } + + // 12.6 Iteration Statements + + function parseDoWhileStatement() { + var body, test, oldInIteration; + + expectKeyword('do'); + + oldInIteration = state.inIteration; + state.inIteration = true; + + body = parseStatement(); + + state.inIteration = oldInIteration; + + expectKeyword('while'); + + expect('('); + + test = parseExpression(); + + expect(')'); + + if (match(';')) { + lex(); + } + + return delegate.createDoWhileStatement(body, test); + } + + function parseWhileStatement() { + var test, body, oldInIteration; + + expectKeyword('while'); + + expect('('); + + test = parseExpression(); + + expect(')'); + + oldInIteration = state.inIteration; + state.inIteration = true; + + body = parseStatement(); + + state.inIteration = oldInIteration; + + return delegate.createWhileStatement(test, body); + } + + function parseForVariableDeclaration() { + var token, declarations, startToken; + + startToken = lookahead; + token = lex(); + declarations = parseVariableDeclarationList(); + + return delegate.markEnd(delegate.createVariableDeclaration(declarations, token.value), startToken); + } + + function parseForStatement() { + var init, test, update, left, right, body, oldInIteration; + + init = test = update = null; + + expectKeyword('for'); + + expect('('); + + if (match(';')) { + lex(); + } else { + if (matchKeyword('var') || matchKeyword('let')) { + state.allowIn = false; + init = parseForVariableDeclaration(); + state.allowIn = true; + + if (init.declarations.length === 1 && matchKeyword('in')) { + lex(); + left = init; + right = parseExpression(); + init = null; + } + } else { + state.allowIn = false; + init = parseExpression(); + state.allowIn = true; + + if (matchKeyword('in')) { + // LeftHandSideExpression + if (!isLeftHandSide(init)) { + throwErrorTolerant({}, Messages.InvalidLHSInForIn); + } + + lex(); + left = init; + right = parseExpression(); + init = null; + } + } + + if (typeof left === 'undefined') { + expect(';'); + } + } + + if (typeof left === 'undefined') { + + if (!match(';')) { + test = parseExpression(); + } + expect(';'); + + if (!match(')')) { + update = parseExpression(); + } + } + + expect(')'); + + oldInIteration = state.inIteration; + state.inIteration = true; + + body = parseStatement(); + + state.inIteration = oldInIteration; + + return (typeof left === 'undefined') ? + delegate.createForStatement(init, test, update, body) : + delegate.createForInStatement(left, right, body); + } + + // 12.7 The continue statement + + function parseContinueStatement() { + var label = null, key; + + expectKeyword('continue'); + + // Optimize the most common form: 'continue;'. + if (source.charCodeAt(index) === 0x3B) { + lex(); + + if (!state.inIteration) { + throwError({}, Messages.IllegalContinue); + } + + return delegate.createContinueStatement(null); + } + + if (peekLineTerminator()) { + if (!state.inIteration) { + throwError({}, Messages.IllegalContinue); + } + + return delegate.createContinueStatement(null); + } + + if (lookahead.type === Token.Identifier) { + label = parseVariableIdentifier(); + + key = '$' + label.name; + if (!Object.prototype.hasOwnProperty.call(state.labelSet, key)) { + throwError({}, Messages.UnknownLabel, label.name); + } + } + + consumeSemicolon(); + + if (label === null && !state.inIteration) { + throwError({}, Messages.IllegalContinue); + } + + return delegate.createContinueStatement(label); + } + + // 12.8 The break statement + + function parseBreakStatement() { + var label = null, key; + + expectKeyword('break'); + + // Catch the very common case first: immediately a semicolon (U+003B). + if (source.charCodeAt(index) === 0x3B) { + lex(); + + if (!(state.inIteration || state.inSwitch)) { + throwError({}, Messages.IllegalBreak); + } + + return delegate.createBreakStatement(null); + } + + if (peekLineTerminator()) { + if (!(state.inIteration || state.inSwitch)) { + throwError({}, Messages.IllegalBreak); + } + + return delegate.createBreakStatement(null); + } + + if (lookahead.type === Token.Identifier) { + label = parseVariableIdentifier(); + + key = '$' + label.name; + if (!Object.prototype.hasOwnProperty.call(state.labelSet, key)) { + throwError({}, Messages.UnknownLabel, label.name); + } + } + + consumeSemicolon(); + + if (label === null && !(state.inIteration || state.inSwitch)) { + throwError({}, Messages.IllegalBreak); + } + + return delegate.createBreakStatement(label); + } + + // 12.9 The return statement + + function parseReturnStatement() { + var argument = null; + + expectKeyword('return'); + + if (!state.inFunctionBody) { + throwErrorTolerant({}, Messages.IllegalReturn); + } + + // 'return' followed by a space and an identifier is very common. + if (source.charCodeAt(index) === 0x20) { + if (isIdentifierStart(source.charCodeAt(index + 1))) { + argument = parseExpression(); + consumeSemicolon(); + return delegate.createReturnStatement(argument); + } + } + + if (peekLineTerminator()) { + return delegate.createReturnStatement(null); + } + + if (!match(';')) { + if (!match('}') && lookahead.type !== Token.EOF) { + argument = parseExpression(); + } + } + + consumeSemicolon(); + + return delegate.createReturnStatement(argument); + } + + // 12.10 The with statement + + function parseWithStatement() { + var object, body; + + if (strict) { + // TODO(ikarienator): Should we update the test cases instead? + skipComment(); + throwErrorTolerant({}, Messages.StrictModeWith); + } + + expectKeyword('with'); + + expect('('); + + object = parseExpression(); + + expect(')'); + + body = parseStatement(); + + return delegate.createWithStatement(object, body); + } + + // 12.10 The swith statement + + function parseSwitchCase() { + var test, consequent = [], statement, startToken; + + startToken = lookahead; + if (matchKeyword('default')) { + lex(); + test = null; + } else { + expectKeyword('case'); + test = parseExpression(); + } + expect(':'); + + while (index < length) { + if (match('}') || matchKeyword('default') || matchKeyword('case')) { + break; + } + statement = parseStatement(); + consequent.push(statement); + } + + return delegate.markEnd(delegate.createSwitchCase(test, consequent), startToken); + } + + function parseSwitchStatement() { + var discriminant, cases, clause, oldInSwitch, defaultFound; + + expectKeyword('switch'); + + expect('('); + + discriminant = parseExpression(); + + expect(')'); + + expect('{'); + + cases = []; + + if (match('}')) { + lex(); + return delegate.createSwitchStatement(discriminant, cases); + } + + oldInSwitch = state.inSwitch; + state.inSwitch = true; + defaultFound = false; + + while (index < length) { + if (match('}')) { + break; + } + clause = parseSwitchCase(); + if (clause.test === null) { + if (defaultFound) { + throwError({}, Messages.MultipleDefaultsInSwitch); + } + defaultFound = true; + } + cases.push(clause); + } + + state.inSwitch = oldInSwitch; + + expect('}'); + + return delegate.createSwitchStatement(discriminant, cases); + } + + // 12.13 The throw statement + + function parseThrowStatement() { + var argument; + + expectKeyword('throw'); + + if (peekLineTerminator()) { + throwError({}, Messages.NewlineAfterThrow); + } + + argument = parseExpression(); + + consumeSemicolon(); + + return delegate.createThrowStatement(argument); + } + + // 12.14 The try statement + + function parseCatchClause() { + var param, body, startToken; + + startToken = lookahead; + expectKeyword('catch'); + + expect('('); + if (match(')')) { + throwUnexpected(lookahead); + } + + param = parseVariableIdentifier(); + // 12.14.1 + if (strict && isRestrictedWord(param.name)) { + throwErrorTolerant({}, Messages.StrictCatchVariable); + } + + expect(')'); + body = parseBlock(); + return delegate.markEnd(delegate.createCatchClause(param, body), startToken); + } + + function parseTryStatement() { + var block, handlers = [], finalizer = null; + + expectKeyword('try'); + + block = parseBlock(); + + if (matchKeyword('catch')) { + handlers.push(parseCatchClause()); + } + + if (matchKeyword('finally')) { + lex(); + finalizer = parseBlock(); + } + + if (handlers.length === 0 && !finalizer) { + throwError({}, Messages.NoCatchOrFinally); + } + + return delegate.createTryStatement(block, [], handlers, finalizer); + } + + // 12.15 The debugger statement + + function parseDebuggerStatement() { + expectKeyword('debugger'); + + consumeSemicolon(); + + return delegate.createDebuggerStatement(); + } + + // 12 Statements + + function parseStatement() { + var type = lookahead.type, + expr, + labeledBody, + key, + startToken; + + if (type === Token.EOF) { + throwUnexpected(lookahead); + } + + if (type === Token.Punctuator && lookahead.value === '{') { + return parseBlock(); + } + + startToken = lookahead; + + if (type === Token.Punctuator) { + switch (lookahead.value) { + case ';': + return delegate.markEnd(parseEmptyStatement(), startToken); + case '(': + return delegate.markEnd(parseExpressionStatement(), startToken); + default: + break; + } + } + + if (type === Token.Keyword) { + switch (lookahead.value) { + case 'break': + return delegate.markEnd(parseBreakStatement(), startToken); + case 'continue': + return delegate.markEnd(parseContinueStatement(), startToken); + case 'debugger': + return delegate.markEnd(parseDebuggerStatement(), startToken); + case 'do': + return delegate.markEnd(parseDoWhileStatement(), startToken); + case 'for': + return delegate.markEnd(parseForStatement(), startToken); + case 'function': + return delegate.markEnd(parseFunctionDeclaration(), startToken); + case 'if': + return delegate.markEnd(parseIfStatement(), startToken); + case 'return': + return delegate.markEnd(parseReturnStatement(), startToken); + case 'switch': + return delegate.markEnd(parseSwitchStatement(), startToken); + case 'throw': + return delegate.markEnd(parseThrowStatement(), startToken); + case 'try': + return delegate.markEnd(parseTryStatement(), startToken); + case 'var': + return delegate.markEnd(parseVariableStatement(), startToken); + case 'while': + return delegate.markEnd(parseWhileStatement(), startToken); + case 'with': + return delegate.markEnd(parseWithStatement(), startToken); + default: + break; + } + } + + expr = parseExpression(); + + // 12.12 Labelled Statements + if ((expr.type === Syntax.Identifier) && match(':')) { + lex(); + + key = '$' + expr.name; + if (Object.prototype.hasOwnProperty.call(state.labelSet, key)) { + throwError({}, Messages.Redeclaration, 'Label', expr.name); + } + + state.labelSet[key] = true; + labeledBody = parseStatement(); + delete state.labelSet[key]; + return delegate.markEnd(delegate.createLabeledStatement(expr, labeledBody), startToken); + } + + consumeSemicolon(); + + return delegate.markEnd(delegate.createExpressionStatement(expr), startToken); + } + + // 13 Function Definition + + function parseFunctionSourceElements() { + var sourceElement, sourceElements = [], token, directive, firstRestricted, + oldLabelSet, oldInIteration, oldInSwitch, oldInFunctionBody, startToken; + + startToken = lookahead; + expect('{'); + + while (index < length) { + if (lookahead.type !== Token.StringLiteral) { + break; + } + token = lookahead; + + sourceElement = parseSourceElement(); + sourceElements.push(sourceElement); + if (sourceElement.expression.type !== Syntax.Literal) { + // this is not directive + break; + } + directive = source.slice(token.start + 1, token.end - 1); + if (directive === 'use strict') { + strict = true; + if (firstRestricted) { + throwErrorTolerant(firstRestricted, Messages.StrictOctalLiteral); + } + } else { + if (!firstRestricted && token.octal) { + firstRestricted = token; + } + } + } + + oldLabelSet = state.labelSet; + oldInIteration = state.inIteration; + oldInSwitch = state.inSwitch; + oldInFunctionBody = state.inFunctionBody; + + state.labelSet = {}; + state.inIteration = false; + state.inSwitch = false; + state.inFunctionBody = true; + + while (index < length) { + if (match('}')) { + break; + } + sourceElement = parseSourceElement(); + if (typeof sourceElement === 'undefined') { + break; + } + sourceElements.push(sourceElement); + } + + expect('}'); + + state.labelSet = oldLabelSet; + state.inIteration = oldInIteration; + state.inSwitch = oldInSwitch; + state.inFunctionBody = oldInFunctionBody; + + return delegate.markEnd(delegate.createBlockStatement(sourceElements), startToken); + } + + function parseParams(firstRestricted) { + var param, params = [], token, stricted, paramSet, key, message; + expect('('); + + if (!match(')')) { + paramSet = {}; + while (index < length) { + token = lookahead; + param = parseVariableIdentifier(); + key = '$' + token.value; + if (strict) { + if (isRestrictedWord(token.value)) { + stricted = token; + message = Messages.StrictParamName; + } + if (Object.prototype.hasOwnProperty.call(paramSet, key)) { + stricted = token; + message = Messages.StrictParamDupe; + } + } else if (!firstRestricted) { + if (isRestrictedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictParamName; + } else if (isStrictModeReservedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictReservedWord; + } else if (Object.prototype.hasOwnProperty.call(paramSet, key)) { + firstRestricted = token; + message = Messages.StrictParamDupe; + } + } + params.push(param); + paramSet[key] = true; + if (match(')')) { + break; + } + expect(','); + } + } + + expect(')'); + + return { + params: params, + stricted: stricted, + firstRestricted: firstRestricted, + message: message + }; + } + + function parseFunctionDeclaration() { + var id, params = [], body, token, stricted, tmp, firstRestricted, message, previousStrict, startToken; + + startToken = lookahead; + + expectKeyword('function'); + token = lookahead; + id = parseVariableIdentifier(); + if (strict) { + if (isRestrictedWord(token.value)) { + throwErrorTolerant(token, Messages.StrictFunctionName); + } + } else { + if (isRestrictedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictFunctionName; + } else if (isStrictModeReservedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictReservedWord; + } + } + + tmp = parseParams(firstRestricted); + params = tmp.params; + stricted = tmp.stricted; + firstRestricted = tmp.firstRestricted; + if (tmp.message) { + message = tmp.message; + } + + previousStrict = strict; + body = parseFunctionSourceElements(); + if (strict && firstRestricted) { + throwError(firstRestricted, message); + } + if (strict && stricted) { + throwErrorTolerant(stricted, message); + } + strict = previousStrict; + + return delegate.markEnd(delegate.createFunctionDeclaration(id, params, [], body), startToken); + } + + function parseFunctionExpression() { + var token, id = null, stricted, firstRestricted, message, tmp, params = [], body, previousStrict, startToken; + + startToken = lookahead; + expectKeyword('function'); + + if (!match('(')) { + token = lookahead; + id = parseVariableIdentifier(); + if (strict) { + if (isRestrictedWord(token.value)) { + throwErrorTolerant(token, Messages.StrictFunctionName); + } + } else { + if (isRestrictedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictFunctionName; + } else if (isStrictModeReservedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictReservedWord; + } + } + } + + tmp = parseParams(firstRestricted); + params = tmp.params; + stricted = tmp.stricted; + firstRestricted = tmp.firstRestricted; + if (tmp.message) { + message = tmp.message; + } + + previousStrict = strict; + body = parseFunctionSourceElements(); + if (strict && firstRestricted) { + throwError(firstRestricted, message); + } + if (strict && stricted) { + throwErrorTolerant(stricted, message); + } + strict = previousStrict; + + return delegate.markEnd(delegate.createFunctionExpression(id, params, [], body), startToken); + } + + // 14 Program + + function parseSourceElement() { + if (lookahead.type === Token.Keyword) { + switch (lookahead.value) { + case 'const': + case 'let': + return parseConstLetDeclaration(lookahead.value); + case 'function': + return parseFunctionDeclaration(); + default: + return parseStatement(); + } + } + + if (lookahead.type !== Token.EOF) { + return parseStatement(); + } + } + + function parseSourceElements() { + var sourceElement, sourceElements = [], token, directive, firstRestricted; + + while (index < length) { + token = lookahead; + if (token.type !== Token.StringLiteral) { + break; + } + + sourceElement = parseSourceElement(); + sourceElements.push(sourceElement); + if (sourceElement.expression.type !== Syntax.Literal) { + // this is not directive + break; + } + directive = source.slice(token.start + 1, token.end - 1); + if (directive === 'use strict') { + strict = true; + if (firstRestricted) { + throwErrorTolerant(firstRestricted, Messages.StrictOctalLiteral); + } + } else { + if (!firstRestricted && token.octal) { + firstRestricted = token; + } + } + } + + while (index < length) { + sourceElement = parseSourceElement(); + /* istanbul ignore if */ + if (typeof sourceElement === 'undefined') { + break; + } + sourceElements.push(sourceElement); + } + return sourceElements; + } + + function parseProgram() { + var body, startToken; + + skipComment(); + peek(); + startToken = lookahead; + strict = false; + + body = parseSourceElements(); + return delegate.markEnd(delegate.createProgram(body), startToken); + } + + function filterTokenLocation() { + var i, entry, token, tokens = []; + + for (i = 0; i < extra.tokens.length; ++i) { + entry = extra.tokens[i]; + token = { + type: entry.type, + value: entry.value + }; + if (extra.range) { + token.range = entry.range; + } + if (extra.loc) { + token.loc = entry.loc; + } + tokens.push(token); + } + + extra.tokens = tokens; + } + + function tokenize(code, options) { + var toString, + token, + tokens; + + toString = String; + if (typeof code !== 'string' && !(code instanceof String)) { + code = toString(code); + } + + delegate = SyntaxTreeDelegate; + source = code; + index = 0; + lineNumber = (source.length > 0) ? 1 : 0; + lineStart = 0; + length = source.length; + lookahead = null; + state = { + allowIn: true, + labelSet: {}, + inFunctionBody: false, + inIteration: false, + inSwitch: false, + lastCommentStart: -1 + }; + + extra = {}; + + // Options matching. + options = options || {}; + + // Of course we collect tokens here. + options.tokens = true; + extra.tokens = []; + extra.tokenize = true; + // The following two fields are necessary to compute the Regex tokens. + extra.openParenToken = -1; + extra.openCurlyToken = -1; + + extra.range = (typeof options.range === 'boolean') && options.range; + extra.loc = (typeof options.loc === 'boolean') && options.loc; + + if (typeof options.comment === 'boolean' && options.comment) { + extra.comments = []; + } + if (typeof options.tolerant === 'boolean' && options.tolerant) { + extra.errors = []; + } + + try { + peek(); + if (lookahead.type === Token.EOF) { + return extra.tokens; + } + + token = lex(); + while (lookahead.type !== Token.EOF) { + try { + token = lex(); + } catch (lexError) { + token = lookahead; + if (extra.errors) { + extra.errors.push(lexError); + // We have to break on the first error + // to avoid infinite loops. + break; + } else { + throw lexError; + } + } + } + + filterTokenLocation(); + tokens = extra.tokens; + if (typeof extra.comments !== 'undefined') { + tokens.comments = extra.comments; + } + if (typeof extra.errors !== 'undefined') { + tokens.errors = extra.errors; + } + } catch (e) { + throw e; + } finally { + extra = {}; + } + return tokens; + } + + function parse(code, options) { + var program, toString; + + toString = String; + if (typeof code !== 'string' && !(code instanceof String)) { + code = toString(code); + } + + delegate = SyntaxTreeDelegate; + source = code; + index = 0; + lineNumber = (source.length > 0) ? 1 : 0; + lineStart = 0; + length = source.length; + lookahead = null; + state = { + allowIn: true, + labelSet: {}, + inFunctionBody: false, + inIteration: false, + inSwitch: false, + lastCommentStart: -1 + }; + + extra = {}; + if (typeof options !== 'undefined') { + extra.range = (typeof options.range === 'boolean') && options.range; + extra.loc = (typeof options.loc === 'boolean') && options.loc; + extra.attachComment = (typeof options.attachComment === 'boolean') && options.attachComment; + + if (extra.loc && options.source !== null && options.source !== undefined) { + extra.source = toString(options.source); + } + + if (typeof options.tokens === 'boolean' && options.tokens) { + extra.tokens = []; + } + if (typeof options.comment === 'boolean' && options.comment) { + extra.comments = []; + } + if (typeof options.tolerant === 'boolean' && options.tolerant) { + extra.errors = []; + } + if (extra.attachComment) { + extra.range = true; + extra.comments = []; + extra.bottomRightStack = []; + extra.trailingComments = []; + extra.leadingComments = []; + } + } + + try { + program = parseProgram(); + if (typeof extra.comments !== 'undefined') { + program.comments = extra.comments; + } + if (typeof extra.tokens !== 'undefined') { + filterTokenLocation(); + program.tokens = extra.tokens; + } + if (typeof extra.errors !== 'undefined') { + program.errors = extra.errors; + } + } catch (e) { + throw e; + } finally { + extra = {}; + } + + return program; + } + + // Sync with *.json manifests. + exports.version = '1.2.2'; + + exports.tokenize = tokenize; + + exports.parse = parse; + + // Deep copy. + /* istanbul ignore next */ + exports.Syntax = (function () { + var name, types = {}; + + if (typeof Object.create === 'function') { + types = Object.create(null); + } + + for (name in Syntax) { + if (Syntax.hasOwnProperty(name)) { + types[name] = Syntax[name]; + } + } + + if (typeof Object.freeze === 'function') { + Object.freeze(types); + } + + return types; + }()); + +})); +/* vim: set sw=4 ts=4 et tw=80 : */ + +})(null); +/*! + * falafel (c) James Halliday / MIT License + * https://github.com/substack/node-falafel + */ + +(function(require,module){ +var parse = require('esprima').parse; +var objectKeys = Object.keys || function (obj) { + var keys = []; + for (var key in obj) keys.push(key); + return keys; +}; +var forEach = function (xs, fn) { + if (xs.forEach) return xs.forEach(fn); + for (var i = 0; i < xs.length; i++) { + fn.call(xs, xs[i], i, xs); + } +}; + +var isArray = Array.isArray || function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]'; +}; + +module.exports = function (src, opts, fn) { + if (typeof opts === 'function') { + fn = opts; + opts = {}; + } + if (typeof src === 'object') { + opts = src; + src = opts.source; + delete opts.source; + } + src = src === undefined ? opts.source : src; + opts.range = true; + if (typeof src !== 'string') src = String(src); + + var ast = parse(src, opts); + + var result = { + chunks : src.split(''), + toString : function () { return result.chunks.join('') }, + inspect : function () { return result.toString() } + }; + var index = 0; + + (function walk (node, parent) { + insertHelpers(node, parent, result.chunks); + + forEach(objectKeys(node), function (key) { + if (key === 'parent') return; + + var child = node[key]; + if (isArray(child)) { + forEach(child, function (c) { + if (c && typeof c.type === 'string') { + walk(c, node); + } + }); + } + else if (child && typeof child.type === 'string') { + insertHelpers(child, node, result.chunks); + walk(child, node); + } + }); + fn(node); + })(ast, undefined); + + return result; +}; + +function insertHelpers (node, parent, chunks) { + if (!node.range) return; + + node.parent = parent; + + node.source = function () { + return chunks.slice( + node.range[0], node.range[1] + ).join(''); + }; + + if (node.update && typeof node.update === 'object') { + var prev = node.update; + forEach(objectKeys(prev), function (key) { + update[key] = prev[key]; + }); + node.update = update; + } + else { + node.update = update; + } + + function update (s) { + chunks[node.range[0]] = s; + for (var i = node.range[0] + 1; i < node.range[1]; i++) { + chunks[i] = ''; + } + }; +} + +window.falafel = module.exports;})(function(){return {parse: esprima.parse};},{exports: {}}); +var inBrowser = typeof window !== 'undefined' && this === window; +var parseAndModify = (inBrowser ? window.falafel : require("falafel")); + +(inBrowser ? window : exports).blanket = (function(){ + var linesToAddTracking = [ + "ExpressionStatement", + "BreakStatement" , + "ContinueStatement" , + "VariableDeclaration", + "ReturnStatement" , + "ThrowStatement" , + "TryStatement" , + "FunctionDeclaration" , + "IfStatement" , + "WhileStatement" , + "DoWhileStatement" , + "ForStatement" , + "ForInStatement" , + "SwitchStatement" , + "WithStatement" + ], + linesToAddBrackets = [ + "IfStatement" , + "WhileStatement" , + "DoWhileStatement" , + "ForStatement" , + "ForInStatement" , + "WithStatement" + ], + __blanket, + copynumber = Math.floor(Math.random()*1000), + coverageInfo = {},options = { + reporter: null, + adapter:null, + filter: null, + customVariable: null, + loader: null, + ignoreScriptError: false, + existingRequireJS:false, + autoStart: false, + timeout: 3000, + ignoreCors: false, + branchTracking: false, + sourceURL: false, + debug:false, + engineOnly:false, + testReadyCallback:null, + commonJS:false, + instrumentCache:false, + modulePattern: null + }; + + if (inBrowser && typeof window.blanket !== 'undefined'){ + __blanket = window.blanket.noConflict(); + } + + _blanket = { + noConflict: function(){ + if (__blanket){ + return __blanket; + } + return _blanket; + }, + _getCopyNumber: function(){ + //internal method + //for differentiating between instances + return copynumber; + }, + extend: function(obj) { + //borrowed from underscore + _blanket._extend(_blanket,obj); + }, + _extend: function(dest,source){ + if (source) { + for (var prop in source) { + if ( dest[prop] instanceof Object && typeof dest[prop] !== "function"){ + _blanket._extend(dest[prop],source[prop]); + }else{ + dest[prop] = source[prop]; + } + } + } + }, + getCovVar: function(){ + var opt = _blanket.options("customVariable"); + if (opt){ + if (_blanket.options("debug")) {console.log("BLANKET-Using custom tracking variable:",opt);} + return inBrowser ? "window."+opt : opt; + } + return inBrowser ? "window._$blanket" : "_$jscoverage"; + }, + options: function(key,value){ + if (typeof key !== "string"){ + _blanket._extend(options,key); + }else if (typeof value === 'undefined'){ + return options[key]; + }else{ + options[key]=value; + } + }, + instrument: function(config, next){ + //check instrumented hash table, + //return instrumented code if available. + var inFile = config.inputFile, + inFileName = config.inputFileName; + //check instrument cache + if (_blanket.options("instrumentCache") && sessionStorage && sessionStorage.getItem("blanket_instrument_store-"+inFileName)){ + if (_blanket.options("debug")) {console.log("BLANKET-Reading instrumentation from cache: ",inFileName);} + next(sessionStorage.getItem("blanket_instrument_store-"+inFileName)); + }else{ + var sourceArray = _blanket._prepareSource(inFile); + _blanket._trackingArraySetup=[]; + //remove shebang + inFile = inFile.replace(/^\#\!.*/, ""); + var instrumented = parseAndModify(inFile,{loc:true,comment:true}, _blanket._addTracking(inFileName)); + instrumented = _blanket._trackingSetup(inFileName,sourceArray)+instrumented; + if (_blanket.options("sourceURL")){ + instrumented += "\n//@ sourceURL="+inFileName.replace("http://",""); + } + if (_blanket.options("debug")) {console.log("BLANKET-Instrumented file: ",inFileName);} + if (_blanket.options("instrumentCache") && sessionStorage){ + if (_blanket.options("debug")) {console.log("BLANKET-Saving instrumentation to cache: ",inFileName);} + sessionStorage.setItem("blanket_instrument_store-"+inFileName,instrumented); + } + next(instrumented); + } + }, + _trackingArraySetup: [], + _branchingArraySetup: [], + _prepareSource: function(source){ + return source.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/(\r\n|\n|\r)/gm,"\n").split('\n'); + }, + _trackingSetup: function(filename,sourceArray){ + var branches = _blanket.options("branchTracking"); + var sourceString = sourceArray.join("',\n'"); + var intro = ""; + var covVar = _blanket.getCovVar(); + + intro += "if (typeof "+covVar+" === 'undefined') "+covVar+" = {};\n"; + if (branches){ + intro += "var _$branchFcn=function(f,l,c,r){ "; + intro += "if (!!r) { "; + intro += covVar+"[f].branchData[l][c][0] = "+covVar+"[f].branchData[l][c][0] || [];"; + intro += covVar+"[f].branchData[l][c][0].push(r); }"; + intro += "else { "; + intro += covVar+"[f].branchData[l][c][1] = "+covVar+"[f].branchData[l][c][1] || [];"; + intro += covVar+"[f].branchData[l][c][1].push(r); }"; + intro += "return r;};\n"; + } + intro += "if (typeof "+covVar+"['"+filename+"'] === 'undefined'){"; + + intro += covVar+"['"+filename+"']=[];\n"; + if (branches){ + intro += covVar+"['"+filename+"'].branchData=[];\n"; + } + intro += covVar+"['"+filename+"'].source=['"+sourceString+"'];\n"; + //initialize array values + _blanket._trackingArraySetup.sort(function(a,b){ + return parseInt(a,10) > parseInt(b,10); + }).forEach(function(item){ + intro += covVar+"['"+filename+"']["+item+"]=0;\n"; + }); + if (branches){ + _blanket._branchingArraySetup.sort(function(a,b){ + return a.line > b.line; + }).sort(function(a,b){ + return a.column > b.column; + }).forEach(function(item){ + if (item.file === filename){ + intro += "if (typeof "+ covVar+"['"+filename+"'].branchData["+item.line+"] === 'undefined'){\n"; + intro += covVar+"['"+filename+"'].branchData["+item.line+"]=[];\n"; + intro += "}"; + intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"] = [];\n"; + intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"].consequent = "+JSON.stringify(item.consequent)+";\n"; + intro += covVar+"['"+filename+"'].branchData["+item.line+"]["+item.column+"].alternate = "+JSON.stringify(item.alternate)+";\n"; + } + }); + } + intro += "}"; + + return intro; + }, + _blockifyIf: function(node){ + if (linesToAddBrackets.indexOf(node.type) > -1){ + var bracketsExistObject = node.consequent || node.body; + var bracketsExistAlt = node.alternate; + if( bracketsExistAlt && bracketsExistAlt.type !== "BlockStatement") { + bracketsExistAlt.update("{\n"+bracketsExistAlt.source()+"}\n"); + } + if( bracketsExistObject && bracketsExistObject.type !== "BlockStatement") { + bracketsExistObject.update("{\n"+bracketsExistObject.source()+"}\n"); + } + } + }, + _trackBranch: function(node,filename){ + //recursive on consequent and alternative + var line = node.loc.start.line; + var col = node.loc.start.column; + + _blanket._branchingArraySetup.push({ + line: line, + column: col, + file:filename, + consequent: node.consequent.loc, + alternate: node.alternate.loc + }); + + var updated = "_$branchFcn"+ + "('"+filename+"',"+line+","+col+","+node.test.source()+ + ")?"+node.consequent.source()+":"+node.alternate.source(); + node.update(updated); + }, + _addTracking: function (filename) { + //falafel doesn't take a file name + //so we include the filename in a closure + //and return the function to falafel + var covVar = _blanket.getCovVar(); + + return function(node){ + _blanket._blockifyIf(node); + + if (linesToAddTracking.indexOf(node.type) > -1 && node.parent.type !== "LabeledStatement") { + _blanket._checkDefs(node,filename); + if (node.type === "VariableDeclaration" && + (node.parent.type === "ForStatement" || node.parent.type === "ForInStatement")){ + return; + } + if (node.loc && node.loc.start){ + node.update(covVar+"['"+filename+"']["+node.loc.start.line+"]++;\n"+node.source()); + _blanket._trackingArraySetup.push(node.loc.start.line); + }else{ + //I don't think we can handle a node with no location + throw new Error("The instrumenter encountered a node with no location: "+Object.keys(node)); + } + }else if (_blanket.options("branchTracking") && node.type === "ConditionalExpression"){ + _blanket._trackBranch(node,filename); + } + }; + }, + _checkDefs: function(node,filename){ + // Make sure developers don't redefine window. if they do, inform them it is wrong. + if (inBrowser){ + if (node.type === "VariableDeclaration" && node.declarations) { + node.declarations.forEach(function(declaration) { + if (declaration.id.name === "window") { + throw new Error("Instrumentation error, you cannot redefine the 'window' variable in " + filename + ":" + node.loc.start.line); + } + }); + } + if (node.type === "FunctionDeclaration" && node.params) { + node.params.forEach(function(param) { + if (param.name === "window") { + throw new Error("Instrumentation error, you cannot redefine the 'window' variable in " + filename + ":" + node.loc.start.line); + } + }); + } + //Make sure developers don't redefine the coverage variable + if (node.type === "ExpressionStatement" && + node.expression && node.expression.left && + node.expression.left.object && node.expression.left.property && + node.expression.left.object.name + + "." + node.expression.left.property.name === _blanket.getCovVar()) { + throw new Error("Instrumentation error, you cannot redefine the coverage variable in " + filename + ":" + node.loc.start.line); + } + }else{ + //Make sure developers don't redefine the coverage variable in node + if (node.type === "ExpressionStatement" && + node.expression && node.expression.left && + !node.expression.left.object && !node.expression.left.property && + node.expression.left.name === _blanket.getCovVar()) { + throw new Error("Instrumentation error, you cannot redefine the coverage variable in " + filename + ":" + node.loc.start.line); + } + } + }, + setupCoverage: function(){ + coverageInfo.instrumentation = "blanket"; + coverageInfo.stats = { + "suites": 0, + "tests": 0, + "passes": 0, + "pending": 0, + "failures": 0, + "start": new Date() + }; + }, + _checkIfSetup: function(){ + if (!coverageInfo.stats){ + throw new Error("You must call blanket.setupCoverage() first."); + } + }, + onTestStart: function(){ + if (_blanket.options("debug")) {console.log("BLANKET-Test event started");} + this._checkIfSetup(); + coverageInfo.stats.tests++; + coverageInfo.stats.pending++; + }, + onTestDone: function(total,passed){ + this._checkIfSetup(); + if(passed === total){ + coverageInfo.stats.passes++; + }else{ + coverageInfo.stats.failures++; + } + coverageInfo.stats.pending--; + }, + onModuleStart: function(){ + this._checkIfSetup(); + coverageInfo.stats.suites++; + }, + onTestsDone: function(){ + if (_blanket.options("debug")) {console.log("BLANKET-Test event done");} + this._checkIfSetup(); + coverageInfo.stats.end = new Date(); + + if (inBrowser){ + this.report(coverageInfo); + }else{ + if (!_blanket.options("branchTracking")){ + delete (inBrowser ? window : global)[_blanket.getCovVar()].branchFcn; + } + this.options("reporter").call(this,coverageInfo); + } + } + }; + return _blanket; +})(); + +(function(_blanket){ + var oldOptions = _blanket.options; +_blanket.extend({ + outstandingRequireFiles:[], + options: function(key,value){ + var newVal={}; + + if (typeof key !== "string"){ + //key is key/value map + oldOptions(key); + newVal = key; + }else if (typeof value === 'undefined'){ + //accessor + return oldOptions(key); + }else{ + //setter + oldOptions(key,value); + newVal[key] = value; + } + + if (newVal.adapter){ + _blanket._loadFile(newVal.adapter); + } + if (newVal.loader){ + _blanket._loadFile(newVal.loader); + } + }, + requiringFile: function(filename,done){ + if (typeof filename === "undefined"){ + _blanket.outstandingRequireFiles=[]; + }else if (typeof done === "undefined"){ + _blanket.outstandingRequireFiles.push(filename); + }else{ + _blanket.outstandingRequireFiles.splice(_blanket.outstandingRequireFiles.indexOf(filename),1); + } + }, + requireFilesLoaded: function(){ + return _blanket.outstandingRequireFiles.length === 0; + }, + showManualLoader: function(){ + if (document.getElementById("blanketLoaderDialog")){ + return; + } + //copied from http://blog.avtex.com/2012/01/26/cross-browser-css-only-modal-box/ + var loader = "
"; + loader += " 
"; + loader += "
"; + loader += "
"; + loader += "Error: Blanket.js encountered a cross origin request error while instrumenting the source files. "; + loader += "

This is likely caused by the source files being referenced locally (using the file:// protocol). "; + loader += "

Some solutions include starting Chrome with special flags, running a server locally, or using a browser without these CORS restrictions (Safari)."; + loader += "
"; + if (typeof FileReader !== "undefined"){ + loader += "
Or, try the experimental loader. When prompted, simply click on the directory containing all the source files you want covered."; + loader += "Start Loader"; + loader += ""; + } + loader += "
Close"; + loader += "
"; + loader += "
"; + + var css = ".blanketDialogWrapper {"; + css += "display:block;"; + css += "position:fixed;"; + css += "z-index:40001; }"; + + css += ".blanketDialogOverlay {"; + css += "position:fixed;"; + css += "width:100%;"; + css += "height:100%;"; + css += "background-color:black;"; + css += "opacity:.5; "; + css += "-ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=50)'; "; + css += "filter:alpha(opacity=50); "; + css += "z-index:40001; }"; + + css += ".blanketDialogVerticalOffset { "; + css += "position:fixed;"; + css += "top:30%;"; + css += "width:100%;"; + css += "z-index:40002; }"; + + css += ".blanketDialogBox { "; + css += "width:405px; "; + css += "position:relative;"; + css += "margin:0 auto;"; + css += "background-color:white;"; + css += "padding:10px;"; + css += "border:1px solid black; }"; + + var dom = document.createElement("style"); + dom.innerHTML = css; + document.head.appendChild(dom); + + var div = document.createElement("div"); + div.id = "blanketLoaderDialog"; + div.className = "blanketDialogWrapper"; + div.innerHTML = loader; + document.body.insertBefore(div,document.body.firstChild); + + }, + manualFileLoader: function(files){ + var toArray =Array.prototype.slice; + files = toArray.call(files).filter(function(item){ + return item.type !== ""; + }); + var sessionLength = files.length-1; + var sessionIndx=0; + var sessionArray = {}; + if (sessionStorage["blanketSessionLoader"]){ + sessionArray = JSON.parse(sessionStorage["blanketSessionLoader"]); + } + + + var fileLoader = function(event){ + var fileContent = event.currentTarget.result; + var file = files[sessionIndx]; + var filename = file.webkitRelativePath && file.webkitRelativePath !== '' ? file.webkitRelativePath : file.name; + sessionArray[filename] = fileContent; + sessionIndx++; + if (sessionIndx === sessionLength){ + sessionStorage.setItem("blanketSessionLoader", JSON.stringify(sessionArray)); + document.location.reload(); + }else{ + readFile(files[sessionIndx]); + } + }; + function readFile(file){ + var reader = new FileReader(); + reader.onload = fileLoader; + reader.readAsText(file); + } + readFile(files[sessionIndx]); + }, + _loadFile: function(path){ + if (typeof path !== "undefined"){ + var request = new XMLHttpRequest(); + request.open('GET', path, false); + request.send(); + _blanket._addScript(request.responseText); + } + }, + _addScript: function(data){ + var script = document.createElement("script"); + script.type = "text/javascript"; + script.text = data; + (document.body || document.getElementsByTagName('head')[0]).appendChild(script); + }, + hasAdapter: function(callback){ + return _blanket.options("adapter") !== null; + }, + report: function(coverage_data){ + if (!document.getElementById("blanketLoaderDialog")){ + //all found, clear it + _blanket.blanketSession = null; + } + coverage_data.files = window._$blanket; + var require = blanket.options("commonJS") ? blanket._commonjs.require : window.require; + + // Check if we have any covered files that requires reporting + // otherwise just exit gracefully. + if (!coverage_data.files || !Object.keys(coverage_data.files).length) { + if (_blanket.options("debug")) {console.log("BLANKET-Reporting No files were instrumented.");} + return; + } + + if (typeof coverage_data.files.branchFcn !== "undefined"){ + delete coverage_data.files.branchFcn; + } + if (typeof _blanket.options("reporter") === "string"){ + _blanket._loadFile(_blanket.options("reporter")); + _blanket.customReporter(coverage_data,_blanket.options("reporter_options")); + }else if (typeof _blanket.options("reporter") === "function"){ + _blanket.options("reporter")(coverage_data,_blanket.options("reporter_options")); + }else if (typeof _blanket.defaultReporter === 'function'){ + _blanket.defaultReporter(coverage_data,_blanket.options("reporter_options")); + }else{ + throw new Error("no reporter defined."); + } + }, + _bindStartTestRunner: function(bindEvent,startEvent){ + if (bindEvent){ + bindEvent(startEvent); + }else{ + window.addEventListener("load",startEvent,false); + } + }, + _loadSourceFiles: function(callback){ + var require = blanket.options("commonJS") ? blanket._commonjs.require : window.require; + function copy(o){ + var _copy = Object.create( Object.getPrototypeOf(o) ); + var propNames = Object.getOwnPropertyNames(o); + + propNames.forEach(function(name){ + var desc = Object.getOwnPropertyDescriptor(o, name); + Object.defineProperty(_copy, name, desc); + }); + + return _copy; + } + if (_blanket.options("debug")) {console.log("BLANKET-Collecting page scripts");} + if (window.goog) { + // Fix goog loader issues. The required files have already executed, + // so neither re-require nor re-provide anything using closure. + window.goog.require = function() {}; + window.goog.provide = function() {}; + } + var scripts = _blanket.utils.collectPageScripts(); + //_blanket.options("filter",scripts); + if (scripts.length === 0){ + callback(); + }else{ + + //check session state + if (sessionStorage["blanketSessionLoader"]){ + _blanket.blanketSession = JSON.parse(sessionStorage["blanketSessionLoader"]); + } + + scripts.forEach(function(file,indx){ + _blanket.utils.cache[file]={ + loaded:false + }; + }); + + var currScript=-1; + _blanket.utils.loadAll(function(test){ + if (test){ + return typeof scripts[currScript+1] !== 'undefined'; + } + currScript++; + if (currScript >= scripts.length){ + return null; + } + return scripts[currScript]; + },callback); + } + }, + beforeStartTestRunner: function(opts){ + opts = opts || {}; + opts.checkRequirejs = typeof opts.checkRequirejs === "undefined" ? true : opts.checkRequirejs; + opts.callback = opts.callback || function() { }; + opts.coverage = typeof opts.coverage === "undefined" ? true : opts.coverage; + if (opts.coverage) { + _blanket._bindStartTestRunner(opts.bindEvent, + function(){ + _blanket._loadSourceFiles(function() { + + var allLoaded = function(){ + return opts.condition ? opts.condition() : _blanket.requireFilesLoaded(); + }; + var check = function() { + if (allLoaded()) { + if (_blanket.options("debug")) {console.log("BLANKET-All files loaded, init start test runner callback.");} + var cb = _blanket.options("testReadyCallback"); + + if (cb){ + if (typeof cb === "function"){ + cb(opts.callback); + }else if (typeof cb === "string"){ + _blanket._addScript(cb); + opts.callback(); + } + }else{ + opts.callback(); + } + } else { + setTimeout(check, 13); + } + }; + check(); + }); + }); + }else{ + opts.callback(); + } + }, + utils: { + qualifyURL: function (url) { + //http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue + var a = document.createElement('a'); + a.href = url; + return a.href; + } + } +}); + +})(blanket); + +blanket.defaultReporter = function(coverage){ + var cssSytle = "#blanket-main {margin:2px;background:#EEE;color:#333;clear:both;font-family:'Helvetica Neue Light', 'HelveticaNeue-Light', 'Helvetica Neue', Calibri, Helvetica, Arial, sans-serif; font-size:17px;} #blanket-main a {color:#333;text-decoration:none;} #blanket-main a:hover {text-decoration:underline;} .blanket {margin:0;padding:5px;clear:both;border-bottom: 1px solid #FFFFFF;} .bl-error {color:red;}.bl-success {color:#5E7D00;} .bl-file{width:auto;} .bl-cl{float:left;} .blanket div.rs {margin-left:50px; width:150px; float:right} .bl-nb {padding-right:10px;} #blanket-main a.bl-logo {color: #EB1764;cursor: pointer;font-weight: bold;text-decoration: none} .bl-source{ overflow-x:scroll; background-color: #FFFFFF; border: 1px solid #CBCBCB; color: #363636; margin: 25px 20px; width: 80%;} .bl-source div{white-space: pre;font-family: monospace;} .bl-source > div > span:first-child{background-color: #EAEAEA;color: #949494;display: inline-block;padding: 0 10px;text-align: center;width: 30px;} .bl-source .miss{background-color:#e6c3c7} .bl-source span.branchWarning{color:#000;background-color:yellow;} .bl-source span.branchOkay{color:#000;background-color:transparent;}", + successRate = 60, + head = document.head, + fileNumber = 0, + body = document.body, + headerContent, + hasBranchTracking = Object.keys(coverage.files).some(function(elem){ + return typeof coverage.files[elem].branchData !== 'undefined'; + }), + bodyContent = "
results
Coverage (%)
Covered/Total Smts.
"+(hasBranchTracking ? "
Covered/Total Branches
":"")+"
", + fileTemplate = "
{{fileNumber}}.{{file}}
{{percentage}} %
{{numberCovered}}/{{totalSmts}}
"+( hasBranchTracking ? "
{{passedBranches}}/{{totalBranches}}
" : "" )+"
"; + grandTotalTemplate = "
{{rowTitle}}
{{percentage}} %
{{numberCovered}}/{{totalSmts}}
"+( hasBranchTracking ? "
{{passedBranches}}/{{totalBranches}}
" : "" ) + "
"; + + function blanket_toggleSource(id) { + var element = document.getElementById(id); + if(element.style.display === 'block') { + element.style.display = 'none'; + } else { + element.style.display = 'block'; + } + } + + + var script = document.createElement("script"); + script.type = "text/javascript"; + script.text = blanket_toggleSource.toString().replace('function ' + blanket_toggleSource.name, 'function blanket_toggleSource'); + body.appendChild(script); + + var percentage = function(number, total) { + return (Math.round(((number/total) * 100)*100)/100); + }; + + var appendTag = function (type, el, str) { + var dom = document.createElement(type); + dom.innerHTML = str; + el.appendChild(dom); + }; + + function escapeInvalidXmlChars(str) { + return str.replace(/\&/g, "&") + .replace(//g, ">") + .replace(/\"/g, """) + .replace(/\'/g, "'"); + } + + function isBranchFollowed(data,bool){ + var mode = bool ? 0 : 1; + if (typeof data === 'undefined' || + typeof data === null || + typeof data[mode] === 'undefined'){ + return false; + } + return data[mode].length > 0; + } + + var branchStack = []; + + function branchReport(colsIndex,src,cols,offset,lineNum){ + var newsrc=""; + var postfix=""; + if (branchStack.length > 0){ + newsrc += ""; + if (branchStack[0][0].end.line === lineNum){ + newsrc += escapeInvalidXmlChars(src.slice(0,branchStack[0][0].end.column)) + ""; + src = src.slice(branchStack[0][0].end.column); + branchStack.shift(); + if (branchStack.length > 0){ + newsrc += ""; + if (branchStack[0][0].end.line === lineNum){ + newsrc += escapeInvalidXmlChars(src.slice(0,branchStack[0][0].end.column)) + ""; + src = src.slice(branchStack[0][0].end.column); + branchStack.shift(); + if (!cols){ + return {src: newsrc + escapeInvalidXmlChars(src) ,cols:cols}; + } + } + else if (!cols){ + return {src: newsrc + escapeInvalidXmlChars(src) + "",cols:cols}; + } + else{ + postfix = ""; + } + }else if (!cols){ + return {src: newsrc + escapeInvalidXmlChars(src) ,cols:cols}; + } + }else if(!cols){ + return {src: newsrc + escapeInvalidXmlChars(src) + "",cols:cols}; + }else{ + postfix = ""; + } + } + var thisline = cols[colsIndex]; + //consequent + + var cons = thisline.consequent; + if (cons.start.line > lineNum){ + branchStack.unshift([thisline.alternate,thisline]); + branchStack.unshift([cons,thisline]); + src = escapeInvalidXmlChars(src); + }else{ + var style = ""; + newsrc += escapeInvalidXmlChars(src.slice(0,cons.start.column-offset)) + style; + + if (cols.length > colsIndex+1 && + cols[colsIndex+1].consequent.start.line === lineNum && + cols[colsIndex+1].consequent.start.column-offset < cols[colsIndex].consequent.end.column-offset) + { + var res = branchReport(colsIndex+1,src.slice(cons.start.column-offset,cons.end.column-offset),cols,cons.start.column-offset,lineNum); + newsrc += res.src; + cols = res.cols; + cols[colsIndex+1] = cols[colsIndex+2]; + cols.length--; + }else{ + newsrc += escapeInvalidXmlChars(src.slice(cons.start.column-offset,cons.end.column-offset)); + } + newsrc += ""; + + var alt = thisline.alternate; + if (alt.start.line > lineNum){ + newsrc += escapeInvalidXmlChars(src.slice(cons.end.column-offset)); + branchStack.unshift([alt,thisline]); + }else{ + newsrc += escapeInvalidXmlChars(src.slice(cons.end.column-offset,alt.start.column-offset)); + style = ""; + newsrc += style; + if (cols.length > colsIndex+1 && + cols[colsIndex+1].consequent.start.line === lineNum && + cols[colsIndex+1].consequent.start.column-offset < cols[colsIndex].alternate.end.column-offset) + { + var res2 = branchReport(colsIndex+1,src.slice(alt.start.column-offset,alt.end.column-offset),cols,alt.start.column-offset,lineNum); + newsrc += res2.src; + cols = res2.cols; + cols[colsIndex+1] = cols[colsIndex+2]; + cols.length--; + }else{ + newsrc += escapeInvalidXmlChars(src.slice(alt.start.column-offset,alt.end.column-offset)); + } + newsrc += ""; + newsrc += escapeInvalidXmlChars(src.slice(alt.end.column-offset)); + src = newsrc; + } + } + return {src:src+postfix, cols:cols}; + } + + var isUndefined = function(item){ + return typeof item !== 'undefined'; + }; + + var files = coverage.files; + var totals = { + totalSmts: 0, + numberOfFilesCovered: 0, + passedBranches: 0, + totalBranches: 0, + moduleTotalStatements : {}, + moduleTotalCoveredStatements : {}, + moduleTotalBranches : {}, + moduleTotalCoveredBranches : {} + }; + + // check if a data-cover-modulepattern was provided for per-module coverage reporting + var modulePattern = _blanket.options("modulePattern"); + var modulePatternRegex = ( modulePattern ? new RegExp(modulePattern) : null ); + + for(var file in files) + { + if (!files.hasOwnProperty(file)) { + continue; + } + + fileNumber++; + + var statsForFile = files[file], + totalSmts = 0, + numberOfFilesCovered = 0, + code = [], + i; + + + var end = []; + for(i = 0; i < statsForFile.source.length; i +=1){ + var src = statsForFile.source[i]; + + if (branchStack.length > 0 || + typeof statsForFile.branchData !== 'undefined') + { + if (typeof statsForFile.branchData[i+1] !== 'undefined') + { + var cols = statsForFile.branchData[i+1].filter(isUndefined); + var colsIndex=0; + + + src = branchReport(colsIndex,src,cols,0,i+1).src; + + }else if (branchStack.length){ + src = branchReport(0,src,null,0,i+1).src; + }else{ + src = escapeInvalidXmlChars(src); + } + }else{ + src = escapeInvalidXmlChars(src); + } + var lineClass=""; + if(statsForFile[i+1]) { + numberOfFilesCovered += 1; + totalSmts += 1; + lineClass = 'hit'; + }else{ + if(statsForFile[i+1] === 0){ + totalSmts++; + lineClass = 'miss'; + } + } + code[i + 1] = "
"+(i + 1)+""+src+"
"; + } + totals.totalSmts += totalSmts; + totals.numberOfFilesCovered += numberOfFilesCovered; + var totalBranches=0; + var passedBranches=0; + if (typeof statsForFile.branchData !== 'undefined'){ + for(var j=0;j 0 && + typeof statsForFile.branchData[j][k][1] !== 'undefined' && + statsForFile.branchData[j][k][1].length > 0){ + passedBranches++; + } + } + } + } + } + } + totals.passedBranches += passedBranches; + totals.totalBranches += totalBranches; + + // if "data-cover-modulepattern" was provided, + // track totals per module name as well as globally + if (modulePatternRegex) { + var moduleName = file.match(modulePatternRegex)[1]; + + if(!totals.moduleTotalStatements.hasOwnProperty(moduleName)) { + totals.moduleTotalStatements[moduleName] = 0; + totals.moduleTotalCoveredStatements[moduleName] = 0; + } + + totals.moduleTotalStatements[moduleName] += totalSmts; + totals.moduleTotalCoveredStatements[moduleName] += numberOfFilesCovered; + + if(!totals.moduleTotalBranches.hasOwnProperty(moduleName)) { + totals.moduleTotalBranches[moduleName] = 0; + totals.moduleTotalCoveredBranches[moduleName] = 0; + } + + totals.moduleTotalBranches[moduleName] += totalBranches; + totals.moduleTotalCoveredBranches[moduleName] += passedBranches; + } + + var result = percentage(numberOfFilesCovered, totalSmts); + + var output = fileTemplate.replace("{{file}}", file) + .replace("{{percentage}}",result) + .replace("{{numberCovered}}", numberOfFilesCovered) + .replace(/\{\{fileNumber\}\}/g, fileNumber) + .replace("{{totalSmts}}", totalSmts) + .replace("{{totalBranches}}", totalBranches) + .replace("{{passedBranches}}", passedBranches) + .replace("{{source}}", code.join(" ")); + if(result < successRate) + { + output = output.replace("{{statusclass}}", "bl-error"); + } else { + output = output.replace("{{statusclass}}", "bl-success"); + } + bodyContent += output; + } + + // create temporary function for use by the global totals reporter, + // as well as the per-module totals reporter + var createAggregateTotal = function(numSt, numCov, numBranch, numCovBr, moduleName) { + + var totalPercent = percentage(numCov, numSt); + var statusClass = totalPercent < successRate ? "bl-error" : "bl-success"; + var rowTitle = ( moduleName ? "Total for module: " + moduleName : "Global total" ); + var totalsOutput = grandTotalTemplate.replace("{{rowTitle}}", rowTitle) + .replace("{{percentage}}", totalPercent) + .replace("{{numberCovered}}", numCov) + .replace("{{totalSmts}}", numSt) + .replace("{{passedBranches}}", numCovBr) + .replace("{{totalBranches}}", numBranch) + .replace("{{statusclass}}", statusClass); + + bodyContent += totalsOutput; + }; + + // if "data-cover-modulepattern" was provided, + // output the per-module totals alongside the global totals + if (modulePatternRegex) { + for (var thisModuleName in totals.moduleTotalStatements) { + if (totals.moduleTotalStatements.hasOwnProperty(thisModuleName)) { + + var moduleTotalSt = totals.moduleTotalStatements[thisModuleName]; + var moduleTotalCovSt = totals.moduleTotalCoveredStatements[thisModuleName]; + + var moduleTotalBr = totals.moduleTotalBranches[thisModuleName]; + var moduleTotalCovBr = totals.moduleTotalCoveredBranches[thisModuleName]; + + createAggregateTotal(moduleTotalSt, moduleTotalCovSt, moduleTotalBr, moduleTotalCovBr, thisModuleName); + } + } + } + + createAggregateTotal(totals.totalSmts, totals.numberOfFilesCovered, totals.totalBranches, totals.passedBranches, null); + bodyContent += "
"; //closing main + + + appendTag('style', head, cssSytle); + //appendStyle(body, headerContent); + if (document.getElementById("blanket-main")){ + document.getElementById("blanket-main").innerHTML= + bodyContent.slice(23,-6); + }else{ + appendTag('div', body, bodyContent); + } + //appendHtml(body, ''); +}; + +(function(){ + var newOptions={}; + //http://stackoverflow.com/a/2954896 + var toArray =Array.prototype.slice; + var scripts = toArray.call(document.scripts); + toArray.call(scripts[scripts.length - 1].attributes) + .forEach(function(es){ + if(es.nodeName === "data-cover-only"){ + newOptions.filter = es.nodeValue; + } + if(es.nodeName === "data-cover-never"){ + newOptions.antifilter = es.nodeValue; + } + if(es.nodeName === "data-cover-reporter"){ + newOptions.reporter = es.nodeValue; + } + if (es.nodeName === "data-cover-adapter"){ + newOptions.adapter = es.nodeValue; + } + if (es.nodeName === "data-cover-loader"){ + newOptions.loader = es.nodeValue; + } + if (es.nodeName === "data-cover-timeout"){ + newOptions.timeout = es.nodeValue; + } + if (es.nodeName === "data-cover-modulepattern") { + newOptions.modulePattern = es.nodeValue; + } + if (es.nodeName === "data-cover-reporter-options"){ + try{ + newOptions.reporter_options = JSON.parse(es.nodeValue); + }catch(e){ + if (blanket.options("debug")){ + throw new Error("Invalid reporter options. Must be a valid stringified JSON object."); + } + } + } + if (es.nodeName === "data-cover-testReadyCallback"){ + newOptions.testReadyCallback = es.nodeValue; + } + if (es.nodeName === "data-cover-customVariable"){ + newOptions.customVariable = es.nodeValue; + } + if (es.nodeName === "data-cover-flags"){ + var flags = " "+es.nodeValue+" "; + if (flags.indexOf(" ignoreError ") > -1){ + newOptions.ignoreScriptError = true; + } + if (flags.indexOf(" autoStart ") > -1){ + newOptions.autoStart = true; + } + if (flags.indexOf(" ignoreCors ") > -1){ + newOptions.ignoreCors = true; + } + if (flags.indexOf(" branchTracking ") > -1){ + newOptions.branchTracking = true; + } + if (flags.indexOf(" sourceURL ") > -1){ + newOptions.sourceURL = true; + } + if (flags.indexOf(" debug ") > -1){ + newOptions.debug = true; + } + if (flags.indexOf(" engineOnly ") > -1){ + newOptions.engineOnly = true; + } + if (flags.indexOf(" commonJS ") > -1){ + newOptions.commonJS = true; + } + if (flags.indexOf(" instrumentCache ") > -1){ + newOptions.instrumentCache = true; + } + } + }); + blanket.options(newOptions); + + if (typeof requirejs !== 'undefined'){ + blanket.options("existingRequireJS",true); + } + /* setup requirejs loader, if needed */ + + if (blanket.options("commonJS")){ + blanket._commonjs = {}; + } +})(); +(function(_blanket){ +_blanket.extend({ + utils: { + normalizeBackslashes: function(str) { + return str.replace(/\\/g, '/'); + }, + matchPatternAttribute: function(filename,pattern){ + if (typeof pattern === 'string'){ + if (pattern.indexOf("[") === 0){ + //treat as array + var pattenArr = pattern.slice(1,pattern.length-1).split(","); + return pattenArr.some(function(elem){ + return _blanket.utils.matchPatternAttribute(filename,_blanket.utils.normalizeBackslashes(elem.slice(1,-1))); + //return filename.indexOf(_blanket.utils.normalizeBackslashes(elem.slice(1,-1))) > -1; + }); + }else if ( pattern.indexOf("//") === 0){ + var ex = pattern.slice(2,pattern.lastIndexOf('/')); + var mods = pattern.slice(pattern.lastIndexOf('/')+1); + var regex = new RegExp(ex,mods); + return regex.test(filename); + }else if (pattern.indexOf("#") === 0){ + return window[pattern.slice(1)].call(window,filename); + }else{ + return filename.indexOf(_blanket.utils.normalizeBackslashes(pattern)) > -1; + } + }else if ( pattern instanceof Array ){ + return pattern.some(function(elem){ + return _blanket.utils.matchPatternAttribute(filename,elem); + }); + }else if (pattern instanceof RegExp){ + return pattern.test(filename); + }else if (typeof pattern === "function"){ + return pattern.call(window,filename); + } + }, + blanketEval: function(data){ + _blanket._addScript(data); + }, + collectPageScripts: function(){ + var toArray = Array.prototype.slice; + var scripts = toArray.call(document.scripts); + var selectedScripts=[],scriptNames=[]; + var filter = _blanket.options("filter"); + if(filter != null){ + //global filter in place, data-cover-only + var antimatch = _blanket.options("antifilter"); + selectedScripts = toArray.call(document.scripts) + .filter(function(s){ + return toArray.call(s.attributes).filter(function(sn){ + return sn.nodeName === "src" && _blanket.utils.matchPatternAttribute(sn.nodeValue,filter) && + (typeof antimatch === "undefined" || !_blanket.utils.matchPatternAttribute(sn.nodeValue,antimatch)); + }).length === 1; + }); + }else{ + selectedScripts = toArray.call(document.querySelectorAll("script[data-cover]")); + } + scriptNames = selectedScripts.map(function(s){ + return _blanket.utils.qualifyURL( + toArray.call(s.attributes).filter( + function(sn){ + return sn.nodeName === "src"; + })[0].nodeValue); + }); + if (!filter){ + _blanket.options("filter","['"+scriptNames.join("','")+"']"); + } + return scriptNames; + }, + loadAll: function(nextScript,cb,preprocessor){ + /** + * load dependencies + * @param {nextScript} factory for priority level + * @param {cb} the done callback + */ + var currScript=nextScript(); + var isLoaded = _blanket.utils.scriptIsLoaded( + currScript, + _blanket.utils.ifOrdered, + nextScript, + cb + ); + + if (!(_blanket.utils.cache[currScript] && _blanket.utils.cache[currScript].loaded)){ + var attach = function(){ + if (_blanket.options("debug")) {console.log("BLANKET-Mark script:"+currScript+", as loaded and move to next script.");} + isLoaded(); + }; + var whenDone = function(result){ + if (_blanket.options("debug")) {console.log("BLANKET-File loading finished");} + if (typeof result !== 'undefined'){ + if (_blanket.options("debug")) {console.log("BLANKET-Add file to DOM.");} + _blanket._addScript(result); + } + attach(); + }; + + _blanket.utils.attachScript( + { + url: currScript + }, + function (content){ + _blanket.utils.processFile( + content, + currScript, + whenDone, + whenDone + ); + } + ); + }else{ + isLoaded(); + } + }, + attachScript: function(options,cb){ + var timeout = _blanket.options("timeout") || 3000; + setTimeout(function(){ + if (!_blanket.utils.cache[options.url].loaded){ + throw new Error("error loading source script: " + options.url); + } + },timeout); + _blanket.utils.getFile( + options.url, + cb, + function(){ throw new Error("error loading source script");} + ); + }, + ifOrdered: function(nextScript,cb){ + /** + * ordered loading callback + * @param {nextScript} factory for priority level + * @param {cb} the done callback + */ + var currScript = nextScript(true); + if (currScript){ + _blanket.utils.loadAll(nextScript,cb); + }else{ + cb(new Error("Error in loading chain.")); + } + }, + scriptIsLoaded: function(url,orderedCb,nextScript,cb){ + /** + * returns a callback that checks a loading list to see if a script is loaded. + * @param {orderedCb} callback if ordered loading is being done + * @param {nextScript} factory for next priority level + * @param {cb} the done callback + */ + if (_blanket.options("debug")) {console.log("BLANKET-Returning function");} + return function(){ + if (_blanket.options("debug")) {console.log("BLANKET-Marking file as loaded: "+url);} + + _blanket.utils.cache[url].loaded=true; + + if (_blanket.utils.allLoaded()){ + if (_blanket.options("debug")) {console.log("BLANKET-All files loaded");} + cb(); + }else if (orderedCb){ + //if it's ordered we need to + //traverse down to the next + //priority level + if (_blanket.options("debug")) {console.log("BLANKET-Load next file.");} + orderedCb(nextScript,cb); + } + }; + }, + cache: {}, + allLoaded: function (){ + /** + * check if depdencies are loaded in cache + */ + var cached = Object.keys(_blanket.utils.cache); + for (var i=0;i -1){ + callback(_blanket.blanketSession[key]); + foundInSession=true; + return; + } + } + } + if (!foundInSession){ + var xhr = _blanket.utils.createXhr(); + xhr.open('GET', url, true); + + //Allow overrides specified in config + if (onXhr) { + onXhr(xhr, url); + } + + xhr.onreadystatechange = function (evt) { + var status, err; + + //Do not explicitly handle errors, those should be + //visible via console output in the browser. + if (xhr.readyState === 4) { + status = xhr.status; + if ((status > 399 && status < 600) /*|| + (status === 0 && + navigator.userAgent.toLowerCase().indexOf('firefox') > -1) + */ ) { + //An http 4xx or 5xx error. Signal an error. + err = new Error(url + ' HTTP status: ' + status); + err.xhr = xhr; + errback(err); + } else { + callback(xhr.responseText); + } + } + }; + try{ + xhr.send(null); + }catch(e){ + if (e.code && (e.code === 101 || e.code === 1012) && _blanket.options("ignoreCors") === false){ + //running locally and getting error from browser + _blanket.showManualLoader(); + } else { + throw e; + } + } + } + } + } +}); + +(function(){ + var require = blanket.options("commonJS") ? blanket._commonjs.require : window.require; + var requirejs = blanket.options("commonJS") ? blanket._commonjs.requirejs : window.requirejs; + if (!_blanket.options("engineOnly") && _blanket.options("existingRequireJS")){ + + _blanket.utils.oldloader = requirejs.load; + + requirejs.load = function (context, moduleName, url) { + _blanket.requiringFile(url); + _blanket.utils.getFile(url, + function(content){ + _blanket.utils.processFile( + content, + url, + function newLoader(){ + context.completeLoad(moduleName); + }, + function oldLoader(){ + _blanket.utils.oldloader(context, moduleName, url); + } + ); + }, function (err) { + _blanket.requiringFile(); + throw err; + }); + }; + } + // Save the XHR constructor, just in case frameworks like Sinon would sandbox it. + _blanket.utils.cacheXhrConstructor(); +})(); + +})(blanket); + +(function() { + + if (! jasmine) { + throw new Exception("jasmine library does not exist in global namespace!"); + } + + function elapsed(startTime, endTime) { + return (endTime - startTime)/1000; + } + + function ISODateString(d) { + function pad(n) { return n < 10 ? '0'+n : n; } + + return d.getFullYear() + '-' + + pad(d.getMonth()+1) + '-' + + pad(d.getDate()) + 'T' + + pad(d.getHours()) + ':' + + pad(d.getMinutes()) + ':' + + pad(d.getSeconds()); + } + + function trim(str) { + return str.replace(/^\s+/, "" ).replace(/\s+$/, "" ); + } + + function escapeInvalidXmlChars(str) { + return str.replace(/\&/g, "&") + .replace(//g, ">") + .replace(/\"/g, """) + .replace(/\'/g, "'"); + } + + /** + * based on https://raw.github.com/larrymyers/jasmine-reporters/master/src/jasmine.junit_reporter.js + */ + var BlanketReporter = function(savePath, consolidate, useDotNotation) { + + blanket.setupCoverage(); + }; + BlanketReporter.finished_at = null; // will be updated after all files have been written + + BlanketReporter.prototype = { + specStarted: function(spec) { + blanket.onTestStart(); + }, + + specDone: function(result) { + var passed = result.status === "passed" ? 1 : 0; + blanket.onTestDone(1, passed); + }, + + jasmineDone: function() { + blanket.onTestsDone(); + }, + + log: function(str) { + var console = jasmine.getGlobal().console; + + if (console && console.log) { + console.log(str); + } + } + }; + + + // export public + jasmine.BlanketReporter = BlanketReporter; + + //override existing jasmine execute + var originalJasmineExecute = jasmine.getEnv().execute; + jasmine.getEnv().execute = function(){ console.log("waiting for blanket..."); }; + + blanket.beforeStartTestRunner({ + checkRequirejs:true, + callback:function(){ + jasmine.getEnv().addReporter(new jasmine.BlanketReporter()); + jasmine.getEnv().execute = originalJasmineExecute; + jasmine.getEnv().execute(); + } + }); +})(); diff --git a/third_party/closure/LICENSE.txt b/third_party/closure/LICENSE.txt new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/third_party/closure/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third_party/closure/compiler.jar b/third_party/closure/compiler.jar new file mode 100644 index 0000000000..c7006a8803 Binary files /dev/null and b/third_party/closure/compiler.jar differ diff --git a/third_party/closure/deps/depswriter.py b/third_party/closure/deps/depswriter.py new file mode 100755 index 0000000000..dfecc4bf74 --- /dev/null +++ b/third_party/closure/deps/depswriter.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python +# +# Copyright 2009 The Closure Library Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Generates out a Closure deps.js file given a list of JavaScript sources. + +Paths can be specified as arguments or (more commonly) specifying trees +with the flags (call with --help for descriptions). + +Usage: depswriter.py [path/to/js1.js [path/to/js2.js] ...] +""" + +import logging +import optparse +import os +import posixpath +import shlex +import sys + +import source +import treescan + + +__author__ = 'nnaze@google.com (Nathan Naze)' + + +def MakeDepsFile(source_map): + """Make a generated deps file. + + Args: + source_map: A dict map of the source path to source.Source object. + + Returns: + str, A generated deps file source. + """ + + # Write in path alphabetical order + paths = sorted(source_map.keys()) + + lines = [] + + for path in paths: + js_source = source_map[path] + + # We don't need to add entries that don't provide anything. + if js_source.provides: + lines.append(_GetDepsLine(path, js_source)) + + return ''.join(lines) + + +def _GetDepsLine(path, js_source): + """Get a deps.js file string for a source.""" + + provides = sorted(js_source.provides) + requires = sorted(js_source.requires) + + return 'goog.addDependency(\'%s\', %s, %s);\n' % (path, provides, requires) + + +def _GetOptionsParser(): + """Get the options parser.""" + + parser = optparse.OptionParser(__doc__) + + parser.add_option('--output_file', + dest='output_file', + action='store', + help=('If specified, write output to this path instead of ' + 'writing to standard output.')) + parser.add_option('--root', + dest='roots', + default=[], + action='append', + help='A root directory to scan for JS source files. ' + 'Paths of JS files in generated deps file will be ' + 'relative to this path. This flag may be specified ' + 'multiple times.') + parser.add_option('--root_with_prefix', + dest='roots_with_prefix', + default=[], + action='append', + help='A root directory to scan for JS source files, plus ' + 'a prefix (if either contains a space, surround with ' + 'quotes). Paths in generated deps file will be relative ' + 'to the root, but preceded by the prefix. This flag ' + 'may be specified multiple times.') + parser.add_option('--path_with_depspath', + dest='paths_with_depspath', + default=[], + action='append', + help='A path to a source file and an alternate path to ' + 'the file in the generated deps file (if either contains ' + 'a space, surround with whitespace). This flag may be ' + 'specified multiple times.') + return parser + + +def _NormalizePathSeparators(path): + """Replaces OS-specific path separators with POSIX-style slashes. + + Args: + path: str, A file path. + + Returns: + str, The path with any OS-specific path separators (such as backslash on + Windows) replaced with URL-compatible forward slashes. A no-op on systems + that use POSIX paths. + """ + return path.replace(os.sep, posixpath.sep) + + +def _GetRelativePathToSourceDict(root, prefix=''): + """Scans a top root directory for .js sources. + + Args: + root: str, Root directory. + prefix: str, Prefix for returned paths. + + Returns: + dict, A map of relative paths (with prefix, if given), to source.Source + objects. + """ + # Remember and restore the cwd when we're done. We work from the root so + # that paths are relative from the root. + start_wd = os.getcwd() + os.chdir(root) + + path_to_source = {} + for path in treescan.ScanTreeForJsFiles('.'): + prefixed_path = _NormalizePathSeparators(os.path.join(prefix, path)) + path_to_source[prefixed_path] = source.Source(source.GetFileContents(path)) + + os.chdir(start_wd) + + return path_to_source + + +def _GetPair(s): + """Return a string as a shell-parsed tuple. Two values expected.""" + try: + # shlex uses '\' as an escape character, so they must be escaped. + s = s.replace('\\', '\\\\') + first, second = shlex.split(s) + return (first, second) + except: + raise Exception('Unable to parse input line as a pair: %s' % s) + + +def main(): + """CLI frontend to MakeDepsFile.""" + logging.basicConfig(format=(sys.argv[0] + ': %(message)s'), + level=logging.INFO) + options, args = _GetOptionsParser().parse_args() + + path_to_source = {} + + # Roots without prefixes + for root in options.roots: + path_to_source.update(_GetRelativePathToSourceDict(root)) + + # Roots with prefixes + for root_and_prefix in options.roots_with_prefix: + root, prefix = _GetPair(root_and_prefix) + path_to_source.update(_GetRelativePathToSourceDict(root, prefix=prefix)) + + # Source paths + for path in args: + path_to_source[path] = source.Source(source.GetFileContents(path)) + + # Source paths with alternate deps paths + for path_with_depspath in options.paths_with_depspath: + srcpath, depspath = _GetPair(path_with_depspath) + path_to_source[depspath] = source.Source(source.GetFileContents(srcpath)) + + # Make our output pipe. + if options.output_file: + out = open(options.output_file, 'w') + else: + out = sys.stdout + + out.write('// This file was autogenerated by %s.\n' % sys.argv[0]) + out.write('// Please do not edit.\n') + + out.write(MakeDepsFile(path_to_source)) + + +if __name__ == '__main__': + main() diff --git a/third_party/closure/deps/source.py b/third_party/closure/deps/source.py new file mode 100644 index 0000000000..64e748587d --- /dev/null +++ b/third_party/closure/deps/source.py @@ -0,0 +1,118 @@ +# Copyright 2009 The Closure Library Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Scans a source JS file for its provided and required namespaces. + +Simple class to scan a JavaScript file and express its dependencies. +""" + +__author__ = 'nnaze@google.com' + + +import re + +_BASE_REGEX_STRING = '^\s*goog\.%s\(\s*[\'"](.+)[\'"]\s*\)' +_PROVIDE_REGEX = re.compile(_BASE_REGEX_STRING % 'provide') +_REQUIRES_REGEX = re.compile(_BASE_REGEX_STRING % 'require') + + +class Source(object): + """Scans a JavaScript source for its provided and required namespaces.""" + + # Matches a "/* ... */" comment. + # Note: We can't definitively distinguish a "/*" in a string literal without a + # state machine tokenizer. We'll assume that a line starting with whitespace + # and "/*" is a comment. + _COMMENT_REGEX = re.compile( + r""" + ^\s* # Start of a new line and whitespace + /\* # Opening "/*" + .*? # Non greedy match of any characters (including newlines) + \*/ # Closing "*/""", + re.MULTILINE | re.DOTALL | re.VERBOSE) + + def __init__(self, source): + """Initialize a source. + + Args: + source: str, The JavaScript source. + """ + + self.provides = set() + self.requires = set() + + self._source = source + self._ScanSource() + + def GetSource(self): + """Get the source as a string.""" + return self._source + + @classmethod + def _StripComments(cls, source): + return cls._COMMENT_REGEX.sub('', source) + + @classmethod + def _HasProvideGoogFlag(cls, source): + """Determines whether the @provideGoog flag is in a comment.""" + for comment_content in cls._COMMENT_REGEX.findall(source): + if '@provideGoog' in comment_content: + return True + + return False + + def _ScanSource(self): + """Fill in provides and requires by scanning the source.""" + + stripped_source = self._StripComments(self.GetSource()) + + source_lines = stripped_source.splitlines() + for line in source_lines: + match = _PROVIDE_REGEX.match(line) + if match: + self.provides.add(match.group(1)) + match = _REQUIRES_REGEX.match(line) + if match: + self.requires.add(match.group(1)) + + # Closure's base file implicitly provides 'goog'. + # This is indicated with the @provideGoog flag. + if self._HasProvideGoogFlag(self.GetSource()): + + if len(self.provides) or len(self.requires): + raise Exception( + 'Base file should not provide or require namespaces.') + + self.provides.add('goog') + + +def GetFileContents(path): + """Get a file's contents as a string. + + Args: + path: str, Path to file. + + Returns: + str, Contents of file. + + Raises: + IOError: An error occurred opening or reading the file. + + """ + fileobj = open(path) + try: + return fileobj.read() + finally: + fileobj.close() diff --git a/third_party/closure/deps/treescan.py b/third_party/closure/deps/treescan.py new file mode 100644 index 0000000000..6694593aab --- /dev/null +++ b/third_party/closure/deps/treescan.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# +# Copyright 2010 The Closure Library Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Shared utility functions for scanning directory trees.""" + +import os +import re + + +__author__ = 'nnaze@google.com (Nathan Naze)' + + +# Matches a .js file path. +_JS_FILE_REGEX = re.compile(r'^.+\.js$') + + +def ScanTreeForJsFiles(root): + """Scans a directory tree for JavaScript files. + + Args: + root: str, Path to a root directory. + + Returns: + An iterable of paths to JS files, relative to cwd. + """ + return ScanTree(root, path_filter=_JS_FILE_REGEX) + + +def ScanTree(root, path_filter=None, ignore_hidden=True): + """Scans a directory tree for files. + + Args: + root: str, Path to a root directory. + path_filter: A regular expression filter. If set, only paths matching + the path_filter are returned. + ignore_hidden: If True, do not follow or return hidden directories or files + (those starting with a '.' character). + + Yields: + A string path to files, relative to cwd. + """ + + def OnError(os_error): + raise os_error + + for dirpath, dirnames, filenames in os.walk(root, onerror=OnError): + # os.walk allows us to modify dirnames to prevent decent into particular + # directories. Avoid hidden directories. + for dirname in dirnames: + if ignore_hidden and dirname.startswith('.'): + dirnames.remove(dirname) + + for filename in filenames: + + # nothing that starts with '.' + if ignore_hidden and filename.startswith('.'): + continue + + fullpath = os.path.join(dirpath, filename) + + if path_filter and not path_filter.match(fullpath): + continue + + yield os.path.normpath(fullpath) diff --git a/third_party/closure/goog/base.js b/third_party/closure/goog/base.js new file mode 100644 index 0000000000..34983943e0 --- /dev/null +++ b/third_party/closure/goog/base.js @@ -0,0 +1,843 @@ +// Copyright 2006 The Closure Library Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS-IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview Bootstrap for the Google JS Library (Closure). + * + * In uncompiled mode base.js will write out Closure's deps file, unless the + * global CLOSURE_NO_DEPS is set to true. This allows projects to + * include their own deps file(s) from different locations. + * + * + * @provideGoog + */ + + +/** + * @define {boolean} Overridden to true by the compiler when --closure_pass + * or --mark_as_compiled is specified. + */ +var COMPILED = false; + + +/** + * Base namespace for the Closure library. Checks to see goog is already + * defined in the current scope before assigning to prevent clobbering if + * base.js is loaded more than once. + * + * @const + */ +var goog = goog || {}; + + +/** + * Reference to the global context. In most cases this will be 'window'. + */ +goog.global = this; + + +/** + * A hook for overriding the define values in uncompiled mode. + * + * In uncompiled mode, {@code CLOSURE_UNCOMPILED_DEFINES} may be defined before + * loading base.js. If a key is defined in {@code CLOSURE_UNCOMPILED_DEFINES}, + * {@code goog.define} will use the value instead of the default value. This + * allows flags to be overwritten without compilation (this is normally + * accomplished with the compiler's "define" flag). + * + * Example: + *
+ *   var CLOSURE_UNCOMPILED_DEFINES = {'goog.DEBUG': false};
+ * 
+ * + * @type {Object.|undefined} + */ +goog.global.CLOSURE_UNCOMPILED_DEFINES; + + +/** + * A hook for overriding the define values in uncompiled or compiled mode, + * like CLOSURE_UNCOMPILED_DEFINES but effective in compiled code. In + * uncompiled code CLOSURE_UNCOMPILED_DEFINES takes precedence. + * + * Also unlike CLOSURE_UNCOMPILED_DEFINES the values must be number, boolean or + * string literals or the compiler will emit an error. + * + * While any @define value may be set, only those set with goog.define will be + * effective for uncompiled code. + * + * Example: + *
+ *   var CLOSURE_DEFINES = {'goog.DEBUG': false};
+ * 
+ * + * @type {Object.|undefined} + */ +goog.global.CLOSURE_DEFINES; + + +/** + * Returns true if the specified value is not undefined. + * WARNING: Do not use this to test if an object has a property. Use the in + * operator instead. + * + * @param {?} val Variable to test. + * @return {boolean} Whether variable is defined. + */ +goog.isDef = function(val) { + // void 0 always evaluates to undefined and hence we do not need to depend on + // the definition of the global variable named 'undefined'. + return val !== void 0; +}; + + +/** + * Builds an object structure for the provided namespace path, ensuring that + * names that already exist are not overwritten. For example: + * "a.b.c" -> a = {};a.b={};a.b.c={}; + * Used by goog.provide and goog.exportSymbol. + * @param {string} name name of the object that this file defines. + * @param {*=} opt_object the object to expose at the end of the path. + * @param {Object=} opt_objectToExportTo The object to add the path to; default + * is |goog.global|. + * @private + */ +goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) { + var parts = name.split('.'); + var cur = opt_objectToExportTo || goog.global; + + // Internet Explorer exhibits strange behavior when throwing errors from + // methods externed in this manner. See the testExportSymbolExceptions in + // base_test.html for an example. + if (!(parts[0] in cur) && cur.execScript) { + cur.execScript('var ' + parts[0]); + } + + // Certain browsers cannot parse code in the form for((a in b); c;); + // This pattern is produced by the JSCompiler when it collapses the + // statement above into the conditional loop below. To prevent this from + // happening, use a for-loop and reserve the init logic as below. + + // Parentheses added to eliminate strict JS warning in Firefox. + for (var part; parts.length && (part = parts.shift());) { + if (!parts.length && goog.isDef(opt_object)) { + // last part and we have an object; use it + cur[part] = opt_object; + } else if (cur[part]) { + cur = cur[part]; + } else { + cur = cur[part] = {}; + } + } +}; + + +/** + * Defines a named value. In uncompiled mode, the value is retreived from + * CLOSURE_DEFINES or CLOSURE_UNCOMPILED_DEFINES if the object is defined and + * has the property specified, and otherwise used the defined defaultValue. + * When compiled, the default can be overridden using compiler command-line + * options. + * + * @param {string} name The distinguished name to provide. + * @param {string|number|boolean} defaultValue + */ +goog.define = function(name, defaultValue) { + var value = defaultValue; + if (!COMPILED) { + if (goog.global.CLOSURE_UNCOMPILED_DEFINES && + Object.prototype.hasOwnProperty.call( + goog.global.CLOSURE_UNCOMPILED_DEFINES, name)) { + value = goog.global.CLOSURE_UNCOMPILED_DEFINES[name]; + } else if (goog.global.CLOSURE_DEFINES && + Object.prototype.hasOwnProperty.call( + goog.global.CLOSURE_DEFINES, name)) { + value = goog.global.CLOSURE_DEFINES[name]; + } + } + goog.exportPath_(name, value); +}; + + +/** + * @define {boolean} DEBUG is provided as a convenience so that debugging code + * that should not be included in a production js_binary can be easily stripped + * by specifying --define goog.DEBUG=false to the JSCompiler. For example, most + * toString() methods should be declared inside an "if (goog.DEBUG)" conditional + * because they are generally used for debugging purposes and it is difficult + * for the JSCompiler to statically determine whether they are used. + */ +goog.DEBUG = true; + + +/** + * @define {string} LOCALE defines the locale being used for compilation. It is + * used to select locale specific data to be compiled in js binary. BUILD rule + * can specify this value by "--define goog.LOCALE=" as JSCompiler + * option. + * + * Take into account that the locale code format is important. You should use + * the canonical Unicode format with hyphen as a delimiter. Language must be + * lowercase, Language Script - Capitalized, Region - UPPERCASE. + * There are few examples: pt-BR, en, en-US, sr-Latin-BO, zh-Hans-CN. + * + * See more info about locale codes here: + * http://www.unicode.org/reports/tr35/#Unicode_Language_and_Locale_Identifiers + * + * For language codes you should use values defined by ISO 693-1. See it here + * http://www.w3.org/WAI/ER/IG/ert/iso639.htm. There is only one exception from + * this rule: the Hebrew language. For legacy reasons the old code (iw) should + * be used instead of the new code (he), see http://wiki/Main/IIISynonyms. + */ +goog.define('goog.LOCALE', 'en'); // default to en + + +/** + * @define {boolean} Whether this code is running on trusted sites. + * + * On untrusted sites, several native functions can be defined or overridden by + * external libraries like Prototype, Datejs, and JQuery and setting this flag + * to false forces closure to use its own implementations when possible. + * + * If your JavaScript can be loaded by a third party site and you are wary about + * relying on non-standard implementations, specify + * "--define goog.TRUSTED_SITE=false" to the JSCompiler. + */ +goog.define('goog.TRUSTED_SITE', true); + + +/** + * @define {boolean} Whether a project is expected to be running in strict mode. + * + * This define can be used to trigger alternate implementations compatible with + * running in EcmaScript Strict mode or warn about unavailable functionality. + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/Strict_mode + */ +goog.define('goog.STRICT_MODE_COMPATIBLE', false); + + +/** + * Creates object stubs for a namespace. The presence of one or more + * goog.provide() calls indicate that the file defines the given + * objects/namespaces. Provided objects must not be null or undefined. + * Build tools also scan for provide/require statements + * to discern dependencies, build dependency files (see deps.js), etc. + * @see goog.require + * @param {string} name Namespace provided by this file in the form + * "goog.package.part". + */ +goog.provide = function(name) { + if (!COMPILED) { + // Ensure that the same namespace isn't provided twice. + // A goog.module/goog.provide maps a goog.require to a specific file + if (goog.isProvided_(name)) { + throw Error('Namespace "' + name + '" already declared.'); + } + delete goog.implicitNamespaces_[name]; + + var namespace = name; + while ((namespace = namespace.substring(0, namespace.lastIndexOf('.')))) { + if (goog.getObjectByName(namespace)) { + break; + } + goog.implicitNamespaces_[namespace] = true; + } + } + + goog.exportPath_(name); +}; + + +/** + * Forward declares a symbol. This is an indication to the compiler that the + * symbol may be used in the source yet is not required and may not be provided + * in compilation. + * + * The most common usage of forward declaration is code that takes a type as a + * function parameter but does not need to require it. By forward declaring + * instead of requiring, no hard dependency is made, and (if not required + * elsewhere) the namespace may never be required and thus, not be pulled + * into the JavaScript binary. If it is required elsewhere, it will be type + * checked as normal. + * + * + * @param {string} name The namespace to forward declare in the form of + * "goog.package.part". + */ +goog.forwardDeclare = function(name) {}; + + +if (!COMPILED) { + + /** + * Check if the given name has been goog.provided. This will return false for + * names that are available only as implicit namespaces. + * @param {string} name name of the object to look for. + * @return {boolean} Whether the name has been provided. + * @private + */ + goog.isProvided_ = function(name) { + return (!goog.implicitNamespaces_[name] && + goog.isDefAndNotNull(goog.getObjectByName(name))); + }; + + /** + * Namespaces implicitly defined by goog.provide. For example, + * goog.provide('goog.events.Event') implicitly declares that 'goog' and + * 'goog.events' must be namespaces. + * + * @type {Object.} + * @private + */ + goog.implicitNamespaces_ = {}; +} + + +/** + * Returns an object based on its fully qualified external name. The object + * is not found if null or undefined. If you are using a compilation pass that + * renames property names beware that using this function will not find renamed + * properties. + * + * @param {string} name The fully qualified name. + * @param {Object=} opt_obj The object within which to look; default is + * |goog.global|. + * @return {?} The value (object or primitive) or, if not found, null. + */ +goog.getObjectByName = function(name, opt_obj) { + var parts = name.split('.'); + var cur = opt_obj || goog.global; + for (var part; part = parts.shift(); ) { + if (goog.isDefAndNotNull(cur[part])) { + cur = cur[part]; + } else { + return null; + } + } + return cur; +}; + + +/** + * Globalizes a whole namespace, such as goog or goog.lang. + * + * @param {Object} obj The namespace to globalize. + * @param {Object=} opt_global The object to add the properties to. + * @deprecated Properties may be explicitly exported to the global scope, but + * this should no longer be done in bulk. + */ +goog.globalize = function(obj, opt_global) { + var global = opt_global || goog.global; + for (var x in obj) { + global[x] = obj[x]; + } +}; + + +/** + * Adds a dependency from a file to the files it requires. + * @param {string} relPath The path to the js file. + * @param {Array} provides An array of strings with the names of the objects + * this file provides. + * @param {Array} requires An array of strings with the names of the objects + * this file requires. + */ +goog.addDependency = function(relPath, provides, requires) { + if (goog.DEPENDENCIES_ENABLED) { + var provide, require; + var path = relPath.replace(/\\/g, '/'); + var deps = goog.dependencies_; + for (var i = 0; provide = provides[i]; i++) { + deps.nameToPath[provide] = path; + } + for (var j = 0; require = requires[j]; j++) { + if (!(path in deps.requires)) { + deps.requires[path] = {}; + } + deps.requires[path][require] = true; + } + } +}; + + + + +// NOTE(nnaze): The debug DOM loader was included in base.js as an original way +// to do "debug-mode" development. The dependency system can sometimes be +// confusing, as can the debug DOM loader's asynchronous nature. +// +// With the DOM loader, a call to goog.require() is not blocking -- the script +// will not load until some point after the current script. If a namespace is +// needed at runtime, it needs to be defined in a previous script, or loaded via +// require() with its registered dependencies. +// User-defined namespaces may need their own deps file. See http://go/js_deps, +// http://go/genjsdeps, or, externally, DepsWriter. +// https://developers.google.com/closure/library/docs/depswriter +// +// Because of legacy clients, the DOM loader can't be easily removed from +// base.js. Work is being done to make it disableable or replaceable for +// different environments (DOM-less JavaScript interpreters like Rhino or V8, +// for example). See bootstrap/ for more information. + + +/** + * @define {boolean} Whether to enable the debug loader. + * + * If enabled, a call to goog.require() will attempt to load the namespace by + * appending a script tag to the DOM (if the namespace has been registered). + * + * If disabled, goog.require() will simply assert that the namespace has been + * provided (and depend on the fact that some outside tool correctly ordered + * the script). + */ +goog.define('goog.ENABLE_DEBUG_LOADER', true); + + +/** + * @param {string} msg + * @private + */ +goog.logToConsole_ = function(msg) { + if (goog.global.console) { + goog.global.console['error'](msg); + } +}; + + +/** + * Implements a system for the dynamic resolution of dependencies that works in + * parallel with the BUILD system. Note that all calls to goog.require will be + * stripped by the JSCompiler when the --closure_pass option is used. + * @see goog.provide + * @param {string} name Namespace to include (as was given in goog.provide()) in + * the form "goog.package.part". + * @return {?} If called within a goog.module file, the associated namespace or + * module otherwise null. + */ +goog.require = function(name) { + + // If the object already exists we do not need do do anything. + if (!COMPILED) { + if (goog.isProvided_(name)) { + return null; + } + + if (goog.ENABLE_DEBUG_LOADER) { + var path = goog.getPathFromDeps_(name); + if (path) { + goog.included_[path] = true; + goog.writeScripts_(); + return null; + } + } + + var errorMessage = 'goog.require could not find: ' + name; + goog.logToConsole_(errorMessage); + + throw Error(errorMessage); + } +}; + + +/** + * Path for included scripts. + * @type {string} + */ +goog.basePath = ''; + + +/** + * A hook for overriding the base path. + * @type {string|undefined} + */ +goog.global.CLOSURE_BASE_PATH; + + +/** + * Whether to write out Closure's deps file. By default, the deps are written. + * @type {boolean|undefined} + */ +goog.global.CLOSURE_NO_DEPS; + + +/** + * A function to import a single script. This is meant to be overridden when + * Closure is being run in non-HTML contexts, such as web workers. It's defined + * in the global scope so that it can be set before base.js is loaded, which + * allows deps.js to be imported properly. + * + * The function is passed the script source, which is a relative URI. It should + * return true if the script was imported, false otherwise. + * @type {(function(string): boolean)|undefined} + */ +goog.global.CLOSURE_IMPORT_SCRIPT; + + +/** + * True if goog.dependencies_ is available. + * @const {boolean} + */ +goog.DEPENDENCIES_ENABLED = !COMPILED && goog.ENABLE_DEBUG_LOADER; + + +if (goog.DEPENDENCIES_ENABLED) { + /** + * Object used to keep track of urls that have already been added. This record + * allows the prevention of circular dependencies. + * @type {Object} + * @private + */ + goog.included_ = {}; + + + /** + * This object is used to keep track of dependencies and other data that is + * used for loading scripts. + * @private + * @type {Object} + */ + goog.dependencies_ = { + nameToPath: {}, // many to 1 + requires: {}, // 1 to many + // Used when resolving dependencies to prevent us from visiting file twice. + visited: {}, + written: {} // Used to keep track of script files we have written. + }; + + + /** + * Tries to detect whether is in the context of an HTML document. + * @return {boolean} True if it looks like HTML document. + * @private + */ + goog.inHtmlDocument_ = function() { + var doc = goog.global.document; + return typeof doc != 'undefined' && + 'write' in doc; // XULDocument misses write. + }; + + + /** + * Tries to detect the base path of base.js script that bootstraps Closure. + * @private + */ + goog.findBasePath_ = function() { + if (goog.global.CLOSURE_BASE_PATH) { + goog.basePath = goog.global.CLOSURE_BASE_PATH; + return; + } else if (!goog.inHtmlDocument_()) { + return; + } + var doc = goog.global.document; + var scripts = doc.getElementsByTagName('script'); + // Search backwards since the current script is in almost all cases the one + // that has base.js. + for (var i = scripts.length - 1; i >= 0; --i) { + var src = scripts[i].src; + var qmark = src.lastIndexOf('?'); + var l = qmark == -1 ? src.length : qmark; + if (src.substr(l - 7, 7) == 'base.js') { + goog.basePath = src.substr(0, l - 7); + return; + } + } + }; + + + /** + * Imports a script if, and only if, that script hasn't already been imported. + * (Must be called at execution time) + * @param {string} src Script source. + * @param {string=} opt_sourceText The optionally source text to evaluate + * @private + */ + goog.importScript_ = function(src, opt_sourceText) { + var importScript = goog.global.CLOSURE_IMPORT_SCRIPT || + goog.writeScriptTag_; + if (importScript(src, opt_sourceText)) { + goog.dependencies_.written[src] = true; + } + }; + + + /** + * The default implementation of the import function. Writes a script tag to + * import the script. + * + * @param {string} src The script url. + * @param {string=} opt_sourceText The optionally source text to evaluate + * @return {boolean} True if the script was imported, false otherwise. + * @private + */ + goog.writeScriptTag_ = function(src, opt_sourceText) { + if (goog.inHtmlDocument_()) { + var doc = goog.global.document; + + // If the user tries to require a new symbol after document load, + // something has gone terribly wrong. Doing a document.write would + // wipe out the page. + if (doc.readyState == 'complete') { + // Certain test frameworks load base.js multiple times, which tries + // to write deps.js each time. If that happens, just fail silently. + // These frameworks wipe the page between each load of base.js, so this + // is OK. + var isDeps = /\bdeps.js$/.test(src); + if (isDeps) { + return false; + } else { + throw Error('Cannot write "' + src + '" after document load'); + } + } + + if (opt_sourceText === undefined) { + doc.write( + ' tag, strip the whitespace. + if data.rstrip(' \t') != data.rstrip(' \t\n\r\f'): + data = data.rstrip(' \t') + self._text += data + else: + self._AppendNewlines(data) + + def handle_comment(self, data): + """Internal handler for HTML comments. + + Args: + data: The text of the comment. + """ + self._AppendNewlines(data) + + def _AppendNewlines(self, data): + """Count the number of newlines in the given string and append them. + + This ensures line numbers are correct for reported errors. + + Args: + data: The data to count newlines in. + """ + # We append 'x' to both sides of the string to ensure that splitlines + # gives us an accurate count. + for i in xrange(len(('x' + data + 'x').splitlines()) - 1): + self._text += '\n' + + def GetScriptLines(self): + """Return the extracted script lines. + + Returns: + The extracted script lines as a list of strings. + """ + return self._text.splitlines() + + +def GetScriptLines(f): + """Extract script tag contents from the given HTML file. + + Args: + f: The HTML file. + + Returns: + Lines in the HTML file that are from script tags. + """ + extractor = ScriptExtractor() + + # The HTML parser chokes on text like Array., so we patch + # that bug by replacing the < with < - escaping all text inside script + # tags would be better but it's a bit of a catch 22. + contents = f.read() + contents = re.sub(r'<([^\s\w/])', + lambda x: '<%s' % x.group(1), + contents) + + extractor.feed(contents) + extractor.close() + return extractor.GetScriptLines() + + +def StripTags(str): + """Returns the string with HTML tags stripped. + + Args: + str: An html string. + + Returns: + The html string with all tags stripped. If there was a parse error, returns + the text successfully parsed so far. + """ + # Brute force approach to stripping as much HTML as possible. If there is a + # parsing error, don't strip text before parse error position, and continue + # trying from there. + final_text = '' + finished = False + while not finished: + try: + strip = _HtmlStripper() + strip.feed(str) + strip.close() + str = strip.get_output() + final_text += str + finished = True + except HTMLParser.HTMLParseError, e: + final_text += str[:e.offset] + str = str[e.offset + 1:] + + return final_text + + +class _HtmlStripper(HTMLParser.HTMLParser): + """Simple class to strip tags from HTML. + + Does so by doing nothing when encountering tags, and appending character data + to a buffer when that is encountered. + """ + def __init__(self): + self.reset() + self.__output = cStringIO.StringIO() + + def handle_data(self, d): + self.__output.write(d) + + def get_output(self): + return self.__output.getvalue() diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/lintrunner.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/lintrunner.py new file mode 100755 index 0000000000..07842c7bfe --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/lintrunner.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +# +# Copyright 2008 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Interface for a lint running wrapper.""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)') + + +class LintRunner(object): + """Interface for a lint running wrapper.""" + + def __init__(self): + if self.__class__ == LintRunner: + raise NotImplementedError('class LintRunner is abstract') + + def Run(self, filenames, error_handler): + """Run a linter on the given filenames. + + Args: + filenames: The filenames to check + error_handler: An ErrorHandler object + + Returns: + The error handler, which may have been used to collect error info. + """ diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/matcher.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/matcher.py new file mode 100755 index 0000000000..9b4402c671 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/matcher.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# +# Copyright 2007 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Regular expression based JavaScript matcher classes.""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)') + +from closure_linter.common import position +from closure_linter.common import tokens + +# Shorthand +Token = tokens.Token +Position = position.Position + + +class Matcher(object): + """A token matcher. + + Specifies a pattern to match, the type of token it represents, what mode the + token changes to, and what mode the token applies to. + + Modes allow more advanced grammars to be incorporated, and are also necessary + to tokenize line by line. We can have different patterns apply to different + modes - i.e. looking for documentation while in comment mode. + + Attributes: + regex: The regular expression representing this matcher. + type: The type of token indicated by a successful match. + result_mode: The mode to move to after a successful match. + """ + + def __init__(self, regex, token_type, result_mode=None, line_start=False): + """Create a new matcher template. + + Args: + regex: The regular expression to match. + token_type: The type of token a successful match indicates. + result_mode: What mode to change to after a successful match. Defaults to + None, which means to not change the current mode. + line_start: Whether this matcher should only match string at the start + of a line. + """ + self.regex = regex + self.type = token_type + self.result_mode = result_mode + self.line_start = line_start diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/position.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/position.py new file mode 100755 index 0000000000..cebf17ef36 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/position.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +# +# Copyright 2008 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Classes to represent positions within strings.""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)') + + +class Position(object): + """Object representing a segment of a string. + + Attributes: + start: The index in to the string where the segment starts. + length: The length of the string segment. + """ + + def __init__(self, start, length): + """Initialize the position object. + + Args: + start: The start index. + length: The number of characters to include. + """ + self.start = start + self.length = length + + def Get(self, string): + """Returns this range of the given string. + + Args: + string: The string to slice. + + Returns: + The string within the range specified by this object. + """ + return string[self.start:self.start + self.length] + + def Set(self, target, source): + """Sets this range within the target string to the source string. + + Args: + target: The target string. + source: The source string. + + Returns: + The resulting string + """ + return target[:self.start] + source + target[self.start + self.length:] + + def AtEnd(string): + """Create a Position representing the end of the given string. + + Args: + string: The string to represent the end of. + + Returns: + The created Position object. + """ + return Position(len(string), 0) + AtEnd = staticmethod(AtEnd) + + def IsAtEnd(self, string): + """Returns whether this position is at the end of the given string. + + Args: + string: The string to test for the end of. + + Returns: + Whether this position is at the end of the given string. + """ + return self.start == len(string) and self.length == 0 + + def AtBeginning(): + """Create a Position representing the beginning of any string. + + Returns: + The created Position object. + """ + return Position(0, 0) + AtBeginning = staticmethod(AtBeginning) + + def IsAtBeginning(self): + """Returns whether this position is at the beginning of any string. + + Returns: + Whether this position is at the beginning of any string. + """ + return self.start == 0 and self.length == 0 + + def All(string): + """Create a Position representing the entire string. + + Args: + string: The string to represent the entirety of. + + Returns: + The created Position object. + """ + return Position(0, len(string)) + All = staticmethod(All) + + def Index(index): + """Returns a Position object for the specified index. + + Args: + index: The index to select, inclusively. + + Returns: + The created Position object. + """ + return Position(index, 1) + Index = staticmethod(Index) diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/simplefileflags.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/simplefileflags.py new file mode 100755 index 0000000000..3402bef3a1 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/simplefileflags.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python +# +# Copyright 2008 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Determines the list of files to be checked from command line arguments.""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)') + +import glob +import os +import re + +import gflags as flags + + +FLAGS = flags.FLAGS + +flags.DEFINE_multistring( + 'recurse', + None, + 'Recurse in to the subdirectories of the given path', + short_name='r') +flags.DEFINE_list( + 'exclude_directories', + ('_demos'), + 'Exclude the specified directories (only applicable along with -r or ' + '--presubmit)', + short_name='e') +flags.DEFINE_list( + 'exclude_files', + ('deps.js'), + 'Exclude the specified files', + short_name='x') + + +def MatchesSuffixes(filename, suffixes): + """Returns whether the given filename matches one of the given suffixes. + + Args: + filename: Filename to check. + suffixes: Sequence of suffixes to check. + + Returns: + Whether the given filename matches one of the given suffixes. + """ + suffix = filename[filename.rfind('.'):] + return suffix in suffixes + + +def _GetUserSpecifiedFiles(argv, suffixes): + """Returns files to be linted, specified directly on the command line. + + Can handle the '*' wildcard in filenames, but no other wildcards. + + Args: + argv: Sequence of command line arguments. The second and following arguments + are assumed to be files that should be linted. + suffixes: Expected suffixes for the file type being checked. + + Returns: + A sequence of files to be linted. + """ + files = argv[1:] or [] + all_files = [] + lint_files = [] + + # Perform any necessary globs. + for f in files: + if f.find('*') != -1: + for result in glob.glob(f): + all_files.append(result) + else: + all_files.append(f) + + for f in all_files: + if MatchesSuffixes(f, suffixes): + lint_files.append(f) + return lint_files + + +def _GetRecursiveFiles(suffixes): + """Returns files to be checked specified by the --recurse flag. + + Args: + suffixes: Expected suffixes for the file type being checked. + + Returns: + A list of files to be checked. + """ + lint_files = [] + # Perform any request recursion + if FLAGS.recurse: + for start in FLAGS.recurse: + for root, subdirs, files in os.walk(start): + for f in files: + if MatchesSuffixes(f, suffixes): + lint_files.append(os.path.join(root, f)) + return lint_files + + +def GetAllSpecifiedFiles(argv, suffixes): + """Returns all files specified by the user on the commandline. + + Args: + argv: Sequence of command line arguments. The second and following arguments + are assumed to be files that should be linted. + suffixes: Expected suffixes for the file type + + Returns: + A list of all files specified directly or indirectly (via flags) on the + command line by the user. + """ + files = _GetUserSpecifiedFiles(argv, suffixes) + + if FLAGS.recurse: + files += _GetRecursiveFiles(suffixes) + + return FilterFiles(files) + + +def FilterFiles(files): + """Filters the list of files to be linted be removing any excluded files. + + Filters out files excluded using --exclude_files and --exclude_directories. + + Args: + files: Sequence of files that needs filtering. + + Returns: + Filtered list of files to be linted. + """ + num_files = len(files) + + ignore_dirs_regexs = [] + for ignore in FLAGS.exclude_directories: + ignore_dirs_regexs.append(re.compile(r'(^|[\\/])%s[\\/]' % ignore)) + + result_files = [] + for f in files: + add_file = True + for exclude in FLAGS.exclude_files: + if f.endswith('/' + exclude) or f == exclude: + add_file = False + break + for ignore in ignore_dirs_regexs: + if ignore.search(f): + # Break out of ignore loop so we don't add to + # filtered files. + add_file = False + break + if add_file: + # Convert everything to absolute paths so we can easily remove duplicates + # using a set. + result_files.append(os.path.abspath(f)) + + skipped = num_files - len(result_files) + if skipped: + print 'Skipping %d file(s).' % skipped + + return set(result_files) + + +def GetFileList(argv, file_type, suffixes): + """Parse the flags and return the list of files to check. + + Args: + argv: Sequence of command line arguments. + suffixes: Sequence of acceptable suffixes for the file type. + + Returns: + The list of files to check. + """ + return sorted(GetAllSpecifiedFiles(argv, suffixes)) + + +def IsEmptyArgumentList(argv): + return not (len(argv[1:]) or FLAGS.recurse) diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/tokenizer.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/tokenizer.py new file mode 100755 index 0000000000..9420ea3267 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/tokenizer.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python +# +# Copyright 2007 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Regular expression based lexer.""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)') + +from closure_linter.common import tokens + +# Shorthand +Type = tokens.TokenType + + +class Tokenizer(object): + """General purpose tokenizer. + + Attributes: + mode: The latest mode of the tokenizer. This allows patterns to distinguish + if they are mid-comment, mid-parameter list, etc. + matchers: Dictionary of modes to sequences of matchers that define the + patterns to check at any given time. + default_types: Dictionary of modes to types, defining what type to give + non-matched text when in the given mode. Defaults to Type.NORMAL. + """ + + def __init__(self, starting_mode, matchers, default_types): + """Initialize the tokenizer. + + Args: + starting_mode: Mode to start in. + matchers: Dictionary of modes to sequences of matchers that defines the + patterns to check at any given time. + default_types: Dictionary of modes to types, defining what type to give + non-matched text when in the given mode. Defaults to Type.NORMAL. + """ + self.__starting_mode = starting_mode + self.matchers = matchers + self.default_types = default_types + + def TokenizeFile(self, file): + """Tokenizes the given file. + + Args: + file: An iterable that yields one line of the file at a time. + + Returns: + The first token in the file + """ + # The current mode. + self.mode = self.__starting_mode + # The first token in the stream. + self.__first_token = None + # The last token added to the token stream. + self.__last_token = None + # The current line number. + self.__line_number = 0 + + for line in file: + self.__line_number += 1 + self.__TokenizeLine(line) + + return self.__first_token + + def _CreateToken(self, string, token_type, line, line_number, values=None): + """Creates a new Token object (or subclass). + + Args: + string: The string of input the token represents. + token_type: The type of token. + line: The text of the line this token is in. + line_number: The line number of the token. + values: A dict of named values within the token. For instance, a + function declaration may have a value called 'name' which captures the + name of the function. + + Returns: + The newly created Token object. + """ + return tokens.Token(string, token_type, line, line_number, values, + line_number) + + def __TokenizeLine(self, line): + """Tokenizes the given line. + + Args: + line: The contents of the line. + """ + string = line.rstrip('\n\r\f') + line_number = self.__line_number + self.__start_index = 0 + + if not string: + self.__AddToken(self._CreateToken('', Type.BLANK_LINE, line, line_number)) + return + + normal_token = '' + index = 0 + while index < len(string): + for matcher in self.matchers[self.mode]: + if matcher.line_start and index > 0: + continue + + match = matcher.regex.match(string, index) + + if match: + if normal_token: + self.__AddToken( + self.__CreateNormalToken(self.mode, normal_token, line, + line_number)) + normal_token = '' + + # Add the match. + self.__AddToken(self._CreateToken(match.group(), matcher.type, line, + line_number, match.groupdict())) + + # Change the mode to the correct one for after this match. + self.mode = matcher.result_mode or self.mode + + # Shorten the string to be matched. + index = match.end() + + break + + else: + # If the for loop finishes naturally (i.e. no matches) we just add the + # first character to the string of consecutive non match characters. + # These will constitute a NORMAL token. + if string: + normal_token += string[index:index + 1] + index += 1 + + if normal_token: + self.__AddToken( + self.__CreateNormalToken(self.mode, normal_token, line, line_number)) + + def __CreateNormalToken(self, mode, string, line, line_number): + """Creates a normal token. + + Args: + mode: The current mode. + string: The string to tokenize. + line: The line of text. + line_number: The line number within the file. + + Returns: + A Token object, of the default type for the current mode. + """ + type = Type.NORMAL + if mode in self.default_types: + type = self.default_types[mode] + return self._CreateToken(string, type, line, line_number) + + def __AddToken(self, token): + """Add the given token to the token stream. + + Args: + token: The token to add. + """ + # Store the first token, or point the previous token to this one. + if not self.__first_token: + self.__first_token = token + else: + self.__last_token.next = token + + # Establish the doubly linked list + token.previous = self.__last_token + self.__last_token = token + + # Compute the character indices + token.start_index = self.__start_index + self.__start_index += token.length diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/tokens.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/tokens.py new file mode 100755 index 0000000000..4703998752 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/tokens.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +# +# Copyright 2008 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Classes to represent tokens and positions within them.""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)') + + +class TokenType(object): + """Token types common to all languages.""" + NORMAL = 'normal' + WHITESPACE = 'whitespace' + BLANK_LINE = 'blank line' + + +class Token(object): + """Token class for intelligent text splitting. + + The token class represents a string of characters and an identifying type. + + Attributes: + type: The type of token. + string: The characters the token comprises. + length: The length of the token. + line: The text of the line the token is found in. + line_number: The number of the line the token is found in. + values: Dictionary of values returned from the tokens regex match. + previous: The token before this one. + next: The token after this one. + start_index: The character index in the line where this token starts. + attached_object: Object containing more information about this token. + metadata: Object containing metadata about this token. Must be added by + a separate metadata pass. + """ + + def __init__(self, string, token_type, line, line_number, values=None, + orig_line_number=None): + """Creates a new Token object. + + Args: + string: The string of input the token contains. + token_type: The type of token. + line: The text of the line this token is in. + line_number: The line number of the token. + values: A dict of named values within the token. For instance, a + function declaration may have a value called 'name' which captures the + name of the function. + orig_line_number: The line number of the original file this token comes + from. This should be only set during the tokenization process. For newly + created error fix tokens after that, it should be None. + """ + self.type = token_type + self.string = string + self.length = len(string) + self.line = line + self.line_number = line_number + self.orig_line_number = orig_line_number + self.values = values + self.is_deleted = False + + # These parts can only be computed when the file is fully tokenized + self.previous = None + self.next = None + self.start_index = None + + # This part is set in statetracker.py + # TODO(robbyw): Wrap this in to metadata + self.attached_object = None + + # This part is set in *metadatapass.py + self.metadata = None + + def IsFirstInLine(self): + """Tests if this token is the first token in its line. + + Returns: + Whether the token is the first token in its line. + """ + return not self.previous or self.previous.line_number != self.line_number + + def IsLastInLine(self): + """Tests if this token is the last token in its line. + + Returns: + Whether the token is the last token in its line. + """ + return not self.next or self.next.line_number != self.line_number + + def IsType(self, token_type): + """Tests if this token is of the given type. + + Args: + token_type: The type to test for. + + Returns: + True if the type of this token matches the type passed in. + """ + return self.type == token_type + + def IsAnyType(self, *token_types): + """Tests if this token is any of the given types. + + Args: + token_types: The types to check. Also accepts a single array. + + Returns: + True if the type of this token is any of the types passed in. + """ + if not isinstance(token_types[0], basestring): + return self.type in token_types[0] + else: + return self.type in token_types + + def __repr__(self): + return '' % (self.type, self.string, + self.values, self.line_number, + self.metadata) + + def __iter__(self): + """Returns a token iterator.""" + node = self + while node: + yield node + node = node.next + + def __reversed__(self): + """Returns a reverse-direction token iterator.""" + node = self + while node: + yield node + node = node.previous diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/tokens_test.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/tokens_test.py new file mode 100644 index 0000000000..01ec89d01b --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/common/tokens_test.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# Copyright 2011 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +__author__ = 'nnaze@google.com (Nathan Naze)' + +import unittest as googletest +from closure_linter.common import tokens + + +def _CreateDummyToken(): + return tokens.Token('foo', None, 1, 1) + + +def _CreateDummyTokens(count): + dummy_tokens = [] + for _ in xrange(count): + dummy_tokens.append(_CreateDummyToken()) + return dummy_tokens + + +def _SetTokensAsNeighbors(neighbor_tokens): + for i in xrange(len(neighbor_tokens)): + prev_index = i - 1 + next_index = i + 1 + + if prev_index >= 0: + neighbor_tokens[i].previous = neighbor_tokens[prev_index] + + if next_index < len(neighbor_tokens): + neighbor_tokens[i].next = neighbor_tokens[next_index] + + +class TokensTest(googletest.TestCase): + + def testIsFirstInLine(self): + + # First token in file (has no previous). + self.assertTrue(_CreateDummyToken().IsFirstInLine()) + + a, b = _CreateDummyTokens(2) + _SetTokensAsNeighbors([a, b]) + + # Tokens on same line + a.line_number = 30 + b.line_number = 30 + + self.assertFalse(b.IsFirstInLine()) + + # Tokens on different lines + b.line_number = 31 + self.assertTrue(b.IsFirstInLine()) + + def testIsLastInLine(self): + # Last token in file (has no next). + self.assertTrue(_CreateDummyToken().IsLastInLine()) + + a, b = _CreateDummyTokens(2) + _SetTokensAsNeighbors([a, b]) + + # Tokens on same line + a.line_number = 30 + b.line_number = 30 + self.assertFalse(a.IsLastInLine()) + + b.line_number = 31 + self.assertTrue(a.IsLastInLine()) + + def testIsType(self): + a = tokens.Token('foo', 'fakeType1', 1, 1) + self.assertTrue(a.IsType('fakeType1')) + self.assertFalse(a.IsType('fakeType2')) + + def testIsAnyType(self): + a = tokens.Token('foo', 'fakeType1', 1, 1) + self.assertTrue(a.IsAnyType(['fakeType1', 'fakeType2'])) + self.assertFalse(a.IsAnyType(['fakeType3', 'fakeType4'])) + + def testRepr(self): + a = tokens.Token('foo', 'fakeType1', 1, 1) + self.assertEquals('', str(a)) + + def testIter(self): + dummy_tokens = _CreateDummyTokens(5) + _SetTokensAsNeighbors(dummy_tokens) + a, b, c, d, e = dummy_tokens + + i = iter(a) + self.assertListEqual([a, b, c, d, e], list(i)) + + def testReverseIter(self): + dummy_tokens = _CreateDummyTokens(5) + _SetTokensAsNeighbors(dummy_tokens) + a, b, c, d, e = dummy_tokens + + ri = reversed(e) + self.assertListEqual([e, d, c, b, a], list(ri)) + + +if __name__ == '__main__': + googletest.main() diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/ecmalintrules.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/ecmalintrules.py new file mode 100755 index 0000000000..0db4d34374 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/ecmalintrules.py @@ -0,0 +1,828 @@ +#!/usr/bin/env python +# +# Copyright 2008 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Core methods for checking EcmaScript files for common style guide violations. +""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)', + 'jacobr@google.com (Jacob Richman)') + +import re + +import gflags as flags + +from closure_linter import checkerbase +from closure_linter import ecmametadatapass +from closure_linter import error_check +from closure_linter import errorrules +from closure_linter import errors +from closure_linter import indentation +from closure_linter import javascripttokenizer +from closure_linter import javascripttokens +from closure_linter import statetracker +from closure_linter import tokenutil +from closure_linter.common import error +from closure_linter.common import position + + +FLAGS = flags.FLAGS +flags.DEFINE_list('custom_jsdoc_tags', '', 'Extra jsdoc tags to allow') + +# TODO(robbyw): Check for extra parens on return statements +# TODO(robbyw): Check for 0px in strings +# TODO(robbyw): Ensure inline jsDoc is in {} +# TODO(robbyw): Check for valid JS types in parameter docs + +# Shorthand +Context = ecmametadatapass.EcmaContext +Error = error.Error +Modes = javascripttokenizer.JavaScriptModes +Position = position.Position +Rule = error_check.Rule +Type = javascripttokens.JavaScriptTokenType + + +class EcmaScriptLintRules(checkerbase.LintRulesBase): + """EmcaScript lint style checking rules. + + Can be used to find common style errors in JavaScript, ActionScript and other + Ecma like scripting languages. Style checkers for Ecma scripting languages + should inherit from this style checker. + Please do not add any state to EcmaScriptLintRules or to any subclasses. + + All state should be added to the StateTracker subclass used for a particular + language. + """ + + # It will be initialized in constructor so the flags are initialized. + max_line_length = -1 + + # Static constants. + MISSING_PARAMETER_SPACE = re.compile(r',\S') + + EXTRA_SPACE = re.compile(r'(\(\s|\s\))') + + ENDS_WITH_SPACE = re.compile(r'\s$') + + ILLEGAL_TAB = re.compile(r'\t') + + # Regex used to split up complex types to check for invalid use of ? and |. + TYPE_SPLIT = re.compile(r'[,<>()]') + + # Regex for form of author lines after the @author tag. + AUTHOR_SPEC = re.compile(r'(\s*)[^\s]+@[^(\s]+(\s*)\(.+\)') + + # Acceptable tokens to remove for line too long testing. + LONG_LINE_IGNORE = frozenset( + ['*', '//', '@see'] + + ['@%s' % tag for tag in statetracker.DocFlag.HAS_TYPE]) + + JSDOC_FLAGS_DESCRIPTION_NOT_REQUIRED = frozenset([ + '@param', '@return', '@returns']) + + def __init__(self): + """Initialize this lint rule object.""" + checkerbase.LintRulesBase.__init__(self) + if EcmaScriptLintRules.max_line_length == -1: + EcmaScriptLintRules.max_line_length = errorrules.GetMaxLineLength() + + def Initialize(self, checker, limited_doc_checks, is_html): + """Initialize this lint rule object before parsing a new file.""" + checkerbase.LintRulesBase.Initialize(self, checker, limited_doc_checks, + is_html) + self._indentation = indentation.IndentationRules() + + def HandleMissingParameterDoc(self, token, param_name): + """Handle errors associated with a parameter missing a @param tag.""" + raise TypeError('Abstract method HandleMissingParameterDoc not implemented') + + def _CheckLineLength(self, last_token, state): + """Checks whether the line is too long. + + Args: + last_token: The last token in the line. + state: parser_state object that indicates the current state in the page + """ + # Start from the last token so that we have the flag object attached to + # and DOC_FLAG tokens. + line_number = last_token.line_number + token = last_token + + # Build a representation of the string where spaces indicate potential + # line-break locations. + line = [] + while token and token.line_number == line_number: + if state.IsTypeToken(token): + line.insert(0, 'x' * len(token.string)) + elif token.type in (Type.IDENTIFIER, Type.NORMAL): + # Dots are acceptable places to wrap. + line.insert(0, token.string.replace('.', ' ')) + else: + line.insert(0, token.string) + token = token.previous + + line = ''.join(line) + line = line.rstrip('\n\r\f') + try: + length = len(unicode(line, 'utf-8')) + except LookupError: + # Unknown encoding. The line length may be wrong, as was originally the + # case for utf-8 (see bug 1735846). For now just accept the default + # length, but as we find problems we can either add test for other + # possible encodings or return without an error to protect against + # false positives at the cost of more false negatives. + length = len(line) + + if length > EcmaScriptLintRules.max_line_length: + + # If the line matches one of the exceptions, then it's ok. + for long_line_regexp in self.GetLongLineExceptions(): + if long_line_regexp.match(last_token.line): + return + + # If the line consists of only one "word", or multiple words but all + # except one are ignoreable, then it's ok. + parts = set(line.split()) + + # We allow two "words" (type and name) when the line contains @param + max_parts = 1 + if '@param' in parts: + max_parts = 2 + + # Custom tags like @requires may have url like descriptions, so ignore + # the tag, similar to how we handle @see. + custom_tags = set(['@%s' % f for f in FLAGS.custom_jsdoc_tags]) + if (len(parts.difference(self.LONG_LINE_IGNORE | custom_tags)) + > max_parts): + self._HandleError( + errors.LINE_TOO_LONG, + 'Line too long (%d characters).' % len(line), last_token) + + def _CheckJsDocType(self, token): + """Checks the given type for style errors. + + Args: + token: The DOC_FLAG token for the flag whose type to check. + """ + flag = token.attached_object + flag_type = flag.type + if flag_type and flag_type is not None and not flag_type.isspace(): + pieces = self.TYPE_SPLIT.split(flag_type) + if len(pieces) == 1 and flag_type.count('|') == 1 and ( + flag_type.endswith('|null') or flag_type.startswith('null|')): + self._HandleError( + errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL, + 'Prefer "?Type" to "Type|null": "%s"' % flag_type, token) + + # TODO(user): We should do actual parsing of JsDoc types to report an + # error for wrong usage of '?' and '|' e.g. {?number|string|null} etc. + + if error_check.ShouldCheck(Rule.BRACES_AROUND_TYPE) and ( + flag.type_start_token.type != Type.DOC_START_BRACE or + flag.type_end_token.type != Type.DOC_END_BRACE): + self._HandleError( + errors.MISSING_BRACES_AROUND_TYPE, + 'Type must always be surrounded by curly braces.', token) + + def _CheckForMissingSpaceBeforeToken(self, token): + """Checks for a missing space at the beginning of a token. + + Reports a MISSING_SPACE error if the token does not begin with a space or + the previous token doesn't end with a space and the previous token is on the + same line as the token. + + Args: + token: The token being checked + """ + # TODO(user): Check if too many spaces? + if (len(token.string) == len(token.string.lstrip()) and + token.previous and token.line_number == token.previous.line_number and + len(token.previous.string) - len(token.previous.string.rstrip()) == 0): + self._HandleError( + errors.MISSING_SPACE, + 'Missing space before "%s"' % token.string, + token, + position=Position.AtBeginning()) + + def _CheckOperator(self, token): + """Checks an operator for spacing and line style. + + Args: + token: The operator token. + """ + last_code = token.metadata.last_code + + if not self._ExpectSpaceBeforeOperator(token): + if (token.previous and token.previous.type == Type.WHITESPACE and + last_code and last_code.type in (Type.NORMAL, Type.IDENTIFIER)): + self._HandleError( + errors.EXTRA_SPACE, 'Extra space before "%s"' % token.string, + token.previous, position=Position.All(token.previous.string)) + + elif (token.previous and + not token.previous.IsComment() and + token.previous.type in Type.EXPRESSION_ENDER_TYPES): + self._HandleError(errors.MISSING_SPACE, + 'Missing space before "%s"' % token.string, token, + position=Position.AtBeginning()) + + # Check that binary operators are not used to start lines. + if ((not last_code or last_code.line_number != token.line_number) and + not token.metadata.IsUnaryOperator()): + self._HandleError( + errors.LINE_STARTS_WITH_OPERATOR, + 'Binary operator should go on previous line "%s"' % token.string, + token) + + def _ExpectSpaceBeforeOperator(self, token): + """Returns whether a space should appear before the given operator token. + + Args: + token: The operator token. + + Returns: + Whether there should be a space before the token. + """ + if token.string == ',' or token.metadata.IsUnaryPostOperator(): + return False + + # Colons should appear in labels, object literals, the case of a switch + # statement, and ternary operator. Only want a space in the case of the + # ternary operator. + if (token.string == ':' and + token.metadata.context.type in (Context.LITERAL_ELEMENT, + Context.CASE_BLOCK, + Context.STATEMENT)): + return False + + if token.metadata.IsUnaryOperator() and token.IsFirstInLine(): + return False + + return True + + def CheckToken(self, token, state): + """Checks a token, given the current parser_state, for warnings and errors. + + Args: + token: The current token under consideration + state: parser_state object that indicates the current state in the page + """ + # Store some convenience variables + first_in_line = token.IsFirstInLine() + last_in_line = token.IsLastInLine() + last_non_space_token = state.GetLastNonSpaceToken() + + token_type = token.type + + # Process the line change. + if not self._is_html and error_check.ShouldCheck(Rule.INDENTATION): + # TODO(robbyw): Support checking indentation in HTML files. + indentation_errors = self._indentation.CheckToken(token, state) + for indentation_error in indentation_errors: + self._HandleError(*indentation_error) + + if last_in_line: + self._CheckLineLength(token, state) + + if token_type == Type.PARAMETERS: + # Find missing spaces in parameter lists. + if self.MISSING_PARAMETER_SPACE.search(token.string): + fix_data = ', '.join([s.strip() for s in token.string.split(',')]) + self._HandleError(errors.MISSING_SPACE, 'Missing space after ","', + token, position=None, fix_data=fix_data.strip()) + + # Find extra spaces at the beginning of parameter lists. Make sure + # we aren't at the beginning of a continuing multi-line list. + if not first_in_line: + space_count = len(token.string) - len(token.string.lstrip()) + if space_count: + self._HandleError(errors.EXTRA_SPACE, 'Extra space after "("', + token, position=Position(0, space_count)) + + elif (token_type == Type.START_BLOCK and + token.metadata.context.type == Context.BLOCK): + self._CheckForMissingSpaceBeforeToken(token) + + elif token_type == Type.END_BLOCK: + # This check is for object literal end block tokens, but there is no need + # to test that condition since a comma at the end of any other kind of + # block is undoubtedly a parse error. + last_code = token.metadata.last_code + if last_code.IsOperator(','): + self._HandleError( + errors.COMMA_AT_END_OF_LITERAL, + 'Illegal comma at end of object literal', last_code, + position=Position.All(last_code.string)) + + if state.InFunction() and state.IsFunctionClose(): + is_immediately_called = (token.next and + token.next.type == Type.START_PAREN) + if state.InTopLevelFunction(): + # A semicolons should not be included at the end of a function + # declaration. + if not state.InAssignedFunction(): + if not last_in_line and token.next.type == Type.SEMICOLON: + self._HandleError( + errors.ILLEGAL_SEMICOLON_AFTER_FUNCTION, + 'Illegal semicolon after function declaration', + token.next, position=Position.All(token.next.string)) + + # A semicolon should be included at the end of a function expression + # that is not immediately called. + if state.InAssignedFunction(): + if not is_immediately_called and ( + last_in_line or token.next.type != Type.SEMICOLON): + self._HandleError( + errors.MISSING_SEMICOLON_AFTER_FUNCTION, + 'Missing semicolon after function assigned to a variable', + token, position=Position.AtEnd(token.string)) + + if state.InInterfaceMethod() and last_code.type != Type.START_BLOCK: + self._HandleError(errors.INTERFACE_METHOD_CANNOT_HAVE_CODE, + 'Interface methods cannot contain code', last_code) + + elif (state.IsBlockClose() and + token.next and token.next.type == Type.SEMICOLON): + if (last_code.metadata.context.parent.type != Context.OBJECT_LITERAL + and last_code.metadata.context.type != Context.OBJECT_LITERAL): + self._HandleError( + errors.REDUNDANT_SEMICOLON, + 'No semicolon is required to end a code block', + token.next, position=Position.All(token.next.string)) + + elif token_type == Type.SEMICOLON: + if token.previous and token.previous.type == Type.WHITESPACE: + self._HandleError( + errors.EXTRA_SPACE, 'Extra space before ";"', + token.previous, position=Position.All(token.previous.string)) + + if token.next and token.next.line_number == token.line_number: + if token.metadata.context.type != Context.FOR_GROUP_BLOCK: + # TODO(robbyw): Error about no multi-statement lines. + pass + + elif token.next.type not in ( + Type.WHITESPACE, Type.SEMICOLON, Type.END_PAREN): + self._HandleError( + errors.MISSING_SPACE, + 'Missing space after ";" in for statement', + token.next, + position=Position.AtBeginning()) + + last_code = token.metadata.last_code + if last_code and last_code.type == Type.SEMICOLON: + # Allow a single double semi colon in for loops for cases like: + # for (;;) { }. + # NOTE(user): This is not a perfect check, and will not throw an error + # for cases like: for (var i = 0;; i < n; i++) {}, but then your code + # probably won't work either. + for_token = tokenutil.CustomSearch( + last_code, + lambda token: token.type == Type.KEYWORD and token.string == 'for', + end_func=lambda token: token.type == Type.SEMICOLON, + distance=None, + reverse=True) + + if not for_token: + self._HandleError(errors.REDUNDANT_SEMICOLON, 'Redundant semicolon', + token, position=Position.All(token.string)) + + elif token_type == Type.START_PAREN: + # Ensure that opening parentheses have a space before any keyword + # that is not being invoked like a member function. + if (token.previous and token.previous.type == Type.KEYWORD and + (not token.previous.metadata or + not token.previous.metadata.last_code or + not token.previous.metadata.last_code.string or + token.previous.metadata.last_code.string[-1:] != '.')): + self._HandleError(errors.MISSING_SPACE, 'Missing space before "("', + token, position=Position.AtBeginning()) + elif token.previous and token.previous.type == Type.WHITESPACE: + before_space = token.previous.previous + # Ensure that there is no extra space before a function invocation, + # even if the function being invoked happens to be a keyword. + if (before_space and before_space.line_number == token.line_number and + before_space.type == Type.IDENTIFIER or + (before_space.type == Type.KEYWORD and before_space.metadata and + before_space.metadata.last_code and + before_space.metadata.last_code.string and + before_space.metadata.last_code.string[-1:] == '.')): + self._HandleError( + errors.EXTRA_SPACE, 'Extra space before "("', + token.previous, position=Position.All(token.previous.string)) + + elif token_type == Type.START_BRACKET: + self._HandleStartBracket(token, last_non_space_token) + elif token_type in (Type.END_PAREN, Type.END_BRACKET): + # Ensure there is no space before closing parentheses, except when + # it's in a for statement with an omitted section, or when it's at the + # beginning of a line. + if (token.previous and token.previous.type == Type.WHITESPACE and + not token.previous.IsFirstInLine() and + not (last_non_space_token and last_non_space_token.line_number == + token.line_number and + last_non_space_token.type == Type.SEMICOLON)): + self._HandleError( + errors.EXTRA_SPACE, 'Extra space before "%s"' % + token.string, token.previous, + position=Position.All(token.previous.string)) + + if token.type == Type.END_BRACKET: + last_code = token.metadata.last_code + if last_code.IsOperator(','): + self._HandleError( + errors.COMMA_AT_END_OF_LITERAL, + 'Illegal comma at end of array literal', last_code, + position=Position.All(last_code.string)) + + elif token_type == Type.WHITESPACE: + if self.ILLEGAL_TAB.search(token.string): + if token.IsFirstInLine(): + if token.next: + self._HandleError( + errors.ILLEGAL_TAB, + 'Illegal tab in whitespace before "%s"' % token.next.string, + token, position=Position.All(token.string)) + else: + self._HandleError( + errors.ILLEGAL_TAB, + 'Illegal tab in whitespace', + token, position=Position.All(token.string)) + else: + self._HandleError( + errors.ILLEGAL_TAB, + 'Illegal tab in whitespace after "%s"' % token.previous.string, + token, position=Position.All(token.string)) + + # Check whitespace length if it's not the first token of the line and + # if it's not immediately before a comment. + if last_in_line: + # Check for extra whitespace at the end of a line. + self._HandleError(errors.EXTRA_SPACE, 'Extra space at end of line', + token, position=Position.All(token.string)) + elif not first_in_line and not token.next.IsComment(): + if token.length > 1: + self._HandleError( + errors.EXTRA_SPACE, 'Extra space after "%s"' % + token.previous.string, token, + position=Position(1, len(token.string) - 1)) + + elif token_type == Type.OPERATOR: + self._CheckOperator(token) + elif token_type == Type.DOC_FLAG: + flag = token.attached_object + + if flag.flag_type == 'bug': + # TODO(robbyw): Check for exactly 1 space on the left. + string = token.next.string.lstrip() + string = string.split(' ', 1)[0] + + if not string.isdigit(): + self._HandleError(errors.NO_BUG_NUMBER_AFTER_BUG_TAG, + '@bug should be followed by a bug number', token) + + elif flag.flag_type == 'suppress': + if flag.type is None: + # A syntactically invalid suppress tag will get tokenized as a normal + # flag, indicating an error. + self._HandleError( + errors.INCORRECT_SUPPRESS_SYNTAX, + 'Invalid suppress syntax: should be @suppress {errortype}. ' + 'Spaces matter.', token) + else: + for suppress_type in re.split(r'\||,', flag.type): + if suppress_type not in state.GetDocFlag().SUPPRESS_TYPES: + self._HandleError( + errors.INVALID_SUPPRESS_TYPE, + 'Invalid suppression type: %s' % suppress_type, token) + + elif (error_check.ShouldCheck(Rule.WELL_FORMED_AUTHOR) and + flag.flag_type == 'author'): + # TODO(user): In non strict mode check the author tag for as much as + # it exists, though the full form checked below isn't required. + string = token.next.string + result = self.AUTHOR_SPEC.match(string) + if not result: + self._HandleError(errors.INVALID_AUTHOR_TAG_DESCRIPTION, + 'Author tag line should be of the form: ' + '@author foo@somewhere.com (Your Name)', + token.next) + else: + # Check spacing between email address and name. Do this before + # checking earlier spacing so positions are easier to calculate for + # autofixing. + num_spaces = len(result.group(2)) + if num_spaces < 1: + self._HandleError(errors.MISSING_SPACE, + 'Missing space after email address', + token.next, position=Position(result.start(2), 0)) + elif num_spaces > 1: + self._HandleError( + errors.EXTRA_SPACE, 'Extra space after email address', + token.next, + position=Position(result.start(2) + 1, num_spaces - 1)) + + # Check for extra spaces before email address. Can't be too few, if + # not at least one we wouldn't match @author tag. + num_spaces = len(result.group(1)) + if num_spaces > 1: + self._HandleError(errors.EXTRA_SPACE, + 'Extra space before email address', + token.next, position=Position(1, num_spaces - 1)) + + elif (flag.flag_type in state.GetDocFlag().HAS_DESCRIPTION and + not self._limited_doc_checks): + if flag.flag_type == 'param': + if flag.name is None: + self._HandleError(errors.MISSING_JSDOC_PARAM_NAME, + 'Missing name in @param tag', token) + + if not flag.description or flag.description is None: + flag_name = token.type + if 'name' in token.values: + flag_name = '@' + token.values['name'] + + if flag_name not in self.JSDOC_FLAGS_DESCRIPTION_NOT_REQUIRED: + self._HandleError( + errors.MISSING_JSDOC_TAG_DESCRIPTION, + 'Missing description in %s tag' % flag_name, token) + else: + self._CheckForMissingSpaceBeforeToken(flag.description_start_token) + + if flag.flag_type in state.GetDocFlag().HAS_TYPE: + if flag.type_start_token is not None: + self._CheckForMissingSpaceBeforeToken( + token.attached_object.type_start_token) + + if flag.type and not flag.type.isspace(): + self._CheckJsDocType(token) + + if token_type in (Type.DOC_FLAG, Type.DOC_INLINE_FLAG): + if (token.values['name'] not in state.GetDocFlag().LEGAL_DOC and + token.values['name'] not in FLAGS.custom_jsdoc_tags): + self._HandleError( + errors.INVALID_JSDOC_TAG, + 'Invalid JsDoc tag: %s' % token.values['name'], token) + + if (error_check.ShouldCheck(Rule.NO_BRACES_AROUND_INHERIT_DOC) and + token.values['name'] == 'inheritDoc' and + token_type == Type.DOC_INLINE_FLAG): + self._HandleError(errors.UNNECESSARY_BRACES_AROUND_INHERIT_DOC, + 'Unnecessary braces around @inheritDoc', + token) + + elif token_type == Type.SIMPLE_LVALUE: + identifier = token.values['identifier'] + + if ((not state.InFunction() or state.InConstructor()) and + state.InTopLevel() and not state.InObjectLiteralDescendant()): + jsdoc = state.GetDocComment() + if not state.HasDocComment(identifier): + # Only test for documentation on identifiers with .s in them to + # avoid checking things like simple variables. We don't require + # documenting assignments to .prototype itself (bug 1880803). + if (not state.InConstructor() and + identifier.find('.') != -1 and not + identifier.endswith('.prototype') and not + self._limited_doc_checks): + comment = state.GetLastComment() + if not (comment and comment.lower().count('jsdoc inherited')): + self._HandleError( + errors.MISSING_MEMBER_DOCUMENTATION, + "No docs found for member '%s'" % identifier, + token) + elif jsdoc and (not state.InConstructor() or + identifier.startswith('this.')): + # We are at the top level and the function/member is documented. + if identifier.endswith('_') and not identifier.endswith('__'): + # Can have a private class which inherits documentation from a + # public superclass. + # + # @inheritDoc is deprecated in favor of using @override, and they + if (jsdoc.HasFlag('override') and not jsdoc.HasFlag('constructor') + and ('accessControls' not in jsdoc.suppressions)): + self._HandleError( + errors.INVALID_OVERRIDE_PRIVATE, + '%s should not override a private member.' % identifier, + jsdoc.GetFlag('override').flag_token) + if (jsdoc.HasFlag('inheritDoc') and not jsdoc.HasFlag('constructor') + and ('accessControls' not in jsdoc.suppressions)): + self._HandleError( + errors.INVALID_INHERIT_DOC_PRIVATE, + '%s should not inherit from a private member.' % identifier, + jsdoc.GetFlag('inheritDoc').flag_token) + if (not jsdoc.HasFlag('private') and + ('underscore' not in jsdoc.suppressions) and not + ((jsdoc.HasFlag('inheritDoc') or jsdoc.HasFlag('override')) and + ('accessControls' in jsdoc.suppressions))): + self._HandleError( + errors.MISSING_PRIVATE, + 'Member "%s" must have @private JsDoc.' % + identifier, token) + if jsdoc.HasFlag('private') and 'underscore' in jsdoc.suppressions: + self._HandleError( + errors.UNNECESSARY_SUPPRESS, + '@suppress {underscore} is not necessary with @private', + jsdoc.suppressions['underscore']) + elif (jsdoc.HasFlag('private') and + not self.InExplicitlyTypedLanguage()): + # It is convention to hide public fields in some ECMA + # implementations from documentation using the @private tag. + self._HandleError( + errors.EXTRA_PRIVATE, + 'Member "%s" must not have @private JsDoc' % + identifier, token) + + # These flags are only legal on localizable message definitions; + # such variables always begin with the prefix MSG_. + for f in ('desc', 'hidden', 'meaning'): + if (jsdoc.HasFlag(f) + and not identifier.startswith('MSG_') + and identifier.find('.MSG_') == -1): + self._HandleError( + errors.INVALID_USE_OF_DESC_TAG, + 'Member "%s" should not have @%s JsDoc' % (identifier, f), + token) + + # Check for illegaly assigning live objects as prototype property values. + index = identifier.find('.prototype.') + # Ignore anything with additional .s after the prototype. + if index != -1 and identifier.find('.', index + 11) == -1: + equal_operator = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES) + next_code = tokenutil.SearchExcept(equal_operator, Type.NON_CODE_TYPES) + if next_code and ( + next_code.type in (Type.START_BRACKET, Type.START_BLOCK) or + next_code.IsOperator('new')): + self._HandleError( + errors.ILLEGAL_PROTOTYPE_MEMBER_VALUE, + 'Member %s cannot have a non-primitive value' % identifier, + token) + + elif token_type == Type.END_PARAMETERS: + # Find extra space at the end of parameter lists. We check the token + # prior to the current one when it is a closing paren. + if (token.previous and token.previous.type == Type.PARAMETERS + and self.ENDS_WITH_SPACE.search(token.previous.string)): + self._HandleError(errors.EXTRA_SPACE, 'Extra space before ")"', + token.previous) + + jsdoc = state.GetDocComment() + if state.GetFunction().is_interface: + if token.previous and token.previous.type == Type.PARAMETERS: + self._HandleError( + errors.INTERFACE_CONSTRUCTOR_CANNOT_HAVE_PARAMS, + 'Interface constructor cannot have parameters', + token.previous) + elif (state.InTopLevel() and jsdoc and not jsdoc.HasFlag('see') + and not jsdoc.InheritsDocumentation() + and not state.InObjectLiteralDescendant() and not + jsdoc.IsInvalidated()): + distance, edit = jsdoc.CompareParameters(state.GetParams()) + if distance: + params_iter = iter(state.GetParams()) + docs_iter = iter(jsdoc.ordered_params) + + for op in edit: + if op == 'I': + # Insertion. + # Parsing doc comments is the same for all languages + # but some languages care about parameters that don't have + # doc comments and some languages don't care. + # Languages that don't allow variables to by typed such as + # JavaScript care but languages such as ActionScript or Java + # that allow variables to be typed don't care. + if not self._limited_doc_checks: + self.HandleMissingParameterDoc(token, params_iter.next()) + + elif op == 'D': + # Deletion + self._HandleError(errors.EXTRA_PARAMETER_DOCUMENTATION, + 'Found docs for non-existing parameter: "%s"' % + docs_iter.next(), token) + elif op == 'S': + # Substitution + if not self._limited_doc_checks: + self._HandleError( + errors.WRONG_PARAMETER_DOCUMENTATION, + 'Parameter mismatch: got "%s", expected "%s"' % + (params_iter.next(), docs_iter.next()), token) + + else: + # Equality - just advance the iterators + params_iter.next() + docs_iter.next() + + elif token_type == Type.STRING_TEXT: + # If this is the first token after the start of the string, but it's at + # the end of a line, we know we have a multi-line string. + if token.previous.type in ( + Type.SINGLE_QUOTE_STRING_START, + Type.DOUBLE_QUOTE_STRING_START) and last_in_line: + self._HandleError(errors.MULTI_LINE_STRING, + 'Multi-line strings are not allowed', token) + + # This check is orthogonal to the ones above, and repeats some types, so + # it is a plain if and not an elif. + if token.type in Type.COMMENT_TYPES: + if self.ILLEGAL_TAB.search(token.string): + self._HandleError(errors.ILLEGAL_TAB, + 'Illegal tab in comment "%s"' % token.string, token) + + trimmed = token.string.rstrip() + if last_in_line and token.string != trimmed: + # Check for extra whitespace at the end of a line. + self._HandleError( + errors.EXTRA_SPACE, 'Extra space at end of line', token, + position=Position(len(trimmed), len(token.string) - len(trimmed))) + + # This check is also orthogonal since it is based on metadata. + if token.metadata.is_implied_semicolon: + self._HandleError(errors.MISSING_SEMICOLON, + 'Missing semicolon at end of line', token) + + def _HandleStartBracket(self, token, last_non_space_token): + """Handles a token that is an open bracket. + + Args: + token: The token to handle. + last_non_space_token: The last token that was not a space. + """ + if (not token.IsFirstInLine() and token.previous.type == Type.WHITESPACE and + last_non_space_token and + last_non_space_token.type in Type.EXPRESSION_ENDER_TYPES): + self._HandleError( + errors.EXTRA_SPACE, 'Extra space before "["', + token.previous, position=Position.All(token.previous.string)) + # If the [ token is the first token in a line we shouldn't complain + # about a missing space before [. This is because some Ecma script + # languages allow syntax like: + # [Annotation] + # class MyClass {...} + # So we don't want to blindly warn about missing spaces before [. + # In the the future, when rules for computing exactly how many spaces + # lines should be indented are added, then we can return errors for + # [ tokens that are improperly indented. + # For example: + # var someVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongVariableName = + # [a,b,c]; + # should trigger a proper indentation warning message as [ is not indented + # by four spaces. + elif (not token.IsFirstInLine() and token.previous and + token.previous.type not in ( + [Type.WHITESPACE, Type.START_PAREN, Type.START_BRACKET] + + Type.EXPRESSION_ENDER_TYPES)): + self._HandleError(errors.MISSING_SPACE, 'Missing space before "["', + token, position=Position.AtBeginning()) + + def Finalize(self, state): + """Perform all checks that need to occur after all lines are processed. + + Args: + state: State of the parser after parsing all tokens + + Raises: + TypeError: If not overridden. + """ + last_non_space_token = state.GetLastNonSpaceToken() + # Check last line for ending with newline. + if state.GetLastLine() and not ( + state.GetLastLine().isspace() or + state.GetLastLine().rstrip('\n\r\f') != state.GetLastLine()): + self._HandleError( + errors.FILE_MISSING_NEWLINE, + 'File does not end with new line. (%s)' % state.GetLastLine(), + last_non_space_token) + + try: + self._indentation.Finalize() + except Exception, e: + self._HandleError( + errors.FILE_DOES_NOT_PARSE, + str(e), + last_non_space_token) + + def GetLongLineExceptions(self): + """Gets a list of regexps for lines which can be longer than the limit. + + Returns: + A list of regexps, used as matches (rather than searches). + """ + return [] + + def InExplicitlyTypedLanguage(self): + """Returns whether this ecma implementation is explicitly typed.""" + return False diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/ecmametadatapass.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/ecmametadatapass.py new file mode 100755 index 0000000000..a8657d2f4d --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/ecmametadatapass.py @@ -0,0 +1,579 @@ +#!/usr/bin/env python +# +# Copyright 2010 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Metadata pass for annotating tokens in EcmaScript files.""" + +__author__ = ('robbyw@google.com (Robert Walker)') + +from closure_linter import javascripttokens +from closure_linter import tokenutil + + +TokenType = javascripttokens.JavaScriptTokenType + + +class ParseError(Exception): + """Exception indicating a parse error at the given token. + + Attributes: + token: The token where the parse error occurred. + """ + + def __init__(self, token, message=None): + """Initialize a parse error at the given token with an optional message. + + Args: + token: The token where the parse error occurred. + message: A message describing the parse error. + """ + Exception.__init__(self, message) + self.token = token + + +class EcmaContext(object): + """Context object for EcmaScript languages. + + Attributes: + type: The context type. + start_token: The token where this context starts. + end_token: The token where this context ends. + parent: The parent context. + """ + + # The root context. + ROOT = 'root' + + # A block of code. + BLOCK = 'block' + + # A pseudo-block of code for a given case or default section. + CASE_BLOCK = 'case_block' + + # Block of statements in a for loop's parentheses. + FOR_GROUP_BLOCK = 'for_block' + + # An implied block of code for 1 line if, while, and for statements + IMPLIED_BLOCK = 'implied_block' + + # An index in to an array or object. + INDEX = 'index' + + # An array literal in []. + ARRAY_LITERAL = 'array_literal' + + # An object literal in {}. + OBJECT_LITERAL = 'object_literal' + + # An individual element in an array or object literal. + LITERAL_ELEMENT = 'literal_element' + + # The portion of a ternary statement between ? and : + TERNARY_TRUE = 'ternary_true' + + # The portion of a ternary statment after : + TERNARY_FALSE = 'ternary_false' + + # The entire switch statment. This will contain a GROUP with the variable + # and a BLOCK with the code. + + # Since that BLOCK is not a normal block, it can not contain statements except + # for case and default. + SWITCH = 'switch' + + # A normal comment. + COMMENT = 'comment' + + # A JsDoc comment. + DOC = 'doc' + + # An individual statement. + STATEMENT = 'statement' + + # Code within parentheses. + GROUP = 'group' + + # Parameter names in a function declaration. + PARAMETERS = 'parameters' + + # A set of variable declarations appearing after the 'var' keyword. + VAR = 'var' + + # Context types that are blocks. + BLOCK_TYPES = frozenset([ + ROOT, BLOCK, CASE_BLOCK, FOR_GROUP_BLOCK, IMPLIED_BLOCK]) + + def __init__(self, context_type, start_token, parent=None): + """Initializes the context object. + + Args: + context_type: The context type. + start_token: The token where this context starts. + parent: The parent context. + + Attributes: + type: The context type. + start_token: The token where this context starts. + end_token: The token where this context ends. + parent: The parent context. + children: The child contexts of this context, in order. + """ + self.type = context_type + self.start_token = start_token + self.end_token = None + + self.parent = None + self.children = [] + + if parent: + parent.AddChild(self) + + def __repr__(self): + """Returns a string representation of the context object.""" + stack = [] + context = self + while context: + stack.append(context.type) + context = context.parent + return 'Context(%s)' % ' > '.join(stack) + + def AddChild(self, child): + """Adds a child to this context and sets child's parent to this context. + + Args: + child: A child EcmaContext. The child's parent will be set to this + context. + """ + + child.parent = self + + self.children.append(child) + self.children.sort(EcmaContext._CompareContexts) + + def GetRoot(self): + """Get the root context that contains this context, if any.""" + context = self + while context: + if context.type is EcmaContext.ROOT: + return context + context = context.parent + + @staticmethod + def _CompareContexts(context1, context2): + """Sorts contexts 1 and 2 by start token document position.""" + return tokenutil.Compare(context1.start_token, context2.start_token) + + +class EcmaMetaData(object): + """Token metadata for EcmaScript languages. + + Attributes: + last_code: The last code token to appear before this one. + context: The context this token appears in. + operator_type: The operator type, will be one of the *_OPERATOR constants + defined below. + aliased_symbol: The full symbol being identified, as a string (e.g. an + 'XhrIo' alias for 'goog.net.XhrIo'). Only applicable to identifier + tokens. This is set in aliaspass.py and is a best guess. + is_alias_definition: True if the symbol is part of an alias definition. + If so, these symbols won't be counted towards goog.requires/provides. + """ + + UNARY_OPERATOR = 'unary' + + UNARY_POST_OPERATOR = 'unary_post' + + BINARY_OPERATOR = 'binary' + + TERNARY_OPERATOR = 'ternary' + + def __init__(self): + """Initializes a token metadata object.""" + self.last_code = None + self.context = None + self.operator_type = None + self.is_implied_semicolon = False + self.is_implied_block = False + self.is_implied_block_close = False + self.aliased_symbol = None + self.is_alias_definition = False + + def __repr__(self): + """Returns a string representation of the context object.""" + parts = ['%r' % self.context] + if self.operator_type: + parts.append('optype: %r' % self.operator_type) + if self.is_implied_semicolon: + parts.append('implied;') + if self.aliased_symbol: + parts.append('alias for: %s' % self.aliased_symbol) + return 'MetaData(%s)' % ', '.join(parts) + + def IsUnaryOperator(self): + return self.operator_type in (EcmaMetaData.UNARY_OPERATOR, + EcmaMetaData.UNARY_POST_OPERATOR) + + def IsUnaryPostOperator(self): + return self.operator_type == EcmaMetaData.UNARY_POST_OPERATOR + + +class EcmaMetaDataPass(object): + """A pass that iterates over all tokens and builds metadata about them.""" + + def __init__(self): + """Initialize the meta data pass object.""" + self.Reset() + + def Reset(self): + """Resets the metadata pass to prepare for the next file.""" + self._token = None + self._context = None + self._AddContext(EcmaContext.ROOT) + self._last_code = None + + def _CreateContext(self, context_type): + """Overridable by subclasses to create the appropriate context type.""" + return EcmaContext(context_type, self._token, self._context) + + def _CreateMetaData(self): + """Overridable by subclasses to create the appropriate metadata type.""" + return EcmaMetaData() + + def _AddContext(self, context_type): + """Adds a context of the given type to the context stack. + + Args: + context_type: The type of context to create + """ + self._context = self._CreateContext(context_type) + + def _PopContext(self): + """Moves up one level in the context stack. + + Returns: + The former context. + + Raises: + ParseError: If the root context is popped. + """ + top_context = self._context + top_context.end_token = self._token + self._context = top_context.parent + if self._context: + return top_context + else: + raise ParseError(self._token) + + def _PopContextType(self, *stop_types): + """Pops the context stack until a context of the given type is popped. + + Args: + *stop_types: The types of context to pop to - stops at the first match. + + Returns: + The context object of the given type that was popped. + """ + last = None + while not last or last.type not in stop_types: + last = self._PopContext() + return last + + def _EndStatement(self): + """Process the end of a statement.""" + self._PopContextType(EcmaContext.STATEMENT) + if self._context.type == EcmaContext.IMPLIED_BLOCK: + self._token.metadata.is_implied_block_close = True + self._PopContext() + + def _ProcessContext(self): + """Process the context at the current token. + + Returns: + The context that should be assigned to the current token, or None if + the current context after this method should be used. + + Raises: + ParseError: When the token appears in an invalid context. + """ + token = self._token + token_type = token.type + + if self._context.type in EcmaContext.BLOCK_TYPES: + # Whenever we're in a block, we add a statement context. We make an + # exception for switch statements since they can only contain case: and + # default: and therefore don't directly contain statements. + # The block we add here may be immediately removed in some cases, but + # that causes no harm. + parent = self._context.parent + if not parent or parent.type != EcmaContext.SWITCH: + self._AddContext(EcmaContext.STATEMENT) + + elif self._context.type == EcmaContext.ARRAY_LITERAL: + self._AddContext(EcmaContext.LITERAL_ELEMENT) + + if token_type == TokenType.START_PAREN: + if self._last_code and self._last_code.IsKeyword('for'): + # for loops contain multiple statements in the group unlike while, + # switch, if, etc. + self._AddContext(EcmaContext.FOR_GROUP_BLOCK) + else: + self._AddContext(EcmaContext.GROUP) + + elif token_type == TokenType.END_PAREN: + result = self._PopContextType(EcmaContext.GROUP, + EcmaContext.FOR_GROUP_BLOCK) + keyword_token = result.start_token.metadata.last_code + # keyword_token will not exist if the open paren is the first line of the + # file, for example if all code is wrapped in an immediately executed + # annonymous function. + if keyword_token and keyword_token.string in ('if', 'for', 'while'): + next_code = tokenutil.SearchExcept(token, TokenType.NON_CODE_TYPES) + if next_code.type != TokenType.START_BLOCK: + # Check for do-while. + is_do_while = False + pre_keyword_token = keyword_token.metadata.last_code + if (pre_keyword_token and + pre_keyword_token.type == TokenType.END_BLOCK): + start_block_token = pre_keyword_token.metadata.context.start_token + is_do_while = start_block_token.metadata.last_code.string == 'do' + + # If it's not do-while, it's an implied block. + if not is_do_while: + self._AddContext(EcmaContext.IMPLIED_BLOCK) + token.metadata.is_implied_block = True + + return result + + # else (not else if) with no open brace after it should be considered the + # start of an implied block, similar to the case with if, for, and while + # above. + elif (token_type == TokenType.KEYWORD and + token.string == 'else'): + next_code = tokenutil.SearchExcept(token, TokenType.NON_CODE_TYPES) + if (next_code.type != TokenType.START_BLOCK and + (next_code.type != TokenType.KEYWORD or next_code.string != 'if')): + self._AddContext(EcmaContext.IMPLIED_BLOCK) + token.metadata.is_implied_block = True + + elif token_type == TokenType.START_PARAMETERS: + self._AddContext(EcmaContext.PARAMETERS) + + elif token_type == TokenType.END_PARAMETERS: + return self._PopContextType(EcmaContext.PARAMETERS) + + elif token_type == TokenType.START_BRACKET: + if (self._last_code and + self._last_code.type in TokenType.EXPRESSION_ENDER_TYPES): + self._AddContext(EcmaContext.INDEX) + else: + self._AddContext(EcmaContext.ARRAY_LITERAL) + + elif token_type == TokenType.END_BRACKET: + return self._PopContextType(EcmaContext.INDEX, EcmaContext.ARRAY_LITERAL) + + elif token_type == TokenType.START_BLOCK: + if (self._last_code.type in (TokenType.END_PAREN, + TokenType.END_PARAMETERS) or + self._last_code.IsKeyword('else') or + self._last_code.IsKeyword('do') or + self._last_code.IsKeyword('try') or + self._last_code.IsKeyword('finally') or + (self._last_code.IsOperator(':') and + self._last_code.metadata.context.type == EcmaContext.CASE_BLOCK)): + # else, do, try, and finally all might have no () before {. + # Also, handle the bizzare syntax case 10: {...}. + self._AddContext(EcmaContext.BLOCK) + else: + self._AddContext(EcmaContext.OBJECT_LITERAL) + + elif token_type == TokenType.END_BLOCK: + context = self._PopContextType(EcmaContext.BLOCK, + EcmaContext.OBJECT_LITERAL) + if self._context.type == EcmaContext.SWITCH: + # The end of the block also means the end of the switch statement it + # applies to. + return self._PopContext() + return context + + elif token.IsKeyword('switch'): + self._AddContext(EcmaContext.SWITCH) + + elif (token_type == TokenType.KEYWORD and + token.string in ('case', 'default') and + self._context.type != EcmaContext.OBJECT_LITERAL): + # Pop up to but not including the switch block. + while self._context.parent.type != EcmaContext.SWITCH: + self._PopContext() + if self._context.parent is None: + raise ParseError(token, 'Encountered case/default statement ' + 'without switch statement') + + elif token.IsOperator('?'): + self._AddContext(EcmaContext.TERNARY_TRUE) + + elif token.IsOperator(':'): + if self._context.type == EcmaContext.OBJECT_LITERAL: + self._AddContext(EcmaContext.LITERAL_ELEMENT) + + elif self._context.type == EcmaContext.TERNARY_TRUE: + self._PopContext() + self._AddContext(EcmaContext.TERNARY_FALSE) + + # Handle nested ternary statements like: + # foo = bar ? baz ? 1 : 2 : 3 + # When we encounter the second ":" the context is + # ternary_false > ternary_true > statement > root + elif (self._context.type == EcmaContext.TERNARY_FALSE and + self._context.parent.type == EcmaContext.TERNARY_TRUE): + self._PopContext() # Leave current ternary false context. + self._PopContext() # Leave current parent ternary true + self._AddContext(EcmaContext.TERNARY_FALSE) + + elif self._context.parent.type == EcmaContext.SWITCH: + self._AddContext(EcmaContext.CASE_BLOCK) + + elif token.IsKeyword('var'): + self._AddContext(EcmaContext.VAR) + + elif token.IsOperator(','): + while self._context.type not in (EcmaContext.VAR, + EcmaContext.ARRAY_LITERAL, + EcmaContext.OBJECT_LITERAL, + EcmaContext.STATEMENT, + EcmaContext.PARAMETERS, + EcmaContext.GROUP): + self._PopContext() + + elif token_type == TokenType.SEMICOLON: + self._EndStatement() + + def Process(self, first_token): + """Processes the token stream starting with the given token.""" + self._token = first_token + while self._token: + self._ProcessToken() + + if self._token.IsCode(): + self._last_code = self._token + + self._token = self._token.next + + try: + self._PopContextType(self, EcmaContext.ROOT) + except ParseError: + # Ignore the "popped to root" error. + pass + + def _ProcessToken(self): + """Process the given token.""" + token = self._token + token.metadata = self._CreateMetaData() + context = (self._ProcessContext() or self._context) + token.metadata.context = context + token.metadata.last_code = self._last_code + + # Determine the operator type of the token, if applicable. + if token.type == TokenType.OPERATOR: + token.metadata.operator_type = self._GetOperatorType(token) + + # Determine if there is an implied semicolon after the token. + if token.type != TokenType.SEMICOLON: + next_code = tokenutil.SearchExcept(token, TokenType.NON_CODE_TYPES) + # A statement like if (x) does not need a semicolon after it + is_implied_block = self._context == EcmaContext.IMPLIED_BLOCK + is_last_code_in_line = token.IsCode() and ( + not next_code or next_code.line_number != token.line_number) + is_continued_identifier = (token.type == TokenType.IDENTIFIER and + token.string.endswith('.')) + is_continued_operator = (token.type == TokenType.OPERATOR and + not token.metadata.IsUnaryPostOperator()) + is_continued_dot = token.string == '.' + next_code_is_operator = next_code and next_code.type == TokenType.OPERATOR + next_code_is_dot = next_code and next_code.string == '.' + is_end_of_block = ( + token.type == TokenType.END_BLOCK and + token.metadata.context.type != EcmaContext.OBJECT_LITERAL) + is_multiline_string = token.type == TokenType.STRING_TEXT + is_continued_var_decl = (token.IsKeyword('var') and + next_code and + (next_code.type in [TokenType.IDENTIFIER, + TokenType.SIMPLE_LVALUE]) and + token.line_number < next_code.line_number) + next_code_is_block = next_code and next_code.type == TokenType.START_BLOCK + if (is_last_code_in_line and + self._StatementCouldEndInContext() and + not is_multiline_string and + not is_end_of_block and + not is_continued_var_decl and + not is_continued_identifier and + not is_continued_operator and + not is_continued_dot and + not next_code_is_dot and + not next_code_is_operator and + not is_implied_block and + not next_code_is_block): + token.metadata.is_implied_semicolon = True + self._EndStatement() + + def _StatementCouldEndInContext(self): + """Returns if the current statement (if any) may end in this context.""" + # In the basic statement or variable declaration context, statement can + # always end in this context. + if self._context.type in (EcmaContext.STATEMENT, EcmaContext.VAR): + return True + + # End of a ternary false branch inside a statement can also be the + # end of the statement, for example: + # var x = foo ? foo.bar() : null + # In this case the statement ends after the null, when the context stack + # looks like ternary_false > var > statement > root. + if (self._context.type == EcmaContext.TERNARY_FALSE and + self._context.parent.type in (EcmaContext.STATEMENT, EcmaContext.VAR)): + return True + + # In all other contexts like object and array literals, ternary true, etc. + # the statement can't yet end. + return False + + def _GetOperatorType(self, token): + """Returns the operator type of the given operator token. + + Args: + token: The token to get arity for. + + Returns: + The type of the operator. One of the *_OPERATOR constants defined in + EcmaMetaData. + """ + if token.string == '?': + return EcmaMetaData.TERNARY_OPERATOR + + if token.string in TokenType.UNARY_OPERATORS: + return EcmaMetaData.UNARY_OPERATOR + + last_code = token.metadata.last_code + if not last_code or last_code.type == TokenType.END_BLOCK: + return EcmaMetaData.UNARY_OPERATOR + + if (token.string in TokenType.UNARY_POST_OPERATORS and + last_code.type in TokenType.EXPRESSION_ENDER_TYPES): + return EcmaMetaData.UNARY_POST_OPERATOR + + if (token.string in TokenType.UNARY_OK_OPERATORS and + last_code.type not in TokenType.EXPRESSION_ENDER_TYPES and + last_code.string not in TokenType.UNARY_POST_OPERATORS): + return EcmaMetaData.UNARY_OPERATOR + + return EcmaMetaData.BINARY_OPERATOR diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/error_check.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/error_check.py new file mode 100755 index 0000000000..9e2ac01cc6 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/error_check.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +# +# Copyright 2011 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""Specific JSLint errors checker.""" + + + +import gflags as flags + +FLAGS = flags.FLAGS + + +class Rule(object): + """Different rules to check.""" + + # Documentations for specific rules goes in flag definition. + BLANK_LINES_AT_TOP_LEVEL = 'blank_lines_at_top_level' + INDENTATION = 'indentation' + WELL_FORMED_AUTHOR = 'well_formed_author' + NO_BRACES_AROUND_INHERIT_DOC = 'no_braces_around_inherit_doc' + BRACES_AROUND_TYPE = 'braces_around_type' + OPTIONAL_TYPE_MARKER = 'optional_type_marker' + VARIABLE_ARG_MARKER = 'variable_arg_marker' + UNUSED_PRIVATE_MEMBERS = 'unused_private_members' + UNUSED_LOCAL_VARIABLES = 'unused_local_variables' + + # Rule to raise all known errors. + ALL = 'all' + + # All rules that are to be checked when using the strict flag. E.g. the rules + # that are specific to the stricter Closure style. + CLOSURE_RULES = frozenset([BLANK_LINES_AT_TOP_LEVEL, + INDENTATION, + WELL_FORMED_AUTHOR, + NO_BRACES_AROUND_INHERIT_DOC, + BRACES_AROUND_TYPE, + OPTIONAL_TYPE_MARKER, + VARIABLE_ARG_MARKER]) + + +flags.DEFINE_boolean('strict', False, + 'Whether to validate against the stricter Closure style. ' + 'This includes ' + (', '.join(Rule.CLOSURE_RULES)) + '.') +flags.DEFINE_multistring('jslint_error', [], + 'List of specific lint errors to check. Here is a list' + ' of accepted values:\n' + ' - ' + Rule.ALL + ': enables all following errors.\n' + ' - ' + Rule.BLANK_LINES_AT_TOP_LEVEL + ': validates' + 'number of blank lines between blocks at top level.\n' + ' - ' + Rule.INDENTATION + ': checks correct ' + 'indentation of code.\n' + ' - ' + Rule.WELL_FORMED_AUTHOR + ': validates the ' + '@author JsDoc tags.\n' + ' - ' + Rule.NO_BRACES_AROUND_INHERIT_DOC + ': ' + 'forbids braces around @inheritdoc JsDoc tags.\n' + ' - ' + Rule.BRACES_AROUND_TYPE + ': enforces braces ' + 'around types in JsDoc tags.\n' + ' - ' + Rule.OPTIONAL_TYPE_MARKER + ': checks correct ' + 'use of optional marker = in param types.\n' + ' - ' + Rule.UNUSED_PRIVATE_MEMBERS + ': checks for ' + 'unused private variables.\n') + + +def ShouldCheck(rule): + """Returns whether the optional rule should be checked. + + Computes different flags (strict, jslint_error, jslint_noerror) to find out if + this specific rule should be checked. + + Args: + rule: Name of the rule (see Rule). + + Returns: + True if the rule should be checked according to the flags, otherwise False. + """ + if rule in FLAGS.jslint_error or Rule.ALL in FLAGS.jslint_error: + return True + # Checks strict rules. + return FLAGS.strict and rule in Rule.CLOSURE_RULES diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/error_fixer.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/error_fixer.py new file mode 100755 index 0000000000..fb2db5035e --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/error_fixer.py @@ -0,0 +1,525 @@ +#!/usr/bin/env python +# +# Copyright 2007 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Main class responsible for automatically fixing simple style violations.""" + +# Allow non-Google copyright +# pylint: disable=g-bad-file-header + +__author__ = 'robbyw@google.com (Robert Walker)' + +import re + +import gflags as flags +from closure_linter import errors +from closure_linter import javascriptstatetracker +from closure_linter import javascripttokens +from closure_linter import requireprovidesorter +from closure_linter import tokenutil +from closure_linter.common import errorhandler + +# Shorthand +Token = javascripttokens.JavaScriptToken +Type = javascripttokens.JavaScriptTokenType + +END_OF_FLAG_TYPE = re.compile(r'(}?\s*)$') + +# Regex to represent common mistake inverting author name and email as +# @author User Name (user@company) +INVERTED_AUTHOR_SPEC = re.compile(r'(?P\s*)' + r'(?P[^(]+)' + r'(?P\s+)' + r'\(' + r'(?P[^\s]+@[^)\s]+)' + r'\)' + r'(?P.*)') + +FLAGS = flags.FLAGS +flags.DEFINE_boolean('disable_indentation_fixing', False, + 'Whether to disable automatic fixing of indentation.') + + +class ErrorFixer(errorhandler.ErrorHandler): + """Object that fixes simple style errors.""" + + def __init__(self, external_file=None): + """Initialize the error fixer. + + Args: + external_file: If included, all output will be directed to this file + instead of overwriting the files the errors are found in. + """ + errorhandler.ErrorHandler.__init__(self) + + self._file_name = None + self._file_token = None + self._external_file = external_file + + def HandleFile(self, filename, first_token): + """Notifies this ErrorPrinter that subsequent errors are in filename. + + Args: + filename: The name of the file about to be checked. + first_token: The first token in the file. + """ + self._file_name = filename + self._file_is_html = filename.endswith('.html') or filename.endswith('.htm') + self._file_token = first_token + self._file_fix_count = 0 + self._file_changed_lines = set() + + def _AddFix(self, tokens): + """Adds the fix to the internal count. + + Args: + tokens: The token or sequence of tokens changed to fix an error. + """ + self._file_fix_count += 1 + if hasattr(tokens, 'line_number'): + self._file_changed_lines.add(tokens.line_number) + else: + for token in tokens: + self._file_changed_lines.add(token.line_number) + + def HandleError(self, error): + """Attempts to fix the error. + + Args: + error: The error object + """ + code = error.code + token = error.token + + if code == errors.JSDOC_PREFER_QUESTION_TO_PIPE_NULL: + iterator = token.attached_object.type_start_token + if iterator.type == Type.DOC_START_BRACE or iterator.string.isspace(): + iterator = iterator.next + + leading_space = len(iterator.string) - len(iterator.string.lstrip()) + iterator.string = '%s?%s' % (' ' * leading_space, + iterator.string.lstrip()) + + # Cover the no outer brace case where the end token is part of the type. + while iterator and iterator != token.attached_object.type_end_token.next: + iterator.string = iterator.string.replace( + 'null|', '').replace('|null', '') + iterator = iterator.next + + # Create a new flag object with updated type info. + token.attached_object = javascriptstatetracker.JsDocFlag(token) + self._AddFix(token) + + elif code == errors.JSDOC_MISSING_OPTIONAL_TYPE: + iterator = token.attached_object.type_end_token + if iterator.type == Type.DOC_END_BRACE or iterator.string.isspace(): + iterator = iterator.previous + + ending_space = len(iterator.string) - len(iterator.string.rstrip()) + iterator.string = '%s=%s' % (iterator.string.rstrip(), + ' ' * ending_space) + + # Create a new flag object with updated type info. + token.attached_object = javascriptstatetracker.JsDocFlag(token) + self._AddFix(token) + + elif code == errors.JSDOC_MISSING_VAR_ARGS_TYPE: + iterator = token.attached_object.type_start_token + if iterator.type == Type.DOC_START_BRACE or iterator.string.isspace(): + iterator = iterator.next + + starting_space = len(iterator.string) - len(iterator.string.lstrip()) + iterator.string = '%s...%s' % (' ' * starting_space, + iterator.string.lstrip()) + + # Create a new flag object with updated type info. + token.attached_object = javascriptstatetracker.JsDocFlag(token) + self._AddFix(token) + + elif code in (errors.MISSING_SEMICOLON_AFTER_FUNCTION, + errors.MISSING_SEMICOLON): + semicolon_token = Token(';', Type.SEMICOLON, token.line, + token.line_number) + tokenutil.InsertTokenAfter(semicolon_token, token) + token.metadata.is_implied_semicolon = False + semicolon_token.metadata.is_implied_semicolon = False + self._AddFix(token) + + elif code in (errors.ILLEGAL_SEMICOLON_AFTER_FUNCTION, + errors.REDUNDANT_SEMICOLON, + errors.COMMA_AT_END_OF_LITERAL): + self._DeleteToken(token) + self._AddFix(token) + + elif code == errors.INVALID_JSDOC_TAG: + if token.string == '@returns': + token.string = '@return' + self._AddFix(token) + + elif code == errors.FILE_MISSING_NEWLINE: + # This error is fixed implicitly by the way we restore the file + self._AddFix(token) + + elif code == errors.MISSING_SPACE: + if error.fix_data: + token.string = error.fix_data + self._AddFix(token) + elif error.position: + if error.position.IsAtBeginning(): + tokenutil.InsertSpaceTokenAfter(token.previous) + elif error.position.IsAtEnd(token.string): + tokenutil.InsertSpaceTokenAfter(token) + else: + token.string = error.position.Set(token.string, ' ') + self._AddFix(token) + + elif code == errors.EXTRA_SPACE: + if error.position: + token.string = error.position.Set(token.string, '') + self._AddFix(token) + + elif code == errors.MISSING_LINE: + if error.position.IsAtBeginning(): + tokenutil.InsertBlankLineAfter(token.previous) + else: + tokenutil.InsertBlankLineAfter(token) + self._AddFix(token) + + elif code == errors.EXTRA_LINE: + self._DeleteToken(token) + self._AddFix(token) + + elif code == errors.WRONG_BLANK_LINE_COUNT: + if not token.previous: + # TODO(user): Add an insertBefore method to tokenutil. + return + + num_lines = error.fix_data + should_delete = False + + if num_lines < 0: + num_lines *= -1 + should_delete = True + + for unused_i in xrange(1, num_lines + 1): + if should_delete: + # TODO(user): DeleteToken should update line numbers. + self._DeleteToken(token.previous) + else: + tokenutil.InsertBlankLineAfter(token.previous) + self._AddFix(token) + + elif code == errors.UNNECESSARY_DOUBLE_QUOTED_STRING: + end_quote = tokenutil.Search(token, Type.DOUBLE_QUOTE_STRING_END) + if end_quote: + single_quote_start = Token( + "'", Type.SINGLE_QUOTE_STRING_START, token.line, token.line_number) + single_quote_end = Token( + "'", Type.SINGLE_QUOTE_STRING_START, end_quote.line, + token.line_number) + + tokenutil.InsertTokenAfter(single_quote_start, token) + tokenutil.InsertTokenAfter(single_quote_end, end_quote) + self._DeleteToken(token) + self._DeleteToken(end_quote) + self._AddFix([token, end_quote]) + + elif code == errors.MISSING_BRACES_AROUND_TYPE: + fixed_tokens = [] + start_token = token.attached_object.type_start_token + + if start_token.type != Type.DOC_START_BRACE: + leading_space = ( + len(start_token.string) - len(start_token.string.lstrip())) + if leading_space: + start_token = tokenutil.SplitToken(start_token, leading_space) + # Fix case where start and end token were the same. + if token.attached_object.type_end_token == start_token.previous: + token.attached_object.type_end_token = start_token + + new_token = Token('{', Type.DOC_START_BRACE, start_token.line, + start_token.line_number) + tokenutil.InsertTokenAfter(new_token, start_token.previous) + token.attached_object.type_start_token = new_token + fixed_tokens.append(new_token) + + end_token = token.attached_object.type_end_token + if end_token.type != Type.DOC_END_BRACE: + # If the start token was a brace, the end token will be a + # FLAG_ENDING_TYPE token, if there wasn't a starting brace then + # the end token is the last token of the actual type. + last_type = end_token + if not fixed_tokens: + last_type = end_token.previous + + while last_type.string.isspace(): + last_type = last_type.previous + + # If there was no starting brace then a lone end brace wouldn't have + # been type end token. Now that we've added any missing start brace, + # see if the last effective type token was an end brace. + if last_type.type != Type.DOC_END_BRACE: + trailing_space = (len(last_type.string) - + len(last_type.string.rstrip())) + if trailing_space: + tokenutil.SplitToken(last_type, + len(last_type.string) - trailing_space) + + new_token = Token('}', Type.DOC_END_BRACE, last_type.line, + last_type.line_number) + tokenutil.InsertTokenAfter(new_token, last_type) + token.attached_object.type_end_token = new_token + fixed_tokens.append(new_token) + + self._AddFix(fixed_tokens) + + elif code == errors.GOOG_REQUIRES_NOT_ALPHABETIZED: + require_start_token = error.fix_data + sorter = requireprovidesorter.RequireProvideSorter() + sorter.FixRequires(require_start_token) + + self._AddFix(require_start_token) + + elif code == errors.GOOG_PROVIDES_NOT_ALPHABETIZED: + provide_start_token = error.fix_data + sorter = requireprovidesorter.RequireProvideSorter() + sorter.FixProvides(provide_start_token) + + self._AddFix(provide_start_token) + + elif code == errors.UNNECESSARY_BRACES_AROUND_INHERIT_DOC: + if token.previous.string == '{' and token.next.string == '}': + self._DeleteToken(token.previous) + self._DeleteToken(token.next) + self._AddFix([token]) + + elif code == errors.INVALID_AUTHOR_TAG_DESCRIPTION: + match = INVERTED_AUTHOR_SPEC.match(token.string) + if match: + token.string = '%s%s%s(%s)%s' % (match.group('leading_whitespace'), + match.group('email'), + match.group('whitespace_after_name'), + match.group('name'), + match.group('trailing_characters')) + self._AddFix(token) + + elif (code == errors.WRONG_INDENTATION and + not FLAGS.disable_indentation_fixing): + token = tokenutil.GetFirstTokenInSameLine(token) + actual = error.position.start + expected = error.position.length + + # Cases where first token is param but with leading spaces. + if (len(token.string.lstrip()) == len(token.string) - actual and + token.string.lstrip()): + token.string = token.string.lstrip() + actual = 0 + + if token.type in (Type.WHITESPACE, Type.PARAMETERS) and actual != 0: + token.string = token.string.lstrip() + (' ' * expected) + self._AddFix([token]) + else: + # We need to add indentation. + new_token = Token(' ' * expected, Type.WHITESPACE, + token.line, token.line_number) + # Note that we'll never need to add indentation at the first line, + # since it will always not be indented. Therefore it's safe to assume + # token.previous exists. + tokenutil.InsertTokenAfter(new_token, token.previous) + self._AddFix([token]) + + elif code in [errors.MALFORMED_END_OF_SCOPE_COMMENT, + errors.MISSING_END_OF_SCOPE_COMMENT]: + # Only fix cases where }); is found with no trailing content on the line + # other than a comment. Value of 'token' is set to } for this error. + if (token.type == Type.END_BLOCK and + token.next.type == Type.END_PAREN and + token.next.next.type == Type.SEMICOLON): + current_token = token.next.next.next + removed_tokens = [] + while current_token and current_token.line_number == token.line_number: + if current_token.IsAnyType(Type.WHITESPACE, + Type.START_SINGLE_LINE_COMMENT, + Type.COMMENT): + removed_tokens.append(current_token) + current_token = current_token.next + else: + return + + if removed_tokens: + self._DeleteTokens(removed_tokens[0], len(removed_tokens)) + + whitespace_token = Token(' ', Type.WHITESPACE, token.line, + token.line_number) + start_comment_token = Token('//', Type.START_SINGLE_LINE_COMMENT, + token.line, token.line_number) + comment_token = Token(' goog.scope', Type.COMMENT, token.line, + token.line_number) + insertion_tokens = [whitespace_token, start_comment_token, + comment_token] + + tokenutil.InsertTokensAfter(insertion_tokens, token.next.next) + self._AddFix(removed_tokens + insertion_tokens) + + elif code in [errors.EXTRA_GOOG_PROVIDE, errors.EXTRA_GOOG_REQUIRE]: + tokens_in_line = tokenutil.GetAllTokensInSameLine(token) + self._DeleteTokens(tokens_in_line[0], len(tokens_in_line)) + self._AddFix(tokens_in_line) + + elif code in [errors.MISSING_GOOG_PROVIDE, errors.MISSING_GOOG_REQUIRE]: + is_provide = code == errors.MISSING_GOOG_PROVIDE + is_require = code == errors.MISSING_GOOG_REQUIRE + + missing_namespaces = error.fix_data[0] + need_blank_line = error.fix_data[1] + + if need_blank_line is None: + # TODO(user): This happens when there are no existing + # goog.provide or goog.require statements to position new statements + # relative to. Consider handling this case with a heuristic. + return + + insert_location = token.previous + + # If inserting a missing require with no existing requires, insert a + # blank line first. + if need_blank_line and is_require: + tokenutil.InsertBlankLineAfter(insert_location) + insert_location = insert_location.next + + for missing_namespace in missing_namespaces: + new_tokens = self._GetNewRequireOrProvideTokens( + is_provide, missing_namespace, insert_location.line_number + 1) + tokenutil.InsertLineAfter(insert_location, new_tokens) + insert_location = new_tokens[-1] + self._AddFix(new_tokens) + + # If inserting a missing provide with no existing provides, insert a + # blank line after. + if need_blank_line and is_provide: + tokenutil.InsertBlankLineAfter(insert_location) + + def _GetNewRequireOrProvideTokens(self, is_provide, namespace, line_number): + """Returns a list of tokens to create a goog.require/provide statement. + + Args: + is_provide: True if getting tokens for a provide, False for require. + namespace: The required or provided namespaces to get tokens for. + line_number: The line number the new require or provide statement will be + on. + + Returns: + Tokens to create a new goog.require or goog.provide statement. + """ + string = 'goog.require' + if is_provide: + string = 'goog.provide' + line_text = string + '(\'' + namespace + '\');\n' + return [ + Token(string, Type.IDENTIFIER, line_text, line_number), + Token('(', Type.START_PAREN, line_text, line_number), + Token('\'', Type.SINGLE_QUOTE_STRING_START, line_text, line_number), + Token(namespace, Type.STRING_TEXT, line_text, line_number), + Token('\'', Type.SINGLE_QUOTE_STRING_END, line_text, line_number), + Token(')', Type.END_PAREN, line_text, line_number), + Token(';', Type.SEMICOLON, line_text, line_number) + ] + + def _DeleteToken(self, token): + """Deletes the specified token from the linked list of tokens. + + Updates instance variables pointing to tokens such as _file_token if + they reference the deleted token. + + Args: + token: The token to delete. + """ + if token == self._file_token: + self._file_token = token.next + + tokenutil.DeleteToken(token) + + def _DeleteTokens(self, token, token_count): + """Deletes the given number of tokens starting with the given token. + + Updates instance variables pointing to tokens such as _file_token if + they reference the deleted token. + + Args: + token: The first token to delete. + token_count: The total number of tokens to delete. + """ + if token == self._file_token: + for unused_i in xrange(token_count): + self._file_token = self._file_token.next + + tokenutil.DeleteTokens(token, token_count) + + def FinishFile(self): + """Called when the current file has finished style checking. + + Used to go back and fix any errors in the file. It currently supports both + js and html files. For js files it does a simple dump of all tokens, but in + order to support html file, we need to merge the original file with the new + token set back together. This works because the tokenized html file is the + original html file with all non js lines kept but blanked out with one blank + line token per line of html. + """ + if self._file_fix_count: + # Get the original file content for html. + if self._file_is_html: + f = open(self._file_name, 'r') + original_lines = f.readlines() + f.close() + + f = self._external_file + if not f: + print 'Fixed %d errors in %s' % (self._file_fix_count, self._file_name) + f = open(self._file_name, 'w') + + token = self._file_token + # If something got inserted before first token (e.g. due to sorting) + # then move to start. Bug 8398202. + while token.previous: + token = token.previous + char_count = 0 + line = '' + while token: + line += token.string + char_count += len(token.string) + + if token.IsLastInLine(): + # We distinguish if a blank line in html was from stripped original + # file or newly added error fix by looking at the "org_line_number" + # field on the token. It is only set in the tokenizer, so for all + # error fixes, the value should be None. + if (line or not self._file_is_html or + token.orig_line_number is None): + f.write(line) + f.write('\n') + else: + f.write(original_lines[token.orig_line_number - 1]) + line = '' + if char_count > 80 and token.line_number in self._file_changed_lines: + print 'WARNING: Line %d of %s is now longer than 80 characters.' % ( + token.line_number, self._file_name) + + char_count = 0 + + token = token.next + + if not self._external_file: + # Close the file if we created it + f.close() diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/error_fixer_test.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/error_fixer_test.py new file mode 100644 index 0000000000..49f449de42 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/error_fixer_test.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python +# +# Copyright 2012 The Closure Linter Authors. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for the error_fixer module.""" + +# Allow non-Google copyright +# pylint: disable=g-bad-file-header + + + +import unittest as googletest +from closure_linter import error_fixer +from closure_linter import testutil + + +class ErrorFixerTest(googletest.TestCase): + """Unit tests for error_fixer.""" + + def setUp(self): + self.error_fixer = error_fixer.ErrorFixer() + + def testDeleteToken(self): + start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_SCRIPT) + second_token = start_token.next + self.error_fixer.HandleFile('test_file', start_token) + + self.error_fixer._DeleteToken(start_token) + + self.assertEqual(second_token, self.error_fixer._file_token) + + def testDeleteTokens(self): + start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_SCRIPT) + fourth_token = start_token.next.next.next + self.error_fixer.HandleFile('test_file', start_token) + + self.error_fixer._DeleteTokens(start_token, 3) + + self.assertEqual(fourth_token, self.error_fixer._file_token) + +_TEST_SCRIPT = """\ +var x = 3; +""" + +if __name__ == '__main__': + googletest.main() diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/errorrecord.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/errorrecord.py new file mode 100644 index 0000000000..ce9fb908c7 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/errorrecord.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# Copyright 2012 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +"""A simple, pickle-serializable class to represent a lint error.""" + +__author__ = 'nnaze@google.com (Nathan Naze)' + +import gflags as flags + +from closure_linter import errors +from closure_linter.common import erroroutput + +FLAGS = flags.FLAGS + + +class ErrorRecord(object): + """Record-keeping struct that can be serialized back from a process. + + Attributes: + path: Path to the file. + error_string: Error string for the user. + new_error: Whether this is a "new error" (see errors.NEW_ERRORS). + """ + + def __init__(self, path, error_string, new_error): + self.path = path + self.error_string = error_string + self.new_error = new_error + + +def MakeErrorRecord(path, error): + """Make an error record with correctly formatted error string. + + Errors are not able to be serialized (pickled) over processes because of + their pointers to the complex token/context graph. We use an intermediary + serializable class to pass back just the relevant information. + + Args: + path: Path of file the error was found in. + error: An error.Error instance. + + Returns: + _ErrorRecord instance. + """ + new_error = error.code in errors.NEW_ERRORS + + if FLAGS.unix_mode: + error_string = erroroutput.GetUnixErrorOutput( + path, error, new_error=new_error) + else: + error_string = erroroutput.GetErrorOutput(error, new_error=new_error) + + return ErrorRecord(path, error_string, new_error) diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/errorrules.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/errorrules.py new file mode 100755 index 0000000000..b1b72aab6d --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/errorrules.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# +# Copyright 2010 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Linter error rules class for Closure Linter.""" + +__author__ = 'robbyw@google.com (Robert Walker)' + +import gflags as flags +from closure_linter import errors + + +FLAGS = flags.FLAGS +flags.DEFINE_boolean('jsdoc', True, + 'Whether to report errors for missing JsDoc.') +flags.DEFINE_list('disable', None, + 'Disable specific error. Usage Ex.: gjslint --disable 1,' + '0011 foo.js.') +flags.DEFINE_integer('max_line_length', 80, 'Maximum line length allowed ' + 'without warning.', lower_bound=1) + +disabled_error_nums = None + + +def GetMaxLineLength(): + """Returns allowed maximum length of line. + + Returns: + Length of line allowed without any warning. + """ + return FLAGS.max_line_length + + +def ShouldReportError(error): + """Whether the given error should be reported. + + Returns: + True for all errors except missing documentation errors and disabled + errors. For missing documentation, it returns the value of the + jsdoc flag. + """ + global disabled_error_nums + if disabled_error_nums is None: + disabled_error_nums = [] + if FLAGS.disable: + for error_str in FLAGS.disable: + error_num = 0 + try: + error_num = int(error_str) + except ValueError: + pass + disabled_error_nums.append(error_num) + + return ((FLAGS.jsdoc or error not in ( + errors.MISSING_PARAMETER_DOCUMENTATION, + errors.MISSING_RETURN_DOCUMENTATION, + errors.MISSING_MEMBER_DOCUMENTATION, + errors.MISSING_PRIVATE, + errors.MISSING_JSDOC_TAG_THIS)) and + (not FLAGS.disable or error not in disabled_error_nums)) diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/errorrules_test.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/errorrules_test.py new file mode 100644 index 0000000000..cb903785e6 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/errorrules_test.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# Copyright 2013 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Medium tests for the gjslint errorrules. + +Currently its just verifying that warnings can't be disabled. +""" + + + +import gflags as flags +import unittest as googletest + +from closure_linter import errors +from closure_linter import runner +from closure_linter.common import erroraccumulator + +flags.FLAGS.strict = True +flags.FLAGS.limited_doc_files = ('dummy.js', 'externs.js') +flags.FLAGS.closurized_namespaces = ('goog', 'dummy') + + +class ErrorRulesTest(googletest.TestCase): + """Test case to for gjslint errorrules.""" + + def testNoMaxLineLengthFlagExists(self): + """Tests that --max_line_length flag does not exists.""" + self.assertTrue('max_line_length' not in flags.FLAGS.FlagDict()) + + def testGetMaxLineLength(self): + """Tests warning are reported for line greater than 80. + """ + + # One line > 100 and one line > 80 and < 100. So should produce two + # line too long error. + original = [ + 'goog.require(\'dummy.aa\');', + '', + 'function a() {', + ' dummy.aa.i = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13' + ' + 14 + 15 + 16 + 17 + 18 + 19 + 20;', + ' dummy.aa.j = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13' + ' + 14 + 15 + 16 + 17 + 18;', + '}', + '' + ] + + # Expect line too long. + expected = [errors.LINE_TOO_LONG, errors.LINE_TOO_LONG] + + self._AssertErrors(original, expected) + + def testNoDisableFlagExists(self): + """Tests that --disable flag does not exists.""" + self.assertTrue('disable' not in flags.FLAGS.FlagDict()) + + def testWarningsNotDisabled(self): + """Tests warnings are reported when nothing is disabled. + """ + original = [ + 'goog.require(\'dummy.aa\');', + 'goog.require(\'dummy.Cc\');', + 'goog.require(\'dummy.Dd\');', + '', + 'function a() {', + ' dummy.aa.i = 1;', + ' dummy.Cc.i = 1;', + ' dummy.Dd.i = 1;', + '}', + ] + + expected = [errors.GOOG_REQUIRES_NOT_ALPHABETIZED, + errors.FILE_MISSING_NEWLINE] + + self._AssertErrors(original, expected) + + def _AssertErrors(self, original, expected_errors, include_header=True): + """Asserts that the error fixer corrects original to expected.""" + if include_header: + original = self._GetHeader() + original + + # Trap gjslint's output parse it to get messages added. + error_accumulator = erroraccumulator.ErrorAccumulator() + runner.Run('testing.js', error_accumulator, source=original) + error_nums = [e.code for e in error_accumulator.GetErrors()] + + error_nums.sort() + expected_errors.sort() + self.assertListEqual(error_nums, expected_errors) + + def _GetHeader(self): + """Returns a fake header for a JavaScript file.""" + return [ + '// Copyright 2011 Google Inc. All Rights Reserved.', + '', + '/**', + ' * @fileoverview Fake file overview.', + ' * @author fake@google.com (Fake Person)', + ' */', + '' + ] + + +if __name__ == '__main__': + googletest.main() diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/errors.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/errors.py new file mode 100755 index 0000000000..7bdfecd9f3 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/errors.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +# Copyright 2007 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Error codes for JavaScript style checker.""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)') + + +def ByName(name): + """Get the error code for the given error name. + + Args: + name: The name of the error + + Returns: + The error code + """ + return globals()[name] + + +# "File-fatal" errors - these errors stop further parsing of a single file +FILE_NOT_FOUND = -1 +FILE_DOES_NOT_PARSE = -2 + +# Spacing +EXTRA_SPACE = 1 +MISSING_SPACE = 2 +EXTRA_LINE = 3 +MISSING_LINE = 4 +ILLEGAL_TAB = 5 +WRONG_INDENTATION = 6 +WRONG_BLANK_LINE_COUNT = 7 + +# Semicolons +MISSING_SEMICOLON = 10 +MISSING_SEMICOLON_AFTER_FUNCTION = 11 +ILLEGAL_SEMICOLON_AFTER_FUNCTION = 12 +REDUNDANT_SEMICOLON = 13 + +# Miscellaneous +ILLEGAL_PROTOTYPE_MEMBER_VALUE = 100 +LINE_TOO_LONG = 110 +LINE_STARTS_WITH_OPERATOR = 120 +COMMA_AT_END_OF_LITERAL = 121 +MULTI_LINE_STRING = 130 +UNNECESSARY_DOUBLE_QUOTED_STRING = 131 +UNUSED_PRIVATE_MEMBER = 132 +UNUSED_LOCAL_VARIABLE = 133 + +# Requires, provides +GOOG_REQUIRES_NOT_ALPHABETIZED = 140 +GOOG_PROVIDES_NOT_ALPHABETIZED = 141 +MISSING_GOOG_REQUIRE = 142 +MISSING_GOOG_PROVIDE = 143 +EXTRA_GOOG_REQUIRE = 144 +EXTRA_GOOG_PROVIDE = 145 + +# JsDoc +INVALID_JSDOC_TAG = 200 +INVALID_USE_OF_DESC_TAG = 201 +NO_BUG_NUMBER_AFTER_BUG_TAG = 202 +MISSING_PARAMETER_DOCUMENTATION = 210 +EXTRA_PARAMETER_DOCUMENTATION = 211 +WRONG_PARAMETER_DOCUMENTATION = 212 +MISSING_JSDOC_TAG_TYPE = 213 +MISSING_JSDOC_TAG_DESCRIPTION = 214 +MISSING_JSDOC_PARAM_NAME = 215 +OUT_OF_ORDER_JSDOC_TAG_TYPE = 216 +MISSING_RETURN_DOCUMENTATION = 217 +UNNECESSARY_RETURN_DOCUMENTATION = 218 +MISSING_BRACES_AROUND_TYPE = 219 +MISSING_MEMBER_DOCUMENTATION = 220 +MISSING_PRIVATE = 221 +EXTRA_PRIVATE = 222 +INVALID_OVERRIDE_PRIVATE = 223 +INVALID_INHERIT_DOC_PRIVATE = 224 +MISSING_JSDOC_TAG_THIS = 225 +UNNECESSARY_BRACES_AROUND_INHERIT_DOC = 226 +INVALID_AUTHOR_TAG_DESCRIPTION = 227 +JSDOC_PREFER_QUESTION_TO_PIPE_NULL = 230 +JSDOC_ILLEGAL_QUESTION_WITH_PIPE = 231 +JSDOC_MISSING_OPTIONAL_TYPE = 232 +JSDOC_MISSING_OPTIONAL_PREFIX = 233 +JSDOC_MISSING_VAR_ARGS_TYPE = 234 +JSDOC_MISSING_VAR_ARGS_NAME = 235 +# TODO(robbyw): Split this in to more specific syntax problems. +INCORRECT_SUPPRESS_SYNTAX = 250 +INVALID_SUPPRESS_TYPE = 251 +UNNECESSARY_SUPPRESS = 252 + +# File ending +FILE_MISSING_NEWLINE = 300 +FILE_IN_BLOCK = 301 + +# Interfaces +INTERFACE_CONSTRUCTOR_CANNOT_HAVE_PARAMS = 400 +INTERFACE_METHOD_CANNOT_HAVE_CODE = 401 + +# Comments +MISSING_END_OF_SCOPE_COMMENT = 500 +MALFORMED_END_OF_SCOPE_COMMENT = 501 + +# goog.scope - Namespace aliasing +# TODO(nnaze) Add additional errors here and in aliaspass.py +INVALID_USE_OF_GOOG_SCOPE = 600 +EXTRA_GOOG_SCOPE_USAGE = 601 + +# ActionScript specific errors: +# TODO(user): move these errors to their own file and move all JavaScript +# specific errors to their own file as well. +# All ActionScript specific errors should have error number at least 1000. +FUNCTION_MISSING_RETURN_TYPE = 1132 +PARAMETER_MISSING_TYPE = 1133 +VAR_MISSING_TYPE = 1134 +PARAMETER_MISSING_DEFAULT_VALUE = 1135 +IMPORTS_NOT_ALPHABETIZED = 1140 +IMPORT_CONTAINS_WILDCARD = 1141 +UNUSED_IMPORT = 1142 +INVALID_TRACE_SEVERITY_LEVEL = 1250 +MISSING_TRACE_SEVERITY_LEVEL = 1251 +MISSING_TRACE_MESSAGE = 1252 +REMOVE_TRACE_BEFORE_SUBMIT = 1253 +REMOVE_COMMENT_BEFORE_SUBMIT = 1254 +# End of list of ActionScript specific errors. + +NEW_ERRORS = frozenset([ + # Errors added after 2.0.2: + WRONG_INDENTATION, + MISSING_SEMICOLON, + # Errors added after 2.3.9: + JSDOC_MISSING_VAR_ARGS_TYPE, + JSDOC_MISSING_VAR_ARGS_NAME, + # Errors added after 2.3.13: + ]) diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/fixjsstyle.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/fixjsstyle.py new file mode 100755 index 0000000000..1b5905b131 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/fixjsstyle.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# +# Copyright 2007 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Automatically fix simple style guide violations.""" + +__author__ = 'robbyw@google.com (Robert Walker)' + +import sys + +import gflags as flags + +from closure_linter import error_fixer +from closure_linter import runner +from closure_linter.common import simplefileflags as fileflags + +FLAGS = flags.FLAGS +flags.DEFINE_list('additional_extensions', None, 'List of additional file ' + 'extensions (not js) that should be treated as ' + 'JavaScript files.') + + +def main(argv=None): + """Main function. + + Args: + argv: Sequence of command line arguments. + """ + if argv is None: + argv = flags.FLAGS(sys.argv) + + suffixes = ['.js'] + if FLAGS.additional_extensions: + suffixes += ['.%s' % ext for ext in FLAGS.additional_extensions] + + files = fileflags.GetFileList(argv, 'JavaScript', suffixes) + + fixer = error_fixer.ErrorFixer() + + # Check the list of files. + for filename in files: + runner.Run(filename, fixer) + + +if __name__ == '__main__': + main() diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/fixjsstyle_test.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/fixjsstyle_test.py new file mode 100755 index 0000000000..27b15eca9e --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/fixjsstyle_test.py @@ -0,0 +1,375 @@ +#!/usr/bin/env python +# Copyright 2008 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Medium tests for the gpylint auto-fixer.""" + +__author__ = 'robbyw@google.com (Robby Walker)' + +import StringIO + +import gflags as flags +import unittest as googletest +from closure_linter import error_fixer +from closure_linter import runner + + +_RESOURCE_PREFIX = 'closure_linter/testdata' + +flags.FLAGS.strict = True +flags.FLAGS.limited_doc_files = ('dummy.js', 'externs.js') +flags.FLAGS.closurized_namespaces = ('goog', 'dummy') + + +class FixJsStyleTest(googletest.TestCase): + """Test case to for gjslint auto-fixing.""" + + def testFixJsStyle(self): + test_cases = [ + ['fixjsstyle.in.js', 'fixjsstyle.out.js'], + ['indentation.js', 'fixjsstyle.indentation.out.js'], + ['fixjsstyle.html.in.html', 'fixjsstyle.html.out.html']] + for [running_input_file, running_output_file] in test_cases: + input_filename = None + golden_filename = None + current_filename = None + try: + input_filename = '%s/%s' % (_RESOURCE_PREFIX, running_input_file) + current_filename = input_filename + + golden_filename = '%s/%s' % (_RESOURCE_PREFIX, running_output_file) + current_filename = golden_filename + except IOError as ex: + raise IOError('Could not find testdata resource for %s: %s' % + (current_filename, ex)) + + if running_input_file == 'fixjsstyle.in.js': + with open(input_filename) as f: + for line in f: + # Go to last line. + pass + self.assertTrue(line == line.rstrip(), '%s file should not end ' + 'with a new line.' % (input_filename)) + + # Autofix the file, sending output to a fake file. + actual = StringIO.StringIO() + runner.Run(input_filename, error_fixer.ErrorFixer(actual)) + + # Now compare the files. + actual.seek(0) + expected = open(golden_filename, 'r') + + self.assertEqual(actual.readlines(), expected.readlines()) + + def testUnsortedRequires(self): + """Tests handling of unsorted goog.require statements without header. + + Bug 8398202. + """ + original = [ + 'goog.require(\'dummy.aa\');', + 'goog.require(\'dummy.Cc\');', + 'goog.require(\'dummy.Dd\');', + '', + 'function a() {', + ' dummy.aa.i = 1;', + ' dummy.Cc.i = 1;', + ' dummy.Dd.i = 1;', + '}', + ] + + expected = [ + 'goog.require(\'dummy.Cc\');', + 'goog.require(\'dummy.Dd\');', + 'goog.require(\'dummy.aa\');', + '', + 'function a() {', + ' dummy.aa.i = 1;', + ' dummy.Cc.i = 1;', + ' dummy.Dd.i = 1;', + '}', + ] + + self._AssertFixes(original, expected, include_header=False) + + def testMissingExtraAndUnsortedRequires(self): + """Tests handling of missing extra and unsorted goog.require statements.""" + original = [ + 'goog.require(\'dummy.aa\');', + 'goog.require(\'dummy.Cc\');', + 'goog.require(\'dummy.Dd\');', + '', + 'var x = new dummy.Bb();', + 'dummy.Cc.someMethod();', + 'dummy.aa.someMethod();', + ] + + expected = [ + 'goog.require(\'dummy.Bb\');', + 'goog.require(\'dummy.Cc\');', + 'goog.require(\'dummy.aa\');', + '', + 'var x = new dummy.Bb();', + 'dummy.Cc.someMethod();', + 'dummy.aa.someMethod();', + ] + + self._AssertFixes(original, expected) + + def testUnsortedProvides(self): + """Tests handling of unsorted goog.provide statements without header. + + Bug 8398202. + """ + original = [ + 'goog.provide(\'dummy.aa\');', + 'goog.provide(\'dummy.Cc\');', + 'goog.provide(\'dummy.Dd\');', + '', + 'dummy.aa = function() {};' + 'dummy.Cc = function() {};' + 'dummy.Dd = function() {};' + ] + + expected = [ + 'goog.provide(\'dummy.Cc\');', + 'goog.provide(\'dummy.Dd\');', + 'goog.provide(\'dummy.aa\');', + '', + 'dummy.aa = function() {};' + 'dummy.Cc = function() {};' + 'dummy.Dd = function() {};' + ] + + self._AssertFixes(original, expected, include_header=False) + + def testMissingExtraAndUnsortedProvides(self): + """Tests handling of missing extra and unsorted goog.provide statements.""" + original = [ + 'goog.provide(\'dummy.aa\');', + 'goog.provide(\'dummy.Cc\');', + 'goog.provide(\'dummy.Dd\');', + '', + 'dummy.Cc = function() {};', + 'dummy.Bb = function() {};', + 'dummy.aa.someMethod = function();', + ] + + expected = [ + 'goog.provide(\'dummy.Bb\');', + 'goog.provide(\'dummy.Cc\');', + 'goog.provide(\'dummy.aa\');', + '', + 'dummy.Cc = function() {};', + 'dummy.Bb = function() {};', + 'dummy.aa.someMethod = function();', + ] + + self._AssertFixes(original, expected) + + def testNoRequires(self): + """Tests positioning of missing requires without existing requires.""" + original = [ + 'goog.provide(\'dummy.Something\');', + '', + 'dummy.Something = function() {};', + '', + 'var x = new dummy.Bb();', + ] + + expected = [ + 'goog.provide(\'dummy.Something\');', + '', + 'goog.require(\'dummy.Bb\');', + '', + 'dummy.Something = function() {};', + '', + 'var x = new dummy.Bb();', + ] + + self._AssertFixes(original, expected) + + def testNoProvides(self): + """Tests positioning of missing provides without existing provides.""" + original = [ + 'goog.require(\'dummy.Bb\');', + '', + 'dummy.Something = function() {};', + '', + 'var x = new dummy.Bb();', + ] + + expected = [ + 'goog.provide(\'dummy.Something\');', + '', + 'goog.require(\'dummy.Bb\');', + '', + 'dummy.Something = function() {};', + '', + 'var x = new dummy.Bb();', + ] + + self._AssertFixes(original, expected) + + def testOutputOkayWhenFirstTokenIsDeleted(self): + """Tests that autofix output is is correct when first token is deleted. + + Regression test for bug 4581567 + """ + original = ['"use strict";'] + expected = ["'use strict';"] + + self._AssertFixes(original, expected, include_header=False) + + def testGoogScopeIndentation(self): + """Tests Handling a typical end-of-scope indentation fix.""" + original = [ + 'goog.scope(function() {', + ' // TODO(brain): Take over the world.', + '}); // goog.scope', + ] + + expected = [ + 'goog.scope(function() {', + '// TODO(brain): Take over the world.', + '}); // goog.scope', + ] + + self._AssertFixes(original, expected) + + def testMissingEndOfScopeComment(self): + """Tests Handling a missing comment at end of goog.scope.""" + original = [ + 'goog.scope(function() {', + '});', + ] + + expected = [ + 'goog.scope(function() {', + '}); // goog.scope', + ] + + self._AssertFixes(original, expected) + + def testMissingEndOfScopeCommentWithOtherComment(self): + """Tests handling an irrelevant comment at end of goog.scope.""" + original = [ + 'goog.scope(function() {', + "}); // I don't belong here!", + ] + + expected = [ + 'goog.scope(function() {', + '}); // goog.scope', + ] + + self._AssertFixes(original, expected) + + def testMalformedEndOfScopeComment(self): + """Tests Handling a malformed comment at end of goog.scope.""" + original = [ + 'goog.scope(function() {', + '}); // goog.scope FTW', + ] + + expected = [ + 'goog.scope(function() {', + '}); // goog.scope', + ] + + self._AssertFixes(original, expected) + + def testEndsWithIdentifier(self): + """Tests Handling case where script ends with identifier. Bug 7643404.""" + original = [ + 'goog.provide(\'xyz\');', + '', + 'abc' + ] + + expected = [ + 'goog.provide(\'xyz\');', + '', + 'abc;' + ] + + self._AssertFixes(original, expected) + + def testFileStartsWithSemicolon(self): + """Tests handling files starting with semicolon. + + b/10062516 + """ + original = [ + ';goog.provide(\'xyz\');', + '', + 'abc;' + ] + + expected = [ + 'goog.provide(\'xyz\');', + '', + 'abc;' + ] + + self._AssertFixes(original, expected, include_header=False) + + def testCodeStartsWithSemicolon(self): + """Tests handling code in starting with semicolon after comments. + + b/10062516 + """ + original = [ + ';goog.provide(\'xyz\');', + '', + 'abc;' + ] + + expected = [ + 'goog.provide(\'xyz\');', + '', + 'abc;' + ] + + self._AssertFixes(original, expected) + + def _AssertFixes(self, original, expected, include_header=True): + """Asserts that the error fixer corrects original to expected.""" + if include_header: + original = self._GetHeader() + original + expected = self._GetHeader() + expected + + actual = StringIO.StringIO() + runner.Run('testing.js', error_fixer.ErrorFixer(actual), original) + actual.seek(0) + + expected = [x + '\n' for x in expected] + + self.assertListEqual(actual.readlines(), expected) + + def _GetHeader(self): + """Returns a fake header for a JavaScript file.""" + return [ + '// Copyright 2011 Google Inc. All Rights Reserved.', + '', + '/**', + ' * @fileoverview Fake file overview.', + ' * @author fake@google.com (Fake Person)', + ' */', + '' + ] + + +if __name__ == '__main__': + googletest.main() diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/full_test.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/full_test.py new file mode 100755 index 0000000000..37f99b489a --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/full_test.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# +# Copyright 2007 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Full regression-type (Medium) tests for gjslint. + +Tests every error that can be thrown by gjslint. Based heavily on +devtools/javascript/gpylint/full_test.py +""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)') + +import os +import sys +import unittest + +import gflags as flags +import unittest as googletest + +from closure_linter import error_check +from closure_linter import errors +from closure_linter import runner +from closure_linter.common import filetestcase + +_RESOURCE_PREFIX = 'closure_linter/testdata' + +flags.FLAGS.strict = True +flags.FLAGS.custom_jsdoc_tags = ('customtag', 'requires') +flags.FLAGS.closurized_namespaces = ('goog', 'dummy') +flags.FLAGS.limited_doc_files = ('externs.js', 'dummy.js', + 'limited_doc_checks.js') +flags.FLAGS.jslint_error = error_check.Rule.ALL + +# List of files under testdata to test. +# We need to list files explicitly since pyglib can't list directories. +# TODO(user): Figure out how to list the directory. +_TEST_FILES = [ + 'all_js_wrapped.js', + 'blank_lines.js', + 'ends_with_block.js', + 'externs.js', + 'externs_jsdoc.js', + 'goog_scope.js', + 'html_parse_error.html', + 'indentation.js', + 'interface.js', + 'jsdoc.js', + 'limited_doc_checks.js', + 'minimal.js', + 'other.js', + 'provide_blank.js', + 'provide_extra.js', + 'provide_missing.js', + 'require_all_caps.js', + 'require_blank.js', + 'require_extra.js', + 'require_function.js', + 'require_function_missing.js', + 'require_function_through_both.js', + 'require_function_through_namespace.js', + 'require_interface.js', + 'require_interface_base.js', + 'require_lower_case.js', + 'require_missing.js', + 'require_numeric.js', + 'require_provide_blank.js', + 'require_provide_missing.js', + 'require_provide_ok.js', + 'semicolon_missing.js', + 'simple.html', + 'spaces.js', + 'tokenizer.js', + 'unparseable.js', + 'unused_local_variables.js', + 'unused_private_members.js', + 'utf8.html', +] + + +class GJsLintTestSuite(unittest.TestSuite): + """Test suite to run a GJsLintTest for each of several files. + + If sys.argv[1:] is non-empty, it is interpreted as a list of filenames in + testdata to test. Otherwise, _TEST_FILES is used. + """ + + def __init__(self, tests=()): + unittest.TestSuite.__init__(self, tests) + + argv = sys.argv and sys.argv[1:] or [] + if argv: + test_files = argv + else: + test_files = _TEST_FILES + for test_file in test_files: + resource_path = os.path.join(_RESOURCE_PREFIX, test_file) + self.addTest( + filetestcase.AnnotatedFileTestCase( + resource_path, + runner.Run, + errors.ByName)) + +if __name__ == '__main__': + # Don't let main parse args; it happens in the TestSuite. + googletest.main(argv=sys.argv[0:1], defaultTest='GJsLintTestSuite') diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/gjslint.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/gjslint.py new file mode 100755 index 0000000000..ca4069a881 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/gjslint.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python +# Copyright 2007 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Checks JavaScript files for common style guide violations. + +gjslint.py is designed to be used as a PRESUBMIT script to check for javascript +style guide violations. As of now, it checks for the following violations: + + * Missing and extra spaces + * Lines longer than 80 characters + * Missing newline at end of file + * Missing semicolon after function declaration + * Valid JsDoc including parameter matching + +Someday it will validate to the best of its ability against the entirety of the +JavaScript style guide. + +This file is a front end that parses arguments and flags. The core of the code +is in tokenizer.py and checker.py. +""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)', + 'nnaze@google.com (Nathan Naze)',) + +import errno +import itertools +import platform +import sys +import time + +import gflags as flags + +from closure_linter import errorrecord +from closure_linter import runner +from closure_linter.common import erroraccumulator +from closure_linter.common import simplefileflags as fileflags + +# Attempt import of multiprocessing (should be available in Python 2.6 and up). +try: + # pylint: disable=g-import-not-at-top + import multiprocessing +except ImportError: + multiprocessing = None + +FLAGS = flags.FLAGS +flags.DEFINE_boolean('unix_mode', False, + 'Whether to emit warnings in standard unix format.') +flags.DEFINE_boolean('beep', True, 'Whether to beep when errors are found.') +flags.DEFINE_boolean('time', False, 'Whether to emit timing statistics.') +flags.DEFINE_boolean('check_html', False, + 'Whether to check javascript in html files.') +flags.DEFINE_boolean('summary', False, + 'Whether to show an error count summary.') +flags.DEFINE_list('additional_extensions', None, 'List of additional file ' + 'extensions (not js) that should be treated as ' + 'JavaScript files.') +flags.DEFINE_boolean('multiprocess', + platform.system() is 'Linux' and bool(multiprocessing), + 'Whether to attempt parallelized linting using the ' + 'multiprocessing module. Enabled by default on Linux ' + 'if the multiprocessing module is present (Python 2.6+). ' + 'Otherwise disabled by default. ' + 'Disabling may make debugging easier.') + + +GJSLINT_ONLY_FLAGS = ['--unix_mode', '--beep', '--nobeep', '--time', + '--check_html', '--summary'] + + +def _MultiprocessCheckPaths(paths): + """Run _CheckPath over mutltiple processes. + + Tokenization, passes, and checks are expensive operations. Running in a + single process, they can only run on one CPU/core. Instead, + shard out linting over all CPUs with multiprocessing to parallelize. + + Args: + paths: paths to check. + + Yields: + errorrecord.ErrorRecords for any found errors. + """ + + pool = multiprocessing.Pool() + + path_results = pool.imap(_CheckPath, paths) + for results in path_results: + for result in results: + yield result + + # Force destruct before returning, as this can sometimes raise spurious + # "interrupted system call" (EINTR), which we can ignore. + try: + pool.close() + pool.join() + del pool + except OSError as err: + if err.errno is not errno.EINTR: + raise err + + +def _CheckPaths(paths): + """Run _CheckPath on all paths in one thread. + + Args: + paths: paths to check. + + Yields: + errorrecord.ErrorRecords for any found errors. + """ + + for path in paths: + results = _CheckPath(path) + for record in results: + yield record + + +def _CheckPath(path): + """Check a path and return any errors. + + Args: + path: paths to check. + + Returns: + A list of errorrecord.ErrorRecords for any found errors. + """ + + error_handler = erroraccumulator.ErrorAccumulator() + runner.Run(path, error_handler) + + make_error_record = lambda err: errorrecord.MakeErrorRecord(path, err) + return map(make_error_record, error_handler.GetErrors()) + + +def _GetFilePaths(argv): + suffixes = ['.js'] + if FLAGS.additional_extensions: + suffixes += ['.%s' % ext for ext in FLAGS.additional_extensions] + if FLAGS.check_html: + suffixes += ['.html', '.htm'] + return fileflags.GetFileList(argv, 'JavaScript', suffixes) + + +# Error printing functions + + +def _PrintFileSummary(paths, records): + """Print a detailed summary of the number of errors in each file.""" + + paths = list(paths) + paths.sort() + + for path in paths: + path_errors = [e for e in records if e.path == path] + print '%s: %d' % (path, len(path_errors)) + + +def _PrintFileSeparator(path): + print '----- FILE : %s -----' % path + + +def _PrintSummary(paths, error_records): + """Print a summary of the number of errors and files.""" + + error_count = len(error_records) + all_paths = set(paths) + all_paths_count = len(all_paths) + + if error_count is 0: + print '%d files checked, no errors found.' % all_paths_count + + new_error_count = len([e for e in error_records if e.new_error]) + + error_paths = set([e.path for e in error_records]) + error_paths_count = len(error_paths) + no_error_paths_count = all_paths_count - error_paths_count + + if error_count or new_error_count: + print ('Found %d errors, including %d new errors, in %d files ' + '(%d files OK).' % ( + error_count, + new_error_count, + error_paths_count, + no_error_paths_count)) + + +def _PrintErrorRecords(error_records): + """Print error records strings in the expected format.""" + + current_path = None + for record in error_records: + + if current_path != record.path: + current_path = record.path + if not FLAGS.unix_mode: + _PrintFileSeparator(current_path) + + print record.error_string + + +def _FormatTime(t): + """Formats a duration as a human-readable string. + + Args: + t: A duration in seconds. + + Returns: + A formatted duration string. + """ + if t < 1: + return '%dms' % round(t * 1000) + else: + return '%.2fs' % t + + +def main(argv=None): + """Main function. + + Args: + argv: Sequence of command line arguments. + """ + if argv is None: + argv = flags.FLAGS(sys.argv) + + if FLAGS.time: + start_time = time.time() + + suffixes = ['.js'] + if FLAGS.additional_extensions: + suffixes += ['.%s' % ext for ext in FLAGS.additional_extensions] + if FLAGS.check_html: + suffixes += ['.html', '.htm'] + paths = fileflags.GetFileList(argv, 'JavaScript', suffixes) + + if FLAGS.multiprocess: + records_iter = _MultiprocessCheckPaths(paths) + else: + records_iter = _CheckPaths(paths) + + records_iter, records_iter_copy = itertools.tee(records_iter, 2) + _PrintErrorRecords(records_iter_copy) + + error_records = list(records_iter) + _PrintSummary(paths, error_records) + + exit_code = 0 + + # If there are any errors + if error_records: + exit_code += 1 + + # If there are any new errors + if [r for r in error_records if r.new_error]: + exit_code += 2 + + if exit_code: + if FLAGS.summary: + _PrintFileSummary(paths, error_records) + + if FLAGS.beep: + # Make a beep noise. + sys.stdout.write(chr(7)) + + if FLAGS.time: + print 'Done in %s.' % _FormatTime(time.time() - start_time) + + sys.exit(exit_code) + + +if __name__ == '__main__': + main() diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/indentation.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/indentation.py new file mode 100755 index 0000000000..0abb25b149 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/indentation.py @@ -0,0 +1,581 @@ +#!/usr/bin/env python +# Copyright 2010 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Methods for checking EcmaScript files for indentation issues.""" + +__author__ = ('robbyw@google.com (Robert Walker)') + +import gflags as flags + +from closure_linter import ecmametadatapass +from closure_linter import errors +from closure_linter import javascripttokens +from closure_linter import tokenutil +from closure_linter.common import error +from closure_linter.common import position + + +flags.DEFINE_boolean('debug_indentation', False, + 'Whether to print debugging information for indentation.') + + +# Shorthand +Context = ecmametadatapass.EcmaContext +Error = error.Error +Position = position.Position +Type = javascripttokens.JavaScriptTokenType + + +# The general approach: +# +# 1. Build a stack of tokens that can affect indentation. +# For each token, we determine if it is a block or continuation token. +# Some tokens need to be temporarily overwritten in case they are removed +# before the end of the line. +# Much of the work here is determining which tokens to keep on the stack +# at each point. Operators, for example, should be removed once their +# expression or line is gone, while parentheses must stay until the matching +# end parentheses is found. +# +# 2. Given that stack, determine the allowable indentations. +# Due to flexible indentation rules in JavaScript, there may be many +# allowable indentations for each stack. We follows the general +# "no false positives" approach of GJsLint and build the most permissive +# set possible. + + +class TokenInfo(object): + """Stores information about a token. + + Attributes: + token: The token + is_block: Whether the token represents a block indentation. + is_transient: Whether the token should be automatically removed without + finding a matching end token. + overridden_by: TokenInfo for a token that overrides the indentation that + this token would require. + is_permanent_override: Whether the override on this token should persist + even after the overriding token is removed from the stack. For example: + x([ + 1], + 2); + needs this to be set so the last line is not required to be a continuation + indent. + line_number: The effective line number of this token. Will either be the + actual line number or the one before it in the case of a mis-wrapped + operator. + """ + + def __init__(self, token, is_block=False): + """Initializes a TokenInfo object. + + Args: + token: The token + is_block: Whether the token represents a block indentation. + """ + self.token = token + self.overridden_by = None + self.is_permanent_override = False + self.is_block = is_block + self.is_transient = not is_block and token.type not in ( + Type.START_PAREN, Type.START_PARAMETERS) + self.line_number = token.line_number + + def __repr__(self): + result = '\n %s' % self.token + if self.overridden_by: + result = '%s OVERRIDDEN [by "%s"]' % ( + result, self.overridden_by.token.string) + result += ' {is_block: %s, is_transient: %s}' % ( + self.is_block, self.is_transient) + return result + + +class IndentationRules(object): + """EmcaScript indentation rules. + + Can be used to find common indentation errors in JavaScript, ActionScript and + other Ecma like scripting languages. + """ + + def __init__(self): + """Initializes the IndentationRules checker.""" + self._stack = [] + + # Map from line number to number of characters it is off in indentation. + self._start_index_offset = {} + + def Finalize(self): + if self._stack: + old_stack = self._stack + self._stack = [] + raise Exception('INTERNAL ERROR: indentation stack is not empty: %r' % + old_stack) + + def CheckToken(self, token, state): + """Checks a token for indentation errors. + + Args: + token: The current token under consideration + state: Additional information about the current tree state + + Returns: + An error array [error code, error string, error token] if the token is + improperly indented, or None if indentation is correct. + """ + + token_type = token.type + indentation_errors = [] + stack = self._stack + is_first = self._IsFirstNonWhitespaceTokenInLine(token) + + # Add tokens that could decrease indentation before checking. + if token_type == Type.END_PAREN: + self._PopTo(Type.START_PAREN) + + elif token_type == Type.END_PARAMETERS: + self._PopTo(Type.START_PARAMETERS) + + elif token_type == Type.END_BRACKET: + self._PopTo(Type.START_BRACKET) + + elif token_type == Type.END_BLOCK: + start_token = self._PopTo(Type.START_BLOCK) + # Check for required goog.scope comment. + if start_token: + goog_scope = tokenutil.GoogScopeOrNoneFromStartBlock(start_token.token) + if goog_scope is not None: + if not token.line.endswith('; // goog.scope\n'): + if (token.line.find('//') > -1 and + token.line.find('goog.scope') > + token.line.find('//')): + indentation_errors.append([ + errors.MALFORMED_END_OF_SCOPE_COMMENT, + ('Malformed end of goog.scope comment. Please use the ' + 'exact following syntax to close the scope:\n' + '}); // goog.scope'), + token, + Position(token.start_index, token.length)]) + else: + indentation_errors.append([ + errors.MISSING_END_OF_SCOPE_COMMENT, + ('Missing comment for end of goog.scope which opened at line ' + '%d. End the scope with:\n' + '}); // goog.scope' % + (start_token.line_number)), + token, + Position(token.start_index, token.length)]) + + elif token_type == Type.KEYWORD and token.string in ('case', 'default'): + self._Add(self._PopTo(Type.START_BLOCK)) + + elif is_first and token.string == '.': + # This token should have been on the previous line, so treat it as if it + # was there. + info = TokenInfo(token) + info.line_number = token.line_number - 1 + self._Add(info) + + elif token_type == Type.SEMICOLON: + self._PopTransient() + + not_binary_operator = (token_type != Type.OPERATOR or + token.metadata.IsUnaryOperator()) + not_dot = token.string != '.' + if is_first and not_binary_operator and not_dot and token.type not in ( + Type.COMMENT, Type.DOC_PREFIX, Type.STRING_TEXT): + if flags.FLAGS.debug_indentation: + print 'Line #%d: stack %r' % (token.line_number, stack) + + # Ignore lines that start in JsDoc since we don't check them properly yet. + # TODO(robbyw): Support checking JsDoc indentation. + # Ignore lines that start as multi-line strings since indentation is N/A. + # Ignore lines that start with operators since we report that already. + # Ignore lines with tabs since we report that already. + expected = self._GetAllowableIndentations() + actual = self._GetActualIndentation(token) + + # Special case comments describing else, case, and default. Allow them + # to outdent to the parent block. + if token_type in Type.COMMENT_TYPES: + next_code = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES) + if next_code and next_code.type == Type.END_BLOCK: + next_code = tokenutil.SearchExcept(next_code, Type.NON_CODE_TYPES) + if next_code and next_code.string in ('else', 'case', 'default'): + # TODO(robbyw): This almost certainly introduces false negatives. + expected |= self._AddToEach(expected, -2) + + if actual >= 0 and actual not in expected: + expected = sorted(expected) + indentation_errors.append([ + errors.WRONG_INDENTATION, + 'Wrong indentation: expected any of {%s} but got %d' % ( + ', '.join( + ['%d' % x for x in expected]), actual), + token, + Position(actual, expected[0])]) + self._start_index_offset[token.line_number] = expected[0] - actual + + # Add tokens that could increase indentation. + if token_type == Type.START_BRACKET: + self._Add(TokenInfo( + token=token, + is_block=token.metadata.context.type == Context.ARRAY_LITERAL)) + + elif token_type == Type.START_BLOCK or token.metadata.is_implied_block: + self._Add(TokenInfo(token=token, is_block=True)) + + elif token_type in (Type.START_PAREN, Type.START_PARAMETERS): + self._Add(TokenInfo(token=token, is_block=False)) + + elif token_type == Type.KEYWORD and token.string == 'return': + self._Add(TokenInfo(token)) + + elif not token.IsLastInLine() and ( + token.IsAssignment() or token.IsOperator('?')): + self._Add(TokenInfo(token=token)) + + # Handle implied block closes. + if token.metadata.is_implied_block_close: + self._PopToImpliedBlock() + + # Add some tokens only if they appear at the end of the line. + is_last = self._IsLastCodeInLine(token) + if is_last: + if token_type == Type.OPERATOR: + if token.string == ':': + if stack and stack[-1].token.string == '?': + # When a ternary : is on a different line than its '?', it doesn't + # add indentation. + if token.line_number == stack[-1].token.line_number: + self._Add(TokenInfo(token)) + elif token.metadata.context.type == Context.CASE_BLOCK: + # Pop transient tokens from say, line continuations, e.g., + # case x. + # y: + # Want to pop the transient 4 space continuation indent. + self._PopTransient() + # Starting the body of the case statement, which is a type of + # block. + self._Add(TokenInfo(token=token, is_block=True)) + elif token.metadata.context.type == Context.LITERAL_ELEMENT: + # When in an object literal, acts as operator indicating line + # continuations. + self._Add(TokenInfo(token)) + pass + else: + # ':' might also be a statement label, no effect on indentation in + # this case. + pass + + elif token.string != ',': + self._Add(TokenInfo(token)) + else: + # The token is a comma. + if token.metadata.context.type == Context.VAR: + self._Add(TokenInfo(token)) + elif token.metadata.context.type != Context.PARAMETERS: + self._PopTransient() + + elif (token.string.endswith('.') + and token_type in (Type.IDENTIFIER, Type.NORMAL)): + self._Add(TokenInfo(token)) + elif token_type == Type.PARAMETERS and token.string.endswith(','): + # Parameter lists. + self._Add(TokenInfo(token)) + elif token.IsKeyword('var'): + self._Add(TokenInfo(token)) + elif token.metadata.is_implied_semicolon: + self._PopTransient() + elif token.IsAssignment(): + self._Add(TokenInfo(token)) + + return indentation_errors + + def _AddToEach(self, original, amount): + """Returns a new set with the given amount added to each element. + + Args: + original: The original set of numbers + amount: The amount to add to each element + + Returns: + A new set containing each element of the original set added to the amount. + """ + return set([x + amount for x in original]) + + _HARD_STOP_TYPES = (Type.START_PAREN, Type.START_PARAMETERS, + Type.START_BRACKET) + + _HARD_STOP_STRINGS = ('return', '?') + + def _IsHardStop(self, token): + """Determines if the given token can have a hard stop after it. + + Args: + token: token to examine + + Returns: + Whether the token can have a hard stop after it. + + Hard stops are indentations defined by the position of another token as in + indentation lined up with return, (, [, and ?. + """ + return (token.type in self._HARD_STOP_TYPES or + token.string in self._HARD_STOP_STRINGS or + token.IsAssignment()) + + def _GetAllowableIndentations(self): + """Computes the set of allowable indentations. + + Returns: + The set of allowable indentations, given the current stack. + """ + expected = set([0]) + hard_stops = set([]) + + # Whether the tokens are still in the same continuation, meaning additional + # indentation is optional. As an example: + # x = 5 + + # 6 + + # 7; + # The second '+' does not add any required indentation. + in_same_continuation = False + + for token_info in self._stack: + token = token_info.token + + # Handle normal additive indentation tokens. + if not token_info.overridden_by and token.string != 'return': + if token_info.is_block: + expected = self._AddToEach(expected, 2) + hard_stops = self._AddToEach(hard_stops, 2) + in_same_continuation = False + elif in_same_continuation: + expected |= self._AddToEach(expected, 4) + hard_stops |= self._AddToEach(hard_stops, 4) + else: + expected = self._AddToEach(expected, 4) + hard_stops |= self._AddToEach(hard_stops, 4) + in_same_continuation = True + + # Handle hard stops after (, [, return, =, and ? + if self._IsHardStop(token): + override_is_hard_stop = (token_info.overridden_by and + self._IsHardStop( + token_info.overridden_by.token)) + if not override_is_hard_stop: + start_index = token.start_index + if token.line_number in self._start_index_offset: + start_index += self._start_index_offset[token.line_number] + if (token.type in (Type.START_PAREN, Type.START_PARAMETERS) and + not token_info.overridden_by): + hard_stops.add(start_index + 1) + + elif token.string == 'return' and not token_info.overridden_by: + hard_stops.add(start_index + 7) + + elif token.type == Type.START_BRACKET: + hard_stops.add(start_index + 1) + + elif token.IsAssignment(): + hard_stops.add(start_index + len(token.string) + 1) + + elif token.IsOperator('?') and not token_info.overridden_by: + hard_stops.add(start_index + 2) + + return (expected | hard_stops) or set([0]) + + def _GetActualIndentation(self, token): + """Gets the actual indentation of the line containing the given token. + + Args: + token: Any token on the line. + + Returns: + The actual indentation of the line containing the given token. Returns + -1 if this line should be ignored due to the presence of tabs. + """ + # Move to the first token in the line + token = tokenutil.GetFirstTokenInSameLine(token) + + # If it is whitespace, it is the indentation. + if token.type == Type.WHITESPACE: + if token.string.find('\t') >= 0: + return -1 + else: + return len(token.string) + elif token.type == Type.PARAMETERS: + return len(token.string) - len(token.string.lstrip()) + else: + return 0 + + def _IsFirstNonWhitespaceTokenInLine(self, token): + """Determines if the given token is the first non-space token on its line. + + Args: + token: The token. + + Returns: + True if the token is the first non-whitespace token on its line. + """ + if token.type in (Type.WHITESPACE, Type.BLANK_LINE): + return False + if token.IsFirstInLine(): + return True + return (token.previous and token.previous.IsFirstInLine() and + token.previous.type == Type.WHITESPACE) + + def _IsLastCodeInLine(self, token): + """Determines if the given token is the last code token on its line. + + Args: + token: The token. + + Returns: + True if the token is the last code token on its line. + """ + if token.type in Type.NON_CODE_TYPES: + return False + start_token = token + while True: + token = token.next + if not token or token.line_number != start_token.line_number: + return True + if token.type not in Type.NON_CODE_TYPES: + return False + + def _Add(self, token_info): + """Adds the given token info to the stack. + + Args: + token_info: The token information to add. + """ + if self._stack and self._stack[-1].token == token_info.token: + # Don't add the same token twice. + return + + if token_info.is_block or token_info.token.type == Type.START_PAREN: + token_info.overridden_by = ( + tokenutil.GoogScopeOrNoneFromStartBlock(token_info.token)) + index = 1 + while index <= len(self._stack): + stack_info = self._stack[-index] + stack_token = stack_info.token + + if stack_info.line_number == token_info.line_number: + # In general, tokens only override each other when they are on + # the same line. + stack_info.overridden_by = token_info + if (token_info.token.type == Type.START_BLOCK and + (stack_token.IsAssignment() or + stack_token.type in (Type.IDENTIFIER, Type.START_PAREN))): + # Multi-line blocks have lasting overrides, as in: + # callFn({ + # a: 10 + # }, + # 30); + # b/11450054. If a string is not closed properly then close_block + # could be null. + close_block = token_info.token.metadata.context.end_token + stack_info.is_permanent_override = close_block and ( + close_block.line_number != token_info.token.line_number) + elif (token_info.token.type == Type.START_BLOCK and + token_info.token.metadata.context.type == Context.BLOCK and + (stack_token.IsAssignment() or + stack_token.type == Type.IDENTIFIER)): + # When starting a function block, the override can transcend lines. + # For example + # long.long.name = function( + # a) { + # In this case the { and the = are on different lines. But the + # override should still apply. + stack_info.overridden_by = token_info + stack_info.is_permanent_override = True + else: + break + index += 1 + + self._stack.append(token_info) + + def _Pop(self): + """Pops the top token from the stack. + + Returns: + The popped token info. + """ + token_info = self._stack.pop() + if token_info.token.type not in (Type.START_BLOCK, Type.START_BRACKET): + # Remove any temporary overrides. + self._RemoveOverrides(token_info) + else: + # For braces and brackets, which can be object and array literals, remove + # overrides when the literal is closed on the same line. + token_check = token_info.token + same_type = token_check.type + goal_type = None + if token_info.token.type == Type.START_BRACKET: + goal_type = Type.END_BRACKET + else: + goal_type = Type.END_BLOCK + line_number = token_info.token.line_number + count = 0 + while token_check and token_check.line_number == line_number: + if token_check.type == goal_type: + count -= 1 + if not count: + self._RemoveOverrides(token_info) + break + if token_check.type == same_type: + count += 1 + token_check = token_check.next + return token_info + + def _PopToImpliedBlock(self): + """Pops the stack until an implied block token is found.""" + while not self._Pop().token.metadata.is_implied_block: + pass + + def _PopTo(self, stop_type): + """Pops the stack until a token of the given type is popped. + + Args: + stop_type: The type of token to pop to. + + Returns: + The token info of the given type that was popped. + """ + last = None + while True: + last = self._Pop() + if last.token.type == stop_type: + break + return last + + def _RemoveOverrides(self, token_info): + """Marks any token that was overridden by this token as active again. + + Args: + token_info: The token that is being removed from the stack. + """ + for stack_token in self._stack: + if (stack_token.overridden_by == token_info and + not stack_token.is_permanent_override): + stack_token.overridden_by = None + + def _PopTransient(self): + """Pops all transient tokens - i.e. not blocks, literals, or parens.""" + while self._stack and self._stack[-1].is_transient: + self._Pop() diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascriptlintrules.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascriptlintrules.py new file mode 100755 index 0000000000..0be01ee26b --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascriptlintrules.py @@ -0,0 +1,738 @@ +#!/usr/bin/env python +# Copyright 2011 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Methods for checking JS files for common style guide violations. + +These style guide violations should only apply to JavaScript and not an Ecma +scripting languages. +""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)', + 'jacobr@google.com (Jacob Richman)') + +import re + +from closure_linter import ecmalintrules +from closure_linter import error_check +from closure_linter import errors +from closure_linter import javascripttokenizer +from closure_linter import javascripttokens +from closure_linter import requireprovidesorter +from closure_linter import tokenutil +from closure_linter.common import error +from closure_linter.common import position + +# Shorthand +Error = error.Error +Position = position.Position +Rule = error_check.Rule +Type = javascripttokens.JavaScriptTokenType + + +class JavaScriptLintRules(ecmalintrules.EcmaScriptLintRules): + """JavaScript lint rules that catch JavaScript specific style errors.""" + + def __init__(self, namespaces_info): + """Initializes a JavaScriptLintRules instance.""" + ecmalintrules.EcmaScriptLintRules.__init__(self) + self._namespaces_info = namespaces_info + self._declared_private_member_tokens = {} + self._declared_private_members = set() + self._used_private_members = set() + # A stack of dictionaries, one for each function scope entered. Each + # dictionary is keyed by an identifier that defines a local variable and has + # a token as its value. + self._unused_local_variables_by_scope = [] + + def HandleMissingParameterDoc(self, token, param_name): + """Handle errors associated with a parameter missing a param tag.""" + self._HandleError(errors.MISSING_PARAMETER_DOCUMENTATION, + 'Missing docs for parameter: "%s"' % param_name, token) + + def __ContainsRecordType(self, token): + """Check whether the given token contains a record type. + + Args: + token: The token being checked + + Returns: + True if the token contains a record type, False otherwise. + """ + # If we see more than one left-brace in the string of an annotation token, + # then there's a record type in there. + return ( + token and token.type == Type.DOC_FLAG and + token.attached_object.type is not None and + token.attached_object.type.find('{') != token.string.rfind('{')) + + def CheckToken(self, token, state): + """Checks a token, given the current parser_state, for warnings and errors. + + Args: + token: The current token under consideration + state: parser_state object that indicates the current state in the page + """ + + # For @param don't ignore record type. + if (self.__ContainsRecordType(token) and + token.attached_object.flag_type != 'param'): + # We should bail out and not emit any warnings for this annotation. + # TODO(nicksantos): Support record types for real. + state.GetDocComment().Invalidate() + return + + # Call the base class's CheckToken function. + super(JavaScriptLintRules, self).CheckToken(token, state) + + # Store some convenience variables + namespaces_info = self._namespaces_info + + if error_check.ShouldCheck(Rule.UNUSED_LOCAL_VARIABLES): + self._CheckUnusedLocalVariables(token, state) + + if error_check.ShouldCheck(Rule.UNUSED_PRIVATE_MEMBERS): + # Find all assignments to private members. + if token.type == Type.SIMPLE_LVALUE: + identifier = token.string + if identifier.endswith('_') and not identifier.endswith('__'): + doc_comment = state.GetDocComment() + suppressed = (doc_comment and doc_comment.HasFlag('suppress') and + (doc_comment.GetFlag('suppress').type == 'underscore' or + doc_comment.GetFlag('suppress').type == + 'unusedPrivateMembers')) + if not suppressed: + # Look for static members defined on a provided namespace. + if namespaces_info: + namespace = namespaces_info.GetClosurizedNamespace(identifier) + provided_namespaces = namespaces_info.GetProvidedNamespaces() + else: + namespace = None + provided_namespaces = set() + + # Skip cases of this.something_.somethingElse_. + regex = re.compile(r'^this\.[a-zA-Z_]+$') + if namespace in provided_namespaces or regex.match(identifier): + variable = identifier.split('.')[-1] + self._declared_private_member_tokens[variable] = token + self._declared_private_members.add(variable) + elif not identifier.endswith('__'): + # Consider setting public members of private members to be a usage. + for piece in identifier.split('.'): + if piece.endswith('_'): + self._used_private_members.add(piece) + + # Find all usages of private members. + if token.type == Type.IDENTIFIER: + for piece in token.string.split('.'): + if piece.endswith('_'): + self._used_private_members.add(piece) + + if token.type == Type.DOC_FLAG: + flag = token.attached_object + + if flag.flag_type == 'param' and flag.name_token is not None: + self._CheckForMissingSpaceBeforeToken( + token.attached_object.name_token) + + if flag.type is not None and flag.name is not None: + if error_check.ShouldCheck(Rule.VARIABLE_ARG_MARKER): + # Check for variable arguments marker in type. + if (flag.type.startswith('...') and + flag.name != 'var_args'): + self._HandleError(errors.JSDOC_MISSING_VAR_ARGS_NAME, + 'Variable length argument %s must be renamed ' + 'to var_args.' % flag.name, + token) + elif (not flag.type.startswith('...') and + flag.name == 'var_args'): + self._HandleError(errors.JSDOC_MISSING_VAR_ARGS_TYPE, + 'Variable length argument %s type must start ' + 'with \'...\'.' % flag.name, + token) + + if error_check.ShouldCheck(Rule.OPTIONAL_TYPE_MARKER): + # Check for optional marker in type. + if (flag.type.endswith('=') and + not flag.name.startswith('opt_')): + self._HandleError(errors.JSDOC_MISSING_OPTIONAL_PREFIX, + 'Optional parameter name %s must be prefixed ' + 'with opt_.' % flag.name, + token) + elif (not flag.type.endswith('=') and + flag.name.startswith('opt_')): + self._HandleError(errors.JSDOC_MISSING_OPTIONAL_TYPE, + 'Optional parameter %s type must end with =.' % + flag.name, + token) + + if flag.flag_type in state.GetDocFlag().HAS_TYPE: + # Check for both missing type token and empty type braces '{}' + # Missing suppress types are reported separately and we allow enums + # and const without types. + if (flag.flag_type not in ('suppress', 'enum', 'const') and + (not flag.type or flag.type.isspace())): + self._HandleError(errors.MISSING_JSDOC_TAG_TYPE, + 'Missing type in %s tag' % token.string, token) + + elif flag.name_token and flag.type_end_token and tokenutil.Compare( + flag.type_end_token, flag.name_token) > 0: + self._HandleError( + errors.OUT_OF_ORDER_JSDOC_TAG_TYPE, + 'Type should be immediately after %s tag' % token.string, + token) + + elif token.type == Type.DOUBLE_QUOTE_STRING_START: + next_token = token.next + while next_token.type == Type.STRING_TEXT: + if javascripttokenizer.JavaScriptTokenizer.SINGLE_QUOTE.search( + next_token.string): + break + next_token = next_token.next + else: + self._HandleError( + errors.UNNECESSARY_DOUBLE_QUOTED_STRING, + 'Single-quoted string preferred over double-quoted string.', + token, + position=Position.All(token.string)) + + elif token.type == Type.END_DOC_COMMENT: + doc_comment = state.GetDocComment() + + # When @externs appears in a @fileoverview comment, it should trigger + # the same limited doc checks as a special filename like externs.js. + if doc_comment.HasFlag('fileoverview') and doc_comment.HasFlag('externs'): + self._SetLimitedDocChecks(True) + + if (error_check.ShouldCheck(Rule.BLANK_LINES_AT_TOP_LEVEL) and + not self._is_html and + state.InTopLevel() and + not state.InNonScopeBlock()): + + # Check if we're in a fileoverview or constructor JsDoc. + is_constructor = ( + doc_comment.HasFlag('constructor') or + doc_comment.HasFlag('interface')) + # @fileoverview is an optional tag so if the dosctring is the first + # token in the file treat it as a file level docstring. + is_file_level_comment = ( + doc_comment.HasFlag('fileoverview') or + not doc_comment.start_token.previous) + + # If the comment is not a file overview, and it does not immediately + # precede some code, skip it. + # NOTE: The tokenutil methods are not used here because of their + # behavior at the top of a file. + next_token = token.next + if (not next_token or + (not is_file_level_comment and + next_token.type in Type.NON_CODE_TYPES)): + return + + # Don't require extra blank lines around suppression of extra + # goog.require errors. + if (doc_comment.SuppressionOnly() and + next_token.type == Type.IDENTIFIER and + next_token.string in ['goog.provide', 'goog.require']): + return + + # Find the start of this block (include comments above the block, unless + # this is a file overview). + block_start = doc_comment.start_token + if not is_file_level_comment: + token = block_start.previous + while token and token.type in Type.COMMENT_TYPES: + block_start = token + token = token.previous + + # Count the number of blank lines before this block. + blank_lines = 0 + token = block_start.previous + while token and token.type in [Type.WHITESPACE, Type.BLANK_LINE]: + if token.type == Type.BLANK_LINE: + # A blank line. + blank_lines += 1 + elif token.type == Type.WHITESPACE and not token.line.strip(): + # A line with only whitespace on it. + blank_lines += 1 + token = token.previous + + # Log errors. + error_message = False + expected_blank_lines = 0 + + # Only need blank line before file overview if it is not the beginning + # of the file, e.g. copyright is first. + if is_file_level_comment and blank_lines == 0 and block_start.previous: + error_message = 'Should have a blank line before a file overview.' + expected_blank_lines = 1 + elif is_constructor and blank_lines != 3: + error_message = ( + 'Should have 3 blank lines before a constructor/interface.') + expected_blank_lines = 3 + elif (not is_file_level_comment and not is_constructor and + blank_lines != 2): + error_message = 'Should have 2 blank lines between top-level blocks.' + expected_blank_lines = 2 + + if error_message: + self._HandleError( + errors.WRONG_BLANK_LINE_COUNT, error_message, + block_start, position=Position.AtBeginning(), + fix_data=expected_blank_lines - blank_lines) + + elif token.type == Type.END_BLOCK: + if state.InFunction() and state.IsFunctionClose(): + is_immediately_called = (token.next and + token.next.type == Type.START_PAREN) + + function = state.GetFunction() + if not self._limited_doc_checks: + if (function.has_return and function.doc and + not is_immediately_called and + not function.doc.HasFlag('return') and + not function.doc.InheritsDocumentation() and + not function.doc.HasFlag('constructor')): + # Check for proper documentation of return value. + self._HandleError( + errors.MISSING_RETURN_DOCUMENTATION, + 'Missing @return JsDoc in function with non-trivial return', + function.doc.end_token, position=Position.AtBeginning()) + elif (not function.has_return and + not function.has_throw and + function.doc and + function.doc.HasFlag('return') and + not state.InInterfaceMethod()): + return_flag = function.doc.GetFlag('return') + if (return_flag.type is None or ( + 'undefined' not in return_flag.type and + 'void' not in return_flag.type and + '*' not in return_flag.type)): + self._HandleError( + errors.UNNECESSARY_RETURN_DOCUMENTATION, + 'Found @return JsDoc on function that returns nothing', + return_flag.flag_token, position=Position.AtBeginning()) + + # b/4073735. Method in object literal definition of prototype can + # safely reference 'this'. + prototype_object_literal = False + block_start = None + previous_code = None + previous_previous_code = None + + # Search for cases where prototype is defined as object literal. + # previous_previous_code + # | previous_code + # | | block_start + # | | | + # a.b.prototype = { + # c : function() { + # this.d = 1; + # } + # } + + # If in object literal, find first token of block so to find previous + # tokens to check above condition. + if state.InObjectLiteral(): + block_start = state.GetCurrentBlockStart() + + # If an object literal then get previous token (code type). For above + # case it should be '='. + if block_start: + previous_code = tokenutil.SearchExcept(block_start, + Type.NON_CODE_TYPES, + reverse=True) + + # If previous token to block is '=' then get its previous token. + if previous_code and previous_code.IsOperator('='): + previous_previous_code = tokenutil.SearchExcept(previous_code, + Type.NON_CODE_TYPES, + reverse=True) + + # If variable/token before '=' ends with '.prototype' then its above + # case of prototype defined with object literal. + prototype_object_literal = (previous_previous_code and + previous_previous_code.string.endswith( + '.prototype')) + + if (function.has_this and function.doc and + not function.doc.HasFlag('this') and + not function.is_constructor and + not function.is_interface and + '.prototype.' not in function.name and + not prototype_object_literal): + self._HandleError( + errors.MISSING_JSDOC_TAG_THIS, + 'Missing @this JsDoc in function referencing "this". (' + 'this usually means you are trying to reference "this" in ' + 'a static function, or you have forgotten to mark a ' + 'constructor with @constructor)', + function.doc.end_token, position=Position.AtBeginning()) + + elif token.type == Type.IDENTIFIER: + if token.string == 'goog.inherits' and not state.InFunction(): + if state.GetLastNonSpaceToken().line_number == token.line_number: + self._HandleError( + errors.MISSING_LINE, + 'Missing newline between constructor and goog.inherits', + token, + position=Position.AtBeginning()) + + extra_space = state.GetLastNonSpaceToken().next + while extra_space != token: + if extra_space.type == Type.BLANK_LINE: + self._HandleError( + errors.EXTRA_LINE, + 'Extra line between constructor and goog.inherits', + extra_space) + extra_space = extra_space.next + + # TODO(robbyw): Test the last function was a constructor. + # TODO(robbyw): Test correct @extends and @implements documentation. + + elif (token.string == 'goog.provide' and + not state.InFunction() and + namespaces_info is not None): + namespace = tokenutil.GetStringAfterToken(token) + + # Report extra goog.provide statement. + if not namespace or namespaces_info.IsExtraProvide(token): + if not namespace: + msg = 'Empty namespace in goog.provide' + else: + msg = 'Unnecessary goog.provide: ' + namespace + + # Hint to user if this is a Test namespace. + if namespace.endswith('Test'): + msg += (' *Test namespaces must be mentioned in the ' + 'goog.setTestOnly() call') + + self._HandleError( + errors.EXTRA_GOOG_PROVIDE, + msg, + token, position=Position.AtBeginning()) + + if namespaces_info.IsLastProvide(token): + # Report missing provide statements after the last existing provide. + missing_provides = namespaces_info.GetMissingProvides() + if missing_provides: + self._ReportMissingProvides( + missing_provides, + tokenutil.GetLastTokenInSameLine(token).next, + False) + + # If there are no require statements, missing requires should be + # reported after the last provide. + if not namespaces_info.GetRequiredNamespaces(): + missing_requires = namespaces_info.GetMissingRequires() + if missing_requires: + self._ReportMissingRequires( + missing_requires, + tokenutil.GetLastTokenInSameLine(token).next, + True) + + elif (token.string == 'goog.require' and + not state.InFunction() and + namespaces_info is not None): + namespace = tokenutil.GetStringAfterToken(token) + + # If there are no provide statements, missing provides should be + # reported before the first require. + if (namespaces_info.IsFirstRequire(token) and + not namespaces_info.GetProvidedNamespaces()): + missing_provides = namespaces_info.GetMissingProvides() + if missing_provides: + self._ReportMissingProvides( + missing_provides, + tokenutil.GetFirstTokenInSameLine(token), + True) + + # Report extra goog.require statement. + if not namespace or namespaces_info.IsExtraRequire(token): + if not namespace: + msg = 'Empty namespace in goog.require' + else: + msg = 'Unnecessary goog.require: ' + namespace + + self._HandleError( + errors.EXTRA_GOOG_REQUIRE, + msg, + token, position=Position.AtBeginning()) + + # Report missing goog.require statements. + if namespaces_info.IsLastRequire(token): + missing_requires = namespaces_info.GetMissingRequires() + if missing_requires: + self._ReportMissingRequires( + missing_requires, + tokenutil.GetLastTokenInSameLine(token).next, + False) + + elif token.type == Type.OPERATOR: + last_in_line = token.IsLastInLine() + # If the token is unary and appears to be used in a unary context + # it's ok. Otherwise, if it's at the end of the line or immediately + # before a comment, it's ok. + # Don't report an error before a start bracket - it will be reported + # by that token's space checks. + if (not token.metadata.IsUnaryOperator() and not last_in_line + and not token.next.IsComment() + and not token.next.IsOperator(',') + and token.next.type not in (Type.WHITESPACE, Type.END_PAREN, + Type.END_BRACKET, Type.SEMICOLON, + Type.START_BRACKET)): + self._HandleError( + errors.MISSING_SPACE, + 'Missing space after "%s"' % token.string, + token, + position=Position.AtEnd(token.string)) + elif token.type == Type.WHITESPACE: + first_in_line = token.IsFirstInLine() + last_in_line = token.IsLastInLine() + # Check whitespace length if it's not the first token of the line and + # if it's not immediately before a comment. + if not last_in_line and not first_in_line and not token.next.IsComment(): + # Ensure there is no space after opening parentheses. + if (token.previous.type in (Type.START_PAREN, Type.START_BRACKET, + Type.FUNCTION_NAME) + or token.next.type == Type.START_PARAMETERS): + self._HandleError( + errors.EXTRA_SPACE, + 'Extra space after "%s"' % token.previous.string, + token, + position=Position.All(token.string)) + elif token.type == Type.SEMICOLON: + previous_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES, + reverse=True) + if not previous_token: + self._HandleError( + errors.REDUNDANT_SEMICOLON, + 'Semicolon without any statement', + token, + position=Position.AtEnd(token.string)) + elif (previous_token.type == Type.KEYWORD and + previous_token.string not in ['break', 'continue', 'return']): + self._HandleError( + errors.REDUNDANT_SEMICOLON, + ('Semicolon after \'%s\' without any statement.' + ' Looks like an error.' % previous_token.string), + token, + position=Position.AtEnd(token.string)) + + def _CheckUnusedLocalVariables(self, token, state): + """Checks for unused local variables in function blocks. + + Args: + token: The token to check. + state: The state tracker. + """ + # We don't use state.InFunction because that disregards scope functions. + in_function = state.FunctionDepth() > 0 + if token.type == Type.SIMPLE_LVALUE or token.type == Type.IDENTIFIER: + if in_function: + identifier = token.string + # Check whether the previous token was var. + previous_code_token = tokenutil.CustomSearch( + token, + lambda t: t.type not in Type.NON_CODE_TYPES, + reverse=True) + if previous_code_token and previous_code_token.IsKeyword('var'): + # Add local variable declaration to the top of the unused locals + # stack. + self._unused_local_variables_by_scope[-1][identifier] = token + elif token.type == Type.IDENTIFIER: + # This covers most cases where the variable is used as an identifier. + self._MarkLocalVariableUsed(token) + elif token.type == Type.SIMPLE_LVALUE and '.' in identifier: + # This covers cases where a value is assigned to a property of the + # variable. + self._MarkLocalVariableUsed(token) + elif token.type == Type.START_BLOCK: + if in_function and state.IsFunctionOpen(): + # Push a new map onto the stack + self._unused_local_variables_by_scope.append({}) + elif token.type == Type.END_BLOCK: + if state.IsFunctionClose(): + # Pop the stack and report any remaining locals as unused. + unused_local_variables = self._unused_local_variables_by_scope.pop() + for unused_token in unused_local_variables.values(): + self._HandleError( + errors.UNUSED_LOCAL_VARIABLE, + 'Unused local variable: %s.' % unused_token.string, + unused_token) + + def _MarkLocalVariableUsed(self, token): + """Marks the local variable as used in the relevant scope. + + Marks the local variable as used in the scope nearest to the current + scope that matches the given token. + + Args: + token: The token representing the potential usage of a local variable. + """ + + identifier = token.string.split('.')[0] + # Find the first instance of the identifier in the stack of function scopes + # and mark it used. + for unused_local_variables in reversed( + self._unused_local_variables_by_scope): + if identifier in unused_local_variables: + del unused_local_variables[identifier] + break + + def _ReportMissingProvides(self, missing_provides, token, need_blank_line): + """Reports missing provide statements to the error handler. + + Args: + missing_provides: A dictionary of string(key) and integer(value) where + each string(key) is a namespace that should be provided, but is not + and integer(value) is first line number where it's required. + token: The token where the error was detected (also where the new provides + will be inserted. + need_blank_line: Whether a blank line needs to be inserted after the new + provides are inserted. May be True, False, or None, where None + indicates that the insert location is unknown. + """ + + missing_provides_msg = 'Missing the following goog.provide statements:\n' + missing_provides_msg += '\n'.join(['goog.provide(\'%s\');' % x for x in + sorted(missing_provides)]) + missing_provides_msg += '\n' + + missing_provides_msg += '\nFirst line where provided: \n' + missing_provides_msg += '\n'.join( + [' %s : line %d' % (x, missing_provides[x]) for x in + sorted(missing_provides)]) + missing_provides_msg += '\n' + + self._HandleError( + errors.MISSING_GOOG_PROVIDE, + missing_provides_msg, + token, position=Position.AtBeginning(), + fix_data=(missing_provides.keys(), need_blank_line)) + + def _ReportMissingRequires(self, missing_requires, token, need_blank_line): + """Reports missing require statements to the error handler. + + Args: + missing_requires: A dictionary of string(key) and integer(value) where + each string(key) is a namespace that should be required, but is not + and integer(value) is first line number where it's required. + token: The token where the error was detected (also where the new requires + will be inserted. + need_blank_line: Whether a blank line needs to be inserted before the new + requires are inserted. May be True, False, or None, where None + indicates that the insert location is unknown. + """ + + missing_requires_msg = 'Missing the following goog.require statements:\n' + missing_requires_msg += '\n'.join(['goog.require(\'%s\');' % x for x in + sorted(missing_requires)]) + missing_requires_msg += '\n' + + missing_requires_msg += '\nFirst line where required: \n' + missing_requires_msg += '\n'.join( + [' %s : line %d' % (x, missing_requires[x]) for x in + sorted(missing_requires)]) + missing_requires_msg += '\n' + + self._HandleError( + errors.MISSING_GOOG_REQUIRE, + missing_requires_msg, + token, position=Position.AtBeginning(), + fix_data=(missing_requires.keys(), need_blank_line)) + + def Finalize(self, state): + """Perform all checks that need to occur after all lines are processed.""" + # Call the base class's Finalize function. + super(JavaScriptLintRules, self).Finalize(state) + + if error_check.ShouldCheck(Rule.UNUSED_PRIVATE_MEMBERS): + # Report an error for any declared private member that was never used. + unused_private_members = (self._declared_private_members - + self._used_private_members) + + for variable in unused_private_members: + token = self._declared_private_member_tokens[variable] + self._HandleError(errors.UNUSED_PRIVATE_MEMBER, + 'Unused private member: %s.' % token.string, + token) + + # Clear state to prepare for the next file. + self._declared_private_member_tokens = {} + self._declared_private_members = set() + self._used_private_members = set() + + namespaces_info = self._namespaces_info + if namespaces_info is not None: + # If there are no provide or require statements, missing provides and + # requires should be reported on line 1. + if (not namespaces_info.GetProvidedNamespaces() and + not namespaces_info.GetRequiredNamespaces()): + missing_provides = namespaces_info.GetMissingProvides() + if missing_provides: + self._ReportMissingProvides( + missing_provides, state.GetFirstToken(), None) + + missing_requires = namespaces_info.GetMissingRequires() + if missing_requires: + self._ReportMissingRequires( + missing_requires, state.GetFirstToken(), None) + + self._CheckSortedRequiresProvides(state.GetFirstToken()) + + def _CheckSortedRequiresProvides(self, token): + """Checks that all goog.require and goog.provide statements are sorted. + + Note that this method needs to be run after missing statements are added to + preserve alphabetical order. + + Args: + token: The first token in the token stream. + """ + sorter = requireprovidesorter.RequireProvideSorter() + first_provide_token = sorter.CheckProvides(token) + if first_provide_token: + new_order = sorter.GetFixedProvideString(first_provide_token) + self._HandleError( + errors.GOOG_PROVIDES_NOT_ALPHABETIZED, + 'goog.provide classes must be alphabetized. The correct code is:\n' + + new_order, + first_provide_token, + position=Position.AtBeginning(), + fix_data=first_provide_token) + + first_require_token = sorter.CheckRequires(token) + if first_require_token: + new_order = sorter.GetFixedRequireString(first_require_token) + self._HandleError( + errors.GOOG_REQUIRES_NOT_ALPHABETIZED, + 'goog.require classes must be alphabetized. The correct code is:\n' + + new_order, + first_require_token, + position=Position.AtBeginning(), + fix_data=first_require_token) + + def GetLongLineExceptions(self): + """Gets a list of regexps for lines which can be longer than the limit. + + Returns: + A list of regexps, used as matches (rather than searches). + """ + return [ + re.compile(r'goog\.require\(.+\);?\s*$'), + re.compile(r'goog\.provide\(.+\);?\s*$'), + re.compile(r'[\s/*]*@visibility\s*{.*}[\s*/]*$'), + ] diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascriptstatetracker.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascriptstatetracker.py new file mode 100755 index 0000000000..1b051d3bf6 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascriptstatetracker.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +# Copyright 2008 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Parser for JavaScript files.""" + + + +from closure_linter import javascripttokens +from closure_linter import statetracker +from closure_linter import tokenutil + +# Shorthand +Type = javascripttokens.JavaScriptTokenType + + +class JsDocFlag(statetracker.DocFlag): + """Javascript doc flag object. + + Attribute: + flag_type: param, return, define, type, etc. + flag_token: The flag token. + type_start_token: The first token specifying the flag JS type, + including braces. + type_end_token: The last token specifying the flag JS type, + including braces. + type: The JavaScript type spec. + name_token: The token specifying the flag name. + name: The flag name + description_start_token: The first token in the description. + description_end_token: The end token in the description. + description: The description. + """ + + # Please keep these lists alphabetized. + + # Some projects use the following extensions to JsDoc. + # TODO(robbyw): determine which of these, if any, should be illegal. + EXTENDED_DOC = frozenset([ + 'class', 'code', 'desc', 'final', 'hidden', 'inheritDoc', 'link', + 'meaning', 'provideGoog', 'throws']) + + LEGAL_DOC = EXTENDED_DOC | statetracker.DocFlag.LEGAL_DOC + + def __init__(self, flag_token): + """Creates the JsDocFlag object and attaches it to the given start token. + + Args: + flag_token: The starting token of the flag. + """ + statetracker.DocFlag.__init__(self, flag_token) + + +class JavaScriptStateTracker(statetracker.StateTracker): + """JavaScript state tracker. + + Inherits from the core EcmaScript StateTracker adding extra state tracking + functionality needed for JavaScript. + """ + + def __init__(self): + """Initializes a JavaScript token stream state tracker.""" + statetracker.StateTracker.__init__(self, JsDocFlag) + + def Reset(self): + self._scope_depth = 0 + self._block_stack = [] + super(JavaScriptStateTracker, self).Reset() + + def InTopLevel(self): + """Compute whether we are at the top level in the class. + + This function call is language specific. In some languages like + JavaScript, a function is top level if it is not inside any parenthesis. + In languages such as ActionScript, a function is top level if it is directly + within a class. + + Returns: + Whether we are at the top level in the class. + """ + return self._scope_depth == self.ParenthesesDepth() + + def InFunction(self): + """Returns true if the current token is within a function. + + This js-specific override ignores goog.scope functions. + + Returns: + True if the current token is within a function. + """ + return self._scope_depth != self.FunctionDepth() + + def InNonScopeBlock(self): + """Compute whether we are nested within a non-goog.scope block. + + Returns: + True if the token is not enclosed in a block that does not originate from + a goog.scope statement. False otherwise. + """ + return self._scope_depth != self.BlockDepth() + + def GetBlockType(self, token): + """Determine the block type given a START_BLOCK token. + + Code blocks come after parameters, keywords like else, and closing parens. + + Args: + token: The current token. Can be assumed to be type START_BLOCK + Returns: + Code block type for current token. + """ + last_code = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES, reverse=True) + if last_code.type in (Type.END_PARAMETERS, Type.END_PAREN, + Type.KEYWORD) and not last_code.IsKeyword('return'): + return self.CODE + else: + return self.OBJECT_LITERAL + + def GetCurrentBlockStart(self): + """Gets the start token of current block. + + Returns: + Starting token of current block. None if not in block. + """ + if self._block_stack: + return self._block_stack[-1] + else: + return None + + def HandleToken(self, token, last_non_space_token): + """Handles the given token and updates state. + + Args: + token: The token to handle. + last_non_space_token: The last non space token encountered + """ + if token.type == Type.START_BLOCK: + self._block_stack.append(token) + if token.type == Type.IDENTIFIER and token.string == 'goog.scope': + self._scope_depth += 1 + if token.type == Type.END_BLOCK: + start_token = self._block_stack.pop() + if tokenutil.GoogScopeOrNoneFromStartBlock(start_token): + self._scope_depth -= 1 + super(JavaScriptStateTracker, self).HandleToken(token, + last_non_space_token) diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascriptstatetracker_test.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascriptstatetracker_test.py new file mode 100644 index 0000000000..76dabd2c70 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascriptstatetracker_test.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python +# +# Copyright 2012 The Closure Linter Authors. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for the javascriptstatetracker module.""" + +# Allow non-Google copyright +# pylint: disable=g-bad-file-header + +__author__ = ('nnaze@google.com (Nathan Naze)') + + +import unittest as googletest + +from closure_linter import javascripttokens +from closure_linter import testutil +from closure_linter import tokenutil + + +_FUNCTION_SCRIPT = """\ +var a = 3; + +function foo(aaa, bbb, ccc) { + var b = 4; +} + + +/** + * JSDoc comment. + */ +var bar = function(ddd, eee, fff) { + +}; + + +/** + * Verify that nested functions get their proper parameters recorded. + */ +var baz = function(ggg, hhh, iii) { + var qux = function(jjj, kkk, lll) { + }; + // make sure that entering a new block does not change baz' parameters. + {}; +}; + +""" + + +class FunctionTest(googletest.TestCase): + + def testFunctionParse(self): + functions, _ = testutil.ParseFunctionsAndComments(_FUNCTION_SCRIPT) + self.assertEquals(4, len(functions)) + + # First function + function = functions[0] + self.assertEquals(['aaa', 'bbb', 'ccc'], function.parameters) + + start_token = function.start_token + end_token = function.end_token + + self.assertEquals( + javascripttokens.JavaScriptTokenType.FUNCTION_DECLARATION, + function.start_token.type) + + self.assertEquals('function', start_token.string) + self.assertEquals(3, start_token.line_number) + self.assertEquals(0, start_token.start_index) + + self.assertEquals('}', end_token.string) + self.assertEquals(5, end_token.line_number) + self.assertEquals(0, end_token.start_index) + + self.assertEquals('foo', function.name) + + self.assertIsNone(function.doc) + + # Second function + function = functions[1] + self.assertEquals(['ddd', 'eee', 'fff'], function.parameters) + + start_token = function.start_token + end_token = function.end_token + + self.assertEquals( + javascripttokens.JavaScriptTokenType.FUNCTION_DECLARATION, + function.start_token.type) + + self.assertEquals('function', start_token.string) + self.assertEquals(11, start_token.line_number) + self.assertEquals(10, start_token.start_index) + + self.assertEquals('}', end_token.string) + self.assertEquals(13, end_token.line_number) + self.assertEquals(0, end_token.start_index) + + self.assertEquals('bar', function.name) + + self.assertIsNotNone(function.doc) + + # Check function JSDoc + doc = function.doc + doc_tokens = tokenutil.GetTokenRange(doc.start_token, doc.end_token) + + comment_type = javascripttokens.JavaScriptTokenType.COMMENT + comment_tokens = filter(lambda t: t.type is comment_type, doc_tokens) + + self.assertEquals('JSDoc comment.', + tokenutil.TokensToString(comment_tokens).strip()) + + # Third function + function = functions[2] + self.assertEquals(['ggg', 'hhh', 'iii'], function.parameters) + + start_token = function.start_token + end_token = function.end_token + + self.assertEquals( + javascripttokens.JavaScriptTokenType.FUNCTION_DECLARATION, + function.start_token.type) + + self.assertEquals('function', start_token.string) + self.assertEquals(19, start_token.line_number) + self.assertEquals(10, start_token.start_index) + + self.assertEquals('}', end_token.string) + self.assertEquals(24, end_token.line_number) + self.assertEquals(0, end_token.start_index) + + self.assertEquals('baz', function.name) + self.assertIsNotNone(function.doc) + + # Fourth function (inside third function) + function = functions[3] + self.assertEquals(['jjj', 'kkk', 'lll'], function.parameters) + + start_token = function.start_token + end_token = function.end_token + + self.assertEquals( + javascripttokens.JavaScriptTokenType.FUNCTION_DECLARATION, + function.start_token.type) + + self.assertEquals('function', start_token.string) + self.assertEquals(20, start_token.line_number) + self.assertEquals(12, start_token.start_index) + + self.assertEquals('}', end_token.string) + self.assertEquals(21, end_token.line_number) + self.assertEquals(2, end_token.start_index) + + self.assertEquals('qux', function.name) + self.assertIsNone(function.doc) + + + +class CommentTest(googletest.TestCase): + + def testGetDescription(self): + comment = self._ParseComment(""" + /** + * Comment targeting goog.foo. + * + * This is the second line. + * @param {number} foo The count of foo. + */ + target;""") + + self.assertEqual( + 'Comment targeting goog.foo.\n\nThis is the second line.', + comment.description) + + def testCommentGetTarget(self): + self.assertCommentTarget('goog.foo', """ + /** + * Comment targeting goog.foo. + */ + goog.foo = 6; + """) + + self.assertCommentTarget('bar', """ + /** + * Comment targeting bar. + */ + var bar = "Karate!"; + """) + + self.assertCommentTarget('doThing', """ + /** + * Comment targeting doThing. + */ + function doThing() {}; + """) + + self.assertCommentTarget('this.targetProperty', """ + goog.bar.Baz = function() { + /** + * Comment targeting targetProperty. + */ + this.targetProperty = 3; + }; + """) + + self.assertCommentTarget('goog.bar.prop', """ + /** + * Comment targeting goog.bar.prop. + */ + goog.bar.prop; + """) + + self.assertCommentTarget('goog.aaa.bbb', """ + /** + * Comment targeting goog.aaa.bbb. + */ + (goog.aaa.bbb) + """) + + self.assertCommentTarget('theTarget', """ + /** + * Comment targeting symbol preceded by newlines, whitespace, + * and parens -- things we ignore. + */ + (theTarget) + """) + + self.assertCommentTarget(None, """ + /** + * @fileoverview File overview. + */ + (notATarget) + """) + + self.assertCommentTarget(None, """ + /** + * Comment that doesn't find a target. + */ + """) + + self.assertCommentTarget('theTarget.is.split.across.lines', """ + /** + * Comment that addresses a symbol split across lines. + */ + (theTarget.is.split + .across.lines) + """) + + self.assertCommentTarget('theTarget.is.split.across.lines', """ + /** + * Comment that addresses a symbol split across lines. + */ + (theTarget.is.split. + across.lines) + """) + + def _ParseComment(self, script): + """Parse a script that contains one comment and return it.""" + _, comments = testutil.ParseFunctionsAndComments(script) + self.assertEquals(1, len(comments)) + return comments[0] + + def assertCommentTarget(self, target, script): + comment = self._ParseComment(script) + self.assertEquals(target, comment.GetTargetIdentifier()) + + +if __name__ == '__main__': + googletest.main() diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascripttokenizer.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascripttokenizer.py new file mode 100755 index 0000000000..a0e9dca8a4 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascripttokenizer.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python +# +# Copyright 2007 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Regular expression based JavaScript parsing classes.""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)') + +import copy +import re + +from closure_linter import javascripttokens +from closure_linter.common import matcher +from closure_linter.common import tokenizer + +# Shorthand +Type = javascripttokens.JavaScriptTokenType +Matcher = matcher.Matcher + + +class JavaScriptModes(object): + """Enumeration of the different matcher modes used for JavaScript.""" + TEXT_MODE = 'text' + SINGLE_QUOTE_STRING_MODE = 'single_quote_string' + DOUBLE_QUOTE_STRING_MODE = 'double_quote_string' + BLOCK_COMMENT_MODE = 'block_comment' + DOC_COMMENT_MODE = 'doc_comment' + DOC_COMMENT_LEX_SPACES_MODE = 'doc_comment_spaces' + LINE_COMMENT_MODE = 'line_comment' + PARAMETER_MODE = 'parameter' + FUNCTION_MODE = 'function' + + +class JavaScriptTokenizer(tokenizer.Tokenizer): + """JavaScript tokenizer. + + Convert JavaScript code in to an array of tokens. + """ + + # Useful patterns for JavaScript parsing. + IDENTIFIER_CHAR = r'A-Za-z0-9_$.' + + # Number patterns based on: + # http://www.mozilla.org/js/language/js20-2000-07/formal/lexer-grammar.html + MANTISSA = r""" + (\d+(?!\.)) | # Matches '10' + (\d+\.(?!\d)) | # Matches '10.' + (\d*\.\d+) # Matches '.5' or '10.5' + """ + DECIMAL_LITERAL = r'(%s)([eE][-+]?\d+)?' % MANTISSA + HEX_LITERAL = r'0[xX][0-9a-fA-F]+' + NUMBER = re.compile(r""" + ((%s)|(%s)) + """ % (HEX_LITERAL, DECIMAL_LITERAL), re.VERBOSE) + + # Strings come in three parts - first we match the start of the string, then + # the contents, then the end. The contents consist of any character except a + # backslash or end of string, or a backslash followed by any character, or a + # backslash followed by end of line to support correct parsing of multi-line + # strings. + SINGLE_QUOTE = re.compile(r"'") + SINGLE_QUOTE_TEXT = re.compile(r"([^'\\]|\\(.|$))+") + DOUBLE_QUOTE = re.compile(r'"') + DOUBLE_QUOTE_TEXT = re.compile(r'([^"\\]|\\(.|$))+') + + START_SINGLE_LINE_COMMENT = re.compile(r'//') + END_OF_LINE_SINGLE_LINE_COMMENT = re.compile(r'//$') + + START_DOC_COMMENT = re.compile(r'/\*\*') + START_BLOCK_COMMENT = re.compile(r'/\*') + END_BLOCK_COMMENT = re.compile(r'\*/') + BLOCK_COMMENT_TEXT = re.compile(r'([^*]|\*(?!/))+') + + # Comment text is anything that we are not going to parse into another special + # token like (inline) flags or end comments. Complicated regex to match + # most normal characters, and '*', '{', '}', and '@' when we are sure that + # it is safe. Expression [^*{\s]@ must come first, or the other options will + # match everything before @, and we won't match @'s that aren't part of flags + # like in email addresses in the @author tag. + DOC_COMMENT_TEXT = re.compile(r'([^*{}\s]@|[^*{}@]|\*(?!/))+') + DOC_COMMENT_NO_SPACES_TEXT = re.compile(r'([^*{}\s]@|[^*{}@\s]|\*(?!/))+') + + # Match the prefix ' * ' that starts every line of jsdoc. Want to include + # spaces after the '*', but nothing else that occurs after a '*', and don't + # want to match the '*' in '*/'. + DOC_PREFIX = re.compile(r'\s*\*(\s+|(?!/))') + + START_BLOCK = re.compile('{') + END_BLOCK = re.compile('}') + + REGEX_CHARACTER_CLASS = r""" + \[ # Opening bracket + ([^\]\\]|\\.)* # Anything but a ] or \, + # or a backslash followed by anything + \] # Closing bracket + """ + # We ensure the regex is followed by one of the above tokens to avoid + # incorrectly parsing something like x / y / z as x REGEX(/ y /) z + POST_REGEX_LIST = [ + ';', ',', r'\.', r'\)', r'\]', '$', r'\/\/', r'\/\*', ':', '}'] + + REGEX = re.compile(r""" + / # opening slash + (?!\*) # not the start of a comment + (\\.|[^\[\/\\]|(%s))* # a backslash followed by anything, + # or anything but a / or [ or \, + # or a character class + / # closing slash + [gimsx]* # optional modifiers + (?=\s*(%s)) + """ % (REGEX_CHARACTER_CLASS, '|'.join(POST_REGEX_LIST)), + re.VERBOSE) + + ANYTHING = re.compile(r'.*') + PARAMETERS = re.compile(r'[^\)]+') + CLOSING_PAREN_WITH_SPACE = re.compile(r'\)\s*') + + FUNCTION_DECLARATION = re.compile(r'\bfunction\b') + + OPENING_PAREN = re.compile(r'\(') + CLOSING_PAREN = re.compile(r'\)') + + OPENING_BRACKET = re.compile(r'\[') + CLOSING_BRACKET = re.compile(r'\]') + + # We omit these JS keywords from the list: + # function - covered by FUNCTION_DECLARATION. + # delete, in, instanceof, new, typeof - included as operators. + # this - included in identifiers. + # null, undefined - not included, should go in some "special constant" list. + KEYWORD_LIST = ['break', 'case', 'catch', 'continue', 'default', 'do', 'else', + 'finally', 'for', 'if', 'return', 'switch', 'throw', 'try', 'var', + 'while', 'with'] + # Match a keyword string followed by a non-identifier character in order to + # not match something like doSomething as do + Something. + KEYWORD = re.compile('(%s)((?=[^%s])|$)' % ( + '|'.join(KEYWORD_LIST), IDENTIFIER_CHAR)) + + # List of regular expressions to match as operators. Some notes: for our + # purposes, the comma behaves similarly enough to a normal operator that we + # include it here. r'\bin\b' actually matches 'in' surrounded by boundary + # characters - this may not match some very esoteric uses of the in operator. + # Operators that are subsets of larger operators must come later in this list + # for proper matching, e.g., '>>' must come AFTER '>>>'. + OPERATOR_LIST = [',', r'\+\+', '===', '!==', '>>>=', '>>>', '==', '>=', '<=', + '!=', '<<=', '>>=', '<<', '>>', '>', '<', r'\+=', r'\+', + '--', '\^=', '-=', '-', '/=', '/', r'\*=', r'\*', '%=', '%', + '&&', r'\|\|', '&=', '&', r'\|=', r'\|', '=', '!', ':', '\?', + r'\^', r'\bdelete\b', r'\bin\b', r'\binstanceof\b', + r'\bnew\b', r'\btypeof\b', r'\bvoid\b'] + OPERATOR = re.compile('|'.join(OPERATOR_LIST)) + + WHITESPACE = re.compile(r'\s+') + SEMICOLON = re.compile(r';') + # Technically JavaScript identifiers can't contain '.', but we treat a set of + # nested identifiers as a single identifier. + NESTED_IDENTIFIER = r'[a-zA-Z_$][%s.]*' % IDENTIFIER_CHAR + IDENTIFIER = re.compile(NESTED_IDENTIFIER) + + SIMPLE_LVALUE = re.compile(r""" + (?P%s) # a valid identifier + (?=\s* # optional whitespace + \= # look ahead to equal sign + (?!=)) # not follwed by equal + """ % NESTED_IDENTIFIER, re.VERBOSE) + + # A doc flag is a @ sign followed by non-space characters that appears at the + # beginning of the line, after whitespace, or after a '{'. The look-behind + # check is necessary to not match someone@google.com as a flag. + DOC_FLAG = re.compile(r'(^|(?<=\s))@(?P[a-zA-Z]+)') + # To properly parse parameter names, we need to tokenize whitespace into a + # token. + DOC_FLAG_LEX_SPACES = re.compile(r'(^|(?<=\s))@(?P%s)\b' % + '|'.join(['param'])) + + DOC_INLINE_FLAG = re.compile(r'(?<={)@(?P[a-zA-Z]+)') + + # Star followed by non-slash, i.e a star that does not end a comment. + # This is used for TYPE_GROUP below. + SAFE_STAR = r'(\*(?!/))' + + COMMON_DOC_MATCHERS = [ + # Find the end of the comment. + Matcher(END_BLOCK_COMMENT, Type.END_DOC_COMMENT, + JavaScriptModes.TEXT_MODE), + + # Tokenize documented flags like @private. + Matcher(DOC_INLINE_FLAG, Type.DOC_INLINE_FLAG), + Matcher(DOC_FLAG_LEX_SPACES, Type.DOC_FLAG, + JavaScriptModes.DOC_COMMENT_LEX_SPACES_MODE), + + # Encountering a doc flag should leave lex spaces mode. + Matcher(DOC_FLAG, Type.DOC_FLAG, JavaScriptModes.DOC_COMMENT_MODE), + + # Tokenize braces so we can find types. + Matcher(START_BLOCK, Type.DOC_START_BRACE), + Matcher(END_BLOCK, Type.DOC_END_BRACE), + Matcher(DOC_PREFIX, Type.DOC_PREFIX, None, True)] + + + # The token matcher groups work as follows: it is an list of Matcher objects. + # The matchers will be tried in this order, and the first to match will be + # returned. Hence the order is important because the matchers that come first + # overrule the matchers that come later. + JAVASCRIPT_MATCHERS = { + # Matchers for basic text mode. + JavaScriptModes.TEXT_MODE: [ + # Check a big group - strings, starting comments, and regexes - all + # of which could be intertwined. 'string with /regex/', + # /regex with 'string'/, /* comment with /regex/ and string */ (and so + # on) + Matcher(START_DOC_COMMENT, Type.START_DOC_COMMENT, + JavaScriptModes.DOC_COMMENT_MODE), + Matcher(START_BLOCK_COMMENT, Type.START_BLOCK_COMMENT, + JavaScriptModes.BLOCK_COMMENT_MODE), + Matcher(END_OF_LINE_SINGLE_LINE_COMMENT, + Type.START_SINGLE_LINE_COMMENT), + Matcher(START_SINGLE_LINE_COMMENT, Type.START_SINGLE_LINE_COMMENT, + JavaScriptModes.LINE_COMMENT_MODE), + Matcher(SINGLE_QUOTE, Type.SINGLE_QUOTE_STRING_START, + JavaScriptModes.SINGLE_QUOTE_STRING_MODE), + Matcher(DOUBLE_QUOTE, Type.DOUBLE_QUOTE_STRING_START, + JavaScriptModes.DOUBLE_QUOTE_STRING_MODE), + Matcher(REGEX, Type.REGEX), + + # Next we check for start blocks appearing outside any of the items + # above. + Matcher(START_BLOCK, Type.START_BLOCK), + Matcher(END_BLOCK, Type.END_BLOCK), + + # Then we search for function declarations. + Matcher(FUNCTION_DECLARATION, Type.FUNCTION_DECLARATION, + JavaScriptModes.FUNCTION_MODE), + + # Next, we convert non-function related parens to tokens. + Matcher(OPENING_PAREN, Type.START_PAREN), + Matcher(CLOSING_PAREN, Type.END_PAREN), + + # Next, we convert brackets to tokens. + Matcher(OPENING_BRACKET, Type.START_BRACKET), + Matcher(CLOSING_BRACKET, Type.END_BRACKET), + + # Find numbers. This has to happen before operators because scientific + # notation numbers can have + and - in them. + Matcher(NUMBER, Type.NUMBER), + + # Find operators and simple assignments + Matcher(SIMPLE_LVALUE, Type.SIMPLE_LVALUE), + Matcher(OPERATOR, Type.OPERATOR), + + # Find key words and whitespace. + Matcher(KEYWORD, Type.KEYWORD), + Matcher(WHITESPACE, Type.WHITESPACE), + + # Find identifiers. + Matcher(IDENTIFIER, Type.IDENTIFIER), + + # Finally, we convert semicolons to tokens. + Matcher(SEMICOLON, Type.SEMICOLON)], + + # Matchers for single quote strings. + JavaScriptModes.SINGLE_QUOTE_STRING_MODE: [ + Matcher(SINGLE_QUOTE_TEXT, Type.STRING_TEXT), + Matcher(SINGLE_QUOTE, Type.SINGLE_QUOTE_STRING_END, + JavaScriptModes.TEXT_MODE)], + + # Matchers for double quote strings. + JavaScriptModes.DOUBLE_QUOTE_STRING_MODE: [ + Matcher(DOUBLE_QUOTE_TEXT, Type.STRING_TEXT), + Matcher(DOUBLE_QUOTE, Type.DOUBLE_QUOTE_STRING_END, + JavaScriptModes.TEXT_MODE)], + + # Matchers for block comments. + JavaScriptModes.BLOCK_COMMENT_MODE: [ + # First we check for exiting a block comment. + Matcher(END_BLOCK_COMMENT, Type.END_BLOCK_COMMENT, + JavaScriptModes.TEXT_MODE), + + # Match non-comment-ending text.. + Matcher(BLOCK_COMMENT_TEXT, Type.COMMENT)], + + # Matchers for doc comments. + JavaScriptModes.DOC_COMMENT_MODE: COMMON_DOC_MATCHERS + [ + Matcher(DOC_COMMENT_TEXT, Type.COMMENT)], + + JavaScriptModes.DOC_COMMENT_LEX_SPACES_MODE: COMMON_DOC_MATCHERS + [ + Matcher(WHITESPACE, Type.COMMENT), + Matcher(DOC_COMMENT_NO_SPACES_TEXT, Type.COMMENT)], + + # Matchers for single line comments. + JavaScriptModes.LINE_COMMENT_MODE: [ + # We greedy match until the end of the line in line comment mode. + Matcher(ANYTHING, Type.COMMENT, JavaScriptModes.TEXT_MODE)], + + # Matchers for code after the function keyword. + JavaScriptModes.FUNCTION_MODE: [ + # Must match open paren before anything else and move into parameter + # mode, otherwise everything inside the parameter list is parsed + # incorrectly. + Matcher(OPENING_PAREN, Type.START_PARAMETERS, + JavaScriptModes.PARAMETER_MODE), + Matcher(WHITESPACE, Type.WHITESPACE), + Matcher(IDENTIFIER, Type.FUNCTION_NAME)], + + # Matchers for function parameters + JavaScriptModes.PARAMETER_MODE: [ + # When in function parameter mode, a closing paren is treated specially. + # Everything else is treated as lines of parameters. + Matcher(CLOSING_PAREN_WITH_SPACE, Type.END_PARAMETERS, + JavaScriptModes.TEXT_MODE), + Matcher(PARAMETERS, Type.PARAMETERS, JavaScriptModes.PARAMETER_MODE)]} + + # When text is not matched, it is given this default type based on mode. + # If unspecified in this map, the default default is Type.NORMAL. + JAVASCRIPT_DEFAULT_TYPES = { + JavaScriptModes.DOC_COMMENT_MODE: Type.COMMENT, + JavaScriptModes.DOC_COMMENT_LEX_SPACES_MODE: Type.COMMENT + } + + def __init__(self, parse_js_doc = True): + """Create a tokenizer object. + + Args: + parse_js_doc: Whether to do detailed parsing of javascript doc comments, + or simply treat them as normal comments. Defaults to parsing JsDoc. + """ + matchers = self.JAVASCRIPT_MATCHERS + if not parse_js_doc: + # Make a copy so the original doesn't get modified. + matchers = copy.deepcopy(matchers) + matchers[JavaScriptModes.DOC_COMMENT_MODE] = matchers[ + JavaScriptModes.BLOCK_COMMENT_MODE] + + tokenizer.Tokenizer.__init__(self, JavaScriptModes.TEXT_MODE, matchers, + self.JAVASCRIPT_DEFAULT_TYPES) + + def _CreateToken(self, string, token_type, line, line_number, values=None): + """Creates a new JavaScriptToken object. + + Args: + string: The string of input the token contains. + token_type: The type of token. + line: The text of the line this token is in. + line_number: The line number of the token. + values: A dict of named values within the token. For instance, a + function declaration may have a value called 'name' which captures the + name of the function. + """ + return javascripttokens.JavaScriptToken(string, token_type, line, + line_number, values, line_number) diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascripttokens.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascripttokens.py new file mode 100755 index 0000000000..f46d4e17bc --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/javascripttokens.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python +# +# Copyright 2008 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Classes to represent JavaScript tokens.""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)') + +from closure_linter.common import tokens + +class JavaScriptTokenType(tokens.TokenType): + """Enumeration of JavaScript token types, and useful sets of token types.""" + NUMBER = 'number' + START_SINGLE_LINE_COMMENT = '//' + START_BLOCK_COMMENT = '/*' + START_DOC_COMMENT = '/**' + END_BLOCK_COMMENT = '*/' + END_DOC_COMMENT = 'doc */' + COMMENT = 'comment' + SINGLE_QUOTE_STRING_START = "'string" + SINGLE_QUOTE_STRING_END = "string'" + DOUBLE_QUOTE_STRING_START = '"string' + DOUBLE_QUOTE_STRING_END = 'string"' + STRING_TEXT = 'string' + START_BLOCK = '{' + END_BLOCK = '}' + START_PAREN = '(' + END_PAREN = ')' + START_BRACKET = '[' + END_BRACKET = ']' + REGEX = '/regex/' + FUNCTION_DECLARATION = 'function(...)' + FUNCTION_NAME = 'function functionName(...)' + START_PARAMETERS = 'startparams(' + PARAMETERS = 'pa,ra,ms' + END_PARAMETERS = ')endparams' + SEMICOLON = ';' + DOC_FLAG = '@flag' + DOC_INLINE_FLAG = '{@flag ...}' + DOC_START_BRACE = 'doc {' + DOC_END_BRACE = 'doc }' + DOC_PREFIX = 'comment prefix: * ' + SIMPLE_LVALUE = 'lvalue=' + KEYWORD = 'keyword' + OPERATOR = 'operator' + IDENTIFIER = 'identifier' + + STRING_TYPES = frozenset([ + SINGLE_QUOTE_STRING_START, SINGLE_QUOTE_STRING_END, + DOUBLE_QUOTE_STRING_START, DOUBLE_QUOTE_STRING_END, STRING_TEXT]) + + COMMENT_TYPES = frozenset([START_SINGLE_LINE_COMMENT, COMMENT, + START_BLOCK_COMMENT, START_DOC_COMMENT, + END_BLOCK_COMMENT, END_DOC_COMMENT, + DOC_START_BRACE, DOC_END_BRACE, + DOC_FLAG, DOC_INLINE_FLAG, DOC_PREFIX]) + + FLAG_DESCRIPTION_TYPES = frozenset([ + DOC_INLINE_FLAG, COMMENT, DOC_START_BRACE, DOC_END_BRACE]) + + FLAG_ENDING_TYPES = frozenset([DOC_FLAG, END_DOC_COMMENT]) + + NON_CODE_TYPES = COMMENT_TYPES | frozenset([ + tokens.TokenType.WHITESPACE, tokens.TokenType.BLANK_LINE]) + + UNARY_OPERATORS = ['!', 'new', 'delete', 'typeof', 'void'] + + UNARY_OK_OPERATORS = ['--', '++', '-', '+'] + UNARY_OPERATORS + + UNARY_POST_OPERATORS = ['--', '++'] + + # An expression ender is any token that can end an object - i.e. we could have + # x.y or [1, 2], or (10 + 9) or {a: 10}. + EXPRESSION_ENDER_TYPES = [tokens.TokenType.NORMAL, IDENTIFIER, NUMBER, + SIMPLE_LVALUE, END_BRACKET, END_PAREN, END_BLOCK, + SINGLE_QUOTE_STRING_END, DOUBLE_QUOTE_STRING_END] + + +class JavaScriptToken(tokens.Token): + """JavaScript token subclass of Token, provides extra instance checks. + + The following token types have data in attached_object: + - All JsDoc flags: a parser.JsDocFlag object. + """ + + def IsKeyword(self, keyword): + """Tests if this token is the given keyword. + + Args: + keyword: The keyword to compare to. + + Returns: + True if this token is a keyword token with the given name. + """ + return self.type == JavaScriptTokenType.KEYWORD and self.string == keyword + + def IsOperator(self, operator): + """Tests if this token is the given operator. + + Args: + operator: The operator to compare to. + + Returns: + True if this token is a operator token with the given name. + """ + return self.type == JavaScriptTokenType.OPERATOR and self.string == operator + + def IsAssignment(self): + """Tests if this token is an assignment operator. + + Returns: + True if this token is an assignment operator. + """ + return (self.type == JavaScriptTokenType.OPERATOR and + self.string.endswith('=') and + self.string not in ('==', '!=', '>=', '<=', '===', '!==')) + + def IsComment(self): + """Tests if this token is any part of a comment. + + Returns: + True if this token is any part of a comment. + """ + return self.type in JavaScriptTokenType.COMMENT_TYPES + + def IsCode(self): + """Tests if this token is code, as opposed to a comment or whitespace.""" + return self.type not in JavaScriptTokenType.NON_CODE_TYPES + + def __repr__(self): + return '' % (self.line_number, + self.type, self.string, + self.values, + self.metadata) diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/not_strict_test.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/not_strict_test.py new file mode 100755 index 0000000000..c92c13ee03 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/not_strict_test.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# +# Copyright 2011 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for gjslint --nostrict. + +Tests errors that can be thrown by gjslint when not in strict mode. +""" + + + +import os +import sys +import unittest + +import gflags as flags +import unittest as googletest + +from closure_linter import errors +from closure_linter import runner +from closure_linter.common import filetestcase + +_RESOURCE_PREFIX = 'closure_linter/testdata' + +flags.FLAGS.strict = False +flags.FLAGS.custom_jsdoc_tags = ('customtag', 'requires') +flags.FLAGS.closurized_namespaces = ('goog', 'dummy') +flags.FLAGS.limited_doc_files = ('externs.js', 'dummy.js', + 'limited_doc_checks.js') + + +# List of files under testdata to test. +# We need to list files explicitly since pyglib can't list directories. +_TEST_FILES = [ + 'not_strict.js' + ] + + +class GJsLintTestSuite(unittest.TestSuite): + """Test suite to run a GJsLintTest for each of several files. + + If sys.argv[1:] is non-empty, it is interpreted as a list of filenames in + testdata to test. Otherwise, _TEST_FILES is used. + """ + + def __init__(self, tests=()): + unittest.TestSuite.__init__(self, tests) + + argv = sys.argv and sys.argv[1:] or [] + if argv: + test_files = argv + else: + test_files = _TEST_FILES + for test_file in test_files: + resource_path = os.path.join(_RESOURCE_PREFIX, test_file) + self.addTest(filetestcase.AnnotatedFileTestCase(resource_path, + runner.Run, + errors.ByName)) + +if __name__ == '__main__': + # Don't let main parse args; it happens in the TestSuite. + googletest.main(argv=sys.argv[0:1], defaultTest='GJsLintTestSuite') diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/requireprovidesorter.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/requireprovidesorter.py new file mode 100755 index 0000000000..20f0f93de9 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/requireprovidesorter.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python +# +# Copyright 2011 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Contains logic for sorting goog.provide and goog.require statements. + +Closurized JavaScript files use goog.provide and goog.require statements at the +top of the file to manage dependencies. These statements should be sorted +alphabetically, however, it is common for them to be accompanied by inline +comments or suppression annotations. In order to sort these statements without +disrupting their comments and annotations, the association between statements +and comments/annotations must be maintained while sorting. + + RequireProvideSorter: Handles checking/fixing of provide/require statements. +""" + + + +from closure_linter import javascripttokens +from closure_linter import tokenutil + +# Shorthand +Type = javascripttokens.JavaScriptTokenType + + +class RequireProvideSorter(object): + """Checks for and fixes alphabetization of provide and require statements. + + When alphabetizing, comments on the same line or comments directly above a + goog.provide or goog.require statement are associated with that statement and + stay with the statement as it gets sorted. + """ + + def CheckProvides(self, token): + """Checks alphabetization of goog.provide statements. + + Iterates over tokens in given token stream, identifies goog.provide tokens, + and checks that they occur in alphabetical order by the object being + provided. + + Args: + token: A token in the token stream before any goog.provide tokens. + + Returns: + The first provide token in the token stream. + + None is returned if all goog.provide statements are already sorted. + """ + provide_tokens = self._GetRequireOrProvideTokens(token, 'goog.provide') + provide_strings = self._GetRequireOrProvideTokenStrings(provide_tokens) + sorted_provide_strings = sorted(provide_strings) + if provide_strings != sorted_provide_strings: + return provide_tokens[0] + return None + + def CheckRequires(self, token): + """Checks alphabetization of goog.require statements. + + Iterates over tokens in given token stream, identifies goog.require tokens, + and checks that they occur in alphabetical order by the dependency being + required. + + Args: + token: A token in the token stream before any goog.require tokens. + + Returns: + The first require token in the token stream. + + None is returned if all goog.require statements are already sorted. + """ + require_tokens = self._GetRequireOrProvideTokens(token, 'goog.require') + require_strings = self._GetRequireOrProvideTokenStrings(require_tokens) + sorted_require_strings = sorted(require_strings) + if require_strings != sorted_require_strings: + return require_tokens[0] + return None + + def FixProvides(self, token): + """Sorts goog.provide statements in the given token stream alphabetically. + + Args: + token: The first token in the token stream. + """ + self._FixProvidesOrRequires( + self._GetRequireOrProvideTokens(token, 'goog.provide')) + + def FixRequires(self, token): + """Sorts goog.require statements in the given token stream alphabetically. + + Args: + token: The first token in the token stream. + """ + self._FixProvidesOrRequires( + self._GetRequireOrProvideTokens(token, 'goog.require')) + + def _FixProvidesOrRequires(self, tokens): + """Sorts goog.provide or goog.require statements. + + Args: + tokens: A list of goog.provide or goog.require tokens in the order they + appear in the token stream. i.e. the first token in this list must + be the first goog.provide or goog.require token. + """ + strings = self._GetRequireOrProvideTokenStrings(tokens) + sorted_strings = sorted(strings) + + # Make a separate pass to remove any blank lines between goog.require/ + # goog.provide tokens. + first_token = tokens[0] + last_token = tokens[-1] + i = last_token + while i != first_token: + if i.type is Type.BLANK_LINE: + tokenutil.DeleteToken(i) + i = i.previous + + # A map from required/provided object name to tokens that make up the line + # it was on, including any comments immediately before it or after it on the + # same line. + tokens_map = self._GetTokensMap(tokens) + + # Iterate over the map removing all tokens. + for name in tokens_map: + tokens_to_delete = tokens_map[name] + for i in tokens_to_delete: + tokenutil.DeleteToken(i) + + # Save token to rest of file. Sorted token will be inserted before this. + rest_of_file = tokens_map[strings[-1]][-1].next + + # Re-add all tokens in the map in alphabetical order. + insert_after = tokens[0].previous + for string in sorted_strings: + for i in tokens_map[string]: + if rest_of_file: + tokenutil.InsertTokenBefore(i, rest_of_file) + else: + tokenutil.InsertTokenAfter(i, insert_after) + insert_after = i + + def _GetRequireOrProvideTokens(self, token, token_string): + """Gets all goog.provide or goog.require tokens in the given token stream. + + Args: + token: The first token in the token stream. + token_string: One of 'goog.provide' or 'goog.require' to indicate which + tokens to find. + + Returns: + A list of goog.provide or goog.require tokens in the order they appear in + the token stream. + """ + tokens = [] + while token: + if token.type == Type.IDENTIFIER: + if token.string == token_string: + tokens.append(token) + elif token.string not in [ + 'goog.provide', 'goog.require', 'goog.setTestOnly']: + # These 3 identifiers are at the top of the file. So if any other + # identifier is encountered, return. + break + token = token.next + + return tokens + + def _GetRequireOrProvideTokenStrings(self, tokens): + """Gets a list of strings corresponding to the given list of tokens. + + The string will be the next string in the token stream after each token in + tokens. This is used to find the object being provided/required by a given + goog.provide or goog.require token. + + Args: + tokens: A list of goog.provide or goog.require tokens. + + Returns: + A list of object names that are being provided or required by the given + list of tokens. For example: + + ['object.a', 'object.c', 'object.b'] + """ + token_strings = [] + for token in tokens: + name = tokenutil.GetStringAfterToken(token) + token_strings.append(name) + return token_strings + + def _GetTokensMap(self, tokens): + """Gets a map from object name to tokens associated with that object. + + Starting from the goog.provide/goog.require token, searches backwards in the + token stream for any lines that start with a comment. These lines are + associated with the goog.provide/goog.require token. Also associates any + tokens on the same line as the goog.provide/goog.require token with that + token. + + Args: + tokens: A list of goog.provide or goog.require tokens. + + Returns: + A dictionary that maps object names to the tokens associated with the + goog.provide or goog.require of that object name. For example: + + { + 'object.a': [JavaScriptToken, JavaScriptToken, ...], + 'object.b': [...] + } + + The list of tokens includes any comment lines above the goog.provide or + goog.require statement and everything after the statement on the same + line. For example, all of the following would be associated with + 'object.a': + + /** @suppress {extraRequire} */ + goog.require('object.a'); // Some comment. + """ + tokens_map = {} + for token in tokens: + object_name = tokenutil.GetStringAfterToken(token) + # If the previous line starts with a comment, presume that the comment + # relates to the goog.require or goog.provide and keep them together when + # sorting. + first_token = token + previous_first_token = tokenutil.GetFirstTokenInPreviousLine(first_token) + while (previous_first_token and + previous_first_token.IsAnyType(Type.COMMENT_TYPES)): + first_token = previous_first_token + previous_first_token = tokenutil.GetFirstTokenInPreviousLine( + first_token) + + # Find the last token on the line. + last_token = tokenutil.GetLastTokenInSameLine(token) + + all_tokens = self._GetTokenList(first_token, last_token) + tokens_map[object_name] = all_tokens + return tokens_map + + def _GetTokenList(self, first_token, last_token): + """Gets a list of all tokens from first_token to last_token, inclusive. + + Args: + first_token: The first token to get. + last_token: The last token to get. + + Returns: + A list of all tokens between first_token and last_token, including both + first_token and last_token. + + Raises: + Exception: If the token stream ends before last_token is reached. + """ + token_list = [] + token = first_token + while token != last_token: + if not token: + raise Exception('ran out of tokens') + token_list.append(token) + token = token.next + token_list.append(last_token) + + return token_list + + def GetFixedRequireString(self, token): + """Get fixed/sorted order of goog.require statements. + + Args: + token: The first token in the token stream. + + Returns: + A string for correct sorted order of goog.require. + """ + return self._GetFixedRequireOrProvideString( + self._GetRequireOrProvideTokens(token, 'goog.require')) + + def GetFixedProvideString(self, token): + """Get fixed/sorted order of goog.provide statements. + + Args: + token: The first token in the token stream. + + Returns: + A string for correct sorted order of goog.provide. + """ + return self._GetFixedRequireOrProvideString( + self._GetRequireOrProvideTokens(token, 'goog.provide')) + + def _GetFixedRequireOrProvideString(self, tokens): + """Sorts goog.provide or goog.require statements. + + Args: + tokens: A list of goog.provide or goog.require tokens in the order they + appear in the token stream. i.e. the first token in this list must + be the first goog.provide or goog.require token. + + Returns: + A string for sorted goog.require or goog.provide statements + """ + + # A map from required/provided object name to tokens that make up the line + # it was on, including any comments immediately before it or after it on the + # same line. + tokens_map = self._GetTokensMap(tokens) + sorted_strings = sorted(tokens_map.keys()) + + new_order = '' + for string in sorted_strings: + for i in tokens_map[string]: + new_order += i.string + if i.IsLastInLine(): + new_order += '\n' + + return new_order diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/requireprovidesorter_test.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/requireprovidesorter_test.py new file mode 100644 index 0000000000..fecb6d04da --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/requireprovidesorter_test.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python +# +# Copyright 2012 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for RequireProvideSorter.""" + + + +import unittest as googletest +from closure_linter import javascripttokens +from closure_linter import requireprovidesorter +from closure_linter import testutil + +# pylint: disable=g-bad-name +TokenType = javascripttokens.JavaScriptTokenType + + +class RequireProvideSorterTest(googletest.TestCase): + """Tests for RequireProvideSorter.""" + + def testGetFixedProvideString(self): + """Tests that fixed string constains proper comments also.""" + input_lines = [ + 'goog.provide(\'package.xyz\');', + '/** @suppress {extraprovide} **/', + 'goog.provide(\'package.abcd\');' + ] + + expected_lines = [ + '/** @suppress {extraprovide} **/', + 'goog.provide(\'package.abcd\');', + 'goog.provide(\'package.xyz\');' + ] + + token = testutil.TokenizeSourceAndRunEcmaPass(input_lines) + + sorter = requireprovidesorter.RequireProvideSorter() + fixed_provide_string = sorter.GetFixedProvideString(token) + + self.assertEquals(expected_lines, fixed_provide_string.splitlines()) + + def testGetFixedRequireString(self): + """Tests that fixed string constains proper comments also.""" + input_lines = [ + 'goog.require(\'package.xyz\');', + '/** This is needed for scope. **/', + 'goog.require(\'package.abcd\');' + ] + + expected_lines = [ + '/** This is needed for scope. **/', + 'goog.require(\'package.abcd\');', + 'goog.require(\'package.xyz\');' + ] + + token = testutil.TokenizeSourceAndRunEcmaPass(input_lines) + + sorter = requireprovidesorter.RequireProvideSorter() + fixed_require_string = sorter.GetFixedRequireString(token) + + self.assertEquals(expected_lines, fixed_require_string.splitlines()) + + def testFixRequires_removeBlankLines(self): + """Tests that blank lines are omitted in sorted goog.require statements.""" + input_lines = [ + 'goog.provide(\'package.subpackage.Whatever\');', + '', + 'goog.require(\'package.subpackage.ClassB\');', + '', + 'goog.require(\'package.subpackage.ClassA\');' + ] + expected_lines = [ + 'goog.provide(\'package.subpackage.Whatever\');', + '', + 'goog.require(\'package.subpackage.ClassA\');', + 'goog.require(\'package.subpackage.ClassB\');' + ] + token = testutil.TokenizeSourceAndRunEcmaPass(input_lines) + + sorter = requireprovidesorter.RequireProvideSorter() + sorter.FixRequires(token) + + self.assertEquals(expected_lines, self._GetLines(token)) + + def fixRequiresTest_withTestOnly(self, position): + """Regression-tests sorting even with a goog.setTestOnly statement. + + Args: + position: The position in the list where to insert the goog.setTestOnly + statement. Will be used to test all possible combinations for + this test. + """ + input_lines = [ + 'goog.provide(\'package.subpackage.Whatever\');', + '', + 'goog.require(\'package.subpackage.ClassB\');', + 'goog.require(\'package.subpackage.ClassA\');' + ] + expected_lines = [ + 'goog.provide(\'package.subpackage.Whatever\');', + '', + 'goog.require(\'package.subpackage.ClassA\');', + 'goog.require(\'package.subpackage.ClassB\');' + ] + input_lines.insert(position, 'goog.setTestOnly();') + expected_lines.insert(position, 'goog.setTestOnly();') + + token = testutil.TokenizeSourceAndRunEcmaPass(input_lines) + + sorter = requireprovidesorter.RequireProvideSorter() + sorter.FixRequires(token) + + self.assertEquals(expected_lines, self._GetLines(token)) + + def testFixRequires_withTestOnly(self): + """Regression-tests sorting even after a goog.setTestOnly statement.""" + + # goog.setTestOnly at first line. + self.fixRequiresTest_withTestOnly(position=0) + + # goog.setTestOnly after goog.provide. + self.fixRequiresTest_withTestOnly(position=1) + + # goog.setTestOnly before goog.require. + self.fixRequiresTest_withTestOnly(position=2) + + # goog.setTestOnly after goog.require. + self.fixRequiresTest_withTestOnly(position=4) + + def _GetLines(self, token): + """Returns an array of lines based on the specified token stream.""" + lines = [] + line = '' + while token: + line += token.string + if token.IsLastInLine(): + lines.append(line) + line = '' + token = token.next + return lines + +if __name__ == '__main__': + googletest.main() diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/runner.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/runner.py new file mode 100644 index 0000000000..76718c6706 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/runner.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python +# +# Copyright 2012 The Closure Linter Authors. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Main lint function. Tokenizes file, runs passes, and feeds to checker.""" + +# Allow non-Google copyright +# pylint: disable=g-bad-file-header + +__author__ = 'nnaze@google.com (Nathan Naze)' + +import traceback + +import gflags as flags + +from closure_linter import checker +from closure_linter import ecmametadatapass +from closure_linter import errors +from closure_linter import javascriptstatetracker +from closure_linter import javascripttokenizer + +from closure_linter.common import error +from closure_linter.common import htmlutil +from closure_linter.common import tokens + +flags.DEFINE_list('limited_doc_files', ['dummy.js', 'externs.js'], + 'List of files with relaxed documentation checks. Will not ' + 'report errors for missing documentation, some missing ' + 'descriptions, or methods whose @return tags don\'t have a ' + 'matching return statement.') +flags.DEFINE_boolean('error_trace', False, + 'Whether to show error exceptions.') + + +def _GetLastNonWhiteSpaceToken(start_token): + """Get the last non-whitespace token in a token stream.""" + ret_token = None + + whitespace_tokens = frozenset([ + tokens.TokenType.WHITESPACE, tokens.TokenType.BLANK_LINE]) + for t in start_token: + if t.type not in whitespace_tokens: + ret_token = t + + return ret_token + + +def _IsHtml(filename): + return filename.endswith('.html') or filename.endswith('.htm') + + +def _Tokenize(fileobj): + """Tokenize a file. + + Args: + fileobj: file-like object (or iterable lines) with the source. + + Returns: + The first token in the token stream and the ending mode of the tokenizer. + """ + tokenizer = javascripttokenizer.JavaScriptTokenizer() + start_token = tokenizer.TokenizeFile(fileobj) + return start_token, tokenizer.mode + + +def _IsLimitedDocCheck(filename, limited_doc_files): + """Whether this this a limited-doc file. + + Args: + filename: The filename. + limited_doc_files: Iterable of strings. Suffixes of filenames that should + be limited doc check. + + Returns: + Whether the file should be limited check. + """ + for limited_doc_filename in limited_doc_files: + if filename.endswith(limited_doc_filename): + return True + return False + + +def Run(filename, error_handler, source=None): + """Tokenize, run passes, and check the given file. + + Args: + filename: The path of the file to check + error_handler: The error handler to report errors to. + source: A file-like object with the file source. If omitted, the file will + be read from the filename path. + """ + if not source: + try: + source = open(filename) + except IOError: + error_handler.HandleFile(filename, None) + error_handler.HandleError( + error.Error(errors.FILE_NOT_FOUND, 'File not found')) + error_handler.FinishFile() + return + + if _IsHtml(filename): + source_file = htmlutil.GetScriptLines(source) + else: + source_file = source + + token, tokenizer_mode = _Tokenize(source_file) + + error_handler.HandleFile(filename, token) + + # If we did not end in the basic mode, this a failed parse. + if tokenizer_mode is not javascripttokenizer.JavaScriptModes.TEXT_MODE: + error_handler.HandleError( + error.Error(errors.FILE_IN_BLOCK, + 'File ended in mode "%s".' % tokenizer_mode, + _GetLastNonWhiteSpaceToken(token))) + + # Run the ECMA pass + error_token = None + + ecma_pass = ecmametadatapass.EcmaMetaDataPass() + error_token = RunMetaDataPass(token, ecma_pass, error_handler, filename) + + is_limited_doc_check = ( + _IsLimitedDocCheck(filename, flags.FLAGS.limited_doc_files)) + + _RunChecker(token, error_handler, + is_limited_doc_check, + is_html=_IsHtml(filename), + stop_token=error_token) + + error_handler.FinishFile() + + +def RunMetaDataPass(start_token, metadata_pass, error_handler, filename=''): + """Run a metadata pass over a token stream. + + Args: + start_token: The first token in a token stream. + metadata_pass: Metadata pass to run. + error_handler: The error handler to report errors to. + filename: Filename of the source. + + Returns: + The token where the error occurred (if any). + """ + + try: + metadata_pass.Process(start_token) + except ecmametadatapass.ParseError, parse_err: + if flags.FLAGS.error_trace: + traceback.print_exc() + error_token = parse_err.token + error_msg = str(parse_err) + error_handler.HandleError( + error.Error(errors.FILE_DOES_NOT_PARSE, + ('Error parsing file at token "%s". Unable to ' + 'check the rest of file.' + '\nError "%s"' % (error_token, error_msg)), error_token)) + return error_token + except Exception: # pylint: disable=broad-except + traceback.print_exc() + error_handler.HandleError( + error.Error( + errors.FILE_DOES_NOT_PARSE, + 'Internal error in %s' % filename)) + + +def _RunChecker(start_token, error_handler, + limited_doc_checks, is_html, + stop_token=None): + + state_tracker = javascriptstatetracker.JavaScriptStateTracker() + + style_checker = checker.JavaScriptStyleChecker( + state_tracker=state_tracker, + error_handler=error_handler) + + style_checker.Check(start_token, + is_html=is_html, + limited_doc_checks=limited_doc_checks, + stop_token=stop_token) diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/runner_test.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/runner_test.py new file mode 100644 index 0000000000..da5857d309 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/runner_test.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python +# +# Copyright 2008 The Closure Linter Authors. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for the runner module.""" + +__author__ = ('nnaze@google.com (Nathan Naze)') + +import StringIO + + +import mox + + +import unittest as googletest + +from closure_linter import errors +from closure_linter import runner +from closure_linter.common import error +from closure_linter.common import errorhandler +from closure_linter.common import tokens + + +class LimitedDocTest(googletest.TestCase): + + def testIsLimitedDocCheck(self): + self.assertTrue(runner._IsLimitedDocCheck('foo_test.js', ['_test.js'])) + self.assertFalse(runner._IsLimitedDocCheck('foo_bar.js', ['_test.js'])) + + self.assertTrue(runner._IsLimitedDocCheck( + 'foo_moo.js', ['moo.js', 'quack.js'])) + self.assertFalse(runner._IsLimitedDocCheck( + 'foo_moo.js', ['woof.js', 'quack.js'])) + + +class RunnerTest(googletest.TestCase): + + def setUp(self): + self.mox = mox.Mox() + + def testRunOnMissingFile(self): + mock_error_handler = self.mox.CreateMock(errorhandler.ErrorHandler) + + def ValidateError(err): + return (isinstance(err, error.Error) and + err.code is errors.FILE_NOT_FOUND and + err.token is None) + + mock_error_handler.HandleFile('does_not_exist.js', None) + mock_error_handler.HandleError(mox.Func(ValidateError)) + mock_error_handler.FinishFile() + + self.mox.ReplayAll() + + runner.Run('does_not_exist.js', mock_error_handler) + + self.mox.VerifyAll() + + def testBadTokenization(self): + mock_error_handler = self.mox.CreateMock(errorhandler.ErrorHandler) + + def ValidateError(err): + return (isinstance(err, error.Error) and + err.code is errors.FILE_IN_BLOCK and + err.token.string == '}') + + mock_error_handler.HandleFile('foo.js', mox.IsA(tokens.Token)) + mock_error_handler.HandleError(mox.Func(ValidateError)) + mock_error_handler.HandleError(mox.IsA(error.Error)) + mock_error_handler.FinishFile() + + self.mox.ReplayAll() + + source = StringIO.StringIO(_BAD_TOKENIZATION_SCRIPT) + runner.Run('foo.js', mock_error_handler, source) + + self.mox.VerifyAll() + + +_BAD_TOKENIZATION_SCRIPT = """ +function foo () { + var a = 3; + var b = 2; + return b + a; /* Comment not closed +} +""" + + +if __name__ == '__main__': + googletest.main() diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/scopeutil.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/scopeutil.py new file mode 100644 index 0000000000..a612d1a98f --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/scopeutil.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python +# +# Copyright 2012 The Closure Linter Authors. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tools to match goog.scope alias statements.""" + +# Allow non-Google copyright +# pylint: disable=g-bad-file-header + +__author__ = ('nnaze@google.com (Nathan Naze)') + +import itertools + +from closure_linter import ecmametadatapass +from closure_linter import tokenutil +from closure_linter.javascripttokens import JavaScriptTokenType + + + +def IsGoogScopeBlock(context): + """Whether the given context is a goog.scope block. + + This function only checks that the block is a function block inside + a goog.scope() call. + + TODO(nnaze): Implement goog.scope checks that verify the call is + in the root context and contains only a single function literal. + + Args: + context: An EcmaContext of type block. + + Returns: + Whether the context is a goog.scope block. + """ + + if context.type != ecmametadatapass.EcmaContext.BLOCK: + return False + + if not _IsFunctionLiteralBlock(context): + return False + + # Check that this function is contained by a group + # of form "goog.scope(...)". + parent = context.parent + if parent and parent.type is ecmametadatapass.EcmaContext.GROUP: + + last_code_token = parent.start_token.metadata.last_code + + if (last_code_token and + last_code_token.type is JavaScriptTokenType.IDENTIFIER and + last_code_token.string == 'goog.scope'): + return True + + return False + + +def _IsFunctionLiteralBlock(block_context): + """Check if a context is a function literal block (without parameters). + + Example function literal block: 'function() {}' + + Args: + block_context: An EcmaContext of type block. + + Returns: + Whether this context is a function literal block. + """ + + previous_code_tokens_iter = itertools.ifilter( + lambda token: token not in JavaScriptTokenType.NON_CODE_TYPES, + reversed(block_context.start_token)) + + # Ignore the current token + next(previous_code_tokens_iter, None) + + # Grab the previous three tokens and put them in correct order. + previous_code_tokens = list(itertools.islice(previous_code_tokens_iter, 3)) + previous_code_tokens.reverse() + + # There aren't three previous tokens. + if len(previous_code_tokens) is not 3: + return False + + # Check that the previous three code tokens are "function ()" + previous_code_token_types = [token.type for token in previous_code_tokens] + if (previous_code_token_types == [ + JavaScriptTokenType.FUNCTION_DECLARATION, + JavaScriptTokenType.START_PARAMETERS, + JavaScriptTokenType.END_PARAMETERS]): + return True + + return False + + +def IsInClosurizedNamespace(symbol, closurized_namespaces): + """Match a goog.scope alias. + + Args: + symbol: An identifier like 'goog.events.Event'. + closurized_namespaces: Iterable of valid Closurized namespaces (strings). + + Returns: + True if symbol is an identifier in a Closurized namespace, otherwise False. + """ + for ns in closurized_namespaces: + if symbol.startswith(ns + '.'): + return True + + return False + + +def MatchAlias(context): + """Match an alias statement (some identifier assigned to a variable). + + Example alias: var MyClass = proj.longNamespace.MyClass. + + Args: + context: An EcmaContext of type EcmaContext.STATEMENT. + + Returns: + If a valid alias, returns a tuple of alias and symbol, otherwise None. + """ + + if context.type != ecmametadatapass.EcmaContext.STATEMENT: + return + + # Get the tokens in this statement. + if context.start_token and context.end_token: + statement_tokens = tokenutil.GetTokenRange(context.start_token, + context.end_token) + else: + return + + # And now just those tokens that are actually code. + is_non_code_type = lambda t: t.type not in JavaScriptTokenType.NON_CODE_TYPES + code_tokens = filter(is_non_code_type, statement_tokens) + + # This section identifies statements of the alias form "var alias = symbol". + + # Pop off the semicolon if present. + if code_tokens and code_tokens[-1].IsType(JavaScriptTokenType.SEMICOLON): + code_tokens.pop() + + if not (len(code_tokens) == 4 and + code_tokens[0].IsKeyword('var') and + (code_tokens[0].metadata.context.type == + ecmametadatapass.EcmaContext.VAR)): + return + + # Verify the only code tokens in this statement are part of the var + # declaration. + var_context = code_tokens[0].metadata.context + for token in code_tokens: + if token.metadata.context is not var_context: + return + + # Verify that this is of the form "var lvalue = identifier;". + if not(code_tokens[0].IsKeyword('var') and + code_tokens[1].IsType(JavaScriptTokenType.SIMPLE_LVALUE) and + code_tokens[2].IsOperator('=') and + code_tokens[3].IsType(JavaScriptTokenType.IDENTIFIER)): + return + + alias, symbol = code_tokens[1], code_tokens[3] + # Mark both tokens as an alias definition to avoid counting them as usages. + alias.metadata.is_alias_definition = True + symbol.metadata.is_alias_definition = True + + return alias.string, symbol.string diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/scopeutil_test.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/scopeutil_test.py new file mode 100644 index 0000000000..9bb836e232 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/scopeutil_test.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python +# +# Copyright 2012 The Closure Linter Authors. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for the scopeutil module.""" + +# Allow non-Google copyright +# pylint: disable=g-bad-file-header + +__author__ = ('nnaze@google.com (Nathan Naze)') + + +import unittest as googletest + +from closure_linter import ecmametadatapass +from closure_linter import scopeutil +from closure_linter import testutil + + +def _FindContexts(start_token): + """Depth first search of all contexts referenced by a token stream. + + Includes contexts' parents, which might not be directly referenced + by any token in the stream. + + Args: + start_token: First token in the token stream. + + Yields: + All contexts referenced by this token stream. + """ + + seen_contexts = set() + + # For each token, yield the context if we haven't seen it before. + for token in start_token: + + token_context = token.metadata.context + contexts = [token_context] + + # Also grab all the context's ancestors. + parent = token_context.parent + while parent: + contexts.append(parent) + parent = parent.parent + + # Yield each of these contexts if we've not seen them. + for context in contexts: + if context not in seen_contexts: + yield context + + seen_contexts.add(context) + + +def _FindFirstContextOfType(token, context_type): + """Returns the first statement context.""" + for context in _FindContexts(token): + if context.type == context_type: + return context + + +class StatementTest(googletest.TestCase): + + def assertAlias(self, expected_match, script): + start_token = testutil.TokenizeSourceAndRunEcmaPass(script) + statement = _FindFirstContextOfType( + start_token, ecmametadatapass.EcmaContext.STATEMENT) + match = scopeutil.MatchAlias(statement) + self.assertEquals(expected_match, match) + + def testSimpleAliases(self): + self.assertAlias( + ('foo', 'goog.foo'), + 'var foo = goog.foo;') + + self.assertAlias( + ('foo', 'goog.foo'), + 'var foo = goog.foo') # No semicolon + + def testAliasWithComment(self): + self.assertAlias( + ('Component', 'goog.ui.Component'), + 'var Component = /* comment */ goog.ui.Component;') + + def testMultilineComment(self): + self.assertAlias( + ('Component', 'goog.ui.Component'), + 'var Component = \n goog.ui.Component;') + + def testNonSymbolAliasVarStatements(self): + self.assertAlias(None, 'var foo = 3;') + self.assertAlias(None, 'var foo = function() {};') + self.assertAlias(None, 'for(var foo = bar;;){}') + self.assertAlias(None, 'var foo = bar ? baz : qux;') + + +class ScopeBlockTest(googletest.TestCase): + + @staticmethod + def _GetBlocks(source): + start_token = testutil.TokenizeSourceAndRunEcmaPass(source) + for context in _FindContexts(start_token): + if context.type is ecmametadatapass.EcmaContext.BLOCK: + yield context + + def assertNoBlocks(self, script): + blocks = list(self._GetBlocks(script)) + self.assertEquals([], blocks) + + def testNotBlocks(self): + # Ensure these are not considered blocks. + self.assertNoBlocks('goog.scope(if{});') + self.assertNoBlocks('goog.scope(for{});') + self.assertNoBlocks('goog.scope(switch{});') + self.assertNoBlocks('goog.scope(function foo{});') + + def testNonScopeBlocks(self): + + blocks = list(self._GetBlocks('goog.scope(try{});')) + self.assertEquals(1, len(blocks)) + self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop())) + + blocks = list(self._GetBlocks('goog.scope(function(a,b){});')) + self.assertEquals(1, len(blocks)) + self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop())) + + blocks = list(self._GetBlocks('goog.scope(try{} catch(){});')) + # Two blocks: try and catch. + self.assertEquals(2, len(blocks)) + self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop())) + self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop())) + + blocks = list(self._GetBlocks('goog.scope(try{} catch(){} finally {});')) + self.assertEquals(3, len(blocks)) + self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop())) + self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop())) + self.assertFalse(scopeutil.IsGoogScopeBlock(blocks.pop())) + + +class AliasTest(googletest.TestCase): + + def setUp(self): + self.start_token = testutil.TokenizeSourceAndRunEcmaPass(_TEST_SCRIPT) + + def testMatchAliasStatement(self): + matches = set() + for context in _FindContexts(self.start_token): + match = scopeutil.MatchAlias(context) + if match: + matches.add(match) + + self.assertEquals( + set([('bar', 'baz'), + ('foo', 'this.foo_'), + ('Component', 'goog.ui.Component'), + ('MyClass', 'myproject.foo.MyClass'), + ('NonClosurizedClass', 'aaa.bbb.NonClosurizedClass')]), + matches) + + def testMatchAliasStatement_withClosurizedNamespaces(self): + + closurized_namepaces = frozenset(['goog', 'myproject']) + + matches = set() + for context in _FindContexts(self.start_token): + match = scopeutil.MatchAlias(context) + if match: + unused_alias, symbol = match + if scopeutil.IsInClosurizedNamespace(symbol, closurized_namepaces): + matches.add(match) + + self.assertEquals( + set([('MyClass', 'myproject.foo.MyClass'), + ('Component', 'goog.ui.Component')]), + matches) + +_TEST_SCRIPT = """ +goog.scope(function() { + var Component = goog.ui.Component; // scope alias + var MyClass = myproject.foo.MyClass; // scope alias + + // Scope alias of non-Closurized namespace. + var NonClosurizedClass = aaa.bbb.NonClosurizedClass; + + var foo = this.foo_; // non-scope object property alias + var bar = baz; // variable alias + + var component = new Component(); +}); + +""" + +if __name__ == '__main__': + googletest.main() diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/statetracker.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/statetracker.py new file mode 100644 index 0000000000..079950f0f5 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/statetracker.py @@ -0,0 +1,1212 @@ +#!/usr/bin/env python +# +# Copyright 2007 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Light weight EcmaScript state tracker that reads tokens and tracks state.""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)') + +import re + +from closure_linter import javascripttokenizer +from closure_linter import javascripttokens +from closure_linter import tokenutil + +# Shorthand +Type = javascripttokens.JavaScriptTokenType + + +class DocFlag(object): + """Generic doc flag object. + + Attribute: + flag_type: param, return, define, type, etc. + flag_token: The flag token. + type_start_token: The first token specifying the flag type, + including braces. + type_end_token: The last token specifying the flag type, + including braces. + type: The type spec. + name_token: The token specifying the flag name. + name: The flag name + description_start_token: The first token in the description. + description_end_token: The end token in the description. + description: The description. + """ + + # Please keep these lists alphabetized. + + # The list of standard jsdoc tags is from + STANDARD_DOC = frozenset([ + 'author', + 'bug', + 'classTemplate', + 'consistentIdGenerator', + 'const', + 'constructor', + 'define', + 'deprecated', + 'dict', + 'enum', + 'export', + 'expose', + 'extends', + 'externs', + 'fileoverview', + 'idGenerator', + 'implements', + 'implicitCast', + 'interface', + 'lends', + 'license', + 'ngInject', # This annotation is specific to AngularJS. + 'noalias', + 'nocompile', + 'nosideeffects', + 'override', + 'owner', + 'param', + 'preserve', + 'private', + 'protected', + 'public', + 'return', + 'see', + 'stableIdGenerator', + 'struct', + 'supported', + 'template', + 'this', + 'type', + 'typedef', + 'wizaction', # This annotation is specific to Wiz. + 'wizmodule', # This annotation is specific to Wiz. + ]) + + ANNOTATION = frozenset(['preserveTry', 'suppress']) + + LEGAL_DOC = STANDARD_DOC | ANNOTATION + + # Includes all Closure Compiler @suppress types. + # Not all of these annotations are interpreted by Closure Linter. + # + # Specific cases: + # - accessControls is supported by the compiler at the expression + # and method level to suppress warnings about private/protected + # access (method level applies to all references in the method). + # The linter mimics the compiler behavior. + SUPPRESS_TYPES = frozenset([ + 'accessControls', + 'ambiguousFunctionDecl', + 'checkRegExp', + 'checkStructDictInheritance', + 'checkTypes', + 'checkVars', + 'const', + 'constantProperty', + 'deprecated', + 'duplicate', + 'es5Strict', + 'externsValidation', + 'extraProvide', + 'extraRequire', + 'fileoverviewTags', + 'globalThis', + 'internetExplorerChecks', + 'invalidCasts', + 'missingProperties', + 'missingProvide', + 'missingRequire', + 'missingReturn', + 'nonStandardJsDocs', + 'strictModuleDepCheck', + 'tweakValidation', + 'typeInvalidation', + 'undefinedNames', + 'undefinedVars', + 'underscore', + 'unknownDefines', + 'unusedPrivateMembers', + 'uselessCode', + 'visibility', + 'with']) + + HAS_DESCRIPTION = frozenset([ + 'define', 'deprecated', 'desc', 'fileoverview', 'license', 'param', + 'preserve', 'return', 'supported']) + + HAS_TYPE = frozenset([ + 'define', 'enum', 'extends', 'implements', 'param', 'return', 'type', + 'suppress', 'const']) + + TYPE_ONLY = frozenset(['enum', 'extends', 'implements', 'suppress', 'type', + 'const']) + + HAS_NAME = frozenset(['param']) + + EMPTY_COMMENT_LINE = re.compile(r'^\s*\*?\s*$') + EMPTY_STRING = re.compile(r'^\s*$') + + def __init__(self, flag_token): + """Creates the DocFlag object and attaches it to the given start token. + + Args: + flag_token: The starting token of the flag. + """ + self.flag_token = flag_token + self.flag_type = flag_token.string.strip().lstrip('@') + + # Extract type, if applicable. + self.type = None + self.type_start_token = None + self.type_end_token = None + if self.flag_type in self.HAS_TYPE: + brace = tokenutil.SearchUntil(flag_token, [Type.DOC_START_BRACE], + Type.FLAG_ENDING_TYPES) + if brace: + end_token, contents = _GetMatchingEndBraceAndContents(brace) + self.type = contents + self.type_start_token = brace + self.type_end_token = end_token + elif (self.flag_type in self.TYPE_ONLY and + flag_token.next.type not in Type.FLAG_ENDING_TYPES and + flag_token.line_number == flag_token.next.line_number): + # b/10407058. If the flag is expected to be followed by a type then + # search for type in same line only. If no token after flag in same + # line then conclude that no type is specified. + self.type_start_token = flag_token.next + self.type_end_token, self.type = _GetEndTokenAndContents( + self.type_start_token) + if self.type is not None: + self.type = self.type.strip() + + # Extract name, if applicable. + self.name_token = None + self.name = None + if self.flag_type in self.HAS_NAME: + # Handle bad case, name could be immediately after flag token. + self.name_token = _GetNextPartialIdentifierToken(flag_token) + + # Handle good case, if found token is after type start, look for + # a identifier (substring to cover cases like [cnt] b/4197272) after + # type end, since types contain identifiers. + if (self.type and self.name_token and + tokenutil.Compare(self.name_token, self.type_start_token) > 0): + self.name_token = _GetNextPartialIdentifierToken(self.type_end_token) + + if self.name_token: + self.name = self.name_token.string + + # Extract description, if applicable. + self.description_start_token = None + self.description_end_token = None + self.description = None + if self.flag_type in self.HAS_DESCRIPTION: + search_start_token = flag_token + if self.name_token and self.type_end_token: + if tokenutil.Compare(self.type_end_token, self.name_token) > 0: + search_start_token = self.type_end_token + else: + search_start_token = self.name_token + elif self.name_token: + search_start_token = self.name_token + elif self.type: + search_start_token = self.type_end_token + + interesting_token = tokenutil.Search(search_start_token, + Type.FLAG_DESCRIPTION_TYPES | Type.FLAG_ENDING_TYPES) + if interesting_token.type in Type.FLAG_DESCRIPTION_TYPES: + self.description_start_token = interesting_token + self.description_end_token, self.description = ( + _GetEndTokenAndContents(interesting_token)) + + +class DocComment(object): + """JavaScript doc comment object. + + Attributes: + ordered_params: Ordered list of parameters documented. + start_token: The token that starts the doc comment. + end_token: The token that ends the doc comment. + suppressions: Map of suppression type to the token that added it. + """ + def __init__(self, start_token): + """Create the doc comment object. + + Args: + start_token: The first token in the doc comment. + """ + self.__flags = [] + self.start_token = start_token + self.end_token = None + self.suppressions = {} + self.invalidated = False + + @property + def ordered_params(self): + """Gives the list of parameter names as a list of strings.""" + params = [] + for flag in self.__flags: + if flag.flag_type == 'param' and flag.name: + params.append(flag.name) + return params + + def Invalidate(self): + """Indicate that the JSDoc is well-formed but we had problems parsing it. + + This is a short-circuiting mechanism so that we don't emit false + positives about well-formed doc comments just because we don't support + hot new syntaxes. + """ + self.invalidated = True + + def IsInvalidated(self): + """Test whether Invalidate() has been called.""" + return self.invalidated + + def AddSuppression(self, token): + """Add a new error suppression flag. + + Args: + token: The suppression flag token. + """ + #TODO(user): Error if no braces + brace = tokenutil.SearchUntil(token, [Type.DOC_START_BRACE], + [Type.DOC_FLAG]) + if brace: + end_token, contents = _GetMatchingEndBraceAndContents(brace) + for suppression in contents.split('|'): + self.suppressions[suppression] = token + + def SuppressionOnly(self): + """Returns whether this comment contains only suppression flags.""" + if not self.__flags: + return False + + for flag in self.__flags: + if flag.flag_type != 'suppress': + return False + + return True + + def AddFlag(self, flag): + """Add a new document flag. + + Args: + flag: DocFlag object. + """ + self.__flags.append(flag) + + def InheritsDocumentation(self): + """Test if the jsdoc implies documentation inheritance. + + Returns: + True if documentation may be pulled off the superclass. + """ + return self.HasFlag('inheritDoc') or self.HasFlag('override') + + def HasFlag(self, flag_type): + """Test if the given flag has been set. + + Args: + flag_type: The type of the flag to check. + + Returns: + True if the flag is set. + """ + for flag in self.__flags: + if flag.flag_type == flag_type: + return True + return False + + def GetFlag(self, flag_type): + """Gets the last flag of the given type. + + Args: + flag_type: The type of the flag to get. + + Returns: + The last instance of the given flag type in this doc comment. + """ + for flag in reversed(self.__flags): + if flag.flag_type == flag_type: + return flag + + def GetDocFlags(self): + """Return the doc flags for this comment.""" + return list(self.__flags) + + def _YieldDescriptionTokens(self): + for token in self.start_token: + + if (token is self.end_token or + token.type is javascripttokens.JavaScriptTokenType.DOC_FLAG or + token.type not in javascripttokens.JavaScriptTokenType.COMMENT_TYPES): + return + + if token.type not in [ + javascripttokens.JavaScriptTokenType.START_DOC_COMMENT, + javascripttokens.JavaScriptTokenType.END_DOC_COMMENT, + javascripttokens.JavaScriptTokenType.DOC_PREFIX]: + yield token + + @property + def description(self): + return tokenutil.TokensToString( + self._YieldDescriptionTokens()) + + def GetTargetIdentifier(self): + """Returns the identifier (as a string) that this is a comment for. + + Note that this uses method uses GetIdentifierForToken to get the full + identifier, even if broken up by whitespace, newlines, or comments, + and thus could be longer than GetTargetToken().string. + + Returns: + The identifier for the token this comment is for. + """ + token = self.GetTargetToken() + if token: + return tokenutil.GetIdentifierForToken(token) + + def GetTargetToken(self): + """Get this comment's target token. + + Returns: + The token that is the target of this comment, or None if there isn't one. + """ + + # File overviews describe the file, not a token. + if self.HasFlag('fileoverview'): + return + + skip_types = frozenset([ + Type.WHITESPACE, + Type.BLANK_LINE, + Type.START_PAREN]) + + target_types = frozenset([ + Type.FUNCTION_NAME, + Type.IDENTIFIER, + Type.SIMPLE_LVALUE]) + + token = self.end_token.next + while token: + if token.type in target_types: + return token + + # Handles the case of a comment on "var foo = ...' + if token.IsKeyword('var'): + next_code_token = tokenutil.CustomSearch( + token, + lambda t: t.type not in Type.NON_CODE_TYPES) + + if (next_code_token and + next_code_token.IsType(Type.SIMPLE_LVALUE)): + return next_code_token + + return + + # Handles the case of a comment on "function foo () {}" + if token.type is Type.FUNCTION_DECLARATION: + next_code_token = tokenutil.CustomSearch( + token, + lambda t: t.type not in Type.NON_CODE_TYPES) + + if next_code_token.IsType(Type.FUNCTION_NAME): + return next_code_token + + return + + # Skip types will end the search. + if token.type not in skip_types: + return + + token = token.next + + def CompareParameters(self, params): + """Computes the edit distance and list from the function params to the docs. + + Uses the Levenshtein edit distance algorithm, with code modified from + http://en.wikibooks.org/wiki/Algorithm_implementation/Strings/Levenshtein_distance#Python + + Args: + params: The parameter list for the function declaration. + + Returns: + The edit distance, the edit list. + """ + source_len, target_len = len(self.ordered_params), len(params) + edit_lists = [[]] + distance = [[]] + for i in range(target_len+1): + edit_lists[0].append(['I'] * i) + distance[0].append(i) + + for j in range(1, source_len+1): + edit_lists.append([['D'] * j]) + distance.append([j]) + + for i in range(source_len): + for j in range(target_len): + cost = 1 + if self.ordered_params[i] == params[j]: + cost = 0 + + deletion = distance[i][j+1] + 1 + insertion = distance[i+1][j] + 1 + substitution = distance[i][j] + cost + + edit_list = None + best = None + if deletion <= insertion and deletion <= substitution: + # Deletion is best. + best = deletion + edit_list = list(edit_lists[i][j+1]) + edit_list.append('D') + + elif insertion <= substitution: + # Insertion is best. + best = insertion + edit_list = list(edit_lists[i+1][j]) + edit_list.append('I') + edit_lists[i+1].append(edit_list) + + else: + # Substitution is best. + best = substitution + edit_list = list(edit_lists[i][j]) + if cost: + edit_list.append('S') + else: + edit_list.append('=') + + edit_lists[i+1].append(edit_list) + distance[i+1].append(best) + + return distance[source_len][target_len], edit_lists[source_len][target_len] + + def __repr__(self): + """Returns a string representation of this object. + + Returns: + A string representation of this object. + """ + return '' % ( + str(self.ordered_params), str(self.__flags)) + + +# +# Helper methods used by DocFlag and DocComment to parse out flag information. +# + + +def _GetMatchingEndBraceAndContents(start_brace): + """Returns the matching end brace and contents between the two braces. + + If any FLAG_ENDING_TYPE token is encountered before a matching end brace, then + that token is used as the matching ending token. Contents will have all + comment prefixes stripped out of them, and all comment prefixes in between the + start and end tokens will be split out into separate DOC_PREFIX tokens. + + Args: + start_brace: The DOC_START_BRACE token immediately before desired contents. + + Returns: + The matching ending token (DOC_END_BRACE or FLAG_ENDING_TYPE) and a string + of the contents between the matching tokens, minus any comment prefixes. + """ + open_count = 1 + close_count = 0 + contents = [] + + # We don't consider the start brace part of the type string. + token = start_brace.next + while open_count != close_count: + if token.type == Type.DOC_START_BRACE: + open_count += 1 + elif token.type == Type.DOC_END_BRACE: + close_count += 1 + + if token.type != Type.DOC_PREFIX: + contents.append(token.string) + + if token.type in Type.FLAG_ENDING_TYPES: + break + token = token.next + + #Don't include the end token (end brace, end doc comment, etc.) in type. + token = token.previous + contents = contents[:-1] + + return token, ''.join(contents) + + +def _GetNextPartialIdentifierToken(start_token): + """Returns the first token having identifier as substring after a token. + + Searches each token after the start to see if it contains an identifier. + If found, token is returned. If no identifier is found returns None. + Search is abandoned when a FLAG_ENDING_TYPE token is found. + + Args: + start_token: The token to start searching after. + + Returns: + The token found containing identifier, None otherwise. + """ + token = start_token.next + + while token and token.type not in Type.FLAG_ENDING_TYPES: + match = javascripttokenizer.JavaScriptTokenizer.IDENTIFIER.search( + token.string) + if match is not None and token.type == Type.COMMENT: + return token + + token = token.next + + return None + + +def _GetEndTokenAndContents(start_token): + """Returns last content token and all contents before FLAG_ENDING_TYPE token. + + Comment prefixes are split into DOC_PREFIX tokens and stripped from the + returned contents. + + Args: + start_token: The token immediately before the first content token. + + Returns: + The last content token and a string of all contents including start and + end tokens, with comment prefixes stripped. + """ + iterator = start_token + last_line = iterator.line_number + last_token = None + contents = '' + doc_depth = 0 + while not iterator.type in Type.FLAG_ENDING_TYPES or doc_depth > 0: + if (iterator.IsFirstInLine() and + DocFlag.EMPTY_COMMENT_LINE.match(iterator.line)): + # If we have a blank comment line, consider that an implicit + # ending of the description. This handles a case like: + # + # * @return {boolean} True + # * + # * Note: This is a sentence. + # + # The note is not part of the @return description, but there was + # no definitive ending token. Rather there was a line containing + # only a doc comment prefix or whitespace. + break + + # b/2983692 + # don't prematurely match against a @flag if inside a doc flag + # need to think about what is the correct behavior for unterminated + # inline doc flags + if (iterator.type == Type.DOC_START_BRACE and + iterator.next.type == Type.DOC_INLINE_FLAG): + doc_depth += 1 + elif (iterator.type == Type.DOC_END_BRACE and + doc_depth > 0): + doc_depth -= 1 + + if iterator.type in Type.FLAG_DESCRIPTION_TYPES: + contents += iterator.string + last_token = iterator + + iterator = iterator.next + if iterator.line_number != last_line: + contents += '\n' + last_line = iterator.line_number + + end_token = last_token + if DocFlag.EMPTY_STRING.match(contents): + contents = None + else: + # Strip trailing newline. + contents = contents[:-1] + + return end_token, contents + + +class Function(object): + """Data about a JavaScript function. + + Attributes: + block_depth: Block depth the function began at. + doc: The DocComment associated with the function. + has_return: If the function has a return value. + has_this: If the function references the 'this' object. + is_assigned: If the function is part of an assignment. + is_constructor: If the function is a constructor. + name: The name of the function, whether given in the function keyword or + as the lvalue the function is assigned to. + start_token: First token of the function (the function' keyword token). + end_token: Last token of the function (the closing '}' token). + parameters: List of parameter names. + """ + + def __init__(self, block_depth, is_assigned, doc, name): + self.block_depth = block_depth + self.is_assigned = is_assigned + self.is_constructor = doc and doc.HasFlag('constructor') + self.is_interface = doc and doc.HasFlag('interface') + self.has_return = False + self.has_throw = False + self.has_this = False + self.name = name + self.doc = doc + self.start_token = None + self.end_token = None + self.parameters = None + + +class StateTracker(object): + """EcmaScript state tracker. + + Tracks block depth, function names, etc. within an EcmaScript token stream. + """ + + OBJECT_LITERAL = 'o' + CODE = 'c' + + def __init__(self, doc_flag=DocFlag): + """Initializes a JavaScript token stream state tracker. + + Args: + doc_flag: An optional custom DocFlag used for validating + documentation flags. + """ + self._doc_flag = doc_flag + self.Reset() + + def Reset(self): + """Resets the state tracker to prepare for processing a new page.""" + self._block_depth = 0 + self._is_block_close = False + self._paren_depth = 0 + self._function_stack = [] + self._functions_by_name = {} + self._last_comment = None + self._doc_comment = None + self._cumulative_params = None + self._block_types = [] + self._last_non_space_token = None + self._last_line = None + self._first_token = None + self._documented_identifiers = set() + self._variables_in_scope = [] + + def InFunction(self): + """Returns true if the current token is within a function. + + Returns: + True if the current token is within a function. + """ + return bool(self._function_stack) + + def InConstructor(self): + """Returns true if the current token is within a constructor. + + Returns: + True if the current token is within a constructor. + """ + return self.InFunction() and self._function_stack[-1].is_constructor + + def InInterfaceMethod(self): + """Returns true if the current token is within an interface method. + + Returns: + True if the current token is within an interface method. + """ + if self.InFunction(): + if self._function_stack[-1].is_interface: + return True + else: + name = self._function_stack[-1].name + prototype_index = name.find('.prototype.') + if prototype_index != -1: + class_function_name = name[0:prototype_index] + if (class_function_name in self._functions_by_name and + self._functions_by_name[class_function_name].is_interface): + return True + + return False + + def InTopLevelFunction(self): + """Returns true if the current token is within a top level function. + + Returns: + True if the current token is within a top level function. + """ + return len(self._function_stack) == 1 and self.InTopLevel() + + def InAssignedFunction(self): + """Returns true if the current token is within a function variable. + + Returns: + True if if the current token is within a function variable + """ + return self.InFunction() and self._function_stack[-1].is_assigned + + def IsFunctionOpen(self): + """Returns true if the current token is a function block open. + + Returns: + True if the current token is a function block open. + """ + return (self._function_stack and + self._function_stack[-1].block_depth == self._block_depth - 1) + + def IsFunctionClose(self): + """Returns true if the current token is a function block close. + + Returns: + True if the current token is a function block close. + """ + return (self._function_stack and + self._function_stack[-1].block_depth == self._block_depth) + + def InBlock(self): + """Returns true if the current token is within a block. + + Returns: + True if the current token is within a block. + """ + return bool(self._block_depth) + + def IsBlockClose(self): + """Returns true if the current token is a block close. + + Returns: + True if the current token is a block close. + """ + return self._is_block_close + + def InObjectLiteral(self): + """Returns true if the current token is within an object literal. + + Returns: + True if the current token is within an object literal. + """ + return self._block_depth and self._block_types[-1] == self.OBJECT_LITERAL + + def InObjectLiteralDescendant(self): + """Returns true if the current token has an object literal ancestor. + + Returns: + True if the current token has an object literal ancestor. + """ + return self.OBJECT_LITERAL in self._block_types + + def InParentheses(self): + """Returns true if the current token is within parentheses. + + Returns: + True if the current token is within parentheses. + """ + return bool(self._paren_depth) + + def ParenthesesDepth(self): + """Returns the number of parens surrounding the token. + + Returns: + The number of parenthesis surrounding the token. + """ + return self._paren_depth + + def BlockDepth(self): + """Returns the number of blocks in which the token is nested. + + Returns: + The number of blocks in which the token is nested. + """ + return self._block_depth + + def FunctionDepth(self): + """Returns the number of functions in which the token is nested. + + Returns: + The number of functions in which the token is nested. + """ + return len(self._function_stack) + + def InTopLevel(self): + """Whether we are at the top level in the class. + + This function call is language specific. In some languages like + JavaScript, a function is top level if it is not inside any parenthesis. + In languages such as ActionScript, a function is top level if it is directly + within a class. + """ + raise TypeError('Abstract method InTopLevel not implemented') + + def GetBlockType(self, token): + """Determine the block type given a START_BLOCK token. + + Code blocks come after parameters, keywords like else, and closing parens. + + Args: + token: The current token. Can be assumed to be type START_BLOCK. + Returns: + Code block type for current token. + """ + raise TypeError('Abstract method GetBlockType not implemented') + + def GetParams(self): + """Returns the accumulated input params as an array. + + In some EcmasSript languages, input params are specified like + (param:Type, param2:Type2, ...) + in other they are specified just as + (param, param2) + We handle both formats for specifying parameters here and leave + it to the compilers for each language to detect compile errors. + This allows more code to be reused between lint checkers for various + EcmaScript languages. + + Returns: + The accumulated input params as an array. + """ + params = [] + if self._cumulative_params: + params = re.compile(r'\s+').sub('', self._cumulative_params).split(',') + # Strip out the type from parameters of the form name:Type. + params = map(lambda param: param.split(':')[0], params) + + return params + + def GetLastComment(self): + """Return the last plain comment that could be used as documentation. + + Returns: + The last plain comment that could be used as documentation. + """ + return self._last_comment + + def GetDocComment(self): + """Return the most recent applicable documentation comment. + + Returns: + The last applicable documentation comment. + """ + return self._doc_comment + + def HasDocComment(self, identifier): + """Returns whether the identifier has been documented yet. + + Args: + identifier: The identifier. + + Returns: + Whether the identifier has been documented yet. + """ + return identifier in self._documented_identifiers + + def InDocComment(self): + """Returns whether the current token is in a doc comment. + + Returns: + Whether the current token is in a doc comment. + """ + return self._doc_comment and self._doc_comment.end_token is None + + def GetDocFlag(self): + """Returns the current documentation flags. + + Returns: + The current documentation flags. + """ + return self._doc_flag + + def IsTypeToken(self, t): + if self.InDocComment() and t.type not in (Type.START_DOC_COMMENT, + Type.DOC_FLAG, Type.DOC_INLINE_FLAG, Type.DOC_PREFIX): + f = tokenutil.SearchUntil(t, [Type.DOC_FLAG], [Type.START_DOC_COMMENT], + None, True) + if (f and f.attached_object.type_start_token is not None and + f.attached_object.type_end_token is not None): + return (tokenutil.Compare(t, f.attached_object.type_start_token) > 0 and + tokenutil.Compare(t, f.attached_object.type_end_token) < 0) + return False + + def GetFunction(self): + """Return the function the current code block is a part of. + + Returns: + The current Function object. + """ + if self._function_stack: + return self._function_stack[-1] + + def GetBlockDepth(self): + """Return the block depth. + + Returns: + The current block depth. + """ + return self._block_depth + + def GetLastNonSpaceToken(self): + """Return the last non whitespace token.""" + return self._last_non_space_token + + def GetLastLine(self): + """Return the last line.""" + return self._last_line + + def GetFirstToken(self): + """Return the very first token in the file.""" + return self._first_token + + def IsVariableInScope(self, token_string): + """Checks if string is variable in current scope. + + For given string it checks whether the string is a defined variable + (including function param) in current state. + + E.g. if variables defined (variables in current scope) is docs + then docs, docs.length etc will be considered as variable in current + scope. This will help in avoding extra goog.require for variables. + + Args: + token_string: String to check if its is a variable in current scope. + + Returns: + true if given string is a variable in current scope. + """ + for variable in self._variables_in_scope: + if (token_string == variable + or token_string.startswith(variable + '.')): + return True + + return False + + def HandleToken(self, token, last_non_space_token): + """Handles the given token and updates state. + + Args: + token: The token to handle. + last_non_space_token: + """ + self._is_block_close = False + + if not self._first_token: + self._first_token = token + + # Track block depth. + type = token.type + if type == Type.START_BLOCK: + self._block_depth += 1 + + # Subclasses need to handle block start very differently because + # whether a block is a CODE or OBJECT_LITERAL block varies significantly + # by language. + self._block_types.append(self.GetBlockType(token)) + + # When entering a function body, record its parameters. + if self.InFunction(): + function = self._function_stack[-1] + if self._block_depth == function.block_depth + 1: + function.parameters = self.GetParams() + + # Track block depth. + elif type == Type.END_BLOCK: + self._is_block_close = not self.InObjectLiteral() + self._block_depth -= 1 + self._block_types.pop() + + # Track parentheses depth. + elif type == Type.START_PAREN: + self._paren_depth += 1 + + # Track parentheses depth. + elif type == Type.END_PAREN: + self._paren_depth -= 1 + + elif type == Type.COMMENT: + self._last_comment = token.string + + elif type == Type.START_DOC_COMMENT: + self._last_comment = None + self._doc_comment = DocComment(token) + + elif type == Type.END_DOC_COMMENT: + self._doc_comment.end_token = token + + elif type in (Type.DOC_FLAG, Type.DOC_INLINE_FLAG): + flag = self._doc_flag(token) + token.attached_object = flag + self._doc_comment.AddFlag(flag) + + if flag.flag_type == 'suppress': + self._doc_comment.AddSuppression(token) + + elif type == Type.FUNCTION_DECLARATION: + last_code = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES, None, + True) + doc = None + # Only functions outside of parens are eligible for documentation. + if not self._paren_depth: + doc = self._doc_comment + + name = '' + is_assigned = last_code and (last_code.IsOperator('=') or + last_code.IsOperator('||') or last_code.IsOperator('&&') or + (last_code.IsOperator(':') and not self.InObjectLiteral())) + if is_assigned: + # TODO(robbyw): This breaks for x[2] = ... + # Must use loop to find full function name in the case of line-wrapped + # declarations (bug 1220601) like: + # my.function.foo. + # bar = function() ... + identifier = tokenutil.Search(last_code, Type.SIMPLE_LVALUE, None, True) + while identifier and identifier.type in ( + Type.IDENTIFIER, Type.SIMPLE_LVALUE): + name = identifier.string + name + # Traverse behind us, skipping whitespace and comments. + while True: + identifier = identifier.previous + if not identifier or not identifier.type in Type.NON_CODE_TYPES: + break + + else: + next_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES) + while next_token and next_token.IsType(Type.FUNCTION_NAME): + name += next_token.string + next_token = tokenutil.Search(next_token, Type.FUNCTION_NAME, 2) + + function = Function(self._block_depth, is_assigned, doc, name) + function.start_token = token + + self._function_stack.append(function) + self._functions_by_name[name] = function + + # Add a delimiter in stack for scope variables to define start of + # function. This helps in popping variables of this function when + # function declaration ends. + self._variables_in_scope.append('') + + elif type == Type.START_PARAMETERS: + self._cumulative_params = '' + + elif type == Type.PARAMETERS: + self._cumulative_params += token.string + self._variables_in_scope.extend(self.GetParams()) + + elif type == Type.KEYWORD and token.string == 'return': + next_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES) + if not next_token.IsType(Type.SEMICOLON): + function = self.GetFunction() + if function: + function.has_return = True + + elif type == Type.KEYWORD and token.string == 'throw': + function = self.GetFunction() + if function: + function.has_throw = True + + elif type == Type.KEYWORD and token.string == 'var': + function = self.GetFunction() + next_token = tokenutil.Search(token, [Type.IDENTIFIER, + Type.SIMPLE_LVALUE]) + + if next_token: + if next_token.type == Type.SIMPLE_LVALUE: + self._variables_in_scope.append(next_token.values['identifier']) + else: + self._variables_in_scope.append(next_token.string) + + elif type == Type.SIMPLE_LVALUE: + identifier = token.values['identifier'] + jsdoc = self.GetDocComment() + if jsdoc: + self._documented_identifiers.add(identifier) + + self._HandleIdentifier(identifier, True) + + elif type == Type.IDENTIFIER: + self._HandleIdentifier(token.string, False) + + # Detect documented non-assignments. + next_token = tokenutil.SearchExcept(token, Type.NON_CODE_TYPES) + if next_token and next_token.IsType(Type.SEMICOLON): + if (self._last_non_space_token and + self._last_non_space_token.IsType(Type.END_DOC_COMMENT)): + self._documented_identifiers.add(token.string) + + def _HandleIdentifier(self, identifier, is_assignment): + """Process the given identifier. + + Currently checks if it references 'this' and annotates the function + accordingly. + + Args: + identifier: The identifer to process. + is_assignment: Whether the identifer is being written to. + """ + if identifier == 'this' or identifier.startswith('this.'): + function = self.GetFunction() + if function: + function.has_this = True + + def HandleAfterToken(self, token): + """Handle updating state after a token has been checked. + + This function should be used for destructive state changes such as + deleting a tracked object. + + Args: + token: The token to handle. + """ + type = token.type + if type == Type.SEMICOLON or type == Type.END_PAREN or ( + type == Type.END_BRACKET and + self._last_non_space_token.type not in ( + Type.SINGLE_QUOTE_STRING_END, Type.DOUBLE_QUOTE_STRING_END)): + # We end on any numeric array index, but keep going for string based + # array indices so that we pick up manually exported identifiers. + self._doc_comment = None + self._last_comment = None + + elif type == Type.END_BLOCK: + self._doc_comment = None + self._last_comment = None + + if self.InFunction() and self.IsFunctionClose(): + # TODO(robbyw): Detect the function's name for better errors. + function = self._function_stack.pop() + function.end_token = token + + # Pop all variables till delimiter ('') those were defined in the + # function being closed so make them out of scope. + while self._variables_in_scope and self._variables_in_scope[-1]: + self._variables_in_scope.pop() + + # Pop delimiter + if self._variables_in_scope: + self._variables_in_scope.pop() + + elif type == Type.END_PARAMETERS and self._doc_comment: + self._doc_comment = None + self._last_comment = None + + if not token.IsAnyType(Type.WHITESPACE, Type.BLANK_LINE): + self._last_non_space_token = token + + self._last_line = token.line diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/statetracker_test.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/statetracker_test.py new file mode 100755 index 0000000000..1ec9e0bb4f --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/statetracker_test.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +# +# Copyright 2012 The Closure Linter Authors. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for the statetracker module.""" + +# Allow non-Google copyright +# pylint: disable=g-bad-file-header + +__author__ = ('nnaze@google.com (Nathan Naze)') + + + +import unittest as googletest + +from closure_linter import javascripttokens +from closure_linter import statetracker +from closure_linter import testutil + + +class _FakeDocFlag(object): + + def __repr__(self): + return '@%s %s' % (self.flag_type, self.name) + + +class IdentifierTest(googletest.TestCase): + + def testJustIdentifier(self): + a = javascripttokens.JavaScriptToken( + 'abc', javascripttokens.JavaScriptTokenType.IDENTIFIER, 'abc', 1) + + st = statetracker.StateTracker() + st.HandleToken(a, None) + + +class DocCommentTest(googletest.TestCase): + + @staticmethod + def _MakeDocFlagFake(flag_type, name=None): + flag = _FakeDocFlag() + flag.flag_type = flag_type + flag.name = name + return flag + + def testDocFlags(self): + comment = statetracker.DocComment(None) + + a = self._MakeDocFlagFake('param', 'foo') + comment.AddFlag(a) + + b = self._MakeDocFlagFake('param', '') + comment.AddFlag(b) + + c = self._MakeDocFlagFake('param', 'bar') + comment.AddFlag(c) + + self.assertEquals( + ['foo', 'bar'], + comment.ordered_params) + + self.assertEquals( + [a, b, c], + comment.GetDocFlags()) + + def testInvalidate(self): + comment = statetracker.DocComment(None) + + self.assertFalse(comment.invalidated) + self.assertFalse(comment.IsInvalidated()) + + comment.Invalidate() + + self.assertTrue(comment.invalidated) + self.assertTrue(comment.IsInvalidated()) + + def testSuppressionOnly(self): + comment = statetracker.DocComment(None) + + self.assertFalse(comment.SuppressionOnly()) + comment.AddFlag(self._MakeDocFlagFake('suppress')) + self.assertTrue(comment.SuppressionOnly()) + comment.AddFlag(self._MakeDocFlagFake('foo')) + self.assertFalse(comment.SuppressionOnly()) + + def testRepr(self): + comment = statetracker.DocComment(None) + comment.AddFlag(self._MakeDocFlagFake('param', 'foo')) + comment.AddFlag(self._MakeDocFlagFake('param', 'bar')) + + self.assertEquals( + '', + repr(comment)) + + def testDocFlagParam(self): + comment = self._ParseComment(""" + /** + * @param {string} [name] Name of customer. + */""") + flag = comment.GetFlag('param') + self.assertEquals('string', flag.type) + self.assertEquals('[name]', flag.name) + + def _ParseComment(self, script): + """Parse a script that contains one comment and return it.""" + _, comments = testutil.ParseFunctionsAndComments(script) + self.assertEquals(1, len(comments)) + return comments[0] + +if __name__ == '__main__': + googletest.main() diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/strict_test.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/strict_test.py new file mode 100755 index 0000000000..75044e8a20 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/strict_test.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# Copyright 2013 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for gjslint --strict. + +Tests errors that can be thrown by gjslint when in strict mode. +""" + + + +import unittest + +import gflags as flags +import unittest as googletest + +from closure_linter import errors +from closure_linter import runner +from closure_linter.common import erroraccumulator + +flags.FLAGS.strict = True + + +class StrictTest(unittest.TestCase): + """Tests scenarios where strict generates warnings.""" + + def testUnclosedString(self): + """Tests warnings are reported when nothing is disabled. + + b/11450054. + """ + original = [ + 'bug = function() {', + ' (\'foo\'\');', + '};', + '', + ] + + expected = [errors.FILE_DOES_NOT_PARSE, errors.MULTI_LINE_STRING, + errors.FILE_IN_BLOCK] + self._AssertErrors(original, expected) + + def _AssertErrors(self, original, expected_errors): + """Asserts that the error fixer corrects original to expected.""" + + # Trap gjslint's output parse it to get messages added. + error_accumulator = erroraccumulator.ErrorAccumulator() + runner.Run('testing.js', error_accumulator, source=original) + error_nums = [e.code for e in error_accumulator.GetErrors()] + + error_nums.sort() + expected_errors.sort() + self.assertListEqual(error_nums, expected_errors) + +if __name__ == '__main__': + googletest.main() + diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/testutil.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/testutil.py new file mode 100644 index 0000000000..b106ff0258 --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/testutil.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# Copyright 2012 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Utility functions for testing gjslint components.""" + +# Allow non-Google copyright +# pylint: disable=g-bad-file-header + +__author__ = ('nnaze@google.com (Nathan Naze)') + +import StringIO + +from closure_linter import ecmametadatapass +from closure_linter import javascriptstatetracker +from closure_linter import javascripttokenizer + + +def TokenizeSource(source): + """Convert a source into a string of tokens. + + Args: + source: A source file as a string or file-like object (iterates lines). + + Returns: + The first token of the resulting token stream. + """ + + if isinstance(source, basestring): + source = StringIO.StringIO(source) + + tokenizer = javascripttokenizer.JavaScriptTokenizer() + return tokenizer.TokenizeFile(source) + + +def TokenizeSourceAndRunEcmaPass(source): + """Tokenize a source and run the EcmaMetaDataPass on it. + + Args: + source: A source file as a string or file-like object (iterates lines). + + Returns: + The first token of the resulting token stream. + """ + start_token = TokenizeSource(source) + ecma_pass = ecmametadatapass.EcmaMetaDataPass() + ecma_pass.Process(start_token) + return start_token + + +def ParseFunctionsAndComments(source): + """Run the tokenizer and tracker and return comments and functions found. + + Args: + source: A source file as a string or file-like object (iterates lines). + + Returns: + The functions and comments as a tuple. + """ + start_token = TokenizeSourceAndRunEcmaPass(source) + + tracker = javascriptstatetracker.JavaScriptStateTracker() + + functions = [] + comments = [] + for token in start_token: + tracker.HandleToken(token, tracker.GetLastNonSpaceToken()) + + function = tracker.GetFunction() + if function and function not in functions: + functions.append(function) + + comment = tracker.GetDocComment() + if comment and comment not in comments: + comments.append(comment) + + tracker.HandleAfterToken(token) + + return functions, comments diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/tokenutil.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/tokenutil.py new file mode 100755 index 0000000000..8b5dbe1a9a --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/tokenutil.py @@ -0,0 +1,694 @@ +#!/usr/bin/env python +# +# Copyright 2007 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Token utility functions.""" + +__author__ = ('robbyw@google.com (Robert Walker)', + 'ajp@google.com (Andy Perelson)') + +import copy +import StringIO + +from closure_linter.common import tokens +from closure_linter.javascripttokens import JavaScriptToken +from closure_linter.javascripttokens import JavaScriptTokenType + +# Shorthand +Type = tokens.TokenType + + +def GetFirstTokenInSameLine(token): + """Returns the first token in the same line as token. + + Args: + token: Any token in the line. + + Returns: + The first token in the same line as token. + """ + while not token.IsFirstInLine(): + token = token.previous + return token + + +def GetFirstTokenInPreviousLine(token): + """Returns the first token in the previous line as token. + + Args: + token: Any token in the line. + + Returns: + The first token in the previous line as token, or None if token is on the + first line. + """ + first_in_line = GetFirstTokenInSameLine(token) + if first_in_line.previous: + return GetFirstTokenInSameLine(first_in_line.previous) + + return None + + +def GetLastTokenInSameLine(token): + """Returns the last token in the same line as token. + + Args: + token: Any token in the line. + + Returns: + The last token in the same line as token. + """ + while not token.IsLastInLine(): + token = token.next + return token + + +def GetAllTokensInSameLine(token): + """Returns all tokens in the same line as the given token. + + Args: + token: Any token in the line. + + Returns: + All tokens on the same line as the given token. + """ + first_token = GetFirstTokenInSameLine(token) + last_token = GetLastTokenInSameLine(token) + + tokens_in_line = [] + while first_token != last_token: + tokens_in_line.append(first_token) + first_token = first_token.next + tokens_in_line.append(last_token) + + return tokens_in_line + + +def CustomSearch(start_token, func, end_func=None, distance=None, + reverse=False): + """Returns the first token where func is True within distance of this token. + + Args: + start_token: The token to start searching from + func: The function to call to test a token for applicability + end_func: The function to call to test a token to determine whether to abort + the search. + distance: The number of tokens to look through before failing search. Must + be positive. If unspecified, will search until the end of the token + chain + reverse: When true, search the tokens before this one instead of the tokens + after it + + Returns: + The first token matching func within distance of this token, or None if no + such token is found. + """ + token = start_token + if reverse: + while token and (distance is None or distance > 0): + previous = token.previous + if previous: + if func(previous): + return previous + if end_func and end_func(previous): + return None + + token = previous + if distance is not None: + distance -= 1 + + else: + while token and (distance is None or distance > 0): + next_token = token.next + if next_token: + if func(next_token): + return next_token + if end_func and end_func(next_token): + return None + + token = next_token + if distance is not None: + distance -= 1 + + return None + + +def Search(start_token, token_types, distance=None, reverse=False): + """Returns the first token of type in token_types within distance. + + Args: + start_token: The token to start searching from + token_types: The allowable types of the token being searched for + distance: The number of tokens to look through before failing search. Must + be positive. If unspecified, will search until the end of the token + chain + reverse: When true, search the tokens before this one instead of the tokens + after it + + Returns: + The first token of any type in token_types within distance of this token, or + None if no such token is found. + """ + return CustomSearch(start_token, lambda token: token.IsAnyType(token_types), + None, distance, reverse) + + +def SearchExcept(start_token, token_types, distance=None, reverse=False): + """Returns the first token not of any type in token_types within distance. + + Args: + start_token: The token to start searching from + token_types: The unallowable types of the token being searched for + distance: The number of tokens to look through before failing search. Must + be positive. If unspecified, will search until the end of the token + chain + reverse: When true, search the tokens before this one instead of the tokens + after it + + Returns: + The first token of any type in token_types within distance of this token, or + None if no such token is found. + """ + return CustomSearch(start_token, + lambda token: not token.IsAnyType(token_types), + None, distance, reverse) + + +def SearchUntil(start_token, token_types, end_types, distance=None, + reverse=False): + """Returns the first token of type in token_types before a token of end_type. + + Args: + start_token: The token to start searching from. + token_types: The allowable types of the token being searched for. + end_types: Types of tokens to abort search if we find. + distance: The number of tokens to look through before failing search. Must + be positive. If unspecified, will search until the end of the token + chain + reverse: When true, search the tokens before this one instead of the tokens + after it + + Returns: + The first token of any type in token_types within distance of this token + before any tokens of type in end_type, or None if no such token is found. + """ + return CustomSearch(start_token, lambda token: token.IsAnyType(token_types), + lambda token: token.IsAnyType(end_types), + distance, reverse) + + +def DeleteToken(token): + """Deletes the given token from the linked list. + + Args: + token: The token to delete + """ + # When deleting a token, we do not update the deleted token itself to make + # sure the previous and next pointers are still pointing to tokens which are + # not deleted. Also it is very hard to keep track of all previously deleted + # tokens to update them when their pointers become invalid. So we add this + # flag that any token linked list iteration logic can skip deleted node safely + # when its current token is deleted. + token.is_deleted = True + if token.previous: + token.previous.next = token.next + + if token.next: + token.next.previous = token.previous + + following_token = token.next + while following_token and following_token.metadata.last_code == token: + following_token.metadata.last_code = token.metadata.last_code + following_token = following_token.next + + +def DeleteTokens(token, token_count): + """Deletes the given number of tokens starting with the given token. + + Args: + token: The token to start deleting at. + token_count: The total number of tokens to delete. + """ + for i in xrange(1, token_count): + DeleteToken(token.next) + DeleteToken(token) + + +def InsertTokenBefore(new_token, token): + """Insert new_token before token. + + Args: + new_token: A token to be added to the stream + token: A token already in the stream + """ + new_token.next = token + new_token.previous = token.previous + + new_token.metadata = copy.copy(token.metadata) + + if new_token.IsCode(): + old_last_code = token.metadata.last_code + following_token = token + while (following_token and + following_token.metadata.last_code == old_last_code): + following_token.metadata.last_code = new_token + following_token = following_token.next + + token.previous = new_token + if new_token.previous: + new_token.previous.next = new_token + + if new_token.start_index is None: + if new_token.line_number == token.line_number: + new_token.start_index = token.start_index + else: + previous_token = new_token.previous + if previous_token: + new_token.start_index = (previous_token.start_index + + len(previous_token.string)) + else: + new_token.start_index = 0 + + iterator = new_token.next + while iterator and iterator.line_number == new_token.line_number: + iterator.start_index += len(new_token.string) + iterator = iterator.next + + +def InsertTokenAfter(new_token, token): + """Insert new_token after token. + + Args: + new_token: A token to be added to the stream + token: A token already in the stream + """ + new_token.previous = token + new_token.next = token.next + + new_token.metadata = copy.copy(token.metadata) + + if token.IsCode(): + new_token.metadata.last_code = token + + if new_token.IsCode(): + following_token = token.next + while following_token and following_token.metadata.last_code == token: + following_token.metadata.last_code = new_token + following_token = following_token.next + + token.next = new_token + if new_token.next: + new_token.next.previous = new_token + + if new_token.start_index is None: + if new_token.line_number == token.line_number: + new_token.start_index = token.start_index + len(token.string) + else: + new_token.start_index = 0 + + iterator = new_token.next + while iterator and iterator.line_number == new_token.line_number: + iterator.start_index += len(new_token.string) + iterator = iterator.next + + +def InsertTokensAfter(new_tokens, token): + """Insert multiple tokens after token. + + Args: + new_tokens: An array of tokens to be added to the stream + token: A token already in the stream + """ + # TODO(user): It would be nicer to have InsertTokenAfter defer to here + # instead of vice-versa. + current_token = token + for new_token in new_tokens: + InsertTokenAfter(new_token, current_token) + current_token = new_token + + +def InsertSpaceTokenAfter(token): + """Inserts a space token after the given token. + + Args: + token: The token to insert a space token after + + Returns: + A single space token + """ + space_token = JavaScriptToken(' ', Type.WHITESPACE, token.line, + token.line_number) + InsertTokenAfter(space_token, token) + + +def InsertBlankLineAfter(token): + """Inserts a blank line after the given token. + + Args: + token: The token to insert a blank line after + + Returns: + A single space token + """ + blank_token = JavaScriptToken('', Type.BLANK_LINE, '', + token.line_number + 1) + InsertLineAfter(token, [blank_token]) + + +def InsertLineAfter(token, new_tokens): + """Inserts a new line consisting of new_tokens after the given token. + + Args: + token: The token to insert after. + new_tokens: The tokens that will make up the new line. + """ + insert_location = token + for new_token in new_tokens: + InsertTokenAfter(new_token, insert_location) + insert_location = new_token + + # Update all subsequent line numbers. + next_token = new_tokens[-1].next + while next_token: + next_token.line_number += 1 + next_token = next_token.next + + +def SplitToken(token, position): + """Splits the token into two tokens at position. + + Args: + token: The token to split + position: The position to split at. Will be the beginning of second token. + + Returns: + The new second token. + """ + new_string = token.string[position:] + token.string = token.string[:position] + + new_token = JavaScriptToken(new_string, token.type, token.line, + token.line_number) + InsertTokenAfter(new_token, token) + + return new_token + + +def Compare(token1, token2): + """Compares two tokens and determines their relative order. + + Args: + token1: The first token to compare. + token2: The second token to compare. + + Returns: + A negative integer, zero, or a positive integer as the first token is + before, equal, or after the second in the token stream. + """ + if token2.line_number != token1.line_number: + return token1.line_number - token2.line_number + else: + return token1.start_index - token2.start_index + + +def GoogScopeOrNoneFromStartBlock(token): + """Determines if the given START_BLOCK is part of a goog.scope statement. + + Args: + token: A token of type START_BLOCK. + + Returns: + The goog.scope function call token, or None if such call doesn't exist. + """ + if token.type != JavaScriptTokenType.START_BLOCK: + return None + + # Search for a goog.scope statement, which will be 5 tokens before the + # block. Illustration of the tokens found prior to the start block: + # goog.scope(function() { + # 5 4 3 21 ^ + + maybe_goog_scope = token + for unused_i in xrange(5): + maybe_goog_scope = (maybe_goog_scope.previous if maybe_goog_scope and + maybe_goog_scope.previous else None) + if maybe_goog_scope and maybe_goog_scope.string == 'goog.scope': + return maybe_goog_scope + + +def GetTokenRange(start_token, end_token): + """Returns a list of tokens between the two given, inclusive. + + Args: + start_token: Start token in the range. + end_token: End token in the range. + + Returns: + A list of tokens, in order, from start_token to end_token (including start + and end). Returns none if the tokens do not describe a valid range. + """ + + token_range = [] + token = start_token + + while token: + token_range.append(token) + + if token == end_token: + return token_range + + token = token.next + + +def TokensToString(token_iterable): + """Convert a number of tokens into a string. + + Newlines will be inserted whenever the line_number of two neighboring + strings differ. + + Args: + token_iterable: The tokens to turn to a string. + + Returns: + A string representation of the given tokens. + """ + + buf = StringIO.StringIO() + token_list = list(token_iterable) + if not token_list: + return '' + + line_number = token_list[0].line_number + + for token in token_list: + + while line_number < token.line_number: + line_number += 1 + buf.write('\n') + + if line_number > token.line_number: + line_number = token.line_number + buf.write('\n') + + buf.write(token.string) + + return buf.getvalue() + + +def GetPreviousCodeToken(token): + """Returns the code token before the specified token. + + Args: + token: A token. + + Returns: + The code token before the specified token or None if no such token + exists. + """ + + return CustomSearch( + token, + lambda t: t and t.type not in JavaScriptTokenType.NON_CODE_TYPES, + reverse=True) + + +def GetNextCodeToken(token): + """Returns the next code token after the specified token. + + Args: + token: A token. + + Returns: + The next code token after the specified token or None if no such token + exists. + """ + + return CustomSearch( + token, + lambda t: t and t.type not in JavaScriptTokenType.NON_CODE_TYPES, + reverse=False) + + +def GetIdentifierStart(token): + """Returns the first token in an identifier. + + Given a token which is part of an identifier, returns the token at the start + of the identifier. + + Args: + token: A token which is part of an identifier. + + Returns: + The token at the start of the identifier or None if the identifier was not + of the form 'a.b.c' (e.g. "['a']['b'].c"). + """ + + start_token = token + previous_code_token = GetPreviousCodeToken(token) + + while (previous_code_token and ( + previous_code_token.IsType(JavaScriptTokenType.IDENTIFIER) or + _IsDot(previous_code_token))): + start_token = previous_code_token + previous_code_token = GetPreviousCodeToken(previous_code_token) + + if _IsDot(start_token): + return None + + return start_token + + +def GetIdentifierForToken(token): + """Get the symbol specified by a token. + + Given a token, this function additionally concatenates any parts of an + identifying symbol being identified that are split by whitespace or a + newline. + + The function will return None if the token is not the first token of an + identifier. + + Args: + token: The first token of a symbol. + + Returns: + The whole symbol, as a string. + """ + + # Search backward to determine if this token is the first token of the + # identifier. If it is not the first token, return None to signal that this + # token should be ignored. + prev_token = token.previous + while prev_token: + if (prev_token.IsType(JavaScriptTokenType.IDENTIFIER) or + _IsDot(prev_token)): + return None + + if (prev_token.IsType(tokens.TokenType.WHITESPACE) or + prev_token.IsAnyType(JavaScriptTokenType.COMMENT_TYPES)): + prev_token = prev_token.previous + else: + break + + # A "function foo()" declaration. + if token.type is JavaScriptTokenType.FUNCTION_NAME: + return token.string + + # A "var foo" declaration (if the previous token is 'var') + previous_code_token = GetPreviousCodeToken(token) + + if previous_code_token and previous_code_token.IsKeyword('var'): + return token.string + + # Otherwise, this is potentially a namespaced (goog.foo.bar) identifier that + # could span multiple lines or be broken up by whitespace. We need + # to concatenate. + identifier_types = set([ + JavaScriptTokenType.IDENTIFIER, + JavaScriptTokenType.SIMPLE_LVALUE + ]) + + assert token.type in identifier_types + + # Start with the first token + symbol_tokens = [token] + + if token.next: + for t in token.next: + last_symbol_token = symbol_tokens[-1] + + # An identifier is part of the previous symbol if it has a trailing + # dot. + if t.type in identifier_types: + if last_symbol_token.string.endswith('.'): + symbol_tokens.append(t) + continue + else: + break + + # A dot is part of the previous symbol if it does not have a trailing + # dot. + if _IsDot(t): + if not last_symbol_token.string.endswith('.'): + symbol_tokens.append(t) + continue + else: + break + + # Skip any whitespace + if t.type in JavaScriptTokenType.NON_CODE_TYPES: + continue + + # This is the end of the identifier. Stop iterating. + break + + if symbol_tokens: + return ''.join([t.string for t in symbol_tokens]) + + +def GetStringAfterToken(token): + """Get string after token. + + Args: + token: Search will be done after this token. + + Returns: + String if found after token else None (empty string will also + return None). + + Search until end of string as in case of empty string Type.STRING_TEXT is not + present/found and don't want to return next string. + E.g. + a = ''; + b = 'test'; + When searching for string after 'a' if search is not limited by end of string + then it will return 'test' which is not desirable as there is a empty string + before that. + + This will return None for cases where string is empty or no string found + as in both cases there is no Type.STRING_TEXT. + """ + string_token = SearchUntil(token, JavaScriptTokenType.STRING_TEXT, + [JavaScriptTokenType.SINGLE_QUOTE_STRING_END, + JavaScriptTokenType.DOUBLE_QUOTE_STRING_END]) + if string_token: + return string_token.string + else: + return None + + +def _IsDot(token): + """Whether the token represents a "dot" operator (foo.bar).""" + return token.type is tokens.TokenType.NORMAL and token.string == '.' diff --git a/third_party/gjslint/closure_linter-2.3.13/closure_linter/tokenutil_test.py b/third_party/gjslint/closure_linter-2.3.13/closure_linter/tokenutil_test.py new file mode 100644 index 0000000000..d5af9fac4a --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/closure_linter/tokenutil_test.py @@ -0,0 +1,289 @@ +#!/usr/bin/env python +# +# Copyright 2012 The Closure Linter Authors. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for the scopeutil module.""" + +# Allow non-Google copyright +# pylint: disable=g-bad-file-header + +__author__ = ('nnaze@google.com (Nathan Naze)') + +import unittest as googletest + +from closure_linter import ecmametadatapass +from closure_linter import javascripttokens +from closure_linter import testutil +from closure_linter import tokenutil + + +class FakeToken(object): + pass + + +class TokenUtilTest(googletest.TestCase): + + def testGetTokenRange(self): + + a = FakeToken() + b = FakeToken() + c = FakeToken() + d = FakeToken() + e = FakeToken() + + a.next = b + b.next = c + c.next = d + + self.assertEquals([a, b, c, d], tokenutil.GetTokenRange(a, d)) + + # This is an error as e does not come after a in the token chain. + self.assertRaises(Exception, lambda: tokenutil.GetTokenRange(a, e)) + + def testTokensToString(self): + + a = FakeToken() + b = FakeToken() + c = FakeToken() + d = FakeToken() + e = FakeToken() + + a.string = 'aaa' + b.string = 'bbb' + c.string = 'ccc' + d.string = 'ddd' + e.string = 'eee' + + a.line_number = 5 + b.line_number = 6 + c.line_number = 6 + d.line_number = 10 + e.line_number = 11 + + self.assertEquals( + 'aaa\nbbbccc\n\n\n\nddd\neee', + tokenutil.TokensToString([a, b, c, d, e])) + + self.assertEquals( + 'ddd\neee\naaa\nbbbccc', + tokenutil.TokensToString([d, e, a, b, c]), + 'Neighboring tokens not in line_number order should have a newline ' + 'between them.') + + def testGetPreviousCodeToken(self): + + tokens = testutil.TokenizeSource(""" +start1. // comment + /* another comment */ + end1 +""") + + def _GetTokenStartingWith(token_starts_with): + for t in tokens: + if t.string.startswith(token_starts_with): + return t + + self.assertEquals( + None, + tokenutil.GetPreviousCodeToken(_GetTokenStartingWith('start1'))) + + self.assertEquals( + 'start1.', + tokenutil.GetPreviousCodeToken(_GetTokenStartingWith('end1')).string) + + def testGetNextCodeToken(self): + + tokens = testutil.TokenizeSource(""" +start1. // comment + /* another comment */ + end1 +""") + + def _GetTokenStartingWith(token_starts_with): + for t in tokens: + if t.string.startswith(token_starts_with): + return t + + self.assertEquals( + 'end1', + tokenutil.GetNextCodeToken(_GetTokenStartingWith('start1')).string) + + self.assertEquals( + None, + tokenutil.GetNextCodeToken(_GetTokenStartingWith('end1'))) + + def testGetIdentifierStart(self): + + tokens = testutil.TokenizeSource(""" +start1 . // comment + prototype. /* another comment */ + end1 + +['edge'][case].prototype. + end2 = function() {} +""") + + def _GetTokenStartingWith(token_starts_with): + for t in tokens: + if t.string.startswith(token_starts_with): + return t + + self.assertEquals( + 'start1', + tokenutil.GetIdentifierStart(_GetTokenStartingWith('end1')).string) + + self.assertEquals( + 'start1', + tokenutil.GetIdentifierStart(_GetTokenStartingWith('start1')).string) + + self.assertEquals( + None, + tokenutil.GetIdentifierStart(_GetTokenStartingWith('end2'))) + + def testInsertTokenBefore(self): + + self.AssertInsertTokenAfterBefore(False) + + def testInsertTokenAfter(self): + + self.AssertInsertTokenAfterBefore(True) + + def AssertInsertTokenAfterBefore(self, after): + + new_token = javascripttokens.JavaScriptToken( + 'a', javascripttokens.JavaScriptTokenType.IDENTIFIER, 1, 1) + + existing_token1 = javascripttokens.JavaScriptToken( + 'var', javascripttokens.JavaScriptTokenType.KEYWORD, 1, 1) + existing_token1.start_index = 0 + existing_token1.metadata = ecmametadatapass.EcmaMetaData() + + existing_token2 = javascripttokens.JavaScriptToken( + ' ', javascripttokens.JavaScriptTokenType.WHITESPACE, 1, 1) + existing_token2.start_index = 3 + existing_token2.metadata = ecmametadatapass.EcmaMetaData() + existing_token2.metadata.last_code = existing_token1 + + existing_token1.next = existing_token2 + existing_token2.previous = existing_token1 + + if after: + tokenutil.InsertTokenAfter(new_token, existing_token1) + else: + tokenutil.InsertTokenBefore(new_token, existing_token2) + + self.assertEquals(existing_token1, new_token.previous) + self.assertEquals(existing_token2, new_token.next) + + self.assertEquals(new_token, existing_token1.next) + self.assertEquals(new_token, existing_token2.previous) + + self.assertEquals(existing_token1, new_token.metadata.last_code) + self.assertEquals(new_token, existing_token2.metadata.last_code) + + self.assertEquals(0, existing_token1.start_index) + self.assertEquals(3, new_token.start_index) + self.assertEquals(4, existing_token2.start_index) + + def testGetIdentifierForToken(self): + + tokens = testutil.TokenizeSource(""" +start1.abc.def.prototype. + onContinuedLine + +(start2.abc.def + .hij.klm + .nop) + +start3.abc.def + .hij = function() {}; + +// An absurd multi-liner. +start4.abc.def. + hij. + klm = function() {}; + +start5 . aaa . bbb . ccc + shouldntBePartOfThePreviousSymbol + +start6.abc.def ghi.shouldntBePartOfThePreviousSymbol + +var start7 = 42; + +function start8() { + +} + +start9.abc. // why is there a comment here? + def /* another comment */ + shouldntBePart + +start10.abc // why is there a comment here? + .def /* another comment */ + shouldntBePart + +start11.abc. middle1.shouldNotBeIdentifier +""") + + def _GetTokenStartingWith(token_starts_with): + for t in tokens: + if t.string.startswith(token_starts_with): + return t + + self.assertEquals( + 'start1.abc.def.prototype.onContinuedLine', + tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start1'))) + + self.assertEquals( + 'start2.abc.def.hij.klm.nop', + tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start2'))) + + self.assertEquals( + 'start3.abc.def.hij', + tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start3'))) + + self.assertEquals( + 'start4.abc.def.hij.klm', + tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start4'))) + + self.assertEquals( + 'start5.aaa.bbb.ccc', + tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start5'))) + + self.assertEquals( + 'start6.abc.def', + tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start6'))) + + self.assertEquals( + 'start7', + tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start7'))) + + self.assertEquals( + 'start8', + tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start8'))) + + self.assertEquals( + 'start9.abc.def', + tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start9'))) + + self.assertEquals( + 'start10.abc.def', + tokenutil.GetIdentifierForToken(_GetTokenStartingWith('start10'))) + + self.assertIsNone( + tokenutil.GetIdentifierForToken(_GetTokenStartingWith('middle1'))) + + +if __name__ == '__main__': + googletest.main() diff --git a/third_party/gjslint/closure_linter-2.3.13/setup.py b/third_party/gjslint/closure_linter-2.3.13/setup.py new file mode 100755 index 0000000000..0f0240153d --- /dev/null +++ b/third_party/gjslint/closure_linter-2.3.13/setup.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# +# Copyright 2010 The Closure Linter Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS-IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + +setup(name='closure_linter', + version='2.3.13', + description='Closure Linter', + license='Apache', + author='The Closure Linter Authors', + author_email='opensource@google.com', + url='http://code.google.com/p/closure-linter', + install_requires=['python-gflags'], + package_dir={'closure_linter': 'closure_linter'}, + packages=['closure_linter', 'closure_linter.common'], + entry_points = { + 'console_scripts': [ + 'gjslint = closure_linter.gjslint:main', + 'fixjsstyle = closure_linter.fixjsstyle:main' + ] + } +) diff --git a/third_party/gjslint/gjslint b/third_party/gjslint/gjslint new file mode 100755 index 0000000000..da645069b2 --- /dev/null +++ b/third_party/gjslint/gjslint @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""A shim to wrap around gjslint.py without installing it.""" + +import os +import sys + +dir = os.path.dirname(__file__) + +gflags_path = os.path.join(dir, 'python-gflags-2.0') +linter_path = os.path.join(dir, 'closure_linter-2.3.13') + +sys.path.append(gflags_path) +sys.path.append(linter_path) + +from closure_linter import gjslint + +# Steal gjslint's doc string, which shows up in --help output. +sys.modules["__main__"].__doc__ = gjslint.__doc__ + +gjslint.main() diff --git a/third_party/gjslint/python-gflags-2.0/AUTHORS b/third_party/gjslint/python-gflags-2.0/AUTHORS new file mode 100644 index 0000000000..887918bd00 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/AUTHORS @@ -0,0 +1,2 @@ +google-gflags@googlegroups.com + diff --git a/third_party/gjslint/python-gflags-2.0/ChangeLog b/third_party/gjslint/python-gflags-2.0/ChangeLog new file mode 100644 index 0000000000..87732a2b97 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/ChangeLog @@ -0,0 +1,62 @@ +Wed Jan 18 13:57:39 2012 Google Inc. + + * python-gflags: version 2.0 + * No changes from version 1.8. + +Wed Jan 18 11:54:03 2012 Google Inc. + + * python-gflags: version 1.8 + * Don't raise DuplicateFlag when re-importing a module (mmcdonald) + * Changed the 'official' python-gflags email in setup.py/etc + * Changed copyright text to reflect Google's relinquished ownership + +Tue Dec 20 17:10:41 2011 Google Inc. + + * python-gflags: version 1.7 + * Prepare gflags for python 3.x, keeping 2.4 compatibility (twouters) + * If output is a tty, use terminal's width to wrap help-text (wiesmann) + * PORTING: Fix ImportError for non-Unix platforms (kdeus) + * PORTING: Run correctly when termios isn't available (shines) + * Add unicode support to flags (csilvers) + +Fri Jul 29 12:24:08 2011 Google Inc. + + * python-gflags: version 1.6 + * Document FlagValues.UseGnuGetOpt (garymm) + * replace fchmod with chmod to work on python 2.4 (mshields) + * Fix bug in flag decl reporting for dup flags (craigcitro) + * Add multi_float, and tests for multi_float/int (simonf) + * Make flagfiles expand in place, to follow docs (dmlynch) + * Raise exception if --flagfile can't be read (tlim) + +Wed Jan 26 13:50:46 2011 Google Inc. + + * python-gflags: version 1.5.1 + * Fix manifest and setup.py to include new files + +Mon Jan 24 16:58:10 2011 Google Inc. + + * python-gflags: version 1.5 + * Add support for flag validators (olexiy) + * Better reporting on UnrecognizedFlagError (sorenj) + * Cache ArgumentParser, to save space (tmarek) + +Wed Oct 13 17:40:12 2010 Google Inc. + + * python-gflags: version 1.4 + * Unregister per-command flags after running the command (dnr) + * Allow key-flags to work with special flags (salcianu) + * Allow printing flags of a specific module (mikecurtis) + * BUGFIX: Fix an error message for float flags (olexiy) + * BUGFIX: Can now import while defining flags (salcianu) + * BUGFIX: Fix flagfile parsing in python (chronos) + * DOC: Better explain the format of --helpxml output (salcianu) + * DOC: Better error message on parse failure (tstromberg) + * Better test coverage under python 2.2 (mshields) + * Added a Makefile for building the packages. + +Mon Jan 4 18:46:29 2010 Tim 'mithro' Ansell + + * python-gflags: version 1.3 + * Fork from the C++ package (google-gflags 1.3) + * Add debian packaging diff --git a/third_party/gjslint/python-gflags-2.0/LICENSE.txt b/third_party/gjslint/python-gflags-2.0/LICENSE.txt new file mode 100644 index 0000000000..d15b0c2413 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright (c) 2006, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/third_party/gjslint/python-gflags-2.0/MANIFEST.in b/third_party/gjslint/python-gflags-2.0/MANIFEST.in new file mode 100644 index 0000000000..17851bfa77 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/MANIFEST.in @@ -0,0 +1,19 @@ +include AUTHORS +include COPYING +include ChangeLog +include MANIFEST.in +include Makefile +include NEWS +include README +include debian/README +include debian/changelog +include debian/compat +include debian/control +include debian/copyright +include debian/docs +include debian/rules +include gflags.py +include gflags2man.py +include gflags_validators.py +include setup.py +recursive-include tests *.py diff --git a/third_party/gjslint/python-gflags-2.0/Makefile b/third_party/gjslint/python-gflags-2.0/Makefile new file mode 100644 index 0000000000..6627c32a5e --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/Makefile @@ -0,0 +1,69 @@ + +prep: + @echo + # Install needed packages + sudo apt-get install subversion fakeroot python-setuptools python-subversion + # + @echo + # Check that the person has .pypirc + @if [ ! -e ~/.pypirc ]; then \ + echo "Please create a ~/.pypirc with the following contents:"; \ + echo "[server-login]"; \ + echo "username:google_opensource"; \ + echo "password:"; \ + fi + # + @echo + # FIXME(tansell): Check that the person has .dputrc for PPA + +clean: + # Clean up any build files. + python setup.py clean --all + # + # Clean up the debian stuff + fakeroot ./debian/rules clean + # + # Clean up everything else + rm MANIFEST || true + rm -rf build-* + # + # Clean up the egg files + rm -rf *egg* + # + # Remove dist + rm -rf dist + +dist: + # Generate the tarball based on MANIFEST.in + python setup.py sdist + # + # Build the debian packages + fakeroot ./debian/rules binary + mv ../python-gflags*.deb ./dist/ + # + # Build the python Egg + python setup.py bdist_egg + # + @echo + @echo "Files to upload:" + @echo "--------------------------" + @ls -l ./dist/ + +push: + # Send the updates to svn + # Upload the source package to code.google.com + - /home/build/opensource/tools/googlecode_upload.py \ + -p python-gflags ./dist/* + # + # Upload the package to PyPi + - python setup.py sdist upload + - python setup.py bdist_egg upload + # + # Upload the package to the ppa + # FIXME(tansell): dput should run here + +check: + # Run all the tests. + for test in tests/*.py; do PYTHONPATH=. python $$test || exit 1; done + +.PHONY: prep dist clean push check diff --git a/third_party/gjslint/python-gflags-2.0/NEWS b/third_party/gjslint/python-gflags-2.0/NEWS new file mode 100644 index 0000000000..8aaa72bf30 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/NEWS @@ -0,0 +1,78 @@ +== 18 January 2012 == + +[Prependum:] I just realized I should have named the new version 2.0, +to reflect the new ownership and status as a community run project. +Not too late, I guess. I've just released python-gflags 2.0, which is +identical to python-gflags 1.8 except for the version number. + +I've just released python-gflags 1.8. This fixes a bug, allowing +modules defining flags to be re-imported without raising duplicate +flag errors. + +Administrative note: In the coming weeks, I'll be stepping down as +maintainer for the python-gflags project, and as part of that Google +is relinquishing ownership of the project; it will now be entirely +community run. The remaining +[http://python-gflags.googlecode.com/svn/tags/python-gflags-1.8/ChangeLog changes] +in this release reflect that shift. + + +=== 20 December 2011 === + +I've just released python-gflags 1.7. The major change here is +improved unicode support, in both flag default values and +help-strings. We've also made big steps toward making gflags work +with python 3.x (while keeping 2.4 compatibility), and improving +--help output in the common case where output is a tty. + +For a full list of changes since last release, see the +[http://python-gflags.googlecode.com/svn/tags/python-gflags-1.7/ChangeLog ChangeLog]. + +=== 29 July 2011 === + +I've just released python-gflags 1.6. This release has only minor +changes, including support for multi_float flags. The full list of +changes is in the +[http://python-gflags.googlecode.com/svn/tags/python-gflags-1.6/ChangeLog ChangeLog]. + +The major change with this release is procedural: I've changed the +internal tools used to integrate Google-supplied patches for gflags +into the opensource release. These new tools should result in more +frequent updates with better change descriptions. They will also +result in future `ChangeLog` entries being much more verbose (for +better or for worse). + +=== 26 January 2011 === + +I've just released python-gflags 1.5.1. I had improperly packaged +python-gflags 1.5, so it probably doesn't work. All users who have +updated to python-gflags 1.5 are encouraged to update again to 1.5.1. + +=== 24 January 2011 === + +I've just released python-gflags 1.5. This release adds support for +flag verifiers: small functions you can associate with flags, that are +called whenever the flag value is set or modified, and can verify that +the new value is legal. It also has other, minor changes, described +in the +[http://python-gflags.googlecode.com/svn/tags/python-gflags-1.5/ChangeLog ChangeLog]. + +=== 11 October 2010 === + +I've just released python-gflags 1.4. This release has only minor +changes from 1.3, including support for printing flags of a specific +module, allowing key-flags to work with special flags, somewhat better +error messaging, and +[http://python-gflags.googlecode.com/svn/tags/python-gflags-1.4/ChangeLog so forth]. +If 1.3 is working well for you, there's no particular reason to upgrade. + +=== 4 January 2010 === + +I just released python-gflags 1.3. This is the first python-gflags +release; it is version 1.3 because this code is forked from the 1.3 +release of google-gflags. + +I don't have a tarball or .deb file up quite yet, so for now you will +have to get the source files by browsing under the 'source' +tag. Downloadable files will be available soon. + diff --git a/third_party/gjslint/python-gflags-2.0/PKG-INFO b/third_party/gjslint/python-gflags-2.0/PKG-INFO new file mode 100644 index 0000000000..faab7198f2 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: python-gflags +Version: 2.0 +Summary: Google Commandline Flags Module +Home-page: http://code.google.com/p/python-gflags +Author: Google Inc. and others +Author-email: google-gflags@googlegroups.com +License: BSD +Description: UNKNOWN +Platform: UNKNOWN diff --git a/third_party/gjslint/python-gflags-2.0/README b/third_party/gjslint/python-gflags-2.0/README new file mode 100644 index 0000000000..81daa7ab49 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/README @@ -0,0 +1,23 @@ +This repository contains a python implementation of the Google commandline +flags module. + + GFlags defines a *distributed* command line system, replacing systems like + getopt(), optparse and manual argument processing. Rather than an application + having to define all flags in or near main(), each python module defines flags + that are useful to it. When one python module imports another, it gains + access to the other's flags. + + It includes the ability to define flag types (boolean, float, interger, list), + autogeneration of help (in both human and machine readable format) and reading + arguments from a file. It also includes the ability to automatically generate + man pages from the help flags. + +Documentation for implementation is at the top of gflags.py file. + +To install the python module, run + python ./setup.py install + +When you install this library, you also get a helper application, +gflags2man.py, installed into /usr/local/bin. You can run gflags2man.py to +create an instant man page, with all the commandline flags and their docs, for +any C++ or python program you've written using the gflags library. diff --git a/third_party/gjslint/python-gflags-2.0/debian/README b/third_party/gjslint/python-gflags-2.0/debian/README new file mode 100644 index 0000000000..57becfda75 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/debian/README @@ -0,0 +1,7 @@ +The list of files here isn't complete. For a step-by-step guide on +how to set this package up correctly, check out + http://www.debian.org/doc/maint-guide/ + +Most of the files that are in this directory are boilerplate. +However, you may need to change the list of binary-arch dependencies +in 'rules'. diff --git a/third_party/gjslint/python-gflags-2.0/debian/changelog b/third_party/gjslint/python-gflags-2.0/debian/changelog new file mode 100644 index 0000000000..5e6457e2d7 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/debian/changelog @@ -0,0 +1,54 @@ +python-gflags (2.0-1) unstable; urgency=low + + * New upstream release. + + -- Google Inc. Wed, 18 Jan 2012 13:57:39 -0800 + +python-gflags (1.8-1) unstable; urgency=low + + * New upstream release. + + -- Google Inc. Wed, 18 Jan 2012 11:54:03 -0800 + +python-gflags (1.7-1) unstable; urgency=low + + * New upstream release. + + -- Google Inc. Tue, 20 Dec 2011 17:10:41 -0800 + +python-gflags (1.6-1) unstable; urgency=low + + * New upstream release. + + -- Google Inc. Fri, 29 Jul 2011 12:24:08 -0700 + +python-gflags (1.5.1-1) unstable; urgency=low + + * New upstream release (fixes manifest and setup.py files) + + -- Google Inc. Wed, 26 Jan 2011 13:50:46 -0800 + +python-gflags (1.5-1) unstable; urgency=low + + * New upstream release. + + -- Google Inc. Mon, 24 Jan 2011 16:58:10 -0800 + +python-gflags (1.4-1) unstable; urgency=low + + * New upstream release. + + -- Google Inc. Wed, 13 Oct 2010 17:40:12 -0700 + +python-gflags (1.3-2) unstable; urgency=low + + * Fixed man-page generation. + + -- Tim 'mithro' Ansell Mon, 07 Jan 2010 13:46:10 +1100 + +python-gflags (1.3-1) unstable; urgency=low + + * Initial release. + * Packaging based on gflags 1.3 + + -- Tim 'mithro' Ansell Mon, 04 Jan 2010 18:46:10 -0800 diff --git a/third_party/gjslint/python-gflags-2.0/debian/compat b/third_party/gjslint/python-gflags-2.0/debian/compat new file mode 100644 index 0000000000..7ed6ff82de --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/debian/compat @@ -0,0 +1 @@ +5 diff --git a/third_party/gjslint/python-gflags-2.0/debian/control b/third_party/gjslint/python-gflags-2.0/debian/control new file mode 100644 index 0000000000..4a9b15942a --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/debian/control @@ -0,0 +1,26 @@ +Source: python-gflags +Section: python +XS-Python-Version: all +Priority: optional +Maintainer: Craig Silverstein +Build-Depends-Indep: python-central (>= 0.5.6), python-setuptools (>= 0.6b3-1), python-all +Build-Depends: debhelper (>= 5.0.38) +Standards-Version: 3.7.2 + +Package: python-gflags +Architecture: all +Depends: ${python:Depends} +XB-Python-Version: ${python:Versions} +Description: A Python implementation of the Google commandline flags module + . + GFlags defines a *distributed* command line system, replacing systems like + getopt(), optparse and manual argument processing. Rather than an application + having to define all flags in or near main(), each Python module defines flags + that are useful to it. When one Python module imports another, it gains + access to the other's flags. + . + It includes the ability to define flag types (boolean, float, interger, list), + autogeneration of help (in both human and machine readable format) and reading + arguments from a file. It also includes the ability to automatically generate + man pages from the help flags. + diff --git a/third_party/gjslint/python-gflags-2.0/debian/copyright b/third_party/gjslint/python-gflags-2.0/debian/copyright new file mode 100644 index 0000000000..7d27d62fb4 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/debian/copyright @@ -0,0 +1,41 @@ +This package was debianized by Craig Silverstein on +Wed, 18 Jan 2012 13:57:39 -0800. + +It was downloaded from http://code.google.com/p/python-gflags/downloads/list + +Upstream Author: Google Inc. and others +Copyright: Google Inc. and others + +License: + +Copyright (c) 2006, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The Debian packaging is (C) 2011, Tim 'mithro' Ansell and +is licensed under the above. diff --git a/third_party/gjslint/python-gflags-2.0/debian/docs b/third_party/gjslint/python-gflags-2.0/debian/docs new file mode 100644 index 0000000000..6f12db5084 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/debian/docs @@ -0,0 +1,2 @@ +AUTHORS +README diff --git a/third_party/gjslint/python-gflags-2.0/debian/rules b/third_party/gjslint/python-gflags-2.0/debian/rules new file mode 100755 index 0000000000..0840b5ef8c --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/debian/rules @@ -0,0 +1,62 @@ +#!/usr/bin/make -f +# -*- makefile -*- +# Sample debian/rules that uses debhelper. +# GNU copyright 1997 to 1999 by Joey Hess. + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +PYTHON := /usr/bin/python +#PYVER := $(shell $(PYTHON) -c 'import sys; print sys.version[:3]') +PYVERS = $(shell pyversions -vr) + +build: $(PYVERS:%=build-python%) + touch $@ + +build-python%: + dh_testdir + python$* setup.py build + touch $@ + +clean: + dh_testdir + dh_testroot + rm -f build-python* + rm -rf build + -find . -name '*.py[co]' | xargs rm -f + dh_clean + +install: build $(PYVERS:%=install-python%) + +install-python%: + dh_testdir + dh_testroot + dh_clean -k + dh_installdirs + python$* setup.py install --root=$(CURDIR)/debian/python-gflags --prefix=/usr + # Scripts should not have a .py on the end of them + mv $(CURDIR)/debian/python-gflags/usr/bin/gflags2man.py $(CURDIR)/debian/python-gflags/usr/bin/gflags2man + # Generate a man file for gflags2man + mkdir -p $(CURDIR)/debian/python-gflags/usr/share/man/man1 + PYTHONPATH=$(CURDIR)/debian/.. python$* gflags2man.py --dest_dir $(CURDIR)/debian/python-gflags/usr/share/man/man1 $(CURDIR)/debian/python-gflags/usr/bin/gflags2man + +# Build architecture-independent files here. +binary-indep: build install + dh_testdir + dh_testroot + dh_installchangelogs -k ChangeLog + dh_installdocs + dh_pycentral + dh_compress -X.py + dh_fixperms + dh_installdeb + dh_gencontrol + dh_md5sums + dh_builddeb + +# Build architecture-dependent files here. +binary-arch: build install +# We have nothing to do by default. + +binary: binary-indep binary-arch +.PHONY: build clean binary-indep binary-arch binary install configure diff --git a/third_party/gjslint/python-gflags-2.0/gflags.py b/third_party/gjslint/python-gflags-2.0/gflags.py new file mode 100644 index 0000000000..822256a6f8 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/gflags.py @@ -0,0 +1,2862 @@ +#!/usr/bin/env python +# +# Copyright (c) 2002, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# --- +# Author: Chad Lester +# Design and style contributions by: +# Amit Patel, Bogdan Cocosel, Daniel Dulitz, Eric Tiedemann, +# Eric Veach, Laurence Gonsalves, Matthew Springer +# Code reorganized a bit by Craig Silverstein + +"""This module is used to define and parse command line flags. + +This module defines a *distributed* flag-definition policy: rather than +an application having to define all flags in or near main(), each python +module defines flags that are useful to it. When one python module +imports another, it gains access to the other's flags. (This is +implemented by having all modules share a common, global registry object +containing all the flag information.) + +Flags are defined through the use of one of the DEFINE_xxx functions. +The specific function used determines how the flag is parsed, checked, +and optionally type-converted, when it's seen on the command line. + + +IMPLEMENTATION: DEFINE_* creates a 'Flag' object and registers it with a +'FlagValues' object (typically the global FlagValues FLAGS, defined +here). The 'FlagValues' object can scan the command line arguments and +pass flag arguments to the corresponding 'Flag' objects for +value-checking and type conversion. The converted flag values are +available as attributes of the 'FlagValues' object. + +Code can access the flag through a FlagValues object, for instance +gflags.FLAGS.myflag. Typically, the __main__ module passes the command +line arguments to gflags.FLAGS for parsing. + +At bottom, this module calls getopt(), so getopt functionality is +supported, including short- and long-style flags, and the use of -- to +terminate flags. + +Methods defined by the flag module will throw 'FlagsError' exceptions. +The exception argument will be a human-readable string. + + +FLAG TYPES: This is a list of the DEFINE_*'s that you can do. All flags +take a name, default value, help-string, and optional 'short' name +(one-letter name). Some flags have other arguments, which are described +with the flag. + +DEFINE_string: takes any input, and interprets it as a string. + +DEFINE_bool or +DEFINE_boolean: typically does not take an argument: say --myflag to + set FLAGS.myflag to true, or --nomyflag to set + FLAGS.myflag to false. Alternately, you can say + --myflag=true or --myflag=t or --myflag=1 or + --myflag=false or --myflag=f or --myflag=0 + +DEFINE_float: takes an input and interprets it as a floating point + number. Takes optional args lower_bound and upper_bound; + if the number specified on the command line is out of + range, it will raise a FlagError. + +DEFINE_integer: takes an input and interprets it as an integer. Takes + optional args lower_bound and upper_bound as for floats. + +DEFINE_enum: takes a list of strings which represents legal values. If + the command-line value is not in this list, raise a flag + error. Otherwise, assign to FLAGS.flag as a string. + +DEFINE_list: Takes a comma-separated list of strings on the commandline. + Stores them in a python list object. + +DEFINE_spaceseplist: Takes a space-separated list of strings on the + commandline. Stores them in a python list object. + Example: --myspacesepflag "foo bar baz" + +DEFINE_multistring: The same as DEFINE_string, except the flag can be + specified more than once on the commandline. The + result is a python list object (list of strings), + even if the flag is only on the command line once. + +DEFINE_multi_int: The same as DEFINE_integer, except the flag can be + specified more than once on the commandline. The + result is a python list object (list of ints), even if + the flag is only on the command line once. + + +SPECIAL FLAGS: There are a few flags that have special meaning: + --help prints a list of all the flags in a human-readable fashion + --helpshort prints a list of all key flags (see below). + --helpxml prints a list of all flags, in XML format. DO NOT parse + the output of --help and --helpshort. Instead, parse + the output of --helpxml. For more info, see + "OUTPUT FOR --helpxml" below. + --flagfile=foo read flags from file foo. + --undefok=f1,f2 ignore unrecognized option errors for f1,f2. + For boolean flags, you should use --undefok=boolflag, and + --boolflag and --noboolflag will be accepted. Do not use + --undefok=noboolflag. + -- as in getopt(), terminates flag-processing + + +FLAGS VALIDATORS: If your program: + - requires flag X to be specified + - needs flag Y to match a regular expression + - or requires any more general constraint to be satisfied +then validators are for you! + +Each validator represents a constraint over one flag, which is enforced +starting from the initial parsing of the flags and until the program +terminates. + +Also, lower_bound and upper_bound for numerical flags are enforced using flag +validators. + +Howto: +If you want to enforce a constraint over one flag, use + +gflags.RegisterValidator(flag_name, + checker, + message='Flag validation failed', + flag_values=FLAGS) + +After flag values are initially parsed, and after any change to the specified +flag, method checker(flag_value) will be executed. If constraint is not +satisfied, an IllegalFlagValue exception will be raised. See +RegisterValidator's docstring for a detailed explanation on how to construct +your own checker. + + +EXAMPLE USAGE: + +FLAGS = gflags.FLAGS + +gflags.DEFINE_integer('my_version', 0, 'Version number.') +gflags.DEFINE_string('filename', None, 'Input file name', short_name='f') + +gflags.RegisterValidator('my_version', + lambda value: value % 2 == 0, + message='--my_version must be divisible by 2') +gflags.MarkFlagAsRequired('filename') + + +NOTE ON --flagfile: + +Flags may be loaded from text files in addition to being specified on +the commandline. + +Any flags you don't feel like typing, throw them in a file, one flag per +line, for instance: + --myflag=myvalue + --nomyboolean_flag +You then specify your file with the special flag '--flagfile=somefile'. +You CAN recursively nest flagfile= tokens OR use multiple files on the +command line. Lines beginning with a single hash '#' or a double slash +'//' are comments in your flagfile. + +Any flagfile= will be interpreted as having a relative path from +the current working directory rather than from the place the file was +included from: + myPythonScript.py --flagfile=config/somefile.cfg + +If somefile.cfg includes further --flagfile= directives, these will be +referenced relative to the original CWD, not from the directory the +including flagfile was found in! + +The caveat applies to people who are including a series of nested files +in a different dir than they are executing out of. Relative path names +are always from CWD, not from the directory of the parent include +flagfile. We do now support '~' expanded directory names. + +Absolute path names ALWAYS work! + + +EXAMPLE USAGE: + + + FLAGS = gflags.FLAGS + + # Flag names are globally defined! So in general, we need to be + # careful to pick names that are unlikely to be used by other libraries. + # If there is a conflict, we'll get an error at import time. + gflags.DEFINE_string('name', 'Mr. President', 'your name') + gflags.DEFINE_integer('age', None, 'your age in years', lower_bound=0) + gflags.DEFINE_boolean('debug', False, 'produces debugging output') + gflags.DEFINE_enum('gender', 'male', ['male', 'female'], 'your gender') + + def main(argv): + try: + argv = FLAGS(argv) # parse flags + except gflags.FlagsError, e: + print '%s\\nUsage: %s ARGS\\n%s' % (e, sys.argv[0], FLAGS) + sys.exit(1) + if FLAGS.debug: print 'non-flag arguments:', argv + print 'Happy Birthday', FLAGS.name + if FLAGS.age is not None: + print 'You are a %d year old %s' % (FLAGS.age, FLAGS.gender) + + if __name__ == '__main__': + main(sys.argv) + + +KEY FLAGS: + +As we already explained, each module gains access to all flags defined +by all the other modules it transitively imports. In the case of +non-trivial scripts, this means a lot of flags ... For documentation +purposes, it is good to identify the flags that are key (i.e., really +important) to a module. Clearly, the concept of "key flag" is a +subjective one. When trying to determine whether a flag is key to a +module or not, assume that you are trying to explain your module to a +potential user: which flags would you really like to mention first? + +We'll describe shortly how to declare which flags are key to a module. +For the moment, assume we know the set of key flags for each module. +Then, if you use the app.py module, you can use the --helpshort flag to +print only the help for the flags that are key to the main module, in a +human-readable format. + +NOTE: If you need to parse the flag help, do NOT use the output of +--help / --helpshort. That output is meant for human consumption, and +may be changed in the future. Instead, use --helpxml; flags that are +key for the main module are marked there with a yes element. + +The set of key flags for a module M is composed of: + +1. Flags defined by module M by calling a DEFINE_* function. + +2. Flags that module M explictly declares as key by using the function + + DECLARE_key_flag() + +3. Key flags of other modules that M specifies by using the function + + ADOPT_module_key_flags() + + This is a "bulk" declaration of key flags: each flag that is key for + becomes key for the current module too. + +Notice that if you do not use the functions described at points 2 and 3 +above, then --helpshort prints information only about the flags defined +by the main module of our script. In many cases, this behavior is good +enough. But if you move part of the main module code (together with the +related flags) into a different module, then it is nice to use +DECLARE_key_flag / ADOPT_module_key_flags and make sure --helpshort +lists all relevant flags (otherwise, your code refactoring may confuse +your users). + +Note: each of DECLARE_key_flag / ADOPT_module_key_flags has its own +pluses and minuses: DECLARE_key_flag is more targeted and may lead a +more focused --helpshort documentation. ADOPT_module_key_flags is good +for cases when an entire module is considered key to the current script. +Also, it does not require updates to client scripts when a new flag is +added to the module. + + +EXAMPLE USAGE 2 (WITH KEY FLAGS): + +Consider an application that contains the following three files (two +auxiliary modules and a main module) + +File libfoo.py: + + import gflags + + gflags.DEFINE_integer('num_replicas', 3, 'Number of replicas to start') + gflags.DEFINE_boolean('rpc2', True, 'Turn on the usage of RPC2.') + + ... some code ... + +File libbar.py: + + import gflags + + gflags.DEFINE_string('bar_gfs_path', '/gfs/path', + 'Path to the GFS files for libbar.') + gflags.DEFINE_string('email_for_bar_errors', 'bar-team@google.com', + 'Email address for bug reports about module libbar.') + gflags.DEFINE_boolean('bar_risky_hack', False, + 'Turn on an experimental and buggy optimization.') + + ... some code ... + +File myscript.py: + + import gflags + import libfoo + import libbar + + gflags.DEFINE_integer('num_iterations', 0, 'Number of iterations.') + + # Declare that all flags that are key for libfoo are + # key for this module too. + gflags.ADOPT_module_key_flags(libfoo) + + # Declare that the flag --bar_gfs_path (defined in libbar) is key + # for this module. + gflags.DECLARE_key_flag('bar_gfs_path') + + ... some code ... + +When myscript is invoked with the flag --helpshort, the resulted help +message lists information about all the key flags for myscript: +--num_iterations, --num_replicas, --rpc2, and --bar_gfs_path. + +Of course, myscript uses all the flags declared by it (in this case, +just --num_replicas) or by any of the modules it transitively imports +(e.g., the modules libfoo, libbar). E.g., it can access the value of +FLAGS.bar_risky_hack, even if --bar_risky_hack is not declared as a key +flag for myscript. + + +OUTPUT FOR --helpxml: + +The --helpxml flag generates output with the following structure: + + + + PROGRAM_BASENAME + MAIN_MODULE_DOCSTRING + ( + [yes] + DECLARING_MODULE + FLAG_NAME + FLAG_HELP_MESSAGE + DEFAULT_FLAG_VALUE + CURRENT_FLAG_VALUE + FLAG_TYPE + [OPTIONAL_ELEMENTS] + )* + + +Notes: + +1. The output is intentionally similar to the output generated by the +C++ command-line flag library. The few differences are due to the +Python flags that do not have a C++ equivalent (at least not yet), +e.g., DEFINE_list. + +2. New XML elements may be added in the future. + +3. DEFAULT_FLAG_VALUE is in serialized form, i.e., the string you can +pass for this flag on the command-line. E.g., for a flag defined +using DEFINE_list, this field may be foo,bar, not ['foo', 'bar']. + +4. CURRENT_FLAG_VALUE is produced using str(). This means that the +string 'false' will be represented in the same way as the boolean +False. Using repr() would have removed this ambiguity and simplified +parsing, but would have broken the compatibility with the C++ +command-line flags. + +5. OPTIONAL_ELEMENTS describe elements relevant for certain kinds of +flags: lower_bound, upper_bound (for flags that specify bounds), +enum_value (for enum flags), list_separator (for flags that consist of +a list of values, separated by a special token). + +6. We do not provide any example here: please use --helpxml instead. + +This module requires at least python 2.2.1 to run. +""" + +import cgi +import getopt +import os +import re +import string +import struct +import sys +# pylint: disable-msg=C6204 +try: + import fcntl +except ImportError: + fcntl = None +try: + # Importing termios will fail on non-unix platforms. + import termios +except ImportError: + termios = None + +import gflags_validators +# pylint: enable-msg=C6204 + + +# Are we running under pychecker? +_RUNNING_PYCHECKER = 'pychecker.python' in sys.modules + + +def _GetCallingModuleObjectAndName(): + """Returns the module that's calling into this module. + + We generally use this function to get the name of the module calling a + DEFINE_foo... function. + """ + # Walk down the stack to find the first globals dict that's not ours. + for depth in range(1, sys.getrecursionlimit()): + if not sys._getframe(depth).f_globals is globals(): + globals_for_frame = sys._getframe(depth).f_globals + module, module_name = _GetModuleObjectAndName(globals_for_frame) + if module_name is not None: + return module, module_name + raise AssertionError("No module was found") + + +def _GetCallingModule(): + """Returns the name of the module that's calling into this module.""" + return _GetCallingModuleObjectAndName()[1] + + +def _GetThisModuleObjectAndName(): + """Returns: (module object, module name) for this module.""" + return _GetModuleObjectAndName(globals()) + + +# module exceptions: +class FlagsError(Exception): + """The base class for all flags errors.""" + pass + + +class DuplicateFlag(FlagsError): + """Raised if there is a flag naming conflict.""" + pass + +class CantOpenFlagFileError(FlagsError): + """Raised if flagfile fails to open: doesn't exist, wrong permissions, etc.""" + pass + + +class DuplicateFlagCannotPropagateNoneToSwig(DuplicateFlag): + """Special case of DuplicateFlag -- SWIG flag value can't be set to None. + + This can be raised when a duplicate flag is created. Even if allow_override is + True, we still abort if the new value is None, because it's currently + impossible to pass None default value back to SWIG. See FlagValues.SetDefault + for details. + """ + pass + + +class DuplicateFlagError(DuplicateFlag): + """A DuplicateFlag whose message cites the conflicting definitions. + + A DuplicateFlagError conveys more information than a DuplicateFlag, + namely the modules where the conflicting definitions occur. This + class was created to avoid breaking external modules which depend on + the existing DuplicateFlags interface. + """ + + def __init__(self, flagname, flag_values, other_flag_values=None): + """Create a DuplicateFlagError. + + Args: + flagname: Name of the flag being redefined. + flag_values: FlagValues object containing the first definition of + flagname. + other_flag_values: If this argument is not None, it should be the + FlagValues object where the second definition of flagname occurs. + If it is None, we assume that we're being called when attempting + to create the flag a second time, and we use the module calling + this one as the source of the second definition. + """ + self.flagname = flagname + first_module = flag_values.FindModuleDefiningFlag( + flagname, default='') + if other_flag_values is None: + second_module = _GetCallingModule() + else: + second_module = other_flag_values.FindModuleDefiningFlag( + flagname, default='') + msg = "The flag '%s' is defined twice. First from %s, Second from %s" % ( + self.flagname, first_module, second_module) + DuplicateFlag.__init__(self, msg) + + +class IllegalFlagValue(FlagsError): + """The flag command line argument is illegal.""" + pass + + +class UnrecognizedFlag(FlagsError): + """Raised if a flag is unrecognized.""" + pass + + +# An UnrecognizedFlagError conveys more information than an UnrecognizedFlag. +# Since there are external modules that create DuplicateFlags, the interface to +# DuplicateFlag shouldn't change. The flagvalue will be assigned the full value +# of the flag and its argument, if any, allowing handling of unrecognized flags +# in an exception handler. +# If flagvalue is the empty string, then this exception is an due to a +# reference to a flag that was not already defined. +class UnrecognizedFlagError(UnrecognizedFlag): + def __init__(self, flagname, flagvalue=''): + self.flagname = flagname + self.flagvalue = flagvalue + UnrecognizedFlag.__init__( + self, "Unknown command line flag '%s'" % flagname) + +# Global variable used by expvar +_exported_flags = {} +_help_width = 80 # width of help output + + +def GetHelpWidth(): + """Returns: an integer, the width of help lines that is used in TextWrap.""" + if (not sys.stdout.isatty()) or (termios is None) or (fcntl is None): + return _help_width + try: + data = fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ, '1234') + columns = struct.unpack('hh', data)[1] + # Emacs mode returns 0. + # Here we assume that any value below 40 is unreasonable + if columns >= 40: + return columns + # Returning an int as default is fine, int(int) just return the int. + return int(os.getenv('COLUMNS', _help_width)) + + except (TypeError, IOError, struct.error): + return _help_width + + +def CutCommonSpacePrefix(text): + """Removes a common space prefix from the lines of a multiline text. + + If the first line does not start with a space, it is left as it is and + only in the remaining lines a common space prefix is being searched + for. That means the first line will stay untouched. This is especially + useful to turn doc strings into help texts. This is because some + people prefer to have the doc comment start already after the + apostrophe and then align the following lines while others have the + apostrophes on a separate line. + + The function also drops trailing empty lines and ignores empty lines + following the initial content line while calculating the initial + common whitespace. + + Args: + text: text to work on + + Returns: + the resulting text + """ + text_lines = text.splitlines() + # Drop trailing empty lines + while text_lines and not text_lines[-1]: + text_lines = text_lines[:-1] + if text_lines: + # We got some content, is the first line starting with a space? + if text_lines[0] and text_lines[0][0].isspace(): + text_first_line = [] + else: + text_first_line = [text_lines.pop(0)] + # Calculate length of common leading whitespace (only over content lines) + common_prefix = os.path.commonprefix([line for line in text_lines if line]) + space_prefix_len = len(common_prefix) - len(common_prefix.lstrip()) + # If we have a common space prefix, drop it from all lines + if space_prefix_len: + for index in xrange(len(text_lines)): + if text_lines[index]: + text_lines[index] = text_lines[index][space_prefix_len:] + return '\n'.join(text_first_line + text_lines) + return '' + + +def TextWrap(text, length=None, indent='', firstline_indent=None, tabs=' '): + """Wraps a given text to a maximum line length and returns it. + + We turn lines that only contain whitespace into empty lines. We keep + new lines and tabs (e.g., we do not treat tabs as spaces). + + Args: + text: text to wrap + length: maximum length of a line, includes indentation + if this is None then use GetHelpWidth() + indent: indent for all but first line + firstline_indent: indent for first line; if None, fall back to indent + tabs: replacement for tabs + + Returns: + wrapped text + + Raises: + FlagsError: if indent not shorter than length + FlagsError: if firstline_indent not shorter than length + """ + # Get defaults where callee used None + if length is None: + length = GetHelpWidth() + if indent is None: + indent = '' + if len(indent) >= length: + raise FlagsError('Indent must be shorter than length') + # In line we will be holding the current line which is to be started + # with indent (or firstline_indent if available) and then appended + # with words. + if firstline_indent is None: + firstline_indent = '' + line = indent + else: + line = firstline_indent + if len(firstline_indent) >= length: + raise FlagsError('First line indent must be shorter than length') + + # If the callee does not care about tabs we simply convert them to + # spaces If callee wanted tabs to be single space then we do that + # already here. + if not tabs or tabs == ' ': + text = text.replace('\t', ' ') + else: + tabs_are_whitespace = not tabs.strip() + + line_regex = re.compile('([ ]*)(\t*)([^ \t]+)', re.MULTILINE) + + # Split the text into lines and the lines with the regex above. The + # resulting lines are collected in result[]. For each split we get the + # spaces, the tabs and the next non white space (e.g. next word). + result = [] + for text_line in text.splitlines(): + # Store result length so we can find out whether processing the next + # line gave any new content + old_result_len = len(result) + # Process next line with line_regex. For optimization we do an rstrip(). + # - process tabs (changes either line or word, see below) + # - process word (first try to squeeze on line, then wrap or force wrap) + # Spaces found on the line are ignored, they get added while wrapping as + # needed. + for spaces, current_tabs, word in line_regex.findall(text_line.rstrip()): + # If tabs weren't converted to spaces, handle them now + if current_tabs: + # If the last thing we added was a space anyway then drop + # it. But let's not get rid of the indentation. + if (((result and line != indent) or + (not result and line != firstline_indent)) and line[-1] == ' '): + line = line[:-1] + # Add the tabs, if that means adding whitespace, just add it at + # the line, the rstrip() code while shorten the line down if + # necessary + if tabs_are_whitespace: + line += tabs * len(current_tabs) + else: + # if not all tab replacement is whitespace we prepend it to the word + word = tabs * len(current_tabs) + word + # Handle the case where word cannot be squeezed onto current last line + if len(line) + len(word) > length and len(indent) + len(word) <= length: + result.append(line.rstrip()) + line = indent + word + word = '' + # No space left on line or can we append a space? + if len(line) + 1 >= length: + result.append(line.rstrip()) + line = indent + else: + line += ' ' + # Add word and shorten it up to allowed line length. Restart next + # line with indent and repeat, or add a space if we're done (word + # finished) This deals with words that cannot fit on one line + # (e.g. indent + word longer than allowed line length). + while len(line) + len(word) >= length: + line += word + result.append(line[:length]) + word = line[length:] + line = indent + # Default case, simply append the word and a space + if word: + line += word + ' ' + # End of input line. If we have content we finish the line. If the + # current line is just the indent but we had content in during this + # original line then we need to add an empty line. + if (result and line != indent) or (not result and line != firstline_indent): + result.append(line.rstrip()) + elif len(result) == old_result_len: + result.append('') + line = indent + + return '\n'.join(result) + + +def DocToHelp(doc): + """Takes a __doc__ string and reformats it as help.""" + + # Get rid of starting and ending white space. Using lstrip() or even + # strip() could drop more than maximum of first line and right space + # of last line. + doc = doc.strip() + + # Get rid of all empty lines + whitespace_only_line = re.compile('^[ \t]+$', re.M) + doc = whitespace_only_line.sub('', doc) + + # Cut out common space at line beginnings + doc = CutCommonSpacePrefix(doc) + + # Just like this module's comment, comments tend to be aligned somehow. + # In other words they all start with the same amount of white space + # 1) keep double new lines + # 2) keep ws after new lines if not empty line + # 3) all other new lines shall be changed to a space + # Solution: Match new lines between non white space and replace with space. + doc = re.sub('(?<=\S)\n(?=\S)', ' ', doc, re.M) + + return doc + + +def _GetModuleObjectAndName(globals_dict): + """Returns the module that defines a global environment, and its name. + + Args: + globals_dict: A dictionary that should correspond to an environment + providing the values of the globals. + + Returns: + A pair consisting of (1) module object and (2) module name (a + string). Returns (None, None) if the module could not be + identified. + """ + # The use of .items() (instead of .iteritems()) is NOT a mistake: if + # a parallel thread imports a module while we iterate over + # .iteritems() (not nice, but possible), we get a RuntimeError ... + # Hence, we use the slightly slower but safer .items(). + for name, module in sys.modules.items(): + if getattr(module, '__dict__', None) is globals_dict: + if name == '__main__': + # Pick a more informative name for the main module. + name = sys.argv[0] + return (module, name) + return (None, None) + + +def _GetMainModule(): + """Returns: string, name of the module from which execution started.""" + # First, try to use the same logic used by _GetCallingModuleObjectAndName(), + # i.e., call _GetModuleObjectAndName(). For that we first need to + # find the dictionary that the main module uses to store the + # globals. + # + # That's (normally) the same dictionary object that the deepest + # (oldest) stack frame is using for globals. + deepest_frame = sys._getframe(0) + while deepest_frame.f_back is not None: + deepest_frame = deepest_frame.f_back + globals_for_main_module = deepest_frame.f_globals + main_module_name = _GetModuleObjectAndName(globals_for_main_module)[1] + # The above strategy fails in some cases (e.g., tools that compute + # code coverage by redefining, among other things, the main module). + # If so, just use sys.argv[0]. We can probably always do this, but + # it's safest to try to use the same logic as _GetCallingModuleObjectAndName() + if main_module_name is None: + main_module_name = sys.argv[0] + return main_module_name + + +class FlagValues: + """Registry of 'Flag' objects. + + A 'FlagValues' can then scan command line arguments, passing flag + arguments through to the 'Flag' objects that it owns. It also + provides easy access to the flag values. Typically only one + 'FlagValues' object is needed by an application: gflags.FLAGS + + This class is heavily overloaded: + + 'Flag' objects are registered via __setitem__: + FLAGS['longname'] = x # register a new flag + + The .value attribute of the registered 'Flag' objects can be accessed + as attributes of this 'FlagValues' object, through __getattr__. Both + the long and short name of the original 'Flag' objects can be used to + access its value: + FLAGS.longname # parsed flag value + FLAGS.x # parsed flag value (short name) + + Command line arguments are scanned and passed to the registered 'Flag' + objects through the __call__ method. Unparsed arguments, including + argv[0] (e.g. the program name) are returned. + argv = FLAGS(sys.argv) # scan command line arguments + + The original registered Flag objects can be retrieved through the use + of the dictionary-like operator, __getitem__: + x = FLAGS['longname'] # access the registered Flag object + + The str() operator of a 'FlagValues' object provides help for all of + the registered 'Flag' objects. + """ + + def __init__(self): + # Since everything in this class is so heavily overloaded, the only + # way of defining and using fields is to access __dict__ directly. + + # Dictionary: flag name (string) -> Flag object. + self.__dict__['__flags'] = {} + # Dictionary: module name (string) -> list of Flag objects that are defined + # by that module. + self.__dict__['__flags_by_module'] = {} + # Dictionary: module id (int) -> list of Flag objects that are defined by + # that module. + self.__dict__['__flags_by_module_id'] = {} + # Dictionary: module name (string) -> list of Flag objects that are + # key for that module. + self.__dict__['__key_flags_by_module'] = {} + + # Set if we should use new style gnu_getopt rather than getopt when parsing + # the args. Only possible with Python 2.3+ + self.UseGnuGetOpt(False) + + def UseGnuGetOpt(self, use_gnu_getopt=True): + """Use GNU-style scanning. Allows mixing of flag and non-flag arguments. + + See http://docs.python.org/library/getopt.html#getopt.gnu_getopt + + Args: + use_gnu_getopt: wether or not to use GNU style scanning. + """ + self.__dict__['__use_gnu_getopt'] = use_gnu_getopt + + def IsGnuGetOpt(self): + return self.__dict__['__use_gnu_getopt'] + + def FlagDict(self): + return self.__dict__['__flags'] + + def FlagsByModuleDict(self): + """Returns the dictionary of module_name -> list of defined flags. + + Returns: + A dictionary. Its keys are module names (strings). Its values + are lists of Flag objects. + """ + return self.__dict__['__flags_by_module'] + + def FlagsByModuleIdDict(self): + """Returns the dictionary of module_id -> list of defined flags. + + Returns: + A dictionary. Its keys are module IDs (ints). Its values + are lists of Flag objects. + """ + return self.__dict__['__flags_by_module_id'] + + def KeyFlagsByModuleDict(self): + """Returns the dictionary of module_name -> list of key flags. + + Returns: + A dictionary. Its keys are module names (strings). Its values + are lists of Flag objects. + """ + return self.__dict__['__key_flags_by_module'] + + def _RegisterFlagByModule(self, module_name, flag): + """Records the module that defines a specific flag. + + We keep track of which flag is defined by which module so that we + can later sort the flags by module. + + Args: + module_name: A string, the name of a Python module. + flag: A Flag object, a flag that is key to the module. + """ + flags_by_module = self.FlagsByModuleDict() + flags_by_module.setdefault(module_name, []).append(flag) + + def _RegisterFlagByModuleId(self, module_id, flag): + """Records the module that defines a specific flag. + + Args: + module_id: An int, the ID of the Python module. + flag: A Flag object, a flag that is key to the module. + """ + flags_by_module_id = self.FlagsByModuleIdDict() + flags_by_module_id.setdefault(module_id, []).append(flag) + + def _RegisterKeyFlagForModule(self, module_name, flag): + """Specifies that a flag is a key flag for a module. + + Args: + module_name: A string, the name of a Python module. + flag: A Flag object, a flag that is key to the module. + """ + key_flags_by_module = self.KeyFlagsByModuleDict() + # The list of key flags for the module named module_name. + key_flags = key_flags_by_module.setdefault(module_name, []) + # Add flag, but avoid duplicates. + if flag not in key_flags: + key_flags.append(flag) + + def _GetFlagsDefinedByModule(self, module): + """Returns the list of flags defined by a module. + + Args: + module: A module object or a module name (a string). + + Returns: + A new list of Flag objects. Caller may update this list as he + wishes: none of those changes will affect the internals of this + FlagValue object. + """ + if not isinstance(module, str): + module = module.__name__ + + return list(self.FlagsByModuleDict().get(module, [])) + + def _GetKeyFlagsForModule(self, module): + """Returns the list of key flags for a module. + + Args: + module: A module object or a module name (a string) + + Returns: + A new list of Flag objects. Caller may update this list as he + wishes: none of those changes will affect the internals of this + FlagValue object. + """ + if not isinstance(module, str): + module = module.__name__ + + # Any flag is a key flag for the module that defined it. NOTE: + # key_flags is a fresh list: we can update it without affecting the + # internals of this FlagValues object. + key_flags = self._GetFlagsDefinedByModule(module) + + # Take into account flags explicitly declared as key for a module. + for flag in self.KeyFlagsByModuleDict().get(module, []): + if flag not in key_flags: + key_flags.append(flag) + return key_flags + + def FindModuleDefiningFlag(self, flagname, default=None): + """Return the name of the module defining this flag, or default. + + Args: + flagname: Name of the flag to lookup. + default: Value to return if flagname is not defined. Defaults + to None. + + Returns: + The name of the module which registered the flag with this name. + If no such module exists (i.e. no flag with this name exists), + we return default. + """ + for module, flags in self.FlagsByModuleDict().iteritems(): + for flag in flags: + if flag.name == flagname or flag.short_name == flagname: + return module + return default + + def FindModuleIdDefiningFlag(self, flagname, default=None): + """Return the ID of the module defining this flag, or default. + + Args: + flagname: Name of the flag to lookup. + default: Value to return if flagname is not defined. Defaults + to None. + + Returns: + The ID of the module which registered the flag with this name. + If no such module exists (i.e. no flag with this name exists), + we return default. + """ + for module_id, flags in self.FlagsByModuleIdDict().iteritems(): + for flag in flags: + if flag.name == flagname or flag.short_name == flagname: + return module_id + return default + + def AppendFlagValues(self, flag_values): + """Appends flags registered in another FlagValues instance. + + Args: + flag_values: registry to copy from + """ + for flag_name, flag in flag_values.FlagDict().iteritems(): + # Each flags with shortname appears here twice (once under its + # normal name, and again with its short name). To prevent + # problems (DuplicateFlagError) with double flag registration, we + # perform a check to make sure that the entry we're looking at is + # for its normal name. + if flag_name == flag.name: + try: + self[flag_name] = flag + except DuplicateFlagError: + raise DuplicateFlagError(flag_name, self, + other_flag_values=flag_values) + + def RemoveFlagValues(self, flag_values): + """Remove flags that were previously appended from another FlagValues. + + Args: + flag_values: registry containing flags to remove. + """ + for flag_name in flag_values.FlagDict(): + self.__delattr__(flag_name) + + def __setitem__(self, name, flag): + """Registers a new flag variable.""" + fl = self.FlagDict() + if not isinstance(flag, Flag): + raise IllegalFlagValue(flag) + if not isinstance(name, type("")): + raise FlagsError("Flag name must be a string") + if len(name) == 0: + raise FlagsError("Flag name cannot be empty") + # If running under pychecker, duplicate keys are likely to be + # defined. Disable check for duplicate keys when pycheck'ing. + if (name in fl and not flag.allow_override and + not fl[name].allow_override and not _RUNNING_PYCHECKER): + module, module_name = _GetCallingModuleObjectAndName() + if (self.FindModuleDefiningFlag(name) == module_name and + id(module) != self.FindModuleIdDefiningFlag(name)): + # If the flag has already been defined by a module with the same name, + # but a different ID, we can stop here because it indicates that the + # module is simply being imported a subsequent time. + return + raise DuplicateFlagError(name, self) + short_name = flag.short_name + if short_name is not None: + if (short_name in fl and not flag.allow_override and + not fl[short_name].allow_override and not _RUNNING_PYCHECKER): + raise DuplicateFlagError(short_name, self) + fl[short_name] = flag + fl[name] = flag + global _exported_flags + _exported_flags[name] = flag + + def __getitem__(self, name): + """Retrieves the Flag object for the flag --name.""" + return self.FlagDict()[name] + + def __getattr__(self, name): + """Retrieves the 'value' attribute of the flag --name.""" + fl = self.FlagDict() + if name not in fl: + raise AttributeError(name) + return fl[name].value + + def __setattr__(self, name, value): + """Sets the 'value' attribute of the flag --name.""" + fl = self.FlagDict() + fl[name].value = value + self._AssertValidators(fl[name].validators) + return value + + def _AssertAllValidators(self): + all_validators = set() + for flag in self.FlagDict().itervalues(): + for validator in flag.validators: + all_validators.add(validator) + self._AssertValidators(all_validators) + + def _AssertValidators(self, validators): + """Assert if all validators in the list are satisfied. + + Asserts validators in the order they were created. + Args: + validators: Iterable(gflags_validators.Validator), validators to be + verified + Raises: + AttributeError: if validators work with a non-existing flag. + IllegalFlagValue: if validation fails for at least one validator + """ + for validator in sorted( + validators, key=lambda validator: validator.insertion_index): + try: + validator.Verify(self) + except gflags_validators.Error, e: + message = validator.PrintFlagsWithValues(self) + raise IllegalFlagValue('%s: %s' % (message, str(e))) + + def _FlagIsRegistered(self, flag_obj): + """Checks whether a Flag object is registered under some name. + + Note: this is non trivial: in addition to its normal name, a flag + may have a short name too. In self.FlagDict(), both the normal and + the short name are mapped to the same flag object. E.g., calling + only "del FLAGS.short_name" is not unregistering the corresponding + Flag object (it is still registered under the longer name). + + Args: + flag_obj: A Flag object. + + Returns: + A boolean: True iff flag_obj is registered under some name. + """ + flag_dict = self.FlagDict() + # Check whether flag_obj is registered under its long name. + name = flag_obj.name + if flag_dict.get(name, None) == flag_obj: + return True + # Check whether flag_obj is registered under its short name. + short_name = flag_obj.short_name + if (short_name is not None and + flag_dict.get(short_name, None) == flag_obj): + return True + # The flag cannot be registered under any other name, so we do not + # need to do a full search through the values of self.FlagDict(). + return False + + def __delattr__(self, flag_name): + """Deletes a previously-defined flag from a flag object. + + This method makes sure we can delete a flag by using + + del flag_values_object. + + E.g., + + gflags.DEFINE_integer('foo', 1, 'Integer flag.') + del gflags.FLAGS.foo + + Args: + flag_name: A string, the name of the flag to be deleted. + + Raises: + AttributeError: When there is no registered flag named flag_name. + """ + fl = self.FlagDict() + if flag_name not in fl: + raise AttributeError(flag_name) + + flag_obj = fl[flag_name] + del fl[flag_name] + + if not self._FlagIsRegistered(flag_obj): + # If the Flag object indicated by flag_name is no longer + # registered (please see the docstring of _FlagIsRegistered), then + # we delete the occurrences of the flag object in all our internal + # dictionaries. + self.__RemoveFlagFromDictByModule(self.FlagsByModuleDict(), flag_obj) + self.__RemoveFlagFromDictByModule(self.FlagsByModuleIdDict(), flag_obj) + self.__RemoveFlagFromDictByModule(self.KeyFlagsByModuleDict(), flag_obj) + + def __RemoveFlagFromDictByModule(self, flags_by_module_dict, flag_obj): + """Removes a flag object from a module -> list of flags dictionary. + + Args: + flags_by_module_dict: A dictionary that maps module names to lists of + flags. + flag_obj: A flag object. + """ + for unused_module, flags_in_module in flags_by_module_dict.iteritems(): + # while (as opposed to if) takes care of multiple occurrences of a + # flag in the list for the same module. + while flag_obj in flags_in_module: + flags_in_module.remove(flag_obj) + + def SetDefault(self, name, value): + """Changes the default value of the named flag object.""" + fl = self.FlagDict() + if name not in fl: + raise AttributeError(name) + fl[name].SetDefault(value) + self._AssertValidators(fl[name].validators) + + def __contains__(self, name): + """Returns True if name is a value (flag) in the dict.""" + return name in self.FlagDict() + + has_key = __contains__ # a synonym for __contains__() + + def __iter__(self): + return iter(self.FlagDict()) + + def __call__(self, argv): + """Parses flags from argv; stores parsed flags into this FlagValues object. + + All unparsed arguments are returned. Flags are parsed using the GNU + Program Argument Syntax Conventions, using getopt: + + http://www.gnu.org/software/libc/manual/html_mono/libc.html#Getopt + + Args: + argv: argument list. Can be of any type that may be converted to a list. + + Returns: + The list of arguments not parsed as options, including argv[0] + + Raises: + FlagsError: on any parsing error + """ + # Support any sequence type that can be converted to a list + argv = list(argv) + + shortopts = "" + longopts = [] + + fl = self.FlagDict() + + # This pre parses the argv list for --flagfile=<> options. + argv = argv[:1] + self.ReadFlagsFromFiles(argv[1:], force_gnu=False) + + # Correct the argv to support the google style of passing boolean + # parameters. Boolean parameters may be passed by using --mybool, + # --nomybool, --mybool=(true|false|1|0). getopt does not support + # having options that may or may not have a parameter. We replace + # instances of the short form --mybool and --nomybool with their + # full forms: --mybool=(true|false). + original_argv = list(argv) # list() makes a copy + shortest_matches = None + for name, flag in fl.items(): + if not flag.boolean: + continue + if shortest_matches is None: + # Determine the smallest allowable prefix for all flag names + shortest_matches = self.ShortestUniquePrefixes(fl) + no_name = 'no' + name + prefix = shortest_matches[name] + no_prefix = shortest_matches[no_name] + + # Replace all occurrences of this boolean with extended forms + for arg_idx in range(1, len(argv)): + arg = argv[arg_idx] + if arg.find('=') >= 0: continue + if arg.startswith('--'+prefix) and ('--'+name).startswith(arg): + argv[arg_idx] = ('--%s=true' % name) + elif arg.startswith('--'+no_prefix) and ('--'+no_name).startswith(arg): + argv[arg_idx] = ('--%s=false' % name) + + # Loop over all of the flags, building up the lists of short options + # and long options that will be passed to getopt. Short options are + # specified as a string of letters, each letter followed by a colon + # if it takes an argument. Long options are stored in an array of + # strings. Each string ends with an '=' if it takes an argument. + for name, flag in fl.items(): + longopts.append(name + "=") + if len(name) == 1: # one-letter option: allow short flag type also + shortopts += name + if not flag.boolean: + shortopts += ":" + + longopts.append('undefok=') + undefok_flags = [] + + # In case --undefok is specified, loop to pick up unrecognized + # options one by one. + unrecognized_opts = [] + args = argv[1:] + while True: + try: + if self.__dict__['__use_gnu_getopt']: + optlist, unparsed_args = getopt.gnu_getopt(args, shortopts, longopts) + else: + optlist, unparsed_args = getopt.getopt(args, shortopts, longopts) + break + except getopt.GetoptError, e: + if not e.opt or e.opt in fl: + # Not an unrecognized option, re-raise the exception as a FlagsError + raise FlagsError(e) + # Remove offender from args and try again + for arg_index in range(len(args)): + if ((args[arg_index] == '--' + e.opt) or + (args[arg_index] == '-' + e.opt) or + (args[arg_index].startswith('--' + e.opt + '='))): + unrecognized_opts.append((e.opt, args[arg_index])) + args = args[0:arg_index] + args[arg_index+1:] + break + else: + # We should have found the option, so we don't expect to get + # here. We could assert, but raising the original exception + # might work better. + raise FlagsError(e) + + for name, arg in optlist: + if name == '--undefok': + flag_names = arg.split(',') + undefok_flags.extend(flag_names) + # For boolean flags, if --undefok=boolflag is specified, then we should + # also accept --noboolflag, in addition to --boolflag. + # Since we don't know the type of the undefok'd flag, this will affect + # non-boolean flags as well. + # NOTE: You shouldn't use --undefok=noboolflag, because then we will + # accept --nonoboolflag here. We are choosing not to do the conversion + # from noboolflag -> boolflag because of the ambiguity that flag names + # can start with 'no'. + undefok_flags.extend('no' + name for name in flag_names) + continue + if name.startswith('--'): + # long option + name = name[2:] + short_option = 0 + else: + # short option + name = name[1:] + short_option = 1 + if name in fl: + flag = fl[name] + if flag.boolean and short_option: arg = 1 + flag.Parse(arg) + + # If there were unrecognized options, raise an exception unless + # the options were named via --undefok. + for opt, value in unrecognized_opts: + if opt not in undefok_flags: + raise UnrecognizedFlagError(opt, value) + + if unparsed_args: + if self.__dict__['__use_gnu_getopt']: + # if using gnu_getopt just return the program name + remainder of argv. + ret_val = argv[:1] + unparsed_args + else: + # unparsed_args becomes the first non-flag detected by getopt to + # the end of argv. Because argv may have been modified above, + # return original_argv for this region. + ret_val = argv[:1] + original_argv[-len(unparsed_args):] + else: + ret_val = argv[:1] + + self._AssertAllValidators() + return ret_val + + def Reset(self): + """Resets the values to the point before FLAGS(argv) was called.""" + for f in self.FlagDict().values(): + f.Unparse() + + def RegisteredFlags(self): + """Returns: a list of the names and short names of all registered flags.""" + return list(self.FlagDict()) + + def FlagValuesDict(self): + """Returns: a dictionary that maps flag names to flag values.""" + flag_values = {} + + for flag_name in self.RegisteredFlags(): + flag = self.FlagDict()[flag_name] + flag_values[flag_name] = flag.value + + return flag_values + + def __str__(self): + """Generates a help string for all known flags.""" + return self.GetHelp() + + def GetHelp(self, prefix=''): + """Generates a help string for all known flags.""" + helplist = [] + + flags_by_module = self.FlagsByModuleDict() + if flags_by_module: + + modules = sorted(flags_by_module) + + # Print the help for the main module first, if possible. + main_module = _GetMainModule() + if main_module in modules: + modules.remove(main_module) + modules = [main_module] + modules + + for module in modules: + self.__RenderOurModuleFlags(module, helplist) + + self.__RenderModuleFlags('gflags', + _SPECIAL_FLAGS.FlagDict().values(), + helplist) + + else: + # Just print one long list of flags. + self.__RenderFlagList( + self.FlagDict().values() + _SPECIAL_FLAGS.FlagDict().values(), + helplist, prefix) + + return '\n'.join(helplist) + + def __RenderModuleFlags(self, module, flags, output_lines, prefix=""): + """Generates a help string for a given module.""" + if not isinstance(module, str): + module = module.__name__ + output_lines.append('\n%s%s:' % (prefix, module)) + self.__RenderFlagList(flags, output_lines, prefix + " ") + + def __RenderOurModuleFlags(self, module, output_lines, prefix=""): + """Generates a help string for a given module.""" + flags = self._GetFlagsDefinedByModule(module) + if flags: + self.__RenderModuleFlags(module, flags, output_lines, prefix) + + def __RenderOurModuleKeyFlags(self, module, output_lines, prefix=""): + """Generates a help string for the key flags of a given module. + + Args: + module: A module object or a module name (a string). + output_lines: A list of strings. The generated help message + lines will be appended to this list. + prefix: A string that is prepended to each generated help line. + """ + key_flags = self._GetKeyFlagsForModule(module) + if key_flags: + self.__RenderModuleFlags(module, key_flags, output_lines, prefix) + + def ModuleHelp(self, module): + """Describe the key flags of a module. + + Args: + module: A module object or a module name (a string). + + Returns: + string describing the key flags of a module. + """ + helplist = [] + self.__RenderOurModuleKeyFlags(module, helplist) + return '\n'.join(helplist) + + def MainModuleHelp(self): + """Describe the key flags of the main module. + + Returns: + string describing the key flags of a module. + """ + return self.ModuleHelp(_GetMainModule()) + + def __RenderFlagList(self, flaglist, output_lines, prefix=" "): + fl = self.FlagDict() + special_fl = _SPECIAL_FLAGS.FlagDict() + flaglist = [(flag.name, flag) for flag in flaglist] + flaglist.sort() + flagset = {} + for (name, flag) in flaglist: + # It's possible this flag got deleted or overridden since being + # registered in the per-module flaglist. Check now against the + # canonical source of current flag information, the FlagDict. + if fl.get(name, None) != flag and special_fl.get(name, None) != flag: + # a different flag is using this name now + continue + # only print help once + if flag in flagset: continue + flagset[flag] = 1 + flaghelp = "" + if flag.short_name: flaghelp += "-%s," % flag.short_name + if flag.boolean: + flaghelp += "--[no]%s" % flag.name + ":" + else: + flaghelp += "--%s" % flag.name + ":" + flaghelp += " " + if flag.help: + flaghelp += flag.help + flaghelp = TextWrap(flaghelp, indent=prefix+" ", + firstline_indent=prefix) + if flag.default_as_str: + flaghelp += "\n" + flaghelp += TextWrap("(default: %s)" % flag.default_as_str, + indent=prefix+" ") + if flag.parser.syntactic_help: + flaghelp += "\n" + flaghelp += TextWrap("(%s)" % flag.parser.syntactic_help, + indent=prefix+" ") + output_lines.append(flaghelp) + + def get(self, name, default): + """Returns the value of a flag (if not None) or a default value. + + Args: + name: A string, the name of a flag. + default: Default value to use if the flag value is None. + """ + + value = self.__getattr__(name) + if value is not None: # Can't do if not value, b/c value might be '0' or "" + return value + else: + return default + + def ShortestUniquePrefixes(self, fl): + """Returns: dictionary; maps flag names to their shortest unique prefix.""" + # Sort the list of flag names + sorted_flags = [] + for name, flag in fl.items(): + sorted_flags.append(name) + if flag.boolean: + sorted_flags.append('no%s' % name) + sorted_flags.sort() + + # For each name in the sorted list, determine the shortest unique + # prefix by comparing itself to the next name and to the previous + # name (the latter check uses cached info from the previous loop). + shortest_matches = {} + prev_idx = 0 + for flag_idx in range(len(sorted_flags)): + curr = sorted_flags[flag_idx] + if flag_idx == (len(sorted_flags) - 1): + next = None + else: + next = sorted_flags[flag_idx+1] + next_len = len(next) + for curr_idx in range(len(curr)): + if (next is None + or curr_idx >= next_len + or curr[curr_idx] != next[curr_idx]): + # curr longer than next or no more chars in common + shortest_matches[curr] = curr[:max(prev_idx, curr_idx) + 1] + prev_idx = curr_idx + break + else: + # curr shorter than (or equal to) next + shortest_matches[curr] = curr + prev_idx = curr_idx + 1 # next will need at least one more char + return shortest_matches + + def __IsFlagFileDirective(self, flag_string): + """Checks whether flag_string contain a --flagfile= directive.""" + if isinstance(flag_string, type("")): + if flag_string.startswith('--flagfile='): + return 1 + elif flag_string == '--flagfile': + return 1 + elif flag_string.startswith('-flagfile='): + return 1 + elif flag_string == '-flagfile': + return 1 + else: + return 0 + return 0 + + def ExtractFilename(self, flagfile_str): + """Returns filename from a flagfile_str of form -[-]flagfile=filename. + + The cases of --flagfile foo and -flagfile foo shouldn't be hitting + this function, as they are dealt with in the level above this + function. + """ + if flagfile_str.startswith('--flagfile='): + return os.path.expanduser((flagfile_str[(len('--flagfile=')):]).strip()) + elif flagfile_str.startswith('-flagfile='): + return os.path.expanduser((flagfile_str[(len('-flagfile=')):]).strip()) + else: + raise FlagsError('Hit illegal --flagfile type: %s' % flagfile_str) + + def __GetFlagFileLines(self, filename, parsed_file_list): + """Returns the useful (!=comments, etc) lines from a file with flags. + + Args: + filename: A string, the name of the flag file. + parsed_file_list: A list of the names of the files we have + already read. MUTATED BY THIS FUNCTION. + + Returns: + List of strings. See the note below. + + NOTE(springer): This function checks for a nested --flagfile= + tag and handles the lower file recursively. It returns a list of + all the lines that _could_ contain command flags. This is + EVERYTHING except whitespace lines and comments (lines starting + with '#' or '//'). + """ + line_list = [] # All line from flagfile. + flag_line_list = [] # Subset of lines w/o comments, blanks, flagfile= tags. + try: + file_obj = open(filename, 'r') + except IOError, e_msg: + raise CantOpenFlagFileError('ERROR:: Unable to open flagfile: %s' % e_msg) + + line_list = file_obj.readlines() + file_obj.close() + parsed_file_list.append(filename) + + # This is where we check each line in the file we just read. + for line in line_list: + if line.isspace(): + pass + # Checks for comment (a line that starts with '#'). + elif line.startswith('#') or line.startswith('//'): + pass + # Checks for a nested "--flagfile=" flag in the current file. + # If we find one, recursively parse down into that file. + elif self.__IsFlagFileDirective(line): + sub_filename = self.ExtractFilename(line) + # We do a little safety check for reparsing a file we've already done. + if not sub_filename in parsed_file_list: + included_flags = self.__GetFlagFileLines(sub_filename, + parsed_file_list) + flag_line_list.extend(included_flags) + else: # Case of hitting a circularly included file. + sys.stderr.write('Warning: Hit circular flagfile dependency: %s\n' % + (sub_filename,)) + else: + # Any line that's not a comment or a nested flagfile should get + # copied into 2nd position. This leaves earlier arguments + # further back in the list, thus giving them higher priority. + flag_line_list.append(line.strip()) + return flag_line_list + + def ReadFlagsFromFiles(self, argv, force_gnu=True): + """Processes command line args, but also allow args to be read from file. + + Args: + argv: A list of strings, usually sys.argv[1:], which may contain one or + more flagfile directives of the form --flagfile="./filename". + Note that the name of the program (sys.argv[0]) should be omitted. + force_gnu: If False, --flagfile parsing obeys normal flag semantics. + If True, --flagfile parsing instead follows gnu_getopt semantics. + *** WARNING *** force_gnu=False may become the future default! + + Returns: + + A new list which has the original list combined with what we read + from any flagfile(s). + + References: Global gflags.FLAG class instance. + + This function should be called before the normal FLAGS(argv) call. + This function scans the input list for a flag that looks like: + --flagfile=. Then it opens , reads all valid key + and value pairs and inserts them into the input list between the + first item of the list and any subsequent items in the list. + + Note that your application's flags are still defined the usual way + using gflags DEFINE_flag() type functions. + + Notes (assuming we're getting a commandline of some sort as our input): + --> Flags from the command line argv _should_ always take precedence! + --> A further "--flagfile=" CAN be nested in a flagfile. + It will be processed after the parent flag file is done. + --> For duplicate flags, first one we hit should "win". + --> In a flagfile, a line beginning with # or // is a comment. + --> Entirely blank lines _should_ be ignored. + """ + parsed_file_list = [] + rest_of_args = argv + new_argv = [] + while rest_of_args: + current_arg = rest_of_args[0] + rest_of_args = rest_of_args[1:] + if self.__IsFlagFileDirective(current_arg): + # This handles the case of -(-)flagfile foo. In this case the + # next arg really is part of this one. + if current_arg == '--flagfile' or current_arg == '-flagfile': + if not rest_of_args: + raise IllegalFlagValue('--flagfile with no argument') + flag_filename = os.path.expanduser(rest_of_args[0]) + rest_of_args = rest_of_args[1:] + else: + # This handles the case of (-)-flagfile=foo. + flag_filename = self.ExtractFilename(current_arg) + new_argv.extend( + self.__GetFlagFileLines(flag_filename, parsed_file_list)) + else: + new_argv.append(current_arg) + # Stop parsing after '--', like getopt and gnu_getopt. + if current_arg == '--': + break + # Stop parsing after a non-flag, like getopt. + if not current_arg.startswith('-'): + if not force_gnu and not self.__dict__['__use_gnu_getopt']: + break + + if rest_of_args: + new_argv.extend(rest_of_args) + + return new_argv + + def FlagsIntoString(self): + """Returns a string with the flags assignments from this FlagValues object. + + This function ignores flags whose value is None. Each flag + assignment is separated by a newline. + + NOTE: MUST mirror the behavior of the C++ CommandlineFlagsIntoString + from http://code.google.com/p/google-gflags + """ + s = '' + for flag in self.FlagDict().values(): + if flag.value is not None: + s += flag.Serialize() + '\n' + return s + + def AppendFlagsIntoFile(self, filename): + """Appends all flags assignments from this FlagInfo object to a file. + + Output will be in the format of a flagfile. + + NOTE: MUST mirror the behavior of the C++ AppendFlagsIntoFile + from http://code.google.com/p/google-gflags + """ + out_file = open(filename, 'a') + out_file.write(self.FlagsIntoString()) + out_file.close() + + def WriteHelpInXMLFormat(self, outfile=None): + """Outputs flag documentation in XML format. + + NOTE: We use element names that are consistent with those used by + the C++ command-line flag library, from + http://code.google.com/p/google-gflags + We also use a few new elements (e.g., ), but we do not + interfere / overlap with existing XML elements used by the C++ + library. Please maintain this consistency. + + Args: + outfile: File object we write to. Default None means sys.stdout. + """ + outfile = outfile or sys.stdout + + outfile.write('\n') + outfile.write('\n') + indent = ' ' + _WriteSimpleXMLElement(outfile, 'program', os.path.basename(sys.argv[0]), + indent) + + usage_doc = sys.modules['__main__'].__doc__ + if not usage_doc: + usage_doc = '\nUSAGE: %s [flags]\n' % sys.argv[0] + else: + usage_doc = usage_doc.replace('%s', sys.argv[0]) + _WriteSimpleXMLElement(outfile, 'usage', usage_doc, indent) + + # Get list of key flags for the main module. + key_flags = self._GetKeyFlagsForModule(_GetMainModule()) + + # Sort flags by declaring module name and next by flag name. + flags_by_module = self.FlagsByModuleDict() + all_module_names = list(flags_by_module.keys()) + all_module_names.sort() + for module_name in all_module_names: + flag_list = [(f.name, f) for f in flags_by_module[module_name]] + flag_list.sort() + for unused_flag_name, flag in flag_list: + is_key = flag in key_flags + flag.WriteInfoInXMLFormat(outfile, module_name, + is_key=is_key, indent=indent) + + outfile.write('\n') + outfile.flush() + + def AddValidator(self, validator): + """Register new flags validator to be checked. + + Args: + validator: gflags_validators.Validator + Raises: + AttributeError: if validators work with a non-existing flag. + """ + for flag_name in validator.GetFlagsNames(): + flag = self.FlagDict()[flag_name] + flag.validators.append(validator) + +# end of FlagValues definition + + +# The global FlagValues instance +FLAGS = FlagValues() + + +def _StrOrUnicode(value): + """Converts value to a python string or, if necessary, unicode-string.""" + try: + return str(value) + except UnicodeEncodeError: + return unicode(value) + + +def _MakeXMLSafe(s): + """Escapes <, >, and & from s, and removes XML 1.0-illegal chars.""" + s = cgi.escape(s) # Escape <, >, and & + # Remove characters that cannot appear in an XML 1.0 document + # (http://www.w3.org/TR/REC-xml/#charsets). + # + # NOTE: if there are problems with current solution, one may move to + # XML 1.1, which allows such chars, if they're entity-escaped (&#xHH;). + s = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f]', '', s) + # Convert non-ascii characters to entities. Note: requires python >=2.3 + s = s.encode('ascii', 'xmlcharrefreplace') # u'\xce\x88' -> 'uΈ' + return s + + +def _WriteSimpleXMLElement(outfile, name, value, indent): + """Writes a simple XML element. + + Args: + outfile: File object we write the XML element to. + name: A string, the name of XML element. + value: A Python object, whose string representation will be used + as the value of the XML element. + indent: A string, prepended to each line of generated output. + """ + value_str = _StrOrUnicode(value) + if isinstance(value, bool): + # Display boolean values as the C++ flag library does: no caps. + value_str = value_str.lower() + safe_value_str = _MakeXMLSafe(value_str) + outfile.write('%s<%s>%s\n' % (indent, name, safe_value_str, name)) + + +class Flag: + """Information about a command-line flag. + + 'Flag' objects define the following fields: + .name - the name for this flag + .default - the default value for this flag + .default_as_str - default value as repr'd string, e.g., "'true'" (or None) + .value - the most recent parsed value of this flag; set by Parse() + .help - a help string or None if no help is available + .short_name - the single letter alias for this flag (or None) + .boolean - if 'true', this flag does not accept arguments + .present - true if this flag was parsed from command line flags. + .parser - an ArgumentParser object + .serializer - an ArgumentSerializer object + .allow_override - the flag may be redefined without raising an error + + The only public method of a 'Flag' object is Parse(), but it is + typically only called by a 'FlagValues' object. The Parse() method is + a thin wrapper around the 'ArgumentParser' Parse() method. The parsed + value is saved in .value, and the .present attribute is updated. If + this flag was already present, a FlagsError is raised. + + Parse() is also called during __init__ to parse the default value and + initialize the .value attribute. This enables other python modules to + safely use flags even if the __main__ module neglects to parse the + command line arguments. The .present attribute is cleared after + __init__ parsing. If the default value is set to None, then the + __init__ parsing step is skipped and the .value attribute is + initialized to None. + + Note: The default value is also presented to the user in the help + string, so it is important that it be a legal value for this flag. + """ + + def __init__(self, parser, serializer, name, default, help_string, + short_name=None, boolean=0, allow_override=0): + self.name = name + + if not help_string: + help_string = '(no help available)' + + self.help = help_string + self.short_name = short_name + self.boolean = boolean + self.present = 0 + self.parser = parser + self.serializer = serializer + self.allow_override = allow_override + self.value = None + self.validators = [] + + self.SetDefault(default) + + def __hash__(self): + return hash(id(self)) + + def __eq__(self, other): + return self is other + + def __lt__(self, other): + if isinstance(other, Flag): + return id(self) < id(other) + return NotImplemented + + def __GetParsedValueAsString(self, value): + if value is None: + return None + if self.serializer: + return repr(self.serializer.Serialize(value)) + if self.boolean: + if value: + return repr('true') + else: + return repr('false') + return repr(_StrOrUnicode(value)) + + def Parse(self, argument): + try: + self.value = self.parser.Parse(argument) + except ValueError, e: # recast ValueError as IllegalFlagValue + raise IllegalFlagValue("flag --%s=%s: %s" % (self.name, argument, e)) + self.present += 1 + + def Unparse(self): + if self.default is None: + self.value = None + else: + self.Parse(self.default) + self.present = 0 + + def Serialize(self): + if self.value is None: + return '' + if self.boolean: + if self.value: + return "--%s" % self.name + else: + return "--no%s" % self.name + else: + if not self.serializer: + raise FlagsError("Serializer not present for flag %s" % self.name) + return "--%s=%s" % (self.name, self.serializer.Serialize(self.value)) + + def SetDefault(self, value): + """Changes the default value (and current value too) for this Flag.""" + # We can't allow a None override because it may end up not being + # passed to C++ code when we're overriding C++ flags. So we + # cowardly bail out until someone fixes the semantics of trying to + # pass None to a C++ flag. See swig_flags.Init() for details on + # this behavior. + # TODO(olexiy): Users can directly call this method, bypassing all flags + # validators (we don't have FlagValues here, so we can not check + # validators). + # The simplest solution I see is to make this method private. + # Another approach would be to store reference to the corresponding + # FlagValues with each flag, but this seems to be an overkill. + if value is None and self.allow_override: + raise DuplicateFlagCannotPropagateNoneToSwig(self.name) + + self.default = value + self.Unparse() + self.default_as_str = self.__GetParsedValueAsString(self.value) + + def Type(self): + """Returns: a string that describes the type of this Flag.""" + # NOTE: we use strings, and not the types.*Type constants because + # our flags can have more exotic types, e.g., 'comma separated list + # of strings', 'whitespace separated list of strings', etc. + return self.parser.Type() + + def WriteInfoInXMLFormat(self, outfile, module_name, is_key=False, indent=''): + """Writes common info about this flag, in XML format. + + This is information that is relevant to all flags (e.g., name, + meaning, etc.). If you defined a flag that has some other pieces of + info, then please override _WriteCustomInfoInXMLFormat. + + Please do NOT override this method. + + Args: + outfile: File object we write to. + module_name: A string, the name of the module that defines this flag. + is_key: A boolean, True iff this flag is key for main module. + indent: A string that is prepended to each generated line. + """ + outfile.write(indent + '\n') + inner_indent = indent + ' ' + if is_key: + _WriteSimpleXMLElement(outfile, 'key', 'yes', inner_indent) + _WriteSimpleXMLElement(outfile, 'file', module_name, inner_indent) + # Print flag features that are relevant for all flags. + _WriteSimpleXMLElement(outfile, 'name', self.name, inner_indent) + if self.short_name: + _WriteSimpleXMLElement(outfile, 'short_name', self.short_name, + inner_indent) + if self.help: + _WriteSimpleXMLElement(outfile, 'meaning', self.help, inner_indent) + # The default flag value can either be represented as a string like on the + # command line, or as a Python object. We serialize this value in the + # latter case in order to remain consistent. + if self.serializer and not isinstance(self.default, str): + default_serialized = self.serializer.Serialize(self.default) + else: + default_serialized = self.default + _WriteSimpleXMLElement(outfile, 'default', default_serialized, inner_indent) + _WriteSimpleXMLElement(outfile, 'current', self.value, inner_indent) + _WriteSimpleXMLElement(outfile, 'type', self.Type(), inner_indent) + # Print extra flag features this flag may have. + self._WriteCustomInfoInXMLFormat(outfile, inner_indent) + outfile.write(indent + '\n') + + def _WriteCustomInfoInXMLFormat(self, outfile, indent): + """Writes extra info about this flag, in XML format. + + "Extra" means "not already printed by WriteInfoInXMLFormat above." + + Args: + outfile: File object we write to. + indent: A string that is prepended to each generated line. + """ + # Usually, the parser knows the extra details about the flag, so + # we just forward the call to it. + self.parser.WriteCustomInfoInXMLFormat(outfile, indent) +# End of Flag definition + + +class _ArgumentParserCache(type): + """Metaclass used to cache and share argument parsers among flags.""" + + _instances = {} + + def __call__(mcs, *args, **kwargs): + """Returns an instance of the argument parser cls. + + This method overrides behavior of the __new__ methods in + all subclasses of ArgumentParser (inclusive). If an instance + for mcs with the same set of arguments exists, this instance is + returned, otherwise a new instance is created. + + If any keyword arguments are defined, or the values in args + are not hashable, this method always returns a new instance of + cls. + + Args: + args: Positional initializer arguments. + kwargs: Initializer keyword arguments. + + Returns: + An instance of cls, shared or new. + """ + if kwargs: + return type.__call__(mcs, *args, **kwargs) + else: + instances = mcs._instances + key = (mcs,) + tuple(args) + try: + return instances[key] + except KeyError: + # No cache entry for key exists, create a new one. + return instances.setdefault(key, type.__call__(mcs, *args)) + except TypeError: + # An object in args cannot be hashed, always return + # a new instance. + return type.__call__(mcs, *args) + + +class ArgumentParser(object): + """Base class used to parse and convert arguments. + + The Parse() method checks to make sure that the string argument is a + legal value and convert it to a native type. If the value cannot be + converted, it should throw a 'ValueError' exception with a human + readable explanation of why the value is illegal. + + Subclasses should also define a syntactic_help string which may be + presented to the user to describe the form of the legal values. + + Argument parser classes must be stateless, since instances are cached + and shared between flags. Initializer arguments are allowed, but all + member variables must be derived from initializer arguments only. + """ + __metaclass__ = _ArgumentParserCache + + syntactic_help = "" + + def Parse(self, argument): + """Default implementation: always returns its argument unmodified.""" + return argument + + def Type(self): + return 'string' + + def WriteCustomInfoInXMLFormat(self, outfile, indent): + pass + + +class ArgumentSerializer: + """Base class for generating string representations of a flag value.""" + + def Serialize(self, value): + return _StrOrUnicode(value) + + +class ListSerializer(ArgumentSerializer): + + def __init__(self, list_sep): + self.list_sep = list_sep + + def Serialize(self, value): + return self.list_sep.join([_StrOrUnicode(x) for x in value]) + + +# Flags validators + + +def RegisterValidator(flag_name, + checker, + message='Flag validation failed', + flag_values=FLAGS): + """Adds a constraint, which will be enforced during program execution. + + The constraint is validated when flags are initially parsed, and after each + change of the corresponding flag's value. + Args: + flag_name: string, name of the flag to be checked. + checker: method to validate the flag. + input - value of the corresponding flag (string, boolean, etc. + This value will be passed to checker by the library). See file's + docstring for examples. + output - Boolean. + Must return True if validator constraint is satisfied. + If constraint is not satisfied, it should either return False or + raise gflags_validators.Error(desired_error_message). + message: error text to be shown to the user if checker returns False. + If checker raises gflags_validators.Error, message from the raised + Error will be shown. + flag_values: FlagValues + Raises: + AttributeError: if flag_name is not registered as a valid flag name. + """ + flag_values.AddValidator(gflags_validators.SimpleValidator(flag_name, + checker, + message)) + + +def MarkFlagAsRequired(flag_name, flag_values=FLAGS): + """Ensure that flag is not None during program execution. + + Registers a flag validator, which will follow usual validator + rules. + Args: + flag_name: string, name of the flag + flag_values: FlagValues + Raises: + AttributeError: if flag_name is not registered as a valid flag name. + """ + RegisterValidator(flag_name, + lambda value: value is not None, + message='Flag --%s must be specified.' % flag_name, + flag_values=flag_values) + + +def _RegisterBoundsValidatorIfNeeded(parser, name, flag_values): + """Enforce lower and upper bounds for numeric flags. + + Args: + parser: NumericParser (either FloatParser or IntegerParser). Provides lower + and upper bounds, and help text to display. + name: string, name of the flag + flag_values: FlagValues + """ + if parser.lower_bound is not None or parser.upper_bound is not None: + + def Checker(value): + if value is not None and parser.IsOutsideBounds(value): + message = '%s is not %s' % (value, parser.syntactic_help) + raise gflags_validators.Error(message) + return True + + RegisterValidator(name, + Checker, + flag_values=flag_values) + + +# The DEFINE functions are explained in mode details in the module doc string. + + +def DEFINE(parser, name, default, help, flag_values=FLAGS, serializer=None, + **args): + """Registers a generic Flag object. + + NOTE: in the docstrings of all DEFINE* functions, "registers" is short + for "creates a new flag and registers it". + + Auxiliary function: clients should use the specialized DEFINE_ + function instead. + + Args: + parser: ArgumentParser that is used to parse the flag arguments. + name: A string, the flag name. + default: The default value of the flag. + help: A help string. + flag_values: FlagValues object the flag will be registered with. + serializer: ArgumentSerializer that serializes the flag value. + args: Dictionary with extra keyword args that are passes to the + Flag __init__. + """ + DEFINE_flag(Flag(parser, serializer, name, default, help, **args), + flag_values) + + +def DEFINE_flag(flag, flag_values=FLAGS): + """Registers a 'Flag' object with a 'FlagValues' object. + + By default, the global FLAGS 'FlagValue' object is used. + + Typical users will use one of the more specialized DEFINE_xxx + functions, such as DEFINE_string or DEFINE_integer. But developers + who need to create Flag objects themselves should use this function + to register their flags. + """ + # copying the reference to flag_values prevents pychecker warnings + fv = flag_values + fv[flag.name] = flag + # Tell flag_values who's defining the flag. + if isinstance(flag_values, FlagValues): + # Regarding the above isinstance test: some users pass funny + # values of flag_values (e.g., {}) in order to avoid the flag + # registration (in the past, there used to be a flag_values == + # FLAGS test here) and redefine flags with the same name (e.g., + # debug). To avoid breaking their code, we perform the + # registration only if flag_values is a real FlagValues object. + module, module_name = _GetCallingModuleObjectAndName() + flag_values._RegisterFlagByModule(module_name, flag) + flag_values._RegisterFlagByModuleId(id(module), flag) + + +def _InternalDeclareKeyFlags(flag_names, + flag_values=FLAGS, key_flag_values=None): + """Declares a flag as key for the calling module. + + Internal function. User code should call DECLARE_key_flag or + ADOPT_module_key_flags instead. + + Args: + flag_names: A list of strings that are names of already-registered + Flag objects. + flag_values: A FlagValues object that the flags listed in + flag_names have registered with (the value of the flag_values + argument from the DEFINE_* calls that defined those flags). + This should almost never need to be overridden. + key_flag_values: A FlagValues object that (among possibly many + other things) keeps track of the key flags for each module. + Default None means "same as flag_values". This should almost + never need to be overridden. + + Raises: + UnrecognizedFlagError: when we refer to a flag that was not + defined yet. + """ + key_flag_values = key_flag_values or flag_values + + module = _GetCallingModule() + + for flag_name in flag_names: + if flag_name not in flag_values: + raise UnrecognizedFlagError(flag_name) + flag = flag_values.FlagDict()[flag_name] + key_flag_values._RegisterKeyFlagForModule(module, flag) + + +def DECLARE_key_flag(flag_name, flag_values=FLAGS): + """Declares one flag as key to the current module. + + Key flags are flags that are deemed really important for a module. + They are important when listing help messages; e.g., if the + --helpshort command-line flag is used, then only the key flags of the + main module are listed (instead of all flags, as in the case of + --help). + + Sample usage: + + gflags.DECLARED_key_flag('flag_1') + + Args: + flag_name: A string, the name of an already declared flag. + (Redeclaring flags as key, including flags implicitly key + because they were declared in this module, is a no-op.) + flag_values: A FlagValues object. This should almost never + need to be overridden. + """ + if flag_name in _SPECIAL_FLAGS: + # Take care of the special flags, e.g., --flagfile, --undefok. + # These flags are defined in _SPECIAL_FLAGS, and are treated + # specially during flag parsing, taking precedence over the + # user-defined flags. + _InternalDeclareKeyFlags([flag_name], + flag_values=_SPECIAL_FLAGS, + key_flag_values=flag_values) + return + _InternalDeclareKeyFlags([flag_name], flag_values=flag_values) + + +def ADOPT_module_key_flags(module, flag_values=FLAGS): + """Declares that all flags key to a module are key to the current module. + + Args: + module: A module object. + flag_values: A FlagValues object. This should almost never need + to be overridden. + + Raises: + FlagsError: When given an argument that is a module name (a + string), instead of a module object. + """ + # NOTE(salcianu): an even better test would be if not + # isinstance(module, types.ModuleType) but I didn't want to import + # types for such a tiny use. + if isinstance(module, str): + raise FlagsError('Received module name %s; expected a module object.' + % module) + _InternalDeclareKeyFlags( + [f.name for f in flag_values._GetKeyFlagsForModule(module.__name__)], + flag_values=flag_values) + # If module is this flag module, take _SPECIAL_FLAGS into account. + if module == _GetThisModuleObjectAndName()[0]: + _InternalDeclareKeyFlags( + # As we associate flags with _GetCallingModuleObjectAndName(), the + # special flags defined in this module are incorrectly registered with + # a different module. So, we can't use _GetKeyFlagsForModule. + # Instead, we take all flags from _SPECIAL_FLAGS (a private + # FlagValues, where no other module should register flags). + [f.name for f in _SPECIAL_FLAGS.FlagDict().values()], + flag_values=_SPECIAL_FLAGS, + key_flag_values=flag_values) + + +# +# STRING FLAGS +# + + +def DEFINE_string(name, default, help, flag_values=FLAGS, **args): + """Registers a flag whose value can be any string.""" + parser = ArgumentParser() + serializer = ArgumentSerializer() + DEFINE(parser, name, default, help, flag_values, serializer, **args) + + +# +# BOOLEAN FLAGS +# + + +class BooleanParser(ArgumentParser): + """Parser of boolean values.""" + + def Convert(self, argument): + """Converts the argument to a boolean; raise ValueError on errors.""" + if type(argument) == str: + if argument.lower() in ['true', 't', '1']: + return True + elif argument.lower() in ['false', 'f', '0']: + return False + + bool_argument = bool(argument) + if argument == bool_argument: + # The argument is a valid boolean (True, False, 0, or 1), and not just + # something that always converts to bool (list, string, int, etc.). + return bool_argument + + raise ValueError('Non-boolean argument to boolean flag', argument) + + def Parse(self, argument): + val = self.Convert(argument) + return val + + def Type(self): + return 'bool' + + +class BooleanFlag(Flag): + """Basic boolean flag. + + Boolean flags do not take any arguments, and their value is either + True (1) or False (0). The false value is specified on the command + line by prepending the word 'no' to either the long or the short flag + name. + + For example, if a Boolean flag was created whose long name was + 'update' and whose short name was 'x', then this flag could be + explicitly unset through either --noupdate or --nox. + """ + + def __init__(self, name, default, help, short_name=None, **args): + p = BooleanParser() + Flag.__init__(self, p, None, name, default, help, short_name, 1, **args) + if not self.help: self.help = "a boolean value" + + +def DEFINE_boolean(name, default, help, flag_values=FLAGS, **args): + """Registers a boolean flag. + + Such a boolean flag does not take an argument. If a user wants to + specify a false value explicitly, the long option beginning with 'no' + must be used: i.e. --noflag + + This flag will have a value of None, True or False. None is possible + if default=None and the user does not specify the flag on the command + line. + """ + DEFINE_flag(BooleanFlag(name, default, help, **args), flag_values) + + +# Match C++ API to unconfuse C++ people. +DEFINE_bool = DEFINE_boolean + + +class HelpFlag(BooleanFlag): + """ + HelpFlag is a special boolean flag that prints usage information and + raises a SystemExit exception if it is ever found in the command + line arguments. Note this is called with allow_override=1, so other + apps can define their own --help flag, replacing this one, if they want. + """ + def __init__(self): + BooleanFlag.__init__(self, "help", 0, "show this help", + short_name="?", allow_override=1) + def Parse(self, arg): + if arg: + doc = sys.modules["__main__"].__doc__ + flags = str(FLAGS) + print doc or ("\nUSAGE: %s [flags]\n" % sys.argv[0]) + if flags: + print "flags:" + print flags + sys.exit(1) +class HelpXMLFlag(BooleanFlag): + """Similar to HelpFlag, but generates output in XML format.""" + def __init__(self): + BooleanFlag.__init__(self, 'helpxml', False, + 'like --help, but generates XML output', + allow_override=1) + def Parse(self, arg): + if arg: + FLAGS.WriteHelpInXMLFormat(sys.stdout) + sys.exit(1) +class HelpshortFlag(BooleanFlag): + """ + HelpshortFlag is a special boolean flag that prints usage + information for the "main" module, and rasies a SystemExit exception + if it is ever found in the command line arguments. Note this is + called with allow_override=1, so other apps can define their own + --helpshort flag, replacing this one, if they want. + """ + def __init__(self): + BooleanFlag.__init__(self, "helpshort", 0, + "show usage only for this module", allow_override=1) + def Parse(self, arg): + if arg: + doc = sys.modules["__main__"].__doc__ + flags = FLAGS.MainModuleHelp() + print doc or ("\nUSAGE: %s [flags]\n" % sys.argv[0]) + if flags: + print "flags:" + print flags + sys.exit(1) + +# +# Numeric parser - base class for Integer and Float parsers +# + + +class NumericParser(ArgumentParser): + """Parser of numeric values. + + Parsed value may be bounded to a given upper and lower bound. + """ + + def IsOutsideBounds(self, val): + return ((self.lower_bound is not None and val < self.lower_bound) or + (self.upper_bound is not None and val > self.upper_bound)) + + def Parse(self, argument): + val = self.Convert(argument) + if self.IsOutsideBounds(val): + raise ValueError("%s is not %s" % (val, self.syntactic_help)) + return val + + def WriteCustomInfoInXMLFormat(self, outfile, indent): + if self.lower_bound is not None: + _WriteSimpleXMLElement(outfile, 'lower_bound', self.lower_bound, indent) + if self.upper_bound is not None: + _WriteSimpleXMLElement(outfile, 'upper_bound', self.upper_bound, indent) + + def Convert(self, argument): + """Default implementation: always returns its argument unmodified.""" + return argument + +# End of Numeric Parser + +# +# FLOAT FLAGS +# + + +class FloatParser(NumericParser): + """Parser of floating point values. + + Parsed value may be bounded to a given upper and lower bound. + """ + number_article = "a" + number_name = "number" + syntactic_help = " ".join((number_article, number_name)) + + def __init__(self, lower_bound=None, upper_bound=None): + super(FloatParser, self).__init__() + self.lower_bound = lower_bound + self.upper_bound = upper_bound + sh = self.syntactic_help + if lower_bound is not None and upper_bound is not None: + sh = ("%s in the range [%s, %s]" % (sh, lower_bound, upper_bound)) + elif lower_bound == 0: + sh = "a non-negative %s" % self.number_name + elif upper_bound == 0: + sh = "a non-positive %s" % self.number_name + elif upper_bound is not None: + sh = "%s <= %s" % (self.number_name, upper_bound) + elif lower_bound is not None: + sh = "%s >= %s" % (self.number_name, lower_bound) + self.syntactic_help = sh + + def Convert(self, argument): + """Converts argument to a float; raises ValueError on errors.""" + return float(argument) + + def Type(self): + return 'float' +# End of FloatParser + + +def DEFINE_float(name, default, help, lower_bound=None, upper_bound=None, + flag_values=FLAGS, **args): + """Registers a flag whose value must be a float. + + If lower_bound or upper_bound are set, then this flag must be + within the given range. + """ + parser = FloatParser(lower_bound, upper_bound) + serializer = ArgumentSerializer() + DEFINE(parser, name, default, help, flag_values, serializer, **args) + _RegisterBoundsValidatorIfNeeded(parser, name, flag_values=flag_values) + +# +# INTEGER FLAGS +# + + +class IntegerParser(NumericParser): + """Parser of an integer value. + + Parsed value may be bounded to a given upper and lower bound. + """ + number_article = "an" + number_name = "integer" + syntactic_help = " ".join((number_article, number_name)) + + def __init__(self, lower_bound=None, upper_bound=None): + super(IntegerParser, self).__init__() + self.lower_bound = lower_bound + self.upper_bound = upper_bound + sh = self.syntactic_help + if lower_bound is not None and upper_bound is not None: + sh = ("%s in the range [%s, %s]" % (sh, lower_bound, upper_bound)) + elif lower_bound == 1: + sh = "a positive %s" % self.number_name + elif upper_bound == -1: + sh = "a negative %s" % self.number_name + elif lower_bound == 0: + sh = "a non-negative %s" % self.number_name + elif upper_bound == 0: + sh = "a non-positive %s" % self.number_name + elif upper_bound is not None: + sh = "%s <= %s" % (self.number_name, upper_bound) + elif lower_bound is not None: + sh = "%s >= %s" % (self.number_name, lower_bound) + self.syntactic_help = sh + + def Convert(self, argument): + __pychecker__ = 'no-returnvalues' + if type(argument) == str: + base = 10 + if len(argument) > 2 and argument[0] == "0" and argument[1] == "x": + base = 16 + return int(argument, base) + else: + return int(argument) + + def Type(self): + return 'int' + + +def DEFINE_integer(name, default, help, lower_bound=None, upper_bound=None, + flag_values=FLAGS, **args): + """Registers a flag whose value must be an integer. + + If lower_bound, or upper_bound are set, then this flag must be + within the given range. + """ + parser = IntegerParser(lower_bound, upper_bound) + serializer = ArgumentSerializer() + DEFINE(parser, name, default, help, flag_values, serializer, **args) + _RegisterBoundsValidatorIfNeeded(parser, name, flag_values=flag_values) + + +# +# ENUM FLAGS +# + + +class EnumParser(ArgumentParser): + """Parser of a string enum value (a string value from a given set). + + If enum_values (see below) is not specified, any string is allowed. + """ + + def __init__(self, enum_values=None): + super(EnumParser, self).__init__() + self.enum_values = enum_values + + def Parse(self, argument): + if self.enum_values and argument not in self.enum_values: + raise ValueError("value should be one of <%s>" % + "|".join(self.enum_values)) + return argument + + def Type(self): + return 'string enum' + + +class EnumFlag(Flag): + """Basic enum flag; its value can be any string from list of enum_values.""" + + def __init__(self, name, default, help, enum_values=None, + short_name=None, **args): + enum_values = enum_values or [] + p = EnumParser(enum_values) + g = ArgumentSerializer() + Flag.__init__(self, p, g, name, default, help, short_name, **args) + if not self.help: self.help = "an enum string" + self.help = "<%s>: %s" % ("|".join(enum_values), self.help) + + def _WriteCustomInfoInXMLFormat(self, outfile, indent): + for enum_value in self.parser.enum_values: + _WriteSimpleXMLElement(outfile, 'enum_value', enum_value, indent) + + +def DEFINE_enum(name, default, enum_values, help, flag_values=FLAGS, + **args): + """Registers a flag whose value can be any string from enum_values.""" + DEFINE_flag(EnumFlag(name, default, help, enum_values, ** args), + flag_values) + + +# +# LIST FLAGS +# + + +class BaseListParser(ArgumentParser): + """Base class for a parser of lists of strings. + + To extend, inherit from this class; from the subclass __init__, call + + BaseListParser.__init__(self, token, name) + + where token is a character used to tokenize, and name is a description + of the separator. + """ + + def __init__(self, token=None, name=None): + assert name + super(BaseListParser, self).__init__() + self._token = token + self._name = name + self.syntactic_help = "a %s separated list" % self._name + + def Parse(self, argument): + if isinstance(argument, list): + return argument + elif argument == '': + return [] + else: + return [s.strip() for s in argument.split(self._token)] + + def Type(self): + return '%s separated list of strings' % self._name + + +class ListParser(BaseListParser): + """Parser for a comma-separated list of strings.""" + + def __init__(self): + BaseListParser.__init__(self, ',', 'comma') + + def WriteCustomInfoInXMLFormat(self, outfile, indent): + BaseListParser.WriteCustomInfoInXMLFormat(self, outfile, indent) + _WriteSimpleXMLElement(outfile, 'list_separator', repr(','), indent) + + +class WhitespaceSeparatedListParser(BaseListParser): + """Parser for a whitespace-separated list of strings.""" + + def __init__(self): + BaseListParser.__init__(self, None, 'whitespace') + + def WriteCustomInfoInXMLFormat(self, outfile, indent): + BaseListParser.WriteCustomInfoInXMLFormat(self, outfile, indent) + separators = list(string.whitespace) + separators.sort() + for ws_char in string.whitespace: + _WriteSimpleXMLElement(outfile, 'list_separator', repr(ws_char), indent) + + +def DEFINE_list(name, default, help, flag_values=FLAGS, **args): + """Registers a flag whose value is a comma-separated list of strings.""" + parser = ListParser() + serializer = ListSerializer(',') + DEFINE(parser, name, default, help, flag_values, serializer, **args) + + +def DEFINE_spaceseplist(name, default, help, flag_values=FLAGS, **args): + """Registers a flag whose value is a whitespace-separated list of strings. + + Any whitespace can be used as a separator. + """ + parser = WhitespaceSeparatedListParser() + serializer = ListSerializer(' ') + DEFINE(parser, name, default, help, flag_values, serializer, **args) + + +# +# MULTI FLAGS +# + + +class MultiFlag(Flag): + """A flag that can appear multiple time on the command-line. + + The value of such a flag is a list that contains the individual values + from all the appearances of that flag on the command-line. + + See the __doc__ for Flag for most behavior of this class. Only + differences in behavior are described here: + + * The default value may be either a single value or a list of values. + A single value is interpreted as the [value] singleton list. + + * The value of the flag is always a list, even if the option was + only supplied once, and even if the default value is a single + value + """ + + def __init__(self, *args, **kwargs): + Flag.__init__(self, *args, **kwargs) + self.help += ';\n repeat this option to specify a list of values' + + def Parse(self, arguments): + """Parses one or more arguments with the installed parser. + + Args: + arguments: a single argument or a list of arguments (typically a + list of default values); a single argument is converted + internally into a list containing one item. + """ + if not isinstance(arguments, list): + # Default value may be a list of values. Most other arguments + # will not be, so convert them into a single-item list to make + # processing simpler below. + arguments = [arguments] + + if self.present: + # keep a backup reference to list of previously supplied option values + values = self.value + else: + # "erase" the defaults with an empty list + values = [] + + for item in arguments: + # have Flag superclass parse argument, overwriting self.value reference + Flag.Parse(self, item) # also increments self.present + values.append(self.value) + + # put list of option values back in the 'value' attribute + self.value = values + + def Serialize(self): + if not self.serializer: + raise FlagsError("Serializer not present for flag %s" % self.name) + if self.value is None: + return '' + + s = '' + + multi_value = self.value + + for self.value in multi_value: + if s: s += ' ' + s += Flag.Serialize(self) + + self.value = multi_value + + return s + + def Type(self): + return 'multi ' + self.parser.Type() + + +def DEFINE_multi(parser, serializer, name, default, help, flag_values=FLAGS, + **args): + """Registers a generic MultiFlag that parses its args with a given parser. + + Auxiliary function. Normal users should NOT use it directly. + + Developers who need to create their own 'Parser' classes for options + which can appear multiple times can call this module function to + register their flags. + """ + DEFINE_flag(MultiFlag(parser, serializer, name, default, help, **args), + flag_values) + + +def DEFINE_multistring(name, default, help, flag_values=FLAGS, **args): + """Registers a flag whose value can be a list of any strings. + + Use the flag on the command line multiple times to place multiple + string values into the list. The 'default' may be a single string + (which will be converted into a single-element list) or a list of + strings. + """ + parser = ArgumentParser() + serializer = ArgumentSerializer() + DEFINE_multi(parser, serializer, name, default, help, flag_values, **args) + + +def DEFINE_multi_int(name, default, help, lower_bound=None, upper_bound=None, + flag_values=FLAGS, **args): + """Registers a flag whose value can be a list of arbitrary integers. + + Use the flag on the command line multiple times to place multiple + integer values into the list. The 'default' may be a single integer + (which will be converted into a single-element list) or a list of + integers. + """ + parser = IntegerParser(lower_bound, upper_bound) + serializer = ArgumentSerializer() + DEFINE_multi(parser, serializer, name, default, help, flag_values, **args) + + +def DEFINE_multi_float(name, default, help, lower_bound=None, upper_bound=None, + flag_values=FLAGS, **args): + """Registers a flag whose value can be a list of arbitrary floats. + + Use the flag on the command line multiple times to place multiple + float values into the list. The 'default' may be a single float + (which will be converted into a single-element list) or a list of + floats. + """ + parser = FloatParser(lower_bound, upper_bound) + serializer = ArgumentSerializer() + DEFINE_multi(parser, serializer, name, default, help, flag_values, **args) + + +# Now register the flags that we want to exist in all applications. +# These are all defined with allow_override=1, so user-apps can use +# these flagnames for their own purposes, if they want. +DEFINE_flag(HelpFlag()) +DEFINE_flag(HelpshortFlag()) +DEFINE_flag(HelpXMLFlag()) + +# Define special flags here so that help may be generated for them. +# NOTE: Please do NOT use _SPECIAL_FLAGS from outside this module. +_SPECIAL_FLAGS = FlagValues() + + +DEFINE_string( + 'flagfile', "", + "Insert flag definitions from the given file into the command line.", + _SPECIAL_FLAGS) + +DEFINE_string( + 'undefok', "", + "comma-separated list of flag names that it is okay to specify " + "on the command line even if the program does not define a flag " + "with that name. IMPORTANT: flags in this list that have " + "arguments MUST use the --flag=value format.", _SPECIAL_FLAGS) diff --git a/third_party/gjslint/python-gflags-2.0/gflags2man.py b/third_party/gjslint/python-gflags-2.0/gflags2man.py new file mode 100755 index 0000000000..3a50f9e19f --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/gflags2man.py @@ -0,0 +1,544 @@ +#!/usr/bin/env python + +# Copyright (c) 2006, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""gflags2man runs a Google flags base program and generates a man page. + +Run the program, parse the output, and then format that into a man +page. + +Usage: + gflags2man [program] ... +""" + +# TODO(csilvers): work with windows paths (\) as well as unix (/) + +# This may seem a bit of an end run, but it: doesn't bloat flags, can +# support python/java/C++, supports older executables, and can be +# extended to other document formats. +# Inspired by help2man. + + + +import os +import re +import sys +import stat +import time + +import gflags + +_VERSION = '0.1' + + +def _GetDefaultDestDir(): + home = os.environ.get('HOME', '') + homeman = os.path.join(home, 'man', 'man1') + if home and os.path.exists(homeman): + return homeman + else: + return os.environ.get('TMPDIR', '/tmp') + +FLAGS = gflags.FLAGS +gflags.DEFINE_string('dest_dir', _GetDefaultDestDir(), + 'Directory to write resulting manpage to.' + ' Specify \'-\' for stdout') +gflags.DEFINE_string('help_flag', '--help', + 'Option to pass to target program in to get help') +gflags.DEFINE_integer('v', 0, 'verbosity level to use for output') + + +_MIN_VALID_USAGE_MSG = 9 # if fewer lines than this, help is suspect + + +class Logging: + """A super-simple logging class""" + def error(self, msg): print >>sys.stderr, "ERROR: ", msg + def warn(self, msg): print >>sys.stderr, "WARNING: ", msg + def info(self, msg): print msg + def debug(self, msg): self.vlog(1, msg) + def vlog(self, level, msg): + if FLAGS.v >= level: print msg +logging = Logging() +class App: + def usage(self, shorthelp=0): + print >>sys.stderr, __doc__ + print >>sys.stderr, "flags:" + print >>sys.stderr, str(FLAGS) + def run(self): + main(sys.argv) +app = App() + + +def GetRealPath(filename): + """Given an executable filename, find in the PATH or find absolute path. + Args: + filename An executable filename (string) + Returns: + Absolute version of filename. + None if filename could not be found locally, absolutely, or in PATH + """ + if os.path.isabs(filename): # already absolute + return filename + + if filename.startswith('./') or filename.startswith('../'): # relative + return os.path.abspath(filename) + + path = os.getenv('PATH', '') + for directory in path.split(':'): + tryname = os.path.join(directory, filename) + if os.path.exists(tryname): + if not os.path.isabs(directory): # relative directory + return os.path.abspath(tryname) + return tryname + if os.path.exists(filename): + return os.path.abspath(filename) + return None # could not determine + +class Flag(object): + """The information about a single flag.""" + + def __init__(self, flag_desc, help): + """Create the flag object. + Args: + flag_desc The command line forms this could take. (string) + help The help text (string) + """ + self.desc = flag_desc # the command line forms + self.help = help # the help text + self.default = '' # default value + self.tips = '' # parsing/syntax tips + + +class ProgramInfo(object): + """All the information gleaned from running a program with --help.""" + + # Match a module block start, for python scripts --help + # "goopy.logging:" + module_py_re = re.compile(r'(\S.+):$') + # match the start of a flag listing + # " -v,--verbosity: Logging verbosity" + flag_py_re = re.compile(r'\s+(-\S+):\s+(.*)$') + # " (default: '0')" + flag_default_py_re = re.compile(r'\s+\(default:\s+\'(.*)\'\)$') + # " (an integer)" + flag_tips_py_re = re.compile(r'\s+\((.*)\)$') + + # Match a module block start, for c++ programs --help + # "google/base/commandlineflags": + module_c_re = re.compile(r'\s+Flags from (\S.+):$') + # match the start of a flag listing + # " -v,--verbosity: Logging verbosity" + flag_c_re = re.compile(r'\s+(-\S+)\s+(.*)$') + + # Match a module block start, for java programs --help + # "com.google.common.flags" + module_java_re = re.compile(r'\s+Flags for (\S.+):$') + # match the start of a flag listing + # " -v,--verbosity: Logging verbosity" + flag_java_re = re.compile(r'\s+(-\S+)\s+(.*)$') + + def __init__(self, executable): + """Create object with executable. + Args: + executable Program to execute (string) + """ + self.long_name = executable + self.name = os.path.basename(executable) # name + # Get name without extension (PAR files) + (self.short_name, self.ext) = os.path.splitext(self.name) + self.executable = GetRealPath(executable) # name of the program + self.output = [] # output from the program. List of lines. + self.desc = [] # top level description. List of lines + self.modules = {} # { section_name(string), [ flags ] } + self.module_list = [] # list of module names in their original order + self.date = time.localtime(time.time()) # default date info + + def Run(self): + """Run it and collect output. + + Returns: + 1 (true) If everything went well. + 0 (false) If there were problems. + """ + if not self.executable: + logging.error('Could not locate "%s"' % self.long_name) + return 0 + + finfo = os.stat(self.executable) + self.date = time.localtime(finfo[stat.ST_MTIME]) + + logging.info('Running: %s %s &1' + % (self.executable, FLAGS.help_flag)) + # --help output is often routed to stderr, so we combine with stdout. + # Re-direct stdin to /dev/null to encourage programs that + # don't understand --help to exit. + (child_stdin, child_stdout_and_stderr) = os.popen4( + [self.executable, FLAGS.help_flag]) + child_stdin.close() # ' start_line+1 + and '' == self.output[start_line+1].rstrip()): + start_line += 2 + logging.debug('Flags start (python): %s' % line) + return (start_line, 'python') + # SWIG flags just have the module name followed by colon. + if exec_mod_start == line: + logging.debug('Flags start (swig): %s' % line) + return (start_line, 'python') + # C++ flags begin after a blank line and with a constant string + if after_blank and line.startswith(' Flags from '): + logging.debug('Flags start (c): %s' % line) + return (start_line, 'c') + # java flags begin with a constant string + if line == 'where flags are': + logging.debug('Flags start (java): %s' % line) + start_line += 2 # skip "Standard flags:" + return (start_line, 'java') + + logging.debug('Desc: %s' % line) + self.desc.append(line) + after_blank = (line == '') + else: + logging.warn('Never found the start of the flags section for "%s"!' + % self.long_name) + return (-1, '') + + def ParsePythonFlags(self, start_line=0): + """Parse python/swig style flags.""" + modname = None # name of current module + modlist = [] + flag = None + for line_num in range(start_line, len(self.output)): # collect flags + line = self.output[line_num].rstrip() + if not line: # blank + continue + + mobj = self.module_py_re.match(line) + if mobj: # start of a new module + modname = mobj.group(1) + logging.debug('Module: %s' % line) + if flag: + modlist.append(flag) + self.module_list.append(modname) + self.modules.setdefault(modname, []) + modlist = self.modules[modname] + flag = None + continue + + mobj = self.flag_py_re.match(line) + if mobj: # start of a new flag + if flag: + modlist.append(flag) + logging.debug('Flag: %s' % line) + flag = Flag(mobj.group(1), mobj.group(2)) + continue + + if not flag: # continuation of a flag + logging.error('Flag info, but no current flag "%s"' % line) + mobj = self.flag_default_py_re.match(line) + if mobj: # (default: '...') + flag.default = mobj.group(1) + logging.debug('Fdef: %s' % line) + continue + mobj = self.flag_tips_py_re.match(line) + if mobj: # (tips) + flag.tips = mobj.group(1) + logging.debug('Ftip: %s' % line) + continue + if flag and flag.help: + flag.help += line # multiflags tack on an extra line + else: + logging.info('Extra: %s' % line) + if flag: + modlist.append(flag) + + def ParseCFlags(self, start_line=0): + """Parse C style flags.""" + modname = None # name of current module + modlist = [] + flag = None + for line_num in range(start_line, len(self.output)): # collect flags + line = self.output[line_num].rstrip() + if not line: # blank lines terminate flags + if flag: # save last flag + modlist.append(flag) + flag = None + continue + + mobj = self.module_c_re.match(line) + if mobj: # start of a new module + modname = mobj.group(1) + logging.debug('Module: %s' % line) + if flag: + modlist.append(flag) + self.module_list.append(modname) + self.modules.setdefault(modname, []) + modlist = self.modules[modname] + flag = None + continue + + mobj = self.flag_c_re.match(line) + if mobj: # start of a new flag + if flag: # save last flag + modlist.append(flag) + logging.debug('Flag: %s' % line) + flag = Flag(mobj.group(1), mobj.group(2)) + continue + + # append to flag help. type and default are part of the main text + if flag: + flag.help += ' ' + line.strip() + else: + logging.info('Extra: %s' % line) + if flag: + modlist.append(flag) + + def ParseJavaFlags(self, start_line=0): + """Parse Java style flags (com.google.common.flags).""" + # The java flags prints starts with a "Standard flags" "module" + # that doesn't follow the standard module syntax. + modname = 'Standard flags' # name of current module + self.module_list.append(modname) + self.modules.setdefault(modname, []) + modlist = self.modules[modname] + flag = None + + for line_num in range(start_line, len(self.output)): # collect flags + line = self.output[line_num].rstrip() + logging.vlog(2, 'Line: "%s"' % line) + if not line: # blank lines terminate module + if flag: # save last flag + modlist.append(flag) + flag = None + continue + + mobj = self.module_java_re.match(line) + if mobj: # start of a new module + modname = mobj.group(1) + logging.debug('Module: %s' % line) + if flag: + modlist.append(flag) + self.module_list.append(modname) + self.modules.setdefault(modname, []) + modlist = self.modules[modname] + flag = None + continue + + mobj = self.flag_java_re.match(line) + if mobj: # start of a new flag + if flag: # save last flag + modlist.append(flag) + logging.debug('Flag: %s' % line) + flag = Flag(mobj.group(1), mobj.group(2)) + continue + + # append to flag help. type and default are part of the main text + if flag: + flag.help += ' ' + line.strip() + else: + logging.info('Extra: %s' % line) + if flag: + modlist.append(flag) + + def Filter(self): + """Filter parsed data to create derived fields.""" + if not self.desc: + self.short_desc = '' + return + + for i in range(len(self.desc)): # replace full path with name + if self.desc[i].find(self.executable) >= 0: + self.desc[i] = self.desc[i].replace(self.executable, self.name) + + self.short_desc = self.desc[0] + word_list = self.short_desc.split(' ') + all_names = [ self.name, self.short_name, ] + # Since the short_desc is always listed right after the name, + # trim it from the short_desc + while word_list and (word_list[0] in all_names + or word_list[0].lower() in all_names): + del word_list[0] + self.short_desc = '' # signal need to reconstruct + if not self.short_desc and word_list: + self.short_desc = ' '.join(word_list) + + +class GenerateDoc(object): + """Base class to output flags information.""" + + def __init__(self, proginfo, directory='.'): + """Create base object. + Args: + proginfo A ProgramInfo object + directory Directory to write output into + """ + self.info = proginfo + self.dirname = directory + + def Output(self): + """Output all sections of the page.""" + self.Open() + self.Header() + self.Body() + self.Footer() + + def Open(self): raise NotImplementedError # define in subclass + def Header(self): raise NotImplementedError # define in subclass + def Body(self): raise NotImplementedError # define in subclass + def Footer(self): raise NotImplementedError # define in subclass + + +class GenerateMan(GenerateDoc): + """Output a man page.""" + + def __init__(self, proginfo, directory='.'): + """Create base object. + Args: + proginfo A ProgramInfo object + directory Directory to write output into + """ + GenerateDoc.__init__(self, proginfo, directory) + + def Open(self): + if self.dirname == '-': + logging.info('Writing to stdout') + self.fp = sys.stdout + else: + self.file_path = '%s.1' % os.path.join(self.dirname, self.info.name) + logging.info('Writing: %s' % self.file_path) + self.fp = open(self.file_path, 'w') + + def Header(self): + self.fp.write( + '.\\" DO NOT MODIFY THIS FILE! It was generated by gflags2man %s\n' + % _VERSION) + self.fp.write( + '.TH %s "1" "%s" "%s" "User Commands"\n' + % (self.info.name, time.strftime('%x', self.info.date), self.info.name)) + self.fp.write( + '.SH NAME\n%s \\- %s\n' % (self.info.name, self.info.short_desc)) + self.fp.write( + '.SH SYNOPSIS\n.B %s\n[\\fIFLAGS\\fR]...\n' % self.info.name) + + def Body(self): + self.fp.write( + '.SH DESCRIPTION\n.\\" Add any additional description here\n.PP\n') + for ln in self.info.desc: + self.fp.write('%s\n' % ln) + self.fp.write( + '.SH OPTIONS\n') + # This shows flags in the original order + for modname in self.info.module_list: + if modname.find(self.info.executable) >= 0: + mod = modname.replace(self.info.executable, self.info.name) + else: + mod = modname + self.fp.write('\n.P\n.I %s\n' % mod) + for flag in self.info.modules[modname]: + help_string = flag.help + if flag.default or flag.tips: + help_string += '\n.br\n' + if flag.default: + help_string += ' (default: \'%s\')' % flag.default + if flag.tips: + help_string += ' (%s)' % flag.tips + self.fp.write( + '.TP\n%s\n%s\n' % (flag.desc, help_string)) + + def Footer(self): + self.fp.write( + '.SH COPYRIGHT\nCopyright \(co %s Google.\n' + % time.strftime('%Y', self.info.date)) + self.fp.write('Gflags2man created this page from "%s %s" output.\n' + % (self.info.name, FLAGS.help_flag)) + self.fp.write('\nGflags2man was written by Dan Christian. ' + ' Note that the date on this' + ' page is the modification date of %s.\n' % self.info.name) + + +def main(argv): + argv = FLAGS(argv) # handles help as well + if len(argv) <= 1: + app.usage(shorthelp=1) + return 1 + + for arg in argv[1:]: + prog = ProgramInfo(arg) + if not prog.Run(): + continue + prog.Parse() + prog.Filter() + doc = GenerateMan(prog, FLAGS.dest_dir) + doc.Output() + return 0 + +if __name__ == '__main__': + app.run() diff --git a/third_party/gjslint/python-gflags-2.0/gflags_validators.py b/third_party/gjslint/python-gflags-2.0/gflags_validators.py new file mode 100755 index 0000000000..d83058d50f --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/gflags_validators.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python + +# Copyright (c) 2010, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Module to enforce different constraints on flags. + +A validator represents an invariant, enforced over a one or more flags. +See 'FLAGS VALIDATORS' in gflags.py's docstring for a usage manual. +""" + +__author__ = 'olexiy@google.com (Olexiy Oryeshko)' + + +class Error(Exception): + """Thrown If validator constraint is not satisfied.""" + + +class Validator(object): + """Base class for flags validators. + + Users should NOT overload these classes, and use gflags.Register... + methods instead. + """ + + # Used to assign each validator an unique insertion_index + validators_count = 0 + + def __init__(self, checker, message): + """Constructor to create all validators. + + Args: + checker: function to verify the constraint. + Input of this method varies, see SimpleValidator and + DictionaryValidator for a detailed description. + message: string, error message to be shown to the user + """ + self.checker = checker + self.message = message + Validator.validators_count += 1 + # Used to assert validators in the order they were registered (CL/18694236) + self.insertion_index = Validator.validators_count + + def Verify(self, flag_values): + """Verify that constraint is satisfied. + + flags library calls this method to verify Validator's constraint. + Args: + flag_values: gflags.FlagValues, containing all flags + Raises: + Error: if constraint is not satisfied. + """ + param = self._GetInputToCheckerFunction(flag_values) + if not self.checker(param): + raise Error(self.message) + + def GetFlagsNames(self): + """Return the names of the flags checked by this validator. + + Returns: + [string], names of the flags + """ + raise NotImplementedError('This method should be overloaded') + + def PrintFlagsWithValues(self, flag_values): + raise NotImplementedError('This method should be overloaded') + + def _GetInputToCheckerFunction(self, flag_values): + """Given flag values, construct the input to be given to checker. + + Args: + flag_values: gflags.FlagValues, containing all flags. + Returns: + Return type depends on the specific validator. + """ + raise NotImplementedError('This method should be overloaded') + + +class SimpleValidator(Validator): + """Validator behind RegisterValidator() method. + + Validates that a single flag passes its checker function. The checker function + takes the flag value and returns True (if value looks fine) or, if flag value + is not valid, either returns False or raises an Exception.""" + def __init__(self, flag_name, checker, message): + """Constructor. + + Args: + flag_name: string, name of the flag. + checker: function to verify the validator. + input - value of the corresponding flag (string, boolean, etc). + output - Boolean. Must return True if validator constraint is satisfied. + If constraint is not satisfied, it should either return False or + raise Error. + message: string, error message to be shown to the user if validator's + condition is not satisfied + """ + super(SimpleValidator, self).__init__(checker, message) + self.flag_name = flag_name + + def GetFlagsNames(self): + return [self.flag_name] + + def PrintFlagsWithValues(self, flag_values): + return 'flag --%s=%s' % (self.flag_name, flag_values[self.flag_name].value) + + def _GetInputToCheckerFunction(self, flag_values): + """Given flag values, construct the input to be given to checker. + + Args: + flag_values: gflags.FlagValues + Returns: + value of the corresponding flag. + """ + return flag_values[self.flag_name].value + + +class DictionaryValidator(Validator): + """Validator behind RegisterDictionaryValidator method. + + Validates that flag values pass their common checker function. The checker + function takes flag values and returns True (if values look fine) or, + if values are not valid, either returns False or raises an Exception. + """ + def __init__(self, flag_names, checker, message): + """Constructor. + + Args: + flag_names: [string], containing names of the flags used by checker. + checker: function to verify the validator. + input - dictionary, with keys() being flag_names, and value for each + key being the value of the corresponding flag (string, boolean, etc). + output - Boolean. Must return True if validator constraint is satisfied. + If constraint is not satisfied, it should either return False or + raise Error. + message: string, error message to be shown to the user if validator's + condition is not satisfied + """ + super(DictionaryValidator, self).__init__(checker, message) + self.flag_names = flag_names + + def _GetInputToCheckerFunction(self, flag_values): + """Given flag values, construct the input to be given to checker. + + Args: + flag_values: gflags.FlagValues + Returns: + dictionary, with keys() being self.lag_names, and value for each key + being the value of the corresponding flag (string, boolean, etc). + """ + return dict([key, flag_values[key].value] for key in self.flag_names) + + def PrintFlagsWithValues(self, flag_values): + prefix = 'flags ' + flags_with_values = [] + for key in self.flag_names: + flags_with_values.append('%s=%s' % (key, flag_values[key].value)) + return prefix + ', '.join(flags_with_values) + + def GetFlagsNames(self): + return self.flag_names diff --git a/third_party/gjslint/python-gflags-2.0/python_gflags.egg-info/PKG-INFO b/third_party/gjslint/python-gflags-2.0/python_gflags.egg-info/PKG-INFO new file mode 100644 index 0000000000..faab7198f2 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/python_gflags.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 1.0 +Name: python-gflags +Version: 2.0 +Summary: Google Commandline Flags Module +Home-page: http://code.google.com/p/python-gflags +Author: Google Inc. and others +Author-email: google-gflags@googlegroups.com +License: BSD +Description: UNKNOWN +Platform: UNKNOWN diff --git a/third_party/gjslint/python-gflags-2.0/python_gflags.egg-info/SOURCES.txt b/third_party/gjslint/python-gflags-2.0/python_gflags.egg-info/SOURCES.txt new file mode 100644 index 0000000000..e6068dfde1 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/python_gflags.egg-info/SOURCES.txt @@ -0,0 +1,30 @@ +AUTHORS +COPYING +ChangeLog +MANIFEST.in +Makefile +NEWS +README +gflags.py +gflags2man.py +gflags_validators.py +setup.py +debian/README +debian/changelog +debian/compat +debian/control +debian/copyright +debian/docs +debian/rules +python_gflags.egg-info/PKG-INFO +python_gflags.egg-info/SOURCES.txt +python_gflags.egg-info/dependency_links.txt +python_gflags.egg-info/top_level.txt +tests/gflags_googletest.py +tests/gflags_helpxml_test.py +tests/gflags_unittest.py +tests/gflags_validators_test.py +tests/flags_modules_for_testing/__init__.py +tests/flags_modules_for_testing/module_bar.py +tests/flags_modules_for_testing/module_baz.py +tests/flags_modules_for_testing/module_foo.py \ No newline at end of file diff --git a/third_party/gjslint/python-gflags-2.0/python_gflags.egg-info/dependency_links.txt b/third_party/gjslint/python-gflags-2.0/python_gflags.egg-info/dependency_links.txt new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/python_gflags.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/third_party/gjslint/python-gflags-2.0/python_gflags.egg-info/top_level.txt b/third_party/gjslint/python-gflags-2.0/python_gflags.egg-info/top_level.txt new file mode 100644 index 0000000000..93c1fcdc74 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/python_gflags.egg-info/top_level.txt @@ -0,0 +1,2 @@ +gflags +gflags_validators diff --git a/third_party/gjslint/python-gflags-2.0/setup.cfg b/third_party/gjslint/python-gflags-2.0/setup.cfg new file mode 100644 index 0000000000..861a9f5542 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/third_party/gjslint/python-gflags-2.0/setup.py b/third_party/gjslint/python-gflags-2.0/setup.py new file mode 100755 index 0000000000..573db2d410 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/setup.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python + +# Copyright (c) 2007, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from setuptools import setup + +setup(name='python-gflags', + version='2.0', + description='Google Commandline Flags Module', + license='BSD', + author='Google Inc. and others', + author_email='google-gflags@googlegroups.com', + url='http://code.google.com/p/python-gflags', + py_modules=["gflags", "gflags_validators"], + data_files=[("bin", ["gflags2man.py"])], + include_package_data=True, + ) diff --git a/third_party/gjslint/python-gflags-2.0/tests/flags_modules_for_testing/__init__.py b/third_party/gjslint/python-gflags-2.0/tests/flags_modules_for_testing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/third_party/gjslint/python-gflags-2.0/tests/flags_modules_for_testing/module_bar.py b/third_party/gjslint/python-gflags-2.0/tests/flags_modules_for_testing/module_bar.py new file mode 100755 index 0000000000..230627f23a --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/tests/flags_modules_for_testing/module_bar.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +# Copyright (c) 2009, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +"""Auxiliary module for testing gflags.py. + +The purpose of this module is to define a few flags. We want to make +sure the unit tests for gflags.py involve more than one module. +""" + +__author__ = 'salcianu@google.com (Alex Salcianu)' + +__pychecker__ = 'no-local' # for unittest + +import gflags + +FLAGS = gflags.FLAGS + + +def DefineFlags(flag_values=FLAGS): + """Defines some flags. + + Args: + flag_values: The FlagValues object we want to register the flags + with. + """ + # The 'tmod_bar_' prefix (short for 'test_module_bar') ensures there + # is no name clash with the existing flags. + gflags.DEFINE_boolean('tmod_bar_x', True, 'Boolean flag.', + flag_values=flag_values) + gflags.DEFINE_string('tmod_bar_y', 'default', 'String flag.', + flag_values=flag_values) + gflags.DEFINE_boolean('tmod_bar_z', False, + 'Another boolean flag from module bar.', + flag_values=flag_values) + gflags.DEFINE_integer('tmod_bar_t', 4, 'Sample int flag.', + flag_values=flag_values) + gflags.DEFINE_integer('tmod_bar_u', 5, 'Sample int flag.', + flag_values=flag_values) + gflags.DEFINE_integer('tmod_bar_v', 6, 'Sample int flag.', + flag_values=flag_values) + + +def RemoveOneFlag(flag_name, flag_values=FLAGS): + """Removes the definition of one flag from gflags.FLAGS. + + Note: if the flag is not defined in gflags.FLAGS, this function does + not do anything (in particular, it does not raise any exception). + + Motivation: We use this function for cleanup *after* a test: if + there was a failure during a test and not all flags were declared, + we do not want the cleanup code to crash. + + Args: + flag_name: A string, the name of the flag to delete. + flag_values: The FlagValues object we remove the flag from. + """ + if flag_name in flag_values.FlagDict(): + flag_values.__delattr__(flag_name) + + +def NamesOfDefinedFlags(): + """Returns: List of names of the flags declared in this module.""" + return ['tmod_bar_x', + 'tmod_bar_y', + 'tmod_bar_z', + 'tmod_bar_t', + 'tmod_bar_u', + 'tmod_bar_v'] + + +def RemoveFlags(flag_values=FLAGS): + """Deletes the flag definitions done by the above DefineFlags(). + + Args: + flag_values: The FlagValues object we remove the flags from. + """ + for flag_name in NamesOfDefinedFlags(): + RemoveOneFlag(flag_name, flag_values=flag_values) + + +def GetModuleName(): + """Uses gflags._GetCallingModule() to return the name of this module. + + For checking that _GetCallingModule works as expected. + + Returns: + A string, the name of this module. + """ + # Calling the protected _GetCallingModule generates a lint warning, + # but we do not have any other alternative to test that function. + return gflags._GetCallingModule() + + +def ExecuteCode(code, global_dict): + """Executes some code in a given global environment. + + For testing of _GetCallingModule. + + Args: + code: A string, the code to be executed. + global_dict: A dictionary, the global environment that code should + be executed in. + """ + # Indeed, using exec generates a lint warning. But some user code + # actually uses exec, and we have to test for it ... + exec code in global_dict diff --git a/third_party/gjslint/python-gflags-2.0/tests/flags_modules_for_testing/module_baz.py b/third_party/gjslint/python-gflags-2.0/tests/flags_modules_for_testing/module_baz.py new file mode 100755 index 0000000000..2719c950ad --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/tests/flags_modules_for_testing/module_baz.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +# Copyright (c) 2009, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Auxiliary module for testing gflags.py. + +The purpose of this module is to test the behavior of flags that are defined +before main() executes. +""" + + + + +import gflags + +FLAGS = gflags.FLAGS + +gflags.DEFINE_boolean('tmod_baz_x', True, 'Boolean flag.') diff --git a/third_party/gjslint/python-gflags-2.0/tests/flags_modules_for_testing/module_foo.py b/third_party/gjslint/python-gflags-2.0/tests/flags_modules_for_testing/module_foo.py new file mode 100755 index 0000000000..760a37cc7b --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/tests/flags_modules_for_testing/module_foo.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +# +# Copyright (c) 2009, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Auxiliary module for testing gflags.py. + +The purpose of this module is to define a few flags, and declare some +other flags as being important. We want to make sure the unit tests +for gflags.py involve more than one module. +""" + +__author__ = 'salcianu@google.com (Alex Salcianu)' + +__pychecker__ = 'no-local' # for unittest + +import gflags +from flags_modules_for_testing import module_bar + +FLAGS = gflags.FLAGS + + +DECLARED_KEY_FLAGS = ['tmod_bar_x', 'tmod_bar_z', 'tmod_bar_t', + # Special (not user-defined) flag: + 'flagfile'] + + +def DefineFlags(flag_values=FLAGS): + """Defines a few flags.""" + module_bar.DefineFlags(flag_values=flag_values) + # The 'tmod_foo_' prefix (short for 'test_module_foo') ensures that we + # have no name clash with existing flags. + gflags.DEFINE_boolean('tmod_foo_bool', True, 'Boolean flag from module foo.', + flag_values=flag_values) + gflags.DEFINE_string('tmod_foo_str', 'default', 'String flag.', + flag_values=flag_values) + gflags.DEFINE_integer('tmod_foo_int', 3, 'Sample int flag.', + flag_values=flag_values) + + +def DeclareKeyFlags(flag_values=FLAGS): + """Declares a few key flags.""" + for flag_name in DECLARED_KEY_FLAGS: + gflags.DECLARE_key_flag(flag_name, flag_values=flag_values) + + +def DeclareExtraKeyFlags(flag_values=FLAGS): + """Declares some extra key flags.""" + gflags.ADOPT_module_key_flags(module_bar, flag_values=flag_values) + + +def NamesOfDefinedFlags(): + """Returns: list of names of flags defined by this module.""" + return ['tmod_foo_bool', 'tmod_foo_str', 'tmod_foo_int'] + + +def NamesOfDeclaredKeyFlags(): + """Returns: list of names of key flags for this module.""" + return NamesOfDefinedFlags() + DECLARED_KEY_FLAGS + + +def NamesOfDeclaredExtraKeyFlags(): + """Returns the list of names of additional key flags for this module. + + These are the flags that became key for this module only as a result + of a call to DeclareExtraKeyFlags() above. I.e., the flags declared + by module_bar, that were not already declared as key for this + module. + + Returns: + The list of names of additional key flags for this module. + """ + names_of_extra_key_flags = list(module_bar.NamesOfDefinedFlags()) + for flag_name in NamesOfDeclaredKeyFlags(): + while flag_name in names_of_extra_key_flags: + names_of_extra_key_flags.remove(flag_name) + return names_of_extra_key_flags + + +def RemoveFlags(flag_values=FLAGS): + """Deletes the flag definitions done by the above DefineFlags().""" + for flag_name in NamesOfDefinedFlags(): + module_bar.RemoveOneFlag(flag_name, flag_values=flag_values) + module_bar.RemoveFlags(flag_values=flag_values) + + +def GetModuleName(): + """Uses gflags._GetCallingModule() to return the name of this module. + + For checking that _GetCallingModule works as expected. + + Returns: + A string, the name of this module. + """ + # Calling the protected _GetCallingModule generates a lint warning, + # but we do not have any other alternative to test that function. + return gflags._GetCallingModule() + + +def DuplicateFlags(flagnames=None): + """Returns a new FlagValues object with the requested flagnames. + + Used to test DuplicateFlagError detection. + + Args: + flagnames: str, A list of flag names to create. + + Returns: + A FlagValues object with one boolean flag for each name in flagnames. + """ + flag_values = gflags.FlagValues() + for name in flagnames: + gflags.DEFINE_boolean(name, False, 'Flag named %s' % (name,), + flag_values=flag_values) + return flag_values diff --git a/third_party/gjslint/python-gflags-2.0/tests/gflags_googletest.py b/third_party/gjslint/python-gflags-2.0/tests/gflags_googletest.py new file mode 100644 index 0000000000..9ae614ce80 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/tests/gflags_googletest.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python + +# Copyright (c) 2011, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Some simple additions to the unittest framework useful for gflags testing.""" + + + +import re +import unittest + + +def Sorted(lst): + """Equivalent of sorted(), but not dependent on python version.""" + sorted_list = lst[:] + sorted_list.sort() + return sorted_list + + +def MultiLineEqual(expected, actual): + """Returns True if expected == actual, or returns False and logs.""" + if actual == expected: + return True + + print "Error: FLAGS.MainModuleHelp() didn't return the expected result." + print "Got:" + print actual + print "[End of got]" + + actual_lines = actual.split("\n") + expected_lines = expected.split("\n") + + num_actual_lines = len(actual_lines) + num_expected_lines = len(expected_lines) + + if num_actual_lines != num_expected_lines: + print "Number of actual lines = %d, expected %d" % ( + num_actual_lines, num_expected_lines) + + num_to_match = min(num_actual_lines, num_expected_lines) + + for i in range(num_to_match): + if actual_lines[i] != expected_lines[i]: + print "One discrepancy: Got:" + print actual_lines[i] + print "Expected:" + print expected_lines[i] + break + else: + # If we got here, found no discrepancy, print first new line. + if num_actual_lines > num_expected_lines: + print "New help line:" + print actual_lines[num_expected_lines] + elif num_expected_lines > num_actual_lines: + print "Missing expected help line:" + print expected_lines[num_actual_lines] + else: + print "Bug in this test -- discrepancy detected but not found." + + return False + + +class TestCase(unittest.TestCase): + def assertListEqual(self, list1, list2): + """Asserts that, when sorted, list1 and list2 are identical.""" + # This exists in python 2.7, but not previous versions. Use the + # built-in version if possible. + if hasattr(unittest.TestCase, "assertListEqual"): + unittest.TestCase.assertListEqual(self, Sorted(list1), Sorted(list2)) + else: + self.assertEqual(Sorted(list1), Sorted(list2)) + + def assertMultiLineEqual(self, expected, actual): + # This exists in python 2.7, but not previous versions. Use the + # built-in version if possible. + if hasattr(unittest.TestCase, "assertMultiLineEqual"): + unittest.TestCase.assertMultiLineEqual(self, expected, actual) + else: + self.assertTrue(MultiLineEqual(expected, actual)) + + def assertRaisesWithRegexpMatch(self, exception, regexp, fn, *args, **kwargs): + try: + fn(*args, **kwargs) + except exception, why: + self.assertTrue(re.search(regexp, str(why)), + "'%s' does not match '%s'" % (regexp, why)) + return + self.fail(exception.__name__ + " not raised") + + +def main(): + unittest.main() diff --git a/third_party/gjslint/python-gflags-2.0/tests/gflags_helpxml_test.py b/third_party/gjslint/python-gflags-2.0/tests/gflags_helpxml_test.py new file mode 100755 index 0000000000..fd78004b73 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/tests/gflags_helpxml_test.py @@ -0,0 +1,535 @@ +#!/usr/bin/env python + +# Copyright (c) 2009, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Unit tests for the XML-format help generated by the gflags.py module.""" + +__author__ = 'salcianu@google.com (Alex Salcianu)' + + +import string +import StringIO +import sys +import xml.dom.minidom +import xml.sax.saxutils +import gflags_googletest as googletest +import gflags +from flags_modules_for_testing import module_bar + + +class _MakeXMLSafeTest(googletest.TestCase): + + def _Check(self, s, expected_output): + self.assertEqual(gflags._MakeXMLSafe(s), expected_output) + + def testMakeXMLSafe(self): + self._Check('plain text', 'plain text') + self._Check('(x < y) && (a >= b)', + '(x < y) && (a >= b)') + # Some characters with ASCII code < 32 are illegal in XML 1.0 and + # are removed by us. However, '\n', '\t', and '\r' are legal. + self._Check('\x09\x0btext \x02 with\x0dsome \x08 good & bad chars', + '\ttext with\rsome good & bad chars') + + +def _ListSeparatorsInXMLFormat(separators, indent=''): + """Generates XML encoding of a list of list separators. + + Args: + separators: A list of list separators. Usually, this should be a + string whose characters are the valid list separators, e.g., ',' + means that both comma (',') and space (' ') are valid list + separators. + indent: A string that is added at the beginning of each generated + XML element. + + Returns: + A string. + """ + result = '' + separators = list(separators) + separators.sort() + for sep_char in separators: + result += ('%s%s\n' % + (indent, repr(sep_char))) + return result + + +class WriteFlagHelpInXMLFormatTest(googletest.TestCase): + """Test the XML-format help for a single flag at a time. + + There is one test* method for each kind of DEFINE_* declaration. + """ + + def setUp(self): + # self.fv is a FlagValues object, just like gflags.FLAGS. Each + # test registers one flag with this FlagValues. + self.fv = gflags.FlagValues() + + def _CheckFlagHelpInXML(self, flag_name, module_name, + expected_output, is_key=False): + # StringIO.StringIO is a file object that writes into a memory string. + sio = StringIO.StringIO() + flag_obj = self.fv[flag_name] + flag_obj.WriteInfoInXMLFormat(sio, module_name, is_key=is_key, indent=' ') + self.assertMultiLineEqual(sio.getvalue(), expected_output) + sio.close() + + def testFlagHelpInXML_Int(self): + gflags.DEFINE_integer('index', 17, 'An integer flag', flag_values=self.fv) + expected_output_pattern = ( + ' \n' + ' module.name\n' + ' index\n' + ' An integer flag\n' + ' 17\n' + ' %d\n' + ' int\n' + ' \n') + self._CheckFlagHelpInXML('index', 'module.name', + expected_output_pattern % 17) + # Check that the output is correct even when the current value of + # a flag is different from the default one. + self.fv['index'].value = 20 + self._CheckFlagHelpInXML('index', 'module.name', + expected_output_pattern % 20) + + def testFlagHelpInXML_IntWithBounds(self): + gflags.DEFINE_integer('nb_iters', 17, 'An integer flag', + lower_bound=5, upper_bound=27, + flag_values=self.fv) + expected_output = ( + ' \n' + ' yes\n' + ' module.name\n' + ' nb_iters\n' + ' An integer flag\n' + ' 17\n' + ' 17\n' + ' int\n' + ' 5\n' + ' 27\n' + ' \n') + self._CheckFlagHelpInXML('nb_iters', 'module.name', + expected_output, is_key=True) + + def testFlagHelpInXML_String(self): + gflags.DEFINE_string('file_path', '/path/to/my/dir', 'A test string flag.', + flag_values=self.fv) + expected_output = ( + ' \n' + ' simple_module\n' + ' file_path\n' + ' A test string flag.\n' + ' /path/to/my/dir\n' + ' /path/to/my/dir\n' + ' string\n' + ' \n') + self._CheckFlagHelpInXML('file_path', 'simple_module', + expected_output) + + def testFlagHelpInXML_StringWithXMLIllegalChars(self): + gflags.DEFINE_string('file_path', '/path/to/\x08my/dir', + 'A test string flag.', flag_values=self.fv) + # '\x08' is not a legal character in XML 1.0 documents. Our + # current code purges such characters from the generated XML. + expected_output = ( + ' \n' + ' simple_module\n' + ' file_path\n' + ' A test string flag.\n' + ' /path/to/my/dir\n' + ' /path/to/my/dir\n' + ' string\n' + ' \n') + self._CheckFlagHelpInXML('file_path', 'simple_module', + expected_output) + + def testFlagHelpInXML_Boolean(self): + gflags.DEFINE_boolean('use_hack', False, 'Use performance hack', + flag_values=self.fv) + expected_output = ( + ' \n' + ' yes\n' + ' a_module\n' + ' use_hack\n' + ' Use performance hack\n' + ' false\n' + ' false\n' + ' bool\n' + ' \n') + self._CheckFlagHelpInXML('use_hack', 'a_module', + expected_output, is_key=True) + + def testFlagHelpInXML_Enum(self): + gflags.DEFINE_enum('cc_version', 'stable', ['stable', 'experimental'], + 'Compiler version to use.', flag_values=self.fv) + expected_output = ( + ' \n' + ' tool\n' + ' cc_version\n' + ' <stable|experimental>: ' + 'Compiler version to use.\n' + ' stable\n' + ' stable\n' + ' string enum\n' + ' stable\n' + ' experimental\n' + ' \n') + self._CheckFlagHelpInXML('cc_version', 'tool', expected_output) + + def testFlagHelpInXML_CommaSeparatedList(self): + gflags.DEFINE_list('files', 'a.cc,a.h,archive/old.zip', + 'Files to process.', flag_values=self.fv) + expected_output = ( + ' \n' + ' tool\n' + ' files\n' + ' Files to process.\n' + ' a.cc,a.h,archive/old.zip\n' + ' [\'a.cc\', \'a.h\', \'archive/old.zip\']\n' + ' comma separated list of strings\n' + ' \',\'\n' + ' \n') + self._CheckFlagHelpInXML('files', 'tool', expected_output) + + def testListAsDefaultArgument_CommaSeparatedList(self): + gflags.DEFINE_list('allow_users', ['alice', 'bob'], + 'Users with access.', flag_values=self.fv) + expected_output = ( + ' \n' + ' tool\n' + ' allow_users\n' + ' Users with access.\n' + ' alice,bob\n' + ' [\'alice\', \'bob\']\n' + ' comma separated list of strings\n' + ' \',\'\n' + ' \n') + self._CheckFlagHelpInXML('allow_users', 'tool', expected_output) + + def testFlagHelpInXML_SpaceSeparatedList(self): + gflags.DEFINE_spaceseplist('dirs', 'src libs bin', + 'Directories to search.', flag_values=self.fv) + expected_output = ( + ' \n' + ' tool\n' + ' dirs\n' + ' Directories to search.\n' + ' src libs bin\n' + ' [\'src\', \'libs\', \'bin\']\n' + ' whitespace separated list of strings\n' + 'LIST_SEPARATORS' + ' \n').replace('LIST_SEPARATORS', + _ListSeparatorsInXMLFormat(string.whitespace, + indent=' ')) + self._CheckFlagHelpInXML('dirs', 'tool', expected_output) + + def testFlagHelpInXML_MultiString(self): + gflags.DEFINE_multistring('to_delete', ['a.cc', 'b.h'], + 'Files to delete', flag_values=self.fv) + expected_output = ( + ' \n' + ' tool\n' + ' to_delete\n' + ' Files to delete;\n ' + 'repeat this option to specify a list of values\n' + ' [\'a.cc\', \'b.h\']\n' + ' [\'a.cc\', \'b.h\']\n' + ' multi string\n' + ' \n') + self._CheckFlagHelpInXML('to_delete', 'tool', expected_output) + + def testFlagHelpInXML_MultiInt(self): + gflags.DEFINE_multi_int('cols', [5, 7, 23], + 'Columns to select', flag_values=self.fv) + expected_output = ( + ' \n' + ' tool\n' + ' cols\n' + ' Columns to select;\n ' + 'repeat this option to specify a list of values\n' + ' [5, 7, 23]\n' + ' [5, 7, 23]\n' + ' multi int\n' + ' \n') + self._CheckFlagHelpInXML('cols', 'tool', expected_output) + + +# The next EXPECTED_HELP_XML_* constants are parts of a template for +# the expected XML output from WriteHelpInXMLFormatTest below. When +# we assemble these parts into a single big string, we'll take into +# account the ordering between the name of the main module and the +# name of module_bar. Next, we'll fill in the docstring for this +# module (%(usage_doc)s), the name of the main module +# (%(main_module_name)s) and the name of the module module_bar +# (%(module_bar_name)s). See WriteHelpInXMLFormatTest below. +# +# NOTE: given the current implementation of _GetMainModule(), we +# already know the ordering between the main module and module_bar. +# However, there is no guarantee that _GetMainModule will never be +# changed in the future (especially since it's far from perfect). +EXPECTED_HELP_XML_START = """\ + + + gflags_helpxml_test.py + %(usage_doc)s +""" + +EXPECTED_HELP_XML_FOR_FLAGS_FROM_MAIN_MODULE = """\ + + yes + %(main_module_name)s + allow_users + Users with access. + alice,bob + ['alice', 'bob'] + comma separated list of strings + ',' + + + yes + %(main_module_name)s + cc_version + <stable|experimental>: Compiler version to use. + stable + stable + string enum + stable + experimental + + + yes + %(main_module_name)s + cols + Columns to select; + repeat this option to specify a list of values + [5, 7, 23] + [5, 7, 23] + multi int + + + yes + %(main_module_name)s + dirs + Directories to create. + src libs bins + ['src', 'libs', 'bins'] + whitespace separated list of strings +%(whitespace_separators)s + + yes + %(main_module_name)s + file_path + A test string flag. + /path/to/my/dir + /path/to/my/dir + string + + + yes + %(main_module_name)s + files + Files to process. + a.cc,a.h,archive/old.zip + ['a.cc', 'a.h', 'archive/old.zip'] + comma separated list of strings + \',\' + + + yes + %(main_module_name)s + index + An integer flag + 17 + 17 + int + + + yes + %(main_module_name)s + nb_iters + An integer flag + 17 + 17 + int + 5 + 27 + + + yes + %(main_module_name)s + to_delete + Files to delete; + repeat this option to specify a list of values + ['a.cc', 'b.h'] + ['a.cc', 'b.h'] + multi string + + + yes + %(main_module_name)s + use_hack + Use performance hack + false + false + bool + +""" + +EXPECTED_HELP_XML_FOR_FLAGS_FROM_MODULE_BAR = """\ + + %(module_bar_name)s + tmod_bar_t + Sample int flag. + 4 + 4 + int + + + yes + %(module_bar_name)s + tmod_bar_u + Sample int flag. + 5 + 5 + int + + + %(module_bar_name)s + tmod_bar_v + Sample int flag. + 6 + 6 + int + + + %(module_bar_name)s + tmod_bar_x + Boolean flag. + true + true + bool + + + %(module_bar_name)s + tmod_bar_y + String flag. + default + default + string + + + yes + %(module_bar_name)s + tmod_bar_z + Another boolean flag from module bar. + false + false + bool + +""" + +EXPECTED_HELP_XML_END = """\ + +""" + + +class WriteHelpInXMLFormatTest(googletest.TestCase): + """Big test of FlagValues.WriteHelpInXMLFormat, with several flags.""" + + def testWriteHelpInXMLFormat(self): + fv = gflags.FlagValues() + # Since these flags are defined by the top module, they are all key. + gflags.DEFINE_integer('index', 17, 'An integer flag', flag_values=fv) + gflags.DEFINE_integer('nb_iters', 17, 'An integer flag', + lower_bound=5, upper_bound=27, flag_values=fv) + gflags.DEFINE_string('file_path', '/path/to/my/dir', 'A test string flag.', + flag_values=fv) + gflags.DEFINE_boolean('use_hack', False, 'Use performance hack', + flag_values=fv) + gflags.DEFINE_enum('cc_version', 'stable', ['stable', 'experimental'], + 'Compiler version to use.', flag_values=fv) + gflags.DEFINE_list('files', 'a.cc,a.h,archive/old.zip', + 'Files to process.', flag_values=fv) + gflags.DEFINE_list('allow_users', ['alice', 'bob'], + 'Users with access.', flag_values=fv) + gflags.DEFINE_spaceseplist('dirs', 'src libs bins', + 'Directories to create.', flag_values=fv) + gflags.DEFINE_multistring('to_delete', ['a.cc', 'b.h'], + 'Files to delete', flag_values=fv) + gflags.DEFINE_multi_int('cols', [5, 7, 23], + 'Columns to select', flag_values=fv) + # Define a few flags in a different module. + module_bar.DefineFlags(flag_values=fv) + # And declare only a few of them to be key. This way, we have + # different kinds of flags, defined in different modules, and not + # all of them are key flags. + gflags.DECLARE_key_flag('tmod_bar_z', flag_values=fv) + gflags.DECLARE_key_flag('tmod_bar_u', flag_values=fv) + + # Generate flag help in XML format in the StringIO sio. + sio = StringIO.StringIO() + fv.WriteHelpInXMLFormat(sio) + + # Check that we got the expected result. + expected_output_template = EXPECTED_HELP_XML_START + main_module_name = gflags._GetMainModule() + module_bar_name = module_bar.__name__ + + if main_module_name < module_bar_name: + expected_output_template += EXPECTED_HELP_XML_FOR_FLAGS_FROM_MAIN_MODULE + expected_output_template += EXPECTED_HELP_XML_FOR_FLAGS_FROM_MODULE_BAR + else: + expected_output_template += EXPECTED_HELP_XML_FOR_FLAGS_FROM_MODULE_BAR + expected_output_template += EXPECTED_HELP_XML_FOR_FLAGS_FROM_MAIN_MODULE + + expected_output_template += EXPECTED_HELP_XML_END + + # XML representation of the whitespace list separators. + whitespace_separators = _ListSeparatorsInXMLFormat(string.whitespace, + indent=' ') + expected_output = ( + expected_output_template % + {'usage_doc': sys.modules['__main__'].__doc__, + 'main_module_name': main_module_name, + 'module_bar_name': module_bar_name, + 'whitespace_separators': whitespace_separators}) + + actual_output = sio.getvalue() + self.assertMultiLineEqual(actual_output, expected_output) + + # Also check that our result is valid XML. minidom.parseString + # throws an xml.parsers.expat.ExpatError in case of an error. + xml.dom.minidom.parseString(actual_output) + + +if __name__ == '__main__': + googletest.main() diff --git a/third_party/gjslint/python-gflags-2.0/tests/gflags_unittest.py b/third_party/gjslint/python-gflags-2.0/tests/gflags_unittest.py new file mode 100755 index 0000000000..8e948bf36f --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/tests/gflags_unittest.py @@ -0,0 +1,1949 @@ +#!/usr/bin/env python + +# Copyright (c) 2007, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"Unittest for gflags.py module" + +__pychecker__ = "no-local" # for unittest + + +import cStringIO +import sys +import os +import shutil + +import gflags +from flags_modules_for_testing import module_foo +from flags_modules_for_testing import module_bar +from flags_modules_for_testing import module_baz + +FLAGS=gflags.FLAGS + +import gflags_googletest as googletest + +# TODO(csilvers): add a wrapper function around FLAGS(argv) that +# verifies the input is a list or tuple. This avoids bugs where we +# make argv a string instead of a list, by mistake. + +class FlagsUnitTest(googletest.TestCase): + "Flags Unit Test" + + def setUp(self): + # make sure we are using the old, stupid way of parsing flags. + FLAGS.UseGnuGetOpt(False) + + def test_flags(self): + + ############################################## + # Test normal usage with no (expected) errors. + + # Define flags + number_test_framework_flags = len(FLAGS.RegisteredFlags()) + repeatHelp = "how many times to repeat (0-5)" + gflags.DEFINE_integer("repeat", 4, repeatHelp, + lower_bound=0, short_name='r') + gflags.DEFINE_string("name", "Bob", "namehelp") + gflags.DEFINE_boolean("debug", 0, "debughelp") + gflags.DEFINE_boolean("q", 1, "quiet mode") + gflags.DEFINE_boolean("quack", 0, "superstring of 'q'") + gflags.DEFINE_boolean("noexec", 1, "boolean flag with no as prefix") + gflags.DEFINE_integer("x", 3, "how eXtreme to be") + gflags.DEFINE_integer("l", 0x7fffffff00000000, "how long to be") + gflags.DEFINE_list('letters', 'a,b,c', "a list of letters") + gflags.DEFINE_list('numbers', [1, 2, 3], "a list of numbers") + gflags.DEFINE_enum("kwery", None, ['who', 'what', 'why', 'where', 'when'], + "?") + + # Specify number of flags defined above. The short_name defined + # for 'repeat' counts as an extra flag. + number_defined_flags = 11 + 1 + self.assertEqual(len(FLAGS.RegisteredFlags()), + number_defined_flags + number_test_framework_flags) + + assert FLAGS.repeat == 4, "integer default values not set:" + FLAGS.repeat + assert FLAGS.name == 'Bob', "default values not set:" + FLAGS.name + assert FLAGS.debug == 0, "boolean default values not set:" + FLAGS.debug + assert FLAGS.q == 1, "boolean default values not set:" + FLAGS.q + assert FLAGS.x == 3, "integer default values not set:" + FLAGS.x + assert FLAGS.l == 0x7fffffff00000000, ("integer default values not set:" + + FLAGS.l) + assert FLAGS.letters == ['a', 'b', 'c'], ("list default values not set:" + + FLAGS.letters) + assert FLAGS.numbers == [1, 2, 3], ("list default values not set:" + + FLAGS.numbers) + assert FLAGS.kwery is None, ("enum default None value not set:" + + FLAGS.kwery) + + flag_values = FLAGS.FlagValuesDict() + assert flag_values['repeat'] == 4 + assert flag_values['name'] == 'Bob' + assert flag_values['debug'] == 0 + assert flag_values['r'] == 4 # short for repeat + assert flag_values['q'] == 1 + assert flag_values['quack'] == 0 + assert flag_values['x'] == 3 + assert flag_values['l'] == 0x7fffffff00000000 + assert flag_values['letters'] == ['a', 'b', 'c'] + assert flag_values['numbers'] == [1, 2, 3] + assert flag_values['kwery'] is None + + # Verify string form of defaults + assert FLAGS['repeat'].default_as_str == "'4'" + assert FLAGS['name'].default_as_str == "'Bob'" + assert FLAGS['debug'].default_as_str == "'false'" + assert FLAGS['q'].default_as_str == "'true'" + assert FLAGS['quack'].default_as_str == "'false'" + assert FLAGS['noexec'].default_as_str == "'true'" + assert FLAGS['x'].default_as_str == "'3'" + assert FLAGS['l'].default_as_str == "'9223372032559808512'" + assert FLAGS['letters'].default_as_str == "'a,b,c'" + assert FLAGS['numbers'].default_as_str == "'1,2,3'" + + # Verify that the iterator for flags yields all the keys + keys = list(FLAGS) + keys.sort() + reg_flags = FLAGS.RegisteredFlags() + reg_flags.sort() + self.assertEqual(keys, reg_flags) + + # Parse flags + # .. empty command line + argv = ('./program',) + argv = FLAGS(argv) + assert len(argv) == 1, "wrong number of arguments pulled" + assert argv[0]=='./program', "program name not preserved" + + # .. non-empty command line + argv = ('./program', '--debug', '--name=Bob', '-q', '--x=8') + argv = FLAGS(argv) + assert len(argv) == 1, "wrong number of arguments pulled" + assert argv[0]=='./program', "program name not preserved" + assert FLAGS['debug'].present == 1 + FLAGS['debug'].present = 0 # Reset + assert FLAGS['name'].present == 1 + FLAGS['name'].present = 0 # Reset + assert FLAGS['q'].present == 1 + FLAGS['q'].present = 0 # Reset + assert FLAGS['x'].present == 1 + FLAGS['x'].present = 0 # Reset + + # Flags list + self.assertEqual(len(FLAGS.RegisteredFlags()), + number_defined_flags + number_test_framework_flags) + assert 'name' in FLAGS.RegisteredFlags() + assert 'debug' in FLAGS.RegisteredFlags() + assert 'repeat' in FLAGS.RegisteredFlags() + assert 'r' in FLAGS.RegisteredFlags() + assert 'q' in FLAGS.RegisteredFlags() + assert 'quack' in FLAGS.RegisteredFlags() + assert 'x' in FLAGS.RegisteredFlags() + assert 'l' in FLAGS.RegisteredFlags() + assert 'letters' in FLAGS.RegisteredFlags() + assert 'numbers' in FLAGS.RegisteredFlags() + + # has_key + assert FLAGS.has_key('name') + assert not FLAGS.has_key('name2') + assert 'name' in FLAGS + assert 'name2' not in FLAGS + + # try deleting a flag + del FLAGS.r + self.assertEqual(len(FLAGS.RegisteredFlags()), + number_defined_flags - 1 + number_test_framework_flags) + assert not 'r' in FLAGS.RegisteredFlags() + + # .. command line with extra stuff + argv = ('./program', '--debug', '--name=Bob', 'extra') + argv = FLAGS(argv) + assert len(argv) == 2, "wrong number of arguments pulled" + assert argv[0]=='./program', "program name not preserved" + assert argv[1]=='extra', "extra argument not preserved" + assert FLAGS['debug'].present == 1 + FLAGS['debug'].present = 0 # Reset + assert FLAGS['name'].present == 1 + FLAGS['name'].present = 0 # Reset + + # Test reset + argv = ('./program', '--debug') + argv = FLAGS(argv) + assert len(argv) == 1, "wrong number of arguments pulled" + assert argv[0] == './program', "program name not preserved" + assert FLAGS['debug'].present == 1 + assert FLAGS['debug'].value + FLAGS.Reset() + assert FLAGS['debug'].present == 0 + assert not FLAGS['debug'].value + + # Test that reset restores default value when default value is None. + argv = ('./program', '--kwery=who') + argv = FLAGS(argv) + assert len(argv) == 1, "wrong number of arguments pulled" + assert argv[0] == './program', "program name not preserved" + assert FLAGS['kwery'].present == 1 + assert FLAGS['kwery'].value == 'who' + FLAGS.Reset() + assert FLAGS['kwery'].present == 0 + assert FLAGS['kwery'].value == None + + # Test integer argument passing + argv = ('./program', '--x', '0x12345') + argv = FLAGS(argv) + self.assertEquals(FLAGS.x, 0x12345) + self.assertEquals(type(FLAGS.x), int) + + argv = ('./program', '--x', '0x1234567890ABCDEF1234567890ABCDEF') + argv = FLAGS(argv) + self.assertEquals(FLAGS.x, 0x1234567890ABCDEF1234567890ABCDEF) + self.assertEquals(type(FLAGS.x), long) + + # Treat 0-prefixed parameters as base-10, not base-8 + argv = ('./program', '--x', '012345') + argv = FLAGS(argv) + self.assertEquals(FLAGS.x, 12345) + self.assertEquals(type(FLAGS.x), int) + + argv = ('./program', '--x', '0123459') + argv = FLAGS(argv) + self.assertEquals(FLAGS.x, 123459) + self.assertEquals(type(FLAGS.x), int) + + argv = ('./program', '--x', '0x123efg') + try: + argv = FLAGS(argv) + raise AssertionError("failed to detect invalid hex argument") + except gflags.IllegalFlagValue: + pass + + # Test boolean argument parsing + gflags.DEFINE_boolean("test0", None, "test boolean parsing") + argv = ('./program', '--notest0') + argv = FLAGS(argv) + assert FLAGS.test0 == 0 + + gflags.DEFINE_boolean("test1", None, "test boolean parsing") + argv = ('./program', '--test1') + argv = FLAGS(argv) + assert FLAGS.test1 == 1 + + FLAGS.test0 = None + argv = ('./program', '--test0=false') + argv = FLAGS(argv) + assert FLAGS.test0 == 0 + + FLAGS.test1 = None + argv = ('./program', '--test1=true') + argv = FLAGS(argv) + assert FLAGS.test1 == 1 + + FLAGS.test0 = None + argv = ('./program', '--test0=0') + argv = FLAGS(argv) + assert FLAGS.test0 == 0 + + FLAGS.test1 = None + argv = ('./program', '--test1=1') + argv = FLAGS(argv) + assert FLAGS.test1 == 1 + + # Test booleans that already have 'no' as a prefix + FLAGS.noexec = None + argv = ('./program', '--nonoexec', '--name', 'Bob') + argv = FLAGS(argv) + assert FLAGS.noexec == 0 + + FLAGS.noexec = None + argv = ('./program', '--name', 'Bob', '--noexec') + argv = FLAGS(argv) + assert FLAGS.noexec == 1 + + # Test unassigned booleans + gflags.DEFINE_boolean("testnone", None, "test boolean parsing") + argv = ('./program',) + argv = FLAGS(argv) + assert FLAGS.testnone == None + + # Test get with default + gflags.DEFINE_boolean("testget1", None, "test parsing with defaults") + gflags.DEFINE_boolean("testget2", None, "test parsing with defaults") + gflags.DEFINE_boolean("testget3", None, "test parsing with defaults") + gflags.DEFINE_integer("testget4", None, "test parsing with defaults") + argv = ('./program','--testget1','--notestget2') + argv = FLAGS(argv) + assert FLAGS.get('testget1', 'foo') == 1 + assert FLAGS.get('testget2', 'foo') == 0 + assert FLAGS.get('testget3', 'foo') == 'foo' + assert FLAGS.get('testget4', 'foo') == 'foo' + + # test list code + lists = [['hello','moo','boo','1'], + [],] + + gflags.DEFINE_list('testlist', '', 'test lists parsing') + gflags.DEFINE_spaceseplist('testspacelist', '', 'tests space lists parsing') + + for name, sep in (('testlist', ','), ('testspacelist', ' '), + ('testspacelist', '\n')): + for lst in lists: + argv = ('./program', '--%s=%s' % (name, sep.join(lst))) + argv = FLAGS(argv) + self.assertEquals(getattr(FLAGS, name), lst) + + # Test help text + flagsHelp = str(FLAGS) + assert flagsHelp.find("repeat") != -1, "cannot find flag in help" + assert flagsHelp.find(repeatHelp) != -1, "cannot find help string in help" + + # Test flag specified twice + argv = ('./program', '--repeat=4', '--repeat=2', '--debug', '--nodebug') + argv = FLAGS(argv) + self.assertEqual(FLAGS.get('repeat', None), 2) + self.assertEqual(FLAGS.get('debug', None), 0) + + # Test MultiFlag with single default value + gflags.DEFINE_multistring('s_str', 'sing1', + 'string option that can occur multiple times', + short_name='s') + self.assertEqual(FLAGS.get('s_str', None), [ 'sing1', ]) + + # Test MultiFlag with list of default values + multi_string_defs = [ 'def1', 'def2', ] + gflags.DEFINE_multistring('m_str', multi_string_defs, + 'string option that can occur multiple times', + short_name='m') + self.assertEqual(FLAGS.get('m_str', None), multi_string_defs) + + # Test flag specified multiple times with a MultiFlag + argv = ('./program', '--m_str=str1', '-m', 'str2') + argv = FLAGS(argv) + self.assertEqual(FLAGS.get('m_str', None), [ 'str1', 'str2', ]) + + # Test single-letter flags; should support both single and double dash + argv = ('./program', '-q', '-x8') + argv = FLAGS(argv) + self.assertEqual(FLAGS.get('q', None), 1) + self.assertEqual(FLAGS.get('x', None), 8) + + argv = ('./program', '--q', '--x', '9', '--noqu') + argv = FLAGS(argv) + self.assertEqual(FLAGS.get('q', None), 1) + self.assertEqual(FLAGS.get('x', None), 9) + # --noqu should match '--noquack since it's a unique prefix + self.assertEqual(FLAGS.get('quack', None), 0) + + argv = ('./program', '--noq', '--x=10', '--qu') + argv = FLAGS(argv) + self.assertEqual(FLAGS.get('q', None), 0) + self.assertEqual(FLAGS.get('x', None), 10) + self.assertEqual(FLAGS.get('quack', None), 1) + + #################################### + # Test flag serialization code: + + oldtestlist = FLAGS.testlist + oldtestspacelist = FLAGS.testspacelist + + argv = ('./program', + FLAGS['test0'].Serialize(), + FLAGS['test1'].Serialize(), + FLAGS['testnone'].Serialize(), + FLAGS['s_str'].Serialize()) + argv = FLAGS(argv) + self.assertEqual(FLAGS['test0'].Serialize(), '--notest0') + self.assertEqual(FLAGS['test1'].Serialize(), '--test1') + self.assertEqual(FLAGS['testnone'].Serialize(), '') + self.assertEqual(FLAGS['s_str'].Serialize(), '--s_str=sing1') + + testlist1 = ['aa', 'bb'] + testspacelist1 = ['aa', 'bb', 'cc'] + FLAGS.testlist = list(testlist1) + FLAGS.testspacelist = list(testspacelist1) + argv = ('./program', + FLAGS['testlist'].Serialize(), + FLAGS['testspacelist'].Serialize()) + argv = FLAGS(argv) + self.assertEqual(FLAGS.testlist, testlist1) + self.assertEqual(FLAGS.testspacelist, testspacelist1) + + testlist1 = ['aa some spaces', 'bb'] + testspacelist1 = ['aa', 'bb,some,commas,', 'cc'] + FLAGS.testlist = list(testlist1) + FLAGS.testspacelist = list(testspacelist1) + argv = ('./program', + FLAGS['testlist'].Serialize(), + FLAGS['testspacelist'].Serialize()) + argv = FLAGS(argv) + self.assertEqual(FLAGS.testlist, testlist1) + self.assertEqual(FLAGS.testspacelist, testspacelist1) + + FLAGS.testlist = oldtestlist + FLAGS.testspacelist = oldtestspacelist + + #################################### + # Test flag-update: + + def ArgsString(): + flagnames = FLAGS.RegisteredFlags() + + flagnames.sort() + nonbool_flags = ['--%s %s' % (name, FLAGS.get(name, None)) + for name in flagnames + if not isinstance(FLAGS[name], gflags.BooleanFlag)] + + truebool_flags = ['--%s' % (name) + for name in flagnames + if isinstance(FLAGS[name], gflags.BooleanFlag) and + FLAGS.get(name, None)] + falsebool_flags = ['--no%s' % (name) + for name in flagnames + if isinstance(FLAGS[name], gflags.BooleanFlag) and + not FLAGS.get(name, None)] + return ' '.join(nonbool_flags + truebool_flags + falsebool_flags) + + argv = ('./program', '--repeat=3', '--name=giants', '--nodebug') + + FLAGS(argv) + self.assertEqual(FLAGS.get('repeat', None), 3) + self.assertEqual(FLAGS.get('name', None), 'giants') + self.assertEqual(FLAGS.get('debug', None), 0) + self.assertEqual(ArgsString(), + "--kwery None " + "--l 9223372032559808512 " + "--letters ['a', 'b', 'c'] " + "--m ['str1', 'str2'] --m_str ['str1', 'str2'] " + "--name giants " + "--numbers [1, 2, 3] " + "--repeat 3 " + "--s ['sing1'] --s_str ['sing1'] " + "" + "" + "--testget4 None --testlist [] " + "--testspacelist [] --x 10 " + "--noexec --quack " + "--test1 " + "--testget1 --tmod_baz_x " + "--no? --nodebug --nohelp --nohelpshort --nohelpxml --noq " + "" + "--notest0 --notestget2 --notestget3 --notestnone") + + argv = ('./program', '--debug', '--m_str=upd1', '-s', 'upd2') + FLAGS(argv) + self.assertEqual(FLAGS.get('repeat', None), 3) + self.assertEqual(FLAGS.get('name', None), 'giants') + self.assertEqual(FLAGS.get('debug', None), 1) + + # items appended to existing non-default value lists for --m/--m_str + # new value overwrites default value (not appended to it) for --s/--s_str + self.assertEqual(ArgsString(), + "--kwery None " + "--l 9223372032559808512 " + "--letters ['a', 'b', 'c'] " + "--m ['str1', 'str2', 'upd1'] " + "--m_str ['str1', 'str2', 'upd1'] " + "--name giants " + "--numbers [1, 2, 3] " + "--repeat 3 " + "--s ['upd2'] --s_str ['upd2'] " + "" + "" + "--testget4 None --testlist [] " + "--testspacelist [] --x 10 " + "--debug --noexec --quack " + "--test1 " + "--testget1 --tmod_baz_x " + "--no? --nohelp --nohelpshort --nohelpxml --noq " + "" + "--notest0 --notestget2 --notestget3 --notestnone") + + #################################### + # Test all kind of error conditions. + + # Duplicate flag detection + try: + gflags.DEFINE_boolean("run", 0, "runhelp", short_name='q') + raise AssertionError("duplicate flag detection failed") + except gflags.DuplicateFlag: + pass + + # Duplicate short flag detection + try: + gflags.DEFINE_boolean("zoom1", 0, "runhelp z1", short_name='z') + gflags.DEFINE_boolean("zoom2", 0, "runhelp z2", short_name='z') + raise AssertionError("duplicate short flag detection failed") + except gflags.DuplicateFlag, e: + self.assertTrue("The flag 'z' is defined twice. " in e.args[0]) + self.assertTrue("First from" in e.args[0]) + self.assertTrue(", Second from" in e.args[0]) + + # Duplicate mixed flag detection + try: + gflags.DEFINE_boolean("short1", 0, "runhelp s1", short_name='s') + gflags.DEFINE_boolean("s", 0, "runhelp s2") + raise AssertionError("duplicate mixed flag detection failed") + except gflags.DuplicateFlag, e: + self.assertTrue("The flag 's' is defined twice. " in e.args[0]) + self.assertTrue("First from" in e.args[0]) + self.assertTrue(", Second from" in e.args[0]) + + # Check that duplicate flag detection detects definition sites + # correctly. + flagnames = ["repeated"] + original_flags = gflags.FlagValues() + gflags.DEFINE_boolean(flagnames[0], False, "Flag about to be repeated.", + flag_values=original_flags) + duplicate_flags = module_foo.DuplicateFlags(flagnames) + try: + original_flags.AppendFlagValues(duplicate_flags) + except gflags.DuplicateFlagError, e: + self.assertTrue("flags_unittest" in str(e)) + self.assertTrue("module_foo" in str(e)) + + # Make sure allow_override works + try: + gflags.DEFINE_boolean("dup1", 0, "runhelp d11", short_name='u', + allow_override=0) + flag = FLAGS.FlagDict()['dup1'] + self.assertEqual(flag.default, 0) + + gflags.DEFINE_boolean("dup1", 1, "runhelp d12", short_name='u', + allow_override=1) + flag = FLAGS.FlagDict()['dup1'] + self.assertEqual(flag.default, 1) + except gflags.DuplicateFlag: + raise AssertionError("allow_override did not permit a flag duplication") + + # Make sure allow_override works + try: + gflags.DEFINE_boolean("dup2", 0, "runhelp d21", short_name='u', + allow_override=1) + flag = FLAGS.FlagDict()['dup2'] + self.assertEqual(flag.default, 0) + + gflags.DEFINE_boolean("dup2", 1, "runhelp d22", short_name='u', + allow_override=0) + flag = FLAGS.FlagDict()['dup2'] + self.assertEqual(flag.default, 1) + except gflags.DuplicateFlag: + raise AssertionError("allow_override did not permit a flag duplication") + + # Make sure allow_override doesn't work with None default + try: + gflags.DEFINE_boolean("dup3", 0, "runhelp d31", short_name='u3', + allow_override=0) + flag = FLAGS.FlagDict()['dup3'] + self.assertEqual(flag.default, 0) + + gflags.DEFINE_boolean("dup3", None, "runhelp d32", short_name='u3', + allow_override=1) + raise AssertionError('Cannot override a flag with a default of None') + except gflags.DuplicateFlagCannotPropagateNoneToSwig: + pass + + # Make sure that re-importing a module does not cause a DuplicateFlagError + # to be raised. + try: + sys.modules.pop( + "flags_modules_for_testing.module_baz") + import flags_modules_for_testing.module_baz + except gflags.DuplicateFlagError: + raise AssertionError("Module reimport caused flag duplication error") + + # Make sure that when we override, the help string gets updated correctly + gflags.DEFINE_boolean("dup3", 0, "runhelp d31", short_name='u', + allow_override=1) + gflags.DEFINE_boolean("dup3", 1, "runhelp d32", short_name='u', + allow_override=1) + self.assert_(str(FLAGS).find('runhelp d31') == -1) + self.assert_(str(FLAGS).find('runhelp d32') != -1) + + # Make sure AppendFlagValues works + new_flags = gflags.FlagValues() + gflags.DEFINE_boolean("new1", 0, "runhelp n1", flag_values=new_flags) + gflags.DEFINE_boolean("new2", 0, "runhelp n2", flag_values=new_flags) + self.assertEqual(len(new_flags.FlagDict()), 2) + old_len = len(FLAGS.FlagDict()) + FLAGS.AppendFlagValues(new_flags) + self.assertEqual(len(FLAGS.FlagDict())-old_len, 2) + self.assertEqual("new1" in FLAGS.FlagDict(), True) + self.assertEqual("new2" in FLAGS.FlagDict(), True) + + # Then test that removing those flags works + FLAGS.RemoveFlagValues(new_flags) + self.assertEqual(len(FLAGS.FlagDict()), old_len) + self.assertFalse("new1" in FLAGS.FlagDict()) + self.assertFalse("new2" in FLAGS.FlagDict()) + + # Make sure AppendFlagValues works with flags with shortnames. + new_flags = gflags.FlagValues() + gflags.DEFINE_boolean("new3", 0, "runhelp n3", flag_values=new_flags) + gflags.DEFINE_boolean("new4", 0, "runhelp n4", flag_values=new_flags, + short_name="n4") + self.assertEqual(len(new_flags.FlagDict()), 3) + old_len = len(FLAGS.FlagDict()) + FLAGS.AppendFlagValues(new_flags) + self.assertEqual(len(FLAGS.FlagDict())-old_len, 3) + self.assertTrue("new3" in FLAGS.FlagDict()) + self.assertTrue("new4" in FLAGS.FlagDict()) + self.assertTrue("n4" in FLAGS.FlagDict()) + self.assertEqual(FLAGS.FlagDict()['n4'], FLAGS.FlagDict()['new4']) + + # Then test removing them + FLAGS.RemoveFlagValues(new_flags) + self.assertEqual(len(FLAGS.FlagDict()), old_len) + self.assertFalse("new3" in FLAGS.FlagDict()) + self.assertFalse("new4" in FLAGS.FlagDict()) + self.assertFalse("n4" in FLAGS.FlagDict()) + + # Make sure AppendFlagValues fails on duplicates + gflags.DEFINE_boolean("dup4", 0, "runhelp d41") + new_flags = gflags.FlagValues() + gflags.DEFINE_boolean("dup4", 0, "runhelp d42", flag_values=new_flags) + try: + FLAGS.AppendFlagValues(new_flags) + raise AssertionError("ignore_copy was not set but caused no exception") + except gflags.DuplicateFlag: + pass + + # Integer out of bounds + try: + argv = ('./program', '--repeat=-4') + FLAGS(argv) + raise AssertionError('integer bounds exception not raised:' + + str(FLAGS.repeat)) + except gflags.IllegalFlagValue: + pass + + # Non-integer + try: + argv = ('./program', '--repeat=2.5') + FLAGS(argv) + raise AssertionError("malformed integer value exception not raised") + except gflags.IllegalFlagValue: + pass + + # Missing required arugment + try: + argv = ('./program', '--name') + FLAGS(argv) + raise AssertionError("Flag argument required exception not raised") + except gflags.FlagsError: + pass + + # Non-boolean arguments for boolean + try: + argv = ('./program', '--debug=goofup') + FLAGS(argv) + raise AssertionError("Illegal flag value exception not raised") + except gflags.IllegalFlagValue: + pass + + try: + argv = ('./program', '--debug=42') + FLAGS(argv) + raise AssertionError("Illegal flag value exception not raised") + except gflags.IllegalFlagValue: + pass + + + # Non-numeric argument for integer flag --repeat + try: + argv = ('./program', '--repeat', 'Bob', 'extra') + FLAGS(argv) + raise AssertionError("Illegal flag value exception not raised") + except gflags.IllegalFlagValue: + pass + + # Test ModuleHelp(). + helpstr = FLAGS.ModuleHelp(module_baz) + + expected_help = "\n" + module_baz.__name__ + ":" + """ + --[no]tmod_baz_x: Boolean flag. + (default: 'true')""" + + self.assertMultiLineEqual(expected_help, helpstr) + + # Test MainModuleHelp(). This must be part of test_flags because + # it dpeends on dup1/2/3/etc being introduced first. + helpstr = FLAGS.MainModuleHelp() + + expected_help = "\n" + sys.argv[0] + ':' + """ + --[no]debug: debughelp + (default: 'false') + -u,--[no]dup1: runhelp d12 + (default: 'true') + -u,--[no]dup2: runhelp d22 + (default: 'true') + -u,--[no]dup3: runhelp d32 + (default: 'true') + --[no]dup4: runhelp d41 + (default: 'false') + --kwery: : ? + --l: how long to be + (default: '9223372032559808512') + (an integer) + --letters: a list of letters + (default: 'a,b,c') + (a comma separated list) + -m,--m_str: string option that can occur multiple times; + repeat this option to specify a list of values + (default: "['def1', 'def2']") + --name: namehelp + (default: 'Bob') + --[no]noexec: boolean flag with no as prefix + (default: 'true') + --numbers: a list of numbers + (default: '1,2,3') + (a comma separated list) + --[no]q: quiet mode + (default: 'true') + --[no]quack: superstring of 'q' + (default: 'false') + -r,--repeat: how many times to repeat (0-5) + (default: '4') + (a non-negative integer) + -s,--s_str: string option that can occur multiple times; + repeat this option to specify a list of values + (default: "['sing1']") + --[no]test0: test boolean parsing + --[no]test1: test boolean parsing + --[no]testget1: test parsing with defaults + --[no]testget2: test parsing with defaults + --[no]testget3: test parsing with defaults + --testget4: test parsing with defaults + (an integer) + --testlist: test lists parsing + (default: '') + (a comma separated list) + --[no]testnone: test boolean parsing + --testspacelist: tests space lists parsing + (default: '') + (a whitespace separated list) + --x: how eXtreme to be + (default: '3') + (an integer) + -z,--[no]zoom1: runhelp z1 + (default: 'false')""" + + # Insert the --help flags in their proper place. + help_help = """\ + -?,--[no]help: show this help + --[no]helpshort: show usage only for this module + --[no]helpxml: like --help, but generates XML output +""" + expected_help = expected_help.replace(' --kwery', + help_help + ' --kwery') + + self.assertMultiLineEqual(expected_help, helpstr) + + +class MultiNumericalFlagsTest(googletest.TestCase): + + def testMultiNumericalFlags(self): + """Test multi_int and multi_float flags.""" + + int_defaults = [77, 88,] + gflags.DEFINE_multi_int('m_int', int_defaults, + 'integer option that can occur multiple times', + short_name='mi') + self.assertListEqual(FLAGS.get('m_int', None), int_defaults) + argv = ('./program', '--m_int=-99', '--mi=101') + FLAGS(argv) + self.assertListEqual(FLAGS.get('m_int', None), [-99, 101,]) + + float_defaults = [2.2, 3] + gflags.DEFINE_multi_float('m_float', float_defaults, + 'float option that can occur multiple times', + short_name='mf') + for (expected, actual) in zip(float_defaults, FLAGS.get('m_float', None)): + self.assertAlmostEquals(expected, actual) + argv = ('./program', '--m_float=-17', '--mf=2.78e9') + FLAGS(argv) + expected_floats = [-17.0, 2.78e9] + for (expected, actual) in zip(expected_floats, FLAGS.get('m_float', None)): + self.assertAlmostEquals(expected, actual) + + def testSingleValueDefault(self): + """Test multi_int and multi_float flags with a single default value.""" + int_default = 77 + gflags.DEFINE_multi_int('m_int1', int_default, + 'integer option that can occur multiple times') + self.assertListEqual(FLAGS.get('m_int1', None), [int_default]) + + float_default = 2.2 + gflags.DEFINE_multi_float('m_float1', float_default, + 'float option that can occur multiple times') + actual = FLAGS.get('m_float1', None) + self.assertEquals(1, len(actual)) + self.assertAlmostEquals(actual[0], float_default) + + def testBadMultiNumericalFlags(self): + """Test multi_int and multi_float flags with non-parseable values.""" + + # Test non-parseable defaults. + self.assertRaisesWithRegexpMatch( + gflags.IllegalFlagValue, + 'flag --m_int2=abc: invalid literal for int\(\) with base 10: \'abc\'', + gflags.DEFINE_multi_int, 'm_int2', ['abc'], 'desc') + + self.assertRaisesWithRegexpMatch( + gflags.IllegalFlagValue, + 'flag --m_float2=abc: invalid literal for float\(\): abc', + gflags.DEFINE_multi_float, 'm_float2', ['abc'], 'desc') + + # Test non-parseable command line values. + gflags.DEFINE_multi_int('m_int2', '77', + 'integer option that can occur multiple times') + argv = ('./program', '--m_int2=def') + self.assertRaisesWithRegexpMatch( + gflags.IllegalFlagValue, + 'flag --m_int2=def: invalid literal for int\(\) with base 10: \'def\'', + FLAGS, argv) + + gflags.DEFINE_multi_float('m_float2', 2.2, + 'float option that can occur multiple times') + argv = ('./program', '--m_float2=def') + self.assertRaisesWithRegexpMatch( + gflags.IllegalFlagValue, + 'flag --m_float2=def: invalid literal for float\(\): def', + FLAGS, argv) + + +class UnicodeFlagsTest(googletest.TestCase): + """Testing proper unicode support for flags.""" + + def testUnicodeDefaultAndHelpstring(self): + gflags.DEFINE_string("unicode_str", "\xC3\x80\xC3\xBD".decode("utf-8"), + "help:\xC3\xAA".decode("utf-8")) + argv = ("./program",) + FLAGS(argv) # should not raise any exceptions + + argv = ("./program", "--unicode_str=foo") + FLAGS(argv) # should not raise any exceptions + + def testUnicodeInList(self): + gflags.DEFINE_list("unicode_list", ["abc", "\xC3\x80".decode("utf-8"), + "\xC3\xBD".decode("utf-8")], + "help:\xC3\xAB".decode("utf-8")) + argv = ("./program",) + FLAGS(argv) # should not raise any exceptions + + argv = ("./program", "--unicode_list=hello,there") + FLAGS(argv) # should not raise any exceptions + + def testXMLOutput(self): + gflags.DEFINE_string("unicode1", "\xC3\x80\xC3\xBD".decode("utf-8"), + "help:\xC3\xAC".decode("utf-8")) + gflags.DEFINE_list("unicode2", ["abc", "\xC3\x80".decode("utf-8"), + "\xC3\xBD".decode("utf-8")], + "help:\xC3\xAD".decode("utf-8")) + gflags.DEFINE_list("non_unicode", ["abc", "def", "ghi"], + "help:\xC3\xAD".decode("utf-8")) + + outfile = cStringIO.StringIO() + FLAGS.WriteHelpInXMLFormat(outfile) + actual_output = outfile.getvalue() + + # The xml output is large, so we just check parts of it. + self.assertTrue("unicode1\n" + " help:ì\n" + " Àý\n" + " Àý" + in actual_output) + self.assertTrue("unicode2\n" + " help:í\n" + " abc,À,ý\n" + " [\'abc\', u\'\\xc0\', u\'\\xfd\']" + in actual_output) + self.assertTrue("non_unicode\n" + " help:í\n" + " abc,def,ghi\n" + " [\'abc\', \'def\', \'ghi\']" + in actual_output) + + +class LoadFromFlagFileTest(googletest.TestCase): + """Testing loading flags from a file and parsing them.""" + + def setUp(self): + self.flag_values = gflags.FlagValues() + # make sure we are using the old, stupid way of parsing flags. + self.flag_values.UseGnuGetOpt(False) + gflags.DEFINE_string('UnitTestMessage1', 'Foo!', 'You Add Here.', + flag_values=self.flag_values) + gflags.DEFINE_string('UnitTestMessage2', 'Bar!', 'Hello, Sailor!', + flag_values=self.flag_values) + gflags.DEFINE_boolean('UnitTestBoolFlag', 0, 'Some Boolean thing', + flag_values=self.flag_values) + gflags.DEFINE_integer('UnitTestNumber', 12345, 'Some integer', + lower_bound=0, flag_values=self.flag_values) + gflags.DEFINE_list('UnitTestList', "1,2,3", 'Some list', + flag_values=self.flag_values) + self.files_to_delete = [] + + def tearDown(self): + self._RemoveTestFiles() + + def _SetupTestFiles(self): + """ Creates and sets up some dummy flagfile files with bogus flags""" + + # Figure out where to create temporary files + tmp_path = '/tmp/flags_unittest' + if os.path.exists(tmp_path): + shutil.rmtree(tmp_path) + os.makedirs(tmp_path) + + try: + tmp_flag_file_1 = open(tmp_path + '/UnitTestFile1.tst', 'w') + tmp_flag_file_2 = open(tmp_path + '/UnitTestFile2.tst', 'w') + tmp_flag_file_3 = open(tmp_path + '/UnitTestFile3.tst', 'w') + tmp_flag_file_4 = open(tmp_path + '/UnitTestFile4.tst', 'w') + except IOError, e_msg: + print e_msg + print 'FAIL\n File Creation problem in Unit Test' + sys.exit(1) + + # put some dummy flags in our test files + tmp_flag_file_1.write('#A Fake Comment\n') + tmp_flag_file_1.write('--UnitTestMessage1=tempFile1!\n') + tmp_flag_file_1.write('\n') + tmp_flag_file_1.write('--UnitTestNumber=54321\n') + tmp_flag_file_1.write('--noUnitTestBoolFlag\n') + file_list = [tmp_flag_file_1.name] + # this one includes test file 1 + tmp_flag_file_2.write('//A Different Fake Comment\n') + tmp_flag_file_2.write('--flagfile=%s\n' % tmp_flag_file_1.name) + tmp_flag_file_2.write('--UnitTestMessage2=setFromTempFile2\n') + tmp_flag_file_2.write('\t\t\n') + tmp_flag_file_2.write('--UnitTestNumber=6789a\n') + file_list.append(tmp_flag_file_2.name) + # this file points to itself + tmp_flag_file_3.write('--flagfile=%s\n' % tmp_flag_file_3.name) + tmp_flag_file_3.write('--UnitTestMessage1=setFromTempFile3\n') + tmp_flag_file_3.write('#YAFC\n') + tmp_flag_file_3.write('--UnitTestBoolFlag\n') + file_list.append(tmp_flag_file_3.name) + # this file is unreadable + tmp_flag_file_4.write('--flagfile=%s\n' % tmp_flag_file_3.name) + tmp_flag_file_4.write('--UnitTestMessage1=setFromTempFile3\n') + tmp_flag_file_4.write('--UnitTestMessage1=setFromTempFile3\n') + os.chmod(tmp_path + '/UnitTestFile4.tst', 0) + file_list.append(tmp_flag_file_4.name) + + tmp_flag_file_1.close() + tmp_flag_file_2.close() + tmp_flag_file_3.close() + tmp_flag_file_4.close() + + self.files_to_delete = file_list + + return file_list # these are just the file names + # end SetupFiles def + + def _RemoveTestFiles(self): + """Closes the files we just created. tempfile deletes them for us """ + for file_name in self.files_to_delete: + try: + os.remove(file_name) + except OSError, e_msg: + print '%s\n, Problem deleting test file' % e_msg + #end RemoveTestFiles def + + def _ReadFlagsFromFiles(self, argv, force_gnu): + return argv[:1] + self.flag_values.ReadFlagsFromFiles(argv[1:], + force_gnu=force_gnu) + + #### Flagfile Unit Tests #### + def testMethod_flagfiles_1(self): + """ Test trivial case with no flagfile based options. """ + fake_cmd_line = 'fooScript --UnitTestBoolFlag' + fake_argv = fake_cmd_line.split(' ') + self.flag_values(fake_argv) + self.assertEqual( self.flag_values.UnitTestBoolFlag, 1) + self.assertEqual( fake_argv, self._ReadFlagsFromFiles(fake_argv, False)) + + # end testMethodOne + + def testMethod_flagfiles_2(self): + """Tests parsing one file + arguments off simulated argv""" + tmp_files = self._SetupTestFiles() + # specify our temp file on the fake cmd line + fake_cmd_line = 'fooScript --q --flagfile=%s' % tmp_files[0] + fake_argv = fake_cmd_line.split(' ') + + # We should see the original cmd line with the file's contents spliced in. + # Flags from the file will appear in the order order they are sepcified + # in the file, in the same position as the flagfile argument. + expected_results = ['fooScript', + '--q', + '--UnitTestMessage1=tempFile1!', + '--UnitTestNumber=54321', + '--noUnitTestBoolFlag'] + test_results = self._ReadFlagsFromFiles(fake_argv, False) + self.assertEqual(expected_results, test_results) + # end testTwo def + + def testMethod_flagfiles_3(self): + """Tests parsing nested files + arguments of simulated argv""" + tmp_files = self._SetupTestFiles() + # specify our temp file on the fake cmd line + fake_cmd_line = ('fooScript --UnitTestNumber=77 --flagfile=%s' + % tmp_files[1]) + fake_argv = fake_cmd_line.split(' ') + + expected_results = ['fooScript', + '--UnitTestNumber=77', + '--UnitTestMessage1=tempFile1!', + '--UnitTestNumber=54321', + '--noUnitTestBoolFlag', + '--UnitTestMessage2=setFromTempFile2', + '--UnitTestNumber=6789a'] + test_results = self._ReadFlagsFromFiles(fake_argv, False) + self.assertEqual(expected_results, test_results) + # end testThree def + + def testMethod_flagfiles_4(self): + """Tests parsing self-referential files + arguments of simulated argv. + This test should print a warning to stderr of some sort. + """ + tmp_files = self._SetupTestFiles() + # specify our temp file on the fake cmd line + fake_cmd_line = ('fooScript --flagfile=%s --noUnitTestBoolFlag' + % tmp_files[2]) + fake_argv = fake_cmd_line.split(' ') + expected_results = ['fooScript', + '--UnitTestMessage1=setFromTempFile3', + '--UnitTestBoolFlag', + '--noUnitTestBoolFlag' ] + + test_results = self._ReadFlagsFromFiles(fake_argv, False) + self.assertEqual(expected_results, test_results) + + def testMethod_flagfiles_5(self): + """Test that --flagfile parsing respects the '--' end-of-options marker.""" + tmp_files = self._SetupTestFiles() + # specify our temp file on the fake cmd line + fake_cmd_line = 'fooScript --SomeFlag -- --flagfile=%s' % tmp_files[0] + fake_argv = fake_cmd_line.split(' ') + expected_results = ['fooScript', + '--SomeFlag', + '--', + '--flagfile=%s' % tmp_files[0]] + + test_results = self._ReadFlagsFromFiles(fake_argv, False) + self.assertEqual(expected_results, test_results) + + def testMethod_flagfiles_6(self): + """Test that --flagfile parsing stops at non-options (non-GNU behavior).""" + tmp_files = self._SetupTestFiles() + # specify our temp file on the fake cmd line + fake_cmd_line = ('fooScript --SomeFlag some_arg --flagfile=%s' + % tmp_files[0]) + fake_argv = fake_cmd_line.split(' ') + expected_results = ['fooScript', + '--SomeFlag', + 'some_arg', + '--flagfile=%s' % tmp_files[0]] + + test_results = self._ReadFlagsFromFiles(fake_argv, False) + self.assertEqual(expected_results, test_results) + + def testMethod_flagfiles_7(self): + """Test that --flagfile parsing skips over a non-option (GNU behavior).""" + self.flag_values.UseGnuGetOpt() + tmp_files = self._SetupTestFiles() + # specify our temp file on the fake cmd line + fake_cmd_line = ('fooScript --SomeFlag some_arg --flagfile=%s' + % tmp_files[0]) + fake_argv = fake_cmd_line.split(' ') + expected_results = ['fooScript', + '--SomeFlag', + 'some_arg', + '--UnitTestMessage1=tempFile1!', + '--UnitTestNumber=54321', + '--noUnitTestBoolFlag'] + + test_results = self._ReadFlagsFromFiles(fake_argv, False) + self.assertEqual(expected_results, test_results) + + def testMethod_flagfiles_8(self): + """Test that --flagfile parsing respects force_gnu=True.""" + tmp_files = self._SetupTestFiles() + # specify our temp file on the fake cmd line + fake_cmd_line = ('fooScript --SomeFlag some_arg --flagfile=%s' + % tmp_files[0]) + fake_argv = fake_cmd_line.split(' ') + expected_results = ['fooScript', + '--SomeFlag', + 'some_arg', + '--UnitTestMessage1=tempFile1!', + '--UnitTestNumber=54321', + '--noUnitTestBoolFlag'] + + test_results = self._ReadFlagsFromFiles(fake_argv, True) + self.assertEqual(expected_results, test_results) + + def testMethod_flagfiles_NoPermissions(self): + """Test that --flagfile raises except on file that is unreadable.""" + tmp_files = self._SetupTestFiles() + # specify our temp file on the fake cmd line + fake_cmd_line = ('fooScript --SomeFlag some_arg --flagfile=%s' + % tmp_files[3]) + fake_argv = fake_cmd_line.split(' ') + self.assertRaises(gflags.CantOpenFlagFileError, + self._ReadFlagsFromFiles, fake_argv, True) + + def testMethod_flagfiles_NotFound(self): + """Test that --flagfile raises except on file that does not exist.""" + tmp_files = self._SetupTestFiles() + # specify our temp file on the fake cmd line + fake_cmd_line = ('fooScript --SomeFlag some_arg --flagfile=%sNOTEXIST' + % tmp_files[3]) + fake_argv = fake_cmd_line.split(' ') + self.assertRaises(gflags.CantOpenFlagFileError, + self._ReadFlagsFromFiles, fake_argv, True) + + def test_flagfiles_user_path_expansion(self): + """Test that user directory referenced paths (ie. ~/foo) are correctly + expanded. This test depends on whatever account's running the unit test + to have read/write access to their own home directory, otherwise it'll + FAIL. + """ + fake_flagfile_item_style_1 = '--flagfile=~/foo.file' + fake_flagfile_item_style_2 = '-flagfile=~/foo.file' + + expected_results = os.path.expanduser('~/foo.file') + + test_results = self.flag_values.ExtractFilename(fake_flagfile_item_style_1) + self.assertEqual(expected_results, test_results) + + test_results = self.flag_values.ExtractFilename(fake_flagfile_item_style_2) + self.assertEqual(expected_results, test_results) + + # end testFour def + + def test_no_touchy_non_flags(self): + """ + Test that the flags parser does not mutilate arguments which are + not supposed to be flags + """ + fake_argv = ['fooScript', '--UnitTestBoolFlag', + 'command', '--command_arg1', '--UnitTestBoom', '--UnitTestB'] + argv = self.flag_values(fake_argv) + self.assertEqual(argv, fake_argv[:1] + fake_argv[2:]) + + def test_parse_flags_after_args_if_using_gnu_getopt(self): + """ + Test that flags given after arguments are parsed if using gnu_getopt. + """ + self.flag_values.UseGnuGetOpt() + fake_argv = ['fooScript', '--UnitTestBoolFlag', + 'command', '--UnitTestB'] + argv = self.flag_values(fake_argv) + self.assertEqual(argv, ['fooScript', 'command']) + + def test_SetDefault(self): + """ + Test changing flag defaults. + """ + # Test that SetDefault changes both the default and the value, + # and that the value is changed when one is given as an option. + self.flag_values['UnitTestMessage1'].SetDefault('New value') + self.assertEqual(self.flag_values.UnitTestMessage1, 'New value') + self.assertEqual(self.flag_values['UnitTestMessage1'].default_as_str, + "'New value'") + self.flag_values([ 'dummyscript', '--UnitTestMessage1=Newer value' ]) + self.assertEqual(self.flag_values.UnitTestMessage1, 'Newer value') + + # Test that setting the default to None works correctly. + self.flag_values['UnitTestNumber'].SetDefault(None) + self.assertEqual(self.flag_values.UnitTestNumber, None) + self.assertEqual(self.flag_values['UnitTestNumber'].default_as_str, None) + self.flag_values([ 'dummyscript', '--UnitTestNumber=56' ]) + self.assertEqual(self.flag_values.UnitTestNumber, 56) + + # Test that setting the default to zero works correctly. + self.flag_values['UnitTestNumber'].SetDefault(0) + self.assertEqual(self.flag_values.UnitTestNumber, 0) + self.assertEqual(self.flag_values['UnitTestNumber'].default_as_str, "'0'") + self.flag_values([ 'dummyscript', '--UnitTestNumber=56' ]) + self.assertEqual(self.flag_values.UnitTestNumber, 56) + + # Test that setting the default to "" works correctly. + self.flag_values['UnitTestMessage1'].SetDefault("") + self.assertEqual(self.flag_values.UnitTestMessage1, "") + self.assertEqual(self.flag_values['UnitTestMessage1'].default_as_str, "''") + self.flag_values([ 'dummyscript', '--UnitTestMessage1=fifty-six' ]) + self.assertEqual(self.flag_values.UnitTestMessage1, "fifty-six") + + # Test that setting the default to false works correctly. + self.flag_values['UnitTestBoolFlag'].SetDefault(False) + self.assertEqual(self.flag_values.UnitTestBoolFlag, False) + self.assertEqual(self.flag_values['UnitTestBoolFlag'].default_as_str, + "'false'") + self.flag_values([ 'dummyscript', '--UnitTestBoolFlag=true' ]) + self.assertEqual(self.flag_values.UnitTestBoolFlag, True) + + # Test that setting a list default works correctly. + self.flag_values['UnitTestList'].SetDefault('4,5,6') + self.assertEqual(self.flag_values.UnitTestList, ['4', '5', '6']) + self.assertEqual(self.flag_values['UnitTestList'].default_as_str, "'4,5,6'") + self.flag_values([ 'dummyscript', '--UnitTestList=7,8,9' ]) + self.assertEqual(self.flag_values.UnitTestList, ['7', '8', '9']) + + # Test that setting invalid defaults raises exceptions + self.assertRaises(gflags.IllegalFlagValue, + self.flag_values['UnitTestNumber'].SetDefault, 'oops') + self.assertRaises(gflags.IllegalFlagValue, + self.flag_values.SetDefault, 'UnitTestNumber', -1) + + +class FlagsParsingTest(googletest.TestCase): + """Testing different aspects of parsing: '-f' vs '--flag', etc.""" + + def setUp(self): + self.flag_values = gflags.FlagValues() + + def testMethod_ShortestUniquePrefixes(self): + """Test FlagValues.ShortestUniquePrefixes""" + + gflags.DEFINE_string('a', '', '', flag_values=self.flag_values) + gflags.DEFINE_string('abc', '', '', flag_values=self.flag_values) + gflags.DEFINE_string('common_a_string', '', '', flag_values=self.flag_values) + gflags.DEFINE_boolean('common_b_boolean', 0, '', + flag_values=self.flag_values) + gflags.DEFINE_boolean('common_c_boolean', 0, '', + flag_values=self.flag_values) + gflags.DEFINE_boolean('common', 0, '', flag_values=self.flag_values) + gflags.DEFINE_integer('commonly', 0, '', flag_values=self.flag_values) + gflags.DEFINE_boolean('zz', 0, '', flag_values=self.flag_values) + gflags.DEFINE_integer('nozz', 0, '', flag_values=self.flag_values) + + shorter_flags = self.flag_values.ShortestUniquePrefixes( + self.flag_values.FlagDict()) + + expected_results = {'nocommon_b_boolean': 'nocommon_b', + 'common_c_boolean': 'common_c', + 'common_b_boolean': 'common_b', + 'a': 'a', + 'abc': 'ab', + 'zz': 'z', + 'nozz': 'nozz', + 'common_a_string': 'common_a', + 'commonly': 'commonl', + 'nocommon_c_boolean': 'nocommon_c', + 'nocommon': 'nocommon', + 'common': 'common'} + + for name, shorter in expected_results.iteritems(): + self.assertEquals(shorter_flags[name], shorter) + + self.flag_values.__delattr__('a') + self.flag_values.__delattr__('abc') + self.flag_values.__delattr__('common_a_string') + self.flag_values.__delattr__('common_b_boolean') + self.flag_values.__delattr__('common_c_boolean') + self.flag_values.__delattr__('common') + self.flag_values.__delattr__('commonly') + self.flag_values.__delattr__('zz') + self.flag_values.__delattr__('nozz') + + def test_twodasharg_first(self): + gflags.DEFINE_string("twodash_name", "Bob", "namehelp", + flag_values=self.flag_values) + gflags.DEFINE_string("twodash_blame", "Rob", "blamehelp", + flag_values=self.flag_values) + argv = ('./program', + '--', + '--twodash_name=Harry') + argv = self.flag_values(argv) + self.assertEqual('Bob', self.flag_values.twodash_name) + self.assertEqual(argv[1], '--twodash_name=Harry') + + def test_twodasharg_middle(self): + gflags.DEFINE_string("twodash2_name", "Bob", "namehelp", + flag_values=self.flag_values) + gflags.DEFINE_string("twodash2_blame", "Rob", "blamehelp", + flag_values=self.flag_values) + argv = ('./program', + '--twodash2_blame=Larry', + '--', + '--twodash2_name=Harry') + argv = self.flag_values(argv) + self.assertEqual('Bob', self.flag_values.twodash2_name) + self.assertEqual('Larry', self.flag_values.twodash2_blame) + self.assertEqual(argv[1], '--twodash2_name=Harry') + + def test_onedasharg_first(self): + gflags.DEFINE_string("onedash_name", "Bob", "namehelp", + flag_values=self.flag_values) + gflags.DEFINE_string("onedash_blame", "Rob", "blamehelp", + flag_values=self.flag_values) + argv = ('./program', + '-', + '--onedash_name=Harry') + argv = self.flag_values(argv) + self.assertEqual(argv[1], '-') + # TODO(csilvers): we should still parse --onedash_name=Harry as a + # flag, but currently we don't (we stop flag processing as soon as + # we see the first non-flag). + # - This requires gnu_getopt from Python 2.3+ see FLAGS.UseGnuGetOpt() + + def test_unrecognized_flags(self): + gflags.DEFINE_string("name", "Bob", "namehelp", flag_values=self.flag_values) + # Unknown flag --nosuchflag + try: + argv = ('./program', '--nosuchflag', '--name=Bob', 'extra') + self.flag_values(argv) + raise AssertionError("Unknown flag exception not raised") + except gflags.UnrecognizedFlag, e: + assert e.flagname == 'nosuchflag' + assert e.flagvalue == '--nosuchflag' + + # Unknown flag -w (short option) + try: + argv = ('./program', '-w', '--name=Bob', 'extra') + self.flag_values(argv) + raise AssertionError("Unknown flag exception not raised") + except gflags.UnrecognizedFlag, e: + assert e.flagname == 'w' + assert e.flagvalue == '-w' + + # Unknown flag --nosuchflagwithparam=foo + try: + argv = ('./program', '--nosuchflagwithparam=foo', '--name=Bob', 'extra') + self.flag_values(argv) + raise AssertionError("Unknown flag exception not raised") + except gflags.UnrecognizedFlag, e: + assert e.flagname == 'nosuchflagwithparam' + assert e.flagvalue == '--nosuchflagwithparam=foo' + + # Allow unknown flag --nosuchflag if specified with undefok + argv = ('./program', '--nosuchflag', '--name=Bob', + '--undefok=nosuchflag', 'extra') + argv = self.flag_values(argv) + assert len(argv) == 2, "wrong number of arguments pulled" + assert argv[0]=='./program', "program name not preserved" + assert argv[1]=='extra', "extra argument not preserved" + + # Allow unknown flag --noboolflag if undefok=boolflag is specified + argv = ('./program', '--noboolflag', '--name=Bob', + '--undefok=boolflag', 'extra') + argv = self.flag_values(argv) + assert len(argv) == 2, "wrong number of arguments pulled" + assert argv[0]=='./program', "program name not preserved" + assert argv[1]=='extra', "extra argument not preserved" + + # But not if the flagname is misspelled: + try: + argv = ('./program', '--nosuchflag', '--name=Bob', + '--undefok=nosuchfla', 'extra') + self.flag_values(argv) + raise AssertionError("Unknown flag exception not raised") + except gflags.UnrecognizedFlag, e: + assert e.flagname == 'nosuchflag' + + try: + argv = ('./program', '--nosuchflag', '--name=Bob', + '--undefok=nosuchflagg', 'extra') + self.flag_values(argv) + raise AssertionError("Unknown flag exception not raised") + except gflags.UnrecognizedFlag, e: + assert e.flagname == 'nosuchflag' + + # Allow unknown short flag -w if specified with undefok + argv = ('./program', '-w', '--name=Bob', '--undefok=w', 'extra') + argv = self.flag_values(argv) + assert len(argv) == 2, "wrong number of arguments pulled" + assert argv[0]=='./program', "program name not preserved" + assert argv[1]=='extra', "extra argument not preserved" + + # Allow unknown flag --nosuchflagwithparam=foo if specified + # with undefok + argv = ('./program', '--nosuchflagwithparam=foo', '--name=Bob', + '--undefok=nosuchflagwithparam', 'extra') + argv = self.flag_values(argv) + assert len(argv) == 2, "wrong number of arguments pulled" + assert argv[0]=='./program', "program name not preserved" + assert argv[1]=='extra', "extra argument not preserved" + + # Even if undefok specifies multiple flags + argv = ('./program', '--nosuchflag', '-w', '--nosuchflagwithparam=foo', + '--name=Bob', + '--undefok=nosuchflag,w,nosuchflagwithparam', + 'extra') + argv = self.flag_values(argv) + assert len(argv) == 2, "wrong number of arguments pulled" + assert argv[0]=='./program', "program name not preserved" + assert argv[1]=='extra', "extra argument not preserved" + + # However, not if undefok doesn't specify the flag + try: + argv = ('./program', '--nosuchflag', '--name=Bob', + '--undefok=another_such', 'extra') + self.flag_values(argv) + raise AssertionError("Unknown flag exception not raised") + except gflags.UnrecognizedFlag, e: + assert e.flagname == 'nosuchflag' + + # Make sure --undefok doesn't mask other option errors. + try: + # Provide an option requiring a parameter but not giving it one. + argv = ('./program', '--undefok=name', '--name') + self.flag_values(argv) + raise AssertionError("Missing option parameter exception not raised") + except gflags.UnrecognizedFlag: + raise AssertionError("Wrong kind of error exception raised") + except gflags.FlagsError: + pass + + # Test --undefok + argv = ('./program', '--nosuchflag', '-w', '--nosuchflagwithparam=foo', + '--name=Bob', + '--undefok', + 'nosuchflag,w,nosuchflagwithparam', + 'extra') + argv = self.flag_values(argv) + assert len(argv) == 2, "wrong number of arguments pulled" + assert argv[0]=='./program', "program name not preserved" + assert argv[1]=='extra', "extra argument not preserved" + + +class NonGlobalFlagsTest(googletest.TestCase): + + def test_nonglobal_flags(self): + """Test use of non-global FlagValues""" + nonglobal_flags = gflags.FlagValues() + gflags.DEFINE_string("nonglobal_flag", "Bob", "flaghelp", nonglobal_flags) + argv = ('./program', + '--nonglobal_flag=Mary', + 'extra') + argv = nonglobal_flags(argv) + assert len(argv) == 2, "wrong number of arguments pulled" + assert argv[0]=='./program', "program name not preserved" + assert argv[1]=='extra', "extra argument not preserved" + assert nonglobal_flags['nonglobal_flag'].value == 'Mary' + + def test_unrecognized_nonglobal_flags(self): + """Test unrecognized non-global flags""" + nonglobal_flags = gflags.FlagValues() + argv = ('./program', + '--nosuchflag') + try: + argv = nonglobal_flags(argv) + raise AssertionError("Unknown flag exception not raised") + except gflags.UnrecognizedFlag, e: + assert e.flagname == 'nosuchflag' + pass + + argv = ('./program', + '--nosuchflag', + '--undefok=nosuchflag') + + argv = nonglobal_flags(argv) + assert len(argv) == 1, "wrong number of arguments pulled" + assert argv[0]=='./program', "program name not preserved" + + def test_create_flag_errors(self): + # Since the exception classes are exposed, nothing stops users + # from creating their own instances. This test makes sure that + # people modifying the flags module understand that the external + # mechanisms for creating the exceptions should continue to work. + e = gflags.FlagsError() + e = gflags.FlagsError("message") + e = gflags.DuplicateFlag() + e = gflags.DuplicateFlag("message") + e = gflags.IllegalFlagValue() + e = gflags.IllegalFlagValue("message") + e = gflags.UnrecognizedFlag() + e = gflags.UnrecognizedFlag("message") + + def testFlagValuesDelAttr(self): + """Checks that del self.flag_values.flag_id works.""" + default_value = 'default value for testFlagValuesDelAttr' + # 1. Declare and delete a flag with no short name. + flag_values = gflags.FlagValues() + gflags.DEFINE_string('delattr_foo', default_value, 'A simple flag.', + flag_values=flag_values) + self.assertEquals(flag_values.delattr_foo, default_value) + flag_obj = flag_values['delattr_foo'] + # We also check that _FlagIsRegistered works as expected :) + self.assertTrue(flag_values._FlagIsRegistered(flag_obj)) + del flag_values.delattr_foo + self.assertFalse('delattr_foo' in flag_values.FlagDict()) + self.assertFalse(flag_values._FlagIsRegistered(flag_obj)) + # If the previous del FLAGS.delattr_foo did not work properly, the + # next definition will trigger a redefinition error. + gflags.DEFINE_integer('delattr_foo', 3, 'A simple flag.', + flag_values=flag_values) + del flag_values.delattr_foo + + self.assertFalse('delattr_foo' in flag_values.RegisteredFlags()) + + # 2. Declare and delete a flag with a short name. + gflags.DEFINE_string('delattr_bar', default_value, 'flag with short name', + short_name='x5', flag_values=flag_values) + flag_obj = flag_values['delattr_bar'] + self.assertTrue(flag_values._FlagIsRegistered(flag_obj)) + del flag_values.x5 + self.assertTrue(flag_values._FlagIsRegistered(flag_obj)) + del flag_values.delattr_bar + self.assertFalse(flag_values._FlagIsRegistered(flag_obj)) + + # 3. Just like 2, but del flag_values.name last + gflags.DEFINE_string('delattr_bar', default_value, 'flag with short name', + short_name='x5', flag_values=flag_values) + flag_obj = flag_values['delattr_bar'] + self.assertTrue(flag_values._FlagIsRegistered(flag_obj)) + del flag_values.delattr_bar + self.assertTrue(flag_values._FlagIsRegistered(flag_obj)) + del flag_values.x5 + self.assertFalse(flag_values._FlagIsRegistered(flag_obj)) + + self.assertFalse('delattr_bar' in flag_values.RegisteredFlags()) + self.assertFalse('x5' in flag_values.RegisteredFlags()) + + +class KeyFlagsTest(googletest.TestCase): + + def setUp(self): + self.flag_values = gflags.FlagValues() + + def _GetNamesOfDefinedFlags(self, module, flag_values): + """Returns the list of names of flags defined by a module. + + Auxiliary for the testKeyFlags* methods. + + Args: + module: A module object or a string module name. + flag_values: A FlagValues object. + + Returns: + A list of strings. + """ + return [f.name for f in flag_values._GetFlagsDefinedByModule(module)] + + def _GetNamesOfKeyFlags(self, module, flag_values): + """Returns the list of names of key flags for a module. + + Auxiliary for the testKeyFlags* methods. + + Args: + module: A module object or a string module name. + flag_values: A FlagValues object. + + Returns: + A list of strings. + """ + return [f.name for f in flag_values._GetKeyFlagsForModule(module)] + + def _AssertListsHaveSameElements(self, list_1, list_2): + # Checks that two lists have the same elements with the same + # multiplicity, in possibly different order. + list_1 = list(list_1) + list_1.sort() + list_2 = list(list_2) + list_2.sort() + self.assertListEqual(list_1, list_2) + + def testKeyFlags(self): + # Before starting any testing, make sure no flags are already + # defined for module_foo and module_bar. + self.assertListEqual(self._GetNamesOfKeyFlags(module_foo, self.flag_values), + []) + self.assertListEqual(self._GetNamesOfKeyFlags(module_bar, self.flag_values), + []) + self.assertListEqual(self._GetNamesOfDefinedFlags(module_foo, + self.flag_values), + []) + self.assertListEqual(self._GetNamesOfDefinedFlags(module_bar, + self.flag_values), + []) + + # Defines a few flags in module_foo and module_bar. + module_foo.DefineFlags(flag_values=self.flag_values) + + try: + # Part 1. Check that all flags defined by module_foo are key for + # that module, and similarly for module_bar. + for module in [module_foo, module_bar]: + self._AssertListsHaveSameElements( + self.flag_values._GetFlagsDefinedByModule(module), + self.flag_values._GetKeyFlagsForModule(module)) + # Also check that each module defined the expected flags. + self._AssertListsHaveSameElements( + self._GetNamesOfDefinedFlags(module, self.flag_values), + module.NamesOfDefinedFlags()) + + # Part 2. Check that gflags.DECLARE_key_flag works fine. + # Declare that some flags from module_bar are key for + # module_foo. + module_foo.DeclareKeyFlags(flag_values=self.flag_values) + + # Check that module_foo has the expected list of defined flags. + self._AssertListsHaveSameElements( + self._GetNamesOfDefinedFlags(module_foo, self.flag_values), + module_foo.NamesOfDefinedFlags()) + + # Check that module_foo has the expected list of key flags. + self._AssertListsHaveSameElements( + self._GetNamesOfKeyFlags(module_foo, self.flag_values), + module_foo.NamesOfDeclaredKeyFlags()) + + # Part 3. Check that gflags.ADOPT_module_key_flags works fine. + # Trigger a call to gflags.ADOPT_module_key_flags(module_bar) + # inside module_foo. This should declare a few more key + # flags in module_foo. + module_foo.DeclareExtraKeyFlags(flag_values=self.flag_values) + + # Check that module_foo has the expected list of key flags. + self._AssertListsHaveSameElements( + self._GetNamesOfKeyFlags(module_foo, self.flag_values), + module_foo.NamesOfDeclaredKeyFlags() + + module_foo.NamesOfDeclaredExtraKeyFlags()) + finally: + module_foo.RemoveFlags(flag_values=self.flag_values) + + def testKeyFlagsWithNonDefaultFlagValuesObject(self): + # Check that key flags work even when we use a FlagValues object + # that is not the default gflags.self.flag_values object. Otherwise, this + # test is similar to testKeyFlags, but it uses only module_bar. + # The other test module (module_foo) uses only the default values + # for the flag_values keyword arguments. This way, testKeyFlags + # and this method test both the default FlagValues, the explicitly + # specified one, and a mixed usage of the two. + + # A brand-new FlagValues object, to use instead of gflags.self.flag_values. + fv = gflags.FlagValues() + + # Before starting any testing, make sure no flags are already + # defined for module_foo and module_bar. + self.assertListEqual( + self._GetNamesOfKeyFlags(module_bar, fv), + []) + self.assertListEqual( + self._GetNamesOfDefinedFlags(module_bar, fv), + []) + + module_bar.DefineFlags(flag_values=fv) + + # Check that all flags defined by module_bar are key for that + # module, and that module_bar defined the expected flags. + self._AssertListsHaveSameElements( + fv._GetFlagsDefinedByModule(module_bar), + fv._GetKeyFlagsForModule(module_bar)) + self._AssertListsHaveSameElements( + self._GetNamesOfDefinedFlags(module_bar, fv), + module_bar.NamesOfDefinedFlags()) + + # Pick two flags from module_bar, declare them as key for the + # current (i.e., main) module (via gflags.DECLARE_key_flag), and + # check that we get the expected effect. The important thing is + # that we always use flags_values=fv (instead of the default + # self.flag_values). + main_module = gflags._GetMainModule() + names_of_flags_defined_by_bar = module_bar.NamesOfDefinedFlags() + flag_name_0 = names_of_flags_defined_by_bar[0] + flag_name_2 = names_of_flags_defined_by_bar[2] + + gflags.DECLARE_key_flag(flag_name_0, flag_values=fv) + self._AssertListsHaveSameElements( + self._GetNamesOfKeyFlags(main_module, fv), + [flag_name_0]) + + gflags.DECLARE_key_flag(flag_name_2, flag_values=fv) + self._AssertListsHaveSameElements( + self._GetNamesOfKeyFlags(main_module, fv), + [flag_name_0, flag_name_2]) + + # Try with a special (not user-defined) flag too: + gflags.DECLARE_key_flag('undefok', flag_values=fv) + self._AssertListsHaveSameElements( + self._GetNamesOfKeyFlags(main_module, fv), + [flag_name_0, flag_name_2, 'undefok']) + + gflags.ADOPT_module_key_flags(module_bar, fv) + self._AssertListsHaveSameElements( + self._GetNamesOfKeyFlags(main_module, fv), + names_of_flags_defined_by_bar + ['undefok']) + + # Adopt key flags from the flags module itself. + gflags.ADOPT_module_key_flags(gflags, flag_values=fv) + self._AssertListsHaveSameElements( + self._GetNamesOfKeyFlags(main_module, fv), + names_of_flags_defined_by_bar + ['flagfile', 'undefok']) + + def testMainModuleHelpWithKeyFlags(self): + # Similar to test_main_module_help, but this time we make sure to + # declare some key flags. + + # Safety check that the main module does not declare any flags + # at the beginning of this test. + expected_help = '' + self.assertMultiLineEqual(expected_help, self.flag_values.MainModuleHelp()) + + # Define one flag in this main module and some flags in modules + # a and b. Also declare one flag from module a and one flag + # from module b as key flags for the main module. + gflags.DEFINE_integer('main_module_int_fg', 1, + 'Integer flag in the main module.', + flag_values=self.flag_values) + + try: + main_module_int_fg_help = ( + " --main_module_int_fg: Integer flag in the main module.\n" + " (default: '1')\n" + " (an integer)") + + expected_help += "\n%s:\n%s" % (sys.argv[0], main_module_int_fg_help) + self.assertMultiLineEqual(expected_help, + self.flag_values.MainModuleHelp()) + + # The following call should be a no-op: any flag declared by a + # module is automatically key for that module. + gflags.DECLARE_key_flag('main_module_int_fg', flag_values=self.flag_values) + self.assertMultiLineEqual(expected_help, + self.flag_values.MainModuleHelp()) + + # The definition of a few flags in an imported module should not + # change the main module help. + module_foo.DefineFlags(flag_values=self.flag_values) + self.assertMultiLineEqual(expected_help, + self.flag_values.MainModuleHelp()) + + gflags.DECLARE_key_flag('tmod_foo_bool', flag_values=self.flag_values) + tmod_foo_bool_help = ( + " --[no]tmod_foo_bool: Boolean flag from module foo.\n" + " (default: 'true')") + expected_help += "\n" + tmod_foo_bool_help + self.assertMultiLineEqual(expected_help, + self.flag_values.MainModuleHelp()) + + gflags.DECLARE_key_flag('tmod_bar_z', flag_values=self.flag_values) + tmod_bar_z_help = ( + " --[no]tmod_bar_z: Another boolean flag from module bar.\n" + " (default: 'false')") + # Unfortunately, there is some flag sorting inside + # MainModuleHelp, so we can't keep incrementally extending + # the expected_help string ... + expected_help = ("\n%s:\n%s\n%s\n%s" % + (sys.argv[0], + main_module_int_fg_help, + tmod_bar_z_help, + tmod_foo_bool_help)) + self.assertMultiLineEqual(self.flag_values.MainModuleHelp(), + expected_help) + + finally: + # At the end, delete all the flag information we created. + self.flag_values.__delattr__('main_module_int_fg') + module_foo.RemoveFlags(flag_values=self.flag_values) + + def test_ADOPT_module_key_flags(self): + # Check that ADOPT_module_key_flags raises an exception when + # called with a module name (as opposed to a module object). + self.assertRaises(gflags.FlagsError, + gflags.ADOPT_module_key_flags, + 'pyglib.app') + + +class GetCallingModuleTest(googletest.TestCase): + """Test whether we correctly determine the module which defines the flag.""" + + def test_GetCallingModule(self): + self.assertEqual(gflags._GetCallingModule(), sys.argv[0]) + self.assertEqual( + module_foo.GetModuleName(), + 'flags_modules_for_testing.module_foo') + self.assertEqual( + module_bar.GetModuleName(), + 'flags_modules_for_testing.module_bar') + + # We execute the following exec statements for their side-effect + # (i.e., not raising an error). They emphasize the case that not + # all code resides in one of the imported modules: Python is a + # really dynamic language, where we can dynamically construct some + # code and execute it. + code = ("import gflags\n" + "module_name = gflags._GetCallingModule()") + exec(code) + + # Next two exec statements executes code with a global environment + # that is different from the global environment of any imported + # module. + exec(code, {}) + # vars(self) returns a dictionary corresponding to the symbol + # table of the self object. dict(...) makes a distinct copy of + # this dictionary, such that any new symbol definition by the + # exec-ed code (e.g., import flags, module_name = ...) does not + # affect the symbol table of self. + exec(code, dict(vars(self))) + + # Next test is actually more involved: it checks not only that + # _GetCallingModule does not crash inside exec code, it also checks + # that it returns the expected value: the code executed via exec + # code is treated as being executed by the current module. We + # check it twice: first time by executing exec from the main + # module, second time by executing it from module_bar. + global_dict = {} + exec(code, global_dict) + self.assertEqual(global_dict['module_name'], + sys.argv[0]) + + global_dict = {} + module_bar.ExecuteCode(code, global_dict) + self.assertEqual( + global_dict['module_name'], + 'flags_modules_for_testing.module_bar') + + def test_GetCallingModuleWithIteritemsError(self): + # This test checks that _GetCallingModule is using + # sys.modules.items(), instead of .iteritems(). + orig_sys_modules = sys.modules + + # Mock sys.modules: simulates error produced by importing a module + # in paralel with our iteration over sys.modules.iteritems(). + class SysModulesMock(dict): + def __init__(self, original_content): + dict.__init__(self, original_content) + + def iteritems(self): + # Any dictionary method is fine, but not .iteritems(). + raise RuntimeError('dictionary changed size during iteration') + + sys.modules = SysModulesMock(orig_sys_modules) + try: + # _GetCallingModule should still work as expected: + self.assertEqual(gflags._GetCallingModule(), sys.argv[0]) + self.assertEqual( + module_foo.GetModuleName(), + 'flags_modules_for_testing.module_foo') + finally: + sys.modules = orig_sys_modules + + +class FindModuleTest(googletest.TestCase): + """Testing methods that find a module that defines a given flag.""" + + def testFindModuleDefiningFlag(self): + self.assertEqual('default', FLAGS.FindModuleDefiningFlag( + '__NON_EXISTENT_FLAG__', 'default')) + self.assertEqual( + module_baz.__name__, FLAGS.FindModuleDefiningFlag('tmod_baz_x')) + + def testFindModuleIdDefiningFlag(self): + self.assertEqual('default', FLAGS.FindModuleIdDefiningFlag( + '__NON_EXISTENT_FLAG__', 'default')) + self.assertEqual( + id(module_baz), FLAGS.FindModuleIdDefiningFlag('tmod_baz_x')) + + +class FlagsErrorMessagesTest(googletest.TestCase): + """Testing special cases for integer and float flags error messages.""" + + def setUp(self): + # make sure we are using the old, stupid way of parsing flags. + self.flag_values = gflags.FlagValues() + self.flag_values.UseGnuGetOpt(False) + + def testIntegerErrorText(self): + # Make sure we get proper error text + gflags.DEFINE_integer('positive', 4, 'non-negative flag', lower_bound=1, + flag_values=self.flag_values) + gflags.DEFINE_integer('non_negative', 4, 'positive flag', lower_bound=0, + flag_values=self.flag_values) + gflags.DEFINE_integer('negative', -4, 'negative flag', upper_bound=-1, + flag_values=self.flag_values) + gflags.DEFINE_integer('non_positive', -4, 'non-positive flag', upper_bound=0, + flag_values=self.flag_values) + gflags.DEFINE_integer('greater', 19, 'greater-than flag', lower_bound=4, + flag_values=self.flag_values) + gflags.DEFINE_integer('smaller', -19, 'smaller-than flag', upper_bound=4, + flag_values=self.flag_values) + gflags.DEFINE_integer('usual', 4, 'usual flag', lower_bound=0, + upper_bound=10000, flag_values=self.flag_values) + gflags.DEFINE_integer('another_usual', 0, 'usual flag', lower_bound=-1, + upper_bound=1, flag_values=self.flag_values) + + self._CheckErrorMessage('positive', -4, 'a positive integer') + self._CheckErrorMessage('non_negative', -4, 'a non-negative integer') + self._CheckErrorMessage('negative', 0, 'a negative integer') + self._CheckErrorMessage('non_positive', 4, 'a non-positive integer') + self._CheckErrorMessage('usual', -4, 'an integer in the range [0, 10000]') + self._CheckErrorMessage('another_usual', 4, + 'an integer in the range [-1, 1]') + self._CheckErrorMessage('greater', -5, 'integer >= 4') + self._CheckErrorMessage('smaller', 5, 'integer <= 4') + + def testFloatErrorText(self): + gflags.DEFINE_float('positive', 4, 'non-negative flag', lower_bound=1, + flag_values=self.flag_values) + gflags.DEFINE_float('non_negative', 4, 'positive flag', lower_bound=0, + flag_values=self.flag_values) + gflags.DEFINE_float('negative', -4, 'negative flag', upper_bound=-1, + flag_values=self.flag_values) + gflags.DEFINE_float('non_positive', -4, 'non-positive flag', upper_bound=0, + flag_values=self.flag_values) + gflags.DEFINE_float('greater', 19, 'greater-than flag', lower_bound=4, + flag_values=self.flag_values) + gflags.DEFINE_float('smaller', -19, 'smaller-than flag', upper_bound=4, + flag_values=self.flag_values) + gflags.DEFINE_float('usual', 4, 'usual flag', lower_bound=0, + upper_bound=10000, flag_values=self.flag_values) + gflags.DEFINE_float('another_usual', 0, 'usual flag', lower_bound=-1, + upper_bound=1, flag_values=self.flag_values) + + self._CheckErrorMessage('positive', 0.5, 'number >= 1') + self._CheckErrorMessage('non_negative', -4.0, 'a non-negative number') + self._CheckErrorMessage('negative', 0.5, 'number <= -1') + self._CheckErrorMessage('non_positive', 4.0, 'a non-positive number') + self._CheckErrorMessage('usual', -4.0, 'a number in the range [0, 10000]') + self._CheckErrorMessage('another_usual', 4.0, + 'a number in the range [-1, 1]') + self._CheckErrorMessage('smaller', 5.0, 'number <= 4') + + def _CheckErrorMessage(self, flag_name, flag_value, expected_message_suffix): + """Set a flag to a given value and make sure we get expected message.""" + + try: + self.flag_values.__setattr__(flag_name, flag_value) + raise AssertionError('Bounds exception not raised!') + except gflags.IllegalFlagValue, e: + expected = ('flag --%(name)s=%(value)s: %(value)s is not %(suffix)s' % + {'name': flag_name, 'value': flag_value, + 'suffix': expected_message_suffix}) + self.assertEquals(str(e), expected) + + +def main(): + googletest.main() + + +if __name__ == '__main__': + main() diff --git a/third_party/gjslint/python-gflags-2.0/tests/gflags_validators_test.py b/third_party/gjslint/python-gflags-2.0/tests/gflags_validators_test.py new file mode 100755 index 0000000000..460e6d01d9 --- /dev/null +++ b/third_party/gjslint/python-gflags-2.0/tests/gflags_validators_test.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python + +# Copyright (c) 2010, Google Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Testing that flags validators framework does work. + +This file tests that each flag validator called when it should be, and that +failed validator will throw an exception, etc. +""" + +__author__ = 'olexiy@google.com (Olexiy Oryeshko)' + +import gflags_googletest as googletest +import gflags +import gflags_validators + + +class SimpleValidatorTest(googletest.TestCase): + """Testing gflags.RegisterValidator() method.""" + + def setUp(self): + super(SimpleValidatorTest, self).setUp() + self.flag_values = gflags.FlagValues() + self.call_args = [] + + def testSuccess(self): + def Checker(x): + self.call_args.append(x) + return True + gflags.DEFINE_integer('test_flag', None, 'Usual integer flag', + flag_values=self.flag_values) + gflags.RegisterValidator('test_flag', + Checker, + message='Errors happen', + flag_values=self.flag_values) + + argv = ('./program') + self.flag_values(argv) + self.assertEquals(None, self.flag_values.test_flag) + self.flag_values.test_flag = 2 + self.assertEquals(2, self.flag_values.test_flag) + self.assertEquals([None, 2], self.call_args) + + def testDefaultValueNotUsedSuccess(self): + def Checker(x): + self.call_args.append(x) + return True + gflags.DEFINE_integer('test_flag', None, 'Usual integer flag', + flag_values=self.flag_values) + gflags.RegisterValidator('test_flag', + Checker, + message='Errors happen', + flag_values=self.flag_values) + + argv = ('./program', '--test_flag=1') + self.flag_values(argv) + self.assertEquals(1, self.flag_values.test_flag) + self.assertEquals([1], self.call_args) + + def testValidatorNotCalledWhenOtherFlagIsChanged(self): + def Checker(x): + self.call_args.append(x) + return True + gflags.DEFINE_integer('test_flag', 1, 'Usual integer flag', + flag_values=self.flag_values) + gflags.DEFINE_integer('other_flag', 2, 'Other integer flag', + flag_values=self.flag_values) + gflags.RegisterValidator('test_flag', + Checker, + message='Errors happen', + flag_values=self.flag_values) + + argv = ('./program') + self.flag_values(argv) + self.assertEquals(1, self.flag_values.test_flag) + self.flag_values.other_flag = 3 + self.assertEquals([1], self.call_args) + + def testExceptionRaisedIfCheckerFails(self): + def Checker(x): + self.call_args.append(x) + return x == 1 + gflags.DEFINE_integer('test_flag', None, 'Usual integer flag', + flag_values=self.flag_values) + gflags.RegisterValidator('test_flag', + Checker, + message='Errors happen', + flag_values=self.flag_values) + + argv = ('./program', '--test_flag=1') + self.flag_values(argv) + try: + self.flag_values.test_flag = 2 + raise AssertionError('gflags.IllegalFlagValue expected') + except gflags.IllegalFlagValue, e: + self.assertEquals('flag --test_flag=2: Errors happen', str(e)) + self.assertEquals([1, 2], self.call_args) + + def testExceptionRaisedIfCheckerRaisesException(self): + def Checker(x): + self.call_args.append(x) + if x == 1: + return True + raise gflags_validators.Error('Specific message') + gflags.DEFINE_integer('test_flag', None, 'Usual integer flag', + flag_values=self.flag_values) + gflags.RegisterValidator('test_flag', + Checker, + message='Errors happen', + flag_values=self.flag_values) + + argv = ('./program', '--test_flag=1') + self.flag_values(argv) + try: + self.flag_values.test_flag = 2 + raise AssertionError('gflags.IllegalFlagValue expected') + except gflags.IllegalFlagValue, e: + self.assertEquals('flag --test_flag=2: Specific message', str(e)) + self.assertEquals([1, 2], self.call_args) + + def testErrorMessageWhenCheckerReturnsFalseOnStart(self): + def Checker(x): + self.call_args.append(x) + return False + gflags.DEFINE_integer('test_flag', None, 'Usual integer flag', + flag_values=self.flag_values) + gflags.RegisterValidator('test_flag', + Checker, + message='Errors happen', + flag_values=self.flag_values) + + argv = ('./program', '--test_flag=1') + try: + self.flag_values(argv) + raise AssertionError('gflags.IllegalFlagValue expected') + except gflags.IllegalFlagValue, e: + self.assertEquals('flag --test_flag=1: Errors happen', str(e)) + self.assertEquals([1], self.call_args) + + def testErrorMessageWhenCheckerRaisesExceptionOnStart(self): + def Checker(x): + self.call_args.append(x) + raise gflags_validators.Error('Specific message') + gflags.DEFINE_integer('test_flag', None, 'Usual integer flag', + flag_values=self.flag_values) + gflags.RegisterValidator('test_flag', + Checker, + message='Errors happen', + flag_values=self.flag_values) + + argv = ('./program', '--test_flag=1') + try: + self.flag_values(argv) + raise AssertionError('IllegalFlagValue expected') + except gflags.IllegalFlagValue, e: + self.assertEquals('flag --test_flag=1: Specific message', str(e)) + self.assertEquals([1], self.call_args) + + def testValidatorsCheckedInOrder(self): + + def Required(x): + self.calls.append('Required') + return x is not None + + def Even(x): + self.calls.append('Even') + return x % 2 == 0 + + self.calls = [] + self._DefineFlagAndValidators(Required, Even) + self.assertEquals(['Required', 'Even'], self.calls) + + self.calls = [] + self._DefineFlagAndValidators(Even, Required) + self.assertEquals(['Even', 'Required'], self.calls) + + def _DefineFlagAndValidators(self, first_validator, second_validator): + local_flags = gflags.FlagValues() + gflags.DEFINE_integer('test_flag', 2, 'test flag', flag_values=local_flags) + gflags.RegisterValidator('test_flag', + first_validator, + message='', + flag_values=local_flags) + gflags.RegisterValidator('test_flag', + second_validator, + message='', + flag_values=local_flags) + argv = ('./program') + local_flags(argv) + + +if __name__ == '__main__': + googletest.main() diff --git a/third_party/jasmine/LICENSE.txt b/third_party/jasmine/LICENSE.txt new file mode 100644 index 0000000000..aff8ed47a1 --- /dev/null +++ b/third_party/jasmine/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2008-2014 Pivotal Labs + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/third_party/jasmine/lib/jasmine-2.1.3/boot.js b/third_party/jasmine/lib/jasmine-2.1.3/boot.js new file mode 100644 index 0000000000..164f068b09 --- /dev/null +++ b/third_party/jasmine/lib/jasmine-2.1.3/boot.js @@ -0,0 +1,120 @@ +/** + Starting with version 2.0, this file "boots" Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's specs. This file should be loaded after `jasmine.js` and `jasmine_html.js`, but before any project source files or spec files are loaded. Thus this file can also be used to customize Jasmine for a project. + + If a project is using Jasmine via the standalone distribution, this file can be customized directly. If a project is using Jasmine via the [Ruby gem][jasmine-gem], this file can be copied into the support directory via `jasmine copy_boot_js`. Other environments (e.g., Python) will have different mechanisms. + + The location of `boot.js` can be specified and/or overridden in `jasmine.yml`. + + [jasmine-gem]: http://github.com/pivotal/jasmine-gem + */ + +(function() { + + /** + * ## Require & Instantiate + * + * Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference. + */ + window.jasmine = jasmineRequire.core(jasmineRequire); + + /** + * Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference. + */ + jasmineRequire.html(jasmine); + + /** + * Create the Jasmine environment. This is used to run all specs in a project. + */ + var env = jasmine.getEnv(); + + /** + * ## The Global Interface + * + * Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged. + */ + var jasmineInterface = jasmineRequire.interface(jasmine, env); + + /** + * Add all of the Jasmine global/public interface to the proper global, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`. + */ + if (typeof window == "undefined" && typeof exports == "object") { + extend(exports, jasmineInterface); + } else { + extend(window, jasmineInterface); + } + + /** + * ## Runner Parameters + * + * More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface. + */ + + var queryString = new jasmine.QueryString({ + getWindowLocation: function() { return window.location; } + }); + + var catchingExceptions = queryString.getParam("catch"); + env.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions); + + /** + * ## Reporters + * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any). + */ + var htmlReporter = new jasmine.HtmlReporter({ + env: env, + onRaiseExceptionsClick: function() { queryString.setParam("catch", !env.catchingExceptions()); }, + getContainer: function() { return document.body; }, + createElement: function() { return document.createElement.apply(document, arguments); }, + createTextNode: function() { return document.createTextNode.apply(document, arguments); }, + timer: new jasmine.Timer() + }); + + /** + * The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript. + */ + env.addReporter(jasmineInterface.jsApiReporter); + env.addReporter(htmlReporter); + + /** + * Filter which specs will be run by matching the start of the full name against the `spec` query param. + */ + var specFilter = new jasmine.HtmlSpecFilter({ + filterString: function() { return queryString.getParam("spec"); } + }); + + env.specFilter = function(spec) { + return specFilter.matches(spec.getFullName()); + }; + + /** + * Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack. + */ + window.setTimeout = window.setTimeout; + window.setInterval = window.setInterval; + window.clearTimeout = window.clearTimeout; + window.clearInterval = window.clearInterval; + + /** + * ## Execution + * + * Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded. + */ + var currentWindowOnload = window.onload; + + window.onload = function() { + if (currentWindowOnload) { + currentWindowOnload(); + } + htmlReporter.initialize(); + env.execute(); + }; + + /** + * Helper function for readability above. + */ + function extend(destination, source) { + for (var property in source) destination[property] = source[property]; + return destination; + } + +}()); diff --git a/third_party/jasmine/lib/jasmine-2.1.3/console.js b/third_party/jasmine/lib/jasmine-2.1.3/console.js new file mode 100644 index 0000000000..a65876e911 --- /dev/null +++ b/third_party/jasmine/lib/jasmine-2.1.3/console.js @@ -0,0 +1,190 @@ +/* +Copyright (c) 2008-2014 Pivotal Labs + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +function getJasmineRequireObj() { + if (typeof module !== 'undefined' && module.exports) { + return exports; + } else { + window.jasmineRequire = window.jasmineRequire || {}; + return window.jasmineRequire; + } +} + +getJasmineRequireObj().console = function(jRequire, j$) { + j$.ConsoleReporter = jRequire.ConsoleReporter(); +}; + +getJasmineRequireObj().ConsoleReporter = function() { + + var noopTimer = { + start: function(){}, + elapsed: function(){ return 0; } + }; + + function ConsoleReporter(options) { + var print = options.print, + showColors = options.showColors || false, + onComplete = options.onComplete || function() {}, + timer = options.timer || noopTimer, + specCount, + failureCount, + failedSpecs = [], + pendingCount, + ansi = { + green: '\x1B[32m', + red: '\x1B[31m', + yellow: '\x1B[33m', + none: '\x1B[0m' + }, + failedSuites = []; + + print('ConsoleReporter is deprecated and will be removed in a future version.'); + + this.jasmineStarted = function() { + specCount = 0; + failureCount = 0; + pendingCount = 0; + print('Started'); + printNewline(); + timer.start(); + }; + + this.jasmineDone = function() { + printNewline(); + for (var i = 0; i < failedSpecs.length; i++) { + specFailureDetails(failedSpecs[i]); + } + + if(specCount > 0) { + printNewline(); + + var specCounts = specCount + ' ' + plural('spec', specCount) + ', ' + + failureCount + ' ' + plural('failure', failureCount); + + if (pendingCount) { + specCounts += ', ' + pendingCount + ' pending ' + plural('spec', pendingCount); + } + + print(specCounts); + } else { + print('No specs found'); + } + + printNewline(); + var seconds = timer.elapsed() / 1000; + print('Finished in ' + seconds + ' ' + plural('second', seconds)); + printNewline(); + + for(i = 0; i < failedSuites.length; i++) { + suiteFailureDetails(failedSuites[i]); + } + + onComplete(failureCount === 0); + }; + + this.specDone = function(result) { + specCount++; + + if (result.status == 'pending') { + pendingCount++; + print(colored('yellow', '*')); + return; + } + + if (result.status == 'passed') { + print(colored('green', '.')); + return; + } + + if (result.status == 'failed') { + failureCount++; + failedSpecs.push(result); + print(colored('red', 'F')); + } + }; + + this.suiteDone = function(result) { + if (result.failedExpectations && result.failedExpectations.length > 0) { + failureCount++; + failedSuites.push(result); + } + }; + + return this; + + function printNewline() { + print('\n'); + } + + function colored(color, str) { + return showColors ? (ansi[color] + str + ansi.none) : str; + } + + function plural(str, count) { + return count == 1 ? str : str + 's'; + } + + function repeat(thing, times) { + var arr = []; + for (var i = 0; i < times; i++) { + arr.push(thing); + } + return arr; + } + + function indent(str, spaces) { + var lines = (str || '').split('\n'); + var newArr = []; + for (var i = 0; i < lines.length; i++) { + newArr.push(repeat(' ', spaces).join('') + lines[i]); + } + return newArr.join('\n'); + } + + function specFailureDetails(result) { + printNewline(); + print(result.fullName); + + for (var i = 0; i < result.failedExpectations.length; i++) { + var failedExpectation = result.failedExpectations[i]; + printNewline(); + print(indent(failedExpectation.message, 2)); + print(indent(failedExpectation.stack, 2)); + } + + printNewline(); + } + + function suiteFailureDetails(result) { + for (var i = 0; i < result.failedExpectations.length; i++) { + printNewline(); + print(colored('red', 'An error was thrown in an afterAll')); + printNewline(); + print(colored('red', 'AfterAll ' + result.failedExpectations[i].message)); + + } + printNewline(); + } + } + + return ConsoleReporter; +}; diff --git a/third_party/jasmine/lib/jasmine-2.1.3/jasmine-html.js b/third_party/jasmine/lib/jasmine-2.1.3/jasmine-html.js new file mode 100644 index 0000000000..898108b77d --- /dev/null +++ b/third_party/jasmine/lib/jasmine-2.1.3/jasmine-html.js @@ -0,0 +1,404 @@ +/* +Copyright (c) 2008-2014 Pivotal Labs + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +jasmineRequire.html = function(j$) { + j$.ResultsNode = jasmineRequire.ResultsNode(); + j$.HtmlReporter = jasmineRequire.HtmlReporter(j$); + j$.QueryString = jasmineRequire.QueryString(); + j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter(); +}; + +jasmineRequire.HtmlReporter = function(j$) { + + var noopTimer = { + start: function() {}, + elapsed: function() { return 0; } + }; + + function HtmlReporter(options) { + var env = options.env || {}, + getContainer = options.getContainer, + createElement = options.createElement, + createTextNode = options.createTextNode, + onRaiseExceptionsClick = options.onRaiseExceptionsClick || function() {}, + timer = options.timer || noopTimer, + results = [], + specsExecuted = 0, + failureCount = 0, + pendingSpecCount = 0, + htmlReporterMain, + symbols, + failedSuites = []; + + this.initialize = function() { + clearPrior(); + htmlReporterMain = createDom('div', {className: 'jasmine_html-reporter'}, + createDom('div', {className: 'banner'}, + createDom('a', {className: 'title', href: 'http://jasmine.github.io/', target: '_blank'}), + createDom('span', {className: 'version'}, j$.version) + ), + createDom('ul', {className: 'symbol-summary'}), + createDom('div', {className: 'alert'}), + createDom('div', {className: 'results'}, + createDom('div', {className: 'failures'}) + ) + ); + getContainer().appendChild(htmlReporterMain); + + symbols = find('.symbol-summary'); + }; + + var totalSpecsDefined; + this.jasmineStarted = function(options) { + totalSpecsDefined = options.totalSpecsDefined || 0; + timer.start(); + }; + + var summary = createDom('div', {className: 'summary'}); + + var topResults = new j$.ResultsNode({}, '', null), + currentParent = topResults; + + this.suiteStarted = function(result) { + currentParent.addChild(result, 'suite'); + currentParent = currentParent.last(); + }; + + this.suiteDone = function(result) { + if (result.status == 'failed') { + failedSuites.push(result); + } + + if (currentParent == topResults) { + return; + } + + currentParent = currentParent.parent; + }; + + this.specStarted = function(result) { + currentParent.addChild(result, 'spec'); + }; + + var failures = []; + this.specDone = function(result) { + if(noExpectations(result) && typeof console !== 'undefined' && typeof console.error !== 'undefined') { + console.error('Spec \'' + result.fullName + '\' has no expectations.'); + } + + if (result.status != 'disabled') { + specsExecuted++; + } + + symbols.appendChild(createDom('li', { + className: noExpectations(result) ? 'empty' : result.status, + id: 'spec_' + result.id, + title: result.fullName + } + )); + + if (result.status == 'failed') { + failureCount++; + + var failure = + createDom('div', {className: 'spec-detail failed'}, + createDom('div', {className: 'description'}, + createDom('a', {title: result.fullName, href: specHref(result)}, result.fullName) + ), + createDom('div', {className: 'messages'}) + ); + var messages = failure.childNodes[1]; + + for (var i = 0; i < result.failedExpectations.length; i++) { + var expectation = result.failedExpectations[i]; + messages.appendChild(createDom('div', {className: 'result-message'}, expectation.message)); + messages.appendChild(createDom('div', {className: 'stack-trace'}, expectation.stack)); + } + + failures.push(failure); + } + + if (result.status == 'pending') { + pendingSpecCount++; + } + }; + + this.jasmineDone = function() { + var banner = find('.banner'); + banner.appendChild(createDom('span', {className: 'duration'}, 'finished in ' + timer.elapsed() / 1000 + 's')); + + var alert = find('.alert'); + + alert.appendChild(createDom('span', { className: 'exceptions' }, + createDom('label', { className: 'label', 'for': 'raise-exceptions' }, 'raise exceptions'), + createDom('input', { + className: 'raise', + id: 'raise-exceptions', + type: 'checkbox' + }) + )); + var checkbox = find('#raise-exceptions'); + + checkbox.checked = !env.catchingExceptions(); + checkbox.onclick = onRaiseExceptionsClick; + + if (specsExecuted < totalSpecsDefined) { + var skippedMessage = 'Ran ' + specsExecuted + ' of ' + totalSpecsDefined + ' specs - run all'; + alert.appendChild( + createDom('span', {className: 'bar skipped'}, + createDom('a', {href: '?', title: 'Run all specs'}, skippedMessage) + ) + ); + } + var statusBarMessage = ''; + var statusBarClassName = 'bar '; + + if (totalSpecsDefined > 0) { + statusBarMessage += pluralize('spec', specsExecuted) + ', ' + pluralize('failure', failureCount); + if (pendingSpecCount) { statusBarMessage += ', ' + pluralize('pending spec', pendingSpecCount); } + statusBarClassName += (failureCount > 0) ? 'failed' : 'passed'; + } else { + statusBarClassName += 'skipped'; + statusBarMessage += 'No specs found'; + } + + alert.appendChild(createDom('span', {className: statusBarClassName}, statusBarMessage)); + + for(i = 0; i < failedSuites.length; i++) { + var failedSuite = failedSuites[i]; + for(var j = 0; j < failedSuite.failedExpectations.length; j++) { + var errorBarMessage = 'AfterAll ' + failedSuite.failedExpectations[j].message; + var errorBarClassName = 'bar errored'; + alert.appendChild(createDom('span', {className: errorBarClassName}, errorBarMessage)); + } + } + + var results = find('.results'); + results.appendChild(summary); + + summaryList(topResults, summary); + + function summaryList(resultsTree, domParent) { + var specListNode; + for (var i = 0; i < resultsTree.children.length; i++) { + var resultNode = resultsTree.children[i]; + if (resultNode.type == 'suite') { + var suiteListNode = createDom('ul', {className: 'suite', id: 'suite-' + resultNode.result.id}, + createDom('li', {className: 'suite-detail'}, + createDom('a', {href: specHref(resultNode.result)}, resultNode.result.description) + ) + ); + + summaryList(resultNode, suiteListNode); + domParent.appendChild(suiteListNode); + } + if (resultNode.type == 'spec') { + if (domParent.getAttribute('class') != 'specs') { + specListNode = createDom('ul', {className: 'specs'}); + domParent.appendChild(specListNode); + } + var specDescription = resultNode.result.description; + if(noExpectations(resultNode.result)) { + specDescription = 'SPEC HAS NO EXPECTATIONS ' + specDescription; + } + specListNode.appendChild( + createDom('li', { + className: resultNode.result.status, + id: 'spec-' + resultNode.result.id + }, + createDom('a', {href: specHref(resultNode.result)}, specDescription) + ) + ); + } + } + } + + if (failures.length) { + alert.appendChild( + createDom('span', {className: 'menu bar spec-list'}, + createDom('span', {}, 'Spec List | '), + createDom('a', {className: 'failures-menu', href: '#'}, 'Failures'))); + alert.appendChild( + createDom('span', {className: 'menu bar failure-list'}, + createDom('a', {className: 'spec-list-menu', href: '#'}, 'Spec List'), + createDom('span', {}, ' | Failures '))); + + find('.failures-menu').onclick = function() { + setMenuModeTo('failure-list'); + }; + find('.spec-list-menu').onclick = function() { + setMenuModeTo('spec-list'); + }; + + setMenuModeTo('failure-list'); + + var failureNode = find('.failures'); + for (var i = 0; i < failures.length; i++) { + failureNode.appendChild(failures[i]); + } + } + }; + + return this; + + function find(selector) { + return getContainer().querySelector('.jasmine_html-reporter ' + selector); + } + + function clearPrior() { + // return the reporter + var oldReporter = find(''); + + if(oldReporter) { + getContainer().removeChild(oldReporter); + } + } + + function createDom(type, attrs, childrenVarArgs) { + var el = createElement(type); + + for (var i = 2; i < arguments.length; i++) { + var child = arguments[i]; + + if (typeof child === 'string') { + el.appendChild(createTextNode(child)); + } else { + if (child) { + el.appendChild(child); + } + } + } + + for (var attr in attrs) { + if (attr == 'className') { + el[attr] = attrs[attr]; + } else { + el.setAttribute(attr, attrs[attr]); + } + } + + return el; + } + + function pluralize(singular, count) { + var word = (count == 1 ? singular : singular + 's'); + + return '' + count + ' ' + word; + } + + function specHref(result) { + return '?spec=' + encodeURIComponent(result.fullName); + } + + function setMenuModeTo(mode) { + htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode); + } + + function noExpectations(result) { + return (result.failedExpectations.length + result.passedExpectations.length) === 0 && + result.status === 'passed'; + } + } + + return HtmlReporter; +}; + +jasmineRequire.HtmlSpecFilter = function() { + function HtmlSpecFilter(options) { + var filterString = options && options.filterString() && options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + var filterPattern = new RegExp(filterString); + + this.matches = function(specName) { + return filterPattern.test(specName); + }; + } + + return HtmlSpecFilter; +}; + +jasmineRequire.ResultsNode = function() { + function ResultsNode(result, type, parent) { + this.result = result; + this.type = type; + this.parent = parent; + + this.children = []; + + this.addChild = function(result, type) { + this.children.push(new ResultsNode(result, type, this)); + }; + + this.last = function() { + return this.children[this.children.length - 1]; + }; + } + + return ResultsNode; +}; + +jasmineRequire.QueryString = function() { + function QueryString(options) { + + this.setParam = function(key, value) { + var paramMap = queryStringToParamMap(); + paramMap[key] = value; + options.getWindowLocation().search = toQueryString(paramMap); + }; + + this.getParam = function(key) { + return queryStringToParamMap()[key]; + }; + + return this; + + function toQueryString(paramMap) { + var qStrPairs = []; + for (var prop in paramMap) { + qStrPairs.push(encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop])); + } + return '?' + qStrPairs.join('&'); + } + + function queryStringToParamMap() { + var paramStr = options.getWindowLocation().search.substring(1), + params = [], + paramMap = {}; + + if (paramStr.length > 0) { + params = paramStr.split('&'); + for (var i = 0; i < params.length; i++) { + var p = params[i].split('='); + var value = decodeURIComponent(p[1]); + if (value === 'true' || value === 'false') { + value = JSON.parse(value); + } + paramMap[decodeURIComponent(p[0])] = value; + } + } + + return paramMap; + } + + } + + return QueryString; +}; diff --git a/third_party/jasmine/lib/jasmine-2.1.3/jasmine.css b/third_party/jasmine/lib/jasmine-2.1.3/jasmine.css new file mode 100644 index 0000000000..7ae5834879 --- /dev/null +++ b/third_party/jasmine/lib/jasmine-2.1.3/jasmine.css @@ -0,0 +1,62 @@ +body { overflow-y: scroll; } + +.jasmine_html-reporter { background-color: #eeeeee; padding: 5px; margin: -8px; font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } +.jasmine_html-reporter a { text-decoration: none; } +.jasmine_html-reporter a:hover { text-decoration: underline; } +.jasmine_html-reporter p, .jasmine_html-reporter h1, .jasmine_html-reporter h2, .jasmine_html-reporter h3, .jasmine_html-reporter h4, .jasmine_html-reporter h5, .jasmine_html-reporter h6 { margin: 0; line-height: 14px; } +.jasmine_html-reporter .banner, .jasmine_html-reporter .symbol-summary, .jasmine_html-reporter .summary, .jasmine_html-reporter .result-message, .jasmine_html-reporter .spec .description, .jasmine_html-reporter .spec-detail .description, .jasmine_html-reporter .alert .bar, .jasmine_html-reporter .stack-trace { padding-left: 9px; padding-right: 9px; } +.jasmine_html-reporter .banner { position: relative; } +.jasmine_html-reporter .banner .title { background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFoAAAAZCAMAAACGusnyAAACdlBMVEX/////AP+AgICqVaqAQICZM5mAVYCSSZKAQICOOY6ATYCLRouAQICJO4mSSYCIRIiPQICHPIeOR4CGQ4aMQICGPYaLRoCFQ4WKQICPPYWJRYCOQoSJQICNPoSIRICMQoSHQICHRICKQoOHQICKPoOJO4OJQYOMQICMQ4CIQYKLQICIPoKLQ4CKQICNPoKJQISMQ4KJQoSLQYKJQISLQ4KIQoSKQYKIQICIQISMQoSKQYKLQIOLQoOJQYGLQIOKQIOMQoGKQYOLQYGKQIOLQoGJQYOJQIOKQYGJQIOKQoGKQIGLQIKLQ4KKQoGLQYKJQIGKQYKJQIGKQIKJQoGKQYKLQIGKQYKLQIOJQoKKQoOJQYKKQIOJQoKKQoOKQIOLQoKKQYOLQYKJQIOKQoKKQYKKQoKJQYOKQYKLQIOKQoKLQYOKQYKLQIOJQoGKQYKJQYGJQoGKQYKLQoGLQYGKQoGJQYKKQYGJQIKKQoGJQYKLQIKKQYGLQYKKQYGKQYGKQYKJQYOKQoKJQYOKQYKLQYOLQYOKQYKLQYOKQoKKQYKKQYOKQYOJQYKKQYKLQYKKQIKKQoKKQYKKQYKKQoKJQIKKQYKLQYKKQYKKQIKKQYKKQYKKQYKKQIKKQYKJQYGLQYGKQYKKQYKKQYGKQIKKQYGKQYOJQoKKQYOLQYKKQYOKQoKKQYKKQoKKQYKKQYKJQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKJQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKLQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKKQYKmIDpEAAAA0XRSTlMAAQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAiIyQlJycoKissLS4wMTQ1Njc4OTo7PDw+P0BCQ0RISUpLTE1OUFNUVVdYWFlaW15fYGFiY2ZnaGlqa2xtb3BxcnN0dnh5ent8fX5/gIGChIWIioyNjo+QkZOUlZaYmZqbnJ2eoKGio6WmqKmsra6vsLGztre4ubq7vL2+wMHDxMjJysvNzs/Q0dLU1tfY2dvc3t/g4eLj5ebn6Onq6+zt7u/w8vP09fb3+Pn6+/z9/vkVQXAAAAMaSURBVHhe5dXxV1N1GMfxz2ABbDgIAm5VDJOyVDIJLUMaVpBWUZUaGbmqoGpZRSiGiRWp6KoZ5AB0ZY50RImZQIlahKkMYXv/R90dBvET/rJfOr3Ouc8v99zPec59zvf56j+vYKlViSf7250X4Mr3O29Tgq08BdGB4DhcekEJ5YkQKFsgWZdtj9JpV+I8xPjLFqkrsEIqO8PHSpis36jWazcqjEsfJjkvRssVU37SdIOu4XCf5vEJPsnwJpnRNU9JmxhMk8l1gehIrq7hTFjzOD+Vf88629qKMJVNltInFeRexRQyJlNeqd1iGDlSzrIUIyXbyFfm3RYprcQRe7lqtWyGYbfc6dT0R2vmdOOkX3u55C1rP37ftiH+tDby4r/RBT0w8TyEkr+epB9XgPDmSYYWbrhCuFYaIyw3fDQAXTnSkh+ANofiHmWf9l+FY1I90FdQTetstO00o23novzVsJ7uB3/C5TkbjRwZ5JerwV4iRWq9HFbFMaK/d0TYqayRiQPuIxxS3Bu8JWU90/60tKi7vkhaznez0a/TbVOKj5CaOZh6fWG6/Lyv9B/ZLR1gw/S/fpbeVD3MCW1li6SvWDOn65tr99/uvWtBS0XDm4s1t+sOHpG0kpBKx/l77wOSnxLpcx6TXmXLTPQOKYOf9Q1dfr8/SJ2mFdCvl1Yl93DiHUZvXeLJbGSzYu5gVJ2slbSakOR8dxCq5adQ2oFLqsE9Ex3L4qQO0eOPeU5x56bypXp4onSEb5OkICX6lDat55TeoztNKQcJaakrz9KCb95oD69IKq+yKW4XPjknaS52V0TZqE2cTtXjcHSCRmUO88e+85hj3EP74i9p8pylw7lxgMDyyl6OV7ZejnjNMfatu87LxRbH0IS35gt2a4ZjmGpVBdKK3Wr6INk8jWWSGqbA55CKgjBRC6E9w78ydTg3ABS3AFV1QN0Y4Aa2pgEjWnQURj9L0ayK6R2ysEqxHUKzYnLvvyU+i9KM2JHJzE4vyZOyDcOwOsySajeLPc8sNvPJkFlyJd20wpqAzZeAfZ3oWybxd+P/3j+SG3uSBdf2VQAAAABJRU5ErkJggg==') no-repeat; background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXRoIElua3NjYXBlIChodHRwOi8vd3d3Lmlua3NjYXBlLm9yZy8pIC0tPgoKPHN2ZwogICB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iCiAgIHhtbG5zOmNjPSJodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9ucyMiCiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIKICAgeG1sbnM6c3ZnPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgdmVyc2lvbj0iMS4xIgogICB3aWR0aD0iNjgxLjk2MjUyIgogICBoZWlnaHQ9IjE4Ny41IgogICBpZD0ic3ZnMiIKICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PG1ldGFkYXRhCiAgICAgaWQ9Im1ldGFkYXRhOCI+PHJkZjpSREY+PGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPjxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PjxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz48L2NjOldvcms+PC9yZGY6UkRGPjwvbWV0YWRhdGE+PGRlZnMKICAgICBpZD0iZGVmczYiPjxjbGlwUGF0aAogICAgICAgaWQ9ImNsaXBQYXRoMTgiPjxwYXRoCiAgICAgICAgIGQ9Ik0gMCwxNTAwIDAsMCBsIDU0NTUuNzQsMCAwLDE1MDAgTCAwLDE1MDAgeiIKICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgaWQ9InBhdGgyMCIgLz48L2NsaXBQYXRoPjwvZGVmcz48ZwogICAgIHRyYW5zZm9ybT0ibWF0cml4KDEuMjUsMCwwLC0xLjI1LDAsMTg3LjUpIgogICAgIGlkPSJnMTAiPjxnCiAgICAgICB0cmFuc2Zvcm09InNjYWxlKDAuMSwwLjEpIgogICAgICAgaWQ9ImcxMiI+PGcKICAgICAgICAgaWQ9ImcxNCI+PGcKICAgICAgICAgICBjbGlwLXBhdGg9InVybCgjY2xpcFBhdGgxOCkiCiAgICAgICAgICAgaWQ9ImcxNiI+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTU0NCw1OTkuNDM0IGMgMC45MiwtNDAuMzUyIDI1LjY4LC04MS42MDIgNzEuNTMsLTgxLjYwMiAyNy41MSwwIDQ3LjY4LDEyLjgzMiA2MS40NCwzNS43NTQgMTIuODMsMjIuOTMgMTIuODMsNTYuODUyIDEyLjgzLDgyLjUyNyBsIDAsMzI5LjE4NCAtNzEuNTIsMCAwLDEwNC41NDMgMjY2LjgzLDAgMCwtMTA0LjU0MyAtNzAuNiwwIDAsLTM0NC43NyBjIDAsLTU4LjY5MSAtMy42OCwtMTA0LjUzMSAtNDQuOTMsLTE1Mi4yMTggLTM2LjY4LC00Mi4xOCAtOTYuMjgsLTY2LjAyIC0xNTMuMTQsLTY2LjAyIC0xMTcuMzcsMCAtMjA3LjI0LDc3Ljk0MSAtMjAyLjY0LDE5Ny4xNDUgbCAxMzAuMiwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMjIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDIzMDEuNCw2NjIuNjk1IGMgMCw4MC43MDMgLTY2Ljk0LDE0NS44MTMgLTE0Ny42MywxNDUuODEzIC04My40NCwwIC0xNDcuNjMsLTY4Ljc4MSAtMTQ3LjYzLC0xNTEuMzAxIDAsLTc5Ljc4NSA2Ni45NCwtMTQ1LjgwMSAxNDUuOCwtMTQ1LjgwMSA4NC4zNSwwIDE0OS40Niw2Ny44NTIgMTQ5LjQ2LDE1MS4yODkgeiBtIC0xLjgzLC0xODEuNTQ3IGMgLTM1Ljc3LC01NC4wOTcgLTkzLjUzLC03OC44NTkgLTE1Ny43MiwtNzguODU5IC0xNDAuMywwIC0yNTEuMjQsMTE2LjQ0OSAtMjUxLjI0LDI1NC45MTggMCwxNDIuMTI5IDExMy43LDI2MC40MSAyNTYuNzQsMjYwLjQxIDYzLjI3LDAgMTE4LjI5LC0yOS4zMzYgMTUyLjIyLC04Mi41MjMgbCAwLDY5LjY4NyAxNzUuMTQsMCAwLC0xMDQuNTI3IC02MS40NCwwIDAsLTI4MC41OTggNjEuNDQsMCAwLC0xMDQuNTI3IC0xNzUuMTQsMCAwLDY2LjAxOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAyNjIyLjMzLDU1Ny4yNTggYyAzLjY3LC00NC4wMTYgMzMuMDEsLTczLjM0OCA3OC44NiwtNzMuMzQ4IDMzLjkzLDAgNjYuOTMsMjMuODI0IDY2LjkzLDYwLjUwNCAwLDQ4LjYwNiAtNDUuODQsNTYuODU2IC04My40NCw2Ni45NDEgLTg1LjI4LDIyLjAwNCAtMTc4LjgxLDQ4LjYwNiAtMTc4LjgxLDE1NS44NzkgMCw5My41MzYgNzguODYsMTQ3LjYzMyAxNjUuOTgsMTQ3LjYzMyA0NCwwIDgzLjQzLC05LjE3NiAxMTAuOTQsLTQ0LjAwOCBsIDAsMzMuOTIyIDgyLjUzLDAgMCwtMTMyLjk2NSAtMTA4LjIxLDAgYyAtMS44MywzNC44NTYgLTI4LjQyLDU3Ljc3NCAtNjMuMjYsNTcuNzc0IC0zMC4yNiwwIC02Mi4zNSwtMTcuNDIyIC02Mi4zNSwtNTEuMzQ4IDAsLTQ1Ljg0NyA0NC45MywtNTUuOTMgODAuNjksLTY0LjE4IDg4LjAyLC0yMC4xNzUgMTgyLjQ3LC00Ny42OTUgMTgyLjQ3LC0xNTcuNzM0IDAsLTk5LjAyNyAtODMuNDQsLTE1NC4wMzkgLTE3NS4xMywtMTU0LjAzOSAtNDkuNTMsMCAtOTQuNDYsMTUuNTgyIC0xMjYuNTUsNTMuMTggbCAwLC00MC4zNCAtODUuMjcsMCAwLDE0Mi4xMjkgMTE0LjYyLDAiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGgyNiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMjk4OC4xOCw4MDAuMjU0IC02My4yNiwwIDAsMTA0LjUyNyAxNjUuMDUsMCAwLC03My4zNTUgYyAzMS4xOCw1MS4zNDcgNzguODYsODUuMjc3IDE0MS4yMSw4NS4yNzcgNjcuODUsMCAxMjQuNzEsLTQxLjI1OCAxNTIuMjEsLTEwMi42OTkgMjYuNiw2Mi4zNTEgOTIuNjIsMTAyLjY5OSAxNjAuNDcsMTAyLjY5OSA1My4xOSwwIDEwNS40NiwtMjIgMTQxLjIxLC02Mi4zNTEgMzguNTIsLTQ0LjkzOCAzOC41MiwtOTMuNTMyIDM4LjUyLC0xNDkuNDU3IGwgMCwtMTg1LjIzOSA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40MiwwIDAsMTA0LjUyNyA2My4yOCwwIDAsMTU3LjcxNSBjIDAsMzIuMTAyIDAsNjAuNTI3IC0xNC42Nyw4OC45NTcgLTE4LjM0LDI2LjU4MiAtNDguNjEsNDAuMzQ0IC03OS43Nyw0MC4zNDQgLTMwLjI2LDAgLTYzLjI4LC0xMi44NDQgLTgyLjUzLC0zNi42NzIgLTIyLjkzLC0yOS4zNTUgLTIyLjkzLC01Ni44NjMgLTIyLjkzLC05Mi42MjkgbCAwLC0xNTcuNzE1IDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM4LjQxLDAgMCwxMDQuNTI3IDYzLjI4LDAgMCwxNTAuMzgzIGMgMCwyOS4zNDggMCw2Ni4wMjMgLTE0LjY3LDkxLjY5OSAtMTUuNTksMjkuMzM2IC00Ny42OSw0NC45MzQgLTgwLjcsNDQuOTM0IC0zMS4xOCwwIC01Ny43NywtMTEuMDA4IC03Ny45NCwtMzUuNzc0IC0yNC43NywtMzAuMjUzIC0yNi42LC02Mi4zNDMgLTI2LjYsLTk5Ljk0MSBsIDAsLTE1MS4zMDEgNjMuMjcsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNiwwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDI4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSAzOTk4LjY2LDk1MS41NDcgLTExMS44NywwIDAsMTE4LjI5MyAxMTEuODcsMCAwLC0xMTguMjkzIHogbSAwLC00MzEuODkxIDYzLjI3LDAgMCwtMTA0LjUyNyAtMjM5LjMzLDAgMCwxMDQuNTI3IDY0LjE5LDAgMCwyODAuNTk4IC02My4yNywwIDAsMTA0LjUyNyAxNzUuMTQsMCAwLC0zODUuMTI1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzAiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDQxNTkuMTIsODAwLjI1NCAtNjMuMjcsMCAwLDEwNC41MjcgMTc1LjE0LDAgMCwtNjkuNjg3IGMgMjkuMzUsNTQuMTAxIDg0LjM2LDgwLjY5OSAxNDQuODcsODAuNjk5IDUzLjE5LDAgMTA1LjQ1LC0yMi4wMTYgMTQxLjIyLC02MC41MjcgNDAuMzQsLTQ0LjkzNCA0MS4yNiwtODguMDMyIDQxLjI2LC0xNDMuOTU3IGwgMCwtMTkxLjY1MyA2My4yNywwIDAsLTEwNC41MjcgLTIzOC40LDAgMCwxMDQuNTI3IDYzLjI2LDAgMCwxNTguNjM3IGMgMCwzMC4yNjIgMCw2MS40MzQgLTE5LjI2LDg4LjAzNSAtMjAuMTcsMjYuNTgyIC01My4xOCwzOS40MTQgLTg2LjE5LDM5LjQxNCAtMzMuOTMsMCAtNjguNzcsLTEzLjc1IC04OC45NCwtNDEuMjUgLTIxLjA5LC0yNy41IC0yMS4wOSwtNjkuNjg3IC0yMS4wOSwtMTAyLjcwNyBsIDAsLTE0Mi4xMjkgNjMuMjYsMCAwLC0xMDQuNTI3IC0yMzguNCwwIDAsMTA0LjUyNyA2My4yNywwIDAsMjgwLjU5OCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDMyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA1MDgyLjQ4LDcwMy45NjUgYyAtMTkuMjQsNzAuNjA1IC04MS42LDExNS41NDcgLTE1NC4wNCwxMTUuNTQ3IC02Ni4wNCwwIC0xMjkuMywtNTEuMzQ4IC0xNDMuMDUsLTExNS41NDcgbCAyOTcuMDksMCB6IG0gODUuMjcsLTE0NC44ODMgYyAtMzguNTEsLTkzLjUyMyAtMTI5LjI3LC0xNTYuNzkzIC0yMzEuMDUsLTE1Ni43OTMgLTE0My4wNywwIC0yNTcuNjgsMTExLjg3MSAtMjU3LjY4LDI1NS44MzYgMCwxNDQuODgzIDEwOS4xMiwyNjEuMzI4IDI1NC45MSwyNjEuMzI4IDY3Ljg3LDAgMTM1LjcyLC0zMC4yNTggMTgzLjM5LC03OC44NjMgNDguNjIsLTUxLjM0NCA2OC43OSwtMTEzLjY5NSA2OC43OSwtMTgzLjM4MyBsIC0zLjY3LC0zOS40MzQgLTM5Ni4xMywwIGMgMTQuNjcsLTY3Ljg2MyA3Ny4wMywtMTE3LjM2MyAxNDYuNzIsLTExNy4zNjMgNDguNTksMCA5MC43NiwxOC4zMjggMTE4LjI4LDU4LjY3MiBsIDExNi40NCwwIgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDY5MC44OTUsODUwLjcwMyA5MC43NSwwIDIyLjU0MywzMS4wMzUgMCwyNDMuMTIyIC0xMzUuODI5LDAgMCwtMjQzLjE0MSAyMi41MzYsLTMxLjAxNiIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDM2IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA2MzIuMzk1LDc0Mi4yNTggMjguMDM5LDg2LjMwNCAtMjIuNTUxLDMxLjA0IC0yMzEuMjIzLDc1LjEyOCAtNDEuOTc2LC0xMjkuMTgzIDIzMS4yNTcsLTc1LjEzNyAzNi40NTQsMTEuODQ4IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoMzgiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDcxNy40NDksNjUzLjEwNSAtNzMuNDEsNTMuMzYgLTM2LjQ4OCwtMTEuODc1IC0xNDIuOTAzLC0xOTYuNjkyIDEwOS44ODMsLTc5LjgyOCAxNDIuOTE4LDE5Ni43MDMgMCwzOC4zMzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0MCIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gODI4LjUyLDcwNi40NjUgLTczLjQyNiwtNTMuMzQgMC4wMTEsLTM4LjM1OSBMIDg5OC4wMDQsNDE4LjA3IDEwMDcuOSw0OTcuODk4IDg2NC45NzMsNjk0LjYwOSA4MjguNTIsNzA2LjQ2NSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQyIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA4MTIuMDg2LDgyOC41ODYgMjguMDU1LC04Ni4zMiAzNi40ODQsLTExLjgzNiAyMzEuMjI1LDc1LjExNyAtNDEuOTcsMTI5LjE4MyAtMjMxLjIzOSwtNzUuMTQgLTIyLjU1NSwtMzEuMDA0IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNDQiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDczNi4zMDEsMTMzNS44OCBjIC0zMjMuMDQ3LDAgLTU4NS44NzUsLTI2Mi43OCAtNTg1Ljg3NSwtNTg1Ljc4MiAwLC0zMjMuMTE4IDI2Mi44MjgsLTU4NS45NzcgNTg1Ljg3NSwtNTg1Ljk3NyAzMjMuMDE5LDAgNTg1LjgwOSwyNjIuODU5IDU4NS44MDksNTg1Ljk3NyAwLDMyMy4wMDIgLTI2Mi43OSw1ODUuNzgyIC01ODUuODA5LDU4NS43ODIgbCAwLDAgeiBtIDAsLTExOC42MSBjIDI1Ny45NzIsMCA0NjcuMTg5LC0yMDkuMTMgNDY3LjE4OSwtNDY3LjE3MiAwLC0yNTguMTI5IC0yMDkuMjE3LC00NjcuMzQ4IC00NjcuMTg5LC00NjcuMzQ4IC0yNTguMDc0LDAgLTQ2Ny4yNTQsMjA5LjIxOSAtNDY3LjI1NCw0NjcuMzQ4IDAsMjU4LjA0MiAyMDkuMTgsNDY3LjE3MiA0NjcuMjU0LDQ2Ny4xNzIiCiAgICAgICAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIgogICAgICAgICAgICAgaWQ9InBhdGg0NiIKICAgICAgICAgICAgIHN0eWxlPSJmaWxsOiM4YTQxODI7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmUiIC8+PHBhdGgKICAgICAgICAgICAgIGQ9Im0gMTA5MS4xMyw2MTkuODgzIC0xNzUuNzcxLDU3LjEyMSAxMS42MjksMzUuODA4IDE3NS43NjIsLTU3LjEyMSAtMTEuNjIsLTM1LjgwOCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDQ4IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA4NjYuOTU3LDkwMi4wNzQgODM2LjUsOTI0LjE5OSA5NDUuMTIxLDEwNzMuNzMgOTc1LjU4NiwxMDUxLjYxIDg2Ni45NTcsOTAyLjA3NCIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDUwIgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0iTSA2MDcuNDY1LDkwMy40NDUgNDk4Ljg1NSwxMDUyLjk3IDUyOS4zMiwxMDc1LjEgNjM3LjkzLDkyNS41NjYgNjA3LjQ2NSw5MDMuNDQ1IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTIiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjxwYXRoCiAgICAgICAgICAgICBkPSJtIDM4MC42ODgsNjIyLjEyOSAtMTEuNjI2LDM1LjgwMSAxNzUuNzU4LDU3LjA5IDExLjYyMSwtMzUuODAxIC0xNzUuNzUzLC01Ny4wOSIKICAgICAgICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiCiAgICAgICAgICAgICBpZD0icGF0aDU0IgogICAgICAgICAgICAgc3R5bGU9ImZpbGw6IzhhNDE4MjtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZSIgLz48cGF0aAogICAgICAgICAgICAgZD0ibSA3MTYuMjg5LDM3Ni41OSAzNy42NDA2LDAgMCwxODQuODE2IC0zNy42NDA2LDAgMCwtMTg0LjgxNiB6IgogICAgICAgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIKICAgICAgICAgICAgIGlkPSJwYXRoNTYiCiAgICAgICAgICAgICBzdHlsZT0iZmlsbDojOGE0MTgyO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lIiAvPjwvZz48L2c+PC9nPjwvZz48L3N2Zz4=') no-repeat, none; -webkit-background-size: 100%; -moz-background-size: 100%; -o-background-size: 100%; background-size: 100%; display: block; float: left; width: 90px; height: 25px; } +.jasmine_html-reporter .banner .version { margin-left: 14px; position: relative; top: 6px; } +.jasmine_html-reporter .banner .duration { position: absolute; right: 14px; top: 6px; } +.jasmine_html-reporter #jasmine_content { position: fixed; right: 100%; } +.jasmine_html-reporter .version { color: #aaaaaa; } +.jasmine_html-reporter .banner { margin-top: 14px; } +.jasmine_html-reporter .duration { color: #aaaaaa; float: right; } +.jasmine_html-reporter .symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; } +.jasmine_html-reporter .symbol-summary li { display: inline-block; height: 8px; width: 14px; font-size: 16px; } +.jasmine_html-reporter .symbol-summary li.passed { font-size: 14px; } +.jasmine_html-reporter .symbol-summary li.passed:before { color: #007069; content: "\02022"; } +.jasmine_html-reporter .symbol-summary li.failed { line-height: 9px; } +.jasmine_html-reporter .symbol-summary li.failed:before { color: #ca3a11; content: "\d7"; font-weight: bold; margin-left: -1px; } +.jasmine_html-reporter .symbol-summary li.disabled { font-size: 14px; } +.jasmine_html-reporter .symbol-summary li.disabled:before { color: #bababa; content: "\02022"; } +.jasmine_html-reporter .symbol-summary li.pending { line-height: 17px; } +.jasmine_html-reporter .symbol-summary li.pending:before { color: #ba9d37; content: "*"; } +.jasmine_html-reporter .symbol-summary li.empty { font-size: 14px; } +.jasmine_html-reporter .symbol-summary li.empty:before { color: #ba9d37; content: "\02022"; } +.jasmine_html-reporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } +.jasmine_html-reporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } +.jasmine_html-reporter .bar.failed { background-color: #ca3a11; } +.jasmine_html-reporter .bar.passed { background-color: #007069; } +.jasmine_html-reporter .bar.skipped { background-color: #bababa; } +.jasmine_html-reporter .bar.errored { background-color: #ca3a11; } +.jasmine_html-reporter .bar.menu { background-color: #fff; color: #aaaaaa; } +.jasmine_html-reporter .bar.menu a { color: #333333; } +.jasmine_html-reporter .bar a { color: white; } +.jasmine_html-reporter.spec-list .bar.menu.failure-list, .jasmine_html-reporter.spec-list .results .failures { display: none; } +.jasmine_html-reporter.failure-list .bar.menu.spec-list, .jasmine_html-reporter.failure-list .summary { display: none; } +.jasmine_html-reporter .running-alert { background-color: #666666; } +.jasmine_html-reporter .results { margin-top: 14px; } +.jasmine_html-reporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } +.jasmine_html-reporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } +.jasmine_html-reporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } +.jasmine_html-reporter.showDetails .summary { display: none; } +.jasmine_html-reporter.showDetails #details { display: block; } +.jasmine_html-reporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } +.jasmine_html-reporter .summary { margin-top: 14px; } +.jasmine_html-reporter .summary ul { list-style-type: none; margin-left: 14px; padding-top: 0; padding-left: 0; } +.jasmine_html-reporter .summary ul.suite { margin-top: 7px; margin-bottom: 7px; } +.jasmine_html-reporter .summary li.passed a { color: #007069; } +.jasmine_html-reporter .summary li.failed a { color: #ca3a11; } +.jasmine_html-reporter .summary li.empty a { color: #ba9d37; } +.jasmine_html-reporter .summary li.pending a { color: #ba9d37; } +.jasmine_html-reporter .description + .suite { margin-top: 0; } +.jasmine_html-reporter .suite { margin-top: 14px; } +.jasmine_html-reporter .suite a { color: #333333; } +.jasmine_html-reporter .failures .spec-detail { margin-bottom: 28px; } +.jasmine_html-reporter .failures .spec-detail .description { background-color: #ca3a11; } +.jasmine_html-reporter .failures .spec-detail .description a { color: white; } +.jasmine_html-reporter .result-message { padding-top: 14px; color: #333333; white-space: pre; } +.jasmine_html-reporter .result-message span.result { display: block; } +.jasmine_html-reporter .stack-trace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } diff --git a/third_party/jasmine/lib/jasmine-2.1.3/jasmine.js b/third_party/jasmine/lib/jasmine-2.1.3/jasmine.js new file mode 100644 index 0000000000..56209b45b4 --- /dev/null +++ b/third_party/jasmine/lib/jasmine-2.1.3/jasmine.js @@ -0,0 +1,2913 @@ +/* +Copyright (c) 2008-2014 Pivotal Labs + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +getJasmineRequireObj = (function (jasmineGlobal) { + var jasmineRequire; + + if (typeof module !== 'undefined' && module.exports) { + jasmineGlobal = global; + jasmineRequire = exports; + } else { + jasmineRequire = jasmineGlobal.jasmineRequire = jasmineGlobal.jasmineRequire || {}; + } + + function getJasmineRequire() { + return jasmineRequire; + } + + getJasmineRequire().core = function(jRequire) { + var j$ = {}; + + jRequire.base(j$, jasmineGlobal); + j$.util = jRequire.util(); + j$.Any = jRequire.Any(); + j$.CallTracker = jRequire.CallTracker(); + j$.MockDate = jRequire.MockDate(); + j$.Clock = jRequire.Clock(); + j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(); + j$.Env = jRequire.Env(j$); + j$.ExceptionFormatter = jRequire.ExceptionFormatter(); + j$.Expectation = jRequire.Expectation(); + j$.buildExpectationResult = jRequire.buildExpectationResult(); + j$.JsApiReporter = jRequire.JsApiReporter(); + j$.matchersUtil = jRequire.matchersUtil(j$); + j$.ObjectContaining = jRequire.ObjectContaining(j$); + j$.pp = jRequire.pp(j$); + j$.QueueRunner = jRequire.QueueRunner(j$); + j$.ReportDispatcher = jRequire.ReportDispatcher(); + j$.Spec = jRequire.Spec(j$); + j$.SpyRegistry = jRequire.SpyRegistry(j$); + j$.SpyStrategy = jRequire.SpyStrategy(); + j$.Suite = jRequire.Suite(); + j$.Timer = jRequire.Timer(); + j$.version = jRequire.version(); + + j$.matchers = jRequire.requireMatchers(jRequire, j$); + + return j$; + }; + + return getJasmineRequire; +})(this); + +getJasmineRequireObj().requireMatchers = function(jRequire, j$) { + var availableMatchers = [ + 'toBe', + 'toBeCloseTo', + 'toBeDefined', + 'toBeFalsy', + 'toBeGreaterThan', + 'toBeLessThan', + 'toBeNaN', + 'toBeNull', + 'toBeTruthy', + 'toBeUndefined', + 'toContain', + 'toEqual', + 'toHaveBeenCalled', + 'toHaveBeenCalledWith', + 'toMatch', + 'toThrow', + 'toThrowError' + ], + matchers = {}; + + for (var i = 0; i < availableMatchers.length; i++) { + var name = availableMatchers[i]; + matchers[name] = jRequire[name](j$); + } + + return matchers; +}; + +getJasmineRequireObj().base = function(j$, jasmineGlobal) { + j$.unimplementedMethod_ = function() { + throw new Error('unimplemented method'); + }; + + j$.MAX_PRETTY_PRINT_DEPTH = 40; + j$.MAX_PRETTY_PRINT_ARRAY_LENGTH = 100; + j$.DEFAULT_TIMEOUT_INTERVAL = 5000; + + j$.getGlobal = function() { + return jasmineGlobal; + }; + + j$.getEnv = function(options) { + var env = j$.currentEnv_ = j$.currentEnv_ || new j$.Env(options); + //jasmine. singletons in here (setTimeout blah blah). + return env; + }; + + j$.isArray_ = function(value) { + return j$.isA_('Array', value); + }; + + j$.isString_ = function(value) { + return j$.isA_('String', value); + }; + + j$.isNumber_ = function(value) { + return j$.isA_('Number', value); + }; + + j$.isA_ = function(typeName, value) { + return Object.prototype.toString.apply(value) === '[object ' + typeName + ']'; + }; + + j$.isDomNode = function(obj) { + return obj.nodeType > 0; + }; + + j$.any = function(clazz) { + return new j$.Any(clazz); + }; + + j$.objectContaining = function(sample) { + return new j$.ObjectContaining(sample); + }; + + j$.createSpy = function(name, originalFn) { + + var spyStrategy = new j$.SpyStrategy({ + name: name, + fn: originalFn, + getSpy: function() { return spy; } + }), + callTracker = new j$.CallTracker(), + spy = function() { + var callData = { + object: this, + args: Array.prototype.slice.apply(arguments) + }; + + callTracker.track(callData); + var returnValue = spyStrategy.exec.apply(this, arguments); + callData.returnValue = returnValue; + + return returnValue; + }; + + for (var prop in originalFn) { + if (prop === 'and' || prop === 'calls') { + throw new Error('Jasmine spies would overwrite the \'and\' and \'calls\' properties on the object being spied upon'); + } + + spy[prop] = originalFn[prop]; + } + + spy.and = spyStrategy; + spy.calls = callTracker; + + return spy; + }; + + j$.isSpy = function(putativeSpy) { + if (!putativeSpy) { + return false; + } + return putativeSpy.and instanceof j$.SpyStrategy && + putativeSpy.calls instanceof j$.CallTracker; + }; + + j$.createSpyObj = function(baseName, methodNames) { + if (!j$.isArray_(methodNames) || methodNames.length === 0) { + throw 'createSpyObj requires a non-empty array of method names to create spies for'; + } + var obj = {}; + for (var i = 0; i < methodNames.length; i++) { + obj[methodNames[i]] = j$.createSpy(baseName + '.' + methodNames[i]); + } + return obj; + }; +}; + +getJasmineRequireObj().util = function() { + + var util = {}; + + util.inherit = function(childClass, parentClass) { + var Subclass = function() { + }; + Subclass.prototype = parentClass.prototype; + childClass.prototype = new Subclass(); + }; + + util.htmlEscape = function(str) { + if (!str) { + return str; + } + return str.replace(/&/g, '&') + .replace(//g, '>'); + }; + + util.argsToArray = function(args) { + var arrayOfArgs = []; + for (var i = 0; i < args.length; i++) { + arrayOfArgs.push(args[i]); + } + return arrayOfArgs; + }; + + util.isUndefined = function(obj) { + return obj === void 0; + }; + + util.arrayContains = function(array, search) { + var i = array.length; + while (i--) { + if (array[i] === search) { + return true; + } + } + return false; + }; + + util.clone = function(obj) { + if (Object.prototype.toString.apply(obj) === '[object Array]') { + return obj.slice(); + } + + var cloned = {}; + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + cloned[prop] = obj[prop]; + } + } + + return cloned; + }; + + return util; +}; + +getJasmineRequireObj().Spec = function(j$) { + function Spec(attrs) { + this.expectationFactory = attrs.expectationFactory; + this.resultCallback = attrs.resultCallback || function() {}; + this.id = attrs.id; + this.description = attrs.description || ''; + this.queueableFn = attrs.queueableFn; + this.beforeAndAfterFns = attrs.beforeAndAfterFns || function() { return {befores: [], afters: []}; }; + this.userContext = attrs.userContext || function() { return {}; }; + this.onStart = attrs.onStart || function() {}; + this.getSpecName = attrs.getSpecName || function() { return ''; }; + this.expectationResultFactory = attrs.expectationResultFactory || function() { }; + this.queueRunnerFactory = attrs.queueRunnerFactory || function() {}; + this.catchingExceptions = attrs.catchingExceptions || function() { return true; }; + + if (!this.queueableFn.fn) { + this.pend(); + } + + this.result = { + id: this.id, + description: this.description, + fullName: this.getFullName(), + failedExpectations: [], + passedExpectations: [] + }; + } + + Spec.prototype.addExpectationResult = function(passed, data) { + var expectationResult = this.expectationResultFactory(data); + if (passed) { + this.result.passedExpectations.push(expectationResult); + } else { + this.result.failedExpectations.push(expectationResult); + } + }; + + Spec.prototype.expect = function(actual) { + return this.expectationFactory(actual, this); + }; + + Spec.prototype.execute = function(onComplete) { + var self = this; + + this.onStart(this); + + if (this.markedPending || this.disabled) { + complete(); + return; + } + + var fns = this.beforeAndAfterFns(); + var allFns = fns.befores.concat(this.queueableFn).concat(fns.afters); + + this.queueRunnerFactory({ + queueableFns: allFns, + onException: function() { self.onException.apply(self, arguments); }, + onComplete: complete, + userContext: this.userContext() + }); + + function complete() { + self.result.status = self.status(); + self.resultCallback(self.result); + + if (onComplete) { + onComplete(); + } + } + }; + + Spec.prototype.onException = function onException(e) { + if (Spec.isPendingSpecException(e)) { + this.pend(); + return; + } + + this.addExpectationResult(false, { + matcherName: '', + passed: false, + expected: '', + actual: '', + error: e + }); + }; + + Spec.prototype.disable = function() { + this.disabled = true; + }; + + Spec.prototype.pend = function() { + this.markedPending = true; + }; + + Spec.prototype.status = function() { + if (this.disabled) { + return 'disabled'; + } + + if (this.markedPending) { + return 'pending'; + } + + if (this.result.failedExpectations.length > 0) { + return 'failed'; + } else { + return 'passed'; + } + }; + + Spec.prototype.isExecutable = function() { + return !this.disabled && !this.markedPending; + }; + + Spec.prototype.getFullName = function() { + return this.getSpecName(this); + }; + + Spec.pendingSpecExceptionMessage = '=> marked Pending'; + + Spec.isPendingSpecException = function(e) { + return !!(e && e.toString && e.toString().indexOf(Spec.pendingSpecExceptionMessage) !== -1); + }; + + return Spec; +}; + +if (typeof window == void 0 && typeof exports == 'object') { + exports.Spec = jasmineRequire.Spec; +} + +getJasmineRequireObj().Env = function(j$) { + function Env(options) { + options = options || {}; + + var self = this; + var global = options.global || j$.getGlobal(); + + var totalSpecsDefined = 0; + + var catchExceptions = true; + + var realSetTimeout = j$.getGlobal().setTimeout; + var realClearTimeout = j$.getGlobal().clearTimeout; + this.clock = new j$.Clock(global, new j$.DelayedFunctionScheduler(), new j$.MockDate(global)); + + var runnableLookupTable = {}; + var runnableResources = {}; + + var currentSpec = null; + var currentlyExecutingSuites = []; + var currentDeclarationSuite = null; + + var currentSuite = function() { + return currentlyExecutingSuites[currentlyExecutingSuites.length - 1]; + }; + + var currentRunnable = function() { + return currentSpec || currentSuite(); + }; + + var reporter = new j$.ReportDispatcher([ + 'jasmineStarted', + 'jasmineDone', + 'suiteStarted', + 'suiteDone', + 'specStarted', + 'specDone' + ]); + + this.specFilter = function() { + return true; + }; + + this.addCustomEqualityTester = function(tester) { + if(!currentRunnable()) { + throw new Error('Custom Equalities must be added in a before function or a spec'); + } + runnableResources[currentRunnable().id].customEqualityTesters.push(tester); + }; + + this.addMatchers = function(matchersToAdd) { + if(!currentRunnable()) { + throw new Error('Matchers must be added in a before function or a spec'); + } + var customMatchers = runnableResources[currentRunnable().id].customMatchers; + for (var matcherName in matchersToAdd) { + customMatchers[matcherName] = matchersToAdd[matcherName]; + } + }; + + j$.Expectation.addCoreMatchers(j$.matchers); + + var nextSpecId = 0; + var getNextSpecId = function() { + return 'spec' + nextSpecId++; + }; + + var nextSuiteId = 0; + var getNextSuiteId = function() { + return 'suite' + nextSuiteId++; + }; + + var expectationFactory = function(actual, spec) { + return j$.Expectation.Factory({ + util: j$.matchersUtil, + customEqualityTesters: runnableResources[spec.id].customEqualityTesters, + customMatchers: runnableResources[spec.id].customMatchers, + actual: actual, + addExpectationResult: addExpectationResult + }); + + function addExpectationResult(passed, result) { + return spec.addExpectationResult(passed, result); + } + }; + + var defaultResourcesForRunnable = function(id, parentRunnableId) { + var resources = {spies: [], customEqualityTesters: [], customMatchers: {}}; + + if(runnableResources[parentRunnableId]){ + resources.customEqualityTesters = j$.util.clone(runnableResources[parentRunnableId].customEqualityTesters); + resources.customMatchers = j$.util.clone(runnableResources[parentRunnableId].customMatchers); + } + + runnableResources[id] = resources; + }; + + var clearResourcesForRunnable = function(id) { + spyRegistry.clearSpies(); + delete runnableResources[id]; + }; + + var beforeAndAfterFns = function(suite, runnablesExplictlySet) { + return function() { + var befores = [], + afters = [], + beforeAlls = [], + afterAlls = []; + + while(suite) { + befores = befores.concat(suite.beforeFns); + afters = afters.concat(suite.afterFns); + + if (runnablesExplictlySet()) { + beforeAlls = beforeAlls.concat(suite.beforeAllFns); + afterAlls = afterAlls.concat(suite.afterAllFns); + } + + suite = suite.parentSuite; + } + return { + befores: beforeAlls.reverse().concat(befores.reverse()), + afters: afters.concat(afterAlls) + }; + }; + }; + + var getSpecName = function(spec, suite) { + return suite.getFullName() + ' ' + spec.description; + }; + + // TODO: we may just be able to pass in the fn instead of wrapping here + var buildExpectationResult = j$.buildExpectationResult, + exceptionFormatter = new j$.ExceptionFormatter(), + expectationResultFactory = function(attrs) { + attrs.messageFormatter = exceptionFormatter.message; + attrs.stackFormatter = exceptionFormatter.stack; + + return buildExpectationResult(attrs); + }; + + // TODO: fix this naming, and here's where the value comes in + this.catchExceptions = function(value) { + catchExceptions = !!value; + return catchExceptions; + }; + + this.catchingExceptions = function() { + return catchExceptions; + }; + + var maximumSpecCallbackDepth = 20; + var currentSpecCallbackDepth = 0; + + function clearStack(fn) { + currentSpecCallbackDepth++; + if (currentSpecCallbackDepth >= maximumSpecCallbackDepth) { + currentSpecCallbackDepth = 0; + realSetTimeout(fn, 0); + } else { + fn(); + } + } + + var catchException = function(e) { + return j$.Spec.isPendingSpecException(e) || catchExceptions; + }; + + var queueRunnerFactory = function(options) { + options.catchException = catchException; + options.clearStack = options.clearStack || clearStack; + options.timer = {setTimeout: realSetTimeout, clearTimeout: realClearTimeout}; + options.fail = self.fail; + + new j$.QueueRunner(options).execute(); + }; + + var topSuite = new j$.Suite({ + env: this, + id: getNextSuiteId(), + description: 'Jasmine__TopLevel__Suite', + queueRunner: queueRunnerFactory + }); + runnableLookupTable[topSuite.id] = topSuite; + defaultResourcesForRunnable(topSuite.id); + currentDeclarationSuite = topSuite; + + this.topSuite = function() { + return topSuite; + }; + + this.execute = function(runnablesToRun) { + if(runnablesToRun) { + runnablesExplictlySet = true; + } else if (focusedRunnables.length) { + runnablesExplictlySet = true; + runnablesToRun = focusedRunnables; + } else { + runnablesToRun = [topSuite.id]; + } + + var allFns = []; + for(var i = 0; i < runnablesToRun.length; i++) { + var runnable = runnableLookupTable[runnablesToRun[i]]; + allFns.push((function(runnable) { return { fn: function(done) { runnable.execute(done); } }; })(runnable)); + } + + reporter.jasmineStarted({ + totalSpecsDefined: totalSpecsDefined + }); + + queueRunnerFactory({queueableFns: allFns, onComplete: reporter.jasmineDone}); + }; + + this.addReporter = function(reporterToAdd) { + reporter.addReporter(reporterToAdd); + }; + + var spyRegistry = new j$.SpyRegistry({currentSpies: function() { + if(!currentRunnable()) { + throw new Error('Spies must be created in a before function or a spec'); + } + return runnableResources[currentRunnable().id].spies; + }}); + + this.spyOn = function() { + return spyRegistry.spyOn.apply(spyRegistry, arguments); + }; + + var suiteFactory = function(description) { + var suite = new j$.Suite({ + env: self, + id: getNextSuiteId(), + description: description, + parentSuite: currentDeclarationSuite, + queueRunner: queueRunnerFactory, + onStart: suiteStarted, + expectationFactory: expectationFactory, + expectationResultFactory: expectationResultFactory, + resultCallback: function(attrs) { + if (!suite.disabled) { + clearResourcesForRunnable(suite.id); + currentlyExecutingSuites.pop(); + } + reporter.suiteDone(attrs); + } + }); + + runnableLookupTable[suite.id] = suite; + return suite; + + function suiteStarted(suite) { + currentlyExecutingSuites.push(suite); + defaultResourcesForRunnable(suite.id, suite.parentSuite.id); + reporter.suiteStarted(suite.result); + } + }; + + this.describe = function(description, specDefinitions) { + var suite = suiteFactory(description); + addSpecsToSuite(suite, specDefinitions); + return suite; + }; + + this.xdescribe = function(description, specDefinitions) { + var suite = this.describe(description, specDefinitions); + suite.disable(); + return suite; + }; + + var focusedRunnables = []; + + this.fdescribe = function(description, specDefinitions) { + var suite = suiteFactory(description); + suite.isFocused = true; + + focusedRunnables.push(suite.id); + unfocusAncestor(); + addSpecsToSuite(suite, specDefinitions); + + return suite; + }; + + function addSpecsToSuite(suite, specDefinitions) { + var parentSuite = currentDeclarationSuite; + parentSuite.addChild(suite); + currentDeclarationSuite = suite; + + var declarationError = null; + try { + specDefinitions.call(suite); + } catch (e) { + declarationError = e; + } + + if (declarationError) { + self.it('encountered a declaration exception', function() { + throw declarationError; + }); + } + + currentDeclarationSuite = parentSuite; + } + + function findFocusedAncestor(suite) { + while (suite) { + if (suite.isFocused) { + return suite.id; + } + suite = suite.parentSuite; + } + + return null; + } + + function unfocusAncestor() { + var focusedAncestor = findFocusedAncestor(currentDeclarationSuite); + if (focusedAncestor) { + for (var i = 0; i < focusedRunnables.length; i++) { + if (focusedRunnables[i] === focusedAncestor) { + focusedRunnables.splice(i, 1); + break; + } + } + } + } + + var runnablesExplictlySet = false; + + var runnablesExplictlySetGetter = function(){ + return runnablesExplictlySet; + }; + + var specFactory = function(description, fn, suite, timeout) { + totalSpecsDefined++; + var spec = new j$.Spec({ + id: getNextSpecId(), + beforeAndAfterFns: beforeAndAfterFns(suite, runnablesExplictlySetGetter), + expectationFactory: expectationFactory, + resultCallback: specResultCallback, + getSpecName: function(spec) { + return getSpecName(spec, suite); + }, + onStart: specStarted, + description: description, + expectationResultFactory: expectationResultFactory, + queueRunnerFactory: queueRunnerFactory, + userContext: function() { return suite.clonedSharedUserContext(); }, + queueableFn: { + fn: fn, + timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + } + }); + + runnableLookupTable[spec.id] = spec; + + if (!self.specFilter(spec)) { + spec.disable(); + } + + return spec; + + function specResultCallback(result) { + clearResourcesForRunnable(spec.id); + currentSpec = null; + reporter.specDone(result); + } + + function specStarted(spec) { + currentSpec = spec; + defaultResourcesForRunnable(spec.id, suite.id); + reporter.specStarted(spec.result); + } + }; + + this.it = function(description, fn, timeout) { + var spec = specFactory(description, fn, currentDeclarationSuite, timeout); + currentDeclarationSuite.addChild(spec); + return spec; + }; + + this.xit = function() { + var spec = this.it.apply(this, arguments); + spec.pend(); + return spec; + }; + + this.fit = function(){ + var spec = this.it.apply(this, arguments); + + focusedRunnables.push(spec.id); + unfocusAncestor(); + return spec; + }; + + this.expect = function(actual) { + if (!currentRunnable()) { + throw new Error('\'expect\' was used when there was no current spec, this could be because an asynchronous test timed out'); + } + + return currentRunnable().expect(actual); + }; + + this.beforeEach = function(beforeEachFunction, timeout) { + currentDeclarationSuite.beforeEach({ + fn: beforeEachFunction, + timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + }); + }; + + this.beforeAll = function(beforeAllFunction, timeout) { + currentDeclarationSuite.beforeAll({ + fn: beforeAllFunction, + timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + }); + }; + + this.afterEach = function(afterEachFunction, timeout) { + currentDeclarationSuite.afterEach({ + fn: afterEachFunction, + timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + }); + }; + + this.afterAll = function(afterAllFunction, timeout) { + currentDeclarationSuite.afterAll({ + fn: afterAllFunction, + timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; } + }); + }; + + this.pending = function() { + throw j$.Spec.pendingSpecExceptionMessage; + }; + + this.fail = function(error) { + var message = 'Failed'; + if (error) { + message += ': '; + message += error.message || error; + if (error.stack) { + var stack = error.stack.split('\n'); + // stack[0] repeats the message, so don't use it. + message += '\n' + stack.slice(1).join('\n'); + } + } + + currentRunnable().addExpectationResult(false, { + matcherName: '', + passed: false, + expected: '', + actual: '', + message: message + }); + }; + } + + return Env; +}; + +getJasmineRequireObj().JsApiReporter = function() { + + var noopTimer = { + start: function(){}, + elapsed: function(){ return 0; } + }; + + function JsApiReporter(options) { + var timer = options.timer || noopTimer, + status = 'loaded'; + + this.started = false; + this.finished = false; + + this.jasmineStarted = function() { + this.started = true; + status = 'started'; + timer.start(); + }; + + var executionTime; + + this.jasmineDone = function() { + this.finished = true; + executionTime = timer.elapsed(); + status = 'done'; + }; + + this.status = function() { + return status; + }; + + var suites = [], + suites_hash = {}; + + this.suiteStarted = function(result) { + suites_hash[result.id] = result; + }; + + this.suiteDone = function(result) { + storeSuite(result); + }; + + this.suiteResults = function(index, length) { + return suites.slice(index, index + length); + }; + + function storeSuite(result) { + suites.push(result); + suites_hash[result.id] = result; + } + + this.suites = function() { + return suites_hash; + }; + + var specs = []; + + this.specDone = function(result) { + specs.push(result); + }; + + this.specResults = function(index, length) { + return specs.slice(index, index + length); + }; + + this.specs = function() { + return specs; + }; + + this.executionTime = function() { + return executionTime; + }; + + } + + return JsApiReporter; +}; + +getJasmineRequireObj().Any = function() { + + function Any(expectedObject) { + this.expectedObject = expectedObject; + } + + Any.prototype.jasmineMatches = function(other) { + if (this.expectedObject == String) { + return typeof other == 'string' || other instanceof String; + } + + if (this.expectedObject == Number) { + return typeof other == 'number' || other instanceof Number; + } + + if (this.expectedObject == Function) { + return typeof other == 'function' || other instanceof Function; + } + + if (this.expectedObject == Object) { + return typeof other == 'object'; + } + + if (this.expectedObject == Boolean) { + return typeof other == 'boolean'; + } + + return other instanceof this.expectedObject; + }; + + Any.prototype.jasmineToString = function() { + return ''; + }; + + return Any; +}; + +getJasmineRequireObj().CallTracker = function() { + + function CallTracker() { + var calls = []; + + this.track = function(context) { + calls.push(context); + }; + + this.any = function() { + return !!calls.length; + }; + + this.count = function() { + return calls.length; + }; + + this.argsFor = function(index) { + var call = calls[index]; + return call ? call.args : []; + }; + + this.all = function() { + return calls; + }; + + this.allArgs = function() { + var callArgs = []; + for(var i = 0; i < calls.length; i++){ + callArgs.push(calls[i].args); + } + + return callArgs; + }; + + this.first = function() { + return calls[0]; + }; + + this.mostRecent = function() { + return calls[calls.length - 1]; + }; + + this.reset = function() { + calls = []; + }; + } + + return CallTracker; +}; + +getJasmineRequireObj().Clock = function() { + function Clock(global, delayedFunctionScheduler, mockDate) { + var self = this, + realTimingFunctions = { + setTimeout: global.setTimeout, + clearTimeout: global.clearTimeout, + setInterval: global.setInterval, + clearInterval: global.clearInterval + }, + fakeTimingFunctions = { + setTimeout: setTimeout, + clearTimeout: clearTimeout, + setInterval: setInterval, + clearInterval: clearInterval + }, + installed = false, + timer; + + + self.install = function() { + replace(global, fakeTimingFunctions); + timer = fakeTimingFunctions; + installed = true; + + return self; + }; + + self.uninstall = function() { + delayedFunctionScheduler.reset(); + mockDate.uninstall(); + replace(global, realTimingFunctions); + + timer = realTimingFunctions; + installed = false; + }; + + self.mockDate = function(initialDate) { + mockDate.install(initialDate); + }; + + self.setTimeout = function(fn, delay, params) { + if (legacyIE()) { + if (arguments.length > 2) { + throw new Error('IE < 9 cannot support extra params to setTimeout without a polyfill'); + } + return timer.setTimeout(fn, delay); + } + return Function.prototype.apply.apply(timer.setTimeout, [global, arguments]); + }; + + self.setInterval = function(fn, delay, params) { + if (legacyIE()) { + if (arguments.length > 2) { + throw new Error('IE < 9 cannot support extra params to setInterval without a polyfill'); + } + return timer.setInterval(fn, delay); + } + return Function.prototype.apply.apply(timer.setInterval, [global, arguments]); + }; + + self.clearTimeout = function(id) { + return Function.prototype.call.apply(timer.clearTimeout, [global, id]); + }; + + self.clearInterval = function(id) { + return Function.prototype.call.apply(timer.clearInterval, [global, id]); + }; + + self.tick = function(millis) { + if (installed) { + mockDate.tick(millis); + delayedFunctionScheduler.tick(millis); + } else { + throw new Error('Mock clock is not installed, use jasmine.clock().install()'); + } + }; + + return self; + + function legacyIE() { + //if these methods are polyfilled, apply will be present + return !(realTimingFunctions.setTimeout || realTimingFunctions.setInterval).apply; + } + + function replace(dest, source) { + for (var prop in source) { + dest[prop] = source[prop]; + } + } + + function setTimeout(fn, delay) { + return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2)); + } + + function clearTimeout(id) { + return delayedFunctionScheduler.removeFunctionWithId(id); + } + + function setInterval(fn, interval) { + return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true); + } + + function clearInterval(id) { + return delayedFunctionScheduler.removeFunctionWithId(id); + } + + function argSlice(argsObj, n) { + return Array.prototype.slice.call(argsObj, n); + } + } + + return Clock; +}; + +getJasmineRequireObj().DelayedFunctionScheduler = function() { + function DelayedFunctionScheduler() { + var self = this; + var scheduledLookup = []; + var scheduledFunctions = {}; + var currentTime = 0; + var delayedFnCount = 0; + + self.tick = function(millis) { + millis = millis || 0; + var endTime = currentTime + millis; + + runScheduledFunctions(endTime); + currentTime = endTime; + }; + + self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) { + var f; + if (typeof(funcToCall) === 'string') { + /* jshint evil: true */ + f = function() { return eval(funcToCall); }; + /* jshint evil: false */ + } else { + f = funcToCall; + } + + millis = millis || 0; + timeoutKey = timeoutKey || ++delayedFnCount; + runAtMillis = runAtMillis || (currentTime + millis); + + var funcToSchedule = { + runAtMillis: runAtMillis, + funcToCall: f, + recurring: recurring, + params: params, + timeoutKey: timeoutKey, + millis: millis + }; + + if (runAtMillis in scheduledFunctions) { + scheduledFunctions[runAtMillis].push(funcToSchedule); + } else { + scheduledFunctions[runAtMillis] = [funcToSchedule]; + scheduledLookup.push(runAtMillis); + scheduledLookup.sort(function (a, b) { + return a - b; + }); + } + + return timeoutKey; + }; + + self.removeFunctionWithId = function(timeoutKey) { + for (var runAtMillis in scheduledFunctions) { + var funcs = scheduledFunctions[runAtMillis]; + var i = indexOfFirstToPass(funcs, function (func) { + return func.timeoutKey === timeoutKey; + }); + + if (i > -1) { + if (funcs.length === 1) { + delete scheduledFunctions[runAtMillis]; + deleteFromLookup(runAtMillis); + } else { + funcs.splice(i, 1); + } + + // intervals get rescheduled when executed, so there's never more + // than a single scheduled function with a given timeoutKey + break; + } + } + }; + + self.reset = function() { + currentTime = 0; + scheduledLookup = []; + scheduledFunctions = {}; + delayedFnCount = 0; + }; + + return self; + + function indexOfFirstToPass(array, testFn) { + var index = -1; + + for (var i = 0; i < array.length; ++i) { + if (testFn(array[i])) { + index = i; + break; + } + } + + return index; + } + + function deleteFromLookup(key) { + var value = Number(key); + var i = indexOfFirstToPass(scheduledLookup, function (millis) { + return millis === value; + }); + + if (i > -1) { + scheduledLookup.splice(i, 1); + } + } + + function reschedule(scheduledFn) { + self.scheduleFunction(scheduledFn.funcToCall, + scheduledFn.millis, + scheduledFn.params, + true, + scheduledFn.timeoutKey, + scheduledFn.runAtMillis + scheduledFn.millis); + } + + function runScheduledFunctions(endTime) { + if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) { + return; + } + + do { + currentTime = scheduledLookup.shift(); + + var funcsToRun = scheduledFunctions[currentTime]; + delete scheduledFunctions[currentTime]; + + for (var i = 0; i < funcsToRun.length; ++i) { + var funcToRun = funcsToRun[i]; + + if (funcToRun.recurring) { + reschedule(funcToRun); + } + + funcToRun.funcToCall.apply(null, funcToRun.params || []); + } + } while (scheduledLookup.length > 0 && + // checking first if we're out of time prevents setTimeout(0) + // scheduled in a funcToRun from forcing an extra iteration + currentTime !== endTime && + scheduledLookup[0] <= endTime); + } + } + + return DelayedFunctionScheduler; +}; + +getJasmineRequireObj().ExceptionFormatter = function() { + function ExceptionFormatter() { + this.message = function(error) { + var message = ''; + + if (error.name && error.message) { + message += error.name + ': ' + error.message; + } else { + message += error.toString() + ' thrown'; + } + + if (error.fileName || error.sourceURL) { + message += ' in ' + (error.fileName || error.sourceURL); + } + + if (error.line || error.lineNumber) { + message += ' (line ' + (error.line || error.lineNumber) + ')'; + } + + return message; + }; + + this.stack = function(error) { + return error ? error.stack : null; + }; + } + + return ExceptionFormatter; +}; + +getJasmineRequireObj().Expectation = function() { + + function Expectation(options) { + this.util = options.util || { buildFailureMessage: function() {} }; + this.customEqualityTesters = options.customEqualityTesters || []; + this.actual = options.actual; + this.addExpectationResult = options.addExpectationResult || function(){}; + this.isNot = options.isNot; + + var customMatchers = options.customMatchers || {}; + for (var matcherName in customMatchers) { + this[matcherName] = Expectation.prototype.wrapCompare(matcherName, customMatchers[matcherName]); + } + } + + Expectation.prototype.wrapCompare = function(name, matcherFactory) { + return function() { + var args = Array.prototype.slice.call(arguments, 0), + expected = args.slice(0), + message = ''; + + args.unshift(this.actual); + + var matcher = matcherFactory(this.util, this.customEqualityTesters), + matcherCompare = matcher.compare; + + function defaultNegativeCompare() { + var result = matcher.compare.apply(null, args); + result.pass = !result.pass; + return result; + } + + if (this.isNot) { + matcherCompare = matcher.negativeCompare || defaultNegativeCompare; + } + + var result = matcherCompare.apply(null, args); + + if (!result.pass) { + if (!result.message) { + args.unshift(this.isNot); + args.unshift(name); + message = this.util.buildFailureMessage.apply(null, args); + } else { + if (Object.prototype.toString.apply(result.message) === '[object Function]') { + message = result.message(); + } else { + message = result.message; + } + } + } + + if (expected.length == 1) { + expected = expected[0]; + } + + // TODO: how many of these params are needed? + this.addExpectationResult( + result.pass, + { + matcherName: name, + passed: result.pass, + message: message, + actual: this.actual, + expected: expected // TODO: this may need to be arrayified/sliced + } + ); + }; + }; + + Expectation.addCoreMatchers = function(matchers) { + var prototype = Expectation.prototype; + for (var matcherName in matchers) { + var matcher = matchers[matcherName]; + prototype[matcherName] = prototype.wrapCompare(matcherName, matcher); + } + }; + + Expectation.Factory = function(options) { + options = options || {}; + + var expect = new Expectation(options); + + // TODO: this would be nice as its own Object - NegativeExpectation + // TODO: copy instead of mutate options + options.isNot = true; + expect.not = new Expectation(options); + + return expect; + }; + + return Expectation; +}; + +//TODO: expectation result may make more sense as a presentation of an expectation. +getJasmineRequireObj().buildExpectationResult = function() { + function buildExpectationResult(options) { + var messageFormatter = options.messageFormatter || function() {}, + stackFormatter = options.stackFormatter || function() {}; + + var result = { + matcherName: options.matcherName, + message: message(), + stack: stack(), + passed: options.passed + }; + + if(!result.passed) { + result.expected = options.expected; + result.actual = options.actual; + } + + return result; + + function message() { + if (options.passed) { + return 'Passed.'; + } else if (options.message) { + return options.message; + } else if (options.error) { + return messageFormatter(options.error); + } + return ''; + } + + function stack() { + if (options.passed) { + return ''; + } + + var error = options.error; + if (!error) { + try { + throw new Error(message()); + } catch (e) { + error = e; + } + } + return stackFormatter(error); + } + } + + return buildExpectationResult; +}; + +getJasmineRequireObj().MockDate = function() { + function MockDate(global) { + var self = this; + var currentTime = 0; + + if (!global || !global.Date) { + self.install = function() {}; + self.tick = function() {}; + self.uninstall = function() {}; + return self; + } + + var GlobalDate = global.Date; + + self.install = function(mockDate) { + if (mockDate instanceof GlobalDate) { + currentTime = mockDate.getTime(); + } else { + currentTime = new GlobalDate().getTime(); + } + + global.Date = FakeDate; + }; + + self.tick = function(millis) { + millis = millis || 0; + currentTime = currentTime + millis; + }; + + self.uninstall = function() { + currentTime = 0; + global.Date = GlobalDate; + }; + + createDateProperties(); + + return self; + + function FakeDate() { + switch(arguments.length) { + case 0: + return new GlobalDate(currentTime); + case 1: + return new GlobalDate(arguments[0]); + case 2: + return new GlobalDate(arguments[0], arguments[1]); + case 3: + return new GlobalDate(arguments[0], arguments[1], arguments[2]); + case 4: + return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3]); + case 5: + return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], + arguments[4]); + case 6: + return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], + arguments[4], arguments[5]); + case 7: + return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3], + arguments[4], arguments[5], arguments[6]); + } + } + + function createDateProperties() { + FakeDate.prototype = GlobalDate.prototype; + + FakeDate.now = function() { + if (GlobalDate.now) { + return currentTime; + } else { + throw new Error('Browser does not support Date.now()'); + } + }; + + FakeDate.toSource = GlobalDate.toSource; + FakeDate.toString = GlobalDate.toString; + FakeDate.parse = GlobalDate.parse; + FakeDate.UTC = GlobalDate.UTC; + } + } + + return MockDate; +}; + +getJasmineRequireObj().ObjectContaining = function(j$) { + + function ObjectContaining(sample) { + this.sample = sample; + } + + ObjectContaining.prototype.jasmineMatches = function(other, mismatchKeys, mismatchValues) { + if (typeof(this.sample) !== 'object') { throw new Error('You must provide an object to objectContaining, not \''+this.sample+'\'.'); } + + mismatchKeys = mismatchKeys || []; + mismatchValues = mismatchValues || []; + + var hasKey = function(obj, keyName) { + return obj !== null && !j$.util.isUndefined(obj[keyName]); + }; + + for (var property in this.sample) { + if (!hasKey(other, property) && hasKey(this.sample, property)) { + mismatchKeys.push('expected has key \'' + property + '\', but missing from actual.'); + } + else if (!j$.matchersUtil.equals(other[property], this.sample[property])) { + mismatchValues.push('\'' + property + '\' was \'' + (other[property] ? j$.util.htmlEscape(other[property].toString()) : other[property]) + '\' in actual, but was \'' + (this.sample[property] ? j$.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + '\' in expected.'); + } + } + + return (mismatchKeys.length === 0 && mismatchValues.length === 0); + }; + + ObjectContaining.prototype.jasmineToString = function() { + return ''; + }; + + return ObjectContaining; +}; + +getJasmineRequireObj().pp = function(j$) { + + function PrettyPrinter() { + this.ppNestLevel_ = 0; + this.seen = []; + } + + PrettyPrinter.prototype.format = function(value) { + this.ppNestLevel_++; + try { + if (j$.util.isUndefined(value)) { + this.emitScalar('undefined'); + } else if (value === null) { + this.emitScalar('null'); + } else if (value === 0 && 1/value === -Infinity) { + this.emitScalar('-0'); + } else if (value === j$.getGlobal()) { + this.emitScalar(''); + } else if (value.jasmineToString) { + this.emitScalar(value.jasmineToString()); + } else if (typeof value === 'string') { + this.emitString(value); + } else if (j$.isSpy(value)) { + this.emitScalar('spy on ' + value.and.identity()); + } else if (value instanceof RegExp) { + this.emitScalar(value.toString()); + } else if (typeof value === 'function') { + this.emitScalar('Function'); + } else if (typeof value.nodeType === 'number') { + this.emitScalar('HTMLNode'); + } else if (value instanceof Date) { + this.emitScalar('Date(' + value + ')'); + } else if (j$.util.arrayContains(this.seen, value)) { + this.emitScalar(''); + } else if (j$.isArray_(value) || j$.isA_('Object', value)) { + this.seen.push(value); + if (j$.isArray_(value)) { + this.emitArray(value); + } else { + this.emitObject(value); + } + this.seen.pop(); + } else { + this.emitScalar(value.toString()); + } + } finally { + this.ppNestLevel_--; + } + }; + + PrettyPrinter.prototype.iterateObject = function(obj, fn) { + for (var property in obj) { + if (!Object.prototype.hasOwnProperty.call(obj, property)) { continue; } + fn(property, obj.__lookupGetter__ ? (!j$.util.isUndefined(obj.__lookupGetter__(property)) && + obj.__lookupGetter__(property) !== null) : false); + } + }; + + PrettyPrinter.prototype.emitArray = j$.unimplementedMethod_; + PrettyPrinter.prototype.emitObject = j$.unimplementedMethod_; + PrettyPrinter.prototype.emitScalar = j$.unimplementedMethod_; + PrettyPrinter.prototype.emitString = j$.unimplementedMethod_; + + function StringPrettyPrinter() { + PrettyPrinter.call(this); + + this.string = ''; + } + + j$.util.inherit(StringPrettyPrinter, PrettyPrinter); + + StringPrettyPrinter.prototype.emitScalar = function(value) { + this.append(value); + }; + + StringPrettyPrinter.prototype.emitString = function(value) { + this.append('\'' + value + '\''); + }; + + StringPrettyPrinter.prototype.emitArray = function(array) { + if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { + this.append('Array'); + return; + } + var length = Math.min(array.length, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH); + this.append('[ '); + for (var i = 0; i < length; i++) { + if (i > 0) { + this.append(', '); + } + this.format(array[i]); + } + if(array.length > length){ + this.append(', ...'); + } + this.append(' ]'); + }; + + StringPrettyPrinter.prototype.emitObject = function(obj) { + if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) { + this.append('Object'); + return; + } + + var self = this; + this.append('{ '); + var first = true; + + this.iterateObject(obj, function(property, isGetter) { + if (first) { + first = false; + } else { + self.append(', '); + } + + self.append(property); + self.append(': '); + if (isGetter) { + self.append(''); + } else { + self.format(obj[property]); + } + }); + + this.append(' }'); + }; + + StringPrettyPrinter.prototype.append = function(value) { + this.string += value; + }; + + return function(value) { + var stringPrettyPrinter = new StringPrettyPrinter(); + stringPrettyPrinter.format(value); + return stringPrettyPrinter.string; + }; +}; + +getJasmineRequireObj().QueueRunner = function(j$) { + + function once(fn) { + var called = false; + return function() { + if (!called) { + called = true; + fn(); + } + }; + } + + function QueueRunner(attrs) { + this.queueableFns = attrs.queueableFns || []; + this.onComplete = attrs.onComplete || function() {}; + this.clearStack = attrs.clearStack || function(fn) {fn();}; + this.onException = attrs.onException || function() {}; + this.catchException = attrs.catchException || function() { return true; }; + this.userContext = attrs.userContext || {}; + this.timer = attrs.timeout || {setTimeout: setTimeout, clearTimeout: clearTimeout}; + this.fail = attrs.fail || function() {}; + } + + QueueRunner.prototype.execute = function() { + this.run(this.queueableFns, 0); + }; + + QueueRunner.prototype.run = function(queueableFns, recursiveIndex) { + var length = queueableFns.length, + self = this, + iterativeIndex; + + + for(iterativeIndex = recursiveIndex; iterativeIndex < length; iterativeIndex++) { + var queueableFn = queueableFns[iterativeIndex]; + if (queueableFn.fn.length > 0) { + return attemptAsync(queueableFn); + } else { + attemptSync(queueableFn); + } + } + + var runnerDone = iterativeIndex >= length; + + if (runnerDone) { + this.clearStack(this.onComplete); + } + + function attemptSync(queueableFn) { + try { + queueableFn.fn.call(self.userContext); + } catch (e) { + handleException(e, queueableFn); + } + } + + function attemptAsync(queueableFn) { + var clearTimeout = function () { + Function.prototype.apply.apply(self.timer.clearTimeout, [j$.getGlobal(), [timeoutId]]); + }, + next = once(function () { + clearTimeout(timeoutId); + self.run(queueableFns, iterativeIndex + 1); + }), + timeoutId; + + next.fail = function() { + self.fail.apply(null, arguments); + next(); + }; + + if (queueableFn.timeout) { + timeoutId = Function.prototype.apply.apply(self.timer.setTimeout, [j$.getGlobal(), [function() { + var error = new Error('Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.'); + onException(error, queueableFn); + next(); + }, queueableFn.timeout()]]); + } + + try { + queueableFn.fn.call(self.userContext, next); + } catch (e) { + handleException(e, queueableFn); + next(); + } + } + + function onException(e, queueableFn) { + self.onException(e); + } + + function handleException(e, queueableFn) { + onException(e, queueableFn); + if (!self.catchException(e)) { + //TODO: set a var when we catch an exception and + //use a finally block to close the loop in a nice way.. + throw e; + } + } + }; + + return QueueRunner; +}; + +getJasmineRequireObj().ReportDispatcher = function() { + function ReportDispatcher(methods) { + + var dispatchedMethods = methods || []; + + for (var i = 0; i < dispatchedMethods.length; i++) { + var method = dispatchedMethods[i]; + this[method] = (function(m) { + return function() { + dispatch(m, arguments); + }; + }(method)); + } + + var reporters = []; + + this.addReporter = function(reporter) { + reporters.push(reporter); + }; + + return this; + + function dispatch(method, args) { + for (var i = 0; i < reporters.length; i++) { + var reporter = reporters[i]; + if (reporter[method]) { + reporter[method].apply(reporter, args); + } + } + } + } + + return ReportDispatcher; +}; + + +getJasmineRequireObj().SpyRegistry = function(j$) { + + function SpyRegistry(options) { + options = options || {}; + var currentSpies = options.currentSpies || function() { return []; }; + + this.spyOn = function(obj, methodName) { + if (j$.util.isUndefined(obj)) { + throw new Error('spyOn could not find an object to spy upon for ' + methodName + '()'); + } + + if (j$.util.isUndefined(obj[methodName])) { + throw new Error(methodName + '() method does not exist'); + } + + if (obj[methodName] && j$.isSpy(obj[methodName])) { + //TODO?: should this return the current spy? Downside: may cause user confusion about spy state + throw new Error(methodName + ' has already been spied upon'); + } + + var spy = j$.createSpy(methodName, obj[methodName]); + + currentSpies().push({ + spy: spy, + baseObj: obj, + methodName: methodName, + originalValue: obj[methodName] + }); + + obj[methodName] = spy; + + return spy; + }; + + this.clearSpies = function() { + var spies = currentSpies(); + for (var i = 0; i < spies.length; i++) { + var spyEntry = spies[i]; + spyEntry.baseObj[spyEntry.methodName] = spyEntry.originalValue; + } + }; + } + + return SpyRegistry; +}; + +getJasmineRequireObj().SpyStrategy = function() { + + function SpyStrategy(options) { + options = options || {}; + + var identity = options.name || 'unknown', + originalFn = options.fn || function() {}, + getSpy = options.getSpy || function() {}, + plan = function() {}; + + this.identity = function() { + return identity; + }; + + this.exec = function() { + return plan.apply(this, arguments); + }; + + this.callThrough = function() { + plan = originalFn; + return getSpy(); + }; + + this.returnValue = function(value) { + plan = function() { + return value; + }; + return getSpy(); + }; + + this.returnValues = function() { + var values = Array.prototype.slice.call(arguments); + plan = function () { + return values.shift(); + }; + return getSpy(); + }; + + this.throwError = function(something) { + var error = (something instanceof Error) ? something : new Error(something); + plan = function() { + throw error; + }; + return getSpy(); + }; + + this.callFake = function(fn) { + plan = fn; + return getSpy(); + }; + + this.stub = function(fn) { + plan = function() {}; + return getSpy(); + }; + } + + return SpyStrategy; +}; + +getJasmineRequireObj().Suite = function() { + function Suite(attrs) { + this.env = attrs.env; + this.id = attrs.id; + this.parentSuite = attrs.parentSuite; + this.description = attrs.description; + this.onStart = attrs.onStart || function() {}; + this.resultCallback = attrs.resultCallback || function() {}; + this.clearStack = attrs.clearStack || function(fn) {fn();}; + this.expectationFactory = attrs.expectationFactory; + this.expectationResultFactory = attrs.expectationResultFactory; + + this.beforeFns = []; + this.afterFns = []; + this.beforeAllFns = []; + this.afterAllFns = []; + this.queueRunner = attrs.queueRunner || function() {}; + this.disabled = false; + + this.children = []; + + this.result = { + id: this.id, + description: this.description, + fullName: this.getFullName(), + failedExpectations: [] + }; + } + + Suite.prototype.expect = function(actual) { + return this.expectationFactory(actual, this); + }; + + Suite.prototype.getFullName = function() { + var fullName = this.description; + for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) { + if (parentSuite.parentSuite) { + fullName = parentSuite.description + ' ' + fullName; + } + } + return fullName; + }; + + Suite.prototype.disable = function() { + this.disabled = true; + }; + + Suite.prototype.beforeEach = function(fn) { + this.beforeFns.unshift(fn); + }; + + Suite.prototype.beforeAll = function(fn) { + this.beforeAllFns.push(fn); + }; + + Suite.prototype.afterEach = function(fn) { + this.afterFns.unshift(fn); + }; + + Suite.prototype.afterAll = function(fn) { + this.afterAllFns.push(fn); + }; + + Suite.prototype.addChild = function(child) { + this.children.push(child); + }; + + Suite.prototype.status = function() { + if (this.disabled) { + return 'disabled'; + } + + if (this.result.failedExpectations.length > 0) { + return 'failed'; + } else { + return 'finished'; + } + }; + + Suite.prototype.execute = function(onComplete) { + var self = this; + + this.onStart(this); + + if (this.disabled) { + complete(); + return; + } + + var allFns = []; + + for (var i = 0; i < this.children.length; i++) { + allFns.push(wrapChildAsAsync(this.children[i])); + } + + if (this.isExecutable()) { + allFns = this.beforeAllFns.concat(allFns); + allFns = allFns.concat(this.afterAllFns); + } + + this.queueRunner({ + queueableFns: allFns, + onComplete: complete, + userContext: this.sharedUserContext(), + onException: function() { self.onException.apply(self, arguments); } + }); + + function complete() { + self.result.status = self.status(); + self.resultCallback(self.result); + + if (onComplete) { + onComplete(); + } + } + + function wrapChildAsAsync(child) { + return { fn: function(done) { child.execute(done); } }; + } + }; + + Suite.prototype.isExecutable = function() { + var foundActive = false; + for(var i = 0; i < this.children.length; i++) { + if(this.children[i].isExecutable()) { + foundActive = true; + break; + } + } + return foundActive; + }; + + Suite.prototype.sharedUserContext = function() { + if (!this.sharedContext) { + this.sharedContext = this.parentSuite ? clone(this.parentSuite.sharedUserContext()) : {}; + } + + return this.sharedContext; + }; + + Suite.prototype.clonedSharedUserContext = function() { + return clone(this.sharedUserContext()); + }; + + Suite.prototype.onException = function() { + if(isAfterAll(this.children)) { + var data = { + matcherName: '', + passed: false, + expected: '', + actual: '', + error: arguments[0] + }; + this.result.failedExpectations.push(this.expectationResultFactory(data)); + } else { + for (var i = 0; i < this.children.length; i++) { + var child = this.children[i]; + child.onException.apply(child, arguments); + } + } + }; + + Suite.prototype.addExpectationResult = function () { + if(isAfterAll(this.children) && isFailure(arguments)){ + var data = arguments[1]; + this.result.failedExpectations.push(this.expectationResultFactory(data)); + } else { + for (var i = 0; i < this.children.length; i++) { + var child = this.children[i]; + child.addExpectationResult.apply(child, arguments); + } + } + }; + + function isAfterAll(children) { + return children && children[0].result.status; + } + + function isFailure(args) { + return !args[0]; + } + + function clone(obj) { + var clonedObj = {}; + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + clonedObj[prop] = obj[prop]; + } + } + + return clonedObj; + } + + return Suite; +}; + +if (typeof window == void 0 && typeof exports == 'object') { + exports.Suite = jasmineRequire.Suite; +} + +getJasmineRequireObj().Timer = function() { + var defaultNow = (function(Date) { + return function() { return new Date().getTime(); }; + })(Date); + + function Timer(options) { + options = options || {}; + + var now = options.now || defaultNow, + startTime; + + this.start = function() { + startTime = now(); + }; + + this.elapsed = function() { + return now() - startTime; + }; + } + + return Timer; +}; + +getJasmineRequireObj().matchersUtil = function(j$) { + // TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter? + + return { + equals: function(a, b, customTesters) { + customTesters = customTesters || []; + + return eq(a, b, [], [], customTesters); + }, + + contains: function(haystack, needle, customTesters) { + customTesters = customTesters || []; + + if ((Object.prototype.toString.apply(haystack) === '[object Array]') || + (!!haystack && !haystack.indexOf)) + { + for (var i = 0; i < haystack.length; i++) { + if (eq(haystack[i], needle, [], [], customTesters)) { + return true; + } + } + return false; + } + + return !!haystack && haystack.indexOf(needle) >= 0; + }, + + buildFailureMessage: function() { + var args = Array.prototype.slice.call(arguments, 0), + matcherName = args[0], + isNot = args[1], + actual = args[2], + expected = args.slice(3), + englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); }); + + var message = 'Expected ' + + j$.pp(actual) + + (isNot ? ' not ' : ' ') + + englishyPredicate; + + if (expected.length > 0) { + for (var i = 0; i < expected.length; i++) { + if (i > 0) { + message += ','; + } + message += ' ' + j$.pp(expected[i]); + } + } + + return message + '.'; + } + }; + + // Equality function lovingly adapted from isEqual in + // [Underscore](http://underscorejs.org) + function eq(a, b, aStack, bStack, customTesters) { + var result = true; + + for (var i = 0; i < customTesters.length; i++) { + var customTesterResult = customTesters[i](a, b); + if (!j$.util.isUndefined(customTesterResult)) { + return customTesterResult; + } + } + + if (a instanceof j$.Any) { + result = a.jasmineMatches(b); + if (result) { + return true; + } + } + + if (b instanceof j$.Any) { + result = b.jasmineMatches(a); + if (result) { + return true; + } + } + + if (b instanceof j$.ObjectContaining) { + result = b.jasmineMatches(a); + if (result) { + return true; + } + } + + if (a instanceof Error && b instanceof Error) { + return a.message == b.message; + } + + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) { return a !== 0 || 1 / a == 1 / b; } + // A strict comparison is necessary because `null == undefined`. + if (a === null || b === null) { return a === b; } + var className = Object.prototype.toString.call(a); + if (className != Object.prototype.toString.call(b)) { return false; } + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') { return false; } + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) { return bStack[length] == b; } + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack, customTesters))) { break; } + } + } + } else { + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(isFunction(aCtor) && (aCtor instanceof aCtor) && + isFunction(bCtor) && (bCtor instanceof bCtor))) { + return false; + } + // Deep compare objects. + for (var key in a) { + if (has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = has(b, key) && eq(a[key], b[key], aStack, bStack, customTesters))) { break; } + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (has(b, key) && !(size--)) { break; } + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + + return result; + + function has(obj, key) { + return obj.hasOwnProperty(key); + } + + function isFunction(obj) { + return typeof obj === 'function'; + } + } +}; + +getJasmineRequireObj().toBe = function() { + function toBe() { + return { + compare: function(actual, expected) { + return { + pass: actual === expected + }; + } + }; + } + + return toBe; +}; + +getJasmineRequireObj().toBeCloseTo = function() { + + function toBeCloseTo() { + return { + compare: function(actual, expected, precision) { + if (precision !== 0) { + precision = precision || 2; + } + + return { + pass: Math.abs(expected - actual) < (Math.pow(10, -precision) / 2) + }; + } + }; + } + + return toBeCloseTo; +}; + +getJasmineRequireObj().toBeDefined = function() { + function toBeDefined() { + return { + compare: function(actual) { + return { + pass: (void 0 !== actual) + }; + } + }; + } + + return toBeDefined; +}; + +getJasmineRequireObj().toBeFalsy = function() { + function toBeFalsy() { + return { + compare: function(actual) { + return { + pass: !!!actual + }; + } + }; + } + + return toBeFalsy; +}; + +getJasmineRequireObj().toBeGreaterThan = function() { + + function toBeGreaterThan() { + return { + compare: function(actual, expected) { + return { + pass: actual > expected + }; + } + }; + } + + return toBeGreaterThan; +}; + + +getJasmineRequireObj().toBeLessThan = function() { + function toBeLessThan() { + return { + + compare: function(actual, expected) { + return { + pass: actual < expected + }; + } + }; + } + + return toBeLessThan; +}; +getJasmineRequireObj().toBeNaN = function(j$) { + + function toBeNaN() { + return { + compare: function(actual) { + var result = { + pass: (actual !== actual) + }; + + if (result.pass) { + result.message = 'Expected actual not to be NaN.'; + } else { + result.message = function() { return 'Expected ' + j$.pp(actual) + ' to be NaN.'; }; + } + + return result; + } + }; + } + + return toBeNaN; +}; + +getJasmineRequireObj().toBeNull = function() { + + function toBeNull() { + return { + compare: function(actual) { + return { + pass: actual === null + }; + } + }; + } + + return toBeNull; +}; + +getJasmineRequireObj().toBeTruthy = function() { + + function toBeTruthy() { + return { + compare: function(actual) { + return { + pass: !!actual + }; + } + }; + } + + return toBeTruthy; +}; + +getJasmineRequireObj().toBeUndefined = function() { + + function toBeUndefined() { + return { + compare: function(actual) { + return { + pass: void 0 === actual + }; + } + }; + } + + return toBeUndefined; +}; + +getJasmineRequireObj().toContain = function() { + function toContain(util, customEqualityTesters) { + customEqualityTesters = customEqualityTesters || []; + + return { + compare: function(actual, expected) { + + return { + pass: util.contains(actual, expected, customEqualityTesters) + }; + } + }; + } + + return toContain; +}; + +getJasmineRequireObj().toEqual = function() { + + function toEqual(util, customEqualityTesters) { + customEqualityTesters = customEqualityTesters || []; + + return { + compare: function(actual, expected) { + var result = { + pass: false + }; + + result.pass = util.equals(actual, expected, customEqualityTesters); + + return result; + } + }; + } + + return toEqual; +}; + +getJasmineRequireObj().toHaveBeenCalled = function(j$) { + + function toHaveBeenCalled() { + return { + compare: function(actual) { + var result = {}; + + if (!j$.isSpy(actual)) { + throw new Error('Expected a spy, but got ' + j$.pp(actual) + '.'); + } + + if (arguments.length > 1) { + throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith'); + } + + result.pass = actual.calls.any(); + + result.message = result.pass ? + 'Expected spy ' + actual.and.identity() + ' not to have been called.' : + 'Expected spy ' + actual.and.identity() + ' to have been called.'; + + return result; + } + }; + } + + return toHaveBeenCalled; +}; + +getJasmineRequireObj().toHaveBeenCalledWith = function(j$) { + + function toHaveBeenCalledWith(util, customEqualityTesters) { + return { + compare: function() { + var args = Array.prototype.slice.call(arguments, 0), + actual = args[0], + expectedArgs = args.slice(1), + result = { pass: false }; + + if (!j$.isSpy(actual)) { + throw new Error('Expected a spy, but got ' + j$.pp(actual) + '.'); + } + + if (!actual.calls.any()) { + result.message = function() { return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + j$.pp(expectedArgs) + ' but it was never called.'; }; + return result; + } + + if (util.contains(actual.calls.allArgs(), expectedArgs, customEqualityTesters)) { + result.pass = true; + result.message = function() { return 'Expected spy ' + actual.and.identity() + ' not to have been called with ' + j$.pp(expectedArgs) + ' but it was.'; }; + } else { + result.message = function() { return 'Expected spy ' + actual.and.identity() + ' to have been called with ' + j$.pp(expectedArgs) + ' but actual calls were ' + j$.pp(actual.calls.allArgs()).replace(/^\[ | \]$/g, '') + '.'; }; + } + + return result; + } + }; + } + + return toHaveBeenCalledWith; +}; + +getJasmineRequireObj().toMatch = function() { + + function toMatch() { + return { + compare: function(actual, expected) { + var regexp = new RegExp(expected); + + return { + pass: regexp.test(actual) + }; + } + }; + } + + return toMatch; +}; + +getJasmineRequireObj().toThrow = function(j$) { + + function toThrow(util) { + return { + compare: function(actual, expected) { + var result = { pass: false }, + threw = false, + thrown; + + if (typeof actual != 'function') { + throw new Error('Actual is not a Function'); + } + + try { + actual(); + } catch (e) { + threw = true; + thrown = e; + } + + if (!threw) { + result.message = 'Expected function to throw an exception.'; + return result; + } + + if (arguments.length == 1) { + result.pass = true; + result.message = function() { return 'Expected function not to throw, but it threw ' + j$.pp(thrown) + '.'; }; + + return result; + } + + if (util.equals(thrown, expected)) { + result.pass = true; + result.message = function() { return 'Expected function not to throw ' + j$.pp(expected) + '.'; }; + } else { + result.message = function() { return 'Expected function to throw ' + j$.pp(expected) + ', but it threw ' + j$.pp(thrown) + '.'; }; + } + + return result; + } + }; + } + + return toThrow; +}; + +getJasmineRequireObj().toThrowError = function(j$) { + function toThrowError (util) { + return { + compare: function(actual) { + var threw = false, + pass = {pass: true}, + fail = {pass: false}, + thrown; + + if (typeof actual != 'function') { + throw new Error('Actual is not a Function'); + } + + var errorMatcher = getMatcher.apply(null, arguments); + + try { + actual(); + } catch (e) { + threw = true; + thrown = e; + } + + if (!threw) { + fail.message = 'Expected function to throw an Error.'; + return fail; + } + + if (!(thrown instanceof Error)) { + fail.message = function() { return 'Expected function to throw an Error, but it threw ' + j$.pp(thrown) + '.'; }; + return fail; + } + + if (errorMatcher.hasNoSpecifics()) { + pass.message = 'Expected function not to throw an Error, but it threw ' + fnNameFor(thrown) + '.'; + return pass; + } + + if (errorMatcher.matches(thrown)) { + pass.message = function() { + return 'Expected function not to throw ' + errorMatcher.errorTypeDescription + errorMatcher.messageDescription() + '.'; + }; + return pass; + } else { + fail.message = function() { + return 'Expected function to throw ' + errorMatcher.errorTypeDescription + errorMatcher.messageDescription() + + ', but it threw ' + errorMatcher.thrownDescription(thrown) + '.'; + }; + return fail; + } + } + }; + + function getMatcher() { + var expected = null, + errorType = null; + + if (arguments.length == 2) { + expected = arguments[1]; + if (isAnErrorType(expected)) { + errorType = expected; + expected = null; + } + } else if (arguments.length > 2) { + errorType = arguments[1]; + expected = arguments[2]; + if (!isAnErrorType(errorType)) { + throw new Error('Expected error type is not an Error.'); + } + } + + if (expected && !isStringOrRegExp(expected)) { + if (errorType) { + throw new Error('Expected error message is not a string or RegExp.'); + } else { + throw new Error('Expected is not an Error, string, or RegExp.'); + } + } + + function messageMatch(message) { + if (typeof expected == 'string') { + return expected == message; + } else { + return expected.test(message); + } + } + + return { + errorTypeDescription: errorType ? fnNameFor(errorType) : 'an exception', + thrownDescription: function(thrown) { + var thrownName = errorType ? fnNameFor(thrown.constructor) : 'an exception', + thrownMessage = ''; + + if (expected) { + thrownMessage = ' with message ' + j$.pp(thrown.message); + } + + return thrownName + thrownMessage; + }, + messageDescription: function() { + if (expected === null) { + return ''; + } else if (expected instanceof RegExp) { + return ' with a message matching ' + j$.pp(expected); + } else { + return ' with message ' + j$.pp(expected); + } + }, + hasNoSpecifics: function() { + return expected === null && errorType === null; + }, + matches: function(error) { + return (errorType === null || error.constructor === errorType) && + (expected === null || messageMatch(error.message)); + } + }; + } + + function fnNameFor(func) { + return func.name || func.toString().match(/^\s*function\s*(\w*)\s*\(/)[1]; + } + + function isStringOrRegExp(potential) { + return potential instanceof RegExp || (typeof potential == 'string'); + } + + function isAnErrorType(type) { + if (typeof type !== 'function') { + return false; + } + + var Surrogate = function() {}; + Surrogate.prototype = type.prototype; + return (new Surrogate()) instanceof Error; + } + } + + return toThrowError; +}; + +getJasmineRequireObj().interface = function(jasmine, env) { + var jasmineInterface = { + describe: function(description, specDefinitions) { + return env.describe(description, specDefinitions); + }, + + xdescribe: function(description, specDefinitions) { + return env.xdescribe(description, specDefinitions); + }, + + fdescribe: function(description, specDefinitions) { + return env.fdescribe(description, specDefinitions); + }, + + it: function(desc, func) { + return env.it(desc, func); + }, + + xit: function(desc, func) { + return env.xit(desc, func); + }, + + fit: function(desc, func) { + return env.fit(desc, func); + }, + + beforeEach: function(beforeEachFunction) { + return env.beforeEach(beforeEachFunction); + }, + + afterEach: function(afterEachFunction) { + return env.afterEach(afterEachFunction); + }, + + beforeAll: function(beforeAllFunction) { + return env.beforeAll(beforeAllFunction); + }, + + afterAll: function(afterAllFunction) { + return env.afterAll(afterAllFunction); + }, + + expect: function(actual) { + return env.expect(actual); + }, + + pending: function() { + return env.pending(); + }, + + fail: function() { + return env.fail.apply(env, arguments); + }, + + spyOn: function(obj, methodName) { + return env.spyOn(obj, methodName); + }, + + jsApiReporter: new jasmine.JsApiReporter({ + timer: new jasmine.Timer() + }), + + jasmine: jasmine + }; + + jasmine.addCustomEqualityTester = function(tester) { + env.addCustomEqualityTester(tester); + }; + + jasmine.addMatchers = function(matchers) { + return env.addMatchers(matchers); + }; + + jasmine.clock = function() { + return env.clock; + }; + + return jasmineInterface; +}; + +getJasmineRequireObj().version = function() { + return '2.1.3'; +}; diff --git a/third_party/jasmine/lib/jasmine-2.1.3/jasmine_favicon.png b/third_party/jasmine/lib/jasmine-2.1.3/jasmine_favicon.png new file mode 100644 index 0000000000..3b84583be4 Binary files /dev/null and b/third_party/jasmine/lib/jasmine-2.1.3/jasmine_favicon.png differ diff --git a/third_party/jasmine/lib/jasmine-ajax-trunk/mock-ajax.js b/third_party/jasmine/lib/jasmine-ajax-trunk/mock-ajax.js new file mode 100644 index 0000000000..7cc57b5a54 --- /dev/null +++ b/third_party/jasmine/lib/jasmine-ajax-trunk/mock-ajax.js @@ -0,0 +1,374 @@ +/* + +Jasmine-Ajax : a set of helpers for testing AJAX requests under the Jasmine +BDD framework for JavaScript. + +http://github.com/pivotal/jasmine-ajax + +Jasmine Home page: http://pivotal.github.com/jasmine + +Copyright (c) 2008-2013 Pivotal Labs + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +(function() { + function extend(destination, source, propertiesToSkip) { + propertiesToSkip = propertiesToSkip || []; + for (var property in source) { + if (!arrayContains(propertiesToSkip, property)) { + destination[property] = source[property]; + } + } + return destination; + } + + function arrayContains(arr, item) { + for (var i = 0; i < arr.length; i++) { + if (arr[i] === item) { + return true; + } + } + return false; + } + + function MockAjax(global) { + var requestTracker = new RequestTracker(), + stubTracker = new StubTracker(), + paramParser = new ParamParser(), + realAjaxFunction = global.XMLHttpRequest, + mockAjaxFunction = fakeRequest(requestTracker, stubTracker, paramParser); + + this.install = function() { + global.XMLHttpRequest = mockAjaxFunction; + }; + + this.uninstall = function() { + global.XMLHttpRequest = realAjaxFunction; + + this.stubs.reset(); + this.requests.reset(); + paramParser.reset(); + }; + + this.stubRequest = function(url, data, method) { + var stub = new RequestStub(url, data, method); + stubTracker.addStub(stub); + return stub; + }; + + this.withMock = function(closure) { + this.install(); + try { + closure(); + } finally { + this.uninstall(); + } + }; + + this.addCustomParamParser = function(parser) { + paramParser.add(parser); + }; + + this.requests = requestTracker; + this.stubs = stubTracker; + } + + function StubTracker() { + var stubs = []; + + this.addStub = function(stub) { + stubs.push(stub); + }; + + this.reset = function() { + stubs = []; + }; + + this.findStub = function(url, data, method) { + for (var i = stubs.length - 1; i >= 0; i--) { + var stub = stubs[i]; + if (stub.matches(url, data, method)) { + return stub; + } + } + }; + } + + function ParamParser() { + var defaults = [ + { + test: function(xhr) { + return /^application\/json/.test(xhr.contentType()); + }, + parse: function jsonParser(paramString) { + return JSON.parse(paramString); + } + }, + { + test: function(xhr) { + return true; + }, + parse: function naiveParser(paramString) { + var data = {}; + var params = paramString.split('&'); + + for (var i = 0; i < params.length; ++i) { + var kv = params[i].replace(/\+/g, ' ').split('='); + var key = decodeURIComponent(kv[0]); + data[key] = data[key] || []; + data[key].push(decodeURIComponent(kv[1])); + } + return data; + } + } + ]; + var paramParsers = []; + + this.add = function(parser) { + paramParsers.unshift(parser); + }; + + this.findParser = function(xhr) { + for(var i in paramParsers) { + var parser = paramParsers[i]; + if (parser.test(xhr)) { + return parser; + } + } + }; + + this.reset = function() { + paramParsers = []; + for(var i in defaults) { + paramParsers.push(defaults[i]); + } + }; + + this.reset(); + } + + function fakeRequest(requestTracker, stubTracker, paramParser) { + function FakeXMLHttpRequest() { + requestTracker.track(this); + this.requestHeaders = {}; + } + + var iePropertiesThatCannotBeCopied = ['responseBody', 'responseText', 'responseXML', 'status', 'statusText', 'responseTimeout']; + extend(FakeXMLHttpRequest.prototype, new window.XMLHttpRequest(), iePropertiesThatCannotBeCopied); + extend(FakeXMLHttpRequest.prototype, { + open: function() { + this.method = arguments[0]; + this.url = arguments[1]; + this.username = arguments[3]; + this.password = arguments[4]; + this.readyState = 1; + this.onreadystatechange(); + }, + + setRequestHeader: function(header, value) { + this.requestHeaders[header] = value; + }, + + abort: function() { + this.readyState = 0; + this.status = 0; + this.statusText = "abort"; + this.onreadystatechange(); + }, + + readyState: 0, + + onload: function() { + }, + + onreadystatechange: function(isTimeout) { + }, + + status: null, + + send: function(data) { + this.params = data; + this.readyState = 2; + this.onreadystatechange(); + + var stub = stubTracker.findStub(this.url, data, this.method); + if (stub) { + this.response(stub); + } + }, + + contentType: function() { + for (var header in this.requestHeaders) { + if (header.toLowerCase() === 'content-type') { + return this.requestHeaders[header]; + } + } + }, + + data: function() { + if (!this.params) { + return {}; + } + + return paramParser.findParser(this).parse(this.params); + }, + + getResponseHeader: function(name) { + return this.responseHeaders[name]; + }, + + getAllResponseHeaders: function() { + var responseHeaders = []; + for (var i in this.responseHeaders) { + if (this.responseHeaders.hasOwnProperty(i)) { + responseHeaders.push(i + ': ' + this.responseHeaders[i]); + } + } + return responseHeaders.join('\r\n'); + }, + + responseText: null, + + response: function(response) { + if (this.readyState === 4) { + throw new Error("FakeXMLHttpRequest already completed"); + } + this.status = response.status; + this.statusText = response.statusText || ""; + this.responseText = response.responseText || ""; + this.readyState = 4; + this.responseHeaders = response.responseHeaders || + {"Content-Type": response.contentType || "application/json" }; + + this.onload(); + this.onreadystatechange(); + }, + + responseTimeout: function() { + if (this.readyState === 4) { + throw new Error("FakeXMLHttpRequest already completed"); + } + this.readyState = 4; + jasmine.clock().tick(30000); + this.onreadystatechange('timeout'); + } + }); + + return FakeXMLHttpRequest; + } + + function RequestTracker() { + var requests = []; + + this.track = function(request) { + requests.push(request); + }; + + this.first = function() { + return requests[0]; + }; + + this.count = function() { + return requests.length; + }; + + this.reset = function() { + requests = []; + }; + + this.mostRecent = function() { + return requests[requests.length - 1]; + }; + + this.at = function(index) { + return requests[index]; + }; + + this.filter = function(url_to_match) { + if (requests.length == 0) return []; + var matching_requests = []; + + for (var i = 0; i < requests.length; i++) { + if (url_to_match instanceof RegExp && + url_to_match.test(requests[i].url)) { + matching_requests.push(requests[i]); + } else if (url_to_match instanceof Function && + url_to_match(requests[i])) { + matching_requests.push(requests[i]); + } else { + if (requests[i].url == url_to_match) { + matching_requests.push(requests[i]); + } + } + } + + return matching_requests; + }; + } + + function RequestStub(url, stubData, method) { + var normalizeQuery = function(query) { + return query ? query.split('&').sort().join('&') : undefined; + }; + + if (url instanceof RegExp) { + this.url = url; + this.query = undefined; + } else { + var split = url.split('?'); + this.url = split[0]; + this.query = split.length > 1 ? normalizeQuery(split[1]) : undefined; + } + + this.data = normalizeQuery(stubData); + this.method = method; + + this.andReturn = function(options) { + this.status = options.status || 200; + + this.contentType = options.contentType; + this.responseText = options.responseText; + }; + + this.matches = function(fullUrl, data, method) { + var matches = false; + fullUrl = fullUrl.toString(); + if (this.url instanceof RegExp) { + matches = this.url.test(fullUrl); + } else { + var urlSplit = fullUrl.split('?'), + url = urlSplit[0], + query = urlSplit[1]; + matches = this.url === url && this.query === normalizeQuery(query); + } + return matches && (!this.data || this.data === normalizeQuery(data)) && (!this.method || this.method === method); + }; + } + + if (typeof window === "undefined" && typeof exports === "object") { + exports.MockAjax = MockAjax; + jasmine.Ajax = new MockAjax(exports); + } else { + window.MockAjax = MockAjax; + jasmine.Ajax = new MockAjax(window); + } +}()); diff --git a/third_party/jsdoc/.eslintignore b/third_party/jsdoc/.eslintignore new file mode 100644 index 0000000000..df135046ed --- /dev/null +++ b/third_party/jsdoc/.eslintignore @@ -0,0 +1,7 @@ +node_modules/* +rhino/* +templates/default/static/scripts/* +test/fixtures/* +test/lib/* +test/specs/* +test/tutorials/* diff --git a/third_party/jsdoc/.eslintrc b/third_party/jsdoc/.eslintrc new file mode 100644 index 0000000000..3e57f2274d --- /dev/null +++ b/third_party/jsdoc/.eslintrc @@ -0,0 +1,169 @@ +{ + "env": { + "jasmine": true, + "node": true + }, + + "rules": { + // Possible errors + "no-comma-dangle": 2, + "no-cond-assign": 2, + "no-console": 0, + "no-constant-condition": 0, + "no-control-regex": 2, + "no-debugger": 2, + "no-dupe-keys": 2, + "no-empty": 2, + "no-empty-class": 2, + "no-ex-assign": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": 0, + "no-extra-semi": 2, + "no-func-assign": 2, + "no-inner-declarations": [2, "functions"], + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-negated-in-lhs": 2, + "no-obj-calls": 2, + "no-regex-spaces": 2, + "no-reserved-keys": 0, + "no-sparse-arrays": 2, + "no-unreachable": 2, + "use-isnan": 2, + "valid-jsdoc": 0, // TODO: enable with { "requireReturn": false } + "valid-typeof": 2, + + // Best practices + "block-scoped-var": 0, + "complexity": 0, // TODO: enable + "consistent-return": 2, + "curly": [2, "all"], + "default-case": 2, + "dot-notation": 2, + "eqeqeq": [2, "smart"], + "guard-for-in": 2, + "no-alert": 2, + "no-caller": 2, + "no-div-regex": 2, + "no-else-return": 0, + "no-empty-label": 2, + "no-eq-null": 2, + "no-eval": 2, + "no-extra-bind": 2, + "no-extend-native": 2, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-implied-eval": 2, + "no-iterator": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-loop-func": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-native-reassign": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-wrappers": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-process-env": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-return-assign": 2, + "no-script-url": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-unused-expressions": 2, + "no-void": 2, + "no-warning-comments": 0, + "no-with": 2, + "radix": 2, + "vars-on-top": 0, + "wrap-iife": [2, "inside"], + "yoda": 2, + + // Strict mode + "global-strict": 0, + "no-extra-strict": 2, + "strict": 2, + + // Variables + "no-catch-shadow": 0, + "no-delete-var": 2, + "no-label-var": 2, + "no-shadow": 2, + "no-shadow-restricted-names": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-undefined": 0, + "no-unused-vars": 0, + "no-use-before-define": 2, + + // Node.js + "handle-callback-err": 2, + "no-mixed-requires": 2, + "no-new-require": 2, + "no-path-concat": 2, + "no-process-exit": 2, + "no-restricted-modules": 0, + "no-sync": 0, + + // Stylistic issues + "brace-style": 0, // TODO: enable with "stroustrup" (or "1tbsp" + lots of cleanup) + "camelcase": 2, + "comma-spacing": [2, { + "before": false, + "after": true + }], + "comma-style": [2, "last"], + "consistent-this": [2, "self"], + "eol-last": 2, + "func-names": 0, + "func-style": 0, + "key-spacing": [2, { + "beforeColon": false, + "afterColon": true + }], + "max-nested-callbacks": 0, + "new-cap": 2, + "new-parens": 2, + "no-array-constructor": 2, + "no-lonely-if": 2, + "no-mixed-spaces-and-tabs": 2, + "no-multiple-empty-lines": [2, { + "max": 2 + }], + "no-nested-ternary": 2, + "no-new-object": 2, + "no-space-before-semi": 2, + "no-spaced-func": 2, + "no-ternary": 0, + "no-trailing-spaces": 2, + "no-underscore-dangle": 0, + "no-wrap-func": 2, + "one-var": 0, + "padded-blocks": [2, "never"], + "quotes": [2, "single", "avoid-escape"], + "quote-props": 0, + "semi": [2, "always"], + "sort-vars": 0, + "space-after-keywords": [2, "always"], + "space-before-blocks": [2, "always"], + "space-in-brackets": 0, // TODO: enable? + "space-in-parens": 0, // TODO: enable? + "space-infix-ops": 2, + "space-return-throw-case": 2, + "space-unary-word-ops": 2, + "spaced-line-comment": [2, "always"], + "wrap-regex": 0, + + // Legacy + // TODO: consider enabling some of the `max-*` rules + "max-depth": 0, + "max-len": 0, + "max-params": 0, + "max-statements": 0, + "no-bitwise": 2, + "no-plusplus": 0 + } +} diff --git a/third_party/jsdoc/.gitignore b/third_party/jsdoc/.gitignore new file mode 100644 index 0000000000..28714ae3d9 --- /dev/null +++ b/third_party/jsdoc/.gitignore @@ -0,0 +1,20 @@ +# Development-related files +.tern-port +coverage/ +node_modules/.bin +node_modules/eslint +node_modules/gulp* +node_modules/istanbul + +# Node.js-only modules +node_modules/requizzle + +# User-specific files +conf.json + +# Generated files +test/tutorials/out +out/ + +# Dotfile detritus +.DS_Store diff --git a/third_party/jsdoc/.npmignore b/third_party/jsdoc/.npmignore new file mode 100644 index 0000000000..aaa8ad0add --- /dev/null +++ b/third_party/jsdoc/.npmignore @@ -0,0 +1,14 @@ +# development-related files +.eslintignore +.eslintrc +.gitignore +.travis.yml +gulpfile.js + +# scripts for launching JSDoc with Mozilla Rhino +/jsdoc* +!/jsdoc.js + +# Rhino and test directories +rhino/ +test/ diff --git a/third_party/jsdoc/.travis.yml b/third_party/jsdoc/.travis.yml new file mode 100644 index 0000000000..32230bda12 --- /dev/null +++ b/third_party/jsdoc/.travis.yml @@ -0,0 +1,7 @@ +language: node_js + +node_js: + - "0.10" + - "0.11" + +install: npm install -g gulp; npm install diff --git a/third_party/jsdoc/CONTRIBUTING.md b/third_party/jsdoc/CONTRIBUTING.md new file mode 100644 index 0000000000..2ed19421e6 --- /dev/null +++ b/third_party/jsdoc/CONTRIBUTING.md @@ -0,0 +1,69 @@ +Pull Requests +------------- + +If you're thinking about making some changes, maybe fixing a bug, or adding a +snazzy new feature, first, thank you. Contributions are very welcome. Things +need to be manageable for the maintainers, however. So below you'll find **The +fastest way to get your pull request merged in.** Some things, particularly how +you set up your branches and work with git, are just suggestions, but pretty good +ones. + +1. **Create a remote to track the base jsdoc3/jsdoc repository** + This is just a convenience to make it easier to update your `````` + (more on that shortly). You would execute something like: + + git remote add base git://github.com/jsdoc3/jsdoc.git + + Here 'base' is the name of the remote. Feel free to use whatever you want. + +2. **Set up a tracking branch for the base repository** + We're gonna call this your ``````. You will only ever update + this branch by pulling from the 'base' remote. (as opposed to 'origin') + + git branch --track pullpost base/master + git checkout pullpost + + Here 'pullpost' is the name of the branch. Fell free to use whatever you want. + +3. **Create your change branch** + Once you are in ``````, make sure it's up to date, then create + a branch for your changes off of that one. + + git branch fix-for-issue-395 + git checkout fix-for-issue-395 + + Here 'fix-for-issue-395' is the name of the branch. Feel free to use whatever + you want. We'll call this the ``````. This is the branch that + you will eventually issue your pull request from. + + The purpose of these first three steps is to make sure that your merge request + has a nice clean diff that only involves the changes related to your fix/feature. + +4. **Make your changes** + On your `````` make any changes relevant to your fix/feature. Don't + group fixes for multiple unrelated issues or multiple unrelated features together. + Create a separate branch for each unrelated changeset. For instance, if you're + fixing a bug in the parser and adding some new UI to the default template, those + should be separate branches and merge requests. + +5. **Add tests** + Add tests for your change. If you are submitting a bugfix, include a test that + verifies the existence of the bug along with your fix. If you are submitting + a new feature, include tests that verify proper feature function, if applicable. + See the readme in the 'test' directory for more information + +6. **Commit and publish** + Commit your changes and publish your branch (or push it if it's already published) + +7. **Issue your pull request** + On github.com, switch to your `````` and click the 'Pull Request' + button. Enter some meaningful information about the pull request. If it's a bugfix, + that doesn't already have an issue associated with it, provide some info on what + situations that bug occurs in and a sense of it's severity. If it does already have + an issue, make sure the include the hash and issue number (e.g. '#100') so github + links it. + + If it's a feature, provide some context about the motivations behind the feature, + why it's important/useful/cool/necessary and what it does/how it works. Don't + worry about being too verbose. Folks will be much more amenable to reading through + your code if they know what its supposed to be about. diff --git a/third_party/jsdoc/LICENSE.md b/third_party/jsdoc/LICENSE.md new file mode 100644 index 0000000000..01417b6b39 --- /dev/null +++ b/third_party/jsdoc/LICENSE.md @@ -0,0 +1,380 @@ +# License # + +JSDoc 3 is free software, licensed under the Apache License, Version 2.0 (the +"License"). Commercial and non-commercial use are permitted in compliance with +the License. + +Copyright (c) 2011-2014 Michael Mathews and the +[contributors to JSDoc](https://github.com/jsdoc3/jsdoc/graphs/contributors). +All rights reserved. + +You may obtain a copy of the License at: +http://www.apache.org/licenses/LICENSE-2.0 + +In addition, a copy of the License is included with this distribution. + +As stated in Section 7, "Disclaimer of Warranty," of the License: + +> Licensor provides the Work (and each Contributor provides its Contributions) +> on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +> express or implied, including, without limitation, any warranties or +> conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A +> PARTICULAR PURPOSE. You are solely responsible for determining the +> appropriateness of using or redistributing the Work and assume any risks +> associated with Your exercise of permissions under this License. + +The source code for JSDoc 3 is available at: +https://github.com/jsdoc3/jsdoc + +# Third-Party Software # + +JSDoc 3 includes or depends upon the following third-party software, either in +whole or in part. Each third-party software package is provided under its own +license. + +## MIT License ## + +Several of the following software packages are distributed under the MIT +license, which is reproduced below: + +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all +> copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +> SOFTWARE. + +## Acorn ## + +Portions of the Acorn source code are incorporated into the following files: + +- `lib/jsdoc/src/walker.js` + +Acorn is distributed under the MIT license, which is reproduced above. + +Copyright (C) 2012 Marijn Haverbeke . + +The source code for Acorn is available at: +https://github.com/marijnh/acorn + +## Async.js ## + +Async.js is distributed under the MIT license, which is reproduced above. + +Copyright (c) 2010 Caolan McMahon. + +The source code for Async.js is available at: +https://github.com/caolan/async + +## Catharsis ## + +Catharsis is distributed under the MIT license, which is reproduced above. + +Copyright (c) 2012-2014 Jeff Williams. + +The source code for Catharsis is available at: +https://github.com/hegemonic/catharsis + +## crypto-browserify ## + +crypto-browserify is distributed under the MIT license, which is reproduced +above. + +Copyright (c) 2013 Dominic Tarr. + +The source code for crypto-browserify is available at: +https://github.com/dominictarr/crypto-browserify + +## escape-string-regexp ## + +escape-string-regexp is distributed under the MIT License, which is reproduced +above. + +Copyright (c) Sindre Sorhus . + +The source code for escape-string-regexp is available at: +https://github.com/sindresorhus/escape-string-regexp + +## Esprima ## + +Esprima is distributed under the BSD 2-clause license: + +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions are met: +> +> - Redistributions of source code must retain the above copyright notice, +> this list of conditions and the following disclaimer. +> - Redistributions in binary form must reproduce the above copyright notice, +> this list of conditions and the following disclaimer in the documentation +> and/or other materials provided with the distribution. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +> ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +> DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +> (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +> ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +> (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +> THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Copyright (c) 2011-2013 Ariya Hidayat and other Esprima contributors. + +The source code for Esprima is available at: +https://github.com/ariya/esprima + +## events ## + +Portions of the events source code are incorporated into the following files: + ++ `rhino/events.js` + +events is distributed under the MIT license, which is reproduced above. + +Copyright Joyent, Inc. and other Node contributors. All rights reserved. + +The source code for events is available at: +https://github.com/Gozala/events + +## github-flavored-markdown ## + +github-flavored-markdown is distributed under the BSD 3-clause license: + +> Copyright (c) 2007, John Fraser All rights +> reserved. +> +> Original Markdown copyright (c) 2004, John Gruber +> All rights reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions are met: +> +> - Redistributions of source code must retain the above copyright notice, +> this list of conditions and the following disclaimer. +> +> - Redistributions in binary form must reproduce the above copyright notice, +> this list of conditions and the following disclaimer in the documentation +> and/or other materials provided with the distribution. + +> - Neither the name "Markdown" nor the names of its contributors may be used +> to endorse or promote products derived from this software without specific +> prior written permission. +> +> This software is provided by the copyright holders and contributors "as is" +> and any express or implied warranties, including, but not limited to, the +> implied warranties of merchantability and fitness for a particular purpose are +> disclaimed. In no event shall the copyright owner or contributors be liable +> for any direct, indirect, incidental, special, exemplary, or consequential +> damages (including, but not limited to, procurement of substitute goods or +> services; loss of use, data, or profits; or business interruption) however +> caused and on any theory of liability, whether in contract, strict liability, +> or tort (including negligence or otherwise) arising in any way out of the use +> of this software, even if advised of the possibility of such damage. + +The source code for github-flavored-markdown is available at: +https://github.com/hegemonic/github-flavored-markdown + +## Google Code Prettify ## + +Google Code Prettify is distributed under the Apache License 2.0, which is +included with this package. + +Copyright (c) 2006 Google Inc. + +The source code for Google Code Prettify is available at: +https://code.google.com/p/google-code-prettify/ + +## Jasmine ## + +Jasmine is distributed under the MIT license, which is reproduced above. + +Copyright (c) 2008-2011 Pivotal Labs. + +The source code for Jasmine is available at: +https://github.com/pivotal/jasmine + +## jasmine-node ## + +jasmine-node is distributed under the MIT license, which is reproduced above. + +Copyright (c) 2010 Adam Abrons and Misko Hevery (http://getangular.com). + +The source code for jasmine-node is available at: +https://github.com/mhevery/jasmine-node + +## js2xmlparser ## + +js2xmlparser is distributed under the MIT license, which is reproduced above. + +Copyright (c) 2012 Michael Kourlas. + +The source code for js2xmlparser is available at: +https://github.com/michaelkourlas/node-js2xmlparser + +## Node.js ## + +Portions of the Node.js source code are incorporated into the following files: + +- `rhino/fs.js` +- `rhino/path.js` +- `rhino/querystring.js` +- `rhino/util.js` + +Node.js is distributed under the MIT license, which is reproduced above. + +Copyright Joyent, Inc. and other Node contributors. All rights reserved. + +The source code for Node.js is available at: +https://github.com/joyent/node + +## node-browser-builtins ## + +Portions of the node-browser-builtins source code are incorporated into the +following files: + +- `rhino/assert.js` +- `rhino/rhino-shim.js` + +node-browser-builtins is distributed under the MIT license, which is reproduced +above. + +The source code for node-browser-builtins is available at: +https://github.com/alexgorbatchev/node-browser-builtins + +## Open Sans ## + +Open Sans is distributed under the Apache License 2.0, which is +included with this package. + +Copyright (c) 2010-2011, Google Inc. + +This typeface, including the complete set of variations, are available at: +http://www.google.com/fonts/specimen/Open+Sans + +## Requizzle ## + +Requizzle is distributed under the MIT license, which is reproduced above. + +Copyright (c) 2014 Google Inc. All rights reserved. +Copyright (c) 2012-2013 Johannes Ewald. + +The source code for Requizzle is available at: +https://github.com/hegemonic/requizzle + +## Rhino ## + +Rhino is distributed under the following licenses: + +### MPL 2.0 License ### +The majority of the source code for Rhino is available under the Mozilla Public +License (MPL) 2.0, which is included in this distribution. + +### License for portions of the Rhino debugger ### +Additionally, some files are available under the BSD 3-clause license: + +> Copyright 1997, 1998 Sun Microsystems, Inc. All Rights Reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions are met: +> +> - Redistributions of source code must retain the above copyright notice, +> this list of conditions and the following disclaimer. +> - Redistributions in binary form must reproduce the above copyright +> notice, this list of conditions and the following disclaimer in the +> documentation and/or other materials provided with the distribution. +> - Neither the name of Sun Microsystems nor the names of its contributors +> may be used to endorse or promote products derived from this software +> without specific prior written permission. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +> DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +> FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +> DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +> SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +> OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +> OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +### Source Code ### +The source code for Rhino is available at: +https://github.com/jsdoc3/rhino + +## TaffyDB ## + +TaffyDB is distributed under a modified BSD license: + +> All rights reserved. +> +> Redistribution and use of this software in source and binary forms, with or +> without modification, are permitted provided that the following condition is +> met: +> +> Redistributions of source code must retain the above copyright notice, this +> list of conditions and the following disclaimer. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +> ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +> LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +> CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +> SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +> INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +> CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +> ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +> POSSIBILITY OF SUCH DAMAGE. + +The source code for TaffyDB is available at: +https://github.com/hegemonic/taffydb + +## Tomorrow Theme for Google Code Prettify ## + +License information for the Tomorrow Theme for Google Code Prettify is not +available. It is assumed that the package is distributed under an open source +license that is compatible with the Apache License 2.0. + +Copyright (c) Yoshihide Jimbo. + +The source code for the Tomorrow Theme is available at: +https://github.com/jmblog/color-themes-for-google-code-prettify + +## tv4 ## + +tv4 is in the public domain. It is also distributed under the MIT license, which +is reproduced above. + +The source code for tv4 is available at: +https://github.com/geraintluff/tv4 + +## Underscore.js ## + +Underscore.js is distributed under the MIT license, which is reproduced above. + +Copyright (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative +Reporters & Editors. + +The source code for Underscore.js is available at: +https://github.com/jashkenas/underscore + +## wrench-js ## + +wrench-js is distributed under the MIT license, which is reproduced above. + +Copyright (c) 2010 Ryan McGrath. + +The source code for wrench-js is available at: +https://github.com/ryanmcgrath/wrench-js diff --git a/third_party/jsdoc/LICENSE.txt b/third_party/jsdoc/LICENSE.txt new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/third_party/jsdoc/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third_party/jsdoc/README.md b/third_party/jsdoc/README.md new file mode 100644 index 0000000000..d11b5ced6c --- /dev/null +++ b/third_party/jsdoc/README.md @@ -0,0 +1,135 @@ +JSDoc 3 +======= +[![Build Status](https://img.shields.io/travis/jsdoc3/jsdoc.svg)](http://travis-ci.org/jsdoc3/jsdoc) + +An API documentation generator for JavaScript. + +Want to contribute to JSDoc? Please read `CONTRIBUTING.md`. + +Installation and Usage +---------------------- + +You can run JSDoc on either Node.js or Mozilla Rhino. + +### Node.js + +Native support for Node.js is available in JSDoc 3.3.0 and later. JSDoc +supports Node.js 0.10 and later. + +#### Installing JSDoc for Node.js + +You can install JSDoc in your project's `node_modules` folder, or you can +install it globally. + +To install the latest alpha version: + + npm install jsdoc@"<=3.3.0" + +To install the latest development version: + + npm install git+https://github.com/jsdoc3/jsdoc.git + +#### Running JSDoc with Node.js + +If you installed JSDoc locally, the JSDoc command-line tool is available in +`./node_modules/.bin`. To generate documentation for the file +`yourJavaScriptFile.js`: + + ./node_modules/.bin/jsdoc yourJavaScriptFile.js + +Or if you installed JSDoc globally, simply run the `jsdoc` command: + + jsdoc yourJavaScriptFile.js + +By default, the generated documentation is saved in a directory named `out`. You +can use the `--destination` (`-d`) option to specify another directory. + +Run `jsdoc --help` for a complete list of command-line options. + +### Mozilla Rhino + +All versions of JSDoc 3 run on a customized version of Mozilla Rhino, which +requires Java. You can run JSDoc 3 on Java 1.6 and later. + +#### Installing JSDoc for Mozilla Rhino + +To install JSDoc, download a .zip file for the +[latest development version](https://github.com/jsdoc3/jsdoc/archive/master.zip) +or a [previous release](https://github.com/jsdoc3/jsdoc/tags). + +You can also use git to clone the +[JSDoc repository](https://github.com/jsdoc3/jsdoc): + + git clone git+https://github.com/jsdoc3/jsdoc.git + +The JSDoc repository includes a +[customized version of Mozilla Rhino](https://github.com/jsdoc3/rhino). Make +sure your Java classpath does not include any other versions of Rhino. (On OS X, +you may need to remove the file `~/Library/Java/Extensions/js.jar`.) + +**Note**: In JSDoc 3.3.0 and later, if you need to run JSDoc on Mozilla Rhino, +do not install JSDoc with npm. Use one of the methods described above. + +#### Running JSDoc with Mozilla Rhino + +On OS X, Linux, and other POSIX systems, to generate documentation for the file +`yourJavaScriptFile.js`: + + ./jsdoc yourJavaScriptFile.js + +Or on Windows: + + jsdoc yourJavaScriptFile.js + +By default, the generated documentation is saved in a directory named `out`. You +can use the `--destination` (`-d`) option to specify another directory. + +Run `jsdoc --help` for a complete list of command-line options. + + +Templates and Build Tools +------------------------- + +The JSDoc community has created numerous templates and other tools to help you +generate and customize your documentation. Here are just a few: + +### Templates + ++ [jaguarjs-jsdoc](https://github.com/davidshimjs/jaguarjs-jsdoc) + ([example](http://davidshimjs.github.io/jaguarjs/doc)) ++ [DocStrap](https://github.com/terryweiss/docstrap) ++ [jsdoc3Template](https://github.com/DBCDK/jsdoc3Template) + ([example](https://github.com/danyg/jsdoc3Template/wiki#wiki-screenshots)) + +### Build Tools + ++ [JSDoc Grunt plugin](https://github.com/krampstudio/grunt-jsdoc) ++ [JSDoc ant task](https://github.com/jannon/jsdoc3-ant-task) + +### Generating Typeface Fonts + +JSDoc 3 uses the [OpenSans](https://www.google.com/fonts/specimen/Open+Sans) typeface, the fonts for which can be re-generated as follows: + +1. Open the [OpenSans page at Font Squirrel](). +2. Click on the 'Webfont Kit' tab. +3. Either leave the subset drop-down as 'Western Latin (Default)', or if we decide we need more glyphs than change it to 'No Subsetting'. +4. Click the 'DOWNLOAD @FONT-FACE KIT' button. +5. For each typeface variant we plan to use, copy the 'eot', 'svg' and 'woff' files into the 'templates/default/static/fonts' directory. + +For More Information +-------------------- + ++ Documentation is available at [Use JSDoc](http://usejsdoc.org). ++ Contribute to the docs at [jsdoc3/jsdoc3.github.com](https://github.com/jsdoc3/jsdoc3.github.com). ++ Ask for help on the [JSDoc Users mailing list](http://groups.google.com/group/jsdoc-users). ++ Post questions tagged `jsdoc` to [Stack +Overflow](http://stackoverflow.com/questions/tagged/jsdoc). + +License +------- + +JSDoc 3 is copyright (c) 2011-2014 Michael Mathews and the +[contributors to JSDoc](https://github.com/jsdoc3/jsdoc/graphs/contributors). + +JSDoc 3 is free software, licensed under the Apache License, Version 2.0. See +the file `LICENSE.md` in this distribution for more details. diff --git a/third_party/jsdoc/changes.md b/third_party/jsdoc/changes.md new file mode 100644 index 0000000000..fc832d826c --- /dev/null +++ b/third_party/jsdoc/changes.md @@ -0,0 +1,240 @@ +# JSDoc 3 change history + +This file describes notable changes in each version of JSDoc 3. To download a specific version of JSDoc 3, see [GitHub's tags page](https://github.com/jsdoc3/jsdoc/tags). + +## 3.2.2 (November 2013) + +### Bug fixes ++ Addressed a regression in JSDoc 3.2.1 that could prevent a function declaration from shadowing a declaration with the same name in an outer scope. (#513) ++ If a child class overrides a method in a parent class without documenting the overridden method, the method's documentation is now copied from the parent class. (#503) ++ You can now use inline HTML tags in Markdown-formatted text. In addition, JSDoc now uses only the [marked Markdown parser](https://github.com/chjj/marked); the markdown-js parser has been removed. (#510) ++ Type expressions can now include a much broader range of repeatable types. In addition, you can now use Closure Compiler's nullable and non-nullable modifiers with repeatable types. For example, the type expression `...!string` (a repeatable, non-nullable string) is now parsed correctly. (#502) ++ If a function accepts a parameter named `prototype`, the parameter is no longer renamed during parsing. (#505) ++ If the list of input files includes relative paths, the paths are now resolved relative to the user's working directory. (a3d33842) + +## 3.2.1 (October 2013) + +### Enhancements ++ JSDoc's parser now fires a `processingComplete` event after JSDoc has completed all post-processing of the parse results. This event has a `doclets` property containing an array of doclets. (#421) ++ When JSDoc's parser fires a `parseComplete` event, the event now includes a `doclets` property containing an array of doclets. (#431) ++ You can now use relative paths in the JSDoc configuration file's `source.exclude` option. Relative paths will be resolved relative to the current working directory. (#405) ++ If a symbol uses the `@default` tag, and its default value is an object literal, this value is now stored as a string, and the doclet will have a `defaultvaluetype` property containing the string `object`. This change enables templates to show the default value with appropriate syntax highlighting. (#419) ++ Inline `{@link}` tags can now contain newlines. (#441) + +### Bug fixes ++ Inherited symbols now indicate that they were inherited from the ancestor that defined the symbol, rather than the direct parent. (#422) ++ If the first line of a JavaScript file contains a hashbang (for example, `#!/usr/bin/env node`), the hashbang is now ignored when the file is parsed. (#499) ++ Resolved a crash when a JavaScript file contains a [JavaScript 1.8](https://developer.mozilla.org/en-US/docs/Web/JavaScript/New_in_JavaScript/1.8) keyword, such as `let`. (#477) ++ The type expression `function[]` is now parsed correctly. (#493) ++ If a module is tagged incorrectly, the module's output file now has a valid filename. (#440, #458) ++ For tags that accept names, such as `@module` and `@param`, if a hyphen is used to separate the name and description, the hyphen must appear on the same line as the name. This change prevents a Markdown bullet on the followng line from being interpreted as a separator. (#459) ++ When lenient mode is enabled, a `@param` tag with an invalid type expression no longer causes a crash. (#448) ++ The `@requires` tag can now contain an inline tag in its tag text. (#486) ++ The `@returns` tag can now contain inline tags even if a type is not specified. (#444) ++ When lenient mode is enabled, a `@returns` tag with no value no longer causes a crash. (#451) ++ The `@type` tag now works correctly with type expressions that span multiple lines. (#427) ++ If a string contains inline `{@link}` tags preceded by bracketed link text (for example, `[test]{@link Test#test}`), HTML links are now generated correctly even if the string contains other bracketed text. (#470) ++ On POSIX systems, if you run JSDoc using a symlink to the startup script, JSDoc now works correctly. (#492) + +### Default template ++ Pretty-printed source files are now generated by default. To disable this feature, add the property `templates.default.outputSourceFiles: false` to your `conf.json` file. (#454) ++ Links to a specific line in a source file now work correctly. (#475) ++ Pretty-printed source files are now generated using the encoding specified in the `-e/--encoding` option. (#496) ++ If a `@default` tag is added to a symbol whose default value is an object, the value is now displayed in the output file. (#419) ++ Output files now identify symbols as "abstract" rather than "virtual." (#432) + +## 3.2.0 (May 2013) + +### Major changes ++ JSDoc can now parse any valid [Google Closure Compiler type expression](https://developers.google.com/closure/compiler/docs/js-for-compiler#types). **Note**: As a result of this change, JSDoc quits if a file contains an invalid type expression. To prevent JSDoc from quitting, run JSDoc with the `--lenient` (`-l`) command-line option. (Multiple issues) ++ You can now use the new `@listens` tag to indicate that a symbol listens for an event. (#273) + +### Enhancements ++ The parser now fires a `parseBegin` event before it starts parsing files, as well as a `parseComplete` event after all files have been parsed. Plugins can define event handlers for these events, and `parseBegin` handlers can modify the list of files to parse. (#299) ++ Event handlers for `jsdocCommentFound` events can now modify the JSDoc comment. (#228) ++ You can now exclude tags from Markdown processing using the new option `markdown.excludeTags` in the configuration file. (#337) ++ You can now use the [marked](https://github.com/chjj/marked) Markdown parser by setting the configuration property `markdown.parser` to `marked`. In addition, if `markdown.parser` is set to `gfm`, JSDoc will now use the "marked" parser instead. (#385) ++ The `@typedef` tag no longer requires a name when used with a Closure Compiler-style type definition. For example, the following type definition will automatically get the name `Foo.Bar`: + + ```javascript + /** @typedef {string} */ + Foo.Bar; + ``` + + (#391) ++ You can now use an inline `{@type}` tag in a parameter's description. If this tag is present, JSDoc will assume that the parameter uses the type specified in the inline `{@type}` tag. For example, the following `@param` tag would cause `myParam`'s type to be documented as `Foo`: + + ``` + @param {(boolean|string)} myParam - My special parameter. {@type Foo} + ``` + + (#152) ++ The `console.log` function now behaves the same way as on Node.js. In addition, the functions `console.info`, `console.error`, `console.warn`, and `console.trace` have been implemented. (#298) ++ You can now use npm to install JSDoc globally by running `npm install -g`. **Note**: JSDoc will still run under Mozilla Rhino, not Node.js. (#374) ++ The `jsVersion` configuration property has been removed. (#390) + +### Bug fixes ++ JSDoc now quits if the configuration file cannot be loaded. (#407) ++ JSDoc's `--explain` (`-X`) option now runs much more quickly, and it outputs valid JSON to the console. (#298) ++ JSDoc's `--lenient` (`-l`) option now prints warnings on STDERR rather than STDOUT. (#298) ++ The parser now assigns the correct scope to object properties whose names include single quotes. (#386) ++ The parser now recognizes CommonJS modules that export a single function rather than an object. (#384) ++ The inline `{@link}` tag now works correctly when `@link` is followed by a tab. (#359) ++ On POSIX systems, quoted command-line arguments are no longer split on spaces. (#397) + +### Plugins ++ The new `overloadHelper` plugin makes it easier to link to overloaded methods. (#179) ++ The `markdown` plugin now converts Markdown links in the `@see` tag. (#297) + +### Default template enhancements ++ You can now use the configuration property `templates.default.staticFiles` to copy additional static files to the output directory. (#393) ++ All output files now use human-readable filenames. (#339) ++ The documentation for events now lists the symbols that listen to that event. (#273) ++ Links to source files now allow you to jump to the line where a symbol is defined. (#316) ++ The output files now link to individual types within a Closure Compiler type expression. (Multiple issues) ++ CommonJS modules that export a single function, rather than an object, are now documented more clearly. (#384) ++ Functions that can throw multiple types of errors are now documented more clearly. (#389) ++ If a `@property` tag does not identify the property's name, the template no longer throws an error. (#373) ++ The type of each `@typedef` is now displayed. (#391) ++ If a `@see` tag contains a URL (for example, `@see http://example.com` or `@see `), the tag text is now converted to a link. (#371) ++ Repeatable parameters are now identified. (#381) ++ The "Classes" header is no longer repeated in the navigation bar. (#361) ++ When the only documented symbols in global scope are type definitions, you can now click the "Global" header to view their documentation. (#261) + +## 3.1.1 (February 2013) + ++ Resolved a crash when no input files contain JSDoc comments. (#329) ++ Resolved a crash when JSDoc cannot identify the common prefix of several paths. (#330) ++ Resolved a crash when the full path to JSDoc contained at least one space. (#347) ++ Files named `README.md` or `package.json` will now be processed when they are specified on the command line. (#350) ++ You can now use `@emits` as a synonym for `@fires`. (#324) ++ The module `jsdoc/util/templateHelper` now allows you to specify the CSS class for links that are generated by the following methods: (#331) + + `getAncestorLinks` + + `getSignatureReturns` + + `getSignatureTypes` + + `linkto` + +## 3.1.0 (January 2013) + +### Major changes ++ You can now use the new `@callback` tag to provide information about a callback function's signature. To document a callback function, create a standalone JSDoc comment, as shown in the following example: + + ```javascript + /** + * @class + */ + function MyClass() {} + + /** + * Send a request. + * + * @param {MyClass~responseCb} cb - Called after a response is received. + */ + MyClass.prototype.sendRequest = function(cb) { + // code + }; + + /** + * Callback for sending a request. + * + * @callback MyClass~responseCb + * @param {?string} error - Information about the error. + * @param {?string} response - Body of the response. + */ + ``` ++ The inline link tag, `{@link}`, has been improved: + + You can now use a space as the delimiter between the link target and link text. + + In your `conf.json` file, you can now enable the option `templates.cleverLinks` to display code links in a monospace font and URL links in plain text. You can also enable the option `templates.monospaceLinks` to display all links in a monospace font. **Note**: JSDoc templates must be updated to respect these options. + + You can now use the new inline tags `{@linkplain}`, which forces a plain-text link, and `{@linkcode}`, which forces a monospace link. These tags always override the settings in your `conf.json` file. (#250) ++ JSDoc now provides a `-l/--lenient` option that tells JSDoc to continue running if it encounters a non-fatal error. (Multiple issues) ++ A template's `publish.js` file should now assign its `publish` function to `exports.publish`, rather than defining a global `publish` function. The global `publish` function is deprecated and may not be supported in future versions. JSDoc's built-in templates reflect this change. (#166) ++ The template helper (`templateHelper.js`) exports a variety of new functions for finding information within a parse tree. These functions were previously contained within the default template. (#186) ++ Updated the `fs` and `path` modules to make their behavior more consistent with Node.js. In addition, created extended versions of these modules with additional functionality. (Multiple commits) ++ Updated or replaced numerous third-party modules. (Multiple commits) ++ Reorganized the JSDoc codebase in preparation for future enhancements. (Multiple commits) ++ JSDoc now embeds a version of Mozilla Rhino that recognizes Node.js packages, including `package.json` files. (Multiple commits) ++ Node.js' `npm` utility can now install JSDoc from its GitHub repository. **Note**: JSDoc is not currently compatible with Node.js. However, this change allows JSDoc to be installed as a dependency of a Node.js project. In this version, global installation with `npm` is not supported. (Multiple commits) + +### Enhancements ++ If a `README.md` file is passed to JSDoc, its contents will be included on the `index.html` page of the generated documentation. (#128) ++ The `@augments` tag can now refer to an undocumented member, such as `window.XMLHTTPRequest`. (#160) ++ The `@extends` tag can now refer to an undocumented member, such as `window.XMLHttpRequest`. In addition, you can now use `@host` as a synonym for `@extends`. (#145) ++ The `@lends` tag is now supported in multiline JSDoc comments. (#163) ++ On Windows, `jsdoc.cmd` now provides the same options as the `jsdoc` shell script. (#127) ++ JSDoc now provides `setTimeout()`, `clearTimeout()`, `setInterval()`, and `clearInterval()` functions. (Multiple commits) ++ JSDoc no longer provides a global `exit()` function. Use `process.exit()` instead. (1228a8f7) ++ JSDoc now includes additional shims for Node.js' built-in modules. **Note**: Many of these shims implement only the functions that JSDoc uses, and they may not be consistent with Node.js' behavior in edge cases. (Multiple commits) ++ JSDoc now provides a `-v/--version` option to display information about the current version. (#303) ++ When running tests, you can now use the `--nocolor` option to disable colored output. On Windows, colored output is always disabled. (e17601fe, 8bc33541) + +### Bug fixes ++ When using the `@event` tag to define an event within a class or namespace, the event's longname is now set correctly regardless of tag order. (#280) ++ The `@property` tag no longer results in malformed parse trees. (20f87094) ++ The `jsdoc` and `jsdoc.cmd` scripts now work correctly with paths that include spaces. (#127, #130) ++ The `jsdoc` script now works correctly on Cygwin and MinGW, and with the `dash` shell. (#182, #184, #187) ++ The `-d/--destination` option is no longer treated as a path relative to the JSDoc directory. Instead, it can contain an absolute path, or a path relative to the current working directory. (f5e3f0f3) ++ JSDoc now provides default options for the values in `conf.json`. (#129) ++ If the `conf.json` file does not exist, JSDoc no longer tries to create it, which prevents errors if the current user does not have write access to the JSDoc directory. (d2d05fcb) ++ Doclets for getters and setters are now parsed appropriately. (#150) ++ Only the first asterisk is removed from each line of a JSDoc comment. (#172) ++ If a child member overrides an ancestor member, the ancestor member is no longer documented. (#158) ++ If a member of a namespace has the same name as a namespace, the member is now documented correctly. (#214) ++ The parse tree now uses a single set of properties to track both JSDoc-style type information and Closure Compiler-style type information. (#118) ++ If a type has a leading `!`, indicating that it is non-nullable, the leading `!` is now removed from the type name. (#226) ++ When Markdown formatting is enabled, underscores in inline `{@link}` tags are no longer treated as Markdown formatting characters. (#259) ++ Markdown links now work correctly when a JavaScript reserved word, such as `constructor`, is used as the link text. (#249) ++ Markdown files for tutorials are now parsed based on the settings in `conf.json`, rather than using the "evilstreak" Markdown parser in all cases. (#220) ++ If a folder contains both tutorial source files and `.js` files, JSDoc no longer attempts to parse the `.js` files as JSON files. (#222) ++ The "evilstreak" Markdown parser now works correctly with files that use Windows-style line endings. (#223) ++ JSDoc no longer fails unit tests when the `conf.json` file is not present. (#206) ++ On Windows, JSDoc now passes all unit tests. (Multiple commits) + +### Plugins ++ The new `partial` plugin adds support for a `@partial` tag, which links to an external file that contains JSDoc comments. (#156) ++ The new `commentsOnly` plugin removes everything in a file except JSDoc-style comments. You can use this plugin to document source files that are not valid JavaScript, including source files for other languages. (#304) ++ The new `eventDumper` plugin logs information about parser events to the console. (#242) ++ The new `verbose` plugin logs the name of each input file to the console. (#157) + +### Template enhancements + +#### Default template ++ The template output now includes pretty-printed versions of source files. This feature is enabled by default. To disable this feature, add the property `templates.default.outputSourceFiles: false` to your `conf.json` file. (#208) ++ You can now use the template if it is placed outside of the JSDoc directory. (#198) ++ The template no longer throws an error when a parameter does not have a name. (#175) ++ The navigation bar now includes an "Events" section if any events are documented. (#280) ++ Pages no longer include a "Classes" header when no classes are documented. (eb0186b9) ++ Member details now include "Inherited From" section when a member is inherited from another member. (#154) ++ If an `@author` tag contains text in the format "Jane Doe ", the value is now converted to an HTML `mailto:` link. (#326) ++ Headings for functions now include the function's signature. (#253) ++ Type information is now displayed for events. (#192) ++ Functions now link to their return type when appropriate. (#192) ++ Type definitions that contain functions are now displayed correctly. (#292) ++ Tutorial output is now generated correctly. (#188) ++ Output files now use Google Code Prettify with the Tomorrow theme as a syntax highlighter. (#193) ++ The `index.html` output file is no longer overwritten if a namespace called `index` has been documented. (#244) ++ The current JSDoc version number is now displayed in the footer. (#321) + +#### Haruki template ++ Members are now contained in arrays rather than objects, allowing overloaded members to be documented. (#153) ++ A clearer error message is now provided when the output destination is not specified correctly. (#174) + +## 3.0.1 (June 2012) + +### Enhancements ++ The `conf.json` file may now contain `source.include` and `source.exclude` properties. (#56) + + `source.include` specifies files or directories that JSDoc should _always_ check for documentation. + + `source.exclude` specifies files or directories that JSDoc should _never_ check for documentation. + These settings take precedence over the `source.includePattern` and `source.excludePattern` properties, which contain regular expressions that JSDoc uses to search for source files. ++ The `-t/--template` option may now specify the absolute path to a template. (#122) + +### Bug fixes ++ JSDoc no longer throws exceptions when a symbol has a special name, such as `hasOwnProperty`. (1ef37251) ++ The `@alias` tag now works correctly when documenting inner classes as globals. (810dd7f7) + +### Template improvements ++ The default template now sorts classes by name correctly when the classes come from several modules. (4ce17195) ++ The Haruki template now correctly supports `@example`, `@members`, and `@returns` tags. (6580e176, 59655252, 31c8554d) + +## 3.0.0 (May 2012) + +Initial release. diff --git a/third_party/jsdoc/cli.js b/third_party/jsdoc/cli.js new file mode 100644 index 0000000000..35ab99c721 --- /dev/null +++ b/third_party/jsdoc/cli.js @@ -0,0 +1,463 @@ +/*global java */ +/*eslint no-process-exit:0 */ +/** + * Helper methods for running JSDoc on the command line. + * + * A few critical notes for anyone who works on this module: + * + * + The module should really export an instance of `cli`, and `props` should be properties of a + * `cli` instance. However, Rhino interpreted `this` as a reference to `global` within the + * prototype's methods, so we couldn't do that. + * + On Rhino, for unknown reasons, the `jsdoc/fs` and `jsdoc/path` modules can fail in some cases + * when they are required by this module. You may need to use `fs` and `path` instead. + * + * @private + */ +module.exports = (function() { +'use strict'; + +var logger = require('jsdoc/util/logger'); +var stripJsonComments = require('strip-json-comments'); + +var hasOwnProp = Object.prototype.hasOwnProperty; + +var props = { + docs: [], + packageJson: null, + shouldExitWithError: false, + tmpdir: null +}; + +var app = global.app; +var env = global.env; + +var FATAL_ERROR_MESSAGE = 'Exiting JSDoc because an error occurred. See the previous log ' + + 'messages for details.'; +var cli = {}; + +// TODO: docs +cli.setVersionInfo = function() { + var fs = require('fs'); + var path = require('path'); + + // allow this to throw--something is really wrong if we can't read our own package file + var info = JSON.parse( fs.readFileSync(path.join(env.dirname, 'package.json'), 'utf8') ); + + env.version = { + number: info.version, + revision: new Date( parseInt(info.revision, 10) ).toUTCString() + }; + + return cli; +}; + +// TODO: docs +cli.loadConfig = function() { + var _ = require('underscore'); + var args = require('jsdoc/opts/args'); + var Config = require('jsdoc/config'); + var fs = require('jsdoc/fs'); + var path = require('jsdoc/path'); + + var confPath; + var isFile; + + var defaultOpts = { + destination: './out/', + encoding: 'utf8' + }; + + try { + env.opts = args.parse(env.args); + } + catch (e) { + cli.exit(1, e.message + '\n' + FATAL_ERROR_MESSAGE); + } + + confPath = env.opts.configure || path.join(env.dirname, 'conf.json'); + try { + isFile = fs.statSync(confPath).isFile(); + } + catch(e) { + isFile = false; + } + + if ( !isFile && !env.opts.configure ) { + confPath = path.join(env.dirname, 'conf.json.EXAMPLE'); + } + + try { + env.conf = new Config( stripJsonComments(fs.readFileSync(confPath, 'utf8')) ) + .get(); + } + catch (e) { + cli.exit(1, 'Cannot parse the config file ' + confPath + ': ' + e + '\n' + + FATAL_ERROR_MESSAGE); + } + + // look for options on the command line, in the config file, and in the defaults, in that order + env.opts = _.defaults(env.opts, env.conf.opts, defaultOpts); + + return cli; +}; + +// TODO: docs +cli.configureLogger = function() { + function recoverableError() { + props.shouldExitWithError = true; + } + + function fatalError() { + cli.exit(1); + } + + if (env.opts.debug) { + logger.setLevel(logger.LEVELS.DEBUG); + } + else if (env.opts.verbose) { + logger.setLevel(logger.LEVELS.INFO); + } + + if (env.opts.pedantic) { + logger.once('logger:warn', recoverableError); + logger.once('logger:error', fatalError); + } + else { + logger.once('logger:error', recoverableError); + } + + logger.once('logger:fatal', fatalError); + + return cli; +}; + +// TODO: docs +cli.logStart = function() { + logger.debug( cli.getVersion() ); + + logger.debug('Environment info: %j', { + env: { + conf: env.conf, + opts: env.opts + } + }); +}; + +// TODO: docs +cli.logFinish = function() { + var delta; + var deltaSeconds; + + if (env.run.finish && env.run.start) { + delta = env.run.finish.getTime() - env.run.start.getTime(); + } + + if (delta !== undefined) { + deltaSeconds = (delta / 1000).toFixed(2); + logger.info('Finished running in %s seconds.', deltaSeconds); + } +}; + +// TODO: docs +cli.runCommand = function(cb) { + var cmd; + + var opts = env.opts; + + function done(errorCode) { + if (!errorCode && props.shouldExitWithError) { + cb(1); + } + else { + cb(errorCode); + } + } + + if (opts.help) { + cmd = cli.printHelp; + } + else if (opts.test) { + cmd = cli.runTests; + } + else if (opts.version) { + cmd = cli.printVersion; + } + else { + cmd = cli.main; + } + + cmd(done); +}; + +// TODO: docs +cli.printHelp = function(cb) { + cli.printVersion(); + console.log( '\n' + require('jsdoc/opts/args').help() + '\n' ); + console.log('Visit http://usejsdoc.org for more information.'); + cb(0); +}; + +// TODO: docs +cli.runTests = function(cb) { + var path = require('jsdoc/path'); + + var runner = require( path.join(env.dirname, 'test/runner') ); + + console.log('Running tests...'); + runner(function(failCount) { + cb(failCount); + }); +}; + +// TODO: docs +cli.getVersion = function() { + return 'JSDoc ' + env.version.number + ' (' + env.version.revision + ')'; +}; + +// TODO: docs +cli.printVersion = function(cb) { + console.log( cli.getVersion() ); + + if (cb) { + cb(0); + } +}; + +// TODO: docs +cli.main = function(cb) { + cli.scanFiles(); + + if (env.sourceFiles.length) { + cli.createParser() + .parseFiles() + .processParseResults(); + } + else { + console.log('There are no input files to process.\n'); + cli.printHelp(cb); + } + + env.run.finish = new Date(); + cb(0); +}; + +function readPackageJson(filepath) { + var fs = require('jsdoc/fs'); + + try { + return stripJsonComments( fs.readFileSync(filepath, 'utf8') ); + } + catch (e) { + logger.error('Unable to read the package file "%s"', filepath); + return null; + } +} + +function buildSourceList() { + var fs = require('jsdoc/fs'); + var Readme = require('jsdoc/readme'); + + var packageJson; + var readmeHtml; + var sourceFile; + var sourceFiles = env.opts._ ? env.opts._.slice(0) : []; + + if (env.conf.source && env.conf.source.include) { + sourceFiles = sourceFiles.concat(env.conf.source.include); + } + + // load the user-specified package/README files, if any + if (env.opts.package) { + packageJson = readPackageJson(env.opts.package); + } + if (env.opts.readme) { + readmeHtml = new Readme(env.opts.readme).html; + } + + // source files named `package.json` or `README.md` get special treatment, unless the user + // explicitly specified a package and/or README file + for (var i = 0, l = sourceFiles.length; i < l; i++) { + sourceFile = sourceFiles[i]; + + if ( !env.opts.package && /\bpackage\.json$/i.test(sourceFile) ) { + packageJson = readPackageJson(sourceFile); + sourceFiles.splice(i--, 1); + } + + if ( !env.opts.readme && /(\bREADME|\.md)$/i.test(sourceFile) ) { + readmeHtml = new Readme(sourceFile).html; + sourceFiles.splice(i--, 1); + } + } + + props.packageJson = packageJson; + env.opts.readme = readmeHtml; + + return sourceFiles; +} + +// TODO: docs +cli.scanFiles = function() { + var Filter = require('jsdoc/src/filter').Filter; + + var filter; + + env.opts._ = buildSourceList(); + + // are there any files to scan and parse? + if (env.conf.source && env.opts._.length) { + filter = new Filter(env.conf.source); + + env.sourceFiles = app.jsdoc.scanner.scan(env.opts._, (env.opts.recurse ? 10 : undefined), + filter); + } + + return cli; +}; + +function resolvePluginPaths(paths) { + var path = require('jsdoc/path'); + + var pluginPaths = []; + + paths.forEach(function(plugin) { + var basename = path.basename(plugin); + var dirname = path.dirname(plugin); + var pluginPath = path.getResourcePath(dirname); + + if (!pluginPath) { + logger.error('Unable to find the plugin "%s"', plugin); + return; + } + + pluginPaths.push( path.join(pluginPath, basename) ); + }); + + return pluginPaths; +} + +cli.createParser = function() { + var handlers = require('jsdoc/src/handlers'); + var parser = require('jsdoc/src/parser'); + var path = require('jsdoc/path'); + var plugins = require('jsdoc/plugins'); + + app.jsdoc.parser = parser.createParser(env.conf.parser); + + if (env.conf.plugins) { + env.conf.plugins = resolvePluginPaths(env.conf.plugins); + plugins.installPlugins(env.conf.plugins, app.jsdoc.parser); + } + + handlers.attachTo(app.jsdoc.parser); + + return cli; +}; + +cli.parseFiles = function() { + var augment = require('jsdoc/augment'); + var borrow = require('jsdoc/borrow'); + var Package = require('jsdoc/package').Package; + + var docs; + var packageDocs; + + props.docs = docs = app.jsdoc.parser.parse(env.sourceFiles, env.opts.encoding); + + // If there is no package.json, just create an empty package + packageDocs = new Package(props.packageJson); + packageDocs.files = env.sourceFiles || []; + docs.push(packageDocs); + + logger.debug('Adding inherited symbols...'); + borrow.indexAll(docs); + augment.addInherited(docs); + augment.addImplemented(docs); + borrow.resolveBorrows(docs); + + app.jsdoc.parser.fireProcessingComplete(docs); + + return cli; +}; + +cli.processParseResults = function() { + if (env.opts.explain) { + cli.dumpParseResults(); + } + else { + cli.resolveTutorials(); + cli.generateDocs(); + } + + return cli; +}; + +cli.dumpParseResults = function() { + global.dump(props.docs); + + return cli; +}; + +cli.resolveTutorials = function() { + var resolver = require('jsdoc/tutorial/resolver'); + + if (env.opts.tutorials) { + resolver.load(env.opts.tutorials); + resolver.resolve(); + } + + return cli; +}; + +cli.generateDocs = function() { + var path = require('jsdoc/path'); + var resolver = require('jsdoc/tutorial/resolver'); + var taffy = require('taffydb').taffy; + + var template; + + env.opts.template = (function() { + var publish = env.opts.template || 'templates/default'; + var templatePath = path.getResourcePath(publish); + + // if we didn't find the template, keep the user-specified value so the error message is + // useful + return templatePath || env.opts.template; + })(); + + try { + template = require(env.opts.template + '/publish'); + } + catch(e) { + logger.fatal('Unable to load template: ' + e.message || e); + } + + // templates should include a publish.js file that exports a "publish" function + if (template.publish && typeof template.publish === 'function') { + logger.printInfo('Generating output files...'); + template.publish( + taffy(props.docs), + env.opts, + resolver.root + ); + logger.info('complete.'); + } + else { + logger.fatal(env.opts.template + ' does not export a "publish" function. Global ' + + '"publish" functions are no longer supported.'); + } + + return cli; +}; + +// TODO: docs +cli.exit = function(exitCode, message) { + if (message && exitCode > 0) { + console.error(message); + } + + process.exit(exitCode || 0); +}; + +return cli; +})(); diff --git a/third_party/jsdoc/conf.json.EXAMPLE b/third_party/jsdoc/conf.json.EXAMPLE new file mode 100644 index 0000000000..12bff5bde7 --- /dev/null +++ b/third_party/jsdoc/conf.json.EXAMPLE @@ -0,0 +1,17 @@ +{ + "tags": { + "allowUnknownTags": true + }, + "source": { + "includePattern": ".+\\.js(doc)?$", + "excludePattern": "(^|\\/|\\\\)_" + }, + "plugins": [], + "templates": { + "cleverLinks": false, + "monospaceLinks": false, + "default": { + "outputSourceFiles": true + } + } +} \ No newline at end of file diff --git a/third_party/jsdoc/gulpfile.js b/third_party/jsdoc/gulpfile.js new file mode 100644 index 0000000000..e54c468983 --- /dev/null +++ b/third_party/jsdoc/gulpfile.js @@ -0,0 +1,81 @@ +/*eslint max-nested-callbacks: 0 */ +'use strict'; + +var eslint = require('gulp-eslint'); +var exec = require('child_process').exec; +var gulp = require('gulp'); +var istanbul = require('istanbul'); +var jsonEditor = require('gulp-json-editor'); +var os = require('os'); +var path = require('path'); +var util = require('util'); + +function execCb(cb, err, stdout, stderr) { + console.log(stdout); + console.error(stderr); + cb(err); +} + +var options = { + coveragePaths: [ + '*.js', + 'lib/**/*.js', + 'plugins/*.js' + ], + lintPaths: [ + '*.js', + 'lib/**/*.js', + 'plugins/*.js', + 'templates/default/*.js', + 'templates/haruki/*.js' + ], + nodeBin: path.resolve(__dirname, './jsdoc.js'), + nodePath: process.execPath, + rhinoBin: (function() { + var filepath = path.resolve(__dirname, './jsdoc'); + + if (os.platform().indexOf('win') === 0) { + filepath += '.cmd'; + } + + return filepath; + })() +}; + +gulp.task('bump', function() { + gulp.src('./package.json') + .pipe(jsonEditor({ + revision: String( Date.now() ) + })) + .pipe(gulp.dest('./')); +}); + +gulp.task('coverage', function(cb) { + var cmd = util.format('./node_modules/.bin/istanbul cover %s -- -T', options.nodeBin); + exec(cmd, execCb.bind(null, cb)); +}); + +gulp.task('lint', function() { + var pipeline = gulp.src(options.lintPaths) + .pipe(eslint()) + .pipe(eslint.formatEach()) + .pipe(eslint.failOnError()); +}); + +gulp.task('test-node', function(cb) { + var cmd = util.format('%s "%s" -T', options.nodePath, options.nodeBin); + exec(cmd, execCb.bind(null, cb)); +}); + +gulp.task('test-rhino', function(cb) { + var cmd = util.format('"%s" -T -q "parser=rhino"', options.rhinoBin); + exec(cmd, execCb.bind(null, cb)); +}); + +gulp.task('test-rhino-esprima', function(cb) { + var cmd = util.format('"%s" -T -q "parser=esprima"', options.rhinoBin); + exec(cmd, execCb.bind(null, cb)); +}); + +gulp.task('test', ['lint', 'test-node', 'test-rhino', 'test-rhino-esprima']); +gulp.task('default', ['test']); diff --git a/third_party/jsdoc/jsdoc b/third_party/jsdoc/jsdoc new file mode 100755 index 0000000000..53c9c8f552 --- /dev/null +++ b/third_party/jsdoc/jsdoc @@ -0,0 +1,33 @@ +#!/bin/sh + +SOURCE="$0" +while [ -h "$SOURCE" ] ; do + NEXTSOURCE="$(readlink "$SOURCE")" + echo $NEXTSOURCE | grep -q -e "^/" + if [ $? = 0 ]; then + SOURCE="$NEXTSOURCE" + else + SOURCE="$(dirname $SOURCE)/$NEXTSOURCE" + fi +done +# Get a Windows path under MinGW or Cygwin +BASEPATH="$( cd -P "$( dirname "$SOURCE" )" && (pwd -W 2>/dev/null || cygpath -w $(pwd) 2>/dev/null || pwd))" +if [ "${BASEPATH%${BASEPATH#?}}" != "/" ] ; then + BASEPATH="$( echo "$BASEPATH" | sed -e 's@\\@/@g' )" +fi + +if test "$1" = "--debug" +then + CMD="org.mozilla.javascript.tools.debugger.Main -debug -opt -1" +else + CMD="org.mozilla.javascript.tools.shell.Main" +fi + +if test "$1" = "-T" +then + cd -P "$(dirname "$SOURCE")" + java -classpath "${BASEPATH}/rhino/js.jar" ${CMD} -opt -1 -modules "${BASEPATH}/lib" -modules "${BASEPATH}/node_modules" -modules "${BASEPATH}/rhino" -modules "${BASEPATH}" "${BASEPATH}/jsdoc.js" "$@" + +else + java -classpath "${BASEPATH}/rhino/js.jar" ${CMD} -modules "${BASEPATH}/lib" -modules "${BASEPATH}/node_modules" -modules "${BASEPATH}/rhino" -modules "${BASEPATH}" "${BASEPATH}/jsdoc.js" "$@" +fi diff --git a/third_party/jsdoc/jsdoc.cmd b/third_party/jsdoc/jsdoc.cmd new file mode 100644 index 0000000000..20156cd821 --- /dev/null +++ b/third_party/jsdoc/jsdoc.cmd @@ -0,0 +1,26 @@ +@ECHO OFF + +SETLOCAL + +REM jsdoc.js expects paths without a trailing slash +SET _BASEPATH=%~dp0 +SET _BASEPATH=%_BASEPATH:~0,-1% + +REM we need the ability to resolve paths relative to the user's working +REM directory prior to launching JSDoc +SET PWD=%cd% + +IF [%1]==[--debug] ( + SET CMD=org.mozilla.javascript.tools.debugger.Main -debug -opt -1 +) ELSE ( + SET CMD=org.mozilla.javascript.tools.shell.Main +) +SET ARGS=%* + +IF [%1]==[-T] ( + java -classpath "%_BASEPATH%/rhino/js.jar" %CMD% -opt -1 -modules "%_BASEPATH%/lib" -modules "%_BASEPATH%/node_modules" -modules "%_BASEPATH%/rhino" -modules "%_BASEPATH%" "%_BASEPATH%/jsdoc.js" %ARGS% --nocolor +) ELSE ( + java -classpath "%_BASEPATH%/rhino/js.jar" %CMD% -modules "%_BASEPATH%/lib" -modules "%_BASEPATH%/node_modules" -modules "%_BASEPATH%/rhino" -modules "%_BASEPATH%" "%_BASEPATH%/jsdoc.js" %ARGS% +) + +ENDLOCAL diff --git a/third_party/jsdoc/jsdoc.js b/third_party/jsdoc/jsdoc.js new file mode 100755 index 0000000000..5ea476d48f --- /dev/null +++ b/third_party/jsdoc/jsdoc.js @@ -0,0 +1,184 @@ +#!/usr/bin/env node +/*global arguments, require: true */ +/** + * @project jsdoc + * @author Michael Mathews + * @license See LICENSE.md file included in this distribution. + */ + +/** + * Data representing the environment in which this app is running. + * + * @namespace + * @name env + */ +global.env = { + /** + * Running start and finish times. + * + * @memberof env + */ + run: { + start: new Date(), + finish: null + }, + + /** + * The command-line arguments passed into JSDoc. + * + * @type Array + * @memberof env + */ + args: [], + + /** + * The parsed JSON data from the configuration file. + * + * @type Object + * @memberof env + */ + conf: {}, + + /** + * The absolute path to the base directory of the JSDoc application. + * + * @private + * @type string + * @memberof env + */ + dirname: '.', + + /** + * The user's working directory at the time that JSDoc was started. + * + * @private + * @type string + * @memberof env + */ + pwd: null, + + /** + * The command-line options, parsed into a key/value hash. + * + * @type Object + * @memberof env + * @example if (global.env.opts.help) { console.log('Helpful message.'); } + */ + opts: {}, + + /** + * The source files that JSDoc will parse. + * @type Array + * @memberof env + */ + sourceFiles: [], + + /** + * The JSDoc version number and revision date. + * + * @type Object + * @memberof env + */ + version: {} +}; + +// initialize the environment for the current JavaScript VM +(function(args) { + 'use strict'; + + var path; + + if (args[0] && typeof args[0] === 'object') { + // we should be on Node.js + args = [__dirname, process.cwd()]; + path = require('path'); + + // Create a custom require method that adds `lib/jsdoc` and `node_modules` to the module + // lookup path. This makes it possible to `require('jsdoc/foo')` from external templates and + // plugins, and within JSDoc itself. It also allows external templates and plugins to + // require JSDoc's module dependencies without installing them locally. + require = require('requizzle')({ + requirePaths: { + before: [path.join(__dirname, 'lib')], + after: [path.join(__dirname, 'node_modules')] + }, + infect: true + }); + } + + require('./lib/jsdoc/util/runtime').initialize(args); +})( Array.prototype.slice.call(arguments, 0) ); + +/** + * Data that must be shared across the entire application. + * + * @namespace + * @name app + */ +global.app = { + jsdoc: { + name: require('./lib/jsdoc/name'), + parser: null, + scanner: new (require('./lib/jsdoc/src/scanner').Scanner)() + } +}; + +/** + * Recursively print an object's properties to stdout. This method is safe to use with objects that + * contain circular references. In addition, on Mozilla Rhino, this method is safe to use with + * native Java objects. + * + * @global + * @name dump + * @private + * @param {Object} obj - Object(s) to print to stdout. + */ +global.dump = function() { + 'use strict'; + + var doop = require('./lib/jsdoc/util/doop').doop; + var _dump = require('./lib/jsdoc/util/dumper').dump; + for (var i = 0, l = arguments.length; i < l; i++) { + console.log( _dump(doop(arguments[i])) ); + } +}; + +(function() { + 'use strict'; + + var logger = require('./lib/jsdoc/util/logger'); + var runtime = require('./lib/jsdoc/util/runtime'); + var cli = require('./cli'); + + function cb(errorCode) { + cli.logFinish(); + cli.exit(errorCode || 0); + } + + cli.setVersionInfo() + .loadConfig(); + + if (!global.env.opts.test) { + cli.configureLogger(); + } + + cli.logStart(); + + // On Rhino, we use a try/catch block so we can log the Java exception (if available) + if ( runtime.isRhino() ) { + try { + cli.runCommand(cb); + } + catch(e) { + if (e.rhinoException) { + logger.fatal( e.rhinoException.printStackTrace() ); + } else { + console.trace(e); + cli.exit(1); + } + } + } + else { + cli.runCommand(cb); + } +})(); diff --git a/third_party/jsdoc/lib/jsdoc/augment.js b/third_party/jsdoc/lib/jsdoc/augment.js new file mode 100644 index 0000000000..00f47d167b --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/augment.js @@ -0,0 +1,270 @@ +'use strict'; + +var hasOwnProp = Object.prototype.hasOwnProperty; + +function mapDependencies(index) { + var doclets, doc, len, dependencies = {}; + + Object.keys(index).forEach(function(name) { + doclets = index[name]; + for (var i = 0, ii = doclets.length; i < ii; ++i) { + doc = doclets[i]; + if (doc.kind === 'class' || doc.kind === 'external') { + dependencies[name] = {}; + len = doc.augments && doc.augments.length || 0; + for (var j = 0; j < len; ++j) { + dependencies[name][doc.augments[j]] = true; + } + } + } + }); + + return dependencies; +} + +function Sorter(dependencies) { + this.dependencies = dependencies; + this.visited = {}; + this.sorted = []; +} + +Sorter.prototype.visit = function(key) { + var self = this; + + if (!(key in this.visited)) { + this.visited[key] = true; + + if (this.dependencies[key]) { + Object.keys(this.dependencies[key]).forEach(function(path) { + self.visit(path); + }); + } + + this.sorted.push(key); + } +}; + +Sorter.prototype.sort = function() { + var self = this; + + Object.keys(this.dependencies).forEach(function(key) { + self.visit(key); + }); + + return this.sorted; +}; + +function sort(dependencies) { + var sorter = new Sorter(dependencies); + return sorter.sort(); +} + +function getMembers(longname, docs) { + var candidate, members = []; + for (var i = 0, ii = docs.length; i < ii; ++i) { + candidate = docs[i]; + if (candidate.memberof === longname && candidate.scope === 'instance') { + members.push(candidate); + } + } + return members; +} + +function addOverridesProperty(doclets, overrides) { + for (var i = 0, l = doclets.length; i < l; i++) { + doclets[i].overrides = overrides; + } +} + +function getAdditions(doclets, docs, documented) { + var doop = require('jsdoc/util/doop'); + + var additionIndexes; + var additions = []; + var doc; + var parents; + var members; + var member; + var parts; + + // doclets will be undefined if the inherited symbol isn't documented + doclets = doclets || []; + + for (var i = 0, ii = doclets.length; i < ii; i++) { + doc = doclets[i]; + parents = doc.augments; + if (parents && doc.kind === 'class') { + // reset the lookup table of added doclet indexes by longname + additionIndexes = {}; + for (var j = 0, jj = parents.length; j < jj; j++) { + members = getMembers(parents[j], docs); + for (var k = 0, kk = members.length; k < kk; k++) { + member = doop(members[k]); + + if (!member.inherited) { + member.inherits = member.longname; + } + member.inherited = true; + + // Remove the `overrides` property if present. (For classes A > B > C, if B#a + // overrides A#a, and C#a inherits B#a, we don't want the doclet for C#a to say + // that it overrides A#a.) + if (member.overrides) { + delete member.overrides; + } + + // TODO: this will fail on longnames like: MyClass#"quoted#Longname" + // and nested instance members like: MyClass#MyOtherClass#myMethod + member.memberof = doc.longname; + parts = member.longname.split('#'); + parts[0] = doc.longname; + member.longname = parts.join('#'); + + // Add the ancestor's docs, unless the descendant overrides the ancestor AND + // documents the override. + if ( !hasOwnProp.call(documented, member.longname) ) { + // We add only one doclet per longname. If you inherit from two classes that + // both use the same method name, you get docs for one method rather than + // two. Last one wins; if you write `@extends Class1 @extends Class2`, and + // both classes have the instance method `myMethod`, you get the `myMethod` + // docs from Class2. + if (typeof additionIndexes[member.longname] !== 'undefined') { + // replace the existing doclet + additions[additionIndexes[member.longname]] = member; + } + else { + // add the doclet to the array, and track its index + additions.push(member); + additionIndexes[member.longname] = additions.length - 1; + } + } + // If the descendant is documented and overrides an ancestor, indicate what the + // descendant is overriding. + else { + addOverridesProperty(documented[member.longname], members[k].longname); + } + } + } + } + } + + return additions; +} + +exports.addInherited = function(docs) { + var index = docs.index.longname; + var dependencies = mapDependencies(index); + var sorted = sort(dependencies); + + sorted.forEach(function(name) { + var doclets = index[name]; + var additions = getAdditions(doclets, docs, docs.index.documented); + + additions.forEach(function(doc) { + var longname = doc.longname; + + if ( !hasOwnProp.call(index, longname) ) { + index[longname] = []; + } + index[longname].push(doc); + docs.push(doc); + }); + }); +}; + +// TODO: move as much of this as possible to jsdoc/borrow.indexAll +exports.addImplemented = function(docs) { + var docMap = {}; + var interfaces = {}; + var implemented = {}; + var memberInfo = {}; + + docs.forEach(function(doc) { + var memberof = doc.memberof || doc.name; + + if (!hasOwnProp.call(docMap, memberof)) { + docMap[memberof] = []; + } + docMap[memberof].push(doc); + + if (doc.kind === 'interface') { + interfaces[doc.longname] = doc; + } + else if (doc.implements && doc.implements.length) { + if (!hasOwnProp.call(implemented, doc.memberof)) { + implemented[memberof] = []; + } + implemented[memberof].push(doc); + } + }); + + // create an dictionary of interface doclets + Object.keys(interfaces).forEach(function(ifaceName) { + var iface = interfaces[ifaceName]; + if (hasOwnProp.call(docMap, iface.longname)) { + docMap[iface.longname].forEach(function(doc) { + var members = memberInfo[doc.memberof]; + + if (!members) { + members = memberInfo[doc.memberof] = {}; + } + members[doc.name] = doc; + }); + } + }); + + Object.keys(implemented).forEach(function(key) { + // implemented classes namespace. + var owner = implemented[key]; + + owner.forEach(function(klass) { + // class's interfaces + klass.implements.forEach(function(impl) { + var interfaceMember; + var interfaceMembers = memberInfo[impl]; + var member; + var members; + + // mark the interface as being implemented by the class + if (hasOwnProp.call(interfaces, impl)) { + interfaces[impl].implementations = interfaces[impl].implementations || []; + interfaces[impl].implementations.push(klass.longname); + } + + // if the interface has no members, skip to the next owner + if (!interfaceMembers) { + return; + } + + if (!hasOwnProp.call(docMap, klass.longname)) { + docMap[klass.longname] = []; + } + members = docMap[klass.longname]; + + for (var i = 0, len = members.length; i < len; i++) { + member = members[i]; + interfaceMember = interfaceMembers && interfaceMembers[member.name]; + + // if we didn't find the member name in the interface, skip to the next member + if (!interfaceMember) { + continue; + } + + // mark members that implement an interface + member.implements = member.implements || []; + member.implements.push(interfaceMember.longname); + + // mark interface members that the symbol implements + interfaceMember.implementations = interfaceMember.implementations || []; + interfaceMember.implementations.push(member.longname); + + // inherit docs from the interface if not given in the implementation + member.description = member.description || interfaceMember.description; + member.params = member.params || interfaceMember.params; + member.returns = member.returns || interfaceMember.returns; + member.fires = member.fires || interfaceMember.fires; + } + }); + }); + }); +}; diff --git a/third_party/jsdoc/lib/jsdoc/borrow.js b/third_party/jsdoc/lib/jsdoc/borrow.js new file mode 100644 index 0000000000..5c49882b52 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/borrow.js @@ -0,0 +1,107 @@ +/** + A collection of functions relating to resolving @borrows tags in JSDoc symbols. + @module jsdoc/borrow + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ +'use strict'; + +var doop = require('jsdoc/util/doop'); +var logger = require('jsdoc/util/logger'); +var SCOPE = require('jsdoc/name').SCOPE; + +var hasOwnProp = Object.prototype.hasOwnProperty; + +// TODO: add the index at parse time, so we don't have to iterate over all the doclets again +exports.indexAll = function(doclets) { + var borrowed = []; + var doclet; + var documented = {}; + var longname = {}; + + for (var i = 0, l = doclets.length; i < l; i++) { + doclet = doclets[i]; + + // track all doclets by longname + if ( !hasOwnProp.call(longname, doclet.longname) ) { + longname[doclet.longname] = []; + } + longname[doclet.longname].push(doclet); + + // track longnames of documented symbols + if (!doclet.undocumented) { + if ( !hasOwnProp.call(documented, doclet.longname) ) { + documented[doclet.longname] = []; + } + documented[doclet.longname].push(doclet); + } + + // track doclets with a `borrowed` property + if ( hasOwnProp.call(doclet, 'borrowed') ) { + borrowed.push(doclet); + } + } + + doclets.index = { + borrowed: borrowed, + documented: documented, + longname: longname + }; +}; + +function cloneBorrowedDoclets(doclet, doclets) { + doclet.borrowed.forEach(function(borrowed) { + var borrowedDoclets = doclets.index.longname[borrowed.from]; + var borrowedAs = borrowed.as || borrowed.from; + var clonedDoclets; + var parts; + var scopePunc; + + if (borrowedDoclets) { + borrowedAs = borrowedAs.replace(/^prototype\./, SCOPE.PUNC.INSTANCE); + clonedDoclets = doop(borrowedDoclets).forEach(function(clone) { + // TODO: this will fail on longnames like '"Foo#bar".baz' + parts = borrowedAs.split(SCOPE.PUNC.INSTANCE); + + if (parts.length === 2) { + clone.scope = SCOPE.NAMES.INSTANCE; + scopePunc = SCOPE.PUNC.INSTANCE; + } + else { + clone.scope = SCOPE.NAMES.STATIC; + scopePunc = SCOPE.PUNC.STATIC; + } + + clone.name = parts.pop(); + clone.memberof = doclet.longname; + clone.longname = clone.memberof + scopePunc + clone.name; + doclets.push(clone); + }); + } + }); +} + +// requires docs to have been indexed: docs.index must be defined here +/** + Take a copy of the docs for borrowed symbols and attach them to the + docs for the borrowing symbol. This process changes the symbols involved, + moving docs from the "borrowed" array and into the general docs, then + deleting the "borrowed" array. + */ +exports.resolveBorrows = function(doclets) { + var doclet; + + if (!doclets.index) { + logger.error('Unable to resolve borrowed symbols, because the docs have not been indexed.'); + return; + } + + for (var i = 0, l = doclets.index.borrowed.length; i < l; i++) { + doclet = doclets.index.borrowed[i]; + + cloneBorrowedDoclets(doclet, doclets); + delete doclet.borrowed; + } + + doclets.index.borrowed = []; +}; diff --git a/third_party/jsdoc/lib/jsdoc/config.js b/third_party/jsdoc/lib/jsdoc/config.js new file mode 100644 index 0000000000..ee09b6846d --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/config.js @@ -0,0 +1,60 @@ +/** + @overview + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ + +/** + @module jsdoc/config + */ +'use strict'; + +function mergeRecurse(target, source) { + Object.keys(source).forEach(function(p) { + if ( source[p].constructor === Object ) { + if ( !target[p] ) { target[p] = {}; } + mergeRecurse(target[p], source[p]); + } + else { + target[p] = source[p]; + } + }); + + return target; +} + +// required config values, override these defaults in your config.json if necessary +var defaults = { + tags: { + allowUnknownTags: true, + dictionaries: ['jsdoc', 'closure'] + }, + templates: { + monospaceLinks: false, + cleverLinks: false + }, + source: { + includePattern: '.+\\.js(doc)?$', + excludePattern: '' + }, + plugins: [] +}; + +/** + @class + @classdesc Represents a JSDoc application configuration. + @param {string} [json] - The contents of config.json. + */ +function Config(json) { + json = JSON.parse( (json || '{}') ); + this._config = mergeRecurse(defaults, json); +} + +module.exports = Config; + +/** + Get the merged configuration values. + */ +Config.prototype.get = function() { + return this._config; +}; diff --git a/third_party/jsdoc/lib/jsdoc/doclet.js b/third_party/jsdoc/lib/jsdoc/doclet.js new file mode 100644 index 0000000000..0513db2d67 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/doclet.js @@ -0,0 +1,426 @@ +/** + * @overview + * @author Michael Mathews + * @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ + +/** + * @module jsdoc/doclet + */ +'use strict'; + +var _ = require('underscore'); +var jsdoc = { + name: require('jsdoc/name'), + src: { + astnode: require('jsdoc/src/astnode'), + Syntax: require('jsdoc/src/syntax').Syntax + }, + tag: { + Tag: require('jsdoc/tag').Tag, + dictionary: require('jsdoc/tag/dictionary') + } +}; +var path = require('jsdoc/path'); +var Syntax = jsdoc.src.Syntax; +var util = require('util'); + +function applyTag(doclet, tag) { + if (tag.title === 'name') { + doclet.name = tag.value; + } + + if (tag.title === 'kind') { + doclet.kind = tag.value; + } + + if (tag.title === 'description') { + doclet.description = tag.value; + } +} + +// use the meta info about the source code to guess what the doclet kind should be +function codeToKind(code) { + var parent; + + var isFunction = jsdoc.src.astnode.isFunction; + + // default + var kind = 'member'; + + if (code.type === Syntax.FunctionDeclaration || code.type === Syntax.FunctionExpression) { + kind = 'function'; + } + else if (code.node && code.node.parent) { + parent = code.node.parent; + if ( isFunction(parent) ) { + kind = 'param'; + } + } + + return kind; +} + +function unwrap(docletSrc) { + if (!docletSrc) { return ''; } + + // note: keep trailing whitespace for @examples + // extra opening/closing stars are ignored + // left margin is considered a star and a space + // use the /m flag on regex to avoid having to guess what this platform's newline is + docletSrc = + docletSrc.replace(/^\/\*\*+/, '') // remove opening slash+stars + .replace(/\**\*\/$/, '\\Z') // replace closing star slash with end-marker + .replace(/^\s*(\* ?|\\Z)/gm, '') // remove left margin like: spaces+star or spaces+end-marker + .replace(/\s*\\Z$/g, ''); // remove end-marker + + return docletSrc; +} + +function split(docletSrc) { + var tagSrcs = []; + + // split out the basic tags, keep surrounding whitespace + // like: @tagTitle tagBody + docletSrc + .replace(/^(\s*)@(\S)/gm, '$1\\@$2') // replace splitter ats with an arbitrary sequence + .split('\\@') // then split on that arbitrary sequence + .forEach(function($) { + var parsedTag; + var tagText; + var tagTitle; + + if ($) { + parsedTag = $.match(/^(\S+)(:?\s+(\S[\s\S]*))?/); + + if (parsedTag) { + // we don't need parsedTag[0] + tagTitle = parsedTag[1]; + tagText = parsedTag[2]; + + if (tagTitle) { + tagSrcs.push({ + title: tagTitle, + text: tagText + }); + } + } + } + }); + + return tagSrcs; +} + +/** + * Convert the raw source of the doclet comment into an array of Tag objects. + * @private + */ +function toTags(docletSrc) { + var tags = []; + var tagSrcs = split(docletSrc); + + for (var i = 0, l = tagSrcs.length; i < l; i++) { + tags.push({ title: tagSrcs[i].title, text: tagSrcs[i].text }); + } + + return tags; +} + +function fixDescription(docletSrc) { + if (!/^\s*@/.test(docletSrc) && docletSrc.replace(/\s/g, '').length) { + docletSrc = '@description ' + docletSrc; + } + return docletSrc; +} + +/** + * Replace the existing tag dictionary with a new tag dictionary. + * + * Used for testing only. + * + * @private + * @param {module:jsdoc/tag/dictionary.Dictionary} dict - The new tag dictionary. + */ +exports._replaceDictionary = function _replaceDictionary(dict) { + jsdoc.tag.dictionary = dict; + require('jsdoc/tag')._replaceDictionary(dict); +}; + +/** + * @class + * @classdesc Represents a single JSDoc comment. + * @param {string} docletSrc - The raw source code of the jsdoc comment. + * @param {object=} meta - Properties describing the code related to this comment. + */ +var Doclet = exports.Doclet = function(docletSrc, meta) { + var newTags = []; + + /** The original text of the comment from the source code. */ + this.comment = docletSrc; + this.setMeta(meta); + + docletSrc = unwrap(docletSrc); + docletSrc = fixDescription(docletSrc); + + newTags = toTags.call(this, docletSrc); + + for (var i = 0, l = newTags.length; i < l; i++) { + this.addTag(newTags[i].title, newTags[i].text); + } + + this.postProcess(); +}; + +/** Called once after all tags have been added. */ +Doclet.prototype.postProcess = function() { + var i; + var l; + + if (!this.preserveName) { + jsdoc.name.resolve(this); + } + if (this.name && !this.longname) { + this.setLongname(this.name); + } + if (this.memberof === '') { + delete this.memberof; + } + + if (!this.kind && this.meta && this.meta.code) { + this.addTag( 'kind', codeToKind(this.meta.code) ); + } + + if (this.variation && this.longname && !/\)$/.test(this.longname) ) { + this.longname += '(' + this.variation + ')'; + } + + // add in any missing param names + if (this.params && this.meta && this.meta.code && this.meta.code.paramnames) { + for (i = 0, l = this.params.length; i < l; i++) { + if (!this.params[i].name) { + this.params[i].name = this.meta.code.paramnames[i] || ''; + } + } + } +}; + +/** + * Add a tag to the doclet. + * + * @param {string} title - The title of the tag being added. + * @param {string} [text] - The text of the tag being added. + */ +Doclet.prototype.addTag = function(title, text) { + var tagDef = jsdoc.tag.dictionary.lookUp(title), + newTag = new jsdoc.tag.Tag(title, text, this.meta); + + if (tagDef && tagDef.onTagged) { + tagDef.onTagged(this, newTag); + } + + if (!tagDef) { + this.tags = this.tags || []; + this.tags.push(newTag); + } + + applyTag(this, newTag); +}; + +function removeGlobal(longname) { + var globalRegexp = new RegExp('^' + jsdoc.name.LONGNAMES.GLOBAL + '\\.?'); + + return longname.replace(globalRegexp, ''); +} + +/** + * Set the doclet's `memberof` property. + * + * @param {string} sid - The longname of the doclet's parent symbol. + */ +Doclet.prototype.setMemberof = function(sid) { + /** + * The longname of the symbol that contains this one, if any. + * @type string + */ + this.memberof = removeGlobal(sid) + .replace(/\.prototype/g, jsdoc.name.SCOPE.PUNC.INSTANCE); +}; + +/** + * Set the doclet's `longname` property. + * + * @param {string} name - The longname for the doclet. + */ +Doclet.prototype.setLongname = function(name) { + /** + * The fully resolved symbol name. + * @type string + */ + this.longname = removeGlobal(name); + if (jsdoc.tag.dictionary.isNamespace(this.kind)) { + this.longname = jsdoc.name.applyNamespace(this.longname, this.kind); + } +}; + +/** + * Get the full path to the source file that is associated with a doclet. + * + * @private + * @param {module:jsdoc/doclet.Doclet} The doclet to check for a filepath. + * @return {string} The path to the doclet's source file, or an empty string if the path is not + * available. + */ +function getFilepath(doclet) { + if (!doclet || !doclet.meta || !doclet.meta.filename) { + return ''; + } + + return path.join(doclet.meta.path || '', doclet.meta.filename); +} + +/** + * Set the doclet's `scope` property. Must correspond to a scope name that is defined in + * {@link module:jsdoc/name.SCOPE.NAMES}. + * + * @param {module:jsdoc/name.SCOPE.NAMES} scope - The scope for the doclet relative to the symbol's + * parent. + * @throws {Error} If the scope name is not recognized. + */ +Doclet.prototype.setScope = function(scope) { + var errorMessage; + var filepath; + var scopeNames = _.values(jsdoc.name.SCOPE.NAMES); + + if (scopeNames.indexOf(scope) === -1) { + filepath = getFilepath(this); + + errorMessage = util.format('The scope name "%s" is not recognized. Use one of the ' + + 'following values: %j', scope, scopeNames); + if (filepath) { + errorMessage += util.format(' (Source file: %s)', filepath); + } + + throw new Error(errorMessage); + } + + this.scope = scope; +}; + +/** + * Add a symbol to this doclet's `borrowed` array. + * + * @param {string} source - The longname of the symbol that is the source. + * @param {string} target - The name the symbol is being assigned to. + */ +Doclet.prototype.borrow = function(source, target) { + var about = { from: source }; + if (target) { + about.as = target; + } + + if (!this.borrowed) { + /** + * A list of symbols that are borrowed by this one, if any. + * @type Array. + */ + this.borrowed = []; + } + this.borrowed.push(about); +}; + +Doclet.prototype.mix = function(source) { + /** + * A list of symbols that are mixed into this one, if any. + * @type Array. + */ + this.mixes = this.mixes || []; + this.mixes.push(source); +}; + +/** + * Add a symbol to the doclet's `augments` array. + * + * @param {string} base - The longname of the base symbol. + */ +Doclet.prototype.augment = function(base) { + /** + * A list of symbols that are augmented by this one, if any. + * @type Array. + */ + this.augments = this.augments || []; + this.augments.push(base); +}; + +/** + * Set the `meta` property of this doclet. + * + * @param {object} meta + */ +Doclet.prototype.setMeta = function(meta) { + /** + * Information about the source code associated with this doclet. + * @namespace + */ + this.meta = this.meta || {}; + + if (meta.range) { + /** + * The positions of the first and last characters of the code associated with this doclet. + * @type Array. + */ + this.meta.range = meta.range.slice(0); + } + + if (meta.lineno) { + /** + * The name of the file containing the code associated with this doclet. + * @type string + */ + this.meta.filename = path.basename(meta.filename); + /** + * The line number of the code associated with this doclet. + * @type number + */ + this.meta.lineno = meta.lineno; + + var pathname = path.dirname(meta.filename); + if (pathname && pathname !== '.') { + this.meta.path = pathname; + } + } + + /** + * Information about the code symbol. + * @namespace + */ + this.meta.code = this.meta.code || {}; + if (meta.id) { this.meta.code.id = meta.id; } + if (meta.code) { + if (meta.code.name) { + /** The name of the symbol in the source code. */ + this.meta.code.name = meta.code.name; + } + if (meta.code.type) { + /** The type of the symbol in the source code. */ + this.meta.code.type = meta.code.type; + } + // the AST node is only enumerable in debug mode, which reduces clutter for the + // --explain/-X option + if (meta.code.node) { + Object.defineProperty(this.meta.code, 'node', { + value: meta.code.node, + enumerable: global.env.opts.debug ? true : false + }); + } + if (meta.code.funcscope) { + this.meta.code.funcscope = meta.code.funcscope; + } + if (meta.code.value) { + /** The value of the symbol in the source code. */ + this.meta.code.value = meta.code.value; + } + if (meta.code.paramnames) { + this.meta.code.paramnames = meta.code.paramnames.slice(0); + } + } +}; diff --git a/third_party/jsdoc/lib/jsdoc/fs.js b/third_party/jsdoc/lib/jsdoc/fs.js new file mode 100644 index 0000000000..4bb8966e4a --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/fs.js @@ -0,0 +1,74 @@ +/** + * Extended version of the standard `fs` module. + * @module jsdoc/fs + */ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var runtime = require('jsdoc/util/runtime'); + +var ls = exports.ls = function(dir, recurse, _allFiles, _path) { + var file; + var files; + var isFile; + + // first pass + if (_path === undefined) { + _allFiles = []; + _path = [dir]; + } + + if (!_path.length) { + return _allFiles; + } + + if (recurse === undefined) { + recurse = 1; + } + + try { + isFile = fs.statSync(dir).isFile(); + } + catch (e) { + isFile = false; + } + if (isFile) { + files = [dir]; + } + else { + files = fs.readdirSync(dir); + } + + for (var i = 0, l = files.length; i < l; i++) { + file = String(files[i]); + + // skip dot files + if (file.match(/^\.[^\.\/\\]/)) { + continue; + } + + if ( fs.statSync(path.join(_path.join('/'), file)).isDirectory() ) { + // it's a directory + _path.push(file); + + if (_path.length - 1 < recurse) { + ls(_path.join('/'), recurse, _allFiles, _path); + } + _path.pop(); + } + else { + // it's a file + _allFiles.push( path.normalize(path.join(_path.join('/'), file)) ); + } + } + + return _allFiles; +}; + +// export the VM-specific implementations of the extra methods +// TODO: document extra methods here +var extras = require( runtime.getModulePath('fs') ); +Object.keys(extras).forEach(function(extra) { + exports[extra] = extras[extra]; +}); diff --git a/third_party/jsdoc/lib/jsdoc/name.js b/third_party/jsdoc/lib/jsdoc/name.js new file mode 100644 index 0000000000..f2dc3f32bf --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/name.js @@ -0,0 +1,424 @@ +/** + A collection of functions relating to JSDoc symbol name manipulation. + @module jsdoc/name + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ +'use strict'; + +var _ = require('underscore'); +var escape = require('escape-string-regexp'); + +var hasOwnProp = Object.prototype.hasOwnProperty; + +/** + * Longnames that have a special meaning in JSDoc. + * + * @enum {string} + * @static + * @memberof module:jsdoc/name + */ +var LONGNAMES = exports.LONGNAMES = { + /** Longname used for doclets that do not have a longname, such as anonymous functions. */ + ANONYMOUS: '', + /** Longname that represents global scope. */ + GLOBAL: '' +}; + +// TODO: Consider removing this rather than completing the list. In theory, any new tag can act as +// a namespace by setting its `isNamespace` attribute to `true`. +/** + * Namespaces that can be applied to a longname. + * + * @enum {string} + * @static + * @memberof module:jsdoc/name + */ +var NAMESPACES = exports.NAMESPACES = { + MODULE: 'module:' +}; + +/** + * Names and punctuation marks that identify doclet scopes. + * + * @enum {string} + * @static + * @memberof module:jsdoc/name + */ +var SCOPE = exports.SCOPE = { + NAMES: { + GLOBAL: 'global', + INNER: 'inner', + INSTANCE: 'instance', + STATIC: 'static' + }, + PUNC: { + INNER: '~', + INSTANCE: '#', + STATIC: '.' + } +}; + +// For backwards compatibility, this enum must use lower-case keys +var scopeToPunc = exports.scopeToPunc = { + 'inner': SCOPE.PUNC.INNER, + 'instance': SCOPE.PUNC.INSTANCE, + 'static': SCOPE.PUNC.STATIC +}; +var puncToScope = exports.puncToScope = _.invert(scopeToPunc); + +var DEFAULT_SCOPE = SCOPE.NAMES.STATIC; +var SCOPE_PUNC = _.values(SCOPE.PUNC); +var SCOPE_PUNC_STRING = '[' + SCOPE_PUNC.join() + ']'; +var REGEXP_LEADING_SCOPE = new RegExp('^(' + SCOPE_PUNC_STRING + ')'); +var REGEXP_TRAILING_SCOPE = new RegExp('(' + SCOPE_PUNC_STRING + ')$'); + +var DESCRIPTION = '(?:(?:[ \\t]*\\-\\s*|\\s+)(\\S[\\s\\S]*))?$'; +var REGEXP_DESCRIPTION = new RegExp(DESCRIPTION); +var REGEXP_NAME_DESCRIPTION = new RegExp('^(\\[[^\\]]+\\]|\\S+)' + DESCRIPTION); + +function nameIsLongname(name, memberof) { + var regexp = new RegExp('^' + escape(memberof) + SCOPE_PUNC_STRING); + + return regexp.test(name); +} + +function prototypeToPunc(name) { + return name.replace(/(?:^|\.)prototype\.?/g, SCOPE.PUNC.INSTANCE); +} + +/** + Resolves the longname, memberof, variation and name values of the given doclet. + @param {module:jsdoc/doclet.Doclet} doclet + */ +exports.resolve = function(doclet) { + var about = {}; + var memberof = doclet.memberof || ''; + var name = doclet.name ? String(doclet.name) : ''; + + var parentDoc; + + // change MyClass.prototype.instanceMethod to MyClass#instanceMethod + // (but not in function params, which lack doclet.kind) + // TODO: check for specific doclet.kind values (probably function, class, and module) + if (name && doclet.kind) { + name = prototypeToPunc(name); + } + doclet.name = name; + + // member of a var in an outer scope? + if (name && !memberof && doclet.meta.code && doclet.meta.code.funcscope) { + name = doclet.longname = doclet.meta.code.funcscope + SCOPE.PUNC.INNER + name; + } + + if (memberof || doclet.forceMemberof) { // @memberof tag given + memberof = prototypeToPunc(memberof); + + // the name is a complete longname, like @name foo.bar, @memberof foo + if (name && nameIsLongname(name, memberof) && name !== memberof) { + about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined)); + } + // the name and memberof are identical and refer to a module, + // like @name module:foo, @memberof module:foo (probably a member like 'var exports') + else if (name && name === memberof && name.indexOf(NAMESPACES.MODULE) === 0) { + about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined)); + } + // the name and memberof are identical, like @name foo, @memberof foo + else if (name && name === memberof) { + doclet.scope = doclet.scope || DEFAULT_SCOPE; + name = memberof + scopeToPunc[doclet.scope] + name; + about = exports.shorten(name, (doclet.forceMemberof ? memberof : undefined)); + } + // like @memberof foo# or @memberof foo~ + else if (name && REGEXP_TRAILING_SCOPE.test(memberof) ) { + about = exports.shorten(memberof + name, (doclet.forceMemberof ? memberof : undefined)); + } + else if (name && doclet.scope) { + about = exports.shorten(memberof + (scopeToPunc[doclet.scope] || '') + name, + (doclet.forceMemberof ? memberof : undefined)); + } + } + else { // no @memberof + about = exports.shorten(name); + } + + if (about.name) { + doclet.name = about.name; + } + + if (about.memberof) { + doclet.setMemberof(about.memberof); + } + + if (about.longname && (!doclet.longname || doclet.longname === doclet.name)) { + doclet.setLongname(about.longname); + } + + if (doclet.scope === SCOPE.NAMES.GLOBAL) { // via @global tag? + doclet.setLongname(doclet.name); + delete doclet.memberof; + } + else if (about.scope) { + if (about.memberof === LONGNAMES.GLOBAL) { // via @memberof ? + doclet.scope = SCOPE.NAMES.GLOBAL; + } + else { + doclet.scope = puncToScope[about.scope]; + } + } + else if (doclet.name && doclet.memberof && !doclet.longname) { + if ( REGEXP_LEADING_SCOPE.test(doclet.name) ) { + doclet.scope = puncToScope[RegExp.$1]; + doclet.name = doclet.name.substr(1); + } + else { + doclet.scope = DEFAULT_SCOPE; + } + + doclet.setLongname(doclet.memberof + scopeToPunc[doclet.scope] + doclet.name); + } + + if (about.variation) { + doclet.variation = about.variation; + } + + // if we never found a longname, just use an empty string + if (!doclet.longname) { + doclet.longname = ''; + } +}; + +/** + @method module:jsdoc/name.applyNamespace + @param {string} longname The full longname of the symbol. + @param {string} ns The namespace to be applied. + @returns {string} The longname with the namespace applied. + */ +exports.applyNamespace = function(longname, ns) { + var nameParts = exports.shorten(longname), + name = nameParts.name; + longname = nameParts.longname; + + if ( !/^[a-zA-Z]+?:.+$/i.test(name) ) { + longname = longname.replace( new RegExp(escape(name) + '$'), ns + ':' + name ); + } + + return longname; +}; + +// TODO: docs +function shorten(longname, sliceChars, forcedMemberof) { + var i; + var memberof = ''; + var name = ''; + var parts; + var partsRegExp; + var scope = ''; + var token; + var tokens = []; + var variation; + + // quoted strings in a longname are atomic, so we convert them to tokens + longname = longname.replace(/(\[?["'].+?["']\]?)/g, function($) { + var dot = ''; + if ( /^\[/.test($) ) { + dot = '.'; + $ = $.replace( /^\[/g, '' ).replace( /\]$/g, '' ); + } + + token = '@{' + tokens.length + '}@'; + tokens.push($); + + return dot + token; // foo["bar"] => foo.@{1}@ + }); + + longname = prototypeToPunc(longname); + + if (forcedMemberof !== undefined) { + partsRegExp = new RegExp('^(.*?)([' + sliceChars.join() + ']?)$'); + name = longname.substr(forcedMemberof.length); + parts = forcedMemberof.match(partsRegExp); + + if (parts[1]) { + memberof = parts[1] || forcedMemberof; + } + if (parts[2]) { + scope = parts[2]; + } + } + else if (longname) { + parts = (longname.match(new RegExp('^(:?(.+)([' + sliceChars.join() + ']))?(.+?)$')) || []) + .reverse(); + name = parts[0] || ''; + scope = parts[1] || ''; + memberof = parts[2] || ''; + } + + // like /** @name foo.bar(2) */ + if ( /(.+)\(([^)]+)\)$/.test(name) ) { + name = RegExp.$1; + variation = RegExp.$2; + } + + // restore quoted strings + i = tokens.length; + while (i--) { + longname = longname.replace('@{' + i + '}@', tokens[i]); + memberof = memberof.replace('@{' + i + '}@', tokens[i]); + scope = scope.replace('@{' + i + '}@', tokens[i]); + name = name.replace('@{' + i + '}@', tokens[i]); + } + + return {longname: longname, memberof: memberof, scope: scope, name: name, variation: variation}; +} + +/** + Given a longname like "a.b#c(2)", slice it up into an object + containing the memberof, the scope, the name, and variation. + @param {string} longname + @param {string} forcedMemberof + @returns {object} Representing the properties of the given name. + */ +exports.shorten = function(longname, forcedMemberof) { + return shorten(longname, SCOPE_PUNC, forcedMemberof); +}; + +function stripVariation(name) { + return name.replace(/\([^)]\)$/, ''); +} + +function splitLongname(longname, options) { + var chunks = []; + var currentNameInfo; + var nameInfo = {}; + var previousName = longname; + var splitters = SCOPE_PUNC.concat('/'); + + options = _.defaults(options || {}, { + includeVariation: true + }); + + do { + if (!options.includeVariation) { + previousName = stripVariation(previousName); + } + currentNameInfo = nameInfo[previousName] = shorten(previousName, splitters); + previousName = currentNameInfo.memberof; + chunks.push(currentNameInfo.scope + currentNameInfo.name); + } while (previousName); + + return { + chunks: chunks.reverse(), + nameInfo: nameInfo + }; +} + +// TODO: docs +exports.longnamesToTree = function longnamesToTree(longnames, doclets) { + var splitOptions = { includeVariation: false }; + var tree = {}; + + longnames.forEach(function(longname) { + var chunk; + var currentLongname = ''; + var currentNavItem = tree; + var nameInfo; + var processed; + + // don't try to add empty longnames to the tree + if (!longname) { + return; + } + + processed = splitLongname(longname, splitOptions); + nameInfo = processed.nameInfo; + + processed.chunks.forEach(function(chunk) { + currentLongname += chunk; + + if (!hasOwnProp.call(currentNavItem, chunk)) { + currentNavItem[chunk] = nameInfo[currentLongname]; + } + + if (currentNavItem[chunk]) { + currentNavItem[chunk].doclet = doclets ? doclets[currentLongname] : null; + currentNavItem[chunk].children = currentNavItem[chunk].children || {}; + currentNavItem = currentNavItem[chunk].children; + } + }); + }); + + return tree; +}; + +/** + Split a string that starts with a name and ends with a description into its parts. + Allows the defaultvalue (if present) to contain brackets. If the name is found to have + mismatched brackets, null is returned. + @param {string} nameDesc + @returns {object} Hash with "name" and "description" properties. + */ +function splitNameMatchingBrackets(nameDesc) { + var buffer = []; + var c; + var stack = 0; + var stringEnd = null; + + for (var i = 0; i < nameDesc.length; ++i) { + c = nameDesc[i]; + buffer.push(c); + + if (stringEnd) { + if (c === '\\' && i + 1 < nameDesc.length) { + buffer.push(nameDesc[++i]); + } else if (c === stringEnd) { + stringEnd = null; + } + } else if (c === '"' || c === "'") { + stringEnd = c; + } else if (c === '[') { + ++stack; + } else if (c === ']') { + if (--stack === 0) { + break; + } + } + } + + if (stack || stringEnd) { + return null; + } + + nameDesc.substr(i).match(REGEXP_DESCRIPTION); + return { + name: buffer.join(''), + description: RegExp.$1 + }; +} + + +/** + Split a string that starts with a name and ends with a description into its parts. + @param {string} nameDesc + @returns {object} Hash with "name" and "description" properties. + */ +exports.splitName = function(nameDesc) { + // like: name, [name], name text, [name] text, name - text, or [name] - text + // the hyphen must be on the same line as the name; this prevents us from treating a Markdown + // dash as a separator + + // optional values get special treatment + var result = null; + if (nameDesc[0] === '[') { + result = splitNameMatchingBrackets(nameDesc); + if (result !== null) { + return result; + } + } + + nameDesc.match(REGEXP_NAME_DESCRIPTION); + return { + name: RegExp.$1, + description: RegExp.$2 + }; +}; diff --git a/third_party/jsdoc/lib/jsdoc/opts/argparser.js b/third_party/jsdoc/lib/jsdoc/opts/argparser.js new file mode 100644 index 0000000000..dd23b588b0 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/opts/argparser.js @@ -0,0 +1,301 @@ +/** + * Parse the command line arguments. + * @module jsdoc/opts/argparser + * @author Michael Mathews + * @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ +'use strict'; + +var _ = require('underscore'); + +var hasOwnProp = Object.prototype.hasOwnProperty; + +/** + * Create an instance of the parser. + * @classdesc A parser to interpret the key-value pairs entered on the command line. + * @constructor + * @alias module:jsdoc/opts/argparser + */ +var ArgParser = function() { + this._options = []; + this._shortNameIndex = {}; + this._longNameIndex = {}; +}; + +ArgParser.prototype._getOptionByShortName = function(name) { + if (hasOwnProp.call(this._shortNameIndex, name)) { + return this._options[this._shortNameIndex[name]]; + } + return null; +}; + +ArgParser.prototype._getOptionByLongName = function(name) { + if (hasOwnProp.call(this._longNameIndex, name)) { + return this._options[this._longNameIndex[name]]; + } + return null; +}; + +ArgParser.prototype._addOption = function(option) { + var currentIndex; + + var longName = option.longName; + var shortName = option.shortName; + + this._options.push(option); + currentIndex = this._options.length - 1; + + if (shortName) { + this._shortNameIndex[shortName] = currentIndex; + } + if (longName) { + this._longNameIndex[longName] = currentIndex; + } + + return this; +}; + +/** + * Provide information about a legal option. + * @param {character} shortName The short name of the option, entered like: -T. + * @param {string} longName The equivalent long name of the option, entered like: --test. + * @param {boolean} hasValue Does this option require a value? Like: -t templatename + * @param {string} helpText A brief description of the option. + * @param {boolean} [canHaveMultiple=false] Set to `true` if the option can be provided more than once. + * @param {function} [coercer] A function to coerce the given value to a specific type. + * @return {this} + * @example + * myParser.addOption('t', 'template', true, 'The path to the template.'); + * myParser.addOption('h', 'help', false, 'Show the help message.'); + */ +ArgParser.prototype.addOption = function(shortName, longName, hasValue, helpText, canHaveMultiple, coercer) { + var option = { + shortName: shortName, + longName: longName, + hasValue: hasValue, + helpText: helpText, + canHaveMultiple: (canHaveMultiple || false), + coercer: coercer + }; + + return this._addOption(option); +}; + +// TODO: refactor addOption to accept objects, then get rid of this method +/** + * Provide information about an option that should not cause an error if present, but that is always + * ignored (for example, an option that was used in previous versions but is no longer supported). + * + * @private + * @param {string} shortName - The short name of the option with a leading hyphen (for example, + * `-v`). + * @param {string} longName - The long name of the option with two leading hyphens (for example, + * `--version`). + */ +ArgParser.prototype.addIgnoredOption = function(shortName, longName) { + var option = { + shortName: shortName, + longName: longName, + ignore: true + }; + + return this._addOption(option); +}; + +function padding(length) { + return new Array(length + 1).join(' '); +} + +function padLeft(str, length) { + return padding(length) + str; +} + +function padRight(str, length) { + return str + padding(length); +} + +function findMaxLength(arr) { + var max = 0; + + arr.forEach(function(item) { + if (item.length > max) { + max = item.length; + } + }); + + return max; +} + +function concatWithMaxLength(items, maxLength) { + var result = ''; + // to prevent endless loops, always use the first item, regardless of length + result += items.shift(); + + while ( items.length && (result.length + items[0].length < maxLength) ) { + result += ' ' + items.shift(); + } + + return result; +} + +// we want to format names and descriptions like this: +// | -f, --foo Very long description very long description very long | +// | description very long description. | +function formatHelpInfo(options) { + var MARGIN_LENGTH = 4; + var results = []; + + var maxLength = process.stdout.columns; + var maxNameLength = findMaxLength(options.names); + var maxDescriptionLength = findMaxLength(options.descriptions); + + var wrapDescriptionAt = maxLength - (MARGIN_LENGTH * 3) - maxNameLength; + // build the string for each option + options.names.forEach(function(name, i) { + var result; + var partialDescription; + var words; + + // add a left margin to the name + result = padLeft(options.names[i], MARGIN_LENGTH); + // and a right margin, with extra padding so the descriptions line up with one another + result = padRight(result, maxNameLength - options.names[i].length + MARGIN_LENGTH); + + // split the description on spaces + words = options.descriptions[i].split(' '); + // add as much of the description as we can fit on the first line + result += concatWithMaxLength(words, wrapDescriptionAt); + // if there's anything left, keep going until we've consumed the description + while (words.length) { + partialDescription = padding( maxNameLength + (MARGIN_LENGTH * 2) ); + partialDescription += concatWithMaxLength(words, wrapDescriptionAt); + result += '\n' + partialDescription; + } + + results.push(result); + }); + + return results; +} + +/** + * Generate a summary of all the options with corresponding help text. + * @returns {string} + */ +ArgParser.prototype.help = function() { + var options = { + names: [], + descriptions: [] + }; + + this._options.forEach(function(option) { + var name = ''; + + // don't show ignored options + if (option.ignore) { + return; + } + + if (option.shortName) { + name += '-' + option.shortName + (option.longName ? ', ' : ''); + } + + if (option.longName) { + name += '--' + option.longName; + } + + if (option.hasValue) { + name += ' '; + } + + options.names.push(name); + options.descriptions.push(option.helpText); + }); + + return 'Options:\n' + formatHelpInfo(options).join('\n'); +}; + +/** + * Get the options. + * @param {Array.} args An array, like ['-x', 'hello'] + * @param {Object} [defaults={}] An optional collection of default values. + * @returns {Object} The keys will be the longNames, or the shortName if no longName is defined for + * that option. The values will be the values provided, or `true` if the option accepts no value. + */ +ArgParser.prototype.parse = function(args, defaults) { + var result = defaults && _.defaults({}, defaults) || {}; + + result._ = []; + for (var i = 0, leni = args.length; i < leni; i++) { + var arg = '' + args[i], + next = (i < leni - 1) ? '' + args[i + 1] : null, + option, + shortName = null, + longName, + name, + value = null; + + // like -t + if (arg.charAt(0) === '-') { + // like --template + if (arg.charAt(1) === '-') { + name = longName = arg.slice(2); + option = this._getOptionByLongName(longName); + } + else { + name = shortName = arg.slice(1); + option = this._getOptionByShortName(shortName); + } + + if (option === null) { + throw new Error( 'Unknown command line option found: ' + name ); + } + + if (option.hasValue) { + value = next; + i++; + + if (value === null || value.charAt(0) === '-') { + throw new Error( 'Command line option requires a value: ' + name ); + } + } + else { + value = true; + } + + // skip ignored options now that we've consumed the option text + if (option.ignore) { + continue; + } + + if (option.longName && shortName) { + name = option.longName; + } + + if (typeof option.coercer === 'function') { + value = option.coercer(value); + } + + // Allow for multiple options of the same type to be present + if (option.canHaveMultiple && hasOwnProp.call(result, name)) { + var val = result[name]; + + if (val instanceof Array) { + val.push(value); + } else { + result[name] = [val, value]; + } + } + else { + result[name] = value; + } + } + else { + result._.push(arg); + } + } + + return result; +}; + +module.exports = ArgParser; diff --git a/third_party/jsdoc/lib/jsdoc/opts/args.js b/third_party/jsdoc/lib/jsdoc/opts/args.js new file mode 100644 index 0000000000..4748680d0f --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/opts/args.js @@ -0,0 +1,140 @@ +/** + * @module jsdoc/opts/args + * @requires jsdoc/opts/argparser + * @author Michael Mathews + * @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ +'use strict'; + +var ArgParser = require('jsdoc/opts/argparser'); +var querystring = require('querystring'); +var util = require('util'); + +var ourOptions; + +var argParser = new ArgParser(); +var hasOwnProp = Object.prototype.hasOwnProperty; + +// cast strings to booleans or integers where appropriate +function castTypes(item) { + var integer; + + var result = item; + + switch (result) { + case 'true': + result = true; + break; + + case 'false': + result = false; + break; + + default: + // might be an integer + integer = parseInt(result, 10); + if (String(integer) === result && integer !== 'NaN') { + result = integer; + } + } + + return result; +} + +// check for strings that we need to cast to other types +function fixTypes(item) { + var result = item; + + // recursively process arrays and objects + if ( util.isArray(result) ) { + for (var i = 0, l = result.length; i < l; i++) { + result[i] = fixTypes(result[i]); + } + } + else if (typeof result === 'object') { + Object.keys(result).forEach(function(prop) { + result[prop] = fixTypes(result[prop]); + }); + } + else { + result = castTypes(result); + } + + return result; +} + +function parseQuery(str) { + var result = querystring.parse(str); + + Object.keys(result).forEach(function(prop) { + result[prop] = fixTypes(result[prop]); + }); + + return result; +} + +argParser.addOption('t', 'template', true, 'The path to the template to use. Default: path/to/jsdoc/templates/default'); +argParser.addOption('c', 'configure', true, 'The path to the configuration file. Default: path/to/jsdoc/conf.json'); +argParser.addOption('e', 'encoding', true, 'Assume this encoding when reading all source files. Default: utf8'); +argParser.addOption('T', 'test', false, 'Run all tests and quit.'); +argParser.addOption('d', 'destination', true, 'The path to the output folder. Use "console" to dump data to the console. Default: ./out/'); +argParser.addOption('p', 'private', false, 'Display symbols marked with the @private tag. Default: false'); +argParser.addOption('r', 'recurse', false, 'Recurse into subdirectories when scanning for source code files.'); +argParser.addOption('h', 'help', false, 'Print this message and quit.'); +argParser.addOption('X', 'explain', false, 'Dump all found doclet internals to console and quit.'); +argParser.addOption('q', 'query', true, 'A query string to parse and store in env.opts.query. Example: foo=bar&baz=true', false, parseQuery); +argParser.addOption('u', 'tutorials', true, 'Directory in which JSDoc should search for tutorials.'); +argParser.addOption('P', 'package', true, 'The path to the project\'s package file. Default: path/to/sourcefiles/package.json'); +argParser.addOption('R', 'readme', true, 'The path to the project\'s README file. Default: path/to/sourcefiles/README.md'); +argParser.addOption('v', 'version', false, 'Display the version number and quit.'); +argParser.addOption('', 'debug', false, 'Log information for debugging JSDoc. On Rhino, launches the debugger when passed as the first option.'); +argParser.addOption('', 'verbose', false, 'Log detailed information to the console as JSDoc runs.'); +argParser.addOption('', 'pedantic', false, 'Treat errors as fatal errors, and treat warnings as errors. Default: false'); + +// Options specific to tests +argParser.addOption(null, 'match', true, 'Only run tests containing .', true); +argParser.addOption(null, 'nocolor', false, 'Do not use color in console output from tests.'); + +// Options that are no longer supported and should be ignored +argParser.addIgnoredOption('l', 'lenient'); // removed in JSDoc 3.3.0 + +/** + * Set the options for this app. + * @throws {Error} Illegal arguments will throw errors. + * @param {string|String[]} args The command line arguments for this app. + */ +exports.parse = function(args) { + args = args || []; + + if (typeof args === 'string' || args.constructor === String) { + args = String(args).split(/\s+/g); + } + + ourOptions = argParser.parse(args); + + return ourOptions; +}; + +/** + * Retrieve help message for options. + */ +exports.help = function() { + return argParser.help(); +}; + +/** + * Get a named option. + * @param {string} name The name of the option. + * @return {string} The value associated with the given name. + *//** + * Get all the options for this app. + * @return {Object} A collection of key/values representing all the options. + */ +exports.get = function(name) { + if (typeof name === 'undefined') { + return ourOptions; + } + else if ( hasOwnProp.call(ourOptions, name) ) { + return ourOptions[name]; + } +}; diff --git a/third_party/jsdoc/lib/jsdoc/package.js b/third_party/jsdoc/lib/jsdoc/package.js new file mode 100644 index 0000000000..1a60a3b023 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/package.js @@ -0,0 +1,254 @@ +'use strict'; + +var logger = require('jsdoc/util/logger'); + +/** + * Provides access to information about a JavaScript package. + * + * @module jsdoc/package + * @see https://www.npmjs.org/doc/files/package.json.html + */ + +// Collect all of the license information from a `package.json` file. +function getLicenses(packageInfo) { + var licenses = packageInfo.licenses ? packageInfo.licenses.slice(0) : []; + + if (packageInfo.license) { + licenses.push({ type: packageInfo.license }); + } + + return licenses; +} + +/** + * Information about where to report bugs in the package. + * + * @typedef {Object} module:jsdoc/package.Package~BugInfo + * @property {string} email - The email address for reporting bugs. + * @property {string} url - The URL for reporting bugs. + */ + +/** + * Information about a package's software license. + * + * @typedef {Object} module:jsdoc/package.Package~LicenseInfo + * @property {string} type - An identifier for the type of license. + * @property {string} url - The URL for the complete text of the license. + */ + +/** + * Information about a package author or contributor. + * + * @typedef {Object} module:jsdoc/package.Package~PersonInfo + * @property {string} name - The person's full name. + * @property {string} email - The person's email address. + * @property {string} url - The URL of the person's website. + */ + +/** + * Information about a package's version-control repository. + * + * @typedef {Object} module:jsdoc/package.Package~RepositoryInfo + * @property {string} type - The type of version-control system that the repository uses (for + * example, `git` or `svn`). + * @property {string} url - The URL for the repository. + */ + +/** + * Information about a JavaScript package. JSDoc can extract package information from + * `package.json` files that follow the + * [npm specification](https://www.npmjs.org/doc/files/package.json.html). + * + * **Note**: JSDoc does not validate or normalize the contents of `package.json` files. If your + * `package.json` file does not follow the npm specification, some properties of the `Package` + * object may not use the format documented here. + * + * @class + * @param {string} json - The contents of the `package.json` file. + */ +exports.Package = function(json) { + var packageInfo; + + /** + * The string identifier that is shared by all `Package` objects. + * + * @readonly + * @default + * @type {string} + */ + this.kind = 'package'; + + try { + packageInfo = JSON.parse(json || '{}'); + } + catch (e) { + logger.error('Unable to parse the package file: %s', e.message); + packageInfo = {}; + } + + if (packageInfo.name) { + /** + * The package name. + * + * @type {string} + */ + this.name = packageInfo.name; + } + + /** + * The unique longname for this `Package` object. + * + * @type {string} + */ + this.longname = this.kind + ':' + this.name; + + if (packageInfo.author) { + /** + * The author of this package. Contains either a + * {@link module:jsdoc/package.Package~PersonInfo PersonInfo} object or a string with + * information about the author. + * + * @type {(module:jsdoc/package.Package~PersonInfo|string)} + * @since 3.3.0 + */ + this.author = packageInfo.author; + } + + if (packageInfo.bugs) { + /** + * Information about where to report bugs in the project. May contain a URL, as a string, or + * an object with more detailed information. + * + * @type {(string|module:jsdoc/package.Package~BugInfo)} + * @since 3.3.0 + */ + this.bugs = packageInfo.bugs; + } + + if (packageInfo.contributors) { + /** + * The contributors to this package. + * + * @type {Array.<(module:jsdoc/package.Package~PersonInfo|string)>} + * @since 3.3.0 + */ + this.contributors = packageInfo.contributors; + } + + if (packageInfo.dependencies) { + /** + * The dependencies for this package. + * + * @type {Object} + * @since 3.3.0 + */ + this.dependencies = packageInfo.dependencies; + } + + if (packageInfo.description) { + /** + * A brief description of the package. + * + * @type {string} + */ + this.description = packageInfo.description; + } + + if (packageInfo.devDependencies) { + /** + * The development dependencies for this package. + * + * @type {Object} + * @since 3.3.0 + */ + this.devDependencies = packageInfo.devDependencies; + } + + if (packageInfo.engines) { + /** + * The JavaScript engines that this package supports. Each key is a string that identifies the + * engine (for example, `node`). Each value is a + * [semver](https://www.npmjs.org/doc/misc/semver.html)-compliant version number for the engine. + * + * @type {Object} + * @since 3.3.0 + */ + this.engines = packageInfo.engines; + } + + /** + * The source files associated with the package. + * + * New `Package` objects always contain an empty array, regardless of whether the `package.json` + * file includes a `files` property. + * + * After JSDoc parses your input files, it sets this property to a list of paths to your input + * files. + * + * @type {Array.} + */ + this.files = []; + + if (packageInfo.homepage) { + /** + * The URL for the package's homepage. + * + * @type {string} + * @since 3.3.0 + */ + this.homepage = packageInfo.homepage; + } + + if (packageInfo.keywords) { + /** + * Keywords to help users find the package. + * + * @type {Array.} + * @since 3.3.0 + */ + this.keywords = packageInfo.keywords; + } + + if (packageInfo.license || packageInfo.licenses) { + /** + * The licenses used by this package. Combines information from the `package.json` file's + * `license` property and the deprecated `licenses` property. + * + * @type {Array.} + */ + this.licenses = getLicenses(packageInfo); + } + + if (packageInfo.main) { + /** + * The module ID that provides the primary entry point to the package. For example, if your + * package is a CommonJS module, and the value of this property is `foo`, users should be able + * to load your module with `require('foo')`. + * + * @type {string} + * @since 3.3.0 + */ + this.main = packageInfo.main; + } + + if (packageInfo.repository) { + /** + * The version-control repository for the package. + * + * @type {module:jsdoc/package.Package~RepositoryInfo} + * @since 3.3.0 + */ + this.repository = packageInfo.repository; + } + + if (packageInfo.version) { + /** + * The [semver](https://www.npmjs.org/doc/misc/semver.html)-compliant version number of the + * package. + * + * @type {string} + * @since 3.2.0 + */ + this.version = packageInfo.version; + } +}; diff --git a/third_party/jsdoc/lib/jsdoc/path.js b/third_party/jsdoc/lib/jsdoc/path.js new file mode 100644 index 0000000000..3b8b660c76 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/path.js @@ -0,0 +1,134 @@ +/*global env: true */ +/** + * Extended version of the standard `path` module. + * @module jsdoc/path + */ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var runtime = require('jsdoc/util/runtime'); + +function prefixReducer(previousPath, current) { + var currentPath = []; + + // if previousPath is defined, but has zero length, there's no common prefix; move along + if (previousPath && !previousPath.length) { + return currentPath; + } + + currentPath = path.resolve(global.env.pwd, current).split(path.sep) || []; + + if (previousPath && currentPath.length) { + // remove chunks that exceed the previous path's length + currentPath = currentPath.slice(0, previousPath.length); + + // if a chunk doesn't match the previous path, remove everything from that chunk on + for (var i = 0, l = currentPath.length; i < l; i++) { + if (currentPath[i] !== previousPath[i]) { + currentPath.splice(i, currentPath.length - i); + break; + } + } + } + + return currentPath; +} + +/** + * Find the common prefix for an array of paths. If there is a common prefix, a trailing separator + * is appended to the prefix. Relative paths are resolved relative to the current working directory. + * + * For example, assuming that the current working directory is `/Users/jsdoc`: + * + * + For the single path `foo/bar/baz/qux.js`, the common prefix is `foo/bar/baz/`. + * + For paths `foo/bar/baz/qux.js`, `foo/bar/baz/quux.js`, and `foo/bar/baz.js`, the common prefix + * is `/Users/jsdoc/foo/bar/`. + * + For paths `../jsdoc/foo/bar/baz/qux/quux/test.js`, `/Users/jsdoc/foo/bar/bazzy.js`, and + * `../../Users/jsdoc/foo/bar/foobar.js`, the common prefix is `/Users/jsdoc/foo/bar/`. + * + For paths `foo/bar/baz/qux.js` and `../../Library/foo/bar/baz.js`, there is no common prefix, + * and an empty string is returned. + * + * @param {Array.} paths - The paths to search for a common prefix. + * @return {string} The common prefix, or an empty string if there is no common prefix. + */ +exports.commonPrefix = function(paths) { + var segments; + + var prefix = ''; + + paths = paths || []; + + // if there's only one path, its resolved dirname (plus a trailing slash) is the common prefix + if (paths.length === 1) { + prefix = path.resolve(global.env.pwd, paths[0]); + if ( path.extname(prefix) ) { + prefix = path.dirname(prefix); + } + + prefix += path.sep; + } + else { + segments = paths.reduce(prefixReducer, undefined) || []; + + // if there's anything left (other than a placeholder for a leading slash), add a + // placeholder for a trailing slash + if ( segments.length && (segments.length > 1 || segments[0] !== '') ) { + segments.push(''); + } + + prefix = segments.join(path.sep); + } + + return prefix; +}; + +/** + * Retrieve the fully qualified path to the requested resource. + * + * If the resource path is specified as a relative path, JSDoc searches for the path in the + * directory where the JSDoc configuration file is located, then in the current working directory, + * and finally in the JSDoc directory. + * + * If the resource path is specified as a fully qualified path, JSDoc uses the path as-is. + * + * @param {string} filepath - The path to the requested resource. May be an absolute path; a path + * relative to the JSDoc directory; or a path relative to the current working directory. + * @param {string} [filename] - The filename of the requested resource. + * @return {string} The fully qualified path (or, on Rhino, a URI) to the requested resource. + * Includes the filename if one was provided. + */ +exports.getResourcePath = function(filepath, filename) { + var result = null; + + function pathExists(_path) { + try { + fs.readdirSync(_path); + } + catch(e) { + return false; + } + + return true; + } + + // absolute paths are normalized by path.resolve on the first pass + [path.dirname(global.env.opts.configure || ''), env.pwd, env.dirname].forEach(function(_path) { + if (!result && _path) { + _path = path.resolve(_path, filepath); + if ( pathExists(_path) ) { + result = _path; + } + } + }); + + if (result) { + result = filename ? path.join(result, filename) : result; + } + + return result; +}; + +Object.keys(path).forEach(function(member) { + exports[member] = path[member]; +}); diff --git a/third_party/jsdoc/lib/jsdoc/plugins.js b/third_party/jsdoc/lib/jsdoc/plugins.js new file mode 100644 index 0000000000..92fb4947c4 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/plugins.js @@ -0,0 +1,53 @@ +/*global app: true */ +/** + * Utility functions to support the JSDoc plugin framework. + * @module jsdoc/plugins + */ +'use strict'; + +var logger = require('jsdoc/util/logger'); +var path = require('jsdoc/path'); + +function addHandlers(handlers, parser) { + Object.keys(handlers).forEach(function(eventName) { + parser.on(eventName, handlers[eventName]); + }); +} + +exports.installPlugins = function(plugins, parser) { + var dictionary = require('jsdoc/tag/dictionary'); + + var eventName; + var plugin; + + for (var i = 0, l = plugins.length; i < l; i++) { + plugin = require(plugins[i]); + + // allow user-defined plugins to... + // ...register event handlers + if (plugin.handlers) { + addHandlers(plugin.handlers, parser); + } + + // ...define tags + if (plugin.defineTags) { + plugin.defineTags(dictionary); + } + + // ...add a Rhino node visitor (deprecated in JSDoc 3.3) + if (plugin.nodeVisitor) { + if ( !parser.addNodeVisitor ) { + logger.error('Unable to add the Rhino node visitor from %s, because JSDoc ' + + 'is not using the Rhino JavaScript parser.', plugins[i]); + } + else { + parser.addNodeVisitor(plugin.nodeVisitor); + } + } + + // ...add a Mozilla Parser API node visitor + if (plugin.astNodeVisitor) { + parser.addAstNodeVisitor(plugin.astNodeVisitor); + } + } +}; diff --git a/third_party/jsdoc/lib/jsdoc/readme.js b/third_party/jsdoc/lib/jsdoc/readme.js new file mode 100644 index 0000000000..f24317ae13 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/readme.js @@ -0,0 +1,26 @@ +/*global env: true */ + +/** + * Make the contents of a README file available to include in the output. + * @module jsdoc/readme + * @author Michael Mathews + * @author Ben Blank + */ +'use strict'; + +var fs = require('jsdoc/fs'), + markdown = require('jsdoc/util/markdown'); + +/** + * @class + * @classdesc Represents a README file. + * @param {string} path - The filepath to the README. + */ +function ReadMe(path) { + var content = fs.readFileSync(path, env.opts.encoding), + parse = markdown.getParser(); + + this.html = parse(content); +} + +module.exports = ReadMe; diff --git a/third_party/jsdoc/lib/jsdoc/schema.js b/third_party/jsdoc/lib/jsdoc/schema.js new file mode 100644 index 0000000000..07d710941a --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/schema.js @@ -0,0 +1,736 @@ +/** + * @overview Schema for validating JSDoc doclets. + * + * @author Michael Mathews + * @author Jeff Williams + * @license Apache License 2.0 - See file 'LICENSE.md' in this project. + * @see + */ +'use strict'; + +// JSON schema types +var ARRAY = 'array'; +var BOOLEAN = 'boolean'; +var INTEGER = 'integer'; +var NULL = 'null'; +var NUMBER = 'number'; +var OBJECT = 'object'; +var STRING = 'string'; +var UNDEFINED = 'undefined'; + +var BOOLEAN_OPTIONAL = [BOOLEAN, NULL, UNDEFINED]; +var STRING_OPTIONAL = [STRING, NULL, UNDEFINED]; + +var EVENT_REGEXP = /event\:[\S]+/; +var PACKAGE_REGEXP = /package\:[\S]+/; + +// information about the code associated with a doclet +var META_SCHEMA = exports.META_SCHEMA = { + type: OBJECT, + optional: true, + additionalProperties: false, + properties: { + code: { + type: OBJECT, + additionalProperties: false, + properties: { + funcscope: { + type: STRING, + optional: true + }, + id: { + type: STRING, + optional: true + }, + name: { + type: STRING, + optional: true + }, + node: { + type: OBJECT, + optional: true + }, + paramnames: { + type: ARRAY, + optional: true, + uniqueItems: true, + items: { + type: STRING + } + }, + type: { + type: STRING, + optional: true + }, + value: { + optional: true + } + } + }, + filename: { + title: 'The name of the file that contains the code associated with this doclet.', + type: STRING, + optional: true + }, + lineno: { + title: 'The line number of the code associated with this doclet.', + type: NUMBER, + optional: true + }, + path: { + title: 'The path in which the code associated with this doclet is located.', + type: STRING, + optional: true + }, + range: { + title: 'The positions of the first and last characters of the code associated with ' + + 'this doclet.', + type: ARRAY, + optional: true, + minItems: 2, + maxItems: 2, + items: { + type: NUMBER + } + }, + vars: { + type: OBJECT + } + } +}; + +// type property containing type names +var TYPE_PROPERTY_SCHEMA = exports.TYPE_PROPERTY_SCHEMA = { + type: OBJECT, + additionalProperties: false, + properties: { + names: { + type: ARRAY, + minItems: 1, + items: { + type: STRING + } + }, + // type parser output + parsedType: { + type: OBJECT, + additionalProperties: true + } + } +}; + +// enumeration properties +var ENUM_PROPERTY_SCHEMA = exports.ENUM_PROPERTY_SCHEMA = { + type: OBJECT, + additionalProperties: false, + properties: { + comment: { + type: STRING + }, + defaultvalue: { + type: STRING_OPTIONAL, + optional: true + }, + description: { + type: STRING_OPTIONAL, + optional: true + }, + kind: { + type: STRING, + // TODO: get this from a real enum somewhere + enum: ['member'] + }, + longname: { + type: STRING + }, + memberof: { + type: STRING, + optional: true + }, + meta: META_SCHEMA, + name: { + type: STRING + }, + // is this member nullable? (derived from the type expression) + nullable: { + type: BOOLEAN_OPTIONAL + }, + // is this member optional? (derived from the type expression) + optional: { + type: BOOLEAN_OPTIONAL + }, + scope: { + type: STRING, + // TODO: get this from a real enum somewhere + enum: ['static'] + }, + type: TYPE_PROPERTY_SCHEMA, + // can this member be provided more than once? (derived from the type expression) + variable: { + type: BOOLEAN_OPTIONAL + } + } +}; + +// function parameter, or object property defined with @property tag +var PARAM_SCHEMA = exports.PARAM_SCHEMA = { + type: OBJECT, + additionalProperties: false, + properties: { + // what is the default value for this parameter? + defaultvalue: { + type: STRING_OPTIONAL, + optional: true + }, + // a description of the parameter + description: { + type: STRING_OPTIONAL, + optional: true + }, + // what name does this parameter have within the function? + name: { + type: STRING + }, + // can the value for this parameter be null? + nullable: { + type: BOOLEAN_OPTIONAL, + optional: true + }, + // is a value for this parameter optional? + optional: { + type: BOOLEAN_OPTIONAL, + optional: true + }, + // what are the types of value expected for this parameter? + type: TYPE_PROPERTY_SCHEMA, + // can this parameter be repeated? + variable: { + type: BOOLEAN_OPTIONAL, + optional: true + } + } +}; + +var DOCLET_SCHEMA = exports.DOCLET_SCHEMA = { + type: OBJECT, + additionalProperties: false, + properties: { + // what access privileges are allowed + access: { + type: STRING, + optional: true, + // TODO: define this as an enumeration elsewhere + enum: [ + 'private', + 'protected' + ] + }, + alias: { + type: STRING, + optional: true + }, + augments: { + type: ARRAY, + optional: true, + uniqueItems: true, + items: { + type: STRING + } + }, + author: { + type: ARRAY, + optional: true, + items: { + type: STRING + } + }, + borrowed: { + type: ARRAY, + optional: true, + uniqueItems: true, + items: { + type: OBJECT, + additionalProperties: false, + properties: { + // name of the target + as: { + type: STRING, + optional: true + }, + // name of the source + from: { + type: STRING + } + } + } + }, + // a description of the class that this constructor belongs to + classdesc: { + type: STRING, + optional: true + }, + comment: { + type: STRING + }, + copyright: { + type: STRING, + optional: true + }, + defaultvalue: { + optional: true + }, + defaultvaluetype: { + type: STRING, + optional: true, + enum: [OBJECT, ARRAY] + }, + // is usage of this symbol deprecated? + deprecated: { + type: [STRING, BOOLEAN], + optional: true + }, + // a description + description: { + type: STRING_OPTIONAL, + optional: true + }, + // something else to consider + examples: { + type: ARRAY, + optional: true, + items: { + type: STRING + } + }, + exceptions: { + type: ARRAY, + optional: true, + items: PARAM_SCHEMA + }, + // the path to another constructor + extends: { + type: ARRAY, + optional: true, + uniqueItems: true, + items: { + type: STRING + } + }, + // the path to another doc object + fires: { + type: ARRAY, + optional: true, + uniqueItems: true, + items: { + type: STRING, + pattern: EVENT_REGEXP + } + }, + forceMemberof: { + type: BOOLEAN_OPTIONAL, + optional: true + }, + ignore: { + type: BOOLEAN, + optional: true + }, + implementations: { + type: ARRAY, + optional: true, + items: { + type: STRING + } + }, + implements: { + type: ARRAY, + optional: true, + items: { + type: STRING + } + }, + inherited: { + type: BOOLEAN, + optional: true + }, + inherits: { + type: STRING, + optional: true, + dependency: { + inherited: true + } + }, + isEnum: { + type: BOOLEAN, + optional: true + }, + // what kind of symbol is this? + kind: { + type: STRING, + // TODO: define this as an enumeration elsewhere + enum: [ + 'class', + 'constant', + 'event', + 'external', + 'file', + 'function', + 'interface', + 'member', + 'mixin', + 'module', + 'namespace', + 'package', + 'param', + 'typedef' + ] + }, + license: { + type: STRING, + optional: true + }, + listens: { + type: ARRAY, + optional: true, + uniqueItems: true, + items: { + type: STRING, + pattern: EVENT_REGEXP + } + }, + longname: { + type: STRING + }, + // probably a leading substring of the path + memberof: { + type: STRING, + optional: true + }, + // information about this doc + meta: META_SCHEMA, + mixes: { + type: ARRAY, + optional: true, + uniqueItems: true, + items: { + type: STRING + } + }, + // probably a trailing substring of the path + name: { + type: STRING + }, + // is this member nullable? (derived from the type expression) + nullable: { + type: BOOLEAN_OPTIONAL + }, + // is this member optional? (derived from the type expression) + optional: { + type: BOOLEAN_OPTIONAL + }, + overrides: { + type: STRING, + optional: true + }, + // are there function parameters associated with this doc? + params: { + type: ARRAY, + optional: true, + uniqueItems: true, + items: PARAM_SCHEMA + }, + preserveName: { + type: BOOLEAN, + optional: true + }, + properties: { + type: ARRAY, + optional: true, + uniqueItems: true, + minItems: 1, + items: { + anyOf: [ENUM_PROPERTY_SCHEMA, PARAM_SCHEMA] + } + }, + readonly: { + type: BOOLEAN, + optional: true + }, + // the symbol being documented requires another symbol + requires: { + type: ARRAY, + optional: true, + uniqueItems: true, + minItems: 1, + items: { + type: STRING + } + }, + returns: { + type: ARRAY, + optional: true, + minItems: 1, + items: PARAM_SCHEMA + }, + // what sort of parent scope does this symbol have? + scope: { + type: STRING, + enum: [ + // TODO: make these an enumeration + 'global', + 'inner', + 'instance', + 'static' + ] + }, + // something else to consider + see: { + type: ARRAY, + optional: true, + minItems: 1, + items: { + type: STRING + } + }, + // at what previous version was this doc added? + since: { + type: STRING, + optional: true + }, + summary: { + type: STRING, + optional: true + }, + // arbitrary tags associated with this doc + tags: { + type: ARRAY, + optional: true, + minItems: 1, + items: { + type: OBJECT, + additionalProperties: false, + properties: { + originalTitle: { + type: STRING + }, + text: { + type: STRING, + optional: true + }, + title: { + type: STRING + }, + value: { + type: [STRING, OBJECT], + optional: true, + properties: PARAM_SCHEMA + } + } + } + }, + 'this': { + type: STRING, + optional: true + }, + todo: { + type: ARRAY, + optional: true, + minItems: 1, + items: { + type: STRING + } + }, + // extended tutorials + tutorials: { + type: ARRAY, + optional: true, + minItems: 1, + items: { + type: STRING + } + }, + // what type is the value that this doc is associated with, like `number` + type: TYPE_PROPERTY_SCHEMA, + undocumented: { + type: BOOLEAN, + optional: true + }, + // can this member be provided more than once? (derived from the type expression) + variable: { + type: BOOLEAN_OPTIONAL + }, + variation: { + type: STRING, + optional: true + }, + // what is the version of this doc + version: { + type: STRING, + optional: true + }, + // is a member left to be implemented during inheritance? + virtual: { + type: BOOLEAN, + optional: true + } + } +}; + +var CONTACT_INFO_SCHEMA = exports.CONTACT_INFO_SCHEMA = { + type: OBJECT, + additionalProperties: false, + properties: { + email: { + type: STRING, + optional: true + }, + name: { + type: STRING, + optional: true + }, + url: { + type: STRING, + optional: true, + format: 'uri' + } + } +}; + +var BUGS_SCHEMA = exports.BUGS_SCHEMA = { + type: OBJECT, + additionalProperties: false, + properties: { + email: { + type: STRING, + optional: true + }, + url: { + type: STRING, + optional: true, + format: 'uri' + } + } +}; + +var PACKAGE_SCHEMA = exports.PACKAGE_SCHEMA = { + type: OBJECT, + additionalProperties: false, + properties: { + author: { + anyOf: [STRING, CONTACT_INFO_SCHEMA], + optional: true + }, + bugs: { + anyOf: [STRING, BUGS_SCHEMA], + optional: true + }, + contributors: { + type: ARRAY, + optional: true, + minItems: 0, + items: { + anyOf: [STRING, CONTACT_INFO_SCHEMA] + } + }, + dependencies: { + type: OBJECT, + optional: true + }, + description: { + type: STRING, + optional: true + }, + devDependencies: { + type: OBJECT, + optional: true + }, + engines: { + type: OBJECT, + optional: true + }, + files: { + type: ARRAY, + uniqueItems: true, + minItems: 0, + items: { + type: STRING + } + }, + homepage: { + type: STRING, + optional: true, + format: 'uri' + }, + keywords: { + type: ARRAY, + optional: true, + minItems: 0, + items: { + type: STRING + } + }, + kind: { + type: STRING, + enum: ['package'] + }, + licenses: { + type: ARRAY, + optional: true, + minItems: 1, + items: { + type: OBJECT, + additionalProperties: false, + properties: { + type: { + type: STRING, + optional: true + }, + url: { + type: STRING, + optional: true, + format: 'uri' + } + } + } + }, + longname: { + type: STRING, + optional: true, + pattern: PACKAGE_REGEXP + }, + main: { + type: STRING, + optional: true + }, + name: { + type: STRING, + optional: true + }, + repository: { + type: OBJECT, + optional: true, + additionalProperties: false, + properties: { + type: { + type: STRING, + optional: true + }, + // we don't use `format: 'uri'` here because repo URLs are atypical + url: { + type: STRING, + optional: true + } + } + }, + version: { + type: STRING, + optional: true + } + } +}; + +var DOCLETS_SCHEMA = exports.DOCLETS_SCHEMA = { + type: ARRAY, + items: { + anyOf: [DOCLET_SCHEMA, PACKAGE_SCHEMA] + } +}; diff --git a/third_party/jsdoc/lib/jsdoc/src/astbuilder.js b/third_party/jsdoc/lib/jsdoc/src/astbuilder.js new file mode 100644 index 0000000000..504d6cdb7e --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/src/astbuilder.js @@ -0,0 +1,390 @@ +'use strict'; + +var esprima = require('esprima'); +var jsdoc = { + src: { + syntax: require('jsdoc/src/syntax'), + Walker: require('jsdoc/src/walker').Walker + }, + util: { + logger: require('jsdoc/util/logger') + } +}; +var Syntax = jsdoc.src.syntax.Syntax; + +// TODO: should set e.stopPropagation == true for consistency with Rhino, right? +var VISITOR_CONTINUE = true; +var VISITOR_STOP = false; + +// TODO: docs; empty array means any node type, otherwise only the node types in the array +var acceptsLeadingComments = (function() { + var accepts = {}; + + // these nodes always accept leading comments + var commentable = [ + Syntax.AssignmentExpression, + Syntax.CallExpression, + Syntax.FunctionDeclaration, + Syntax.FunctionExpression, + Syntax.MemberExpression, + Syntax.Property, + Syntax.TryStatement, + Syntax.VariableDeclaration, + Syntax.VariableDeclarator, + Syntax.WithStatement + ]; + for (var i = 0, l = commentable.length; i < l; i++) { + accepts[commentable[i]] = []; + } + + // these nodes accept leading comments if they have specific types of parent nodes + // like: function foo(/** @type {string} */ bar) {} + accepts[Syntax.Identifier] = [ + Syntax.CatchClause, + Syntax.FunctionDeclaration, + Syntax.FunctionExpression + ]; + // like: var Foo = Class.create(/** @lends Foo */{ // ... }) + accepts[Syntax.ObjectExpression] = [ + Syntax.CallExpression, + Syntax.Property, + Syntax.ReturnStatement + ]; + + return accepts; +})(); + +// TODO: docs +function canAcceptComment(node) { + var canAccept = false; + var spec = acceptsLeadingComments[node.type]; + + if (spec) { + // empty array means we don't care about the parent type + if (spec.length === 0) { + canAccept = true; + } + // we can accept the comment if the spec contains the type of the node's parent + else if (node.parent) { + canAccept = spec.indexOf(node.parent.type) !== -1; + } + } + + return canAccept; +} + +// TODO: docs +// check whether node1 is before node2 +function isBefore(beforeRange, afterRange) { + return beforeRange[1] <= afterRange[0]; +} + +// TODO: docs +function isWithin(innerRange, outerRange) { + return innerRange[0] >= outerRange[0] && innerRange[1] <= outerRange[1]; +} + +// TODO: docs +function isJsdocComment(comment) { + return comment && (comment.type === 'Block') && (comment.value[0] === '*'); +} + +/** + * Add the raw comment string to a block comment node. + * + * @private + * @param {!Object} comment - A comment node with `type` and `value` properties. + */ +function addRawComment(comment) { + comment.raw = comment.raw || ('/*' + comment.value + '*/'); + return comment; +} + +// TODO: docs +function scrubComments(comments) { + var comment; + + var scrubbed = []; + + for (var i = 0, l = comments.length; i < l; i++) { + comment = comments[i]; + if ( isJsdocComment(comment) ) { + scrubbed.push( addRawComment(comment) ); + } + } + + return scrubbed; +} + +// TODO: docs +var AstBuilder = exports.AstBuilder = function() {}; + +function parse(source, filename, esprimaOpts) { + var ast; + + try { + ast = esprima.parse(source, esprimaOpts); + } + catch (e) { + jsdoc.util.logger.error('Unable to parse %s: %s', filename, e.message); + } + + return ast; +} + +// TODO: docs +AstBuilder.prototype.build = function(source, filename) { + var ast; + + var esprimaOpts = { + comment: true, + loc: true, + range: true, + tokens: true + }; + + ast = parse(source, filename, esprimaOpts); + + if (ast) { + this._postProcess(filename, ast); + } + + return ast; +}; + +// TODO: docs +function atomSorter(a, b) { + var aRange = a.range; + var bRange = b.range; + var result = 0; + + // does a end before b starts? + if ( isBefore(aRange, bRange) ) { + result = -1; + } + // does a enclose b? + else if ( isWithin(bRange, aRange) ) { + result = -1; + } + // does a start before b? + else if (aRange[0] < bRange[0]) { + result = -1; + } + // are the ranges non-identical? if so, b must be first + else if ( aRange[0] !== bRange[0] || aRange[1] !== bRange[1] ) { + result = 1; + } + + return result; +} + +// TODO: docs +// TODO: export? +function CommentAttacher(comments, tokens) { + this._comments = comments || []; + this._tokens = tokens || []; + + this._tokenIndex = 0; + this._previousNodeEnd = 0; + this._astRoot = null; + this._strayComments = []; + + this._resetPendingComment() + ._resetCandidates(); +} + +// TODO: docs +CommentAttacher.prototype._resetPendingComment = function() { + this._pendingComment = null; + this._pendingCommentRange = null; + + return this; +}; + +// TODO: docs +CommentAttacher.prototype._resetCandidates = function() { + this._candidates = []; + + return this; +}; + +// TODO: docs +CommentAttacher.prototype._nextComment = function() { + return this._comments[0] || null; +}; + +// TODO: docs +CommentAttacher.prototype._nextToken = function() { + return this._tokens[this._tokenIndex] || null; +}; + +// TODO: docs +// find the index of the atom whose end position is closest to (but not after) the specified +// position +CommentAttacher.prototype._nextIndexBefore = function(startIndex, atoms, position) { + var atom; + + var newIndex = startIndex; + + for (var i = newIndex, l = atoms.length; i < l; i++) { + atom = atoms[i]; + + if (atom.range[1] > position) { + break; + } + else { + newIndex = i; + } + } + + return newIndex; +}; + +// TODO: docs +CommentAttacher.prototype._advanceTokenIndex = function(node) { + var position = node.range[0]; + + this._tokenIndex = this._nextIndexBefore(this._tokenIndex, this._tokens, position); + + return this; +}; + +// TODO: docs +CommentAttacher.prototype._fastForwardComments = function(node) { + var position = node.range[0]; + var commentIndex = this._nextIndexBefore(0, this._comments, position); + + // all comments before the node (except the last one) are considered stray comments + if (commentIndex > 0) { + this._strayComments = this._strayComments.concat( this._comments.splice(0, + commentIndex) ); + } +}; + +// TODO: docs +CommentAttacher.prototype._attachPendingComment = function() { + var target; + + if (!this._pendingComment) { + return this; + } + + if (this._candidates.length > 0) { + target = this._candidates[this._candidates.length - 1]; + target.leadingComments = target.leadingComments || []; + target.leadingComments.push(this._pendingComment); + } + else { + this._strayComments.push(this._pendingComment); + } + + this._resetPendingComment() + ._resetCandidates(); + + return this; +}; + +// TODO: docs +CommentAttacher.prototype._isEligible = function(node) { + var atoms; + var token; + + var isEligible = false; + + var comment = this._nextComment(); + if (comment) { + atoms = [node, comment]; + token = this._nextToken(); + if (token) { + atoms.push(token); + } + + atoms.sort(atomSorter); + + // a candidate node must immediately follow the comment + if (atoms.indexOf(node) === atoms.indexOf(comment) + 1) { + isEligible = true; + } + } + + return isEligible; +}; + +// TODO: docs +// TODO: do we ever get multiple candidate nodes? +CommentAttacher.prototype.visit = function(node) { + var isEligible; + + // bail if we're out of comments + if ( !this._nextComment() ) { + return VISITOR_STOP; + } + + // set the AST root if necessary + this._astRoot = this._astRoot || node; + + // move to the next token, and fast-forward past comments that can no longer be attached + this._advanceTokenIndex(node); + this._fastForwardComments(node); + // now we can check whether the current node is in the right position to accept the next comment + isEligible = this._isEligible(node); + + // attach the pending comment, if there is one + this._attachPendingComment(); + + // okay, now that we've done all that bookkeeping, we can check whether the current node accepts + // leading comments and add it to the candidate list if needed + if ( isEligible && canAcceptComment(node) ) { + // make sure we don't go past the end of the outermost target node + if (!this._pendingCommentRange) { + this._pendingCommentRange = node.range.slice(0); + } + this._candidates.push(node); + + // we have a candidate node, so pend the current comment if necessary + this._pendingComment = this._pendingComment || this._comments.splice(0, 1)[0]; + } + + return VISITOR_CONTINUE; +}; + +// TODO: docs +CommentAttacher.prototype.finish = function() { + // any remaining comments are stray comments + this._strayComments = this._strayComments.concat(this._comments); + + // deal with the pending comment, if there is one + this._attachPendingComment(); + + // attach stray comments to the AST root + if (this._strayComments.length) { + this._astRoot.trailingComments = this._strayComments.slice(0); + } +}; + +// TODO: docs +// TODO: refactor to make this extensible +/** + * @param {string} filename - The full path to the source file. + * @param {Object} ast - An abstract syntax tree that conforms to the Mozilla Parser API. + */ +AstBuilder.prototype._postProcess = function(filename, ast) { + var attachComments = !!ast.comments && !!ast.comments.length; + var commentAttacher = new CommentAttacher( scrubComments(ast.comments.slice(0)), ast.tokens ); + var visitor = { + visit: function(node) { + if (attachComments) { + attachComments = commentAttacher.visit(node); + } + } + }; + + var walker = new jsdoc.src.Walker(); + walker.recurse(filename, ast, visitor); + + commentAttacher.finish(); + + // remove the comment/token arrays; we no longer need then + ast.comments = []; + ast.tokens = []; +}; diff --git a/third_party/jsdoc/lib/jsdoc/src/astnode.js b/third_party/jsdoc/lib/jsdoc/src/astnode.js new file mode 100644 index 0000000000..3eac7d52d8 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/src/astnode.js @@ -0,0 +1,299 @@ +// TODO: docs +'use strict'; + +var Syntax = require('jsdoc/src/syntax').Syntax; +var util = require('util'); + +// Counter for generating unique node IDs. +var uid = 100000000; + +/** + * Check whether an AST node represents a function. + * + * @param {Object} node - The AST node to check. + * @return {boolean} Set to `true` if the node is a function or `false` in all other cases. + */ +var isFunction = exports.isFunction = function(node) { + return node.type === Syntax.FunctionDeclaration || node.type === Syntax.FunctionExpression; +}; + +/** + * Check whether an AST node creates a new scope. + * + * @param {Object} node - The AST node to check. + * @return {Boolean} Set to `true` if the node creates a new scope, or `false` in all other cases. + */ +var isScope = exports.isScope = function(node) { + // TODO: handle blocks with "let" declarations + return !!node && typeof node === 'object' && ( node.type === Syntax.CatchClause || + isFunction(node) ); +}; + +// TODO: docs +var addNodeProperties = exports.addNodeProperties = function(node) { + var debugEnabled = !!global.env.opts.debug; + var newProperties = {}; + + if (!node || typeof node !== 'object') { + return null; + } + + if (!node.nodeId) { + newProperties.nodeId = { + value: 'astnode' + uid++, + enumerable: debugEnabled + }; + } + + if (!node.parent && node.parent !== null) { + newProperties.parent = { + // `null` means 'no parent', so use `undefined` for now + value: undefined, + writable: true + }; + } + + if (!node.enclosingScope && node.enclosingScope !== null) { + newProperties.enclosingScope = { + // `null` means 'no enclosing scope', so use `undefined` for now + value: undefined, + writable: true + }; + } + + if (debugEnabled && !node.parentId) { + newProperties.parentId = { + enumerable: true, + get: function() { + return this.parent ? this.parent.nodeId : null; + } + }; + } + + if (debugEnabled && !node.enclosingScopeId) { + newProperties.enclosingScopeId = { + enumerable: true, + get: function() { + return this.enclosingScope ? this.enclosingScope.nodeId : null; + } + }; + } + + Object.defineProperties(node, newProperties); + + return node; +}; + +// TODO: docs +var nodeToString = exports.nodeToString = function(node) { + var tempObject; + + var str = ''; + + switch (node.type) { + case Syntax.ArrayExpression: + tempObject = []; + node.elements.forEach(function(el, i) { + // handle sparse arrays. use `null` to represent missing values, consistent with + // JSON.stringify([,]). + if (!el) { + tempObject[i] = null; + } + // preserve literal values so that the JSON form shows the correct type + else if (el.type === Syntax.Literal) { + tempObject[i] = el.value; + } + else { + tempObject[i] = nodeToString(el); + } + }); + + str = JSON.stringify(tempObject); + break; + + case Syntax.AssignmentExpression: + str = nodeToString(node.left); + break; + + case Syntax.FunctionDeclaration: + // falls through + + case Syntax.FunctionExpression: + str = 'function'; + break; + + case Syntax.Identifier: + str = node.name; + break; + + case Syntax.Literal: + str = String(node.value); + break; + + case Syntax.MemberExpression: + // could be computed (like foo['bar']) or not (like foo.bar) + str = nodeToString(node.object); + if (node.computed) { + str += util.format('[%s]', node.property.raw); + } + else { + str += '.' + nodeToString(node.property); + } + break; + + case Syntax.ObjectExpression: + tempObject = {}; + node.properties.forEach(function(prop) { + var key = prop.key.name; + // preserve literal values so that the JSON form shows the correct type + if (prop.value.type === Syntax.Literal) { + tempObject[key] = prop.value.value; + } + else { + tempObject[key] = nodeToString(prop); + } + }); + + str = JSON.stringify(tempObject); + break; + + case Syntax.ThisExpression: + str = 'this'; + break; + + case Syntax.UnaryExpression: + // like -1. in theory, operator can be prefix or postfix. in practice, any value with a + // valid postfix operator (such as -- or ++) is not a UnaryExpression. + str = nodeToString(node.argument); + + // workaround for https://code.google.com/p/esprima/issues/detail?id=526 + if (node.prefix === true || node.prefix === undefined) { + str = node.operator + str; + } + else { + // this shouldn't happen + throw new Error( util.format('Found a UnaryExpression with a postfix operator: %j', + node) ); + } + break; + + case Syntax.VariableDeclarator: + str = nodeToString(node.id); + break; + + default: + str = ''; + } + + return str; +}; + +// TODO: docs +var getParamNames = exports.getParamNames = function(node) { + if (!node || !node.params) { + return []; + } + + return node.params.map(function(param) { + return nodeToString(param); + }); +}; + +// TODO: docs +var isAccessor = exports.isAccessor = function(node) { + return !!node && typeof node === 'object' && node.type === Syntax.Property && + (node.kind === 'get' || node.kind === 'set'); +}; + +// TODO: docs +var isAssignment = exports.isAssignment = function(node) { + return !!node && typeof node === 'object' && (node.type === Syntax.AssignmentExpression || + node.type === Syntax.VariableDeclarator); +}; + +// TODO: docs +/** + * Retrieve information about the node, including its name and type. + */ +var getInfo = exports.getInfo = function(node) { + var info = {}; + + switch (node.type) { + // like: "foo = 'bar'" (after declaring foo) + // like: "MyClass.prototype.myMethod = function() {}" (after declaring MyClass) + case Syntax.AssignmentExpression: + info.node = node.right; + info.name = nodeToString(node.left); + info.type = info.node.type; + info.value = nodeToString(info.node); + // if the assigned value is a function, we need to capture the parameter names here + info.paramnames = getParamNames(node.right); + break; + + // like: "function foo() {}" + case Syntax.FunctionDeclaration: + info.node = node; + info.name = nodeToString(node.id); + info.type = info.node.type; + info.paramnames = getParamNames(node); + break; + + // like the function in: "var foo = function() {}" + case Syntax.FunctionExpression: + info.node = node; + // TODO: should we add a name for, e.g., "var foo = function bar() {}"? + info.name = ''; + info.type = info.node.type; + info.paramnames = getParamNames(node); + break; + + // like the param "bar" in: "function foo(bar) {}" + case Syntax.Identifier: + info.node = node; + info.name = nodeToString(info.node); + info.type = info.node.type; + break; + + // like "a.b.c" + case Syntax.MemberExpression: + info.node = node; + info.name = nodeToString(info.node); + info.type = info.node.type; + break; + + // like "a: 0" in "var foo = {a: 0}" + case Syntax.Property: + info.node = node.value; + info.name = nodeToString(node.key); + info.value = nodeToString(info.node); + + if ( isAccessor(node) ) { + info.type = nodeToString(info.node); + info.paramnames = getParamNames(info.node); + } + else { + info.type = info.node.type; + } + + break; + + // like: "var i = 0" (has init property) + // like: "var i" (no init property) + case Syntax.VariableDeclarator: + info.node = node.init || node.id; + info.name = node.id.name; + + if (node.init) { + info.type = info.node.type; + info.value = nodeToString(info.node); + } + + break; + + default: + info.node = node; + info.type = info.node.type; + } + + return info; +}; diff --git a/third_party/jsdoc/lib/jsdoc/src/filter.js b/third_party/jsdoc/lib/jsdoc/src/filter.js new file mode 100644 index 0000000000..135c4c4f40 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/src/filter.js @@ -0,0 +1,67 @@ +/*global env: true */ +/** + @module jsdoc/src/filter + + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ +'use strict'; + +var path = require('jsdoc/path'); + +var pwd = env.pwd; + +function makeRegExp(config) { + var regExp = null; + + if (config) { + regExp = (typeof config === 'string') ? new RegExp(config) : config; + } + + return regExp; +} + +/** + @constructor + @param {object} opts + @param {string[]} opts.exclude - Specific files to exclude. + @param {string|RegExp} opts.includePattern + @param {string|RegExp} opts.excludePattern + */ +exports.Filter = function(opts) { + this.exclude = opts.exclude && Array.isArray(opts.exclude) ? + opts.exclude.map(function($) { + return path.resolve(pwd, $); + }) : + null; + this.includePattern = makeRegExp(opts.includePattern); + this.excludePattern = makeRegExp(opts.excludePattern); +}; + +/** + @param {string} filepath - The filepath to check. + @returns {boolean} Should the given file be included? + */ +exports.Filter.prototype.isIncluded = function(filepath) { + var included = true; + + filepath = path.resolve(pwd, filepath); + + if ( this.includePattern && !this.includePattern.test(filepath) ) { + included = false; + } + + if ( this.excludePattern && this.excludePattern.test(filepath) ) { + included = false; + } + + if (this.exclude) { + this.exclude.forEach(function(exclude) { + if ( filepath.indexOf(exclude) === 0 ) { + included = false; + } + }); + } + + return included; +}; diff --git a/third_party/jsdoc/lib/jsdoc/src/handlers.js b/third_party/jsdoc/lib/jsdoc/src/handlers.js new file mode 100644 index 0000000000..6773e9b4ec --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/src/handlers.js @@ -0,0 +1,325 @@ +/** + * @module jsdoc/src/handlers + */ +'use strict'; + +var escape = require('escape-string-regexp'); +var jsdoc = { + doclet: require('jsdoc/doclet'), + name: require('jsdoc/name'), + util: { + logger: require('jsdoc/util/logger') + } +}; +var util = require('util'); + +var currentModule = null; +var SCOPE_NAMES = jsdoc.name.SCOPE.NAMES; +var SCOPE_PUNC = jsdoc.name.SCOPE.PUNC; +var unresolvedName = /^((?:module.)?exports|this)(\.|$)/; + +function CurrentModule(doclet) { + this.doclet = doclet; + this.longname = doclet.longname; + this.originalName = doclet.meta.code.name || ''; +} + +function filterByLongname(doclet) { + // you can't document prototypes + if ( /#$/.test(doclet.longname) ) { + return true; + } + + return false; +} + +function createDoclet(comment, e) { + var doclet; + var err; + + try { + doclet = new jsdoc.doclet.Doclet(comment, e); + } + catch (error) { + err = new Error( util.format('cannot create a doclet for the comment "%s": %s', + comment.replace(/[\r\n]/g, ''), error.message) ); + jsdoc.util.logger.error(err); + doclet = new jsdoc.doclet.Doclet('', e); + } + + return doclet; +} + +/** + * Create a doclet for a `symbolFound` event. The doclet represents an actual symbol that is defined + * in the code. + * + * Here's why this function is useful. A JSDoc comment can define a symbol name by including: + * + * + A `@name` tag + * + Another tag that accepts a name, such as `@function` + * + * When the JSDoc comment defines a symbol name, we treat it as a "virtual comment" for a symbol + * that isn't actually present in the code. And if a virtual comment is attached to a symbol, it's + * possible that the comment and symbol have nothing to do with one another. + * + * To handle this case, this function checks the new doclet to see if we've already added a name + * property by parsing the JSDoc comment. If so, this method creates a replacement doclet that + * ignores the attached JSDoc comment and only looks at the code. + * + * @private + */ +function createSymbolDoclet(comment, e) { + var doclet = createDoclet(comment, e); + + if (doclet.name) { + // try again, without the comment + e.comment = '@undocumented'; + doclet = createDoclet(e.comment, e); + } + + return doclet; +} + +function setCurrentModule(doclet) { + if (doclet.kind === 'module') { + currentModule = new CurrentModule(doclet); + } +} + +function setModuleScopeMemberOf(doclet) { + // handle module symbols that are _not_ assigned to module.exports + if (currentModule && currentModule.longname !== doclet.name) { + // if we don't already know the scope, it must be an inner member + if (!doclet.scope) { + doclet.addTag('inner'); + } + + // if the doclet isn't a memberof anything yet, and it's not a global, it must be a memberof + // the current module + if (!doclet.memberof && doclet.scope !== SCOPE_NAMES.GLOBAL) { + doclet.addTag('memberof', currentModule.longname); + } + } +} + +function setDefaultScope(doclet) { + // module doclets don't get a default scope + if (!doclet.scope && doclet.kind !== 'module') { + doclet.setScope(SCOPE_NAMES.GLOBAL); + } +} + +function addDoclet(parser, newDoclet) { + var e; + if (newDoclet) { + setCurrentModule(newDoclet); + e = { doclet: newDoclet }; + parser.emit('newDoclet', e); + + if ( !e.defaultPrevented && !filterByLongname(e.doclet) ) { + parser.addResult(e.doclet); + } + } +} + +function processAlias(parser, doclet, astNode) { + var memberofName; + + if (doclet.alias === '{@thisClass}') { + memberofName = parser.resolveThis(astNode); + + // "class" refers to the owner of the prototype, not the prototype itself + if ( /^(.+?)(\.prototype|#)$/.test(memberofName) ) { + memberofName = RegExp.$1; + } + doclet.alias = memberofName; + } + + doclet.addTag('name', doclet.alias); + doclet.postProcess(); +} + +// TODO: separate code that resolves `this` from code that resolves the module object +function findSymbolMemberof(parser, doclet, astNode, nameStartsWith, trailingPunc) { + var memberof = ''; + var nameAndPunc = nameStartsWith + (trailingPunc || ''); + var scopePunc = ''; + + // remove stuff that indicates module membership (but don't touch the name `module.exports`, + // which identifies the module object itself) + if (doclet.name !== 'module.exports') { + doclet.name = doclet.name.replace(nameAndPunc, ''); + } + + // like `bar` in: + // exports.bar = 1; + // module.exports.bar = 1; + // module.exports = MyModuleObject; MyModuleObject.bar = 1; + if (nameStartsWith !== 'this' && currentModule && doclet.name !== 'module.exports') { + memberof = currentModule.longname; + scopePunc = SCOPE_PUNC.STATIC; + } + // like: module.exports = 1; + else if (doclet.name === 'module.exports' && currentModule) { + doclet.addTag('name', currentModule.longname); + doclet.postProcess(); + } + else { + memberof = parser.resolveThis(astNode); + + // like the following at the top level of a module: + // this.foo = 1; + if (nameStartsWith === 'this' && currentModule && !memberof) { + memberof = currentModule.longname; + scopePunc = SCOPE_PUNC.STATIC; + } + else { + scopePunc = SCOPE_PUNC.INSTANCE; + } + } + + return { + memberof: memberof, + scopePunc: scopePunc + }; +} + +function addSymbolMemberof(parser, doclet, astNode) { + var basename; + var memberof; + var memberofInfo; + var moduleOriginalName = ''; + var resolveTargetRegExp; + var scopePunc; + var unresolved; + + if (!astNode) { + return; + } + + // check to see if the doclet name is an unresolved reference to the module object, or to `this` + // TODO: handle cases where the module object is shadowed in the current scope + if (currentModule) { + moduleOriginalName = '|' + currentModule.originalName; + } + resolveTargetRegExp = new RegExp('^((?:module.)?exports|this' + moduleOriginalName + + ')(\\.|$)'); + unresolved = resolveTargetRegExp.exec(doclet.name); + + if (unresolved) { + memberofInfo = findSymbolMemberof(parser, doclet, astNode, unresolved[1], unresolved[2]); + memberof = memberofInfo.memberof; + scopePunc = memberofInfo.scopePunc; + + if (memberof) { + doclet.name = doclet.name ? + memberof + scopePunc + doclet.name : + memberof; + } + } + else { + memberofInfo = parser.astnodeToMemberof(astNode); + if ( Array.isArray(memberofInfo) ) { + basename = memberofInfo[1]; + memberof = memberofInfo[0]; + } + else { + memberof = memberofInfo; + } + } + + // if we found a memberof name, apply it to the doclet + if (memberof) { + doclet.addTag('memberof', memberof); + if (basename) { + doclet.name = (doclet.name || '') + .replace(new RegExp('^' + escape(basename) + '.'), ''); + } + } + // otherwise, add the defaults for a module (if we're currently in a module) + else { + setModuleScopeMemberOf(doclet); + } +} + +function newSymbolDoclet(parser, docletSrc, e) { + var memberofName = null; + var newDoclet = createSymbolDoclet(docletSrc, e); + + // if there's an alias, use that as the symbol name + if (newDoclet.alias) { + processAlias(parser, newDoclet, e.astnode); + } + // otherwise, get the symbol name from the code + else if (e.code && e.code.name) { + newDoclet.addTag('name', e.code.name); + if (!newDoclet.memberof) { + addSymbolMemberof(parser, newDoclet, e.astnode); + } + + newDoclet.postProcess(); + } + else { + return false; + } + + // set the scope to global unless any of the following are true: + // a) the doclet is a memberof something + // b) the doclet represents a module + // c) we're in a module that exports only this symbol + if ( !newDoclet.memberof && newDoclet.kind !== 'module' && + (!currentModule || currentModule.longname !== newDoclet.name) ) { + newDoclet.scope = SCOPE_NAMES.GLOBAL; + } + + addDoclet(parser, newDoclet); + e.doclet = newDoclet; +} + +/** + * Attach these event handlers to a particular instance of a parser. + * @param parser + */ +exports.attachTo = function(parser) { + // Handle JSDoc "virtual comments" that include one of the following: + // + A `@name` tag + // + Another tag that accepts a name, such as `@function` + parser.on('jsdocCommentFound', function(e) { + var comments = e.comment.split(/@also\b/g); + var newDoclet; + + for (var i = 0, l = comments.length; i < l; i++) { + newDoclet = createDoclet(comments[i], e); + + // we're only interested in virtual comments here + if (!newDoclet.name) { + continue; + } + + // add the default scope/memberof for a module (if we're in a module) + setModuleScopeMemberOf(newDoclet); + newDoclet.postProcess(); + + // if we _still_ don't have a scope, use the default + setDefaultScope(newDoclet); + + addDoclet(parser, newDoclet); + + e.doclet = newDoclet; + } + }); + + // Handle named symbols in the code. May or may not have a JSDoc comment attached. + parser.on('symbolFound', function(e) { + var comments = e.comment.split(/@also\b/g); + + for (var i = 0, l = comments.length; i < l; i++) { + newSymbolDoclet(parser, comments[i], e); + } + }); + + parser.on('fileComplete', function(e) { + currentModule = null; + }); +}; diff --git a/third_party/jsdoc/lib/jsdoc/src/parser.js b/third_party/jsdoc/lib/jsdoc/src/parser.js new file mode 100644 index 0000000000..aa357a1228 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/src/parser.js @@ -0,0 +1,519 @@ +/*global env, Packages */ +/*eslint no-script-url:0 */ +/** + * @module jsdoc/src/parser + */ +'use strict'; + +var events = require('events'); +var fs = require('jsdoc/fs'); +var jsdoc = { + doclet: require('jsdoc/doclet'), + name: require('jsdoc/name'), + src: { + astnode: require('jsdoc/src/astnode'), + syntax: require('jsdoc/src/syntax') + }, + util: { + doop: require('jsdoc/util/doop'), + runtime: require('jsdoc/util/runtime') + } +}; +var logger = require('jsdoc/util/logger'); +var path = require('jsdoc/path'); +var util = require('util'); + +var hasOwnProp = Object.prototype.hasOwnProperty; +var Syntax = jsdoc.src.syntax.Syntax; + +// Prefix for JavaScript strings that were provided in lieu of a filename. +var SCHEMA = 'javascript:'; +// TODO: docs +var PARSERS = exports.PARSERS = { + esprima: 'jsdoc/src/parser', + rhino: 'rhino/jsdoc/src/parser' +}; + +// TODO: docs +exports.createParser = function(type) { + var modulePath; + + if (!type) { + type = jsdoc.util.runtime.isRhino() ? 'rhino' : 'esprima'; + } + + if (PARSERS[type]) { + modulePath = PARSERS[type]; + } + else { + modulePath = path.join( path.getResourcePath(path.dirname(type)), path.basename(type) ); + } + + try { + return new ( require(modulePath) ).Parser(); + } + catch (e) { + logger.fatal('Unable to create the parser type "' + type + '": ' + e); + } +}; + +// TODO: docs +/** + * @class + * @alias module:jsdoc/src/parser.Parser + * @mixes module:events.EventEmitter + * + * @example Create a new parser. + * var jsdocParser = new (require('jsdoc/src/parser').Parser)(); + */ +var Parser = exports.Parser = function(builderInstance, visitorInstance, walkerInstance) { + this.clear(); + + this._astBuilder = builderInstance || new (require('jsdoc/src/astbuilder')).AstBuilder(); + this._visitor = visitorInstance || new (require('jsdoc/src/visitor')).Visitor(this); + this._walker = walkerInstance || new (require('jsdoc/src/walker')).Walker(); + + Object.defineProperties(this, { + astBuilder: { + get: function() { + return this._astBuilder; + } + }, + visitor: { + get: function() { + return this._visitor; + } + }, + walker: { + get: function() { + return this._walker; + } + } + }); +}; +util.inherits(Parser, events.EventEmitter); + +// TODO: docs +Parser.prototype.clear = function() { + this._resultBuffer = []; + this.refs = {}; + this.refs[jsdoc.name.LONGNAMES.GLOBAL] = {}; + this.refs[jsdoc.name.LONGNAMES.GLOBAL].meta = {}; +}; + +// TODO: update docs +/** + * Parse the given source files for JSDoc comments. + * @param {Array.} sourceFiles An array of filepaths to the JavaScript sources. + * @param {string} [encoding=utf8] + * + * @fires module:jsdoc/src/parser.Parser.parseBegin + * @fires module:jsdoc/src/parser.Parser.fileBegin + * @fires module:jsdoc/src/parser.Parser.jsdocCommentFound + * @fires module:jsdoc/src/parser.Parser.symbolFound + * @fires module:jsdoc/src/parser.Parser.newDoclet + * @fires module:jsdoc/src/parser.Parser.fileComplete + * @fires module:jsdoc/src/parser.Parser.parseComplete + * + * @example Parse two source files. + * var myFiles = ['file1.js', 'file2.js']; + * var docs = jsdocParser.parse(myFiles); + */ +Parser.prototype.parse = function(sourceFiles, encoding) { + encoding = encoding || env.conf.encoding || 'utf8'; + + var filename = ''; + var sourceCode = ''; + var parsedFiles = []; + var e = {}; + + if (typeof sourceFiles === 'string') { + sourceFiles = [sourceFiles]; + } + + e.sourcefiles = sourceFiles; + logger.debug('Parsing source files: %j', sourceFiles); + + this.emit('parseBegin', e); + + for (var i = 0, l = sourceFiles.length; i < l; i++) { + sourceCode = ''; + + if (sourceFiles[i].indexOf(SCHEMA) === 0) { + sourceCode = sourceFiles[i].substr(SCHEMA.length); + filename = '[[string' + i + ']]'; + } + else { + filename = sourceFiles[i]; + try { + sourceCode = fs.readFileSync(filename, encoding); + } + catch(e) { + logger.error('Unable to read and parse the source file %s: %s', filename, e); + } + } + + if (sourceCode.length) { + this._parseSourceCode(sourceCode, filename); + parsedFiles.push(filename); + } + } + + this.emit('parseComplete', { + sourcefiles: parsedFiles, + doclets: this._resultBuffer + }); + logger.debug('Finished parsing source files.'); + + return this._resultBuffer; +}; + +// TODO: docs +Parser.prototype.fireProcessingComplete = function(doclets) { + this.emit('processingComplete', { doclets: doclets }); +}; + +// TODO: docs +Parser.prototype.results = function() { + return this._resultBuffer; +}; + +// TODO: update docs +/** + * @param {Object} o The parse result to add to the result buffer. + */ +Parser.prototype.addResult = function(o) { + this._resultBuffer.push(o); +}; + +// TODO: docs +Parser.prototype.addAstNodeVisitor = function(visitor) { + this._visitor.addAstNodeVisitor(visitor); +}; + +// TODO: docs +Parser.prototype.getAstNodeVisitors = function() { + return this._visitor.getAstNodeVisitors(); +}; + +// TODO: docs +function pretreat(code) { + return code + // comment out hashbang at the top of the file, like: #!/usr/bin/env node + .replace(/^(\#\![\S \t]+\r?\n)/, '// $1') + + // to support code minifiers that preserve /*! comments, treat /*!* as equivalent to /** + .replace(/\/\*\!\*/g, '/**') + // merge adjacent doclets + .replace(/\*\/\/\*\*+/g, '@also'); +} + +/** @private */ +Parser.prototype._parseSourceCode = function(sourceCode, sourceName) { + var ast; + var globalScope; + + var e = { + filename: sourceName + }; + + this.emit('fileBegin', e); + logger.printInfo('Parsing %s ...', sourceName); + + if (!e.defaultPrevented) { + e = { + filename: sourceName, + source: sourceCode + }; + this.emit('beforeParse', e); + sourceCode = e.source; + sourceName = e.filename; + + sourceCode = pretreat(e.source); + + ast = this._astBuilder.build(sourceCode, sourceName); + if (ast) { + this._walker.recurse(sourceName, ast, this._visitor); + } + } + + this.emit('fileComplete', e); + logger.info('complete.'); +}; + +// TODO: docs +Parser.prototype.addDocletRef = function(e) { + var node; + + if (e && e.code && e.code.node) { + node = e.code.node; + // allow lookup from value => doclet + if (e.doclet) { + this.refs[node.nodeId] = e.doclet; + } + // keep references to undocumented anonymous functions, too, as they might have scoped vars + else if ( + (node.type === Syntax.FunctionDeclaration || node.type === Syntax.FunctionExpression) && + !this.refs[node.nodeId] ) { + this.refs[node.nodeId] = { + longname: jsdoc.name.LONGNAMES.ANONYMOUS, + meta: { + code: e.code + } + }; + } + } +}; + +// TODO: docs +Parser.prototype._getDoclet = function(id) { + if ( hasOwnProp.call(this.refs, id) ) { + return this.refs[id]; + } + + return null; +}; + +// TODO: docs +/** + * @param {string} name - The symbol's longname. + * @return {string} The symbol's basename. + */ +Parser.prototype.getBasename = function(name) { + if (name !== undefined) { + return name.replace(/^([$a-z_][$a-z_0-9]*).*?$/i, '$1'); + } +}; + +// TODO: docs +function definedInScope(doclet, basename) { + return !!doclet && !!doclet.meta && !!doclet.meta.vars && !!basename && + hasOwnProp.call(doclet.meta.vars, basename); +} + +// TODO: docs +/** + * Given a node, determine what the node is a member of. + * @param {node} node + * @returns {string} The long name of the node that this is a member of. + */ +Parser.prototype.astnodeToMemberof = function(node) { + var basename; + var doclet; + var scope; + + var result = ''; + var type = node.type; + + if ( (type === Syntax.FunctionDeclaration || type === Syntax.FunctionExpression || + type === Syntax.VariableDeclarator) && node.enclosingScope ) { + doclet = this._getDoclet(node.enclosingScope.nodeId); + + if (!doclet) { + result = jsdoc.name.LONGNAMES.ANONYMOUS + jsdoc.name.SCOPE.PUNC.INNER; + } + else { + result = (doclet.longname || doclet.name) + jsdoc.name.SCOPE.PUNC.INNER; + } + } + else { + // check local references for aliases + scope = node; + basename = this.getBasename( jsdoc.src.astnode.nodeToString(node) ); + + // walk up the scope chain until we find the scope in which the node is defined + while (scope.enclosingScope) { + doclet = this._getDoclet(scope.enclosingScope.nodeId); + if ( doclet && definedInScope(doclet, basename) ) { + result = [doclet.meta.vars[basename], basename]; + break; + } + else { + // move up + scope = scope.enclosingScope; + } + } + + // do we know that it's a global? + doclet = this.refs[jsdoc.name.LONGNAMES.GLOBAL]; + if ( doclet && definedInScope(doclet, basename) ) { + result = [doclet.meta.vars[basename], basename]; + } + + // have we seen the node's parent? if so, use that + else if (node.parent) { + doclet = this._getDoclet(node.parent.nodeId); + + // set the result if we found a doclet. (if we didn't, the AST node may describe a + // global symbol.) + if (doclet) { + result = doclet.longname || doclet.name; + } + } + } + + return result; +}; + +// TODO: docs +/** + * Resolve what "this" refers to relative to a node. + * @param {node} node - The "this" node + * @returns {string} The longname of the enclosing node. + */ +Parser.prototype.resolveThis = function(node) { + var doclet; + var result; + + // In general, if there's an enclosing scope, we use the enclosing scope to resolve `this`. + // For object properties, we use the node's parent (the object) instead. + if (node.type !== Syntax.Property && node.enclosingScope) { + doclet = this._getDoclet(node.enclosingScope.nodeId); + + if (!doclet) { + result = jsdoc.name.LONGNAMES.ANONYMOUS; // TODO handle global this? + } + else if (doclet['this']) { + result = doclet['this']; + } + // like: Foo.constructor = function(n) { /** blah */ this.name = n; } + else if (doclet.kind === 'function' && doclet.memberof) { + result = doclet.memberof; + } + // like: var foo = function(n) { /** blah */ this.bar = n; } + else if ( doclet.kind === 'member' && jsdoc.src.astnode.isAssignment(node) ) { + result = doclet.longname || doclet.name; + } + // walk up to the closest class we can find + else if (doclet.kind === 'class' || doclet.kind === 'module') { + result = doclet.longname || doclet.name; + } + else if (node.enclosingScope) { + result = this.resolveThis(node.enclosingScope); + } + } + else if (node.parent) { + doclet = this.refs[node.parent.nodeId]; + + // TODO: is this behavior correct? when do we get here? + if (!doclet) { + result = ''; // global? + } + else { + result = doclet.longname || doclet.name; + } + } + // TODO: is this behavior correct? when do we get here? + else { + result = ''; // global? + } + + return result; +}; + +/** + * Given an AST node representing an object property, find the doclets for the parent object or + * objects. + * + * If the object is part of a simple assignment (for example, `var foo = { x: 1 }`), this method + * returns a single doclet (in this case, the doclet for `foo`). + * + * If the object is part of a chained assignment (for example, `var foo = exports.FOO = { x: 1 }`, + * this method returns multiple doclets (in this case, the doclets for `foo` and `exports.FOO`). + * + * @param {Object} node - An AST node representing an object property. + * @return {Array.} An array of doclets for the parent object or objects, or + * an empty array if no doclets are found. + */ +Parser.prototype.resolvePropertyParents = function(node) { + var currentAncestor = node.parent; + var nextAncestor = currentAncestor ? currentAncestor.parent : null; + var doclet; + var doclets = []; + + while (currentAncestor) { + doclet = this._getDoclet(currentAncestor.nodeId); + if (doclet) { + doclets.push(doclet); + } + + // if the next ancestor is an assignment expression (for example, `exports.FOO` in + // `var foo = exports.FOO = { x: 1 }`, keep walking upwards + if (nextAncestor && nextAncestor.type === Syntax.AssignmentExpression) { + nextAncestor = nextAncestor.parent; + currentAncestor = currentAncestor.parent; + } + // otherwise, we're done + else { + currentAncestor = null; + } + } + + return doclets; +}; + +// TODO: docs +/** + * Resolve what function a var is limited to. + * @param {astnode} node + * @param {string} basename The leftmost name in the long name: in foo.bar.zip the basename is foo. + */ +Parser.prototype.resolveVar = function(node, basename) { + var doclet; + var result; + var scope = node.enclosingScope; + + // HACK: return an empty string for function declarations so they don't end up in anonymous + // scope (see #685 and #693) + if (node.type === Syntax.FunctionDeclaration) { + result = ''; + } + else if (!scope) { + result = ''; // global + } + else { + doclet = this._getDoclet(scope.nodeId); + if ( definedInScope(doclet, basename) ) { + result = doclet.longname; + } + else { + result = this.resolveVar(scope, basename); + } + } + + return result; +}; + +// TODO: docs +Parser.prototype.resolveEnum = function(e) { + var doclets = this.resolvePropertyParents(e.code.node.parent); + + doclets.forEach(function(doclet) { + if (doclet && doclet.isEnum) { + doclet.properties = doclet.properties || []; + + // members of an enum inherit the enum's type + if (doclet.type && !e.doclet.type) { + // clone the type to prevent circular refs + e.doclet.type = jsdoc.util.doop(doclet.type); + } + + delete e.doclet.undocumented; + e.doclet.defaultvalue = e.doclet.meta.code.value; + + // add the doclet to the parent's properties + doclet.properties.push(e.doclet); + } + }); +}; + +// TODO: document other events +/** + * Fired once for each JSDoc comment in the current source code. + * @event jsdocCommentFound + * @memberof module:jsdoc/src/parser.Parser + * @type {Object} + * @property {string} comment The text content of the JSDoc comment + * @property {number} lineno The line number associated with the found comment. + * @property {string} filename The file name associated with the found comment. + */ diff --git a/third_party/jsdoc/lib/jsdoc/src/scanner.js b/third_party/jsdoc/lib/jsdoc/src/scanner.js new file mode 100644 index 0000000000..a8170a718c --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/src/scanner.js @@ -0,0 +1,70 @@ +/*global env: true */ +/** + @module jsdoc/src/scanner + @requires module:fs + + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ +'use strict'; + +var fs = require('jsdoc/fs'); +var logger = require('jsdoc/util/logger'); +var path = require('jsdoc/path'); + +/** + @constructor + @mixes module:events + */ +exports.Scanner = function() {}; +exports.Scanner.prototype = Object.create( require('events').EventEmitter.prototype ); + +/** + Recursively searches the given searchPaths for js files. + @param {Array.} searchPaths + @param {number} [depth=1] + @fires sourceFileFound + */ +exports.Scanner.prototype.scan = function(searchPaths, depth, filter) { + var currentFile; + var isFile; + + var filePaths = []; + var pwd = env.pwd; + var self = this; + + searchPaths = searchPaths || []; + depth = depth || 1; + + searchPaths.forEach(function($) { + var filepath = path.resolve( pwd, decodeURIComponent($) ); + + try { + currentFile = fs.statSync(filepath); + } + catch (e) { + logger.error('Unable to find the source file or directory %s', filepath); + return; + } + + if ( currentFile.isFile() ) { + filePaths.push(filepath); + } + else { + filePaths = filePaths.concat( fs.ls(filepath, depth) ); + } + }); + + filePaths = filePaths.filter(function($) { + return filter.isIncluded($); + }); + + filePaths = filePaths.filter(function($) { + var e = { fileName: $ }; + self.emit('sourceFileFound', e); + + return !e.defaultPrevented; + }); + + return filePaths; +}; diff --git a/third_party/jsdoc/lib/jsdoc/src/syntax.js b/third_party/jsdoc/lib/jsdoc/src/syntax.js new file mode 100644 index 0000000000..3780129dd7 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/src/syntax.js @@ -0,0 +1,67 @@ +'use strict'; + +// TODO: docs +exports.Syntax = { + ArrayExpression: 'ArrayExpression', + ArrayPattern: 'ArrayPattern', + ArrowFunctionExpression: 'ArrowFunctionExpression', + AssignmentExpression: 'AssignmentExpression', + BinaryExpression: 'BinaryExpression', + BlockStatement: 'BlockStatement', + BreakStatement: 'BreakStatement', + CallExpression: 'CallExpression', + CatchClause: 'CatchClause', + ClassBody: 'ClassBody', + ClassDeclaration: 'ClassDeclaration', + ClassExpression: 'ClassExpression', + ComprehensionBlock: 'ComprehensionBlock', + ComprehensionExpression: 'ComprehensionExpression', + ConditionalExpression: 'ConditionalExpression', + ContinueStatement: 'ContinueStatement', + DebuggerStatement: 'DebuggerStatement', + DoWhileStatement: 'DoWhileStatement', + EmptyStatement: 'EmptyStatement', + ExportBatchSpecifier: 'ExportBatchSpecifier', + ExportDeclaration: 'ExportDeclaration', + ExportSpecifier: 'ExportSpecifier', + ExpressionStatement: 'ExpressionStatement', + ForInStatement: 'ForInStatement', + ForOfStatement: 'ForOfStatement', + ForStatement: 'ForStatement', + FunctionDeclaration: 'FunctionDeclaration', + FunctionExpression: 'FunctionExpression', + Identifier: 'Identifier', + IfStatement: 'IfStatement', + ImportDeclaration: 'ImportDeclaration', + ImportSpecifier: 'ImportSpecifier', + LabeledStatement: 'LabeledStatement', + LetStatement: 'LetStatement', // TODO: update Rhino to use VariableDeclaration + Literal: 'Literal', + LogicalExpression: 'LogicalExpression', + MemberExpression: 'MemberExpression', + MethodDefinition: 'MethodDefinition', + ModuleDeclaration: 'ModuleDeclaration', + NewExpression: 'NewExpression', + ObjectExpression: 'ObjectExpression', + ObjectPattern: 'ObjectPattern', + Program: 'Program', + Property: 'Property', + ReturnStatement: 'ReturnStatement', + SequenceExpression: 'SequenceExpression', + SpreadElement: 'SpreadElement', + SwitchCase: 'SwitchCase', + SwitchStatement: 'SwitchStatement', + TaggedTemplateExpression: 'TaggedTemplateExpression', + TemplateElement: 'TemplateElement', + TemplateLiteral: 'TemplateLiteral', + ThisExpression: 'ThisExpression', + ThrowStatement: 'ThrowStatement', + TryStatement: 'TryStatement', + UnaryExpression: 'UnaryExpression', + UpdateExpression: 'UpdateExpression', + VariableDeclaration: 'VariableDeclaration', + VariableDeclarator: 'VariableDeclarator', + WhileStatement: 'WhileStatement', + WithStatement: 'WithStatement', + YieldExpression: 'YieldExpression' +}; diff --git a/third_party/jsdoc/lib/jsdoc/src/visitor.js b/third_party/jsdoc/lib/jsdoc/src/visitor.js new file mode 100644 index 0000000000..80e909e04a --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/src/visitor.js @@ -0,0 +1,442 @@ +/** + * @module jsdoc/src/visitor + */ +'use strict'; + +// TODO: consider exporting more stuff so users can override it + +var jsdoc = { + doclet: require('jsdoc/doclet'), + name: require('jsdoc/name'), + src: { + astnode: require('jsdoc/src/astnode'), + syntax: require('jsdoc/src/syntax') + }, + util: { + logger: require('jsdoc/util/logger') + } +}; +var util = require('util'); + +var hasOwnProp = Object.prototype.hasOwnProperty; +var Syntax = jsdoc.src.syntax.Syntax; + +// TODO: docs +function getLeadingComment(node) { + var comment = null; + var leadingComments = node.leadingComments; + + if (Array.isArray(leadingComments) && leadingComments.length && leadingComments[0].raw) { + comment = leadingComments[0].raw; + } + + return comment; +} + +// TODO: docs +function makeVarsFinisher(scopeDoclet) { + return function(e) { + // no need to evaluate all things related to scopeDoclet again, just use it + if (scopeDoclet && e.doclet && e.doclet.alias) { + scopeDoclet.meta.vars[e.code.name] = e.doclet.longname; + } + }; +} + +/** + * For function parameters that have inline documentation, create a function that will merge the + * inline documentation into the function's doclet. If the parameter is already documented in the + * function's doclet, the inline documentation will be ignored. + * + * @private + * @param {module:jsdoc/src/parser.Parser} parser - The JSDoc parser. + * @return {function} A function that merges a parameter's inline documentation into the function's + * doclet. + */ +function makeInlineParamsFinisher(parser) { + return function(e) { + var documentedParams; + var knownParams; + var param; + var parentDoclet; + + var i = 0; + + if (e.doclet && e.doclet.meta && e.doclet.meta.code && e.doclet.meta.code.node && + e.doclet.meta.code.node.parent) { + parentDoclet = parser._getDoclet(e.doclet.meta.code.node.parent.nodeId); + } + if (!parentDoclet) { + return; + } + + // we only want to use the doclet if it's param-specific (but not, for example, if it's + // a param tagged with `@exports` in an AMD module) + if (e.doclet.kind !== 'param') { + return; + } + + parentDoclet.params = parentDoclet.params || []; + documentedParams = parentDoclet.params; + knownParams = parentDoclet.meta.code.paramnames; + + while (true) { + param = documentedParams[i]; + + // is the param already documented? if so, we don't need to use the doclet + if (param && param.name === e.doclet.name) { + e.doclet.undocumented = true; + break; + } + + // if we ran out of documented params, or we're at the parameter's actual position, + // splice in the param at the current index + if ( !param || i === knownParams.indexOf(e.doclet.name) ) { + documentedParams.splice(i, 0, { + type: e.doclet.type, + description: '', + name: e.doclet.name + }); + + // the doclet is no longer needed + e.doclet.undocumented = true; + + break; + } + + i++; + } + }; +} + +// TODO: docs +function SymbolFound(node, filename, extras) { + var self = this; + extras = extras || {}; + + this.id = extras.id || node.nodeId; + this.comment = extras.comment || getLeadingComment(node) || '@undocumented'; + this.lineno = extras.lineno || node.loc.start.line; + this.range = extras.range || node.range; + this.filename = extras.filename || filename; + this.astnode = extras.astnode || node; + this.code = extras.code; + this.event = extras.event || 'symbolFound'; + this.finishers = extras.finishers || []; + + // make sure the event includes properties that don't have default values + Object.keys(extras).forEach(function(key) { + self[key] = extras[key]; + }); +} + +// TODO: docs +function JsdocCommentFound(comment, filename) { + this.comment = comment.raw; + this.lineno = comment.loc.start.line; + this.filename = filename; + this.range = comment.range; + + Object.defineProperty(this, 'event', { + value: 'jsdocCommentFound' + }); +} + +// TODO: docs +var Visitor = exports.Visitor = function(parser) { + this._parser = parser; + + // Mozilla Parser API node visitors added by plugins + this._nodeVisitors = []; + // built-in visitors + this._visitors = [ + this.visitNodeComments, + this.visitNode + ]; +}; + +// TODO: docs +Visitor.prototype.addAstNodeVisitor = function(visitor) { + this._nodeVisitors.push(visitor); +}; + +// TODO: docs +Visitor.prototype.removeAstNodeVisitor = function(visitor) { + var idx = this._nodeVisitors.indexOf(visitor); + if (idx !== -1) { + this._nodeVisitors.splice(idx, 1); + } +}; + +// TODO: docs +Visitor.prototype.getAstNodeVisitors = function() { + return this._nodeVisitors; +}; + +// TODO: docs; visitor signature is (node, parser, filename) +Visitor.prototype.visit = function(node, filename) { + var i; + var l; + + for (i = 0, l = this._visitors.length; i < l; i++) { + this._visitors[i].call(this, node, this._parser, filename); + } + + // we also need to visit standalone comments, which are not attached to a node + if (node.type === Syntax.Program && node.comments && node.comments.length) { + for (i = 0, l = node.comments.length; i < l; i++) { + this.visitNodeComments.call(this, node.comments[i], this._parser, filename); + } + } + + return true; +}; + +// TODO: docs +/** + * Verify that a block comment exists and that its leading delimiter does not contain three or more + * asterisks. + * + * @private + * @memberof module:jsdoc/src/parser.Parser + */ +function isValidJsdoc(commentSrc) { + return commentSrc && commentSrc.indexOf('/***') !== 0; +} + +// TODO: docs +function hasJsdocComments(node) { + return (node && node.leadingComments && node.leadingComments.length > 0) || + (node && node.trailingComments && node.trailingComments.length > 0); +} + +// TODO: docs +function removeCommentDelimiters(comment) { + return comment.substring(2, comment.length - 2); +} + +// TODO: docs +function updateCommentNode(commentNode, comment) { + commentNode.raw = comment; + commentNode.value = removeCommentDelimiters(comment); +} + +// TODO: docs +Visitor.prototype.visitNodeComments = function(node, parser, filename) { + var comment; + var comments; + var e; + + var BLOCK_COMMENT = 'Block'; + + if ( !hasJsdocComments(node) && (!node.type || node.type !== BLOCK_COMMENT) ) { + return true; + } + + comments = []; + if (node.type === BLOCK_COMMENT) { + comments.push(node); + } + + if (node.leadingComments && node.leadingComments.length) { + comments = node.leadingComments.slice(0); + } + + if (node.trailingComments && node.trailingComments.length) { + comments = comments.concat( node.trailingComments.slice(0) ); + } + + for (var i = 0, l = comments.length; i < l; i++) { + comment = comments[i]; + if ( isValidJsdoc(comment.raw) ) { + e = new JsdocCommentFound(comment, filename); + + parser.emit(e.event, e, parser); + + if (e.comment !== comment.raw) { + updateCommentNode(comment, e.comment); + } + } + } + + return true; +}; + +// TODO: docs +Visitor.prototype.visitNode = function(node, parser, filename) { + var i; + var l; + + var e = this.makeSymbolFoundEvent(node, parser, filename); + + if (!node.nodeId) { + Object.defineProperty(node, 'nodeId', { + value: parser.getUniqueId() + }); + } + + if (this._nodeVisitors && this._nodeVisitors.length) { + for (i = 0, l = this._nodeVisitors.length; i < l; i++) { + this._nodeVisitors[i].visitNode(node, e, parser, filename); + if (e.stopPropagation) { + break; + } + } + } + + if (!e.preventDefault && e.comment && isValidJsdoc(e.comment)) { + parser.emit(e.event, e, parser); + } + + // add the node to the parser's lookup table + parser.addDocletRef(e); + + for (i = 0, l = e.finishers.length; i < l; i++) { + e.finishers[i].call(parser, e); + } + + return true; +}; + +// TODO: docs +// TODO: note that it's essential to call this function before you try to resolve names! +function trackVars(parser, node, e) { + var enclosingScopeId = node.enclosingScope ? node.enclosingScope.nodeId : + jsdoc.name.LONGNAMES.GLOBAL; + var doclet = parser.refs[enclosingScopeId]; + + if (doclet) { + doclet.meta.vars = doclet.meta.vars || {}; + doclet.meta.vars[e.code.name] = null; + e.finishers.push( makeVarsFinisher(doclet) ); + } +} + +// TODO: docs +Visitor.prototype.makeSymbolFoundEvent = function(node, parser, filename) { + var logger = jsdoc.util.logger; + + var e; + var basename; + var i; + var l; + var parent; + + var extras = { + code: jsdoc.src.astnode.getInfo(node) + }; + + switch (node.type) { + // like: i = 0; + case Syntax.AssignmentExpression: + e = new SymbolFound(node, filename, extras); + + trackVars(parser, node, e); + + basename = parser.getBasename(e.code.name); + if (basename !== 'this') { + e.code.funcscope = parser.resolveVar(node, basename); + } + + break; + + // like: function foo() {} + case Syntax.FunctionDeclaration: + // falls through + + // like: var foo = function() {}; + case Syntax.FunctionExpression: + e = new SymbolFound(node, filename, extras); + + trackVars(parser, node, e); + + basename = parser.getBasename(e.code.name); + e.code.funcscope = parser.resolveVar(node, basename); + + break; + + // like "bar" in: function foo(/** @type {string} */ bar) {} + // or "module" in: define("MyModule", function(/** @exports MyModule */ module) {} + // This is an extremely common type of node; we only care about function parameters with + // inline comments. No need to fire an event unless the node is already commented. + case Syntax.Identifier: + parent = node.parent; + if ( node.leadingComments && parent && jsdoc.src.astnode.isFunction(parent) ) { + extras.finishers = [makeInlineParamsFinisher(parser)]; + e = new SymbolFound(node, filename, extras); + + trackVars(parser, node, e); + } + + break; + + // like "obj.prop" in: /** @typedef {string} */ obj.prop; + // Closure Compiler uses this pattern extensively for enums. + // No need to fire an event unless the node is already commented. + case Syntax.MemberExpression: + if (node.leadingComments) { + e = new SymbolFound(node, filename, extras); + } + + break; + + // like the object literal in: function Foo = Class.create(/** @lends Foo */ {}); + case Syntax.ObjectExpression: + e = new SymbolFound(node, filename, extras); + + break; + + // like "bar: true" in: var foo = { bar: true }; + // like "get bar() {}" in: var foo = { get bar() {} }; + case Syntax.Property: + if ( node.kind !== ('get' || 'set') ) { + extras.finishers = [parser.resolveEnum]; + } + + e = new SymbolFound(node, filename, extras); + + break; + + // like: var i = 0; + case Syntax.VariableDeclarator: + e = new SymbolFound(node, filename, extras); + + trackVars(parser, node, e); + + basename = parser.getBasename(e.code.name); + + break; + + // for now, log a warning for all ES6 nodes, since we don't do anything useful with them + case Syntax.ArrowFunctionExpression: + case Syntax.ClassBody: + case Syntax.ClassDeclaration: + case Syntax.ClassExpression: + case Syntax.ExportBatchSpecifier: + case Syntax.ExportDeclaration: + case Syntax.ExportSpecifier: + case Syntax.ImportDeclaration: + case Syntax.ImportSpecifier: + case Syntax.MethodDefinition: + case Syntax.ModuleDeclaration: + case Syntax.SpreadElement: + case Syntax.TaggedTemplateExpression: + case Syntax.TemplateElement: + case Syntax.TemplateLiteral: + logger.warn('JSDoc does not currently handle %s nodes. Source file: %s, line %s', + node.type, filename, (node.loc && node.loc.start) ? node.loc.start.line : '??'); + + break; + + default: + // ignore + } + + if (!e) { + e = { + finishers: [] + }; + } + + return e; +}; diff --git a/third_party/jsdoc/lib/jsdoc/src/walker.js b/third_party/jsdoc/lib/jsdoc/src/walker.js new file mode 100644 index 0000000000..2407dc89ae --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/src/walker.js @@ -0,0 +1,536 @@ +/** + * Traversal utilities for ASTs that are compatible with the Mozilla Parser API. Adapted from + * [Acorn](http://marijnhaverbeke.nl/acorn/). + * + * @module jsdoc/src/walker + * @license MIT + */ +'use strict'; + +var astnode = require('jsdoc/src/astnode'); +var doclet = require('jsdoc/doclet'); +var Syntax = require('jsdoc/src/syntax').Syntax; + +/** + * Check whether an AST node creates a new scope. + * + * @private + * @param {Object} node - The AST node to check. + * @return {Boolean} Set to `true` if the node creates a new scope, or `false` in all other cases. + */ +function isScopeNode(node) { + // TODO: handle blocks with "let" declarations + return node && typeof node === 'object' && (node.type === Syntax.CatchClause || + node.type === Syntax.FunctionDeclaration || node.type === Syntax.FunctionExpression); +} + +// TODO: docs +function getCurrentScope(scopes) { + return scopes[scopes.length - 1] || null; +} + +// TODO: docs +function moveComments(source, target) { + if (source.leadingComments) { + target.leadingComments = source.leadingComments.slice(0); + source.leadingComments = []; + } +} + +function leafNode(node, parent, state, cb) {} + +// TODO: docs +var walkers = exports.walkers = {}; + +walkers[Syntax.ArrayExpression] = function arrayExpression(node, parent, state, cb) { + for (var i = 0, l = node.elements.length; i < l; i++) { + var e = node.elements[i]; + if (e) { + cb(e, node, state); + } + } +}; + +// TODO: verify correctness +walkers[Syntax.ArrayPattern] = function arrayPattern(node, parent, state, cb) { + for (var i = 0, l = node.elements.length; i < l; i++) { + var e = node.elements[i]; + // must be an identifier or an expression + if (e && e.type !== Syntax.Identifier) { + cb(e, node, state); + } + } +}; + +walkers[Syntax.ArrowFunctionExpression] = + function arrowFunctionExpression(node, parent, state, cb) { + var i; + var l; + + // used for function declarations, so we include it here + if (node.id) { + cb(node.id, node, state); + } + + for (i = 0, l = node.params.length; i < l; i++) { + cb(node.params[i], node, state); + } + + for (i = 0, l = node.defaults.length; i < l; i++) { + cb(node.defaults[i], node, state); + } + + cb(node.body, node, state); + + if (node.rest) { + cb(node.rest, node, state); + } +}; + +walkers[Syntax.AssignmentExpression] = function assignmentExpression(node, parent, state, cb) { + cb(node.left, node, state); + cb(node.right, node, state); +}; + +walkers[Syntax.BinaryExpression] = function binaryExpression(node, parent, state, cb) { + cb(node.left, node, state); + cb(node.right, node, state); +}; + +walkers[Syntax.BlockStatement] = function blockStatement(node, parent, state, cb) { + for (var i = 0, l = node.body.length; i < l; i++) { + cb(node.body[i], node, state); + } +}; + +walkers[Syntax.BreakStatement] = leafNode; + +walkers[Syntax.CallExpression] = function callExpression(node, parent, state, cb) { + var i; + var l; + + cb(node.callee, node, state); + if (node.arguments) { + for (i = 0, l = node.arguments.length; i < l; i++) { + cb(node.arguments[i], node, state); + } + } +}; + +walkers[Syntax.CatchClause] = leafNode; + +walkers[Syntax.ClassBody] = walkers[Syntax.BlockStatement]; + +walkers[Syntax.ClassDeclaration] = function classDeclaration(node, parent, state, cb) { + if (node.id) { + cb(node.id, node, state); + } + + if (node.superClass) { + cb(node.superClass, node, state); + } + + if (node.body) { + cb(node.body, node, state); + } +}; + +walkers[Syntax.ClassExpression] = walkers[Syntax.ClassDeclaration]; + +// TODO: verify correctness +walkers[Syntax.ComprehensionBlock] = walkers[Syntax.AssignmentExpression]; + +// TODO: verify correctness +walkers[Syntax.ComprehensionExpression] = + function comprehensionExpression(node, parent, state, cb) { + cb(node.body, node, state); + + if (node.filter) { + cb(node.filter, node, state); + } + + for (var i = 0, l = node.blocks.length; i < l; i++) { + cb(node.blocks[i], node, state); + } +}; + +walkers[Syntax.ConditionalExpression] = function conditionalExpression(node, parent, state, cb) { + cb(node.test, node, state); + cb(node.consequent, node, state); + cb(node.alternate, node, state); +}; + +walkers[Syntax.ContinueStatement] = leafNode; + +walkers[Syntax.DebuggerStatement] = leafNode; + +walkers[Syntax.DoWhileStatement] = function doWhileStatement(node, parent, state, cb) { + cb(node.test, node, state); + cb(node.body, node, state); +}; + +walkers[Syntax.EmptyStatement] = leafNode; + +walkers[Syntax.ExportBatchSpecifier] = leafNode; + +walkers[Syntax.ExportDeclaration] = function exportDeclaration(node, parent, state, cb) { + var i; + var l; + + if (node.declaration) { + for (i = 0, l = node.declaration.length; i < l; i++) { + cb(node.declaration[i], node, state); + } + } + + if (node.specifiers) { + for (i = 0, l = node.specifiers.length; i < l; i++) { + cb(node.specifiers[i], node, state); + } + } + + if (node.source) { + cb(node.source, node, state); + } +}; + +walkers[Syntax.ExportSpecifier] = function exportSpecifier(node, parent, state, cb) { + if (node.id) { + cb(node.id, node, state); + } + + if (node.name) { + cb(node.name, node, state); + } +}; + +walkers[Syntax.ExpressionStatement] = function expressionStatement(node, parent, state, cb) { + cb(node.expression, node, state); +}; + +walkers[Syntax.ForInStatement] = function forInStatement(node, parent, state, cb) { + cb(node.left, node, state); + cb(node.right, node, state); + cb(node.body, node, state); +}; + +walkers[Syntax.ForOfStatement] = walkers[Syntax.ForInStatement]; + +walkers[Syntax.ForStatement] = function forStatement(node, parent, state, cb) { + if (node.init) { + cb(node.init, node, state); + } + + if (node.test) { + cb(node.test, node, state); + } + + if (node.update) { + cb(node.update, node, state); + } + + cb(node.body, node, state); +}; + +walkers[Syntax.FunctionDeclaration] = walkers[Syntax.ArrowFunctionExpression]; + +walkers[Syntax.FunctionExpression] = walkers[Syntax.ArrowFunctionExpression]; + +walkers[Syntax.Identifier] = leafNode; + +walkers[Syntax.IfStatement] = function ifStatement(node, parent, state, cb) { + cb(node.test, node, state); + cb(node.consequent, node, state); + if (node.alternate) { + cb(node.alternate, node, state); + } +}; + +walkers[Syntax.ImportDeclaration] = function importDeclaration(node, parent, state, cb) { + var i; + var l; + + if (node.specifiers) { + for (i = 0, l = node.specifiers.length; i < l; i++) { + cb(node.specifiers[i], node, state); + } + } + + if (node.source) { + cb(node.source, node, state); + } +}; + +walkers[Syntax.ImportSpecifier] = walkers[Syntax.ExportSpecifier]; + +walkers[Syntax.LabeledStatement] = function labeledStatement(node, parent, state, cb) { + cb(node.body, node, state); +}; + +// TODO: add scope info?? +walkers[Syntax.LetStatement] = function letStatement(node, parent, state, cb) { + for (var i = 0, l = node.head.length; i < l; i++) { + var head = node.head[i]; + cb(head.id, node, state); + if (head.init) { + cb(head.init, node, state); + } + } + + cb(node.body, node, state); +}; + +walkers[Syntax.Literal] = leafNode; + +walkers[Syntax.LogicalExpression] = walkers[Syntax.BinaryExpression]; + +walkers[Syntax.MemberExpression] = function memberExpression(node, parent, state, cb) { + cb(node.object, node, state); + if (node.property) { + cb(node.property, node, state); + } +}; + +walkers[Syntax.MethodDefinition] = function methodDefinition(node, parent, state, cb) { + if (node.key) { + cb(node.key, node, state); + } + + if (node.value) { + cb(node.value, node, state); + } +}; + +walkers[Syntax.ModuleDeclaration] = function moduleDeclaration(node, parent, state, cb) { + if (node.id) { + cb(node.id, node, state); + } + + if (node.source) { + cb(node.source, node, state); + } + + if (node.body) { + cb(node.body, node, state); + } +}; + +walkers[Syntax.NewExpression] = walkers[Syntax.CallExpression]; + +walkers[Syntax.ObjectExpression] = function objectExpression(node, parent, state, cb) { + for (var i = 0, l = node.properties.length; i < l; i++) { + cb(node.properties[i], node, state); + } +}; + +walkers[Syntax.ObjectPattern] = walkers[Syntax.ObjectExpression]; + +walkers[Syntax.Program] = walkers[Syntax.BlockStatement]; + +walkers[Syntax.Property] = function property(node, parent, state, cb) { + // move leading comments from key to property node + moveComments(node.key, node); + + cb(node.value, node, state); +}; + +walkers[Syntax.ReturnStatement] = function returnStatement(node, parent, state, cb) { + if (node.argument) { + cb(node.argument, node, state); + } +}; + +walkers[Syntax.SequenceExpression] = function sequenceExpression(node, parent, state, cb) { + for (var i = 0, l = node.expressions.length; i < l; i++) { + cb(node.expressions[i], node, state); + } +}; + +walkers[Syntax.SpreadElement] = function spreadElement(node, parent, state, cb) { + if (node.argument) { + cb(node.argument, node, state); + } +}; + +walkers[Syntax.SwitchCase] = function switchCase(node, parent, state, cb) { + if (node.test) { + cb(node.test, node, state); + } + + for (var i = 0, l = node.consequent.length; i < l; i++) { + cb(node.consequent[i], node, state); + } +}; + +walkers[Syntax.SwitchStatement] = function switchStatement(node, parent, state, cb) { + cb(node.discriminant, node, state); + + for (var i = 0, l = node.cases.length; i < l; i++) { + cb(node.cases[i], node, state); + } +}; + +walkers[Syntax.TaggedTemplateExpression] = + function taggedTemplateExpression(node, parent, state, cb) { + if (node.tag) { + cb(node.tag, node, state); + } + if (node.quasi) { + cb(node.quasi, node, state); + } +}; + +walkers[Syntax.TemplateElement] = leafNode; + +walkers[Syntax.TemplateLiteral] = function templateLiteral(node, parent, state, cb) { + var i; + var l; + + if (node.quasis && node.quasis.length) { + for (i = 0, l = node.quasis.length; i < l; i++) { + cb(node.quasis[i], node, state); + } + } + + if (node.expressions && node.expressions.length) { + for (i = 0, l = node.expressions.length; i < l; i++) { + cb(node.expressions[i], node, state); + } + } +}; + +walkers[Syntax.ThisExpression] = leafNode; + +walkers[Syntax.ThrowStatement] = function throwStatement(node, parent, state, cb) { + cb(node.argument, node, state); +}; + +walkers[Syntax.TryStatement] = function tryStatement(node, parent, state, cb) { + var i; + var l; + + cb(node.block, node, state); + + // handle Esprima ASTs, which deviate from the spec a bit + if ( node.handlers && Array.isArray(node.handlers) && node.handlers[0] ) { + cb(node.handlers[0].body, node, state); + } + else if (node.handler) { + cb(node.handler.body, node, state); + } + + if (node.guardedHandlers) { + for (i = 0, l = node.guardedHandlers.length; i < l; i++) { + cb(node.guardedHandlers[i].body, node, state); + } + } + + if (node.finalizer) { + cb(node.finalizer, node, state); + } +}; + +walkers[Syntax.UnaryExpression] = function unaryExpression(node, parent, state, cb) { + cb(node.argument, node, state); +}; + +walkers[Syntax.UpdateExpression] = walkers[Syntax.UnaryExpression]; + +walkers[Syntax.VariableDeclaration] = function variableDeclaration(node, parent, state, cb) { + // move leading comments to first declarator + moveComments(node, node.declarations[0]); + + for (var i = 0, l = node.declarations.length; i < l; i++) { + cb(node.declarations[i], node, state); + } +}; + +walkers[Syntax.VariableDeclarator] = function variableDeclarator(node, parent, state, cb) { + cb(node.id, node, state); + + if (node.init) { + cb(node.init, node, state); + } +}; + +walkers[Syntax.WhileStatement] = walkers[Syntax.DoWhileStatement]; + +walkers[Syntax.WithStatement] = function withStatement(node, parent, state, cb) { + cb(node.object, node, state); + cb(node.body, node, state); +}; + +walkers[Syntax.YieldExpression] = function(node, parent, state, cb) { + if (node.argument) { + cb(node.argument, node, state); + } +}; + +/** + * Create a walker that can traverse an AST that is consistent with the Mozilla Parser API. + * + * @todo docs + * @memberof module:jsdoc/src/walker + */ +var Walker = exports.Walker = function(walkerFuncs) { + this._walkers = walkerFuncs || walkers; +}; + +// TODO: docs +Walker.prototype._recurse = function(filename, ast) { + var self = this; + var state = { + filename: filename, + nodes: [], + scopes: [] + }; + + function cb(node, parent, state) { + var currentScope; + + var isScope = astnode.isScope(node); + + // for efficiency, if the node has a `parent` property, assume that we've already + // added the required properties + if (typeof node.parent !== 'undefined') { + astnode.addNodeProperties(node); + } + + node.parent = parent || null; + + currentScope = getCurrentScope(state.scopes); + if (currentScope) { + node.enclosingScope = currentScope; + } + + if (isScope) { + state.scopes.push(node); + } + state.nodes.push(node); + + self._walkers[node.type](node, parent, state, cb); + + if (isScope) { + state.scopes.pop(); + } + } + + cb(ast, null, state); + + return state; +}; + +// TODO: docs +// TODO: allow visitor.visit to prevent traversal of child nodes +// TODO: skip the AST root node to be consistent with Rhino? +Walker.prototype.recurse = function(filename, ast, visitor) { + var state = this._recurse(filename, ast); + + if (visitor) { + for (var i = 0, l = state.nodes.length; i < l; i++) { + visitor.visit.call(visitor, state.nodes[i], filename); + } + } + + return ast; +}; diff --git a/third_party/jsdoc/lib/jsdoc/tag.js b/third_party/jsdoc/lib/jsdoc/tag.js new file mode 100644 index 0000000000..27abc51f61 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/tag.js @@ -0,0 +1,192 @@ +/*global env: true */ +/** + @overview + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ + +/** + Functionality related to JSDoc tags. + @module jsdoc/tag + @requires jsdoc/tag/dictionary + @requires jsdoc/tag/validator + @requires jsdoc/tag/type + */ +'use strict'; + +var jsdoc = { + tag: { + dictionary: require('jsdoc/tag/dictionary'), + validator: require('jsdoc/tag/validator'), + type: require('jsdoc/tag/type') + }, + util: { + logger: require('jsdoc/util/logger') + } +}; +var path = require('jsdoc/path'); +var util = require('util'); + +// Check whether the text is the same as a symbol name with leading or trailing whitespace. If so, +// the text cannot be trimmed. +function textIsUntrimmable(text, meta) { + return meta && meta.code && meta.code.name === text && text.match(/(?:^\s+)|(?:\s+$)/); +} + +function trim(text, opts, meta) { + var indentMatcher; + var match; + + opts = opts || {}; + text = text || ''; + + if ( textIsUntrimmable(text, meta) ) { + text = util.format('"%s"', text); + } + else if (opts.keepsWhitespace) { + text = text.replace(/^[\n\r\f]+|[\n\r\f]+$/g, ''); + if (opts.removesIndent) { + match = text.match(/^([ \t]+)/); + if (match && match[1]) { + indentMatcher = new RegExp('^' + match[1], 'gm'); + text = text.replace(indentMatcher, ''); + } + } + } + else { + text = text.replace(/^\s+|\s+$/g, ''); + } + + return text; +} + +function addHiddenProperty(obj, propName, propValue) { + Object.defineProperty(obj, propName, { + value: propValue, + writable: true, + enumerable: !!global.env.opts.debug, + configurable: true + }); +} + +function processTagText(tag, tagDef) { + var tagType; + + if (tagDef.onTagText) { + tag.text = tagDef.onTagText(tag.text); + } + + if (tagDef.canHaveType || tagDef.canHaveName) { + /** The value property represents the result of parsing the tag text. */ + tag.value = {}; + + tagType = jsdoc.tag.type.parse(tag.text, tagDef.canHaveName, tagDef.canHaveType); + + // It is possible for a tag to *not* have a type but still have + // optional or defaultvalue, e.g. '@param [foo]'. + // Although tagType.type.length == 0 we should still copy the other properties. + if (tagType.type) { + if (tagType.type.length) { + tag.value.type = { + names: tagType.type + }; + addHiddenProperty(tag.value.type, 'parsedType', tagType.parsedType); + } + + ['optional', 'nullable', 'variable', 'defaultvalue'].forEach(function(prop) { + if (typeof tagType[prop] !== 'undefined') { + tag.value[prop] = tagType[prop]; + } + }); + } + + if (tagType.text && tagType.text.length) { + tag.value.description = tagType.text; + } + + if (tagDef.canHaveName) { + // note the dash is a special case: as a param name it means "no name" + if (tagType.name && tagType.name !== '-') { tag.value.name = tagType.name; } + } + } + else { + tag.value = tag.text; + } +} + +/** + * Replace the existing tag dictionary with a new tag dictionary. + * + * Used for testing only. Do not call this method directly. Instead, call + * {@link module:jsdoc/doclet._replaceDictionary}, which also updates this module's tag dictionary. + * + * @private + * @param {module:jsdoc/tag/dictionary.Dictionary} dict - The new tag dictionary. + */ +exports._replaceDictionary = function _replaceDictionary(dict) { + jsdoc.tag.dictionary = dict; +}; + +/** + Constructs a new tag object. Calls the tag validator. + @class + @classdesc Represents a single doclet tag. + @param {string} tagTitle + @param {string=} tagBody + @param {object=} meta + */ +var Tag = exports.Tag = function(tagTitle, tagBody, meta) { + var tagDef; + var trimOpts; + + meta = meta || {}; + + this.originalTitle = trim(tagTitle); + + /** The title of the tag (for example, `title` in `@title text`). */ + this.title = jsdoc.tag.dictionary.normalise(this.originalTitle); + + tagDef = jsdoc.tag.dictionary.lookUp(this.title); + trimOpts = { + keepsWhitespace: tagDef.keepsWhitespace, + removesIndent: tagDef.removesIndent + }; + + /** + * The text following the tag (for example, `text` in `@title text`). + * + * Whitespace is trimmed from the tag text as follows: + * + * + If the tag's `keepsWhitespace` option is falsy, all leading and trailing whitespace are + * removed. + * + If the tag's `keepsWhitespace` option is set to `true`, leading and trailing whitespace are + * not trimmed, unless the `removesIndent` option is also enabled. + * + If the tag's `removesIndent` option is set to `true`, any indentation that is shared by + * every line in the string is removed. This option is ignored unless `keepsWhitespace` is set + * to `true`. + * + * **Note**: If the tag text is the name of a symbol, and the symbol's name includes leading or + * trailing whitespace (for example, the property names in `{ ' ': true, ' foo ': false }`), + * the tag text is not trimmed. Instead, the tag text is wrapped in double quotes to prevent the + * whitespace from being trimmed. + */ + this.text = trim(tagBody, trimOpts, meta); + + if (this.text) { + try { + processTagText(this, tagDef); + } + catch (e) { + // probably a type-parsing error + jsdoc.util.logger.error( + 'Unable to create a Tag object%s with title "%s" and body "%s": %s', + meta.filename ? ( ' for source file ' + path.join(meta.path, meta.filename) ) : '', + tagTitle, + tagBody, + e.message + ); + } + } + + jsdoc.tag.validator.validate(this, tagDef, meta); +}; diff --git a/third_party/jsdoc/lib/jsdoc/tag/dictionary.js b/third_party/jsdoc/lib/jsdoc/tag/dictionary.js new file mode 100644 index 0000000000..38b737982b --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/tag/dictionary.js @@ -0,0 +1,99 @@ +/** @module jsdoc/tag/dictionary */ +'use strict'; + +var definitions = require('jsdoc/tag/dictionary/definitions'); + +var hasOwnProp = Object.prototype.hasOwnProperty; + +var dictionary; + +/** @private */ +function TagDefinition(dictionary, title, etc) { + var self = this; + etc = etc || {}; + + this.title = dictionary.normalise(title); + + Object.defineProperty(this, '_dictionary', { + value: dictionary + }); + + Object.keys(etc).forEach(function(p) { + self[p] = etc[p]; + }); +} + +/** @private */ +TagDefinition.prototype.synonym = function(synonymName) { + this._dictionary.defineSynonym(this.title, synonymName); + return this; // chainable +}; + +/** + * @class + * @alias module:jsdoc/tag/dictionary.Dictionary + */ +function Dictionary() { + this._tags = {}; + this._tagSynonyms = {}; + this._namespaces = []; +} + +/** @function */ +Dictionary.prototype.defineTag = function defineTag(title, opts) { + var tagDef = new TagDefinition(this, title, opts); + this._tags[tagDef.title] = tagDef; + + if (opts && opts.isNamespace) { + this._namespaces.push(tagDef.title); + } + + return this._tags[tagDef.title]; +}; + +/** @function */ +Dictionary.prototype.defineSynonym = function defineSynonym(title, synonym) { + this._tagSynonyms[synonym.toLowerCase()] = this.normalise(title); +}; + +/** @function */ +Dictionary.prototype.lookUp = function lookUp(title) { + title = this.normalise(title); + + if ( hasOwnProp.call(this._tags, title) ) { + return this._tags[title]; + } + + return false; +}; + +/** @function */ +Dictionary.prototype.isNamespace = function isNamespace(kind) { + if (kind) { + kind = this.normalise(kind); + if (this._namespaces.indexOf(kind) !== -1) { + return true; + } + } + + return false; +}; + +/** @function */ +Dictionary.prototype.normalise = function normalise(title) { + var canonicalName = title.toLowerCase(); + + if ( hasOwnProp.call(this._tagSynonyms, canonicalName) ) { + return this._tagSynonyms[canonicalName]; + } + + return canonicalName; +}; + +// initialize the default dictionary +dictionary = new Dictionary(); +dictionary.Dictionary = Dictionary; +definitions.defineTags(dictionary); + +/** @type {module:jsdoc/tag/dictionary.Dictionary} */ +module.exports = dictionary; diff --git a/third_party/jsdoc/lib/jsdoc/tag/dictionary/definitions.js b/third_party/jsdoc/lib/jsdoc/tag/dictionary/definitions.js new file mode 100644 index 0000000000..18b9685ea9 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/tag/dictionary/definitions.js @@ -0,0 +1,891 @@ +/** + Define tags that are known in JSDoc. + @module jsdoc/tag/dictionary/definitions + + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ +'use strict'; + +var hasOwnProp = Object.prototype.hasOwnProperty; +var jsdoc = { + name: require('jsdoc/name'), + src: { + astnode: require('jsdoc/src/astnode') + }, + tag: { + type: require('jsdoc/tag/type') + }, + util: { + doop: require('jsdoc/util/doop'), + logger: require('jsdoc/util/logger') + } +}; +var path = require('jsdoc/path'); +var Syntax = require('jsdoc/src/syntax').Syntax; + +var DEFINITIONS = { + closure: 'closureTags', + jsdoc: 'jsdocTags' +}; +var NAMESPACES = jsdoc.name.NAMESPACES; + +// Clone a tag definition, excluding synonyms. +function cloneTagDef(tagDef) { + var newTagDef = jsdoc.util.doop(tagDef); + delete newTagDef.synonyms; + + return newTagDef; +} + +function getSourcePaths() { + var sourcePaths = global.env.sourceFiles.slice(0) || []; + + if (global.env.opts._) { + global.env.opts._.forEach(function(sourcePath) { + var resolved = path.resolve(global.env.pwd, sourcePath); + if (sourcePaths.indexOf(resolved) === -1) { + sourcePaths.push(resolved); + } + }); + } + + return sourcePaths; +} + +function filepathMinusPrefix(filepath) { + var sourcePaths = getSourcePaths(); + var commonPrefix = path.commonPrefix(sourcePaths); + var result = ''; + + if (filepath) { + filepath = path.normalize(filepath); + // always use forward slashes in the result + result = (filepath + path.sep).replace(commonPrefix, '') + .replace(/\\/g, '/'); + } + + if (result.length > 0 && result[result.length - 1] !== '/') { + result += '/'; + } + + return result; +} + +/** @private */ +function setDocletKindToTitle(doclet, tag) { + doclet.addTag( 'kind', tag.title ); +} + +function setDocletScopeToTitle(doclet, tag) { + try { + doclet.setScope(tag.title); + } + catch(e) { + jsdoc.util.logger.error(e.message); + } +} + +function setDocletNameToValue(doclet, tag) { + if (tag.value && tag.value.description) { // as in a long tag + doclet.addTag( 'name', tag.value.description); + } + else if (tag.text) { // or a short tag + doclet.addTag('name', tag.text); + } +} + +function setDocletNameToValueName(doclet, tag) { + if (tag.value && tag.value.name) { + doclet.addTag('name', tag.value.name); + } +} + +function setDocletDescriptionToValue(doclet, tag) { + if (tag.value) { + doclet.addTag( 'description', tag.value ); + } +} + +function setDocletTypeToValueType(doclet, tag) { + if (tag.value && tag.value.type) { + // Add the type names and other type properties (such as `optional`). + // Don't overwrite existing properties. + Object.keys(tag.value).forEach(function(prop) { + if ( !hasOwnProp.call(doclet, prop) ) { + doclet[prop] = tag.value[prop]; + } + }); + } +} + +function setNameToFile(doclet, tag) { + var name; + + if (doclet.meta.filename) { + name = filepathMinusPrefix(doclet.meta.path) + doclet.meta.filename; + doclet.addTag('name', name); + } +} + +function setDocletMemberof(doclet, tag) { + if (tag.value && tag.value !== '') { + doclet.setMemberof(tag.value); + } +} + +function applyNamespace(docletOrNs, tag) { + if (typeof docletOrNs === 'string') { // ns + tag.value = jsdoc.name.applyNamespace(tag.value, docletOrNs); + } + else { // doclet + if (!docletOrNs.name) { + return; // error? + } + + docletOrNs.longname = jsdoc.name.applyNamespace(docletOrNs.name, tag.title); + } +} + +function setDocletNameToFilename(doclet, tag) { + var name = ''; + + if (doclet.meta.path) { + name = filepathMinusPrefix(doclet.meta.path); + } + name += doclet.meta.filename.replace(/\.js$/i, ''); + + doclet.name = name; +} + +function parseTypeText(text) { + var tagType = jsdoc.tag.type.parse(text, false, true); + return tagType.typeExpression || text; +} + +function parseBorrows(doclet, tag) { + var m = /^(\S+)(?:\s+as\s+(\S+))?$/.exec(tag.text); + if (m) { + if (m[1] && m[2]) { + return { target: m[1], source: m[2] }; + } + else if (m[1]) { + return { target: m[1] }; + } + } else { + return {}; + } +} + +function stripModuleNamespace(name) { + return name.replace(/^module\:/, ''); +} + +function firstWordOf(string) { + var m = /^(\S+)/.exec(string); + if (m) { return m[1]; } + else { return ''; } +} + + +// Core JSDoc tags that are shared with other tag dictionaries. +var baseTags = exports.baseTags = { + abstract: { + mustNotHaveValue: true, + onTagged: function(doclet, tag) { + // we call this `virtual` because `abstract` is a reserved word + doclet.virtual = true; + }, + synonyms: ['virtual'] + }, + access: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + // only valid values are private and protected, public is default + if ( /^(private|protected)$/i.test(tag.value) ) { + doclet.access = tag.value.toLowerCase(); + } + else { + delete doclet.access; + } + } + }, + alias: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + doclet.alias = tag.value; + } + }, + // Special separator tag indicating that multiple doclets should be generated for the same + // comment. Used internally (and by some JSDoc users, although it's not officially supported). + // In the following example, the parser will replace `//**` with an `@also` tag: + // /** + // * Foo. + // *//** + // * Foo with a param. + // * @param {string} bar + // */ + // function foo(bar) {} + also: { + onTagged: function(doclet, tag) { + // let the parser handle it; we define the tag here to avoid "not a known tag" errors + } + }, + augments: { + mustHaveValue: true, + // Allow augments value to be specified as a normal type, e.g. {Type} + onTagText: parseTypeText, + onTagged: function(doclet, tag) { + doclet.augment( firstWordOf(tag.value) ); + }, + synonyms: ['extends'] + }, + author: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + doclet.author = doclet.author || []; + doclet.author.push(tag.value); + } + }, + // this symbol has a member that should use the same docs as another symbol + borrows: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + var borrows = parseBorrows(doclet, tag); + doclet.borrow(borrows.target, borrows.source); + } + }, + class: { + onTagged: function(doclet, tag) { + doclet.addTag('kind', 'class'); + + // handle special case where both @class and @constructor tags exist in same doclet + if (tag.originalTitle === 'class') { + // multiple words after @class? + var looksLikeDesc = (tag.value || '').match(/\S+\s+\S+/); + if ( looksLikeDesc || /@construct(s|or)\b/i.test(doclet.comment) ) { + // treat the @class tag as a @classdesc tag instead + doclet.classdesc = tag.value; + return; + } + } + + setDocletNameToValue(doclet, tag); + }, + synonyms: ['constructor'] + }, + classdesc: { + onTagged: function(doclet, tag) { + doclet.classdesc = tag.value; + } + }, + constant: { + canHaveType: true, + canHaveName: true, + onTagged: function(doclet, tag) { + setDocletKindToTitle(doclet, tag); + setDocletNameToValueName(doclet, tag); + setDocletTypeToValueType(doclet, tag); + }, + synonyms: ['const'] + }, + constructs: { + onTagged: function(doclet, tag) { + var ownerClassName; + if (!tag.value) { + // this can be resolved later in the handlers + ownerClassName = '{@thisClass}'; + } + else { + ownerClassName = firstWordOf(tag.value); + } + doclet.addTag('alias', ownerClassName); + doclet.addTag('kind', 'class'); + } + }, + copyright: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + doclet.copyright = tag.value; + } + }, + default: { + onTagged: function(doclet, tag) { + var type; + var value; + + var nodeToString = jsdoc.src.astnode.nodeToString; + + if (tag.value) { + doclet.defaultvalue = tag.value; + } + else if (doclet.meta && doclet.meta.code && doclet.meta.code.value) { + type = doclet.meta.code.type; + value = doclet.meta.code.value; + + switch (type) { + case Syntax.ArrayExpression: + doclet.defaultvalue = nodeToString(doclet.meta.code.node); + doclet.defaultvaluetype = 'array'; + break; + + case Syntax.Literal: + doclet.defaultvalue = String(value); + break; + + case Syntax.ObjectExpression: + doclet.defaultvalue = nodeToString(doclet.meta.code.node); + doclet.defaultvaluetype = 'object'; + break; + + default: + // do nothing + break; + } + } + }, + synonyms: ['defaultvalue'] + }, + deprecated: { + // value is optional + onTagged: function(doclet, tag) { + doclet.deprecated = tag.value || true; + } + }, + description: { + mustHaveValue: true, + synonyms: ['desc'] + }, + enum: { + canHaveType: true, + onTagged: function(doclet, tag) { + doclet.kind = 'member'; + doclet.isEnum = true; + setDocletTypeToValueType(doclet, tag); + } + }, + event: { + isNamespace: true, + onTagged: function(doclet, tag) { + setDocletKindToTitle(doclet, tag); + setDocletNameToValue(doclet, tag); + } + }, + example: { + keepsWhitespace: true, + removesIndent: true, + mustHaveValue: true, + onTagged: function(doclet, tag) { + doclet.examples = doclet.examples || []; + doclet.examples.push(tag.value); + } + }, + exports: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + var modName = firstWordOf(tag.value); + + // in case the user wrote something like `/** @exports module:foo */`: + doclet.addTag( 'alias', stripModuleNamespace(modName) ); + doclet.addTag('kind', 'module'); + } + }, + external: { + canHaveType: true, + isNamespace: true, + onTagged: function(doclet, tag) { + setDocletKindToTitle(doclet, tag); + if (tag.value && tag.value.type) { + setDocletTypeToValueType(doclet, tag); + doclet.addTag('name', doclet.type.names[0]); + } + else { + setDocletNameToValue(doclet, tag); + } + }, + synonyms: ['host'] + }, + file: { + onTagged: function(doclet, tag) { + setNameToFile(doclet, tag); + setDocletKindToTitle(doclet, tag); + setDocletDescriptionToValue(doclet, tag); + + doclet.preserveName = true; + }, + synonyms: ['fileoverview', 'overview'] + }, + fires: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + doclet.fires = doclet.fires || []; + applyNamespace('event', tag); + doclet.fires.push(tag.value); + }, + synonyms: ['emits'] + }, + function: { + onTagged: function(doclet, tag) { + setDocletKindToTitle(doclet, tag); + setDocletNameToValue(doclet, tag); + }, + synonyms: ['func', 'method'] + }, + global: { + mustNotHaveValue: true, + onTagged: function(doclet, tag) { + doclet.scope = jsdoc.name.SCOPE.NAMES.GLOBAL; + delete doclet.memberof; + } + }, + ignore: { + mustNotHaveValue: true, + onTagged: function(doclet, tag) { + doclet.ignore = true; + } + }, + implements: { + mustHaveValue: true, + onTagText: parseTypeText, + onTagged: function(doclet, tag) { + doclet.implements = doclet.implements || []; + doclet.implements.push(tag.value); + } + }, + inner: { + onTagged: function(doclet, tag) { + setDocletScopeToTitle(doclet, tag); + } + }, + instance: { + onTagged: function(doclet, tag) { + setDocletScopeToTitle(doclet, tag); + } + }, + interface: { + mustNotHaveValue: true, + onTagged: function(doclet, tag) { + doclet.addTag('kind', 'interface'); + } + }, + kind: { + mustHaveValue: true + }, + lends: { + onTagged: function(doclet, tag) { + doclet.alias = tag.value || jsdoc.name.LONGNAMES.GLOBAL; + doclet.addTag('undocumented'); + } + }, + license: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + doclet.license = tag.value; + } + }, + listens: { + mustHaveValue: true, + onTagged: function (doclet, tag) { + doclet.listens = doclet.listens || []; + applyNamespace('event', tag); + doclet.listens.push(tag.value); + } + }, + member: { + canHaveType: true, + canHaveName: true, + onTagged: function(doclet, tag) { + setDocletKindToTitle(doclet, tag); + setDocletNameToValueName(doclet, tag); + setDocletTypeToValueType(doclet, tag); + }, + synonyms: ['var'] + }, + memberof: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + if (tag.originalTitle === 'memberof!') { + doclet.forceMemberof = true; + if (tag.value === jsdoc.name.LONGNAMES.GLOBAL) { + doclet.addTag('global'); + delete doclet.memberof; + } + } + setDocletMemberof(doclet, tag); + }, + synonyms: ['memberof!'] + }, + // this symbol mixes in all of the specified object's members + mixes: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + var source = firstWordOf(tag.value); + doclet.mix(source); + } + }, + mixin: { + onTagged: function(doclet, tag) { + setDocletKindToTitle(doclet, tag); + setDocletNameToValue(doclet, tag); + } + }, + module: { + canHaveType: true, + isNamespace: true, + onTagged: function(doclet, tag) { + setDocletKindToTitle(doclet, tag); + setDocletNameToValue(doclet, tag); + if (!doclet.name) { + setDocletNameToFilename(doclet, tag); + } + // in case the user wrote something like `/** @module module:foo */`: + doclet.name = stripModuleNamespace(doclet.name); + + setDocletTypeToValueType(doclet, tag); + } + }, + name: { + mustHaveValue: true + }, + namespace: { + canHaveType: true, + onTagged: function(doclet, tag) { + setDocletKindToTitle(doclet, tag); + setDocletNameToValue(doclet, tag); + setDocletTypeToValueType(doclet, tag); + } + }, + param: { + canHaveType: true, + canHaveName: true, + onTagged: function(doclet, tag) { + doclet.params = doclet.params || []; + doclet.params.push(tag.value || {}); + }, + synonyms: ['arg', 'argument'] + }, + private: { + mustNotHaveValue: true, + onTagged: function(doclet, tag) { + doclet.access = 'private'; + } + }, + property: { + mustHaveValue: true, + canHaveType: true, + canHaveName: true, + onTagged: function(doclet, tag) { + doclet.properties = doclet.properties || []; + doclet.properties.push(tag.value); + }, + synonyms: ['prop'] + }, + protected: { + mustNotHaveValue: true, + onTagged: function(doclet, tag) { + doclet.access = 'protected'; + } + }, + public: { + mustNotHaveValue: true, + onTagged: function(doclet, tag) { + delete doclet.access; // public is default + } + }, + readonly: { + mustNotHaveValue: true, + onTagged: function(doclet, tag) { + doclet.readonly = true; + } + }, + requires: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + var requiresName; + + // inline link tags are passed through as-is so that `@requires {@link foo}` works + if ( require('jsdoc/tag/inline').isInlineTag(tag.value, 'link\\S*') ) { + requiresName = tag.value; + } + // otherwise, assume it's a module + else { + requiresName = firstWordOf(tag.value); + if (requiresName.indexOf(NAMESPACES.MODULE) !== 0) { + requiresName = NAMESPACES.MODULE + requiresName; + } + } + + doclet.requires = doclet.requires || []; + doclet.requires.push(requiresName); + } + }, + returns: { + mustHaveValue: true, + canHaveType: true, + onTagged: function(doclet, tag) { + doclet.returns = doclet.returns || []; + doclet.returns.push(tag.value); + }, + synonyms: ['return'] + }, + see: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + doclet.see = doclet.see || []; + doclet.see.push(tag.value); + } + }, + since: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + doclet.since = tag.value; + } + }, + static: { + onTagged: function(doclet, tag) { + setDocletScopeToTitle(doclet, tag); + } + }, + summary: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + doclet.summary = tag.value; + } + }, + 'this': { + mustHaveValue: true, + onTagged: function(doclet, tag) { + doclet['this'] = firstWordOf(tag.value); + } + }, + todo: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + doclet.todo = doclet.todo || []; + doclet.todo.push(tag.value); + } + }, + throws: { + mustHaveValue: true, + canHaveType: true, + onTagged: function(doclet, tag) { + doclet.exceptions = doclet.exceptions || []; + doclet.exceptions.push(tag.value); + }, + synonyms: ['exception'] + }, + tutorial: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + doclet.tutorials = doclet.tutorials || []; + doclet.tutorials.push(tag.value); + } + }, + type: { + mustHaveValue: true, + mustNotHaveDescription: true, + canHaveType: true, + onTagText: function(text) { + var closeIdx; + var openIdx; + + var OPEN_BRACE = '{'; + var CLOSE_BRACE = '}'; + + // remove line breaks + text = text.replace(/[\f\n\r]/g, ''); + + // Text must be a type expression; for backwards compatibility, we add braces if they're + // missing. But do NOT add braces to things like `@type {string} some pointless text`. + openIdx = text.indexOf(OPEN_BRACE); + closeIdx = text.indexOf(CLOSE_BRACE); + + // a type expression is at least one character long + if ( openIdx !== 0 || closeIdx <= openIdx + 1) { + text = OPEN_BRACE + text + CLOSE_BRACE; + } + + return text; + }, + onTagged: function(doclet, tag) { + if (tag.value && tag.value.type) { + setDocletTypeToValueType(doclet, tag); + + // for backwards compatibility, we allow @type for functions to imply return type + if (doclet.kind === 'function') { + doclet.addTag('returns', tag.text); + } + } + } + }, + typedef: { + canHaveType: true, + canHaveName: true, + onTagged: function(doclet, tag) { + setDocletKindToTitle(doclet, tag); + + if (tag.value) { + setDocletNameToValueName(doclet, tag); + + // callbacks are always type {function} + if (tag.originalTitle === 'callback') { + doclet.type = { + names: [ + 'function' + ] + }; + } + else { + setDocletTypeToValueType(doclet, tag); + } + } + }, + synonyms: ['callback'] + }, + undocumented: { + mustNotHaveValue: true, + onTagged: function(doclet, tag) { + doclet.undocumented = true; + doclet.comment = ''; + } + }, + variation: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + doclet.variation = tag.value; + } + }, + version: { + mustHaveValue: true, + onTagged: function(doclet, tag) { + doclet.version = tag.value; + } + } +}; + +// Tag dictionary for JSDoc. +var jsdocTags = exports.jsdocTags = baseTags; + +// Tag dictionary for Google Closure Compiler. +var closureTags = exports.closureTags = { + const: cloneTagDef(baseTags.constant), + constructor: cloneTagDef(baseTags.class), + deprecated: cloneTagDef(baseTags.deprecated), + enum: cloneTagDef(baseTags.enum), + extends: cloneTagDef(baseTags.augments), + export: { + mustNotHaveValue: true, + onTagged: function(doclet, tag) { + doclet.visibility = 'export'; + } + }, + expose: { + onTagged: function(doclet, tag) { + doclet.visibility = 'expose'; + } + }, + final: cloneTagDef(baseTags.readonly), + implements: cloneTagDef(baseTags.implements), + interface: cloneTagDef(baseTags.interface), + lends: cloneTagDef(baseTags.lends), + license: cloneTagDef(baseTags.license), + namespace: { + // This is a different use of @namespace compared to standard JSDoc. + // The goal here is to treat this like a class and show it in the list + // of classes, but not show a signature for a non-existent constructor. + onTagged: function(doclet, tag) { + doclet.addTag('kind', 'class'); + setDocletNameToValue(doclet, tag); + doclet.noConstructor = true; + } + }, + param: cloneTagDef(baseTags.param), + private: { + canHaveType: true, + onTagged: function(doclet, tag) { + doclet.access = 'private'; + + if (tag.value && tag.value.type) { + setDocletTypeToValueType(doclet, tag); + } + } + }, + protected: { + canHaveType: true, + onTagged: function(doclet, tag) { + doclet.access = 'protected'; + + if (tag.value && tag.value.type) { + setDocletTypeToValueType(doclet, tag); + } + } + }, + return: cloneTagDef(baseTags.returns), + 'this': cloneTagDef(baseTags['this']), + throws: cloneTagDef(baseTags.throws), + type: (function() { + var tagDef = cloneTagDef(baseTags.type); + tagDef.mustNotHaveDescription = false; + return tagDef; + })(), + typedef: cloneTagDef(baseTags.typedef) +}; + +function addTagDefinitions(dictionary, tagDefs) { + Object.keys(tagDefs).forEach(function(tagName) { + var tagDef; + + tagDef = tagDefs[tagName]; + dictionary.defineTag(tagName, tagDef); + + if (tagDef.synonyms) { + tagDef.synonyms.forEach(function(synonym) { + dictionary.defineSynonym(tagName, synonym); + }); + } + }); +} + +/** + * Populate the given dictionary with the appropriate JSDoc tag definitions. + * + * If the `tagDefs` parameter is omitted, JSDoc uses its configuration settings to decide which + * tags to add to the dictionary. + * + * If the `tagDefs` parameter is included, JSDoc adds only the tag definitions from the `tagDefs` + * object. The configuration settings are ignored. + * + * @param {module:jsdoc/tag/dictionary} dictionary + * @param {Object} [tagDefinitions] - A dictionary whose values define the rules for a JSDoc tag. + */ +exports.defineTags = function(dictionary, tagDefinitions) { + var dictionaries; + + if (!tagDefinitions) { + dictionaries = global.env.conf.tags.dictionaries; + + if (!dictionaries) { + jsdoc.util.logger.error('The configuration setting "tags.dictionaries" is undefined. ' + + 'Unable to load tag definitions.'); + return; + } + else { + dictionaries = dictionaries.slice(0).reverse(); + } + + dictionaries.forEach(function(dictName) { + var tagDefs = exports[DEFINITIONS[dictName]]; + + if (!tagDefs) { + jsdoc.util.logger.error('The configuration setting "tags.dictionaries" contains ' + + 'the unknown dictionary name %s. Ignoring the dictionary.', dictName); + return; + } + + addTagDefinitions(dictionary, tagDefs); + }); + } + else { + addTagDefinitions(dictionary, tagDefinitions); + } +}; diff --git a/third_party/jsdoc/lib/jsdoc/tag/inline.js b/third_party/jsdoc/lib/jsdoc/tag/inline.js new file mode 100644 index 0000000000..e05c40f250 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/tag/inline.js @@ -0,0 +1,141 @@ +/** + * @module jsdoc/tag/inline + * + * @author Jeff Williams + * @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ +'use strict'; + +/** + * Information about an inline tag that was found within a string. + * + * @typedef {Object} InlineTagInfo + * @memberof module:jsdoc/tag/inline + * @property {?string} completeTag - The entire inline tag, including its enclosing braces. + * @property {?string} tag - The tag whose text was found. + * @property {?string} text - The tag text that was found. + */ + +/** + * Information about the results of replacing inline tags within a string. + * + * @typedef {Object} InlineTagResult + * @memberof module:jsdoc/tag/inline + * @property {Array.} tags - The inline tags that were found. + * @property {string} newString - The updated text string after extracting or replacing the inline + * tags. + */ + +/** + * Text-replacing function for strings that contain an inline tag. + * + * @callback InlineTagReplacer + * @memberof module:jsdoc/tag/inline + * @param {string} string - The complete string containing the inline tag. + * @param {module:jsdoc/tag/inline.InlineTagInfo} tagInfo - Information about the inline tag. + * @return {string} An updated version of the complete string. + */ + +/** + * Create a regexp that matches a specific inline tag, or all inline tags. + * + * @private + * @memberof module:jsdoc/tag/inline + * @param {?string} tagName - The inline tag that the regexp will match. May contain regexp + * characters. If omitted, matches any string. + * @param {?string} prefix - A prefix for the regexp. Defaults to an empty string. + * @param {?string} suffix - A suffix for the regexp. Defaults to an empty string. + * @returns {RegExp} A regular expression that matches the requested inline tag. + */ +function regExpFactory(tagName, prefix, suffix) { + tagName = tagName || '\\S+'; + prefix = prefix || ''; + suffix = suffix || ''; + + return new RegExp(prefix + '\\{@' + tagName + '\\s+((?:.|\n)+?)\\}' + suffix, 'gi'); +} + +/** + * Check whether a string is an inline tag. You can check for a specific inline tag or for any valid + * inline tag. + * + * @param {string} string - The string to check. + * @param {?string} tagName - The inline tag to match. May contain regexp characters. If this + * parameter is omitted, this method returns `true` for any valid inline tag. + * @returns {boolean} Set to `true` if the string is a valid inline tag or `false` in all other + * cases. + */ +exports.isInlineTag = function(string, tagName) { + return regExpFactory(tagName, '^', '$').test(string); +}; + +/** + * Replace all instances of multiple inline tags with other text. + * + * @param {string} string - The string in which to replace the inline tags. + * @param {Object} replacers - The functions that are used to replace text in the string. The keys + * must contain tag names (for example, `link`), and the values must contain functions with the + * type {@link module:jsdoc/tag/inline.InlineTagReplacer}. + * @return {module:jsdoc/tag/inline.InlineTagResult} The updated string, as well as information + * about the inline tags that were found. + */ +exports.replaceInlineTags = function(string, replacers) { + var tagInfo = []; + + function replaceMatch(replacer, tag, match, text) { + var matchedTag = { + completeTag: match, + tag: tag, + text: text + }; + tagInfo.push(matchedTag); + + return replacer(string, matchedTag); + } + + string = string || ''; + Object.keys(replacers).forEach(function(replacer) { + var tagRegExp = regExpFactory(replacer); + var matches; + // call the replacer once for each match + while ( (matches = tagRegExp.exec(string)) !== null ) { + string = replaceMatch(replacers[replacer], replacer, matches[0], matches[1]); + } + }); + + return { + tags: tagInfo, + newString: string.trim() + }; +}; + +/** + * Replace all instances of an inline tag with other text. + * + * @param {string} string - The string in which to replace the inline tag. + * @param {string} tag - The name of the inline tag to replace. + * @param {module:jsdoc/tag/inline.InlineTagReplacer} replacer - The function that is used to + * replace text in the string. + * @return {module:jsdoc/tag/inline.InlineTagResult} The updated string, as well as information + * about the inline tags that were found. + */ +exports.replaceInlineTag = function(string, tag, replacer) { + var replacers = {}; + replacers[tag] = replacer; + + return exports.replaceInlineTags(string, replacers); +}; + +/** + * Extract inline tags from a string, replacing them with an empty string. + * + * @param {string} string - The string from which to extract text. + * @param {?string} tag - The inline tag to extract. + * @return {module:jsdoc/tag/inline.InlineTagResult} The updated string, as well as information + * about the inline tags that were found. + */ +exports.extractInlineTag = function(string, tag) { + return exports.replaceInlineTag(string, tag, function(str, tagInfo) { + return str.replace(tagInfo.completeTag, ''); + }); +}; diff --git a/third_party/jsdoc/lib/jsdoc/tag/type.js b/third_party/jsdoc/lib/jsdoc/tag/type.js new file mode 100644 index 0000000000..802c289709 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/tag/type.js @@ -0,0 +1,308 @@ +/** + * @module jsdoc/tag/type + * + * @author Michael Mathews + * @author Jeff Williams + * @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ +'use strict'; + +var catharsis = require('catharsis'); +var jsdoc = { + name: require('jsdoc/name'), + tag: { + inline: require('jsdoc/tag/inline') + } +}; +var util = require('util'); + +/** + * Information about a type expression extracted from tag text. + * + * @typedef TypeExpressionInfo + * @memberof module:jsdoc/tag/type + * @property {string} expression - The type expression. + * @property {string} text - The updated tag text. + */ + +/** @private */ +function unescapeBraces(text) { + return text.replace(/\\\{/g, '{') + .replace(/\\\}/g, '}'); +} + +/** + * Extract a type expression from the tag text. + * + * @private + * @param {string} string - The tag text. + * @return {module:jsdoc/tag/type.TypeExpressionInfo} The type expression and updated tag text. + */ + function extractTypeExpression(string) { + var completeExpression; + var count = 0; + var position = 0; + var expression = ''; + var startIndex = string.search(/\{[^@]/); + var textStartIndex; + + if (startIndex !== -1) { + // advance to the first character in the type expression + position = textStartIndex = startIndex + 1; + count++; + + while (position < string.length) { + switch (string[position]) { + case '\\': + // backslash is an escape character, so skip the next character + position++; + break; + case '{': + count++; + break; + case '}': + count--; + break; + default: + // do nothing + } + + if (count === 0) { + completeExpression = string.slice(startIndex, position + 1); + expression = string.slice(textStartIndex, position).trim(); + break; + } + + position++; + } + } + + string = completeExpression ? string.replace(completeExpression, '') : string; + + return { + expression: unescapeBraces(expression), + newString: string.trim() + }; +} + +/** @private */ +function getTagInfo(tagValue, canHaveName, canHaveType) { + var name = ''; + var typeExpression = ''; + var text = tagValue; + var expressionAndText; + var nameAndDescription; + var typeOverride; + + if (canHaveType) { + expressionAndText = extractTypeExpression(text); + typeExpression = expressionAndText.expression; + text = expressionAndText.newString; + } + + if (canHaveName) { + nameAndDescription = jsdoc.name.splitName(text); + name = nameAndDescription.name; + text = nameAndDescription.description; + } + + // an inline @type tag, like {@type Foo}, overrides the type expression + if (canHaveType) { + typeOverride = jsdoc.tag.inline.extractInlineTag(text, 'type'); + if (typeOverride.tags && typeOverride.tags[0]) { + typeExpression = typeOverride.tags[0].text; + } + text = typeOverride.newString; + } + + return { + name: name, + typeExpression: typeExpression, + text: text + }; +} + +/** + * Information provided in a JSDoc tag. + * + * @typedef {Object} TagInfo + * @memberof module:jsdoc/tag/type + * @property {string} TagInfo.defaultvalue - The default value of the member. + * @property {string} TagInfo.name - The name of the member (for example, `myParamName`). + * @property {boolean} TagInfo.nullable - Indicates whether the member can be set to `null` or + * `undefined`. + * @property {boolean} TagInfo.optional - Indicates whether the member is optional. + * @property {string} TagInfo.text - Descriptive text for the member (for example, `The user's email + * address.`). + * @property {Array.} TagInfo.type - The type or types that the member can contain (for + * example, `string` or `MyNamespace.MyClass`). + * @property {string} TagInfo.typeExpression - The type expression that was parsed to identify the + * types. + * @property {boolean} TagInfo.variable - Indicates whether the number of members that are provided + * can vary (for example, in a function that accepts any number of parameters). + */ + +// TODO: move to module:jsdoc/name? +/** + * Extract JSDoc-style type information from the name specified in the tag info, including the + * member name; whether the member is optional; and the default value of the member. + * + * @private + * @param {module:jsdoc/tag/type.TagInfo} tagInfo - Information contained in the tag. + * @return {module:jsdoc/tag/type.TagInfo} Updated information from the tag. + */ +function parseName(tagInfo) { + // like '[foo]' or '[ foo ]' or '[foo=bar]' or '[ foo=bar ]' or '[ foo = bar ]' + // or 'foo=bar' or 'foo = bar' + if ( /^(\[)?\s*(.+?)\s*(\])?$/.test(tagInfo.name) ) { + tagInfo.name = RegExp.$2; + // were the "optional" brackets present? + if (RegExp.$1 && RegExp.$3) { + tagInfo.optional = true; + } + + // like 'foo=bar' or 'foo = bar' + if ( /^(.+?)\s*=\s*(.+)$/.test(tagInfo.name) ) { + tagInfo.name = RegExp.$1; + tagInfo.defaultvalue = RegExp.$2; + } + } + + return tagInfo; +} + +/** @private */ +function getTypeStrings(parsedType, isOutermostType) { + var applications; + var typeString; + + var types = []; + + var TYPES = catharsis.Types; + + switch (parsedType.type) { + case TYPES.AllLiteral: + types.push('*'); + break; + case TYPES.FunctionType: + types.push( catharsis.stringify(parsedType) ); + break; + case TYPES.NameExpression: + types.push(parsedType.name); + break; + case TYPES.NullLiteral: + types.push('null'); + break; + case TYPES.RecordType: + types.push( catharsis.stringify(parsedType) ); + break; + case TYPES.TypeApplication: + // if this is the outermost type, we strip the modifiers; otherwise, we keep them + if (isOutermostType) { + applications = parsedType.applications.map(function(application) { + return catharsis.stringify(application); + }).join(', '); + typeString = util.format( '%s.<%s>', getTypeStrings(parsedType.expression), + applications ); + + types.push(typeString); + } + else { + types.push( catharsis.stringify(parsedType) ); + } + break; + case TYPES.TypeUnion: + parsedType.elements.forEach(function(element) { + types = types.concat( getTypeStrings(element) ); + }); + break; + case TYPES.UndefinedLiteral: + types.push('undefined'); + break; + case TYPES.UnknownLiteral: + types.push('?'); + break; + default: + // this shouldn't happen + throw new Error( util.format('unrecognized type %s in parsed type: %j', parsedType.type, + parsedType) ); + } + + return types; +} + +/** + * Extract JSDoc-style and Closure Compiler-style type information from the type expression + * specified in the tag info. + * + * @private + * @param {module:jsdoc/tag/type.TagInfo} tagInfo - Information contained in the tag. + * @return {module:jsdoc/tag/type.TagInfo} Updated information from the tag. + */ +function parseTypeExpression(tagInfo) { + var errorMessage; + var parsedType; + + // don't try to parse empty type expressions + if (!tagInfo.typeExpression) { + return tagInfo; + } + + try { + parsedType = catharsis.parse(tagInfo.typeExpression, {jsdoc: true}); + } + catch (e) { + // always re-throw so the caller has a chance to report which file was bad + throw new Error( util.format('Invalid type expression "%s": %s', tagInfo.typeExpression, + e.message) ); + } + + tagInfo.type = tagInfo.type.concat( getTypeStrings(parsedType, true) ); + tagInfo.parsedType = parsedType; + + // Catharsis and JSDoc use the same names for 'optional' and 'nullable'... + ['optional', 'nullable'].forEach(function(key) { + if (parsedType[key] !== null && parsedType[key] !== undefined) { + tagInfo[key] = parsedType[key]; + } + }); + + // ...but not 'variable'. + if (parsedType.repeatable !== null && parsedType.repeatable !== undefined) { + tagInfo.variable = parsedType.repeatable; + } + + return tagInfo; +} + +// TODO: allow users to add/remove type parsers (perhaps via plugins) +var typeParsers = [parseName, parseTypeExpression]; + +/** + * Parse the value of a JSDoc tag. + * + * @param {string} tagValue - The value of the tag. For example, the tag `@param {string} name` has + * a value of `{string} name`. + * @param {boolean} canHaveName - Indicates whether the value can include a symbol name. + * @param {boolean} canHaveType - Indicates whether the value can include a type expression that + * describes the symbol. + * @return {module:jsdoc/tag/type.TagInfo} Information obtained from the tag. + * @throws {Error} Thrown if a type expression cannot be parsed. + */ +exports.parse = function(tagValue, canHaveName, canHaveType) { + if (typeof tagValue !== 'string') { tagValue = ''; } + + var tagInfo = getTagInfo(tagValue, canHaveName, canHaveType); + tagInfo.type = tagInfo.type || []; + + typeParsers.forEach(function(parser) { + tagInfo = parser.call(this, tagInfo); + }); + + // if we wanted a type, but the parsers didn't add any type names, use the type expression + if (canHaveType && !tagInfo.type.length && tagInfo.typeExpression) { + tagInfo.type = [tagInfo.typeExpression]; + } + + return tagInfo; +}; diff --git a/third_party/jsdoc/lib/jsdoc/tag/validator.js b/third_party/jsdoc/lib/jsdoc/tag/validator.js new file mode 100644 index 0000000000..6a7f95bd15 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/tag/validator.js @@ -0,0 +1,56 @@ +/*global env: true */ +/** + @module jsdoc/tag/validator + @requires jsdoc/tag/dictionary + + @author Michael Mathews + @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ +'use strict'; + +var dictionary = require('jsdoc/tag/dictionary'); +var format = require('util').format; +var logger = require('jsdoc/util/logger'); + +function buildMessage(tagName, meta, desc) { + var result = format('The @%s tag %s. File: %s, line: %s', tagName, desc, meta.filename, + meta.lineno); + if (meta.comment) { + result += '\n' + meta.comment; + } + return result; +} + +/** + * Validate the given tag. + */ +exports.validate = function(tag, tagDef, meta) { + // handle cases where the tag definition does not exist + if (!tagDef) { + // log an error if unknown tags are not allowed + if (!env.conf.tags.allowUnknownTags) { + logger.error( buildMessage(tag.title, meta, 'is not a known tag') ); + } + + // stop validation, since there's nothing to validate against + return; + } + + // check for errors that make the tag useless + if (!tagDef && !env.conf.tags.allowUnknownTags) { + logger.error( buildMessage(tag.title, meta, 'is not a known tag') ); + } + else if (!tag.text && tagDef.mustHaveValue) { + logger.error( buildMessage(tag.title, meta, 'requires a value') ); + } + + // check for minor issues that are usually harmless + else if (tag.text && tagDef.mustNotHaveValue) { + logger.warn( buildMessage(tag.title, meta, + 'does not permit a value; the value will be ignored') ); + } + else if (tag.value && tag.value.description && tagDef.mustNotHaveDescription) { + logger.warn( buildMessage(tag.title, meta, + 'does not permit a description; the description will be ignored') ); + } +}; diff --git a/third_party/jsdoc/lib/jsdoc/template.js b/third_party/jsdoc/lib/jsdoc/template.js new file mode 100644 index 0000000000..6c1d4dae3c --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/template.js @@ -0,0 +1,83 @@ +/** + * @file Wrapper for underscore's template utility to allow loading templates from files. + * @author Rafał Wrzeszcz + * @author Matthew Christopher Kastor-Inare III + * @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ +'use strict'; + +var _ = require('underscore'), + fs = require('jsdoc/fs'), + path = require('path'); + +/** + @module jsdoc/template + */ + +/** + @class + @classdesc Underscore template helper. + @param {string} path - Templates directory. + */ +exports.Template = function(path) { + this.path = path; + this.layout = null; + this.cache = {}; + // override default template tag settings + this.settings = { + evaluate: /<\?js([\s\S]+?)\?>/g, + interpolate: /<\?js=([\s\S]+?)\?>/g, + escape: /<\?js~([\s\S]+?)\?>/g + }; +}; + +/** Loads template from given file. + @param {string} file - Template filename. + @return {function} Returns template closure. + */ +exports.Template.prototype.load = function(file) { + return _.template(fs.readFileSync(file, 'utf8'), null, this.settings); +}; + +/** + Renders template using given data. + + This is low-level function, for rendering full templates use {@link Template.render()}. + + @param {string} file - Template filename. + @param {object} data - Template variables (doesn't have to be object, but passing variables dictionary is best way and most common use). + @return {string} Rendered template. + */ +exports.Template.prototype.partial = function(file, data) { + file = path.resolve(this.path, file); + + // load template into cache + if (!(file in this.cache)) { + this.cache[file] = this.load(file); + } + + // keep template helper context + return this.cache[file].call(this, data); +}; + +/** + Renders template with given data. + + This method automaticaly applies layout if set. + + @param {string} file - Template filename. + @param {object} data - Template variables (doesn't have to be object, but passing variables dictionary is best way and most common use). + @return {string} Rendered template. + */ +exports.Template.prototype.render = function(file, data) { + // main content + var content = this.partial(file, data); + + // apply layout + if (this.layout) { + data.content = content; + content = this.partial(this.layout, data); + } + + return content; +}; diff --git a/third_party/jsdoc/lib/jsdoc/tutorial.js b/third_party/jsdoc/lib/jsdoc/tutorial.js new file mode 100644 index 0000000000..304b283cea --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/tutorial.js @@ -0,0 +1,140 @@ +/** + @overview + @author Rafał Wrzeszcz + @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ +'use strict'; + +var markdown = require('jsdoc/util/markdown'); +var util = require('util'); + +var hasOwnProp = Object.prototype.hasOwnProperty; + +/** Removes child tutorial from the parent. Does *not* unset child.parent though. + @param {Tutorial} parent - parent tutorial. + @param {Tutorial} child - Old child. + @private + */ +function removeChild(parent, child) { + var index = parent.children.indexOf(child); + if (index !== -1) { + parent.children.splice(index, 1); + } +} + +/** Adds a child to the parent tutorial. Does *not* set child.parent though. + @param {Tutorial} parent - parent tutorial. + @param {Tutorial} child - New child. + @private + */ +function addChild(parent, child) { + parent.children.push(child); +} + +/** + @module jsdoc/tutorial + */ + +/** + @class + @classdesc Represents a single JSDoc tutorial. + @param {string} name - Tutorial name. + @param {string} content - Text content. + @param {number} type - Source formating. + */ +exports.Tutorial = function(name, content, type) { + this.title = this.name = name; + this.content = content; + this.type = type; + + // default values + this.parent = null; + this.children = []; +}; + +/** Moves children from current parent to different one. + @param {?Tutorial} parent - New parent. If null, the tutorial has no parent. + */ +exports.Tutorial.prototype.setParent = function(parent) { + // removes node from old parent + if (this.parent) { + removeChild(this.parent, this); + } + + this.parent = parent; + if (parent) { + addChild(parent, this); + } +}; + +/** Removes children from current node. + @param {Tutorial} child - Old child. + */ +exports.Tutorial.prototype.removeChild = function(child) { + child.setParent(null); +}; + +/** Adds new children to current node. + @param {Tutorial} child - New child. + */ +exports.Tutorial.prototype.addChild = function(child) { + child.setParent(this); +}; + +/** Prepares source. + @return {string} HTML source. + */ +exports.Tutorial.prototype.parse = function() { + switch (this.type) { + // nothing to do + case exports.TYPES.HTML: + return this.content; + + // markdown + case exports.TYPES.MARKDOWN: + var mdParse = markdown.getParser(); + return mdParse(this.content); + + // uhm... should we react somehow? + // if not then this case can be merged with TYPES.HTML + default: + return this.content; + } +}; + +/** + * @class + * @classdesc Represents the root tutorial. + * @extends {module:jsdoc/tutorial.Tutorial} + */ +exports.RootTutorial = function() { + exports.RootTutorial.super_.call(this, '', ''); + + this._tutorials = {}; +}; +util.inherits(exports.RootTutorial, exports.Tutorial); + +/** + * Retrieve a tutorial by name. + * @param {string} name - Tutorial name. + * @return {module:jsdoc/tutorial.Tutorial} Tutorial instance. + */ +exports.RootTutorial.prototype.getByName = function(name) { + return hasOwnProp.call(this._tutorials, name) && this._tutorials[name]; +}; + +/** + * Add a child tutorial to the root. + * @param {module:jsdoc/tutorial.Tutorial} child - Child tutorial. + */ +exports.RootTutorial.prototype._addTutorial = function(child) { + this._tutorials[child.name] = child; +}; + +/** Tutorial source types. + @enum {number} + */ +exports.TYPES = { + HTML: 1, + MARKDOWN: 2 +}; diff --git a/third_party/jsdoc/lib/jsdoc/tutorial/resolver.js b/third_party/jsdoc/lib/jsdoc/tutorial/resolver.js new file mode 100644 index 0000000000..610405e196 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/tutorial/resolver.js @@ -0,0 +1,192 @@ +/** + @overview + @author Rafał Wrzeszcz + @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ + +/** + @module jsdoc/tutorial/resolver + */ +'use strict'; + +var logger = require('jsdoc/util/logger'); +var fs = require('jsdoc/fs'); +var path = require('path'); +var tutorial = require('jsdoc/tutorial'); + +var hasOwnProp = Object.prototype.hasOwnProperty; + +// TODO: make this an instance member of `RootTutorial`? +var conf = {}; +var finder = /^(.*)\.(x(?:ht)?ml|html?|md|markdown|json)$/i; + +/** checks if `conf` is the metadata for a single tutorial. + * A tutorial's metadata has a property 'title' and/or a property 'children'. + * @param {object} json - the object we want to test (typically from JSON.parse) + * @returns {boolean} whether `json` could be the metadata for a tutorial. + */ +function isTutorialJSON(json) { + // if conf.title exists or conf.children exists, it is metadata for a tutorial + return (hasOwnProp.call(json, 'title') || hasOwnProp.call(json, 'children')); +} + +/** + * Root tutorial. + * @type {module:jsdoc/tutorial.Root} + */ +exports.root = new tutorial.RootTutorial(); + +/** Helper function that adds tutorial configuration to the `conf` variable. + * This helps when multiple tutorial configurations are specified in one object, + * or when a tutorial's children are specified as tutorial configurations as + * opposed to an array of tutorial names. + * + * Recurses as necessary to ensure all tutorials are added. + * + * @param {string} name - if `meta` is a configuration for a single tutorial, + * this is that tutorial's name. + * @param {object} meta - object that contains tutorial information. + * Can either be for a single tutorial, or for multiple + * (where each key in `meta` is the tutorial name and each + * value is the information for a single tutorial). + * Additionally, a tutorial's 'children' property may + * either be an array of strings (names of the child tutorials), + * OR an object giving the configuration for the child tutorials. + */ +function addTutorialConf(name, meta) { + var i; + var l; + var names; + + if (isTutorialJSON(meta)) { + // if the children are themselves tutorial defintions as opposed to an + // array of strings, add each child. + if (hasOwnProp.call(meta, 'children') && !Array.isArray(meta.children)) { + names = Object.keys(meta.children); + for (i = 0, l = names.length; i < l; ++i) { + addTutorialConf(names[i], meta.children[names[i]]); + } + // replace with an array of names. + meta.children = names; + } + // check if the tutorial has already been defined... + if (hasOwnProp.call(conf, name)) { + logger.warn('Metadata for the tutorial %s is defined more than once. Only the first definition will be used.', name ); + } else { + conf[name] = meta; + } + } else { + // keys are tutorial names, values are `Tutorial` instances + names = Object.keys(meta); + for (i = 0, l = names.length; i < l; ++i) { + addTutorialConf(names[i], meta[names[i]]); + } + } +} + +/** + * Add a tutorial. + * @param {module:jsdoc/tutorial.Tutorial} current - Tutorial to add. + */ +exports.addTutorial = function(current) { + if (exports.root.getByName(current.name)) { + logger.warn('The tutorial %s is defined more than once. Only the first definition will be used.', current.name); + } else { + // by default, the root tutorial is the parent + current.setParent(exports.root); + + exports.root._addTutorial(current); + } +}; + +/** + * Load tutorials from the given path. + * @param {string} filepath - Tutorials directory. + */ +exports.load = function(filepath) { + var content; + var current; + var files = fs.ls(filepath, global.env.opts.recurse ? 10 : undefined); + var name; + var match; + var type; + + // tutorials handling + files.forEach(function(file) { + match = file.match(finder); + + // any filetype that can apply to tutorials + if (match) { + name = path.basename(match[1]); + content = fs.readFileSync(file, global.env.opts.encoding); + + switch (match[2].toLowerCase()) { + // HTML type + case 'xml': + case 'xhtml': + case 'html': + case 'htm': + type = tutorial.TYPES.HTML; + break; + + // Markdown typs + case 'md': + case 'markdown': + type = tutorial.TYPES.MARKDOWN; + break; + + // configuration file + case 'json': + var meta = JSON.parse(content); + addTutorialConf(name, meta); + // don't add this as a tutorial + return; + + // how can it be? check `finder' regexp + default: + // not a file we want to work with + return; + } + + current = new tutorial.Tutorial(name, content, type); + exports.addTutorial(current); + } + }); +}; + +/** Resolves hierarchical structure. + */ +exports.resolve = function() { + var item; + var current; + + Object.keys(conf).forEach(function(name) { + current = exports.root.getByName(name); + + // TODO: should we complain about this? + if (!current) { + return; + } + + item = conf[name]; + + // set title + if (item.title) { + current.title = item.title; + } + + // add children + if (item.children) { + item.children.forEach(function(child) { + var childTutorial = exports.root.getByName(child); + + if (!childTutorial) { + logger.error('Missing child tutorial: %s', child); + } + else { + childTutorial.setParent(current); + } + }); + } + }); +}; diff --git a/third_party/jsdoc/lib/jsdoc/util/doop.js b/third_party/jsdoc/lib/jsdoc/util/doop.js new file mode 100644 index 0000000000..ba0fea2f6f --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/util/doop.js @@ -0,0 +1,82 @@ +/*global Set */ + +/** + Deep clone a simple object. Ignores non-enumerable properties. + @private + */ +'use strict'; + +var setDefined = typeof Set !== 'undefined'; + +function addItem(set, item) { + if (setDefined) { + set.add(item); + } + else if (set.indexOf(item) === -1) { + set.push(item); + } +} + +function hasItem(set, item) { + if (setDefined) { + return set.has(item); + } + else { + return set.indexOf(item) !== -1; + } +} + +// TODO: should share code with jsdoc/util/dumper~ObjectWalker +function doop(o, seen) { + var clone; + var descriptor; + var props; + var i; + var l; + + if (!seen) { + seen = setDefined ? new Set() : []; + } + + if (o instanceof Object && o.constructor !== Function) { + if ( hasItem(seen, o) ) { + clone = ''; + } + else { + addItem(seen, o); + + if ( Array.isArray(o) ) { + clone = []; + for (i = 0, l = o.length; i < l; i++) { + clone[i] = (o[i] instanceof Object) ? doop(o[i], seen) : o[i]; + } + } + else { + clone = Object.create( Object.getPrototypeOf(o) ); + props = Object.keys(o); + for (i = 0, l = props.length; i < l; i++) { + descriptor = Object.getOwnPropertyDescriptor(o, props[i]); + if (descriptor.value) { + descriptor.value = doop(descriptor.value, seen); + } + + Object.defineProperty(clone, props[i], descriptor); + } + } + } + + return clone; + } + + return o; +} + +// Wrapper to avoid exposing the 'seen' parameter outside of this module. +function doopWrapper(o) { + return doop(o); +} + +// for backwards compatibility +doopWrapper.doop = doopWrapper; + +module.exports = doopWrapper; diff --git a/third_party/jsdoc/lib/jsdoc/util/dumper.js b/third_party/jsdoc/lib/jsdoc/util/dumper.js new file mode 100644 index 0000000000..3e3f88deda --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/util/dumper.js @@ -0,0 +1,135 @@ +/*global Set */ +/** + * Recursively print out all names and values in a data structure. + * @module jsdoc/util/dumper + * @author Michael Mathews + * @license Apache License 2.0 - See file 'LICENSE.md' in this project. + */ +'use strict'; + +var util = require('util'); +var setDefined = typeof Set !== 'undefined'; + +function ObjectWalker() { + if (setDefined) { + this.seenItems = new Set(); + } else { + this.seenItems = []; + } +} + +ObjectWalker.prototype.seen = function(object) { + var result; + if (setDefined) { + result = this.seenItems.has(object); + } else { + result = object.hasBeenSeenByWalkerDumper; + } + return result; +}; + +ObjectWalker.prototype.markAsSeen = function(object) { + if (setDefined) { + this.seenItems.add(object); + } else { + object.hasBeenSeenByWalkerDumper = true; + this.seenItems.push(object); + } +}; + +ObjectWalker.prototype.cleanSeenFlag = function() { + if (setDefined) { + this.seenItems = new Set(); + } else { + this.seenItems.forEach(function(object) { + delete object.hasBeenSeenByWalkerDumper; + }); + } +}; + +// some objects are unwalkable, like Java native objects +ObjectWalker.prototype.isUnwalkable = function(o) { + return (o && typeof o === 'object' && typeof o.constructor === 'undefined'); +}; + +ObjectWalker.prototype.isFunction = function(o) { + return (o && typeof o === 'function' || o instanceof Function); +}; + +ObjectWalker.prototype.isObject = function(o) { + return o && o instanceof Object || + (o && typeof o.constructor !== 'undefined' && o.constructor.name === 'Object'); +}; + +ObjectWalker.prototype.checkCircularRefs = function(o, func) { + if ( this.seen(o) ) { + return ''; + } + else { + this.markAsSeen(o); + return func(o); + } +}; + +ObjectWalker.prototype.walk = function(o) { + var result; + + var self = this; + + if ( this.isUnwalkable(o) ) { + result = ''; + } + else if ( o === undefined ) { + result = null; + } + else if ( Array.isArray(o) ) { + result = this.checkCircularRefs(o, function(arr) { + var newArray = []; + arr.forEach(function(item) { + newArray.push( self.walk(item) ); + }); + + return newArray; + }); + } + else if ( util.isRegExp(o) ) { + result = ''; + } + else if ( util.isDate(o) ) { + result = ''; + } + else if ( util.isError(o) ) { + result = { message: o.message }; + } + else if ( this.isFunction(o) ) { + result = ''; + } + else if ( this.isObject(o) && o !== null ) { + result = this.checkCircularRefs(o, function(obj) { + var newObj = {}; + Object.keys(obj).forEach(function(key) { + if (!setDefined && key === 'hasBeenSeenByWalkerDumper') { return; } + newObj[key] = self.walk(obj[key]); + }); + + return newObj; + }); + } + // should be safe to JSON.stringify() everything else + else { + result = o; + } + + return result; +}; + +/** + * @param {*} object + */ +exports.dump = function(object) { + var walker = new ObjectWalker(); + var result = JSON.stringify(walker.walk(object), null, 4); + walker.cleanSeenFlag(); + + return result; +}; diff --git a/third_party/jsdoc/lib/jsdoc/util/error.js b/third_party/jsdoc/lib/jsdoc/util/error.js new file mode 100644 index 0000000000..6ed1895d5a --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/util/error.js @@ -0,0 +1,35 @@ +/*global env: true */ +/** + * Helper functions for handling errors. + * + * @deprecated As of JSDoc 3.3.0. This module may be removed in a future release. Use the module + * {@link module:jsdoc/util/logger} to log warnings and errors. + * @module jsdoc/util/error + */ +'use strict'; + +/** + * Log an exception as an error. + * + * Prior to JSDoc 3.3.0, this method would either log the exception (if lenient mode was enabled) or + * re-throw the exception (default). + * + * In JSDoc 3.3.0 and later, lenient mode has been replaced with strict mode, which is disabled by + * default. If strict mode is enabled, calling the `handle` method causes JSDoc to exit immediately, + * just as if the exception had been re-thrown. + * + * @deprecated As of JSDoc 3.3.0. This module may be removed in a future release. + * @param {Error} e - The exception to log. + * @memberof module:jsdoc/util/error + */ +exports.handle = function(e) { + var logger = require('jsdoc/util/logger'); + var msg = e ? ( e.message || JSON.stringify(e) ) : ''; + + // include the error type if it's an Error object + if (e instanceof Error) { + msg = e.name + ': ' + msg; + } + + logger.error(msg); +}; diff --git a/third_party/jsdoc/lib/jsdoc/util/logger.js b/third_party/jsdoc/lib/jsdoc/util/logger.js new file mode 100644 index 0000000000..98f732e4ef --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/util/logger.js @@ -0,0 +1,232 @@ +/** + * Logging tools for JSDoc. + * + * Log messages are printed to the console based on the current logging level. By default, messages + * at level `{@link module:jsdoc/util/logger.LEVELS.ERROR}` or above are logged; all other messages + * are ignored. + * + * In addition, the module object emits an event whenever a logger method is called, regardless of + * the current logging level. The event's name is the string `logger:` followed by the logger's name + * (for example, `logger:error`). The event handler receives an array of arguments that were passed + * to the logger method. + * + * Each logger method accepts a `message` parameter that may contain zero or more placeholders. Each + * placeholder is replaced by the corresponding argument following the message. If the placeholder + * does not have a corresponding argument, the placeholder is not replaced. + * + * The following placeholders are supported: + * + * + `%s`: String. + * + `%d`: Number. + * + `%j`: JSON. + * + * @module jsdoc/util/logger + * @extends module:events.EventEmitter + * @example + * var logger = require('jsdoc/util/logger'); + * + * var data = { + * foo: 'bar' + * }; + * var name = 'baz'; + * + * logger.warn('%j %s', data, name); // prints '{"foo":"bar"} baz' + * @see http://nodejs.org/api/util.html#util_util_format_format + */ +'use strict'; + +var runtime = require('jsdoc/util/runtime'); +var util = require('util'); + +function Logger() {} +util.inherits(Logger, require('events').EventEmitter); + +var logger = module.exports = new Logger(); + +/** + * Logging levels for the JSDoc logger. The default logging level is + * {@link module:jsdoc/util/logger.LEVELS.ERROR}. + * + * @enum + * @type {number} + */ +var LEVELS = logger.LEVELS = { + /** Do not log any messages. */ + SILENT: 0, + /** Log fatal errors that prevent JSDoc from running. */ + FATAL: 10, + /** Log all errors, including errors from which JSDoc can recover. */ + ERROR: 20, + /** + * Log the following messages: + * + * + Warnings + * + Errors + */ + WARN: 30, + /** + * Log the following messages: + * + * + Informational messages + * + Warnings + * + Errors + */ + INFO: 40, + /** + * Log the following messages: + * + * + Debugging messages + * + Informational messages + * + Warnings + * + Errors + */ + DEBUG: 50, + /** Log all messages. */ + VERBOSE: 1000 +}; + +var DEFAULT_LEVEL = LEVELS.WARN; +var logLevel = DEFAULT_LEVEL; + +var PREFIXES = { + DEBUG: 'DEBUG: ', + ERROR: 'ERROR: ', + FATAL: 'FATAL: ', + WARN: 'WARNING: ' +}; + +// Add a prefix to a log message if necessary. +function addPrefix(args, prefix) { + var updatedArgs; + + if (prefix && typeof args[0] === 'string') { + updatedArgs = args.slice(0); + updatedArgs[0] = prefix + updatedArgs[0]; + } + + return updatedArgs || args; +} + +// TODO: document events +function wrapLogFunction(name, func) { + var eventName = 'logger:' + name; + var upperCaseName = name.toUpperCase(); + var level = LEVELS[upperCaseName]; + var prefix = PREFIXES[upperCaseName]; + + return function() { + var loggerArgs; + + var args = Array.prototype.slice.call(arguments, 0); + + if (logLevel >= level) { + loggerArgs = addPrefix(args, prefix); + func.apply(null, loggerArgs); + } + + args.unshift(eventName); + logger.emit.apply(logger, args); + }; +} + +// Print a message to STDOUT without a terminating newline. +function printToStdout() { + var args = Array.prototype.slice.call(arguments, 0); + + process.stdout.write( util.format.apply(util, args) ); +} + +/** + * Log a message at log level {@link module:jsdoc/util/logger.LEVELS.DEBUG}. + * + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.debug = wrapLogFunction('debug', console.info); +/** + * Print a string at log level {@link module:jsdoc/util/logger.LEVELS.DEBUG}. The string is not + * terminated by a newline. + * + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.printDebug = wrapLogFunction('debug', printToStdout); +/** + * Log a message at log level {@link module:jsdoc/util/logger.LEVELS.ERROR}. + * + * @name module:jsdoc/util/logger.error + * @function + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.error = wrapLogFunction('error', console.error); +/** + * Log a message at log level {@link module:jsdoc/util/logger.LEVELS.FATAL}. + * + * @name module:jsdoc/util/logger.fatal + * @function + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.fatal = wrapLogFunction('fatal', console.error); +/** + * Log a message at log level {@link module:jsdoc/util/logger.LEVELS.INFO}. + * + * @name module:jsdoc/util/logger.info + * @function + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.info = wrapLogFunction('info', console.info); +/** + * Print a string at log level {@link module:jsdoc/util/logger.LEVELS.INFO}. The string is not + * terminated by a newline. + * + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.printInfo = wrapLogFunction('info', printToStdout); +/** + * Log a message at log level {@link module:jsdoc/util/logger.LEVELS.VERBOSE}. + * + * @name module:jsdoc/util/logger.verbose + * @function + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.verbose = wrapLogFunction('verbose', console.info); +/** + * Print a string at log level {@link module:jsdoc/util/logger.LEVELS.VERBOSE}. The string is not + * terminated by a newline. + * + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.printVerbose = wrapLogFunction('verbose', printToStdout); +/** + * Log a message at log level {@link module:jsdoc/util/logger.LEVELS.WARN}. + * + * @name module:jsdoc/util/logger.warn + * @function + * @param {string} message - The message to log. + * @param {...*=} values - The values that will replace the message's placeholders. + */ +logger.warn = wrapLogFunction('warn', console.warn); + +/** + * Set the log level. + * + * @param {module:jsdoc/util/logger.LEVELS} level - The log level to use. + */ +logger.setLevel = function setLevel(level) { + logLevel = (level !== undefined) ? level : DEFAULT_LEVEL; +}; + +/** + * Get the current log level. + * + * @return {module:jsdoc/util/logger.LEVELS} The current log level. + */ +logger.getLevel = function getLevel() { + return logLevel; +}; diff --git a/third_party/jsdoc/lib/jsdoc/util/markdown.js b/third_party/jsdoc/lib/jsdoc/util/markdown.js new file mode 100644 index 0000000000..4f2e198cec --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/util/markdown.js @@ -0,0 +1,156 @@ +/*global env */ + +/** + * Provides access to Markdown-related functions. + * @module jsdoc/util/markdown + * @author Michael Mathews + * @author Ben Blank + */ +'use strict'; + +var util = require('util'); + +/** + * Enumeration of Markdown parsers that are available. + * @enum {String} + */ +var parserNames = { + /** + * The "[markdown-js](https://github.com/evilstreak/markdown-js)" (aka "evilstreak") parser. + * + * @deprecated Replaced by "marked," as markdown-js does not support inline HTML. + */ + evilstreak: 'marked', + /** + * The "GitHub-flavored Markdown" parser. + * @deprecated Replaced by "marked." + */ + gfm: 'marked', + /** + * The "[Marked](https://github.com/chjj/marked)" parser. + */ + marked: 'marked' +}; + +/** + * Escape underscores that occur within {@ ... } in order to protect them + * from the markdown parser(s). + * @param {String} source the source text to sanitize. + * @returns {String} `source` where underscores within {@ ... } have been + * protected with a preceding backslash (i.e. \_) -- the markdown parsers + * will strip the backslash and protect the underscore. + */ +function escapeUnderscores(source) { + return source.replace(/\{@[^}\r\n]+\}/g, function (wholeMatch) { + return wholeMatch.replace(/(^|[^\\])_/g, '$1\\_'); + }); +} + +/** + * Escape HTTP/HTTPS URLs so that they are not automatically converted to HTML links. + * + * @param {string} source - The source text to escape. + * @return {string} The source text with escape characters added to HTTP/HTTPS URLs. + */ +function escapeUrls(source) { + return source.replace(/(https?)\:\/\//g, '$1:\\/\\/'); +} + +/** + * Unescape HTTP/HTTPS URLs after Markdown parsing is complete. + * + * @param {string} source - The source text to unescape. + * @return {string} The source text with escape characters removed from HTTP/HTTPS URLs. + */ +function unescapeUrls(source) { + return source.replace(/(https?)\:\\\/\\\//g, '$1://'); +} + +/** + * Escape characters in text within a code block. + * + * @param {string} source - The source text to escape. + * @return {string} The escaped source text. + */ +function escapeCode(source) { + return source.replace(/%s', level, text, level); + }; + + // Allow prettyprint to work on inline code samples + markedRenderer.code = function(code, language) { + var langClass = language ? ' lang-' + language : ''; + + return util.format( '
%s
', + langClass, escapeCode(code) ); + }; + + parserFunction = function(source) { + var result; + + source = escapeUnderscores(source); + source = escapeUrls(source); + + result = marked(source, { renderer: markedRenderer }) + .replace(/\s+$/, '') + .replace(/'/g, "'"); + result = unescapeUrls(result); + + return result; + }; + parserFunction._parser = parserNames.marked; + return parserFunction; + } + else { + logger.error('Unrecognized Markdown parser "%s". Markdown support is disabled.', + parserName); + } +} + +/** + * Retrieve a Markdown parsing function based on the value of the `conf.json` file's + * `env.conf.markdown` property. The parsing function accepts a single parameter containing Markdown + * source. The function uses the parser specified in `conf.json` to transform the Markdown source to + * HTML, then returns the HTML as a string. + * + * @returns {function} A function that accepts Markdown source, feeds it to the selected parser, and + * returns the resulting HTML. + */ +exports.getParser = function() { + var conf = env.conf.markdown; + if (conf && conf.parser) { + return getParseFunction(parserNames[conf.parser], conf); + } + else { + // marked is the default parser + return getParseFunction(parserNames.marked, conf); + } +}; diff --git a/third_party/jsdoc/lib/jsdoc/util/runtime.js b/third_party/jsdoc/lib/jsdoc/util/runtime.js new file mode 100644 index 0000000000..1a50a32115 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/util/runtime.js @@ -0,0 +1,117 @@ +/*global env, java */ +/** + * Helper functions to enable JSDoc to run on multiple JavaScript runtimes. + * + * @module jsdoc/util/runtime + * @private + */ +'use strict'; + +var os = require('os'); + +// These strings represent directory names; do not modify them! +/** @private */ +var RHINO = exports.RHINO = 'rhino'; +/** @private */ +var NODE = exports.NODE = 'node'; + +/** + * The JavaScript runtime that is executing JSDoc: + * + * + `module:jsdoc/util/runtime~RHINO`: Mozilla Rhino. + * + `module:jsdoc/util/runtime~NODE`: Node.js. + * + * @private + */ +var runtime = (function() { + if (global.Packages && typeof global.Packages === 'object' && + Object.prototype.toString.call(global.Packages) === '[object JavaPackage]') { + return RHINO; + } else if (require && require.main && module) { + return NODE; + } else { + // unknown runtime + throw new Error('Unable to identify the current JavaScript runtime.'); + } +})(); + +/** + * Check whether Mozilla Rhino is running JSDoc. + * @return {boolean} Set to `true` if the current runtime is Mozilla Rhino. + */ +exports.isRhino = function() { + return runtime === RHINO; +}; + +/** + * Check whether Node.js is running JSDoc. + * @return {boolean} Set to `true` if the current runtime is Node.js. + */ +exports.isNode = function() { + return runtime === NODE; +}; + +function initializeRhino(args) { + // the JSDoc dirname is the main module URI, minus the filename, converted to a path + var uriParts = require.main.uri.split('/'); + uriParts.pop(); + + env.dirname = String( new java.io.File(new java.net.URI(uriParts.join('/'))) ); + env.pwd = String( java.lang.System.getenv().get('PWD') ); + env.args = args; + + require(env.dirname + '/rhino/rhino-shim.js'); +} + +function initializeNode(args) { + var fs = require('fs'); + var path = require('path'); + + var jsdocPath = args[0]; + var pwd = args[1]; + + // resolve the path if it's a symlink + if ( fs.statSync(jsdocPath).isSymbolicLink() ) { + jsdocPath = path.resolve( path.dirname(jsdocPath), fs.readlinkSync(jsdocPath) ); + } + + env.dirname = jsdocPath; + env.pwd = pwd; + env.args = process.argv.slice(2); +} + +exports.initialize = function(args) { + switch (runtime) { + case RHINO: + initializeRhino(args); + break; + case NODE: + initializeNode(args); + break; + default: + throw new Error('Cannot initialize the unknown JavaScript runtime "' + runtime + '"!'); + } +}; + +/** + * Retrieve the identifier for the current JavaScript runtime. + * + * @private + * @return {string} The runtime identifier. + */ +exports.getRuntime = function() { + return runtime; +}; + +/** + * Get the require path for the runtime-specific implementation of a module. + * + * @param {string} partialPath - The partial path to the module. Use the same format as when calling + * `require()`. + * @return {object} The require path for the runtime-specific implementation of the module. + */ +exports.getModulePath = function(partialPath) { + var path = require('path'); + + return path.join(env.dirname, runtime, partialPath); +}; diff --git a/third_party/jsdoc/lib/jsdoc/util/templateHelper.js b/third_party/jsdoc/lib/jsdoc/util/templateHelper.js new file mode 100644 index 0000000000..2c23052771 --- /dev/null +++ b/third_party/jsdoc/lib/jsdoc/util/templateHelper.js @@ -0,0 +1,843 @@ +/*global env: true */ +/** + * @module jsdoc/util/templateHelper + */ +'use strict'; + +var catharsis = require('catharsis'); +var dictionary = require('jsdoc/tag/dictionary'); +var name = require('jsdoc/name'); +var util = require('util'); + +var hasOwnProp = Object.prototype.hasOwnProperty; +var NAMESPACES = require('jsdoc/name').NAMESPACES; + +var files = {}; +var ids = {}; + +// each container gets its own html file +var containers = ['class', 'module', 'external', 'namespace', 'mixin', 'interface']; + +var tutorials; + +/** Sets tutorials map. + @param {jsdoc.tutorial.Tutorial} root - Root tutorial node. + */ +exports.setTutorials = function(root) { + tutorials = root; +}; + +exports.globalName = name.SCOPE.NAMES.GLOBAL; +exports.fileExtension = '.html'; +exports.scopeToPunc = name.scopeToPunc; + +function getNamespace(kind) { + if (dictionary.isNamespace(kind)) { + return kind + ':'; + } + return ''; +} + +function makeUniqueFilename(filename, str) { + var key = filename.toLowerCase(); + var nonUnique = true; + + // don't allow filenames to begin with an underscore + if (!filename.length || filename[0] === '_') { + filename = 'X' + filename; + key = filename.toLowerCase(); + } + + // append enough underscores to make the filename unique + while (nonUnique) { + if ( hasOwnProp.call(files, key) ) { + filename += '_'; + key = filename.toLowerCase(); + } else { + nonUnique = false; + } + } + + files[key] = str; + return filename; +} + +function makeUniqueId(filename, id) { + var key = id.toLowerCase(); + var nonUnique = true; + + // append enough underscores to make the identifier unique + while (nonUnique) { + if ( hasOwnProp.call(ids, filename) && ids[filename].indexOf(key) !== -1 ) { + id += '_'; + key = id.toLowerCase(); + } + else { + nonUnique = false; + } + } + + ids[filename] = ids[filename] || []; + ids[filename].push(id); + + return id; +} + +var htmlsafe = exports.htmlsafe = function(str) { + return str.replace(/&/g, '&') + .replace(/]/g, '_') + // use - instead of ~ to denote 'inner' + .replace(/~/g, '-') + // use _ instead of # to denote 'instance' + .replace(/\#/g, '_') + // use _ instead of / (for example, in module names) + .replace(/\//g, '_') + // remove the variation, if any + .replace(/\([\s\S]*\)$/, '') + // make sure we don't create hidden files, or files whose names start with a dash + .replace(/^[\.\-]/, ''); + + // in case we've now stripped the entire basename (uncommon, but possible): + basename = basename.length ? basename : '_'; + + return makeUniqueFilename(basename, str) + exports.fileExtension; +}; + +/** + * Convert a string to an identifier that is unique for a specified URL. + * + * Identifiers are not considered unique if they are capitalized differently but are otherwise + * identical. + * + * @method + * @param {string} url - The URL in which the identifier will be used. + * @param {string} str - The string to convert. + * @return {string} A unique identifier based on the original string. + */ +var getUniqueId = exports.getUniqueId = makeUniqueId; + +// two-way lookup +var linkMap = { + longnameToUrl: {}, + urlToLongname: {} +}; + +var tutorialLinkMap = { + nameToUrl: {}, + urlToName: {} +}; + +var longnameToUrl = exports.longnameToUrl = linkMap.longnameToUrl; + +function parseType(longname) { + var err; + + try { + return catharsis.parse(longname, {jsdoc: true}); + } + catch (e) { + err = new Error('unable to parse ' + longname + ': ' + e.message); + require('jsdoc/util/logger').error(err); + return longname; + } +} + +function stringifyType(parsedType, cssClass, linkMap) { + return require('catharsis').stringify(parsedType, { + cssClass: cssClass, + htmlSafe: true, + links: linkMap + }); +} + +function hasUrlPrefix(text) { + return (/^(http|ftp)s?:\/\//).test(text); +} + +function isComplexTypeExpression(expr) { + // record types, type unions, and type applications all count as "complex" + return expr.search(/[{(|]/) !== -1 || expr.search(/ 0; +} + +function fragmentHash(fragmentId) { + if (!fragmentId) { + return ''; + } + + return '#' + fragmentId; +} + +/** + * Build an HTML link to the symbol with the specified longname. If the longname is not + * associated with a URL, this method simply returns the link text, if provided, or the longname. + * + * The `longname` parameter can also contain a URL rather than a symbol's longname. + * + * This method supports type applications that can contain one or more types, such as + * `Array.` or `Array.<(MyClass|YourClass)>`. In these examples, the method attempts to + * replace `Array`, `MyClass`, and `YourClass` with links to the appropriate types. The link text + * is ignored for type applications. + * + * @param {string} longname - The longname (or URL) that is the target of the link. + * @param {string=} linkText - The text to display for the link, or `longname` if no text is + * provided. + * @param {Object} options - Options for building the link. + * @param {string=} options.cssClass - The CSS class (or classes) to include in the link's `` + * tag. + * @param {string=} options.fragmentId - The fragment identifier (for example, `name` in + * `foo.html#name`) to append to the link target. + * @param {string=} options.linkMap - The link map in which to look up the longname. + * @param {boolean=} options.monospace - Indicates whether to display the link text in a monospace + * font. + * @return {string} The HTML link, or the link text if the link is not available. + */ +function buildLink(longname, linkText, options) { + var classString = options.cssClass ? util.format(' class="%s"', options.cssClass) : ''; + var fragmentString = fragmentHash(options.fragmentId); + var stripped; + var text; + var url; + var parsedType; + + // handle cases like: + // @see + // @see http://example.org + stripped = longname ? longname.replace(/^<|>$/g, '') : ''; + if ( hasUrlPrefix(stripped) ) { + url = stripped; + text = linkText || stripped; + } + // handle complex type expressions that may require multiple links + // (but skip anything that looks like an inline tag) + else if (longname && isComplexTypeExpression(longname) && /\{\@.+\}/.test(longname) === false) { + parsedType = parseType(longname); + return stringifyType(parsedType, options.cssClass, options.linkMap); + } + else { + url = hasOwnProp.call(options.linkMap, longname) ? options.linkMap[longname] : ''; + text = linkText || longname; + } + + text = options.monospace ? '' + text + '' : text; + + if (!url) { + return text; + } + else { + return util.format('%s', encodeURI(url + fragmentString), classString, + text); + } +} + +/** + * Retrieve an HTML link to the symbol with the specified longname. If the longname is not + * associated with a URL, this method simply returns the link text, if provided, or the longname. + * + * The `longname` parameter can also contain a URL rather than a symbol's longname. + * + * This method supports type applications that can contain one or more types, such as + * `Array.` or `Array.<(MyClass|YourClass)>`. In these examples, the method attempts to + * replace `Array`, `MyClass`, and `YourClass` with links to the appropriate types. The link text + * is ignored for type applications. + * + * @param {string} longname - The longname (or URL) that is the target of the link. + * @param {string=} linkText - The text to display for the link, or `longname` if no text is + * provided. + * @param {string=} cssClass - The CSS class (or classes) to include in the link's `` tag. + * @param {string=} fragmentId - The fragment identifier (for example, `name` in `foo.html#name`) to + * append to the link target. + * @return {string} The HTML link, or a plain-text string if the link is not available. + */ +var linkto = exports.linkto = function(longname, linkText, cssClass, fragmentId) { + return buildLink(longname, linkText, { + cssClass: cssClass, + fragmentId: fragmentId, + linkMap: longnameToUrl + }); +}; + +function useMonospace(tag, text) { + var cleverLinks; + var monospaceLinks; + var result; + + if ( hasUrlPrefix(text) ) { + result = false; + } + else if (tag === 'linkplain') { + result = false; + } + else if (tag === 'linkcode') { + result = true; + } + else { + cleverLinks = env.conf.templates.cleverLinks; + monospaceLinks = env.conf.templates.monospaceLinks; + + if (monospaceLinks || cleverLinks) { + result = true; + } + } + + return result || false; +} + +function splitLinkText(text) { + var linkText; + var target; + var splitIndex; + + // if a pipe is not present, we split on the first space + splitIndex = text.indexOf('|'); + if (splitIndex === -1) { + splitIndex = text.search(/\s/); + } + + if (splitIndex !== -1) { + linkText = text.substr(splitIndex + 1); + // Normalize subsequent newlines to a single space. + linkText = linkText.replace(/\n+/, ' '); + target = text.substr(0, splitIndex); + } + + return { + linkText: linkText, + target: target || text + }; +} + +var tutorialToUrl = exports.tutorialToUrl = function(tutorial) { + var node = tutorials.getByName(tutorial); + // no such tutorial + if (!node) { + require('jsdoc/util/logger').error( new Error('No such tutorial: ' + tutorial) ); + return null; + } + + var url; + // define the URL if necessary + if (!hasOwnProp.call(tutorialLinkMap.nameToUrl, node.name)) { + url = 'tutorial-' + getUniqueFilename(node.name); + tutorialLinkMap.nameToUrl[node.name] = url; + tutorialLinkMap.urlToName[url] = node.name; + } + + return tutorialLinkMap.nameToUrl[node.name]; +}; + +/** + * Retrieve a link to a tutorial, or the name of the tutorial if the tutorial is missing. If the + * `missingOpts` parameter is supplied, the names of missing tutorials will be prefixed by the + * specified text and wrapped in the specified HTML tag and CSS class. + * + * @todo Deprecate missingOpts once we have a better error-reporting mechanism. + * @param {string} tutorial The name of the tutorial. + * @param {string} content The link text to use. + * @param {object} [missingOpts] Options for displaying the name of a missing tutorial. + * @param {string} missingOpts.classname The CSS class to wrap around the tutorial name. + * @param {string} missingOpts.prefix The prefix to add to the tutorial name. + * @param {string} missingOpts.tag The tag to wrap around the tutorial name. + * @return {string} An HTML link to the tutorial, or the name of the tutorial with the specified + * options. + */ +var toTutorial = exports.toTutorial = function(tutorial, content, missingOpts) { + if (!tutorial) { + require('jsdoc/util/logger').error( new Error('Missing required parameter: tutorial') ); + return null; + } + + var node = tutorials.getByName(tutorial); + // no such tutorial + if (!node) { + missingOpts = missingOpts || {}; + var tag = missingOpts.tag; + var classname = missingOpts.classname; + + var link = tutorial; + if (missingOpts.prefix) { + link = missingOpts.prefix + link; + } + if (tag) { + link = '<' + tag + (classname ? (' class="' + classname + '">') : '>') + link; + link += ''; + } + return link; + } + + content = content || node.title; + + return '' + content + ''; +}; + +/** Find symbol {@link ...} and {@tutorial ...} strings in text and turn into html links */ +exports.resolveLinks = function(str) { + var replaceInlineTags = require('jsdoc/tag/inline').replaceInlineTags; + + function extractLeadingText(string, completeTag) { + var tagIndex = string.indexOf(completeTag); + var leadingText = null; + var leadingTextRegExp = /\[(.+?)\]/g; + var leadingTextInfo = leadingTextRegExp.exec(string); + + // did we find leading text, and if so, does it immediately precede the tag? + while (leadingTextInfo && leadingTextInfo.length) { + if (leadingTextInfo.index + leadingTextInfo[0].length === tagIndex) { + string = string.replace(leadingTextInfo[0], ''); + leadingText = leadingTextInfo[1]; + break; + } + + leadingTextInfo = leadingTextRegExp.exec(string); + } + + return { + leadingText: leadingText, + string: string + }; + } + + function processLink(string, tagInfo) { + var leading = extractLeadingText(string, tagInfo.completeTag); + var linkText = leading.leadingText; + var monospace; + var split; + var target; + string = leading.string; + + split = splitLinkText(tagInfo.text); + target = split.target; + linkText = linkText || split.linkText; + + monospace = useMonospace(tagInfo.tag, tagInfo.text); + + return string.replace( tagInfo.completeTag, buildLink(target, linkText, { + linkMap: longnameToUrl, + monospace: monospace + }) ); + } + + function processTutorial(string, tagInfo) { + var leading = extractLeadingText(string, tagInfo.completeTag); + string = leading.string; + + return string.replace( tagInfo.completeTag, toTutorial(tagInfo.text, leading.leadingText) ); + } + + var replacers = { + link: processLink, + linkcode: processLink, + linkplain: processLink, + tutorial: processTutorial + }; + + return replaceInlineTags(str, replacers).newString; +}; + +/** Convert tag text like "Jane Doe " into a mailto link */ +exports.resolveAuthorLinks = function(str) { + var author; + var matches = str.match(/^\s?([\s\S]+)\b\s+<(\S+@\S+)>\s?$/); + if (matches && matches.length === 3) { + author = '' + htmlsafe(matches[1]) + ''; + } + else { + author = htmlsafe(str); + } + + return author; +}; + +/** + * Find items in a TaffyDB database that match the specified key-value pairs. + * @param {TAFFY} data The TaffyDB database to search. + * @param {object|function} spec Key-value pairs to match against (for example, + * `{ longname: 'foo' }`), or a function that returns `true` if a value matches or `false` if it + * does not match. + * @return {array} The matching items. + */ +var find = exports.find = function(data, spec) { + return data(spec).get(); +}; + +/** + * Check whether a symbol is the only symbol exported by a module (as in + * `module.exports = function() {};`). + * + * @private + * @param {module:jsdoc/doclet.Doclet} doclet - The doclet for the symbol. + * @return {boolean} `true` if the symbol is the only symbol exported by a module; otherwise, + * `false`. + */ +function isModuleExports(doclet) { + return doclet.longname && doclet.longname === doclet.name && + doclet.longname.indexOf(NAMESPACES.MODULE) === 0 && doclet.kind !== 'module'; +} + +/** + * Retrieve all of the following types of members from a set of doclets: + * + * + Classes + * + Externals + * + Globals + * + Mixins + * + Modules + * + Namespaces + * + Events + * @param {TAFFY} data The TaffyDB database to search. + * @return {object} An object with `classes`, `externals`, `globals`, `mixins`, `modules`, + * `events`, and `namespaces` properties. Each property contains an array of objects. + */ +exports.getMembers = function(data) { + var members = { + classes: find( data, {kind: 'class'} ), + externals: find( data, {kind: 'external'} ), + events: find( data, {kind: 'event'} ), + globals: find(data, { + kind: ['member', 'function', 'constant', 'typedef'], + memberof: { isUndefined: true } + }), + mixins: find( data, {kind: 'mixin'} ), + modules: find( data, {kind: 'module'} ), + namespaces: find( data, {kind: 'namespace'} ), + interfaces: find( data, {kind: 'interface'} ) + }; + + // strip quotes from externals, since we allow quoted names that would normally indicate a + // namespace hierarchy (as in `@external "jquery.fn"`) + // TODO: we should probably be doing this for other types of symbols, here or elsewhere; see + // jsdoc3/jsdoc#396 + members.externals = members.externals.map(function(doclet) { + doclet.name = doclet.name.replace(/(^"|"$)/g, ''); + return doclet; + }); + + // functions that are also modules (as in `module.exports = function() {};`) are not globals + members.globals = members.globals.filter(function(doclet) { + return !isModuleExports(doclet); + }); + + return members; +}; + +/** + * Retrieve the member attributes for a doclet (for example, `virtual`, `static`, and + * `readonly`). + * @param {object} d The doclet whose attributes will be retrieved. + * @return {array} The member attributes for the doclet. + */ +exports.getAttribs = function(d) { + var attribs = []; + + if (!d) { + return attribs; + } + + if (d.virtual) { + attribs.push('abstract'); + } + + if (d.access && d.access !== 'public') { + attribs.push(d.access); + } + + if (d.scope && d.scope !== 'instance' && d.scope !== name.SCOPE.NAMES.GLOBAL) { + if (d.kind === 'function' || d.kind === 'member' || d.kind === 'constant') { + attribs.push(d.scope); + } + } + + if (d.readonly === true) { + if (d.kind === 'member') { + attribs.push('readonly'); + } + } + + if (d.kind === 'constant') { + attribs.push('constant'); + } + + if (d.nullable === true) { + attribs.push('nullable'); + } + else if (d.nullable === false) { + attribs.push('non-null'); + } + + return attribs; +}; + +/** + * Retrieve links to allowed types for the member. + * + * @param {Object} d - The doclet whose types will be retrieved. + * @param {string} [cssClass] - The CSS class to include in the `class` attribute for each link. + * @return {Array.} HTML links to allowed types for the member. + */ +exports.getSignatureTypes = function(d, cssClass) { + var types = []; + + if (d.type && d.type.names) { + types = d.type.names; + } + + if (types && types.length) { + types = types.map(function(t) { + return linkto(t, htmlsafe(t), cssClass); + }); + } + + return types; +}; + +/** + * Retrieve names of the parameters that the member accepts. If a value is provided for `optClass`, + * the names of optional parameters will be wrapped in a `` tag with that class. + * @param {object} d The doclet whose parameter names will be retrieved. + * @param {string} [optClass] The class to assign to the `` tag that is wrapped around the + * names of optional parameters. If a value is not provided, optional parameter names will not be + * wrapped with a `` tag. Must be a legal value for a CSS class name. + * @return {array} An array of parameter names, with or without `` tags wrapping the + * names of optional parameters. + */ +exports.getSignatureParams = function(d, optClass) { + var pnames = []; + + if (d.params) { + d.params.forEach(function(p) { + if (p.name && p.name.indexOf('.') === -1) { + if (p.optional && optClass) { + pnames.push('' + p.name + ''); + } + else { + pnames.push(p.name); + } + } + }); + } + + return pnames; +}; + +/** + * Retrieve links to types that the member can return. + * + * @param {Object} d - The doclet whose types will be retrieved. + * @param {string} [cssClass] - The CSS class to include in the `class` attribute for each link. + * @return {Array.} HTML links to types that the member can return. + */ +exports.getSignatureReturns = function(d, cssClass) { + var returnTypes = []; + + if (d.returns) { + d.returns.forEach(function(r) { + if (r && r.type && r.type.names) { + if (!returnTypes.length) { + returnTypes = r.type.names; + } + } + }); + } + + if (returnTypes && returnTypes.length) { + returnTypes = returnTypes.map(function(r) { + return linkto(r, htmlsafe(r), cssClass); + }); + } + + return returnTypes; +}; + +/** + * Retrieve an ordered list of doclets for a symbol's ancestors. + * + * @param {TAFFY} data - The TaffyDB database to search. + * @param {Object} doclet - The doclet whose ancestors will be retrieved. + * @return {Array.} A array of ancestor doclets, sorted from most to + * least distant. + */ +exports.getAncestors = function(data, doclet) { + var ancestors = []; + var doc = doclet; + + while (doc) { + doc = find(data, {longname: doc.memberof})[0]; + + if (doc) { + ancestors.unshift(doc); + } + } + + return ancestors; +}; + +/** + * Retrieve links to a member's ancestors. + * + * @param {TAFFY} data - The TaffyDB database to search. + * @param {Object} doclet - The doclet whose ancestors will be retrieved. + * @param {string} [cssClass] - The CSS class to include in the `class` attribute for each link. + * @return {Array.} HTML links to a member's ancestors. + */ +exports.getAncestorLinks = function(data, doclet, cssClass) { + var ancestors = exports.getAncestors(data, doclet); + var links = []; + + ancestors.forEach(function(ancestor) { + var linkText = (exports.scopeToPunc[ancestor.scope] || '') + ancestor.name; + var link = linkto(ancestor.longname, linkText, cssClass); + links.push(link); + }); + + if (links.length) { + links[links.length - 1] += (exports.scopeToPunc[doclet.scope] || ''); + } + + return links; +}; + +/** + * Iterates through all the doclets in `data`, ensuring that if a method + * @listens to an event, then that event has a 'listeners' array with the + * longname of the listener in it. + * + * @param {TAFFY} data - The TaffyDB database to search. + */ +exports.addEventListeners = function(data) { + // TODO: do this on the *pruned* data + // find all doclets that @listen to something. + var listeners = find(data, function () { return this.listens && this.listens.length; }); + + if (!listeners.length) { + return; + } + + var doc, + l, + _events = {}; // just a cache to prevent me doing so many lookups + + listeners.forEach(function (listener) { + l = listener.listens; + l.forEach(function (eventLongname) { + doc = _events[eventLongname] || find(data, {longname: eventLongname, kind: 'event'})[0]; + if (doc) { + if (!doc.listeners) { + doc.listeners = [listener.longname]; + } else { + doc.listeners.push(listener.longname); + } + _events[eventLongname] = _events[eventLongname] || doc; + } + }); + }); +}; + +/** + * Remove members that will not be included in the output, including: + * + * + Undocumented members. + * + Members tagged `@ignore`. + * + Members of anonymous classes. + * + Members tagged `@private`, unless the `private` option is enabled. + * @param {TAFFY} data The TaffyDB database to prune. + * @return {TAFFY} The pruned database. + */ +exports.prune = function(data) { + data({undocumented: true}).remove(); + data({ignore: true}).remove(); + if (!env.opts.private) { data({access: 'private'}).remove(); } + data({memberof: ''}).remove(); + + return data; +}; + +var registerLink = exports.registerLink = function(longname, url) { + linkMap.longnameToUrl[longname] = url; + linkMap.urlToLongname[url] = longname; +}; + +/** + * Get a longname's filename if one has been registered; otherwise, generate a unique filename, then + * register the filename. + * @private + */ +function getFilename(longname) { + var url; + + if ( longnameToUrl[longname] && hasOwnProp.call(longnameToUrl, longname) ) { + url = longnameToUrl[longname]; + } else { + url = getUniqueFilename(longname); + registerLink(longname, url); + } + + return url; +} + +/** Turn a doclet into a URL. */ +exports.createLink = function(doclet) { + var filename; + var fragment; + var match; + var fakeContainer; + + var url = ''; + var longname = doclet.longname; + + // handle doclets in which doclet.longname implies that the doclet gets its own HTML file, but + // doclet.kind says otherwise. this happens due to mistagged JSDoc (for example, a module that + // somehow has doclet.kind set to `member`). + // TODO: generate a warning (ideally during parsing!) + if (containers.indexOf(doclet.kind) === -1) { + match = /(\S+):/.exec(longname); + if (match && containers.indexOf(match[1]) !== -1) { + fakeContainer = match[1]; + } + } + + // the doclet gets its own HTML file + if ( containers.indexOf(doclet.kind) !== -1 || isModuleExports(doclet) ) { + filename = getFilename(longname); + } + // mistagged version of a doclet that gets its own HTML file + else if ( containers.indexOf(doclet.kind) === -1 && fakeContainer ) { + filename = getFilename(doclet.memberof || longname); + if (doclet.name === doclet.longname) { + fragment = ''; + } + else { + fragment = doclet.name || ''; + } + } + // the doclet is within another HTML file + else { + filename = getFilename(doclet.memberof || exports.globalName); + fragment = getNamespace(doclet.kind) + (doclet.name || ''); + } + + url = encodeURI( filename + fragmentHash(fragment) ); + + return url; +}; + +// TODO: docs +exports.longnamesToTree = name.longnamesToTree; diff --git a/third_party/jsdoc/node/fs.js b/third_party/jsdoc/node/fs.js new file mode 100644 index 0000000000..bf652f27f9 --- /dev/null +++ b/third_party/jsdoc/node/fs.js @@ -0,0 +1,63 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var stream = require('stream'); +var wrench = require('wrench'); + +var toDir = exports.toDir = function(_path) { + var isDirectory; + + _path = path.normalize(_path); + + try { + isDirectory = fs.statSync(_path).isDirectory(); + } + catch(e) { + isDirectory = false; + } + + if (isDirectory) { + return _path; + } else { + return path.dirname(_path); + } +}; + +exports.mkPath = function(/**Array*/ _path) { + if ( Array.isArray(_path) ) { + _path = _path.join(''); + } + + wrench.mkdirSyncRecursive(_path); +}; + +// adapted from http://procbits.com/2011/11/15/synchronous-file-copy-in-node-js +exports.copyFileSync = function(inFile, outDir, fileName) { + var BUF_LENGTH = 64 * 1024; + + var read; + var write; + + var buffer = new Buffer(BUF_LENGTH); + var bytesRead = 1; + var outFile = path.join( outDir, fileName || path.basename(inFile) ); + var pos = 0; + + wrench.mkdirSyncRecursive(outDir); + read = fs.openSync(inFile, 'r'); + write = fs.openSync(outFile, 'w'); + + while (bytesRead > 0) { + bytesRead = fs.readSync(read, buffer, 0, BUF_LENGTH, pos); + fs.writeSync(write, buffer, 0, bytesRead); + pos += bytesRead; + } + + fs.closeSync(read); + return fs.closeSync(write); +}; + +Object.keys(fs).forEach(function(key) { + exports[key] = fs[key]; +}); diff --git a/third_party/jsdoc/node_modules/async/LICENSE b/third_party/jsdoc/node_modules/async/LICENSE new file mode 100644 index 0000000000..b7f9d5001c --- /dev/null +++ b/third_party/jsdoc/node_modules/async/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010 Caolan McMahon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/jsdoc/node_modules/async/index.js b/third_party/jsdoc/node_modules/async/index.js new file mode 100644 index 0000000000..8e238453eb --- /dev/null +++ b/third_party/jsdoc/node_modules/async/index.js @@ -0,0 +1,3 @@ +// This file is just added for convenience so this repository can be +// directly checked out into a project's deps folder +module.exports = require('./lib/async'); diff --git a/third_party/jsdoc/node_modules/async/lib/async.js b/third_party/jsdoc/node_modules/async/lib/async.js new file mode 100644 index 0000000000..7cc4f5eac5 --- /dev/null +++ b/third_party/jsdoc/node_modules/async/lib/async.js @@ -0,0 +1,692 @@ +/*global setTimeout: false, console: false */ +(function () { + + var async = {}; + + // global on the server, window in the browser + var root = this, + previous_async = root.async; + + if (typeof module !== 'undefined' && module.exports) { + module.exports = async; + } + else { + root.async = async; + } + + async.noConflict = function () { + root.async = previous_async; + return async; + }; + + //// cross-browser compatiblity functions //// + + var _forEach = function (arr, iterator) { + if (arr.forEach) { + return arr.forEach(iterator); + } + for (var i = 0; i < arr.length; i += 1) { + iterator(arr[i], i, arr); + } + }; + + var _map = function (arr, iterator) { + if (arr.map) { + return arr.map(iterator); + } + var results = []; + _forEach(arr, function (x, i, a) { + results.push(iterator(x, i, a)); + }); + return results; + }; + + var _reduce = function (arr, iterator, memo) { + if (arr.reduce) { + return arr.reduce(iterator, memo); + } + _forEach(arr, function (x, i, a) { + memo = iterator(memo, x, i, a); + }); + return memo; + }; + + var _keys = function (obj) { + if (Object.keys) { + return Object.keys(obj); + } + var keys = []; + for (var k in obj) { + if (obj.hasOwnProperty(k)) { + keys.push(k); + } + } + return keys; + }; + + //// exported async module functions //// + + //// nextTick implementation with browser-compatible fallback //// + if (typeof process === 'undefined' || !(process.nextTick)) { + async.nextTick = function (fn) { + setTimeout(fn, 0); + }; + } + else { + async.nextTick = process.nextTick; + } + + async.forEach = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + _forEach(arr, function (x) { + iterator(x, function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed === arr.length) { + callback(null); + } + } + }); + }); + }; + + async.forEachSeries = function (arr, iterator, callback) { + callback = callback || function () {}; + if (!arr.length) { + return callback(); + } + var completed = 0; + var iterate = function () { + iterator(arr[completed], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + if (completed === arr.length) { + callback(null); + } + else { + iterate(); + } + } + }); + }; + iterate(); + }; + + async.forEachLimit = function (arr, limit, iterator, callback) { + callback = callback || function () {}; + if (!arr.length || limit <= 0) { + return callback(); + } + var completed = 0; + var started = 0; + var running = 0; + + (function replenish () { + if (completed === arr.length) { + return callback(); + } + + while (running < limit && started < arr.length) { + started += 1; + running += 1; + iterator(arr[started - 1], function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + completed += 1; + running -= 1; + if (completed === arr.length) { + callback(); + } + else { + replenish(); + } + } + }); + } + })(); + }; + + + var doParallel = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.forEach].concat(args)); + }; + }; + var doSeries = function (fn) { + return function () { + var args = Array.prototype.slice.call(arguments); + return fn.apply(null, [async.forEachSeries].concat(args)); + }; + }; + + + var _asyncMap = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (err, v) { + results[x.index] = v; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + }; + async.map = doParallel(_asyncMap); + async.mapSeries = doSeries(_asyncMap); + + + // reduce only has a series version, as doing reduce in parallel won't + // work in many situations. + async.reduce = function (arr, memo, iterator, callback) { + async.forEachSeries(arr, function (x, callback) { + iterator(memo, x, function (err, v) { + memo = v; + callback(err); + }); + }, function (err) { + callback(err, memo); + }); + }; + // inject alias + async.inject = async.reduce; + // foldl alias + async.foldl = async.reduce; + + async.reduceRight = function (arr, memo, iterator, callback) { + var reversed = _map(arr, function (x) { + return x; + }).reverse(); + async.reduce(reversed, memo, iterator, callback); + }; + // foldr alias + async.foldr = async.reduceRight; + + var _filter = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.filter = doParallel(_filter); + async.filterSeries = doSeries(_filter); + // select alias + async.select = async.filter; + async.selectSeries = async.filterSeries; + + var _reject = function (eachfn, arr, iterator, callback) { + var results = []; + arr = _map(arr, function (x, i) { + return {index: i, value: x}; + }); + eachfn(arr, function (x, callback) { + iterator(x.value, function (v) { + if (!v) { + results.push(x); + } + callback(); + }); + }, function (err) { + callback(_map(results.sort(function (a, b) { + return a.index - b.index; + }), function (x) { + return x.value; + })); + }); + }; + async.reject = doParallel(_reject); + async.rejectSeries = doSeries(_reject); + + var _detect = function (eachfn, arr, iterator, main_callback) { + eachfn(arr, function (x, callback) { + iterator(x, function (result) { + if (result) { + main_callback(x); + main_callback = function () {}; + } + else { + callback(); + } + }); + }, function (err) { + main_callback(); + }); + }; + async.detect = doParallel(_detect); + async.detectSeries = doSeries(_detect); + + async.some = function (arr, iterator, main_callback) { + async.forEach(arr, function (x, callback) { + iterator(x, function (v) { + if (v) { + main_callback(true); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(false); + }); + }; + // any alias + async.any = async.some; + + async.every = function (arr, iterator, main_callback) { + async.forEach(arr, function (x, callback) { + iterator(x, function (v) { + if (!v) { + main_callback(false); + main_callback = function () {}; + } + callback(); + }); + }, function (err) { + main_callback(true); + }); + }; + // all alias + async.all = async.every; + + async.sortBy = function (arr, iterator, callback) { + async.map(arr, function (x, callback) { + iterator(x, function (err, criteria) { + if (err) { + callback(err); + } + else { + callback(null, {value: x, criteria: criteria}); + } + }); + }, function (err, results) { + if (err) { + return callback(err); + } + else { + var fn = function (left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }; + callback(null, _map(results.sort(fn), function (x) { + return x.value; + })); + } + }); + }; + + async.auto = function (tasks, callback) { + callback = callback || function () {}; + var keys = _keys(tasks); + if (!keys.length) { + return callback(null); + } + + var results = {}; + + var listeners = []; + var addListener = function (fn) { + listeners.unshift(fn); + }; + var removeListener = function (fn) { + for (var i = 0; i < listeners.length; i += 1) { + if (listeners[i] === fn) { + listeners.splice(i, 1); + return; + } + } + }; + var taskComplete = function () { + _forEach(listeners.slice(0), function (fn) { + fn(); + }); + }; + + addListener(function () { + if (_keys(results).length === keys.length) { + callback(null, results); + callback = function () {}; + } + }); + + _forEach(keys, function (k) { + var task = (tasks[k] instanceof Function) ? [tasks[k]]: tasks[k]; + var taskCallback = function (err) { + if (err) { + callback(err); + // stop subsequent errors hitting callback multiple times + callback = function () {}; + } + else { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + taskComplete(); + } + }; + var requires = task.slice(0, Math.abs(task.length - 1)) || []; + var ready = function () { + return _reduce(requires, function (a, x) { + return (a && results.hasOwnProperty(x)); + }, true) && !results.hasOwnProperty(k); + }; + if (ready()) { + task[task.length - 1](taskCallback, results); + } + else { + var listener = function () { + if (ready()) { + removeListener(listener); + task[task.length - 1](taskCallback, results); + } + }; + addListener(listener); + } + }); + }; + + async.waterfall = function (tasks, callback) { + callback = callback || function () {}; + if (!tasks.length) { + return callback(); + } + var wrapIterator = function (iterator) { + return function (err) { + if (err) { + callback(err); + callback = function () {}; + } + else { + var args = Array.prototype.slice.call(arguments, 1); + var next = iterator.next(); + if (next) { + args.push(wrapIterator(next)); + } + else { + args.push(callback); + } + async.nextTick(function () { + iterator.apply(null, args); + }); + } + }; + }; + wrapIterator(async.iterator(tasks))(); + }; + + async.parallel = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + async.map(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + async.forEach(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.series = function (tasks, callback) { + callback = callback || function () {}; + if (tasks.constructor === Array) { + async.mapSeries(tasks, function (fn, callback) { + if (fn) { + fn(function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + callback.call(null, err, args); + }); + } + }, callback); + } + else { + var results = {}; + async.forEachSeries(_keys(tasks), function (k, callback) { + tasks[k](function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (args.length <= 1) { + args = args[0]; + } + results[k] = args; + callback(err); + }); + }, function (err) { + callback(err, results); + }); + } + }; + + async.iterator = function (tasks) { + var makeCallback = function (index) { + var fn = function () { + if (tasks.length) { + tasks[index].apply(null, arguments); + } + return fn.next(); + }; + fn.next = function () { + return (index < tasks.length - 1) ? makeCallback(index + 1): null; + }; + return fn; + }; + return makeCallback(0); + }; + + async.apply = function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + return function () { + return fn.apply( + null, args.concat(Array.prototype.slice.call(arguments)) + ); + }; + }; + + var _concat = function (eachfn, arr, fn, callback) { + var r = []; + eachfn(arr, function (x, cb) { + fn(x, function (err, y) { + r = r.concat(y || []); + cb(err); + }); + }, function (err) { + callback(err, r); + }); + }; + async.concat = doParallel(_concat); + async.concatSeries = doSeries(_concat); + + async.whilst = function (test, iterator, callback) { + if (test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.whilst(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.until = function (test, iterator, callback) { + if (!test()) { + iterator(function (err) { + if (err) { + return callback(err); + } + async.until(test, iterator, callback); + }); + } + else { + callback(); + } + }; + + async.queue = function (worker, concurrency) { + var workers = 0; + var q = { + tasks: [], + concurrency: concurrency, + saturated: null, + empty: null, + drain: null, + push: function (data, callback) { + if(data.constructor !== Array) { + data = [data]; + } + _forEach(data, function(task) { + q.tasks.push({ + data: task, + callback: typeof callback === 'function' ? callback : null + }); + if (q.saturated && q.tasks.length == concurrency) { + q.saturated(); + } + async.nextTick(q.process); + }); + }, + process: function () { + if (workers < q.concurrency && q.tasks.length) { + var task = q.tasks.shift(); + if(q.empty && q.tasks.length == 0) q.empty(); + workers += 1; + worker(task.data, function () { + workers -= 1; + if (task.callback) { + task.callback.apply(task, arguments); + } + if(q.drain && q.tasks.length + workers == 0) q.drain(); + q.process(); + }); + } + }, + length: function () { + return q.tasks.length; + }, + running: function () { + return workers; + } + }; + return q; + }; + + var _console_fn = function (name) { + return function (fn) { + var args = Array.prototype.slice.call(arguments, 1); + fn.apply(null, args.concat([function (err) { + var args = Array.prototype.slice.call(arguments, 1); + if (typeof console !== 'undefined') { + if (err) { + if (console.error) { + console.error(err); + } + } + else if (console[name]) { + _forEach(args, function (x) { + console[name](x); + }); + } + } + }])); + }; + }; + async.log = _console_fn('log'); + async.dir = _console_fn('dir'); + /*async.info = _console_fn('info'); + async.warn = _console_fn('warn'); + async.error = _console_fn('error');*/ + + async.memoize = function (fn, hasher) { + var memo = {}; + var queues = {}; + hasher = hasher || function (x) { + return x; + }; + var memoized = function () { + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + var key = hasher.apply(null, args); + if (key in memo) { + callback.apply(null, memo[key]); + } + else if (key in queues) { + queues[key].push(callback); + } + else { + queues[key] = [callback]; + fn.apply(null, args.concat([function () { + memo[key] = arguments; + var q = queues[key]; + delete queues[key]; + for (var i = 0, l = q.length; i < l; i++) { + q[i].apply(null, arguments); + } + }])); + } + }; + memoized.unmemoized = fn; + return memoized; + }; + + async.unmemoize = function (fn) { + return function () { + return (fn.unmemoized || fn).apply(null, arguments); + }; + }; + +}()); diff --git a/third_party/jsdoc/node_modules/async/package.json b/third_party/jsdoc/node_modules/async/package.json new file mode 100644 index 0000000000..1caad5bfef --- /dev/null +++ b/third_party/jsdoc/node_modules/async/package.json @@ -0,0 +1,35 @@ +{ + "name": "async", + "description": "Higher-order functions and common patterns for asynchronous code", + "main": "./index", + "author": { + "name": "Caolan McMahon" + }, + "version": "0.1.22", + "repository": { + "type": "git", + "url": "http://github.com/caolan/async.git" + }, + "bugs": { + "url": "http://github.com/caolan/async/issues" + }, + "licenses": [ + { + "type": "MIT", + "url": "http://github.com/caolan/async/raw/master/LICENSE" + } + ], + "devDependencies": { + "nodeunit": ">0.0.0", + "uglify-js": "1.2.x", + "nodelint": ">0.0.0" + }, + "readme": "# Async.js\n\nAsync is a utility module which provides straight-forward, powerful functions\nfor working with asynchronous JavaScript. Although originally designed for\nuse with [node.js](http://nodejs.org), it can also be used directly in the\nbrowser.\n\nAsync provides around 20 functions that include the usual 'functional'\nsuspects (map, reduce, filter, forEach…) as well as some common patterns\nfor asynchronous control flow (parallel, series, waterfall…). All these\nfunctions assume you follow the node.js convention of providing a single\ncallback as the last argument of your async function.\n\n\n## Quick Examples\n\n async.map(['file1','file2','file3'], fs.stat, function(err, results){\n // results is now an array of stats for each file\n });\n\n async.filter(['file1','file2','file3'], path.exists, function(results){\n // results now equals an array of the existing files\n });\n\n async.parallel([\n function(){ ... },\n function(){ ... }\n ], callback);\n\n async.series([\n function(){ ... },\n function(){ ... }\n ]);\n\nThere are many more functions available so take a look at the docs below for a\nfull list. This module aims to be comprehensive, so if you feel anything is\nmissing please create a GitHub issue for it.\n\n\n## Download\n\nReleases are available for download from\n[GitHub](http://github.com/caolan/async/downloads).\nAlternatively, you can install using Node Package Manager (npm):\n\n npm install async\n\n\n__Development:__ [async.js](https://github.com/caolan/async/raw/master/lib/async.js) - 17.5kb Uncompressed\n\n__Production:__ [async.min.js](https://github.com/caolan/async/raw/master/dist/async.min.js) - 1.7kb Packed and Gzipped\n\n\n## In the Browser\n\nSo far its been tested in IE6, IE7, IE8, FF3.6 and Chrome 5. Usage:\n\n \n \n\n\n## Documentation\n\n### Collections\n\n* [forEach](#forEach)\n* [map](#map)\n* [filter](#filter)\n* [reject](#reject)\n* [reduce](#reduce)\n* [detect](#detect)\n* [sortBy](#sortBy)\n* [some](#some)\n* [every](#every)\n* [concat](#concat)\n\n### Control Flow\n\n* [series](#series)\n* [parallel](#parallel)\n* [whilst](#whilst)\n* [until](#until)\n* [waterfall](#waterfall)\n* [queue](#queue)\n* [auto](#auto)\n* [iterator](#iterator)\n* [apply](#apply)\n* [nextTick](#nextTick)\n\n### Utils\n\n* [memoize](#memoize)\n* [unmemoize](#unmemoize)\n* [log](#log)\n* [dir](#dir)\n* [noConflict](#noConflict)\n\n\n## Collections\n\n\n### forEach(arr, iterator, callback)\n\nApplies an iterator function to each item in an array, in parallel.\nThe iterator is called with an item from the list and a callback for when it\nhas finished. If the iterator passes an error to this callback, the main\ncallback for the forEach function is immediately called with the error.\n\nNote, that since this function applies the iterator to each item in parallel\nthere is no guarantee that the iterator functions will complete in order.\n\n__Arguments__\n\n* arr - An array to iterate over.\n* iterator(item, callback) - A function to apply to each item in the array.\n The iterator is passed a callback which must be called once it has completed.\n* callback(err) - A callback which is called after all the iterator functions\n have finished, or an error has occurred.\n\n__Example__\n\n // assuming openFiles is an array of file names and saveFile is a function\n // to save the modified contents of that file:\n\n async.forEach(openFiles, saveFile, function(err){\n // if any of the saves produced an error, err would equal that error\n });\n\n---------------------------------------\n\n\n### forEachSeries(arr, iterator, callback)\n\nThe same as forEach only the iterator is applied to each item in the array in\nseries. The next iterator is only called once the current one has completed\nprocessing. This means the iterator functions will complete in order.\n\n\n---------------------------------------\n\n\n### forEachLimit(arr, limit, iterator, callback)\n\nThe same as forEach only the iterator is applied to batches of items in the\narray, in series. The next batch of iterators is only called once the current\none has completed processing.\n\n__Arguments__\n\n* arr - An array to iterate over.\n* limit - How many items should be in each batch.\n* iterator(item, callback) - A function to apply to each item in the array.\n The iterator is passed a callback which must be called once it has completed.\n* callback(err) - A callback which is called after all the iterator functions\n have finished, or an error has occurred.\n\n__Example__\n\n // Assume documents is an array of JSON objects and requestApi is a\n // function that interacts with a rate-limited REST api.\n\n async.forEachLimit(documents, 20, requestApi, function(err){\n // if any of the saves produced an error, err would equal that error\n });\n---------------------------------------\n\n\n### map(arr, iterator, callback)\n\nProduces a new array of values by mapping each value in the given array through\nthe iterator function. The iterator is called with an item from the array and a\ncallback for when it has finished processing. The callback takes 2 arguments, \nan error and the transformed item from the array. If the iterator passes an\nerror to this callback, the main callback for the map function is immediately\ncalled with the error.\n\nNote, that since this function applies the iterator to each item in parallel\nthere is no guarantee that the iterator functions will complete in order, however\nthe results array will be in the same order as the original array.\n\n__Arguments__\n\n* arr - An array to iterate over.\n* iterator(item, callback) - A function to apply to each item in the array.\n The iterator is passed a callback which must be called once it has completed\n with an error (which can be null) and a transformed item.\n* callback(err, results) - A callback which is called after all the iterator\n functions have finished, or an error has occurred. Results is an array of the\n transformed items from the original array.\n\n__Example__\n\n async.map(['file1','file2','file3'], fs.stat, function(err, results){\n // results is now an array of stats for each file\n });\n\n---------------------------------------\n\n\n### mapSeries(arr, iterator, callback)\n\nThe same as map only the iterator is applied to each item in the array in\nseries. The next iterator is only called once the current one has completed\nprocessing. The results array will be in the same order as the original.\n\n\n---------------------------------------\n\n\n### filter(arr, iterator, callback)\n\n__Alias:__ select\n\nReturns a new array of all the values which pass an async truth test.\n_The callback for each iterator call only accepts a single argument of true or\nfalse, it does not accept an error argument first!_ This is in-line with the\nway node libraries work with truth tests like path.exists. This operation is\nperformed in parallel, but the results array will be in the same order as the\noriginal.\n\n__Arguments__\n\n* arr - An array to iterate over.\n* iterator(item, callback) - A truth test to apply to each item in the array.\n The iterator is passed a callback which must be called once it has completed.\n* callback(results) - A callback which is called after all the iterator\n functions have finished.\n\n__Example__\n\n async.filter(['file1','file2','file3'], path.exists, function(results){\n // results now equals an array of the existing files\n });\n\n---------------------------------------\n\n\n### filterSeries(arr, iterator, callback)\n\n__alias:__ selectSeries\n\nThe same as filter only the iterator is applied to each item in the array in\nseries. The next iterator is only called once the current one has completed\nprocessing. The results array will be in the same order as the original.\n\n---------------------------------------\n\n\n### reject(arr, iterator, callback)\n\nThe opposite of filter. Removes values that pass an async truth test.\n\n---------------------------------------\n\n\n### rejectSeries(arr, iterator, callback)\n\nThe same as filter, only the iterator is applied to each item in the array\nin series.\n\n\n---------------------------------------\n\n\n### reduce(arr, memo, iterator, callback)\n\n__aliases:__ inject, foldl\n\nReduces a list of values into a single value using an async iterator to return\neach successive step. Memo is the initial state of the reduction. This\nfunction only operates in series. For performance reasons, it may make sense to\nsplit a call to this function into a parallel map, then use the normal\nArray.prototype.reduce on the results. This function is for situations where\neach step in the reduction needs to be async, if you can get the data before\nreducing it then its probably a good idea to do so.\n\n__Arguments__\n\n* arr - An array to iterate over.\n* memo - The initial state of the reduction.\n* iterator(memo, item, callback) - A function applied to each item in the\n array to produce the next step in the reduction. The iterator is passed a\n callback which accepts an optional error as its first argument, and the state\n of the reduction as the second. If an error is passed to the callback, the\n reduction is stopped and the main callback is immediately called with the\n error.\n* callback(err, result) - A callback which is called after all the iterator\n functions have finished. Result is the reduced value.\n\n__Example__\n\n async.reduce([1,2,3], 0, function(memo, item, callback){\n // pointless async:\n process.nextTick(function(){\n callback(null, memo + item)\n });\n }, function(err, result){\n // result is now equal to the last value of memo, which is 6\n });\n\n---------------------------------------\n\n\n### reduceRight(arr, memo, iterator, callback)\n\n__Alias:__ foldr\n\nSame as reduce, only operates on the items in the array in reverse order.\n\n\n---------------------------------------\n\n\n### detect(arr, iterator, callback)\n\nReturns the first value in a list that passes an async truth test. The\niterator is applied in parallel, meaning the first iterator to return true will\nfire the detect callback with that result. That means the result might not be\nthe first item in the original array (in terms of order) that passes the test.\n\nIf order within the original array is important then look at detectSeries.\n\n__Arguments__\n\n* arr - An array to iterate over.\n* iterator(item, callback) - A truth test to apply to each item in the array.\n The iterator is passed a callback which must be called once it has completed.\n* callback(result) - A callback which is called as soon as any iterator returns\n true, or after all the iterator functions have finished. Result will be\n the first item in the array that passes the truth test (iterator) or the\n value undefined if none passed.\n\n__Example__\n\n async.detect(['file1','file2','file3'], path.exists, function(result){\n // result now equals the first file in the list that exists\n });\n\n---------------------------------------\n\n\n### detectSeries(arr, iterator, callback)\n\nThe same as detect, only the iterator is applied to each item in the array\nin series. This means the result is always the first in the original array (in\nterms of array order) that passes the truth test.\n\n\n---------------------------------------\n\n\n### sortBy(arr, iterator, callback)\n\nSorts a list by the results of running each value through an async iterator.\n\n__Arguments__\n\n* arr - An array to iterate over.\n* iterator(item, callback) - A function to apply to each item in the array.\n The iterator is passed a callback which must be called once it has completed\n with an error (which can be null) and a value to use as the sort criteria.\n* callback(err, results) - A callback which is called after all the iterator\n functions have finished, or an error has occurred. Results is the items from\n the original array sorted by the values returned by the iterator calls.\n\n__Example__\n\n async.sortBy(['file1','file2','file3'], function(file, callback){\n fs.stat(file, function(err, stats){\n callback(err, stats.mtime);\n });\n }, function(err, results){\n // results is now the original array of files sorted by\n // modified date\n });\n\n\n---------------------------------------\n\n\n### some(arr, iterator, callback)\n\n__Alias:__ any\n\nReturns true if at least one element in the array satisfies an async test.\n_The callback for each iterator call only accepts a single argument of true or\nfalse, it does not accept an error argument first!_ This is in-line with the\nway node libraries work with truth tests like path.exists. Once any iterator\ncall returns true, the main callback is immediately called.\n\n__Arguments__\n\n* arr - An array to iterate over.\n* iterator(item, callback) - A truth test to apply to each item in the array.\n The iterator is passed a callback which must be called once it has completed.\n* callback(result) - A callback which is called as soon as any iterator returns\n true, or after all the iterator functions have finished. Result will be\n either true or false depending on the values of the async tests.\n\n__Example__\n\n async.some(['file1','file2','file3'], path.exists, function(result){\n // if result is true then at least one of the files exists\n });\n\n---------------------------------------\n\n\n### every(arr, iterator, callback)\n\n__Alias:__ all\n\nReturns true if every element in the array satisfies an async test.\n_The callback for each iterator call only accepts a single argument of true or\nfalse, it does not accept an error argument first!_ This is in-line with the\nway node libraries work with truth tests like path.exists.\n\n__Arguments__\n\n* arr - An array to iterate over.\n* iterator(item, callback) - A truth test to apply to each item in the array.\n The iterator is passed a callback which must be called once it has completed.\n* callback(result) - A callback which is called after all the iterator\n functions have finished. Result will be either true or false depending on\n the values of the async tests.\n\n__Example__\n\n async.every(['file1','file2','file3'], path.exists, function(result){\n // if result is true then every file exists\n });\n\n---------------------------------------\n\n\n### concat(arr, iterator, callback)\n\nApplies an iterator to each item in a list, concatenating the results. Returns the\nconcatenated list. The iterators are called in parallel, and the results are\nconcatenated as they return. There is no guarantee that the results array will\nbe returned in the original order of the arguments passed to the iterator function.\n\n__Arguments__\n\n* arr - An array to iterate over\n* iterator(item, callback) - A function to apply to each item in the array.\n The iterator is passed a callback which must be called once it has completed\n with an error (which can be null) and an array of results.\n* callback(err, results) - A callback which is called after all the iterator\n functions have finished, or an error has occurred. Results is an array containing\n the concatenated results of the iterator function.\n\n__Example__\n\n async.concat(['dir1','dir2','dir3'], fs.readdir, function(err, files){\n // files is now a list of filenames that exist in the 3 directories\n });\n\n---------------------------------------\n\n\n### concatSeries(arr, iterator, callback)\n\nSame as async.concat, but executes in series instead of parallel.\n\n\n## Control Flow\n\n\n### series(tasks, [callback])\n\nRun an array of functions in series, each one running once the previous\nfunction has completed. If any functions in the series pass an error to its\ncallback, no more functions are run and the callback for the series is\nimmediately called with the value of the error. Once the tasks have completed,\nthe results are passed to the final callback as an array.\n\nIt is also possible to use an object instead of an array. Each property will be\nrun as a function and the results will be passed to the final callback as an object\ninstead of an array. This can be a more readable way of handling results from\nasync.series.\n\n\n__Arguments__\n\n* tasks - An array or object containing functions to run, each function is passed\n a callback it must call on completion.\n* callback(err, results) - An optional callback to run once all the functions\n have completed. This function gets an array of all the arguments passed to\n the callbacks used in the array.\n\n__Example__\n\n async.series([\n function(callback){\n // do some stuff ...\n callback(null, 'one');\n },\n function(callback){\n // do some more stuff ...\n callback(null, 'two');\n },\n ],\n // optional callback\n function(err, results){\n // results is now equal to ['one', 'two']\n });\n\n\n // an example using an object instead of an array\n async.series({\n one: function(callback){\n setTimeout(function(){\n callback(null, 1);\n }, 200);\n },\n two: function(callback){\n setTimeout(function(){\n callback(null, 2);\n }, 100);\n },\n },\n function(err, results) {\n // results is now equal to: {one: 1, two: 2}\n });\n\n\n---------------------------------------\n\n\n### parallel(tasks, [callback])\n\nRun an array of functions in parallel, without waiting until the previous\nfunction has completed. If any of the functions pass an error to its\ncallback, the main callback is immediately called with the value of the error.\nOnce the tasks have completed, the results are passed to the final callback as an\narray.\n\nIt is also possible to use an object instead of an array. Each property will be\nrun as a function and the results will be passed to the final callback as an object\ninstead of an array. This can be a more readable way of handling results from\nasync.parallel.\n\n\n__Arguments__\n\n* tasks - An array or object containing functions to run, each function is passed a\n callback it must call on completion.\n* callback(err, results) - An optional callback to run once all the functions\n have completed. This function gets an array of all the arguments passed to\n the callbacks used in the array.\n\n__Example__\n\n async.parallel([\n function(callback){\n setTimeout(function(){\n callback(null, 'one');\n }, 200);\n },\n function(callback){\n setTimeout(function(){\n callback(null, 'two');\n }, 100);\n },\n ],\n // optional callback\n function(err, results){\n // the results array will equal ['one','two'] even though\n // the second function had a shorter timeout.\n });\n\n\n // an example using an object instead of an array\n async.parallel({\n one: function(callback){\n setTimeout(function(){\n callback(null, 1);\n }, 200);\n },\n two: function(callback){\n setTimeout(function(){\n callback(null, 2);\n }, 100);\n },\n },\n function(err, results) {\n // results is now equals to: {one: 1, two: 2}\n });\n\n\n---------------------------------------\n\n\n### whilst(test, fn, callback)\n\nRepeatedly call fn, while test returns true. Calls the callback when stopped,\nor an error occurs.\n\n__Arguments__\n\n* test() - synchronous truth test to perform before each execution of fn.\n* fn(callback) - A function to call each time the test passes. The function is\n passed a callback which must be called once it has completed with an optional\n error as the first argument.\n* callback(err) - A callback which is called after the test fails and repeated\n execution of fn has stopped.\n\n__Example__\n\n var count = 0;\n\n async.whilst(\n function () { return count < 5; },\n function (callback) {\n count++;\n setTimeout(callback, 1000);\n },\n function (err) {\n // 5 seconds have passed\n }\n );\n\n\n---------------------------------------\n\n\n### until(test, fn, callback)\n\nRepeatedly call fn, until test returns true. Calls the callback when stopped,\nor an error occurs.\n\nThe inverse of async.whilst.\n\n\n---------------------------------------\n\n\n### waterfall(tasks, [callback])\n\nRuns an array of functions in series, each passing their results to the next in\nthe array. However, if any of the functions pass an error to the callback, the\nnext function is not executed and the main callback is immediately called with\nthe error.\n\n__Arguments__\n\n* tasks - An array of functions to run, each function is passed a callback it\n must call on completion.\n* callback(err, [results]) - An optional callback to run once all the functions\n have completed. This will be passed the results of the last task's callback.\n\n\n\n__Example__\n\n async.waterfall([\n function(callback){\n callback(null, 'one', 'two');\n },\n function(arg1, arg2, callback){\n callback(null, 'three');\n },\n function(arg1, callback){\n // arg1 now equals 'three'\n callback(null, 'done');\n }\n ], function (err, result) {\n // result now equals 'done' \n });\n\n\n---------------------------------------\n\n\n### queue(worker, concurrency)\n\nCreates a queue object with the specified concurrency. Tasks added to the\nqueue will be processed in parallel (up to the concurrency limit). If all\nworkers are in progress, the task is queued until one is available. Once\na worker has completed a task, the task's callback is called.\n\n__Arguments__\n\n* worker(task, callback) - An asynchronous function for processing a queued\n task.\n* concurrency - An integer for determining how many worker functions should be\n run in parallel.\n\n__Queue objects__\n\nThe queue object returned by this function has the following properties and\nmethods:\n\n* length() - a function returning the number of items waiting to be processed.\n* concurrency - an integer for determining how many worker functions should be\n run in parallel. This property can be changed after a queue is created to\n alter the concurrency on-the-fly.\n* push(task, [callback]) - add a new task to the queue, the callback is called\n once the worker has finished processing the task.\n instead of a single task, an array of tasks can be submitted. the respective callback is used for every task in the list.\n* saturated - a callback that is called when the queue length hits the concurrency and further tasks will be queued\n* empty - a callback that is called when the last item from the queue is given to a worker\n* drain - a callback that is called when the last item from the queue has returned from the worker\n\n__Example__\n\n // create a queue object with concurrency 2\n\n var q = async.queue(function (task, callback) {\n console.log('hello ' + task.name);\n callback();\n }, 2);\n\n\n // assign a callback\n q.drain = function() {\n console.log('all items have been processed');\n }\n\n // add some items to the queue\n\n q.push({name: 'foo'}, function (err) {\n console.log('finished processing foo');\n });\n q.push({name: 'bar'}, function (err) {\n console.log('finished processing bar');\n });\n\n // add some items to the queue (batch-wise)\n\n q.push([{name: 'baz'},{name: 'bay'},{name: 'bax'}], function (err) {\n console.log('finished processing bar');\n });\n\n\n---------------------------------------\n\n\n### auto(tasks, [callback])\n\nDetermines the best order for running functions based on their requirements.\nEach function can optionally depend on other functions being completed first,\nand each function is run as soon as its requirements are satisfied. If any of\nthe functions pass an error to their callback, that function will not complete\n(so any other functions depending on it will not run) and the main callback\nwill be called immediately with the error. Functions also receive an object\ncontaining the results of functions which have completed so far.\n\n__Arguments__\n\n* tasks - An object literal containing named functions or an array of\n requirements, with the function itself the last item in the array. The key\n used for each function or array is used when specifying requirements. The\n syntax is easier to understand by looking at the example.\n* callback(err, results) - An optional callback which is called when all the\n tasks have been completed. The callback will receive an error as an argument\n if any tasks pass an error to their callback. If all tasks complete\n successfully, it will receive an object containing their results.\n\n__Example__\n\n async.auto({\n get_data: function(callback){\n // async code to get some data\n },\n make_folder: function(callback){\n // async code to create a directory to store a file in\n // this is run at the same time as getting the data\n },\n write_file: ['get_data', 'make_folder', function(callback){\n // once there is some data and the directory exists,\n // write the data to a file in the directory\n callback(null, filename);\n }],\n email_link: ['write_file', function(callback, results){\n // once the file is written let's email a link to it...\n // results.write_file contains the filename returned by write_file.\n }]\n });\n\nThis is a fairly trivial example, but to do this using the basic parallel and\nseries functions would look like this:\n\n async.parallel([\n function(callback){\n // async code to get some data\n },\n function(callback){\n // async code to create a directory to store a file in\n // this is run at the same time as getting the data\n }\n ],\n function(results){\n async.series([\n function(callback){\n // once there is some data and the directory exists,\n // write the data to a file in the directory\n },\n email_link: function(callback){\n // once the file is written let's email a link to it...\n }\n ]);\n });\n\nFor a complicated series of async tasks using the auto function makes adding\nnew tasks much easier and makes the code more readable.\n\n\n---------------------------------------\n\n\n### iterator(tasks)\n\nCreates an iterator function which calls the next function in the array,\nreturning a continuation to call the next one after that. Its also possible to\n'peek' the next iterator by doing iterator.next().\n\nThis function is used internally by the async module but can be useful when\nyou want to manually control the flow of functions in series.\n\n__Arguments__\n\n* tasks - An array of functions to run, each function is passed a callback it\n must call on completion.\n\n__Example__\n\n var iterator = async.iterator([\n function(){ sys.p('one'); },\n function(){ sys.p('two'); },\n function(){ sys.p('three'); }\n ]);\n\n node> var iterator2 = iterator();\n 'one'\n node> var iterator3 = iterator2();\n 'two'\n node> iterator3();\n 'three'\n node> var nextfn = iterator2.next();\n node> nextfn();\n 'three'\n\n\n---------------------------------------\n\n\n### apply(function, arguments..)\n\nCreates a continuation function with some arguments already applied, a useful\nshorthand when combined with other control flow functions. Any arguments\npassed to the returned function are added to the arguments originally passed\nto apply.\n\n__Arguments__\n\n* function - The function you want to eventually apply all arguments to.\n* arguments... - Any number of arguments to automatically apply when the\n continuation is called.\n\n__Example__\n\n // using apply\n\n async.parallel([\n async.apply(fs.writeFile, 'testfile1', 'test1'),\n async.apply(fs.writeFile, 'testfile2', 'test2'),\n ]);\n\n\n // the same process without using apply\n\n async.parallel([\n function(callback){\n fs.writeFile('testfile1', 'test1', callback);\n },\n function(callback){\n fs.writeFile('testfile2', 'test2', callback);\n },\n ]);\n\nIt's possible to pass any number of additional arguments when calling the\ncontinuation:\n\n node> var fn = async.apply(sys.puts, 'one');\n node> fn('two', 'three');\n one\n two\n three\n\n---------------------------------------\n\n\n### nextTick(callback)\n\nCalls the callback on a later loop around the event loop. In node.js this just\ncalls process.nextTick, in the browser it falls back to setTimeout(callback, 0),\nwhich means other higher priority events may precede the execution of the callback.\n\nThis is used internally for browser-compatibility purposes.\n\n__Arguments__\n\n* callback - The function to call on a later loop around the event loop.\n\n__Example__\n\n var call_order = [];\n async.nextTick(function(){\n call_order.push('two');\n // call_order now equals ['one','two]\n });\n call_order.push('one')\n\n\n## Utils\n\n\n### memoize(fn, [hasher])\n\nCaches the results of an async function. When creating a hash to store function\nresults against, the callback is omitted from the hash and an optional hash\nfunction can be used.\n\n__Arguments__\n\n* fn - the function you to proxy and cache results from.\n* hasher - an optional function for generating a custom hash for storing\n results, it has all the arguments applied to it apart from the callback, and\n must be synchronous.\n\n__Example__\n\n var slow_fn = function (name, callback) {\n // do something\n callback(null, result);\n };\n var fn = async.memoize(slow_fn);\n\n // fn can now be used as if it were slow_fn\n fn('some name', function () {\n // callback\n });\n\n\n### unmemoize(fn)\n\nUndoes a memoized function, reverting it to the original, unmemoized\nform. Comes handy in tests.\n\n__Arguments__\n\n* fn - the memoized function\n\n\n### log(function, arguments)\n\nLogs the result of an async function to the console. Only works in node.js or\nin browsers that support console.log and console.error (such as FF and Chrome).\nIf multiple arguments are returned from the async function, console.log is\ncalled on each argument in order.\n\n__Arguments__\n\n* function - The function you want to eventually apply all arguments to.\n* arguments... - Any number of arguments to apply to the function.\n\n__Example__\n\n var hello = function(name, callback){\n setTimeout(function(){\n callback(null, 'hello ' + name);\n }, 1000);\n };\n\n node> async.log(hello, 'world');\n 'hello world'\n\n\n---------------------------------------\n\n\n### dir(function, arguments)\n\nLogs the result of an async function to the console using console.dir to\ndisplay the properties of the resulting object. Only works in node.js or\nin browsers that support console.dir and console.error (such as FF and Chrome).\nIf multiple arguments are returned from the async function, console.dir is\ncalled on each argument in order.\n\n__Arguments__\n\n* function - The function you want to eventually apply all arguments to.\n* arguments... - Any number of arguments to apply to the function.\n\n__Example__\n\n var hello = function(name, callback){\n setTimeout(function(){\n callback(null, {hello: name});\n }, 1000);\n };\n\n node> async.dir(hello, 'world');\n {hello: 'world'}\n\n\n---------------------------------------\n\n\n### noConflict()\n\nChanges the value of async back to its original value, returning a reference to the\nasync object.\n", + "readmeFilename": "README.md", + "homepage": "https://github.com/caolan/async", + "_id": "async@0.1.22", + "_shasum": "0fc1aaa088a0e3ef0ebe2d8831bab0dcf8845061", + "_from": "async@0.1.22", + "_resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "scripts": {} +} diff --git a/third_party/jsdoc/node_modules/catharsis/LICENSE b/third_party/jsdoc/node_modules/catharsis/LICENSE new file mode 100644 index 0000000000..6fdbb085fb --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/LICENSE @@ -0,0 +1,17 @@ +Copyright (c) 2014 Google Inc. +Copyright (c) 2012-2014 Jeff Williams + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES +OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/third_party/jsdoc/node_modules/catharsis/catharsis.js b/third_party/jsdoc/node_modules/catharsis/catharsis.js new file mode 100644 index 0000000000..7899f3c236 --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/catharsis.js @@ -0,0 +1,166 @@ +/** + * Catharsis + * A parser for Google Closure Compiler type expressions, powered by PEG.js. + * + * @author Jeff Williams + * @license MIT License + */ + +'use strict'; + +var describe = require('./lib/describe'); +var parse = require('./lib/parser').parse; +var stringify = require('./lib/stringify'); + +var typeExpressionCache = { + normal: {}, + jsdoc: {} +}; + +var parsedTypeCache = { + normal: {}, + htmlSafe: {} +}; + +var descriptionCache = { + normal: {} +}; + +function getTypeExpressionCache(options) { + if (options.useCache === false) { + return null; + } else if (options.jsdoc === true) { + return typeExpressionCache.jsdoc; + } else { + return typeExpressionCache.normal; + } +} + +function getParsedTypeCache(options) { + if (options.useCache === false || options.links !== null || options.links !== undefined) { + return null; + } else if (options.htmlSafe === true) { + return parsedTypeCache.htmlSafe; + } else { + return parsedTypeCache.normal; + } +} + +function getDescriptionCache(options) { + if (options.useCache === false || options.links !== null || options.links !== undefined) { + return null; + } else { + return descriptionCache.normal; + } +} + +// can't return the original if any of the following are true: +// 1. restringification was requested +// 2. htmlSafe option was requested +// 3. links option was provided +// 4. typeExpression property is missing +function canReturnOriginalExpression(parsedType, options) { + return options.restringify !== true && options.htmlSafe !== true && + (options.links === null || options.links === undefined) && + Object.prototype.hasOwnProperty.call(parsedType, 'typeExpression'); +} + +// Add non-enumerable properties to a result object, then freeze it. +function prepareFrozenObject(obj, expr, options) { + Object.defineProperty(obj, 'jsdoc', { + value: options.jsdoc === true ? true : false + }); + + if (expr) { + Object.defineProperty(obj, 'typeExpression', { + value: expr + }); + } + + return Object.freeze(obj); +} + +function cachedParse(expr, options) { + var cache = getTypeExpressionCache(options); + var parsedType; + + if (cache && Object.prototype.hasOwnProperty.call(cache, expr)) { + return cache[expr]; + } else { + parsedType = parse(expr, options); + parsedType = prepareFrozenObject(parsedType, expr, options); + + if (cache) { + cache[expr] = parsedType; + } + + return parsedType; + } +} + +function cachedStringify(parsedType, options) { + var cache = getParsedTypeCache(options); + var json; + + if (canReturnOriginalExpression(parsedType, options)) { + return parsedType.typeExpression; + } else if (cache) { + json = JSON.stringify(parsedType); + cache[json] = cache[json] || stringify(parsedType, options); + return cache[json]; + } else { + return stringify(parsedType, options); + } +} + +function cachedDescribe(parsedType, options) { + var cache = getDescriptionCache(options); + var json; + var result; + + if (cache) { + json = JSON.stringify(parsedType); + cache[json] = cache[json] || describe(parsedType, options); + return cache[json]; + } else { + result = describe(parsedType, options); + result = prepareFrozenObject(result, null, options); + + return result; + } +} + +function Catharsis() { + this.Types = require('./lib/types'); +} + +Catharsis.prototype.parse = function(typeExpr, options) { + options = options || {}; + + typeExpr = typeExpr.replace(/[\r\n]/g, '') + .replace(/\s+/g, ' ') + .trim(); + + return cachedParse(typeExpr, options); +}; + +Catharsis.prototype.stringify = function(parsedType, options) { + var result; + + options = options || {}; + + result = cachedStringify(parsedType, options); + if (options.validate) { + this.parse(result, options); + } + + return result; +}; + +Catharsis.prototype.describe = function(parsedType, options) { + options = options || {}; + + return cachedDescribe(parsedType, options); +}; + +module.exports = new Catharsis(); diff --git a/third_party/jsdoc/node_modules/catharsis/lib/describe.js b/third_party/jsdoc/node_modules/catharsis/lib/describe.js new file mode 100644 index 0000000000..eeb777b0c6 --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/lib/describe.js @@ -0,0 +1,532 @@ +'use strict'; + +var _ = require('underscore-contrib'); +var fs = require('fs'); +var path = require('path'); +var stringify = require('./stringify'); +var Types = require('./types'); +var util = require('util'); + +var DEFAULT_OPTIONS = { + language: 'en', + resources: { + en: JSON.parse(fs.readFileSync(path.join(__dirname, '../res/en.json'), 'utf8')) + } +}; + +// order matters for these! +var FUNCTION_DETAILS = ['new', 'this']; +var FUNCTION_DETAILS_VARIABLES = ['functionNew', 'functionThis']; +var MODIFIERS = ['optional', 'nullable', 'repeatable']; + +var TEMPLATE_VARIABLES = [ + 'application', + 'codeTagClose', + 'codeTagOpen', + 'element', + 'field', + 'functionNew', + 'functionParams', + 'functionReturns', + 'functionThis', + 'keyApplication', + 'name', + 'nullable', + 'optional', + 'param', + 'prefix', + 'repeatable', + 'suffix', + 'type' +]; + +var FORMATS = { + EXTENDED: 'extended', + SIMPLE: 'simple' +}; + +function makeTagOpen(codeTag, codeClass) { + var tagOpen = ''; + var tags = codeTag ? codeTag.split(' ') : []; + + tags.forEach(function(tag) { + var tagClass = codeClass ? util.format(' class="%s"', codeClass) : ''; + tagOpen += util.format('<%s%s>', tag, tagClass); + }); + + return tagOpen; +} + +function makeTagClose(codeTag) { + var tagClose = ''; + var tags = codeTag ? codeTag.split(' ') : []; + + tags.reverse(); + tags.forEach(function(tag) { + tagClose += util.format('', tag); + }); + + return tagClose; +} + +function Result() { + this.description = ''; + this.modifiers = { + functionNew: '', + functionThis: '', + optional: '', + nullable: '', + repeatable: '' + }; + this.returns = ''; +} + +function Context(props) { + var self = this; + + props = props || {}; + + TEMPLATE_VARIABLES.forEach(function(variable) { + self[variable] = props[variable] || ''; + }); +} + +function Describer(opts) { + var options; + + this._useLongFormat = true; + options = this._options = _.defaults(opts || {}, DEFAULT_OPTIONS); + this._stringifyOptions = _.defaults(options, {_ignoreModifiers: true}); + + // use a dictionary, not a Context object, so we can more easily merge this into Context objects + this._i18nContext = { + codeTagClose: makeTagClose(options.codeTag), + codeTagOpen: makeTagOpen(options.codeTag, options.codeClass) + }; + + // templates start out as strings; we lazily replace them with template functions + this._templates = options.resources[options.language]; + if (!this._templates) { + throw new Error('I18N resources are not available for the language ' + options.language); + } +} + +function modifierKind(useLongFormat) { + return useLongFormat ? FORMATS.EXTENDED : FORMATS.SIMPLE; +} + +function buildModifierStrings(describer, modifiers, type, useLongFormat) { + var modifierStrings = {}; + var result = {}; + + modifiers.forEach(function(modifier) { + var key = modifierKind(useLongFormat); + var modifierStrings = describer[modifier](type[modifier]); + + result[modifier] = modifierStrings[key]; + }); + + return result; +} + +function addModifiers(describer, context, result, type, useLongFormat) { + var keyPrefix = 'modifiers.' + modifierKind(useLongFormat); + var modifiers = buildModifierStrings(describer, MODIFIERS, type, useLongFormat); + + MODIFIERS.forEach(function(modifier) { + var modifierText = modifiers[modifier] || ''; + + result.modifiers[modifier] = modifierText; + if (!useLongFormat) { + context[modifier] = modifierText; + } + }); + + context.prefix = describer._translate(keyPrefix + '.prefix', context); + context.suffix = describer._translate(keyPrefix + '.suffix', context); +} + +function addFunctionModifiers(describer, context, result, type, useLongFormat) { + var functionDetails = buildModifierStrings(describer, FUNCTION_DETAILS, type, useLongFormat); + var kind = modifierKind(useLongFormat); + var strings = []; + + FUNCTION_DETAILS.forEach(function(functionDetail, i) { + var functionExtraInfo = functionDetails[functionDetail] || ''; + var functionDetailsVariable = FUNCTION_DETAILS_VARIABLES[i]; + + result.modifiers[functionDetailsVariable] = functionExtraInfo; + if (!useLongFormat) { + context[functionDetailsVariable] += functionExtraInfo; + } + }); +} + +// Replace 2+ whitespace characters with a single whitespace character. +function collapseSpaces(string) { + return string.replace(/(\s)+/g, '$1'); +} + +Describer.prototype._stringify = function(type, typeString, useLongFormat) { + var context = new Context({ + type: typeString || stringify(type, this._stringifyOptions) + }); + var result = new Result(); + + addModifiers(this, context, result, type, useLongFormat); + result.description = this._translate('type', context).trim(); + + return result; +}; + +Describer.prototype._translate = function(key, context) { + var result; + var templateFunction = _.getPath(this._templates, key); + + context = context || new Context(); + + if (templateFunction === undefined) { + throw new Error(util.format('The template %s does not exist for the language %s', key, + this._options.language)); + } + + // compile and cache the template function if necessary + if (typeof templateFunction === 'string') { + // force the templates to use the `context` object + templateFunction = templateFunction.replace(/\<\%\= /g, '<%= context.'); + templateFunction = _.template(templateFunction, null, {variable: 'context'}); + _.setPath(this._templates, templateFunction, key); + } + + result = (templateFunction(_.extend(context, this._i18nContext)) || '') + // strip leading spaces + .replace(/^\s+/, ''); + result = collapseSpaces(result); + + return result; +}; + +Describer.prototype._modifierHelper = function(key, modifierPrefix, context) { + modifierPrefix = modifierPrefix || ''; + + return { + extended: key ? + this._translate(util.format('%s.%s.%s', modifierPrefix, FORMATS.EXTENDED, key), + context) : + '', + simple: key ? + this._translate(util.format('%s.%s.%s', modifierPrefix, FORMATS.SIMPLE, key), context) : + '' + }; +}; + +Describer.prototype._translateModifier = function(key, context) { + return this._modifierHelper(key, 'modifiers', context); +}; + +Describer.prototype._translateFunctionModifier = function(key, context) { + return this._modifierHelper(key, 'function', context); +}; + +function getApplicationKey(applications) { + if (applications.length === 1) { + return 'array'; + } else if (/[Ss]tring/.test(applications[0].name)) { + // object with string keys + return 'object'; + } else { + // object with non-string keys + return 'objectNonString'; + } +} + +Describer.prototype.application = function(type, useLongFormat) { + var applications = type.applications.slice(0); + var context = new Context(); + var key = 'application.' + getApplicationKey(applications); + var result = new Result(); + var self = this; + + addModifiers(this, context, result, type, useLongFormat); + + context.application = this.type(applications.pop()).description; + context.keyApplication = applications.length ? this.type(applications.pop()).description : ''; + + result.description = this._translate(key, context).trim(); + + return result; +}; + +function reduceMultiple(context, keyName, contextName, translate, previous, current, index, items) { + var key = + index === 0 ? '.first.many' : + index === (items.length - 1) ? '.last.many' : + '.middle.many'; + + key = keyName + key; + context[contextName] = items[index]; + + return previous + translate(key, context); +} + +Describer.prototype.elements = function(type, useLongFormat) { + var context = new Context(); + var items = type.elements.slice(0); + var result = new Result(); + + addModifiers(this, context, result, type, useLongFormat); + result.description = this._combineMultiple(items, context, 'union', 'element', useLongFormat); + + return result; +}; + +Describer.prototype['new'] = function(funcNew) { + var context = new Context({'functionNew': this.type(funcNew).description}); + var key = funcNew ? 'new' : ''; + + return this._translateFunctionModifier(key, context); +}; + +Describer.prototype.nullable = function(nullable) { + var key = nullable === true ? 'nullable' : + nullable === false ? 'nonNullable' : + ''; + + return this._translateModifier(key); +}; + +Describer.prototype.optional = function(optional) { + var key = (optional === true) ? 'optional' : ''; + + return this._translateModifier(key); +}; + +Describer.prototype.repeatable = function(repeatable) { + var key = (repeatable === true) ? 'repeatable' : ''; + + return this._translateModifier(key); +}; + +Describer.prototype._combineMultiple = function(items, context, keyName, contextName, + useLongFormat) { + var result = new Result(); + var self = this; + var strings; + + strings = typeof items[0] === 'string' ? + items.slice(0) : + items.map(function(item) { + return self.type(item).description; + }); + + switch(strings.length) { + case 0: + // falls through + case 1: + context[contextName] = strings[0] || ''; + result.description = this._translate(keyName + '.first.one', context); + break; + case 2: + strings.forEach(function(item, idx) { + var key = keyName + (idx === 0 ? '.first' : '.last' ) + '.two'; + + context[contextName] = item; + result.description += self._translate(key, context); + }); + break; + default: + result.description = strings.reduce(reduceMultiple.bind(null, context, keyName, + contextName, this._translate.bind(this)), ''); + } + + return result.description.trim(); +}; + +Describer.prototype.params = function(params, functionContext) { + var context = new Context(); + var result = new Result(); + var self = this; + var strings; + + // TODO: this hardcodes the order and placement of functionNew and functionThis; need to move + // this to the template (and also track whether to put a comma after the last modifier) + functionContext = functionContext || {}; + params = params || []; + strings = params.map(function(param) { + return self.type(param).description; + }); + + if (functionContext.functionThis) { + strings.unshift(functionContext.functionThis); + } + if (functionContext.functionNew) { + strings.unshift(functionContext.functionNew); + } + result.description = this._combineMultiple(strings, context, 'params', 'param', false); + + return result; +}; + +Describer.prototype['this'] = function(funcThis) { + var context = new Context({'functionThis': this.type(funcThis).description}); + var key = funcThis ? 'this' : ''; + + return this._translateFunctionModifier(key, context); +}; + +Describer.prototype.type = function(type, useLongFormat) { + var result = new Result(); + + if (useLongFormat === undefined) { + useLongFormat = this._useLongFormat; + } + // ensure we don't use the long format for inner types + this._useLongFormat = false; + + if (!type) { + return result; + } + + switch(type.type) { + case Types.AllLiteral: + result = this._stringify(type, this._translate('all'), useLongFormat); + break; + case Types.FunctionType: + result = this._signature(type, useLongFormat); + break; + case Types.NameExpression: + result = this._stringify(type, null, useLongFormat); + break; + case Types.NullLiteral: + result = this._stringify(type, this._translate('null'), useLongFormat); + break; + case Types.RecordType: + result = this._record(type, useLongFormat); + break; + case Types.TypeApplication: + result = this.application(type, useLongFormat); + break; + case Types.TypeUnion: + result = this.elements(type, useLongFormat); + break; + case Types.UndefinedLiteral: + result = this._stringify(type, this._translate('undefined'), useLongFormat); + break; + case Types.UnknownLiteral: + result = this._stringify(type, this._translate('unknown'), useLongFormat); + break; + default: + throw new Error('Unknown type: ' + JSON.stringify(type)); + } + + return result; +}; + +Describer.prototype._record = function(type, useLongFormat) { + var context = new Context(); + var items; + var result = new Result(); + + items = this._recordFields(type.fields); + + addModifiers(this, context, result, type, useLongFormat); + result.description = this._combineMultiple(items, context, 'record', 'field', useLongFormat); + + return result; +}; + +Describer.prototype._recordFields = function(fields) { + var context = new Context(); + var result = []; + var self = this; + + if (!fields.length) { + return result; + } + + result = fields.map(function(field) { + var key = 'field.' + (field.value ? 'typed' : 'untyped'); + + context.name = self.type(field.key).description; + if (field.value) { + context.type = self.type(field.value).description; + } + + return self._translate(key, context); + }); + + return result; +}; + +Describer.prototype._addLinks = function(nameString) { + var linkClass = ''; + var options = this._options; + var result = nameString; + + + if (options.links && Object.prototype.hasOwnProperty.call(options.links, nameString)) { + if (options.linkClass) { + linkClass = util.format(' class="%s"', options.linkClass); + } + + nameString = util.format('%s', options.links[nameString], linkClass, + nameString); + } + + return nameString; +}; + +Describer.prototype.result = function(type, useLongFormat) { + var context = new Context(); + var description; + var key = 'function.' + modifierKind(useLongFormat) + '.returns'; + var result = new Result(); + + context.type = this.type(type).description; + + addModifiers(this, context, result, type, useLongFormat); + result.description = this._translate(key, context); + + return result; +}; + +Describer.prototype._signature = function(type, useLongFormat) { + var context = new Context(); + var functionModifiers; + var kind = modifierKind(useLongFormat); + var result = new Result(); + var returns; + var self = this; + + addModifiers(this, context, result, type, useLongFormat); + addFunctionModifiers(this, context, result, type, useLongFormat); + + context.functionParams = this.params(type.params || [], context).description; + + if (type.result) { + returns = this.result(type.result, useLongFormat); + if (useLongFormat) { + result.returns = returns.description; + } else { + context.functionReturns = returns.description; + } + } + + result.description += this._translate('function.' + kind + '.signature', context).trim(); + + return result; +}; + +module.exports = function(type, options) { + var simple = new Describer(options).type(type, false); + var extended = new Describer(options).type(type); + + [simple, extended].forEach(function(result) { + result.description = collapseSpaces(result.description.trim()); + }); + + return { + simple: simple.description, + extended: extended + }; +}; diff --git a/third_party/jsdoc/node_modules/catharsis/lib/parser.js b/third_party/jsdoc/node_modules/catharsis/lib/parser.js new file mode 100644 index 0000000000..dab624b40c --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/lib/parser.js @@ -0,0 +1,4 @@ +module.exports=function(){function peg$subclass(child,parent){function ctor(){this.constructor=child}ctor.prototype=parent.prototype;child.prototype=new ctor}function SyntaxError(expected,found,offset,line,column){function buildMessage(expected,found){function stringEscape(s){function hex(ch){return ch.charCodeAt(0).toString(16).toUpperCase()}return s.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\x08/g,"\\b").replace(/\t/g,"\\t").replace(/\n/g,"\\n").replace(/\f/g,"\\f").replace(/\r/g,"\\r").replace(/[\x00-\x07\x0B\x0E\x0F]/g,function(ch){return"\\x0"+hex(ch)}).replace(/[\x10-\x1F\x80-\xFF]/g,function(ch){return"\\x"+hex(ch)}).replace(/[\u0180-\u0FFF]/g,function(ch){return"\\u0"+hex(ch)}).replace(/[\u1080-\uFFFF]/g,function(ch){return"\\u"+hex(ch)})}var expectedDesc,foundDesc;switch(expected.length){case 0:expectedDesc="end of input";break;case 1:expectedDesc=expected[0];break;default:expectedDesc=expected.slice(0,-1).join(", ")+" or "+expected[expected.length-1]}foundDesc=found?'"'+stringEscape(found)+'"':"end of input";return"Expected "+expectedDesc+" but "+foundDesc+" found."}this.expected=expected;this.found=found;this.offset=offset;this.line=line;this.column=column;this.name="SyntaxError";this.message=buildMessage(expected,found)}peg$subclass(SyntaxError,Error);function parse(input){var options=arguments.length>1?arguments[1]:{},peg$startRuleFunctions={TypeExpression:peg$parseTypeExpression},peg$startRuleFunction=peg$parseTypeExpression,peg$c0=null,peg$c1="",peg$c2=function(r,unk){var result=unk;if(r.repeatable){result=repeatable(result)}return result},peg$c3="?",peg$c4='"?"',peg$c5="!",peg$c6='"!"',peg$c7=function(r,prefix,expr){var result=expr;if(r.repeatable){result=repeatable(result)}return nullable(result,prefix)},peg$c8=function(expr,postfix){return nullable(expr,postfix)},peg$c9=function(prefix,expr){return nullable(expr,prefix)},peg$c10=function(expr){return repeatable(expr)},peg$c11=function(lit,opt){var result=lit;if(opt.optional){result.optional=true}return result},peg$c12=function(lit){return repeatable(lit)},peg$c13="*",peg$c14='"*"',peg$c15=function(){return{type:Types.AllLiteral}},peg$c16=function(){return{type:Types.NullLiteral}},peg$c17=function(){return{type:Types.UndefinedLiteral}},peg$c18="...",peg$c19='"..."',peg$c20=function(){return{repeatable:true}},peg$c21="=",peg$c22='"="',peg$c23=function(){return{optional:true}},peg$c24="[]",peg$c25='"[]"',peg$c26=function(name){var result;if(!options.jsdoc){return null}result={type:Types.TypeApplication,expression:{type:Types.NameExpression,name:"Array"},applications:[name]};result.applications[0].type=Types.NameExpression;return result},peg$c27=function(exp,appl,opt){var result={};var nameExp={type:Types.NameExpression,name:exp.name};if(appl.length){result.type=Types.TypeApplication;result.expression=nameExp;result.applications=appl}else{result=nameExp}if(opt.optional){result.optional=true}return result},peg$c28=function(name){if(!options.jsdoc){return null}return name},peg$c29=function(t){return repeatable(t)},peg$c30=function(exp,opt){var result={type:Types.NameExpression,name:exp.name,reservedWord:true};if(opt.optional){result.optional=true}return result},peg$c31=".",peg$c32='"."',peg$c33="<",peg$c34='"<"',peg$c35=">",peg$c36='">"',peg$c37=function(sep,l){return l},peg$c38=[],peg$c39=",",peg$c40='","',peg$c41=function(expr,list){var result=[expr];for(var i=0,l=list.length;ipos){peg$cachedPos=0;peg$cachedPosDetails={line:1,column:1,seenCR:false}}peg$cachedPos=pos;advance(peg$cachedPosDetails,peg$cachedPos)}return peg$cachedPosDetails}function peg$fail(expected){if(peg$currPospeg$maxFailPos){peg$maxFailPos=peg$currPos;peg$maxFailExpected=[]}peg$maxFailExpected.push(expected)}function peg$cleanupExpected(expected){var i=0;expected.sort();while(ipeg$currPos){s0=input.charAt(peg$currPos);peg$currPos++}else{s0=null;if(peg$silentFails===0){peg$fail(peg$c281)}}return s0}function peg$parseHexEscapeSequence(){var s0,s1,s2,s3,s4,s5;s0=peg$currPos;if(input.charCodeAt(peg$currPos)===120){s1=peg$c279;peg$currPos++}else{s1=null;if(peg$silentFails===0){peg$fail(peg$c280)}}if(s1!==null){s2=peg$currPos;s3=peg$currPos;s4=peg$parseHexDigit();if(s4!==null){s5=peg$parseHexDigit();if(s5!==null){s4=[s4,s5];s3=s4}else{peg$currPos=s3;s3=peg$c0}}else{peg$currPos=s3;s3=peg$c0}if(s3!==null){s3=input.substring(s2,peg$currPos)}s2=s3;if(s2!==null){peg$reportedPos=s0;s1=peg$c227(s2);if(s1===null){peg$currPos=s0;s0=s1}else{s0=s1}}else{peg$currPos=s0;s0=peg$c0}}else{peg$currPos=s0;s0=peg$c0}return s0}function peg$parseLineContinuation(){var s0,s1,s2;s0=peg$currPos;if(input.charCodeAt(peg$currPos)===92){s1=peg$c223;peg$currPos++}else{s1=null;if(peg$silentFails===0){peg$fail(peg$c224)}}if(s1!==null){s2=peg$parseLineTerminatorSequence();if(s2!==null){peg$reportedPos=s0;s1=peg$c265(s2);if(s1===null){peg$currPos=s0;s0=s1}else{s0=s1}}else{peg$currPos=s0;s0=peg$c0}}else{peg$currPos=s0;s0=peg$c0}return s0}function peg$parse_(){var s0,s1; +peg$silentFails++;s0=[];s1=peg$parseWhitespace();while(s1!==null){s0.push(s1);s1=peg$parseWhitespace()}peg$silentFails--;if(s0===null){s1=null;if(peg$silentFails===0){peg$fail(peg$c282)}}return s0}function peg$parse__(){var s0,s1;peg$silentFails++;s0=peg$c1;peg$silentFails--;if(s0===null){s1=null;if(peg$silentFails===0){peg$fail(peg$c283)}}return s0}function peg$parseWhitespace(){var s0;if(peg$c284.test(input.charAt(peg$currPos))){s0=input.charAt(peg$currPos);peg$currPos++}else{s0=null;if(peg$silentFails===0){peg$fail(peg$c285)}}if(s0===null){s0=peg$parseUnicodeZs()}return s0}var Types=require("./types");function repeatable(obj){obj.repeatable=true;return obj}function nullable(obj,modifier){if(modifier){obj.nullable=modifier==="?"?true:false}return obj}peg$result=peg$startRuleFunction();if(peg$result!==null&&peg$currPos===input.length){return peg$result}else{peg$cleanupExpected(peg$maxFailExpected);peg$reportedPos=Math.max(peg$currPos,peg$maxFailPos);throw new SyntaxError(peg$maxFailExpected,peg$reportedPos'; + + return result; +}; + +Stringifier.prototype.elements = function(elements) { + var result = ''; + var strings = []; + + if (!elements) { + return result; + } + + for (var i = 0, l = elements.length; i < l; i++) { + strings.push(this.type(elements[i])); + } + + result = '(' + strings.join('|') + ')'; + + return result; +}; + +Stringifier.prototype.name = function(name) { + return name || ''; +}; + +Stringifier.prototype['new'] = function(funcNew) { + return funcNew ? 'new:' + this.type(funcNew) : ''; +}; + +Stringifier.prototype.nullable = function(nullable) { + switch (nullable) { + case true: + return '?'; + case false: + return '!'; + default: + return ''; + } +}; + +Stringifier.prototype.optional = function(optional) { + if (optional === true) { + return '='; + } else { + return ''; + } +}; + +Stringifier.prototype.params = function(params) { + var result = ''; + var strings = []; + + if (!params || params.length === 0) { + return result; + } + + for (var i = 0, l = params.length; i < l; i++) { + strings.push(this.type(params[i])); + } + + result = strings.join(', '); + + return result; +}; + +Stringifier.prototype.result = function(result) { + return result ? ': ' + this.type(result) : ''; +}; + +Stringifier.prototype['this'] = function(funcThis) { + return funcThis ? 'this:' + this.type(funcThis) : ''; +}; + +Stringifier.prototype.type = function(type) { + var typeString = ''; + + if (!type) { + return typeString; + } + + switch(type.type) { + case Types.AllLiteral: + typeString = this._formatNameAndType(type, '*'); + break; + case Types.FunctionType: + typeString = this._signature(type); + break; + case Types.NullLiteral: + typeString = this._formatNameAndType(type, 'null'); + break; + case Types.RecordType: + typeString = this._record(type); + break; + case Types.TypeApplication: + typeString = this.type(type.expression) + this.applications(type.applications); + break; + case Types.UndefinedLiteral: + typeString = this._formatNameAndType(type, 'undefined'); + break; + case Types.TypeUnion: + typeString = this.elements(type.elements); + break; + case Types.UnknownLiteral: + typeString = this._formatNameAndType(type, '?'); + break; + default: + typeString = this._formatNameAndType(type); + } + + // add optional/nullable/repeatable modifiers + if (!this._options._ignoreModifiers) { + typeString = this._addModifiers(type, typeString); + } + + return typeString; +}; + +Stringifier.prototype.stringify = Stringifier.prototype.type; + +Stringifier.prototype.key = Stringifier.prototype.type; + +Stringifier.prototype._record = function(type) { + var fields = this._recordFields(type.fields); + + return '{' + fields.join(', ') + '}'; +}; + +Stringifier.prototype._recordFields = function(fields) { + var field; + var keyAndValue; + + var result = []; + + if (!fields) { + return result; + } + + for (var i = 0, l = fields.length; i < l; i++) { + field = fields[i]; + + keyAndValue = this.key(field.key); + keyAndValue += field.value ? ': ' + this.type(field.value) : ''; + + result.push(keyAndValue); + } + + return result; +}; + +function combineNameAndType(nameString, typeString) { + var separator = (nameString && typeString) ? ':' : ''; + + return nameString + separator + typeString; +} + +// Adds optional, nullable, and repeatable modifiers if necessary. +Stringifier.prototype._addModifiers = function(type, typeString) { + var combined; + + var open = ''; + var close = ''; + var optional = ''; + + if (type.repeatable) { + open = this._inFunctionSignatureParams ? '...[' : '...'; + close = this._inFunctionSignatureParams ? ']' : ''; + } + + combined = this.nullable(type.nullable) + combineNameAndType('', typeString); + optional = this.optional(type.optional); + + return open + combined + close + optional; +}; + +Stringifier.prototype._addLinks = function(nameString) { + var openTag; + + var linkClass = ''; + var options = this._options; + + if (options.links && Object.prototype.hasOwnProperty.call(options.links, nameString)) { + if (options.linkClass) { + linkClass = ' class="' + options.linkClass + '"'; + } + + openTag = ''; + nameString = openTag + nameString + ''; + } + + return nameString; +}; + +Stringifier.prototype._formatNameAndType = function(type, literal) { + var nameString = type.name || literal || ''; + var typeString = type.type ? this.type(type.type) : ''; + + nameString = this._addLinks(nameString); + + return combineNameAndType(nameString, typeString); +}; + +Stringifier.prototype._signature = function(type) { + var param; + var prop; + var signature; + + var params = []; + // these go within the signature's parens, in this order + var props = [ + 'new', + 'this', + 'params' + ]; + + this._inFunctionSignatureParams = true; + for (var i = 0, l = props.length; i < l; i++) { + prop = props[i]; + param = this[prop](type[prop]); + if (param.length > 0) { + params.push(param); + } + } + this._inFunctionSignatureParams = false; + + signature = 'function(' + params.join(', ') + ')'; + signature += this.result(type.result); + + return signature; +}; + + +module.exports = function(type, options) { + return new Stringifier(options).stringify(type); +}; diff --git a/third_party/jsdoc/node_modules/catharsis/lib/types.js b/third_party/jsdoc/node_modules/catharsis/lib/types.js new file mode 100644 index 0000000000..017aba6b5f --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/lib/types.js @@ -0,0 +1,24 @@ +'use strict'; + +module.exports = Object.freeze({ + // `*` + AllLiteral: 'AllLiteral', + // like `blah` in `{blah: string}` + FieldType: 'FieldType', + // like `function(string): string` + FunctionType: 'FunctionType', + // any string literal, such as `string` or `My.Namespace` + NameExpression: 'NameExpression', + // null + NullLiteral: 'NullLiteral', + // like `{foo: string}` + RecordType: 'RecordType', + // like `Array.` + TypeApplication: 'TypeApplication', + // like `(number|string)` + TypeUnion: 'TypeUnion', + // undefined + UndefinedLiteral: 'UndefinedLiteral', + // `?` + UnknownLiteral: 'UnknownLiteral' +}); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/LICENSE b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/LICENSE new file mode 100644 index 0000000000..8e1f77b433 --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2013 Jeremy Ashkenas, Michael Fogus, DocumentCloud and Investigative Reporters & Editors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/index.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/index.js new file mode 100644 index 0000000000..5d4bc175f4 --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/index.js @@ -0,0 +1,16 @@ +require('./underscore.array.builders'); +require('./underscore.array.selectors'); +require('./underscore.collections.walk'); +require('./underscore.function.arity'); +require('./underscore.function.combinators'); +require('./underscore.function.dispatch'); +require('./underscore.function.iterators'); +require('./underscore.function.predicates'); +require('./underscore.object.builders'); +require('./underscore.object.selectors'); +require('./underscore.util.existential'); +require('./underscore.util.operators'); +require('./underscore.util.strings'); +require('./underscore.util.trampolines'); + +module.exports = require('underscore'); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/package.json b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/package.json new file mode 100644 index 0000000000..53f932bc14 --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/package.json @@ -0,0 +1,43 @@ +{ + "name": "underscore-contrib", + "version": "0.3.0", + "main": "index.js", + "dependencies": { + "underscore": "1.6.0" + }, + "devDependencies": { + "grunt": "~0.4.1", + "grunt-contrib-concat": "0.3.0", + "grunt-contrib-uglify": "0.2.0", + "grunt-contrib-qunit": "~0.2.2", + "grunt-contrib-watch": "~0.5.3", + "grunt-contrib-jshint": "~0.6.4", + "grunt-docco": "~0.3.0", + "grunt-tocdoc": "~0.1.0", + "grunt-cli": "~0.1.11" + }, + "repository": { + "type": "git", + "url": "https://github.com/documentcloud/underscore-contrib.git" + }, + "license": "MIT", + "author": { + "name": "Fogus", + "email": "me@fogus.me", + "url": "http://www.fogus.me" + }, + "scripts": { + "test": "node ./node_modules/grunt-cli/bin/grunt test" + }, + "homepage": "https://github.com/documentcloud/underscore-contrib", + "readme": "underscore-contrib\n==================\n\nThe brass buckles on Underscore's utility belt -- a contributors' library for [Underscore](http://underscorejs.org/).\n\nLinks\n-----\n\n * [Documentation](http://documentcloud.github.io/underscore-contrib/)\n * [Source repository](https://github.com/documentcloud/underscore-contrib)\n * [Tickets and bug reports](https://github.com/documentcloud/underscore-contrib/issues?state=open)\n * [Maintainer's website](http://www.fogus.me)\n\nWhy underscore-contrib?\n-----------------------\n\nWhile Underscore provides a bevy of useful tools to support functional programming in JavaScript, it can't\n(and shouldn't) be everything to everyone. Underscore-contrib is intended as a home for functions that, for\nvarious reasons, don't belong in Underscore proper. In particular, it aims to be:\n\n * a home for functions that are limited in scope, but solve certain point problems, and\n * a proving ground for features that belong in Underscore proper, but need some advocacy and/or evolution\n(or devolution) to get them there.\n\nUse\n---\n\nFirst, you’ll need Underscore. Then you can grab the relevant underscore-contrib libraries and simply add\nsomething\nlike the following to your pages:\n\n \n \n\nAt the moment there are no cross-contrib dependencies (i.e. each library can stand by itself), but that may\nchange in the future.\n\nContributing\n------------\n\nThere is still a lot of work to do around perf, documentation, examples, testing and distribution so any help\nin those areas is welcomed. Pull requests are accepted, but please search the [issues](https://github.com/documentcloud/underscore-contrib/issues)\nbefore proposing a new sub-contrib or addition. Additionally, all patches and proposals should have strong\ndocumentation, motivating cases and tests. It would be nice if we could not only provide useful tools built on\nUnderscore, but also provide an educational experience for why and how one might use them.\n\nOther (potentially) useful sub-contribs include the following:\n\n * String utilities\n * Date/time utilities\n * Validators\n * Iterators\n * Generators\n * Promises\n * Monads\n * Currying\n * Laziness\n * Multimethods\n\nWhat do these mean? Well, that’s up for discussion. :-)\n", + "readmeFilename": "README.md", + "description": "underscore-contrib ==================", + "bugs": { + "url": "https://github.com/documentcloud/underscore-contrib/issues" + }, + "_id": "underscore-contrib@0.3.0", + "_shasum": "665b66c24783f8fa2b18c9f8cbb0e2c7d48c26c7", + "_from": "underscore-contrib@~0.3.0", + "_resolved": "https://registry.npmjs.org/underscore-contrib/-/underscore-contrib-0.3.0.tgz" +} diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.array.builders.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.array.builders.js new file mode 100644 index 0000000000..2ad8c56226 --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.array.builders.js @@ -0,0 +1,197 @@ +// Underscore-contrib (underscore.array.builders.js 0.3.0) +// (c) 2013 Michael Fogus, DocumentCloud and Investigative Reporters & Editors +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + // Create quick reference variables for speed access to core prototypes. + var slice = Array.prototype.slice, + concat = Array.prototype.concat; + + var existy = function(x) { return x != null; }; + + // Mixing in the array builders + // ---------------------------- + + _.mixin({ + // Concatenates one or more arrays given as arguments. If given objects and + // scalars as arguments `cat` will plop them down in place in the result + // array. If given an `arguments` object, `cat` will treat it like an array + // and concatenate it likewise. + cat: function() { + return _.reduce(arguments, function(acc, elem) { + if (_.isArguments(elem)) { + return concat.call(acc, slice.call(elem)); + } + else { + return concat.call(acc, elem); + } + }, []); + }, + + // 'Constructs' an array by putting an element at its front + cons: function(head, tail) { + return _.cat([head], tail); + }, + + // Takes an array and chunks it some number of times into + // sub-arrays of size n. Allows and optional padding array as + // the third argument to fill in the tail chunk when n is + // not sufficient to build chunks of the same size. + chunk: function(array, n, pad) { + var p = function(array) { + if (array == null) return []; + + var part = _.take(array, n); + + if (n === _.size(part)) { + return _.cons(part, p(_.drop(array, n))); + } + else { + return pad ? [_.take(_.cat(part, pad), n)] : []; + } + }; + + return p(array); + }, + + // Takes an array and chunks it some number of times into + // sub-arrays of size n. If the array given cannot fill the size + // needs of the final chunk then a smaller chunk is used + // for the last. + chunkAll: function(array, n, step) { + step = (step != null) ? step : n; + + var p = function(array, n, step) { + if (_.isEmpty(array)) return []; + + return _.cons(_.take(array, n), + p(_.drop(array, step), n, step)); + }; + + return p(array, n, step); + }, + + // Maps a function over an array and concatenates all of the results. + mapcat: function(array, fun) { + return _.cat.apply(null, _.map(array, fun)); + }, + + // Returns an array with some item between each element + // of a given array. + interpose: function(array, inter) { + if (!_.isArray(array)) throw new TypeError; + var sz = _.size(array); + if (sz === 0) return array; + if (sz === 1) return array; + + return slice.call(_.mapcat(array, function(elem) { + return _.cons(elem, [inter]); + }), 0, -1); + }, + + // Weaves two or more arrays together + weave: function(/* args */) { + if (!_.some(arguments)) return []; + if (arguments.length == 1) return arguments[0]; + + return _.filter(_.flatten(_.zip.apply(null, arguments), true), function(elem) { + return elem != null; + }); + }, + interleave: _.weave, + + // Returns an array of a value repeated a certain number of + // times. + repeat: function(t, elem) { + return _.times(t, function() { return elem; }); + }, + + // Returns an array built from the contents of a given array repeated + // a certain number of times. + cycle: function(t, elems) { + return _.flatten(_.times(t, function() { return elems; }), true); + }, + + // Returns an array with two internal arrays built from + // taking an original array and spliting it at an index. + splitAt: function(array, index) { + return [_.take(array, index), _.drop(array, index)]; + }, + + // Call a function recursively f(f(f(args))) until a second + // given function goes falsey. Expects a seed value to start. + iterateUntil: function(doit, checkit, seed) { + var ret = []; + var result = doit(seed); + + while (checkit(result)) { + ret.push(result); + result = doit(result); + } + + return ret; + }, + + // Takes every nth item from an array, returning an array of + // the results. + takeSkipping: function(array, n) { + var ret = []; + var sz = _.size(array); + + if (n <= 0) return []; + if (n === 1) return array; + + for(var index = 0; index < sz; index += n) { + ret.push(array[index]); + } + + return ret; + }, + + // Returns an array of each intermediate stage of a call to + // a `reduce`-like function. + reductions: function(array, fun, init) { + var ret = []; + var acc = init; + + _.each(array, function(v,k) { + acc = fun(acc, array[k]); + ret.push(acc); + }); + + return ret; + }, + + // Runs its given function on the index of the elements rather than + // the elements themselves, keeping all of the truthy values in the end. + keepIndexed: function(array, pred) { + return _.filter(_.map(_.range(_.size(array)), function(i) { + return pred(i, array[i]); + }), + existy); + }, + + // Accepts an array-like object (other than strings) as an argument and + // returns an array whose elements are in the reverse order. Unlike the + // built-in `Array.prototype.reverse` method, this does not mutate the + // original object. Note: attempting to use this method on a string will + // result in a `TypeError`, as it cannot properly reverse unicode strings. + + reverseOrder: function(obj) { + if (typeof obj == 'string') + throw new TypeError('Strings cannot be reversed by _.reverseOrder'); + return slice.call(obj).reverse(); + } + }); + +})(this); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.array.selectors.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.array.selectors.js new file mode 100644 index 0000000000..d03f2a544b --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.array.selectors.js @@ -0,0 +1,117 @@ +// Underscore-contrib (underscore.array.selectors.js 0.3.0) +// (c) 2013 Michael Fogus, DocumentCloud and Investigative Reporters & Editors +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + // Create quick reference variables for speed access to core prototypes. + var slice = Array.prototype.slice, + concat = Array.prototype.concat; + + var existy = function(x) { return x != null; }; + var truthy = function(x) { return (x !== false) && existy(x); }; + var isSeq = function(x) { return (_.isArray(x)) || (_.isArguments(x)); }; + + // Mixing in the array selectors + // ---------------------------- + + _.mixin({ + // Returns the second element of an array. Passing **n** will return all but + // the first of the head N values in the array. The **guard** check allows it + // to work with `_.map`. + second: function(array, n, guard) { + if (array == null) return void 0; + return (n != null) && !guard ? slice.call(array, 1, n) : array[1]; + }, + + // Returns the third element of an array. Passing **n** will return all but + // the first two of the head N values in the array. The **guard** check allows it + // to work with `_.map`. + third: function(array, n, guard) { + if (array == null) return void 0; + return (n != null) && !guard ? slice.call(array, 2, n) : array[2]; + }, + + // A function to get at an index into an array + nth: function(array, index, guard) { + if ((index != null) && !guard) return array[index]; + }, + + // Takes all items in an array while a given predicate returns truthy. + takeWhile: function(array, pred) { + if (!isSeq(array)) throw new TypeError; + + var sz = _.size(array); + + for (var index = 0; index < sz; index++) { + if(!truthy(pred(array[index]))) { + break; + } + } + + return _.take(array, index); + }, + + // Drops all items from an array while a given predicate returns truthy. + dropWhile: function(array, pred) { + if (!isSeq(array)) throw new TypeError; + + var sz = _.size(array); + + for (var index = 0; index < sz; index++) { + if(!truthy(pred(array[index]))) + break; + } + + return _.drop(array, index); + }, + + // Returns an array with two internal arrays built from + // taking an original array and spliting it at the index + // where a given function goes falsey. + splitWith: function(array, pred) { + return [_.takeWhile(array, pred), _.dropWhile(array, pred)]; + }, + + // Takes an array and partitions it as the given predicate changes + // truth sense. + partitionBy: function(array, fun){ + if (_.isEmpty(array) || !existy(array)) return []; + + var fst = _.first(array); + var fstVal = fun(fst); + var run = concat.call([fst], _.takeWhile(_.rest(array), function(e) { + return _.isEqual(fstVal, fun(e)); + })); + + return concat.call([run], _.partitionBy(_.drop(array, _.size(run)), fun)); + }, + + // Returns the 'best' value in an array based on the result of a + // given function. + best: function(array, fun) { + return _.reduce(array, function(x, y) { + return fun(x, y) ? x : y; + }); + }, + + // Returns an array of existy results of a function over an source array. + keep: function(array, fun) { + if (!isSeq(array)) throw new TypeError("expected an array as the first argument"); + + return _.filter(_.map(array, function(e) { + return fun(e); + }), existy); + } + }); + +})(this); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.collections.walk.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.collections.walk.js new file mode 100644 index 0000000000..963b146f1f --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.collections.walk.js @@ -0,0 +1,196 @@ +// Underscore-contrib (underscore.collections.walk.js 0.3.0) +// (c) 2013 Patrick Dubroy +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + // An internal object that can be returned from a visitor function to + // prevent a top-down walk from walking subtrees of a node. + var stopRecursion = {}; + + // An internal object that can be returned from a visitor function to + // cause the walk to immediately stop. + var stopWalk = {}; + + var notTreeError = 'Not a tree: same object found in two different branches'; + + // Implements the default traversal strategy: if `obj` is a DOM node, walk + // its DOM children; otherwise, walk all the objects it references. + function defaultTraversal(obj) { + return _.isElement(obj) ? obj.children : obj; + } + + // Walk the tree recursively beginning with `root`, calling `beforeFunc` + // before visiting an objects descendents, and `afterFunc` afterwards. + // If `collectResults` is true, the last argument to `afterFunc` will be a + // collection of the results of walking the node's subtrees. + function walkImpl(root, traversalStrategy, beforeFunc, afterFunc, context, collectResults) { + var visited = []; + return (function _walk(value, key, parent) { + // Keep track of objects that have been visited, and throw an exception + // when trying to visit the same object twice. + if (_.isObject(value)) { + if (visited.indexOf(value) >= 0) throw new TypeError(notTreeError); + visited.push(value); + } + + if (beforeFunc) { + var result = beforeFunc.call(context, value, key, parent); + if (result === stopWalk) return stopWalk; + if (result === stopRecursion) return; + } + + var subResults; + var target = traversalStrategy(value); + if (_.isObject(target) && !_.isEmpty(target)) { + // If collecting results from subtrees, collect them in the same shape + // as the parent node. + if (collectResults) subResults = _.isArray(value) ? [] : {}; + + var stop = _.any(target, function(obj, key) { + var result = _walk(obj, key, value); + if (result === stopWalk) return true; + if (subResults) subResults[key] = result; + }); + if (stop) return stopWalk; + } + if (afterFunc) return afterFunc.call(context, value, key, parent, subResults); + })(root); + } + + // Internal helper providing the implementation for `pluck` and `pluckRec`. + function pluck(obj, propertyName, recursive) { + var results = []; + this.preorder(obj, function(value, key) { + if (!recursive && key == propertyName) + return stopRecursion; + if (_.has(value, propertyName)) + results[results.length] = value[propertyName]; + }); + return results; + } + + var exports = { + // Performs a preorder traversal of `obj` and returns the first value + // which passes a truth test. + find: function(obj, visitor, context) { + var result; + this.preorder(obj, function(value, key, parent) { + if (visitor.call(context, value, key, parent)) { + result = value; + return stopWalk; + } + }, context); + return result; + }, + + // Recursively traverses `obj` and returns all the elements that pass a + // truth test. `strategy` is the traversal function to use, e.g. `preorder` + // or `postorder`. + filter: function(obj, strategy, visitor, context) { + var results = []; + if (obj == null) return results; + strategy(obj, function(value, key, parent) { + if (visitor.call(context, value, key, parent)) results.push(value); + }, null, this._traversalStrategy); + return results; + }, + + // Recursively traverses `obj` and returns all the elements for which a + // truth test fails. + reject: function(obj, strategy, visitor, context) { + return this.filter(obj, strategy, function(value, key, parent) { + return !visitor.call(context, value, key, parent); + }); + }, + + // Produces a new array of values by recursively traversing `obj` and + // mapping each value through the transformation function `visitor`. + // `strategy` is the traversal function to use, e.g. `preorder` or + // `postorder`. + map: function(obj, strategy, visitor, context) { + var results = []; + strategy(obj, function(value, key, parent) { + results[results.length] = visitor.call(context, value, key, parent); + }, null, this._traversalStrategy); + return results; + }, + + // Return the value of properties named `propertyName` reachable from the + // tree rooted at `obj`. Results are not recursively searched; use + // `pluckRec` for that. + pluck: function(obj, propertyName) { + return pluck.call(this, obj, propertyName, false); + }, + + // Version of `pluck` which recursively searches results for nested objects + // with a property named `propertyName`. + pluckRec: function(obj, propertyName) { + return pluck.call(this, obj, propertyName, true); + }, + + // Recursively traverses `obj` in a depth-first fashion, invoking the + // `visitor` function for each object only after traversing its children. + // `traversalStrategy` is intended for internal callers, and is not part + // of the public API. + postorder: function(obj, visitor, context, traversalStrategy) { + traversalStrategy = traversalStrategy || this._traversalStrategy; + walkImpl(obj, traversalStrategy, null, visitor, context); + }, + + // Recursively traverses `obj` in a depth-first fashion, invoking the + // `visitor` function for each object before traversing its children. + // `traversalStrategy` is intended for internal callers, and is not part + // of the public API. + preorder: function(obj, visitor, context, traversalStrategy) { + traversalStrategy = traversalStrategy || this._traversalStrategy; + walkImpl(obj, traversalStrategy, visitor, null, context); + }, + + // Builds up a single value by doing a post-order traversal of `obj` and + // calling the `visitor` function on each object in the tree. For leaf + // objects, the `memo` argument to `visitor` is the value of the `leafMemo` + // argument to `reduce`. For non-leaf objects, `memo` is a collection of + // the results of calling `reduce` on the object's children. + reduce: function(obj, visitor, leafMemo, context) { + var reducer = function(value, key, parent, subResults) { + return visitor(subResults || leafMemo, value, key, parent); + }; + return walkImpl(obj, this._traversalStrategy, null, reducer, context, true); + } + }; + + // Set up aliases to match those in underscore.js. + exports.collect = exports.map; + exports.detect = exports.find; + exports.select = exports.filter; + + // Returns an object containing the walk functions. If `traversalStrategy` + // is specified, it is a function determining how objects should be + // traversed. Given an object, it returns the object to be recursively + // walked. The default strategy is equivalent to `_.identity` for regular + // objects, and for DOM nodes it returns the node's DOM children. + _.walk = function(traversalStrategy) { + var walker = _.clone(exports); + + // Bind all of the public functions in the walker to itself. This allows + // the traversal strategy to be dynamically scoped. + _.bindAll.apply(null, [walker].concat(_.keys(walker))); + + walker._traversalStrategy = traversalStrategy || defaultTraversal; + return walker; + }; + + // Use `_.walk` as a namespace to hold versions of the walk functions which + // use the default traversal strategy. + _.extend(_.walk, _.walk()); +})(this); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.arity.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.arity.js new file mode 100644 index 0000000000..5f123d28cc --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.arity.js @@ -0,0 +1,200 @@ +// Underscore-contrib (underscore.function.arity.js 0.3.0) +// (c) 2013 Michael Fogus, DocumentCloud and Investigative Reporters & Editors +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + function enforcesUnary (fn) { + return function mustBeUnary () { + if (arguments.length === 1) { + return fn.apply(this, arguments); + } + else throw new RangeError('Only a single argument may be accepted.'); + + }; + } + + // Curry + // ------- + var curry = (function () { + function collectArgs(func, that, argCount, args, newArg, reverse) { + if (reverse === true) { + args.unshift(newArg); + } else { + args.push(newArg); + } + if (args.length == argCount) { + return func.apply(that, args); + } else { + return enforcesUnary(function () { + return collectArgs(func, that, argCount, args.slice(0), arguments[0], reverse); + }); + } + } + return function curry (func, reverse) { + var that = this; + return enforcesUnary(function () { + return collectArgs(func, that, func.length, [], arguments[0], reverse); + }); + }; + }()); + + // Enforce Arity + // -------------------- + var enforce = (function () { + var CACHE = []; + return function enforce (func) { + if (typeof func !== 'function') { + throw new Error('Argument 1 must be a function.'); + } + var funcLength = func.length; + if (CACHE[funcLength] === undefined) { + CACHE[funcLength] = function (enforceFunc) { + return function () { + if (arguments.length !== funcLength) { + throw new RangeError(funcLength + ' arguments must be applied.'); + } + return enforceFunc.apply(this, arguments); + }; + }; + } + return CACHE[funcLength](func); + }; + }()); + + // Mixing in the arity functions + // ----------------------------- + + _.mixin({ + // ### Fixed arguments + + // Fixes the arguments to a function based on the parameter template defined by + // the presence of values and the `_` placeholder. + fix: function(fun) { + var fixArgs = _.rest(arguments); + + var f = function() { + var args = fixArgs.slice(); + var arg = 0; + + for ( var i = 0; i < (args.length || arg < arguments.length); i++ ) { + if ( args[i] === _ ) { + args[i] = arguments[arg++]; + } + } + + return fun.apply(null, args); + }; + + f._original = fun; + + return f; + }, + + unary: function (fun) { + return function unary (a) { + return fun.call(this, a); + }; + }, + + binary: function (fun) { + return function binary (a, b) { + return fun.call(this, a, b); + }; + }, + + ternary: function (fun) { + return function ternary (a, b, c) { + return fun.call(this, a, b, c); + }; + }, + + quaternary: function (fun) { + return function quaternary (a, b, c, d) { + return fun.call(this, a, b, c, d); + }; + }, + + // Flexible curry function with strict arity. + // Argument application left to right. + // source: https://github.com/eborden/js-curry + curry: curry, + + // Flexible right to left curry with strict arity. + rCurry: function (func) { + return curry.call(this, func, true); + }, + + + curry2: function (fun) { + return enforcesUnary(function curried (first) { + return enforcesUnary(function (last) { + return fun.call(this, first, last); + }); + }); + }, + + curry3: function (fun) { + return enforcesUnary(function (first) { + return enforcesUnary(function (second) { + return enforcesUnary(function (last) { + return fun.call(this, first, second, last); + }); + }); + }); + }, + + // reverse currying for functions taking two arguments. + rcurry2: function (fun) { + return enforcesUnary(function (last) { + return enforcesUnary(function (first) { + return fun.call(this, first, last); + }); + }); + }, + + rcurry3: function (fun) { + return enforcesUnary(function (last) { + return enforcesUnary(function (second) { + return enforcesUnary(function (first) { + return fun.call(this, first, second, last); + }); + }); + }); + }, + // Dynamic decorator to enforce function arity and defeat varargs. + enforce: enforce + }); + + _.arity = (function () { + // Allow 'new Function', as that is currently the only reliable way + // to manipulate function.length + /* jshint -W054 */ + var FUNCTIONS = {}; + return function arity (numberOfArgs, fun) { + if (FUNCTIONS[numberOfArgs] == null) { + var parameters = new Array(numberOfArgs); + for (var i = 0; i < numberOfArgs; ++i) { + parameters[i] = "__" + i; + } + var pstr = parameters.join(); + var code = "return function ("+pstr+") { return fun.apply(this, arguments); };"; + FUNCTIONS[numberOfArgs] = new Function(['fun'], code); + } + if (fun == null) { + return function (fun) { return arity(numberOfArgs, fun); }; + } + else return FUNCTIONS[numberOfArgs](fun); + }; + })(); + +})(this); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.combinators.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.combinators.js new file mode 100644 index 0000000000..b46089962c --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.combinators.js @@ -0,0 +1,266 @@ +// Underscore-contrib (underscore.function.combinators.js 0.3.0) +// (c) 2013 Michael Fogus, DocumentCloud and Investigative Reporters & Editors +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + var existy = function(x) { return x != null; }; + var truthy = function(x) { return (x !== false) && existy(x); }; + var __reverse = [].reverse; + var __slice = [].slice; + var __map = [].map; + var curry2 = function (fun) { + return function curried (first, optionalLast) { + if (arguments.length === 1) { + return function (last) { + return fun(first, last); + }; + } + else return fun(first, optionalLast); + }; + }; + + // n.b. depends on underscore.function.arity.js + + // Takes a target function and a mapping function. Returns a function + // that applies the mapper to its arguments before evaluating the body. + function baseMapArgs (fun, mapFun) { + return _.arity(fun.length, function () { + return fun.apply(this, __map.call(arguments, mapFun)); + }); + } + + // Mixing in the combinator functions + // ---------------------------------- + + _.mixin({ + // Provide "always" alias for backwards compatibility + always: _.constant, + + // Takes some number of functions, either as an array or variadically + // and returns a function that takes some value as its first argument + // and runs it through a pipeline of the original functions given. + pipeline: function(/*, funs */){ + var funs = (_.isArray(arguments[0])) ? arguments[0] : arguments; + + return function(seed) { + return _.reduce(funs, + function(l,r) { return r(l); }, + seed); + }; + }, + + // Composes a bunch of predicates into a single predicate that + // checks all elements of an array for conformance to all of the + // original predicates. + conjoin: function(/* preds */) { + var preds = arguments; + + return function(array) { + return _.every(array, function(e) { + return _.every(preds, function(p) { + return p(e); + }); + }); + }; + }, + + // Composes a bunch of predicates into a single predicate that + // checks all elements of an array for conformance to any of the + // original predicates. + disjoin: function(/* preds */) { + var preds = arguments; + + return function(array) { + return _.some(array, function(e) { + return _.some(preds, function(p) { + return p(e); + }); + }); + }; + }, + + // Takes a predicate-like and returns a comparator (-1,0,1). + comparator: function(fun) { + return function(x, y) { + if (truthy(fun(x, y))) + return -1; + else if (truthy(fun(y, x))) + return 1; + else + return 0; + }; + }, + + // Returns a function that reverses the sense of a given predicate-like. + complement: function(pred) { + return function() { + return !pred.apply(this, arguments); + }; + }, + + // Takes a function expecting varargs and + // returns a function that takes an array and + // uses its elements as the args to the original + // function + splat: function(fun) { + return function(array) { + return fun.apply(this, array); + }; + }, + + // Takes a function expecting an array and returns + // a function that takes varargs and wraps all + // in an array that is passed to the original function. + unsplat: function(fun) { + var funLength = fun.length; + + if (funLength < 1) { + return fun; + } + else if (funLength === 1) { + return function () { + return fun.call(this, __slice.call(arguments, 0)); + }; + } + else { + return function () { + var numberOfArgs = arguments.length, + namedArgs = __slice.call(arguments, 0, funLength - 1), + numberOfMissingNamedArgs = Math.max(funLength - numberOfArgs - 1, 0), + argPadding = new Array(numberOfMissingNamedArgs), + variadicArgs = __slice.call(arguments, fun.length - 1); + + return fun.apply(this, namedArgs.concat(argPadding).concat([variadicArgs])); + }; + } + }, + + // Same as unsplat, but the rest of the arguments are collected in the + // first parameter, e.g. unsplatl( function (args, callback) { ... ]}) + unsplatl: function(fun) { + var funLength = fun.length; + + if (funLength < 1) { + return fun; + } + else if (funLength === 1) { + return function () { + return fun.call(this, __slice.call(arguments, 0)); + }; + } + else { + return function () { + var numberOfArgs = arguments.length, + namedArgs = __slice.call(arguments, Math.max(numberOfArgs - funLength + 1, 0)), + variadicArgs = __slice.call(arguments, 0, Math.max(numberOfArgs - funLength + 1, 0)); + + return fun.apply(this, [variadicArgs].concat(namedArgs)); + }; + } + }, + + // map the arguments of a function + mapArgs: curry2(baseMapArgs), + + // Returns a function that returns an array of the calls to each + // given function for some arguments. + juxt: function(/* funs */) { + var funs = arguments; + + return function(/* args */) { + var args = arguments; + return _.map(funs, function(f) { + return f.apply(this, args); + }, this); + }; + }, + + // Returns a function that protects a given function from receiving + // non-existy values. Each subsequent value provided to `fnull` acts + // as the default to the original function should a call receive non-existy + // values in the defaulted arg slots. + fnull: function(fun /*, defaults */) { + var defaults = _.rest(arguments); + + return function(/*args*/) { + var args = _.toArray(arguments); + var sz = _.size(defaults); + + for(var i = 0; i < sz; i++) { + if (!existy(args[i])) + args[i] = defaults[i]; + } + + return fun.apply(this, args); + }; + }, + + // Flips the first two args of a function + flip2: function(fun) { + return function(/* args */) { + var flipped = __slice.call(arguments); + flipped[0] = arguments[1]; + flipped[1] = arguments[0]; + + return fun.apply(this, flipped); + }; + }, + + // Flips an arbitrary number of args of a function + flip: function(fun) { + return function(/* args */) { + var reversed = __reverse.call(arguments); + + return fun.apply(this, reversed); + }; + }, + + // Takes a method-style function (one which uses `this`) and pushes + // `this` into the argument list. The returned function uses its first + // argument as the receiver/context of the original function, and the rest + // of the arguments are used as the original's entire argument list. + functionalize: function(method) { + return function(ctx /*, args */) { + return method.apply(ctx, _.rest(arguments)); + }; + }, + + // Takes a function and pulls the first argument out of the argument + // list and into `this` position. The returned function calls the original + // with its receiver (`this`) prepending the argument list. The original + // is called with a receiver of `null`. + methodize: function(func) { + return function(/* args */) { + return func.apply(null, _.cons(this, arguments)); + }; + }, + + k: _.always, + t: _.pipeline + }); + + _.unsplatr = _.unsplat; + + // map the arguments of a function, takes the mapping function + // first so it can be used as a combinator + _.mapArgsWith = curry2(_.flip(baseMapArgs)); + + // Returns function property of object by name, bound to object + _.bound = function(obj, fname) { + var fn = obj[fname]; + if (!_.isFunction(fn)) + throw new TypeError("Expected property to be a function"); + return _.bind(fn, obj); + }; + +})(this); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.dispatch.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.dispatch.js new file mode 100644 index 0000000000..9e0eab2176 --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.dispatch.js @@ -0,0 +1,33 @@ +// Underscore-contrib (underscore.function.dispatch.js 0.3.0) +// (c) 2013 Justin Ridgewell +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + // Create quick reference variable for speed. + var slice = Array.prototype.slice; + + // Mixing in the attempt function + // ------------------------ + + _.mixin({ + // If object is not undefined or null then invoke the named `method` function + // with `object` as context and arguments; otherwise, return undefined. + attempt: function(object, method) { + if (object == null) return void 0; + var func = object[method]; + var args = slice.call(arguments, 2); + return _.isFunction(func) ? func.apply(object, args) : void 0; + } + }); + +})(this); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.iterators.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.iterators.js new file mode 100644 index 0000000000..9b5aed626e --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.iterators.js @@ -0,0 +1,334 @@ +// Underscore-contrib (underscore.function.iterators.js 0.3.0) +// (c) 2013 Michael Fogus and DocumentCloud Inc. +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root, undefined) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + var HASNTBEENRUN = {}; + + function unary (fun) { + return function (first) { + return fun.call(this, first); + }; + } + + function binary (fun) { + return function (first, second) { + return fun.call(this, first, second); + }; + } + + // Mixing in the iterator functions + // -------------------------------- + + function foldl (iter, binaryFn, seed) { + var state, element; + if (seed !== void 0) { + state = seed; + } + else { + state = iter(); + } + element = iter(); + while (element != null) { + state = binaryFn.call(element, state, element); + element = iter(); + } + return state; + } + + function unfold (seed, unaryFn) { + var state = HASNTBEENRUN; + return function () { + if (state === HASNTBEENRUN) { + state = seed; + } else if (state != null) { + state = unaryFn.call(state, state); + } + + return state; + }; + } + + // note that the unfoldWithReturn behaves differently than + // unfold with respect to the first value returned + function unfoldWithReturn (seed, unaryFn) { + var state = seed, + pair, + value; + return function () { + if (state != null) { + pair = unaryFn.call(state, state); + value = pair[1]; + state = value != null ? pair[0] : void 0; + return value; + } + else return void 0; + }; + } + + function accumulate (iter, binaryFn, initial) { + var state = initial; + return function () { + var element = iter(); + if (element == null) { + return element; + } + else { + if (state === void 0) { + state = element; + } else { + state = binaryFn.call(element, state, element); + } + + return state; + } + }; + } + + function accumulateWithReturn (iter, binaryFn, initial) { + var state = initial, + stateAndReturnValue, + element; + return function () { + element = iter(); + if (element == null) { + return element; + } + else { + if (state === void 0) { + state = element; + return state; + } + else { + stateAndReturnValue = binaryFn.call(element, state, element); + state = stateAndReturnValue[0]; + return stateAndReturnValue[1]; + } + } + }; + } + + function map (iter, unaryFn) { + return function() { + var element; + element = iter(); + if (element != null) { + return unaryFn.call(element, element); + } else { + return void 0; + } + }; + } + + function mapcat(iter, unaryFn) { + var lastIter = null; + return function() { + var element; + var gen; + if (lastIter == null) { + gen = iter(); + if (gen == null) { + lastIter = null; + return void 0; + } + lastIter = unaryFn.call(gen, gen); + } + while (element == null) { + element = lastIter(); + if (element == null) { + gen = iter(); + if (gen == null) { + lastIter = null; + return void 0; + } + else { + lastIter = unaryFn.call(gen, gen); + } + } + } + return element; + }; + } + + function select (iter, unaryPredicateFn) { + return function() { + var element; + element = iter(); + while (element != null) { + if (unaryPredicateFn.call(element, element)) { + return element; + } + element = iter(); + } + return void 0; + }; + } + + function reject (iter, unaryPredicateFn) { + return select(iter, function (something) { + return !unaryPredicateFn(something); + }); + } + + function find (iter, unaryPredicateFn) { + return select(iter, unaryPredicateFn)(); + } + + function slice (iter, numberToDrop, numberToTake) { + var count = 0; + while (numberToDrop-- > 0) { + iter(); + } + if (numberToTake != null) { + return function() { + if (++count <= numberToTake) { + return iter(); + } else { + return void 0; + } + }; + } + else return iter; + } + + function drop (iter, numberToDrop) { + return slice(iter, numberToDrop == null ? 1 : numberToDrop); + } + + function take (iter, numberToTake) { + return slice(iter, 0, numberToTake == null ? 1 : numberToTake); + } + + function List (array) { + var index = 0; + return function() { + return array[index++]; + }; + } + + function Tree (array) { + var index, myself, state; + index = 0; + state = []; + myself = function() { + var element, tempState; + element = array[index++]; + if (element instanceof Array) { + state.push({ + array: array, + index: index + }); + array = element; + index = 0; + return myself(); + } else if (element === void 0) { + if (state.length > 0) { + tempState = state.pop(); + array = tempState.array; + index = tempState.index; + return myself(); + } else { + return void 0; + } + } else { + return element; + } + }; + return myself; + } + + function K (value) { + return function () { + return value; + }; + } + + function upRange (from, to, by) { + return function () { + var was; + + if (from > to) { + return void 0; + } + else { + was = from; + from = from + by; + return was; + } + }; + } + + function downRange (from, to, by) { + return function () { + var was; + + if (from < to) { + return void 0; + } + else { + was = from; + from = from - by; + return was; + } + }; + } + + function range (from, to, by) { + if (from == null) { + return upRange(1, Infinity, 1); + } + else if (to == null) { + return upRange(from, Infinity, 1); + } + else if (by == null) { + if (from <= to) { + return upRange(from, to, 1); + } + else return downRange(from, to, 1); + } + else if (by > 0) { + return upRange(from, to, by); + } + else if (by < 0) { + return downRange(from, to, Math.abs(by)); + } + else return k(from); + } + + var numbers = unary(range); + + _.iterators = { + accumulate: accumulate, + accumulateWithReturn: accumulateWithReturn, + foldl: foldl, + reduce: foldl, + unfold: unfold, + unfoldWithReturn: unfoldWithReturn, + map: map, + mapcat: mapcat, + select: select, + reject: reject, + filter: select, + find: find, + slice: slice, + drop: drop, + take: take, + List: List, + Tree: Tree, + constant: K, + K: K, + numbers: numbers, + range: range + }; + +})(this, void 0); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.predicates.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.predicates.js new file mode 100644 index 0000000000..11e60ce3fc --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.function.predicates.js @@ -0,0 +1,112 @@ +// Underscore-contrib (underscore.function.predicates.js 0.3.0) +// (c) 2013 Michael Fogus, DocumentCloud and Investigative Reporters & Editors +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + + // Mixing in the predicate functions + // --------------------------------- + + _.mixin({ + // A wrapper around instanceof + isInstanceOf: function(x, t) { return (x instanceof t); }, + + // An associative object is one where its elements are + // accessed via a key or index. (i.e. array and object) + isAssociative: function(x) { return _.isArray(x) || _.isObject(x) || _.isArguments(x); }, + + // An indexed object is anything that allows numerical index for + // accessing its elements (e.g. arrays and strings). NOTE: Underscore + // does not support cross-browser consistent use of strings as array-like + // objects, so be wary in IE 8 when using String objects and IE<8. + // on string literals & objects. + isIndexed: function(x) { return _.isArray(x) || _.isString(x) || _.isArguments(x); }, + + // A seq is something considered a sequential composite type (i.e. arrays and `arguments`). + isSequential: function(x) { return (_.isArray(x)) || (_.isArguments(x)); }, + + // Check if an object is an object literal, since _.isObject(function() {}) === _.isObject([]) === true + isPlainObject: function(x) { return _.isObject(x) && x.constructor === root.Object; }, + + // These do what you think that they do + isZero: function(x) { return 0 === x; }, + isEven: function(x) { return _.isFinite(x) && (x & 1) === 0; }, + isOdd: function(x) { return _.isFinite(x) && !_.isEven(x); }, + isPositive: function(x) { return x > 0; }, + isNegative: function(x) { return x < 0; }, + isValidDate: function(x) { return _.isDate(x) && !_.isNaN(x.getTime()); }, + + // A numeric is a variable that contains a numeric value, regardless its type + // It can be a String containing a numeric value, exponential notation, or a Number object + // See here for more discussion: http://stackoverflow.com/questions/18082/validate-numbers-in-javascript-isnumeric/1830844#1830844 + isNumeric: function(n) { + return !isNaN(parseFloat(n)) && isFinite(n); + }, + + // An integer contains an optional minus sign to begin and only the digits 0-9 + // Objects that can be parsed that way are also considered ints, e.g. "123" + // Floats that are mathematically equal to integers are considered integers, e.g. 1.0 + // See here for more discussion: http://stackoverflow.com/questions/1019515/javascript-test-for-an-integer + isInteger: function(i) { + return _.isNumeric(i) && i % 1 === 0; + }, + + // A float is a numbr that is not an integer. + isFloat: function(n) { + return _.isNumeric(n) && !_.isInteger(n); + }, + + // checks if a string is a valid JSON + isJSON: function(str) { + try { + JSON.parse(str); + } catch (e) { + return false; + } + return true; + }, + + // Returns true if its arguments are monotonically + // increaing values; false otherwise. + isIncreasing: function() { + var count = _.size(arguments); + if (count === 1) return true; + if (count === 2) return arguments[0] < arguments[1]; + + for (var i = 1; i < count; i++) { + if (arguments[i-1] >= arguments[i]) { + return false; + } + } + + return true; + }, + + // Returns true if its arguments are monotonically + // decreaing values; false otherwise. + isDecreasing: function() { + var count = _.size(arguments); + if (count === 1) return true; + if (count === 2) return arguments[0] > arguments[1]; + + for (var i = 1; i < count; i++) { + if (arguments[i-1] <= arguments[i]) { + return false; + } + } + + return true; + } + }); + +})(this); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.object.builders.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.object.builders.js new file mode 100644 index 0000000000..278f09785b --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.object.builders.js @@ -0,0 +1,120 @@ +// Underscore-contrib (underscore.object.builders.js 0.3.0) +// (c) 2013 Michael Fogus, DocumentCloud and Investigative Reporters & Editors +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + // Create quick reference variables for speed access to core prototypes. + var slice = Array.prototype.slice, + concat = Array.prototype.concat; + + var existy = function(x) { return x != null; }; + var truthy = function(x) { return (x !== false) && existy(x); }; + var isAssociative = function(x) { return _.isArray(x) || _.isObject(x); }; + var curry2 = function(fun) { + return function(last) { + return function(first) { + return fun(first, last); + }; + }; + }; + + // Mixing in the object builders + // ---------------------------- + + _.mixin({ + // Merges two or more objects starting with the left-most and + // applying the keys right-word + // {any:any}* -> {any:any} + merge: function(/* objs */){ + var dest = _.some(arguments) ? {} : null; + + if (truthy(dest)) { + _.extend.apply(null, concat.call([dest], _.toArray(arguments))); + } + + return dest; + }, + + // Takes an object and another object of strings to strings where the second + // object describes the key renaming to occur in the first object. + renameKeys: function(obj, kobj) { + return _.reduce(kobj, function(o, nu, old) { + if (existy(obj[old])) { + o[nu] = obj[old]; + return o; + } + else + return o; + }, + _.omit.apply(null, concat.call([obj], _.keys(kobj)))); + }, + + // Snapshots an object deeply. Based on the version by + // [Keith Devens](http://keithdevens.com/weblog/archive/2007/Jun/07/javascript.clone) + // until we can find a more efficient and robust way to do it. + snapshot: function(obj) { + if(obj == null || typeof(obj) != 'object') { + return obj; + } + + var temp = new obj.constructor(); + + for(var key in obj) { + if (obj.hasOwnProperty(key)) { + temp[key] = _.snapshot(obj[key]); + } + } + + return temp; + }, + + // Updates the value at any depth in a nested object based on the + // path described by the keys given. The function provided is supplied + // the current value and is expected to return a value for use as the + // new value. If no keys are provided, then the object itself is presented + // to the given function. + updatePath: function(obj, fun, ks, defaultValue) { + if (!isAssociative(obj)) throw new TypeError("Attempted to update a non-associative object."); + if (!existy(ks)) return fun(obj); + + var deepness = _.isArray(ks); + var keys = deepness ? ks : [ks]; + var ret = deepness ? _.snapshot(obj) : _.clone(obj); + var lastKey = _.last(keys); + var target = ret; + + _.each(_.initial(keys), function(key) { + if (defaultValue && !_.has(target, key)) { + target[key] = _.clone(defaultValue); + } + target = target[key]; + }); + + target[lastKey] = fun(target[lastKey]); + return ret; + }, + + // Sets the value at any depth in a nested object based on the + // path described by the keys given. + setPath: function(obj, value, ks, defaultValue) { + if (!existy(ks)) throw new TypeError("Attempted to set a property at a null path."); + + return _.updatePath(obj, function() { return value; }, ks, defaultValue); + }, + + // Returns an object where each element of an array is keyed to + // the number of times that it occurred in said array. + frequencies: curry2(_.countBy)(_.identity) + }); + +})(this); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.object.selectors.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.object.selectors.js new file mode 100644 index 0000000000..17308eec95 --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.object.selectors.js @@ -0,0 +1,108 @@ +// Underscore-contrib (underscore.object.selectors.js 0.3.0) +// (c) 2013 Michael Fogus, DocumentCloud and Investigative Reporters & Editors +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + // Create quick reference variables for speed access to core prototypes. + var concat = Array.prototype.concat; + var ArrayProto = Array.prototype; + var slice = ArrayProto.slice; + + // Mixing in the object selectors + // ------------------------------ + + _.mixin({ + // Returns a function that will attempt to look up a named field + // in any object that it's given. + accessor: function(field) { + return function(obj) { + return (obj && obj[field]); + }; + }, + + // Given an object, returns a function that will attempt to look up a field + // that it's given. + dictionary: function (obj) { + return function(field) { + return (obj && field && obj[field]); + }; + }, + + // Like `_.pick` except that it takes an array of keys to pick. + selectKeys: function (obj, ks) { + return _.pick.apply(null, concat.call([obj], ks)); + }, + + // Returns the key/value pair for a given property in an object, undefined if not found. + kv: function(obj, key) { + if (_.has(obj, key)) { + return [key, obj[key]]; + } + + return void 0; + }, + + // Gets the value at any depth in a nested object based on the + // path described by the keys given. Keys may be given as an array + // or as a dot-separated string. + getPath: function getPath (obj, ks) { + if (typeof ks == "string") ks = ks.split("."); + + // If we have reached an undefined property + // then stop executing and return undefined + if (obj === undefined) return void 0; + + // If the path array has no more elements, we've reached + // the intended property and return its value + if (ks.length === 0) return obj; + + // If we still have elements in the path array and the current + // value is null, stop executing and return undefined + if (obj === null) return void 0; + + return getPath(obj[_.first(ks)], _.rest(ks)); + }, + + // Returns a boolean indicating whether there is a property + // at the path described by the keys given + hasPath: function hasPath (obj, ks) { + if (typeof ks == "string") ks = ks.split("."); + + var numKeys = ks.length; + + if (obj == null && numKeys > 0) return false; + + if (!(ks[0] in obj)) return false; + + if (numKeys === 1) return true; + + return hasPath(obj[_.first(ks)], _.rest(ks)); + }, + + pickWhen: function(obj, pred) { + var copy = {}; + + _.each(obj, function(value, key) { + if (pred(obj[key])) copy[key] = obj[key]; + }); + + return copy; + }, + + omitWhen: function(obj, pred) { + return _.pickWhen(obj, function(e) { return !pred(e); }); + } + + }); + +})(this); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.util.existential.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.util.existential.js new file mode 100644 index 0000000000..72b7f000ae --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.util.existential.js @@ -0,0 +1,32 @@ +// Underscore-contrib (underscore.util.existential.js 0.3.0) +// (c) 2013 Michael Fogus, DocumentCloud and Investigative Reporters & Editors +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + + // Mixing in the truthiness + // ------------------------ + + _.mixin({ + exists: function(x) { return x != null; }, + truthy: function(x) { return (x !== false) && _.exists(x); }, + falsey: function(x) { return !_.truthy(x); }, + not: function(b) { return !b; }, + firstExisting: function() { + for (var i = 0; i < arguments.length; i++) { + if (arguments[i] != null) return arguments[i]; + } + } + }); + +})(this); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.util.operators.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.util.operators.js new file mode 100644 index 0000000000..2305716523 --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.util.operators.js @@ -0,0 +1,164 @@ +// Underscore-contrib (underscore.function.arity.js 0.3.0) +// (c) 2013 Michael Fogus, DocumentCloud and Investigative Reporters & Editors +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Setup for variadic operators + // ---------------------------- + + // Turn a binary math operator into a variadic operator + function variadicMath(operator) { + return function() { + return _.reduce(arguments, operator); + }; + } + + // Turn a binary comparator into a variadic comparator + function variadicComparator(comparator) { + return function() { + var result; + for (var i = 0; i < arguments.length - 1; i++) { + result = comparator(arguments[i], arguments[i + 1]); + if (result === false) return result; + } + return result; + }; + } + + // Turn a boolean-returning function into one with the opposite meaning + function invert(fn) { + return function() { + return !fn.apply(this, arguments); + }; + } + + // Basic math operators + function add(x, y) { + return x + y; + } + + function sub(x, y) { + return x - y; + } + + function mul(x, y) { + return x * y; + } + + function div(x, y) { + return x / y; + } + + function mod(x, y) { + return x % y; + } + + function inc(x) { + return ++x; + } + + function dec(x) { + return --x; + } + + function neg(x) { + return -x; + } + + // Bitwise operators + function bitwiseAnd(x, y) { + return x & y; + } + + function bitwiseOr(x, y) { + return x | y; + } + + function bitwiseXor(x, y) { + return x ^ y; + } + + function bitwiseLeft(x, y) { + return x << y; + } + + function bitwiseRight(x, y) { + return x >> y; + } + + function bitwiseZ(x, y) { + return x >>> y; + } + + function bitwiseNot(x) { + return ~x; + } + + // Basic comparators + function eq(x, y) { + return x == y; + } + + function seq(x, y) { + return x === y; + } + + // Not + function not(x) { + return !x; + } + + // Relative comparators + function gt(x, y) { + return x > y; + } + + function lt(x, y) { + return x < y; + } + + function gte(x, y) { + return x >= y; + } + + function lte(x, y) { + return x <= y; + } + + // Mixing in the operator functions + // ----------------------------- + + _.mixin({ + add: variadicMath(add), + sub: variadicMath(sub), + mul: variadicMath(mul), + div: variadicMath(div), + mod: mod, + inc: inc, + dec: dec, + neg: neg, + eq: variadicComparator(eq), + seq: variadicComparator(seq), + neq: invert(variadicComparator(eq)), + sneq: invert(variadicComparator(seq)), + not: not, + gt: variadicComparator(gt), + lt: variadicComparator(lt), + gte: variadicComparator(gte), + lte: variadicComparator(lte), + bitwiseAnd: variadicMath(bitwiseAnd), + bitwiseOr: variadicMath(bitwiseOr), + bitwiseXor: variadicMath(bitwiseXor), + bitwiseNot: bitwiseNot, + bitwiseLeft: variadicMath(bitwiseLeft), + bitwiseRight: variadicMath(bitwiseRight), + bitwiseZ: variadicMath(bitwiseZ) + }); +})(this); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.util.strings.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.util.strings.js new file mode 100644 index 0000000000..15c6eaf192 --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.util.strings.js @@ -0,0 +1,129 @@ +// Underscore-contrib (underscore.util.strings.js 0.3.0) +// (c) 2013 Michael Fogus, DocumentCloud and Investigative Reporters & Editors +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + // No reason to create regex more than once + var plusRegex = /\+/g; + var spaceRegex = /\%20/g; + var bracketRegex = /(?:([^\[]+))|(?:\[(.*?)\])/g; + + var urlDecode = function(s) { + return decodeURIComponent(s.replace(plusRegex, '%20')); + }; + var urlEncode = function(s) { + return encodeURIComponent(s).replace(spaceRegex, '+'); + }; + + var buildParams = function(prefix, val, top) { + if (_.isUndefined(top)) top = true; + if (_.isArray(val)) { + return _.map(val, function(value, key) { + return buildParams(top ? key : prefix + '[]', value, false); + }).join('&'); + } else if (_.isObject(val)) { + return _.map(val, function(value, key) { + return buildParams(top ? key : prefix + '[' + key + ']', value, false); + }).join('&'); + } else { + return urlEncode(prefix) + '=' + urlEncode(val); + } + }; + + // Mixing in the string utils + // ---------------------------- + + _.mixin({ + // Explodes a string into an array of chars + explode: function(s) { + return s.split(''); + }, + + // Parses a query string into a hash + fromQuery: function(str) { + var parameters = str.split('&'), + obj = {}, + parameter, + key, + match, + lastKey, + subKey, + depth; + + // Iterate over key/value pairs + _.each(parameters, function(parameter) { + parameter = parameter.split('='); + key = urlDecode(parameter[0]); + lastKey = key; + depth = obj; + + // Reset so we don't have issues when matching the same string + bracketRegex.lastIndex = 0; + + // Attempt to extract nested values + while ((match = bracketRegex.exec(key)) !== null) { + if (!_.isUndefined(match[1])) { + + // If we're at the top nested level, no new object needed + subKey = match[1]; + + } else { + + // If we're at a lower nested level, we need to step down, and make + // sure that there is an object to place the value into + subKey = match[2]; + depth[lastKey] = depth[lastKey] || (subKey ? {} : []); + depth = depth[lastKey]; + } + + // Save the correct key as a hash or an array + lastKey = subKey || _.size(depth); + } + + // Assign value to nested object + depth[lastKey] = urlDecode(parameter[1]); + }); + + return obj; + }, + + // Implodes and array of chars into a string + implode: function(a) { + return a.join(''); + }, + + // Converts a string to camel case + camelCase : function( string ){ + return string.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); + }, + + // Converts camel case to dashed (opposite of _.camelCase) + toDash : function( string ){ + string = string.replace(/([A-Z])/g, function($1){return "-"+$1.toLowerCase();}); + // remove first dash + return ( string.charAt( 0 ) == '-' ) ? string.substr(1) : string; + }, + + // Creates a query string from a hash + toQuery: function(obj) { + return buildParams('', obj); + }, + + // Reports whether a string contains a search string. + strContains: function(str, search) { + if (typeof str != 'string') throw new TypeError; + return (str.indexOf(search) != -1); + } + + }); +})(this); diff --git a/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.util.trampolines.js b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.util.trampolines.js new file mode 100644 index 0000000000..ce2a1bc7f9 --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/node_modules/underscore-contrib/underscore.util.trampolines.js @@ -0,0 +1,39 @@ +// Underscore-contrib (underscore.util.trampolines.js 0.3.0) +// (c) 2013 Michael Fogus, DocumentCloud and Investigative Reporters & Editors +// Underscore-contrib may be freely distributed under the MIT license. + +(function(root) { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var _ = root._ || require('underscore'); + + // Helpers + // ------- + + + // Mixing in the truthiness + // ------------------------ + + _.mixin({ + done: function(value) { + var ret = _(value); + ret.stopTrampoline = true; + return ret; + }, + + trampoline: function(fun /*, args */) { + var result = fun.apply(fun, _.rest(arguments)); + + while (_.isFunction(result)) { + result = result(); + if ((result instanceof _) && (result.stopTrampoline)) break; + } + + return result.value(); + } + }); + +})(this); diff --git a/third_party/jsdoc/node_modules/catharsis/package.json b/third_party/jsdoc/node_modules/catharsis/package.json new file mode 100644 index 0000000000..3e6ac226ea --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/package.json @@ -0,0 +1,48 @@ +{ + "version": "0.8.3", + "name": "catharsis", + "description": "A JavaScript parser for Google Closure Compiler and JSDoc type expressions.", + "author": { + "name": "Jeff Williams", + "email": "jeffrey.l.williams@gmail.com" + }, + "repository": { + "type": "git", + "url": "https://github.com/hegemonic/catharsis" + }, + "bugs": { + "url": "https://github.com/hegemonic/catharsis/issues" + }, + "main": "catharsis.js", + "dependencies": { + "underscore-contrib": "~0.3.0" + }, + "devDependencies": { + "mocha": "~1.21.3", + "pegjs": "https://github.com/dmajda/pegjs/tarball/76cc5d55b47ff3d5bbe1d435c6f843e2688cb729", + "should": "~4.0.4", + "tv4": "https://github.com/geraintluff/tv4/tarball/eb7561072d44943306e5fd88b55b4a4c98cb6c75", + "uglify-js": "~2.4.15" + }, + "engines": { + "node": ">= 0.8" + }, + "scripts": { + "build": "pegjs ./lib/parser.pegjs", + "prepublish": "pegjs ./lib/parser.pegjs; ./node_modules/.bin/uglifyjs ./lib/parser.js -o ./lib/parser.js", + "test": "mocha" + }, + "licenses": [ + { + "type": "MIT", + "url": "http://github.com/hegemonic/catharsis/raw/master/LICENSE" + } + ], + "readme": "# Catharsis #\n\nA JavaScript parser for\n[Google Closure Compiler](https://developers.google.com/closure/compiler/docs/js-for-compiler#types)\nand [JSDoc](https://github.com/jsdoc3/jsdoc) type expressions.\n\nCatharsis is designed to be:\n\n+ **Accurate**. Catharsis is based on a [PEG.js](http://pegjs.majda.cz/) grammar that's designed to\nhandle any valid type expression. It uses a [Mocha](http://visionmedia.github.com/mocha/) test suite\nto verify the parser's accuracy.\n+ **Fast**. Parse results are cached, so the parser is invoked only when necessary.\n+ **Flexible**. Catharsis can convert a parse result back into a type expression, or into a\ndescription of the type expression. In addition, Catharsis can parse\n[JSDoc](https://github.com/jsdoc3/jsdoc)-style type expressions.\n\n\n## Example ##\n\n```js\nvar catharsis = require('catharsis');\n\n// Google Closure Compiler parsing\nvar type = '!Object';\nvar parsedType;\ntry {\n parsedType = catharsis.parse(type); // {\"type\":\"NameExpression,\"name\":\"Object\",\"nullable\":false}\n} catch(e) {\n console.error('unable to parse %s: %s', type, e);\n}\n\n// JSDoc-style type expressions enabled\nvar jsdocType = 'string[]'; // Closure Compiler expects Array.\nvar parsedJsdocType;\ntry {\n parsedJsdocType = catharsis.parse(jsdocType, {jsdoc: true});\n} catch (e) {\n console.error('unable to parse %s: %s', jsdocType, e);\n}\n\n// Converting parse results back to type expressions\ncatharsis.stringify(parsedType); // !Object\ncatharsis.stringify(parsedJsdocType); // string[]\ncatharsis.stringify(parsedJsdocType, {restringify: true}); // Array.\n\n// Converting parse results to descriptions of the type expression\ncatharsis.describe(parsedType).simple; // non-null Object\ncatharsis.describe(parsedJsdocType).simple; // Array of string\n```\n\nSee the [test/specs directory](test/specs) for more examples of Catharsis' parse results.\n\n\n## Methods ##\n\n### parse(typeExpression, options) ###\nParse a type expression, and return the parse results. Throws an error if the type expression cannot\nbe parsed.\n\nWhen called without options, Catharsis attempts to parse type expressions in the same way as\nClosure Compiler. When the `jsdoc` option is enabled, Catharsis can also parse several kinds of\ntype expressions that are permitted in [JSDoc](https://github.com/jsdoc3/jsdoc):\n\n+ The string `function` is treated as a function type with no parameters.\n+ In a function type with repeatable parameters, the names of repeatable parameters are not required\nto be enclosed in square brackets (for example, `function(...foo)` is allowed).\n+ The period may be omitted from type applications. For example, `Array.` and\n`Array` will be parsed in the same way.\n+ You may append `[]` to a name expression (for example, `string[]`) to interpret it as a type\napplication with the expression `Array` (for example, `Array.`).\n+ Name expressions may contain the characters `#`, `~`, `:`, and `/`.\n+ Name expressions may contain a suffix that is similar to a function signature (for example,\n`MyClass(foo, bar)`).\n+ Name expressions may contain a reserved word.\n+ Record types may use types other than name expressions for keys.\n\n#### Parameters ####\n+ `type`: A string containing a Closure Compiler type expression.\n+ `options`: Options for parsing the type expression.\n + `options.jsdoc`: Specifies whether to enable parsing of JSDoc-style type expressions. Defaults\n to `false`.\n + `options.useCache`: Specifies whether to use the cache of parsed types. Defaults to `true`.\n\n#### Returns ####\nAn object containing the parse results. See the [test/specs directory](test/specs) for examples of\nthe parse results for different type expressions.\n\nThe object also includes two non-enumerable properties:\n\n+ `jsdoc`: A boolean indicating whether the type expression was parsed with JSDoc support enabled.\n+ `typeExpression`: A string containing the type expression that was parsed.\n\n### stringify(parsedType, options) ###\nStringify `parsedType`, and return the type expression. If validation is enabled, throws an error if\nthe stringified type expression cannot be parsed.\n\n#### Parameters ####\n+ `parsedType`: An object containing a parsed Closure Compiler type expression.\n+ `options`: Options for stringifying the parse results.\n + `options.cssClass`: Synonym for `options.linkClass`. Deprecated in version 0.8.0; will be\n removed in a future version.\n + `options.htmlSafe`: Specifies whether to return an HTML-safe string that replaces left angle\n brackets (`<`) with the corresponding entity (`<`). **Note**: Characters in name expressions\n are not escaped.\n + `options.linkClass`: A CSS class to add to HTML links. Used only if `options.links` is\n provided. By default, no CSS class is added.\n + `options.links`: An object whose keys are name expressions and whose values are URIs. If a\n name expression matches a key in `options.links`, the name expression will be wrapped in an\n HTML `` tag that links to the URI. If `options.linkClass` is specified, the `` tag will\n include a `class` attribute. **Note**: When using this option, parsed types are always\n restringified, and the resulting string is not cached.\n + `options.restringify`: Forces Catharsis to restringify the parsed type. If this option is not\n specified, and the parsed type object includes a `typeExpression` property, Catharsis will\n return the `typeExpression` property without modification when possible. Defaults to `false`.\n + `options.useCache`: Specifies whether to use the cache of stringified type expressions.\n Defaults to `true`.\n + `options.validate`: Specifies whether to validate the stringified parse results by attempting\n to parse them as a type expression. If the stringified results are not parsable by default, you\n must also provide the appropriate options to pass to the `parse()` method. Defaults to `false`.\n\n#### Returns ####\nA string containing the type expression.\n\n### describe(parsedType, options) ###\nConvert a parsed type to a description of the type expression. This method is especially useful if\nyour users are not familiar with the syntax for type expressions.\n\nThe `describe()` method returns the description in two formats:\n\n+ **Simple format**. A string that provides a complete description of the type expression.\n+ **Extended format**. An object that separates out some of the details about the outermost type\nexpression, such as whether the type is optional, nullable, or repeatable.\n\nFor example, if you call `describe('?function(new:MyObject, string)=')`, it returns the following\nobject:\n\n```js\n{\n simple: 'optional nullable function(constructs MyObject, string)',\n extended: {\n description: 'function(string)',\n modifiers: {\n functionNew: 'Returns MyObject when called with new.',\n functionThis: '',\n optional: 'Optional.',\n nullable: 'May be null.',\n repeatable: ''\n },\n returns: ''\n }\n}\n```\n\n#### Parameters ####\n+ `parsedType`: An object containing a parsed Closure Compiler type expression.\n+ `options`: Options for creating the description.\n + `options.codeClass`: A CSS class to add to the tag that is wrapped around type names. Used\n only if `options.codeTag` is provided. By default, no CSS class is added.\n + `options.codeTag`: The name of an HTML tag (for example, `code`) to wrap around type names.\n For example, if this option is set to `code`, the type expression `Array.` would have\n the simple description `Array of string`.\n + `options.language`: A string identifying the language in which to generate the description.\n The identifier should be an\n [ISO 639-1 language code](http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) (for example,\n `en`). It can optionally be followed by a hyphen and an\n [ISO 3166-1 alpha-2 country code](http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) (for example,\n `en-US`). If you use values other than `en`, you must provide translation resources in\n `options.resources`. Defaults to `en`.\n + `options.linkClass`: A CSS class to add to HTML links. Used only if `options.links` is\n provided. By default, no CSS class is added.\n + `options.links`: An object whose keys are name expressions and whose values are URIs. If a\n name expression matches a key in `options.links`, the name expression will be wrapped in an\n HTML `` tag that links to the URI. If `options.linkClass` is specified, the `` tag will\n include a `class` attribute. **Note**: When using this option, the description is not cached.\n + `options.resources`: An object that specifies how to describe type expressions for a given\n language. The object's property names should use the same format as `options.language`. Each\n property should contain an object in the same format as the translation resources in\n [res/en.json](res/en.json). If you specify a value for `options.resources.en`, it will override\n the defaults in [res/en.json](res/en.json).\n + `options.useCache`: Specifies whether to use the cache of descriptions. Defaults to `true`.\n\n### Returns ###\nAn object with the following properties:\n\n+ `simple`: A string that provides a complete description of the type expression.\n+ `extended`: An object containing details about the outermost type expression.\n + `extended.description`: A string that provides a basic description of the type expression,\n excluding the information contained in other properties.\n + `extended.modifiers`: Information about modifiers that apply to the type expression.\n + `extended.modifiers.functionNew`: A string describing what a function returns when called\n with `new`. Used only for function types.\n + `extended.modifiers.functionThis`: A string describing what the keyword `this` refers to\n within a function. Used only for function types.\n + `extended.modifiers.nullable`: A string indicating whether the type is nullable or\n non-nullable.\n + `extended.modifiers.optional`: A string indicating whether the type is optional.\n + `extended.modifiers.repeatable`: A string indicating whether the type can be provided\n + `extended.returns`: A string describing the function's return value. Used only for function\n types.\n\n\n## Installation ##\n\nWith [npm](http://npmjs.org):\n\n npm install catharsis\n\nOr without:\n\n git clone git://github.com/hegemonic/catharsis.git\n cd catharsis\n npm install\n\n\n## Roadmap and known issues ##\n\nTake a look at the [issue tracker](https://github.com/hegemonic/catharsis/issues) to see what's in\nstore for Catharsis.\n\nBug reports, feature requests, and pull requests are always welcome! If you're working on a large\npull request, please contact me in advance so I can help things go smoothly.\n\n**Note**: The parse tree's format should not be considered final until Catharsis reaches version\n1.0. I'll do my best to provide release notes for any changes.\n\n\n## Changelog ##\n\n+ 0.8.3 (October 2014):\n + Type applications are no longer required to include a period (`.`) as a separator, regardless\n of whether JSDoc-style type expressions are enabled.\n + Type unions that are not enclosed in parentheses can now include the repeatable (`...`)\n modifier when JSDoc-style type expressions are enabled.\n + Name expressions may now be enclosed in single or double quotation marks when JSDoc-style\n type expressions are enabled.\n+ 0.8.2 (June 2014): Fixed a compatibility issue with the JSDoc fork of Mozilla Rhino.\n+ 0.8.1 (June 2014): Added support for type unions that are not enclosed in parentheses, and that\ncontain nullable or non-nullable modifiers (for example, `!string|!number`).\n+ 0.8.0 (May 2014):\n + Added a `describe()` method, which converts a parsed type to a description of the type.\n + Added a `linkClass` option to the `stringify()` method, and deprecated the existing `cssClass`\n option. The `cssClass` option will be removed in a future release.\n + Clarified and corrected several sections in the `README`.\n+ 0.7.1 (April 2014): In record types, property names that begin with a keyword (for example,\n`undefinedHTML`) are now parsed correctly when JSDoc-style type expressions are enabled.\n+ 0.7.0 (October 2013):\n + Repeatable type expressions other than name expressions (for example, `...function()`) are now\n parsed and stringified correctly.\n + Type expressions that are both repeatable and either nullable or non-nullable (for example,\n `...!number`) are now parsed and stringified correctly.\n + Name expressions are now parsed correctly when they match a property name in an object\n instance (for example, `constructor`).\n+ 0.6.0 (September 2013): Added support for the type expression `function[]` when JSDoc-style type\nexpressions are enabled.\n+ 0.5.6 (April 2013):\n + For consistency with Google Closure Library, parentheses are no longer required around type\n unions. (In previous versions, the parentheses could be omitted when JSDoc support was enabled.)\n + For consistency with Google Closure Library, you can now use postfix notation for the `?`\n (nullable) and `!` (non-nullable) modifiers. For example, `?string` and `string?` are now\n treated as equivalent.\n + String literals and numeric literals are now allowed as property names within name\n expressions. For example, the name expression `Foo.\"bar\"` is now parsed correctly.\n+ 0.5.5 (April 2013): Corrected a parsing issue with name expressions that end with a value enclosed\nin parentheses.\n+ 0.5.4 (April 2013):\n + Repeatable literals (for example, `...*`) are now parsed correctly.\n + When JSDoc-style type expressions are enabled, a name expression can now contain a value\n enclosed in parentheses at the end of the name expression (for example, `MyClass(2)`).\n+ 0.5.3 (March 2013): The `parse()` method now correctly parses name expressions that contain\nhyphens.\n+ 0.5.2 (March 2013): The `parse()` method now correctly parses function types when JSDoc-style type\nexpressions are enabled.\n+ 0.5.1 (March 2013): Newlines and extra spaces are now removed from type expressions before they\nare parsed.\n+ 0.5.0 (March 2013):\n + The `parse()` method's `lenient` option has been renamed to `jsdoc`. **Note**: This change is\n not backwards-compatible with previous versions.\n + The `stringify()` method now accepts `cssClass` and `links` options, which you can use to\n add HTML links to a type expression.\n+ 0.4.3 (March 2013):\n + The `stringify()` method no longer caches HTML-safe type expressions as if they were normal\n type expressions.\n + The `stringify()` method's options parameter may now include an `options.restringify`\n property, and the behavior of the `options.useCache` property has changed.\n+ 0.4.2 (March 2013):\n + When lenient parsing is enabled, name expressions can now contain the characters `:` and `/`.\n + When lenient parsing is enabled, a name expression followed by `[]` (for example, `string[]`)\n will be interpreted as a type application with the expression `Array` (for example,\n `Array.`).\n+ 0.4.1 (March 2013):\n + The `parse()` and `stringify()` methods now honor all of the specified options.\n + When lenient parsing is enabled, name expressions can now contain a reserved word.\n+ 0.4.0 (March 2013):\n + Catharsis now supports a lenient parsing option that can parse several kinds of malformed type\n expressions. See the documentation for details.\n + The objects containing parse results are now frozen.\n + The objects containing parse results now have two non-enumerable properties:\n + `lenient`: A boolean indicating whether the type expression was parsed in lenient mode.\n + `typeExpression`: A string containing the original type expression.\n + The `stringify()` method now honors the `useCache` option. If a parsed type includes a\n `typeExpression` property, and `useCache` is not set to `false`, the stringified type will be\n identical to the original type expression.\n+ 0.3.1 (March 2013): Type expressions that begin with a reserved word, such as `integer`, are now\nparsed correctly.\n+ 0.3.0 (March 2013):\n + The `parse()` and `stringify()` methods are now synchronous, and the `parseSync()` and\n `stringifySync()` methods have been removed. **Note**: This change is not backwards-compatible\n with previous versions.\n + The parse results now use a significantly different format from previous versions. The new\n format is more expressive and is similar, but not identical, to the format used by the\n [doctrine](https://github.com/Constellation/doctrine) parser. **Note**: This change is not\n backwards-compatible with previous versions.\n + Name expressions that contain a reserved word now include a `reservedWord: true` property.\n + Union types that are optional or nullable, or that can be passed a variable number of times,\n are now parsed and stringified correctly.\n + Optional function types and record types are now parsed and stringified correctly.\n + Function types now longer include `new` or `this` properties unless the properties are defined\n in the type expression. In addition, the `new` and `this` properties can now use any type\n expression.\n + In record types, the key for a field type can now use any type expression.\n + Standalone single-character literals, such as ALL (`*`), are now parsed and stringified\n correctly.\n + `null` and `undefined` literals with additional properties, such as `repeatable`, are now\n stringified correctly.\n+ 0.2.0 (November 2012):\n + Added `stringify()` and `stringifySync()` methods, which convert a parsed type to a type\n expression.\n + Simplified the parse results for function signatures. **Note**: This change is not\n backwards-compatible with previous versions.\n + Corrected minor errors in README.md.\n+ 0.1.1 (November 2012): Added `opts` argument to `parse()` and `parseSync()` methods. **Note**: The\nchange to `parse()` is not backwards-compatible with previous versions.\n+ 0.1.0 (November 2012): Initial release.\n\n## License ##\n\n[MIT license](https://github.com/hegemonic/catharsis/blob/master/LICENSE).\n", + "readmeFilename": "README.md", + "gitHead": "8795105b00acf02d0af464ad3432f47b53744934", + "homepage": "https://github.com/hegemonic/catharsis", + "_id": "catharsis@0.8.3", + "_shasum": "573ad3d285dcfc373221712bd382edda61b3a5d5", + "_from": "catharsis@~0.8.3" +} diff --git a/third_party/jsdoc/node_modules/catharsis/res/en.json b/third_party/jsdoc/node_modules/catharsis/res/en.json new file mode 100644 index 0000000000..9afb501b3b --- /dev/null +++ b/third_party/jsdoc/node_modules/catharsis/res/en.json @@ -0,0 +1,91 @@ +{ + "all": "any type", + "application": { + "array": "<%= prefix %> <%= codeTagOpen %>Array<%= codeTagClose %> of <%= application %> <%= suffix %>", + "object": "<%= prefix %> <%= codeTagOpen %>Object<%= codeTagClose %> with <%= application %> properties <%= suffix %>", + "objectNonString": "<%= prefix %> <%= codeTagOpen %>Object<%= codeTagClose %> with <%= keyApplication %> keys and <%= application %> properties <%= suffix %>" + }, + "function": { + "extended": { + "new": "Returns <%= functionNew %> when called with <%= codeTagOpen %>new<%= codeTagClose %>.", + "returns": "Returns <%= type %>.", + "signature": "function(<%= functionParams %>)", + "this": "Within the function, <%= codeTagOpen %>this<%= codeTagClose %> refers to <%= functionThis %>." + }, + "simple": { + "new": "constructs <%= functionNew %>", + "returns": "returns <%= type %>", + "signature": "<%= prefix %> function(<%= functionParams %>) <%= functionReturns %>", + "this": "<%= codeTagOpen %>this<%= codeTagClose %> = <%= functionThis %>" + } + }, + "modifiers": { + "extended": { + "nonNullable": "Must not be null.", + "nullable": "May be null.", + "optional": "Optional.", + "prefix": "", + "repeatable": "May be provided more than once.", + "suffix": "" + }, + "simple": { + "nonNullable": "non-null", + "nullable": "nullable", + "optional": "optional", + "prefix": "<%= optional %> <%= nullable %> <%= repeatable %>", + "repeatable": "repeatable", + "suffix": "" + } + }, + "name": "<%= codeTagOpen %>{{ name }}<%= codeTagClose %> <%= suffix %>", + "null": "null", + "params": { + "first": { + "one": "<%= param %>", + "two": "<%= param %>, ", + "many": "<%= param %>, " + }, + "middle": { + "many": "<%= param %>, " + }, + "last": { + "two": "<%= param %>", + "many": "<%= param %>" + } + }, + "record": { + "first": { + "one": "<%= prefix %> {<%= field %>} <%= suffix %>", + "two": "<%= prefix %> {<%= field %>, ", + "many": "<%= prefix %> {<%= field %>, " + }, + "middle": { + "many": "<%= field %>, " + }, + "last": { + "two": "<%= field %>} <%= suffix %>", + "many": "<%= field %>} <%= suffix %>" + } + }, + "field": { + "typed": "<%= name %>: <%= type %>", + "untyped": "<%= name %>" + }, + "type": "<%= prefix %> <%= codeTagOpen %><%= type %><%= codeTagClose %> <%= suffix %>", + "undefined": "undefined", + "union": { + "first": { + "one": "<%= prefix %> <%= element %> <%= suffix %>", + "two": "<%= prefix %> (<%= element %> ", + "many": "<%= prefix %> (<%= element %>, " + }, + "middle": { + "many": "<%= element %>, " + }, + "last": { + "two": "or <%= element %>) <%= suffix %>", + "many": "or <%= element %>) <%= suffix %>" + } + }, + "unknown": "unknown type" +} diff --git a/third_party/jsdoc/node_modules/crypto-browserify/index.js b/third_party/jsdoc/node_modules/crypto-browserify/index.js new file mode 100644 index 0000000000..a6762ed486 --- /dev/null +++ b/third_party/jsdoc/node_modules/crypto-browserify/index.js @@ -0,0 +1,68 @@ +var sha = require('./sha') +var rng = require('./rng') + +var algorithms = { + sha1: { + hex: sha.hex_sha1, + binary: sha.b64_sha1, + ascii: sha.str_sha1 + } +} + +function error () { + var m = [].slice.call(arguments).join(' ') + throw new Error([ + m, + 'we accept pull requests', + 'http://github.com/dominictarr/crypto-browserify' + ].join('\n')) +} + +exports.createHash = function (alg) { + alg = alg || 'sha1' + if(!algorithms[alg]) + error('algorithm:', alg, 'is not yet supported') + var s = '' + var _alg = algorithms[alg] + return { + update: function (data) { + s += data + return this + }, + digest: function (enc) { + enc = enc || 'binary' + var fn + if(!(fn = _alg[enc])) + error('encoding:', enc , 'is not yet supported for algorithm', alg) + var r = fn(s) + s = null //not meant to use the hash after you've called digest. + return r + } + } +} + +exports.randomBytes = function(size, callback) { + if (callback && callback.call) { + try { + callback.call(this, undefined, rng(size)); + } catch (err) { callback(err); } + } else { + return rng(size); + } +} + +// the least I can do is make error messages for the rest of the node.js/crypto api. +;['createCredentials' +, 'createHmac' +, 'createCypher' +, 'createCypheriv' +, 'createDecipher' +, 'createDecipheriv' +, 'createSign' +, 'createVerify' +, 'createDeffieHellman' +, 'pbkdf2'].forEach(function (name) { + exports[name] = function () { + error('sorry,', name, 'is not implemented yet') + } +}) diff --git a/third_party/jsdoc/node_modules/crypto-browserify/package.json b/third_party/jsdoc/node_modules/crypto-browserify/package.json new file mode 100644 index 0000000000..99a6930c32 --- /dev/null +++ b/third_party/jsdoc/node_modules/crypto-browserify/package.json @@ -0,0 +1,29 @@ +{ + "author": { + "name": "Dominic Tarr", + "email": "dominic.tarr@gmail.com", + "url": "dominictarr.com" + }, + "name": "crypto-browserify", + "description": "partial implementation of crypto for the browser", + "version": "0.1.1", + "homepage": "https://github.com/dominictarr/crypto-browserify", + "repository": { + "url": "" + }, + "scripts": { + "test": "node test/simple.js" + }, + "engines": { + "node": "*" + }, + "dependencies": {}, + "devDependencies": {}, + "optionalDependencies": {}, + "readme": "# crypto-browserify\n\nA (partial) port of `crypto` to the browser.\n\nBasically, I found some crypto implemented in JS lieing on the internet somewhere\nand wrapped it in the part of the `crypto` api that I am currently using.\n\nIn a way that will be compatible with [browserify](https://github.com/substack/node-browserify/).\n\nI will extend this if I need more features, or if anyone else wants to extend this,\nI will add you as a maintainer.\n\nProvided that you agree that it should replicate the [node.js/crypto](http://nodejs.org/api/crypto.html) api exactly, of course.\n\n", + "_id": "crypto-browserify@0.1.1", + "dist": { + "shasum": "251b240c6bd0e95db0654fbc8b178b855cbef45e" + }, + "_from": "crypto-browserify@git://github.com/dominictarr/crypto-browserify.git#95c5d505" +} diff --git a/third_party/jsdoc/node_modules/crypto-browserify/rng.js b/third_party/jsdoc/node_modules/crypto-browserify/rng.js new file mode 100644 index 0000000000..2160788a15 --- /dev/null +++ b/third_party/jsdoc/node_modules/crypto-browserify/rng.js @@ -0,0 +1,37 @@ +// Original code adapted from Robert Kieffer. +// details at https://github.com/broofa/node-uuid +(function() { + var _global = this; + + var mathRNG, whatwgRNG; + + // NOTE: Math.random() does not guarantee "cryptographic quality" + mathRNG = function(size) { + var bytes = new Array(size); + var r; + + for (var i = 0, r; i < size; i++) { + if ((i & 0x03) == 0) r = Math.random() * 0x100000000; + bytes[i] = r >>> ((i & 0x03) << 3) & 0xff; + } + + return bytes; + } + + // currently only available in webkit-based browsers. + if (_global.crypto && crypto.getRandomValues) { + var _rnds = new Uint32Array(4); + whatwgRNG = function(size) { + var bytes = new Array(size); + crypto.getRandomValues(_rnds); + + for (var c = 0 ; c < size; c++) { + bytes[c] = _rnds[c >> 2] >>> ((c & 0x03) * 8) & 0xff; + } + return bytes; + } + } + + module.exports = whatwgRNG || mathRNG; + +}()) \ No newline at end of file diff --git a/third_party/jsdoc/node_modules/crypto-browserify/sha.js b/third_party/jsdoc/node_modules/crypto-browserify/sha.js new file mode 100644 index 0000000000..4f9cc3e97a --- /dev/null +++ b/third_party/jsdoc/node_modules/crypto-browserify/sha.js @@ -0,0 +1,210 @@ +/* + * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined + * in FIPS PUB 180-1 + * Version 2.1a Copyright Paul Johnston 2000 - 2002. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for details. + */ + +exports.hex_sha1 = hex_sha1; +exports.b64_sha1 = b64_sha1; +exports.str_sha1 = str_sha1; +exports.hex_hmac_sha1 = hex_hmac_sha1; +exports.b64_hmac_sha1 = b64_hmac_sha1; +exports.str_hmac_sha1 = str_hmac_sha1; + +/* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ +var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ +var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ +var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ + +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ +function hex_sha1(s){return binb2hex(core_sha1(str2binb(s),s.length * chrsz));} +function b64_sha1(s){return binb2b64(core_sha1(str2binb(s),s.length * chrsz));} +function str_sha1(s){return binb2str(core_sha1(str2binb(s),s.length * chrsz));} +function hex_hmac_sha1(key, data){ return binb2hex(core_hmac_sha1(key, data));} +function b64_hmac_sha1(key, data){ return binb2b64(core_hmac_sha1(key, data));} +function str_hmac_sha1(key, data){ return binb2str(core_hmac_sha1(key, data));} + +/* + * Perform a simple self-test to see if the VM is working + */ +function sha1_vm_test() +{ + return hex_sha1("abc") == "a9993e364706816aba3e25717850c26c9cd0d89d"; +} + +/* + * Calculate the SHA-1 of an array of big-endian words, and a bit length + */ +function core_sha1(x, len) +{ + /* append padding */ + x[len >> 5] |= 0x80 << (24 - len % 32); + x[((len + 64 >> 9) << 4) + 15] = len; + + var w = Array(80); + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + var e = -1009589776; + + for(var i = 0; i < x.length; i += 16) + { + var olda = a; + var oldb = b; + var oldc = c; + var oldd = d; + var olde = e; + + for(var j = 0; j < 80; j++) + { + if(j < 16) w[j] = x[i + j]; + else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); + var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), + safe_add(safe_add(e, w[j]), sha1_kt(j))); + e = d; + d = c; + c = rol(b, 30); + b = a; + a = t; + } + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + e = safe_add(e, olde); + } + return Array(a, b, c, d, e); + +} + +/* + * Perform the appropriate triplet combination function for the current + * iteration + */ +function sha1_ft(t, b, c, d) +{ + if(t < 20) return (b & c) | ((~b) & d); + if(t < 40) return b ^ c ^ d; + if(t < 60) return (b & c) | (b & d) | (c & d); + return b ^ c ^ d; +} + +/* + * Determine the appropriate additive constant for the current iteration + */ +function sha1_kt(t) +{ + return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : + (t < 60) ? -1894007588 : -899497514; +} + +/* + * Calculate the HMAC-SHA1 of a key and some data + */ +function core_hmac_sha1(key, data) +{ + var bkey = str2binb(key); + if(bkey.length > 16) bkey = core_sha1(bkey, key.length * chrsz); + + var ipad = Array(16), opad = Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * chrsz); + return core_sha1(opad.concat(hash), 512 + 160); +} + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function rol(num, cnt) +{ + return (num << cnt) | (num >>> (32 - cnt)); +} + +/* + * Convert an 8-bit or 16-bit string to an array of big-endian words + * In 8-bit function, characters >255 have their hi-byte silently ignored. + */ +function str2binb(str) +{ + var bin = Array(); + var mask = (1 << chrsz) - 1; + for(var i = 0; i < str.length * chrsz; i += chrsz) + bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32); + return bin; +} + +/* + * Convert an array of big-endian words to a string + */ +function binb2str(bin) +{ + var str = ""; + var mask = (1 << chrsz) - 1; + for(var i = 0; i < bin.length * 32; i += chrsz) + str += String.fromCharCode((bin[i>>5] >>> (32 - chrsz - i%32)) & mask); + return str; +} + +/* + * Convert an array of big-endian words to a hex string. + */ +function binb2hex(binarray) +{ + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i++) + { + str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF); + } + return str; +} + +/* + * Convert an array of big-endian words to a base-64 string + */ +function binb2b64(binarray) +{ + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i += 3) + { + var triplet = (((binarray[i >> 2] >> 8 * (3 - i %4)) & 0xFF) << 16) + | (((binarray[i+1 >> 2] >> 8 * (3 - (i+1)%4)) & 0xFF) << 8 ) + | ((binarray[i+2 >> 2] >> 8 * (3 - (i+2)%4)) & 0xFF); + for(var j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; + else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); + } + } + return str; +} + diff --git a/third_party/jsdoc/node_modules/escape-string-regexp/index.js b/third_party/jsdoc/node_modules/escape-string-regexp/index.js new file mode 100644 index 0000000000..ac6572cabe --- /dev/null +++ b/third_party/jsdoc/node_modules/escape-string-regexp/index.js @@ -0,0 +1,11 @@ +'use strict'; + +var matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g; + +module.exports = function (str) { + if (typeof str !== 'string') { + throw new TypeError('Expected a string'); + } + + return str.replace(matchOperatorsRe, '\\$&'); +}; diff --git a/third_party/jsdoc/node_modules/escape-string-regexp/package.json b/third_party/jsdoc/node_modules/escape-string-regexp/package.json new file mode 100644 index 0000000000..5da6f01d80 --- /dev/null +++ b/third_party/jsdoc/node_modules/escape-string-regexp/package.json @@ -0,0 +1,69 @@ +{ + "name": "escape-string-regexp", + "version": "1.0.2", + "description": "Escape RegExp special characters", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/sindresorhus/escape-string-regexp" + }, + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "http://sindresorhus.com" + }, + "engines": { + "node": ">=0.8.0" + }, + "scripts": { + "test": "mocha" + }, + "files": [ + "index.js" + ], + "keywords": [ + "regex", + "regexp", + "re", + "regular", + "expression", + "escape", + "string", + "str", + "special", + "characters" + ], + "devDependencies": { + "mocha": "*" + }, + "gitHead": "0587ee0ee03ea3fcbfa3c15cf67b47f214e20987", + "bugs": { + "url": "https://github.com/sindresorhus/escape-string-regexp/issues" + }, + "homepage": "https://github.com/sindresorhus/escape-string-regexp", + "_id": "escape-string-regexp@1.0.2", + "_shasum": "4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1", + "_from": "escape-string-regexp@1.0.2", + "_npmVersion": "1.4.23", + "_npmUser": { + "name": "jbnicolai", + "email": "jappelman@xebia.com" + }, + "maintainers": [ + { + "name": "sindresorhus", + "email": "sindresorhus@gmail.com" + }, + { + "name": "jbnicolai", + "email": "jappelman@xebia.com" + } + ], + "dist": { + "shasum": "4dbc2fe674e71949caf3fb2695ce7f2dc1d9a8d1", + "tarball": "http://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz" + }, + "directories": {}, + "_resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", + "readme": "ERROR: No README data found!" +} diff --git a/third_party/jsdoc/node_modules/escape-string-regexp/readme.md b/third_party/jsdoc/node_modules/escape-string-regexp/readme.md new file mode 100644 index 0000000000..808a963a86 --- /dev/null +++ b/third_party/jsdoc/node_modules/escape-string-regexp/readme.md @@ -0,0 +1,27 @@ +# escape-string-regexp [![Build Status](https://travis-ci.org/sindresorhus/escape-string-regexp.svg?branch=master)](https://travis-ci.org/sindresorhus/escape-string-regexp) + +> Escape RegExp special characters + + +## Install + +```sh +$ npm install --save escape-string-regexp +``` + + +## Usage + +```js +var escapeStringRegexp = require('escape-string-regexp'); + +var escapedString = escapeStringRegexp('how much $ for a unicorn?'); +//=> how much \$ for a unicorn\? + +new RegExp(escapedString); +``` + + +## License + +MIT © [Sindre Sorhus](http://sindresorhus.com) diff --git a/third_party/jsdoc/node_modules/esprima/esprima.js b/third_party/jsdoc/node_modules/esprima/esprima.js new file mode 100644 index 0000000000..5fa5d22fae --- /dev/null +++ b/third_party/jsdoc/node_modules/esprima/esprima.js @@ -0,0 +1,5195 @@ +/* + Copyright (C) 2013 Ariya Hidayat + Copyright (C) 2013 Thaddee Tyl + Copyright (C) 2012 Ariya Hidayat + Copyright (C) 2012 Mathias Bynens + Copyright (C) 2012 Joost-Wim Boekesteijn + Copyright (C) 2012 Kris Kowal + Copyright (C) 2012 Yusuke Suzuki + Copyright (C) 2012 Arpad Borsos + Copyright (C) 2011 Ariya Hidayat + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/*jslint bitwise:true plusplus:true */ +/*global esprima:true, define:true, exports:true, window: true, +throwError: true, generateStatement: true, peek: true, +parseAssignmentExpression: true, parseBlock: true, +parseClassExpression: true, parseClassDeclaration: true, parseExpression: true, +parseForStatement: true, +parseFunctionDeclaration: true, parseFunctionExpression: true, +parseFunctionSourceElements: true, parseVariableIdentifier: true, +parseImportSpecifier: true, +parseLeftHandSideExpression: true, parseParams: true, validateParam: true, +parseSpreadOrAssignmentExpression: true, +parseStatement: true, parseSourceElement: true, parseModuleBlock: true, parseConciseBody: true, +parseYieldExpression: true +*/ + +(function (root, factory) { + 'use strict'; + + // Universal Module Definition (UMD) to support AMD, CommonJS/Node.js, + // Rhino, and plain browser loading. + if (typeof define === 'function' && define.amd) { + define(['exports'], factory); + } else if (typeof exports !== 'undefined') { + factory(exports); + } else { + factory((root.esprima = {})); + } +}(this, function (exports) { + 'use strict'; + + var Token, + TokenName, + FnExprTokens, + Syntax, + PropertyKind, + Messages, + Regex, + SyntaxTreeDelegate, + ClassPropertyType, + source, + strict, + index, + lineNumber, + lineStart, + length, + delegate, + lookahead, + state, + extra; + + Token = { + BooleanLiteral: 1, + EOF: 2, + Identifier: 3, + Keyword: 4, + NullLiteral: 5, + NumericLiteral: 6, + Punctuator: 7, + StringLiteral: 8, + RegularExpression: 9, + Template: 10 + }; + + TokenName = {}; + TokenName[Token.BooleanLiteral] = 'Boolean'; + TokenName[Token.EOF] = ''; + TokenName[Token.Identifier] = 'Identifier'; + TokenName[Token.Keyword] = 'Keyword'; + TokenName[Token.NullLiteral] = 'Null'; + TokenName[Token.NumericLiteral] = 'Numeric'; + TokenName[Token.Punctuator] = 'Punctuator'; + TokenName[Token.StringLiteral] = 'String'; + TokenName[Token.RegularExpression] = 'RegularExpression'; + + // A function following one of those tokens is an expression. + FnExprTokens = ['(', '{', '[', 'in', 'typeof', 'instanceof', 'new', + 'return', 'case', 'delete', 'throw', 'void', + // assignment operators + '=', '+=', '-=', '*=', '/=', '%=', '<<=', '>>=', '>>>=', + '&=', '|=', '^=', ',', + // binary/unary operators + '+', '-', '*', '/', '%', '++', '--', '<<', '>>', '>>>', '&', + '|', '^', '!', '~', '&&', '||', '?', ':', '===', '==', '>=', + '<=', '<', '>', '!=', '!==']; + + Syntax = { + ArrayExpression: 'ArrayExpression', + ArrayPattern: 'ArrayPattern', + ArrowFunctionExpression: 'ArrowFunctionExpression', + AssignmentExpression: 'AssignmentExpression', + BinaryExpression: 'BinaryExpression', + BlockStatement: 'BlockStatement', + BreakStatement: 'BreakStatement', + CallExpression: 'CallExpression', + CatchClause: 'CatchClause', + ClassBody: 'ClassBody', + ClassDeclaration: 'ClassDeclaration', + ClassExpression: 'ClassExpression', + ComprehensionBlock: 'ComprehensionBlock', + ComprehensionExpression: 'ComprehensionExpression', + ConditionalExpression: 'ConditionalExpression', + ContinueStatement: 'ContinueStatement', + DebuggerStatement: 'DebuggerStatement', + DoWhileStatement: 'DoWhileStatement', + EmptyStatement: 'EmptyStatement', + ExportDeclaration: 'ExportDeclaration', + ExportBatchSpecifier: 'ExportBatchSpecifier', + ExportSpecifier: 'ExportSpecifier', + ExpressionStatement: 'ExpressionStatement', + ForInStatement: 'ForInStatement', + ForOfStatement: 'ForOfStatement', + ForStatement: 'ForStatement', + FunctionDeclaration: 'FunctionDeclaration', + FunctionExpression: 'FunctionExpression', + Identifier: 'Identifier', + IfStatement: 'IfStatement', + ImportDeclaration: 'ImportDeclaration', + ImportSpecifier: 'ImportSpecifier', + LabeledStatement: 'LabeledStatement', + Literal: 'Literal', + LogicalExpression: 'LogicalExpression', + MemberExpression: 'MemberExpression', + MethodDefinition: 'MethodDefinition', + ModuleDeclaration: 'ModuleDeclaration', + NewExpression: 'NewExpression', + ObjectExpression: 'ObjectExpression', + ObjectPattern: 'ObjectPattern', + Program: 'Program', + Property: 'Property', + ReturnStatement: 'ReturnStatement', + SequenceExpression: 'SequenceExpression', + SpreadElement: 'SpreadElement', + SwitchCase: 'SwitchCase', + SwitchStatement: 'SwitchStatement', + TaggedTemplateExpression: 'TaggedTemplateExpression', + TemplateElement: 'TemplateElement', + TemplateLiteral: 'TemplateLiteral', + ThisExpression: 'ThisExpression', + ThrowStatement: 'ThrowStatement', + TryStatement: 'TryStatement', + UnaryExpression: 'UnaryExpression', + UpdateExpression: 'UpdateExpression', + VariableDeclaration: 'VariableDeclaration', + VariableDeclarator: 'VariableDeclarator', + WhileStatement: 'WhileStatement', + WithStatement: 'WithStatement', + YieldExpression: 'YieldExpression' + }; + + PropertyKind = { + Data: 1, + Get: 2, + Set: 4 + }; + + ClassPropertyType = { + 'static': 'static', + prototype: 'prototype' + }; + + // Error messages should be identical to V8. + Messages = { + UnexpectedToken: 'Unexpected token %0', + UnexpectedNumber: 'Unexpected number', + UnexpectedString: 'Unexpected string', + UnexpectedIdentifier: 'Unexpected identifier', + UnexpectedReserved: 'Unexpected reserved word', + UnexpectedTemplate: 'Unexpected quasi %0', + UnexpectedEOS: 'Unexpected end of input', + NewlineAfterThrow: 'Illegal newline after throw', + InvalidRegExp: 'Invalid regular expression', + UnterminatedRegExp: 'Invalid regular expression: missing /', + InvalidLHSInAssignment: 'Invalid left-hand side in assignment', + InvalidLHSInFormalsList: 'Invalid left-hand side in formals list', + InvalidLHSInForIn: 'Invalid left-hand side in for-in', + MultipleDefaultsInSwitch: 'More than one default clause in switch statement', + NoCatchOrFinally: 'Missing catch or finally after try', + UnknownLabel: 'Undefined label \'%0\'', + Redeclaration: '%0 \'%1\' has already been declared', + IllegalContinue: 'Illegal continue statement', + IllegalBreak: 'Illegal break statement', + IllegalDuplicateClassProperty: 'Illegal duplicate property in class definition', + IllegalReturn: 'Illegal return statement', + IllegalYield: 'Illegal yield expression', + IllegalSpread: 'Illegal spread element', + StrictModeWith: 'Strict mode code may not include a with statement', + StrictCatchVariable: 'Catch variable may not be eval or arguments in strict mode', + StrictVarName: 'Variable name may not be eval or arguments in strict mode', + StrictParamName: 'Parameter name eval or arguments is not allowed in strict mode', + StrictParamDupe: 'Strict mode function may not have duplicate parameter names', + ParameterAfterRestParameter: 'Rest parameter must be final parameter of an argument list', + DefaultRestParameter: 'Rest parameter can not have a default value', + ElementAfterSpreadElement: 'Spread must be the final element of an element list', + ObjectPatternAsRestParameter: 'Invalid rest parameter', + ObjectPatternAsSpread: 'Invalid spread argument', + StrictFunctionName: 'Function name may not be eval or arguments in strict mode', + StrictOctalLiteral: 'Octal literals are not allowed in strict mode.', + StrictDelete: 'Delete of an unqualified identifier in strict mode.', + StrictDuplicateProperty: 'Duplicate data property in object literal not allowed in strict mode', + AccessorDataProperty: 'Object literal may not have data and accessor property with the same name', + AccessorGetSet: 'Object literal may not have multiple get/set accessors with the same name', + StrictLHSAssignment: 'Assignment to eval or arguments is not allowed in strict mode', + StrictLHSPostfix: 'Postfix increment/decrement may not have eval or arguments operand in strict mode', + StrictLHSPrefix: 'Prefix increment/decrement may not have eval or arguments operand in strict mode', + StrictReservedWord: 'Use of future reserved word in strict mode', + NewlineAfterModule: 'Illegal newline after module', + NoFromAfterImport: 'Missing from after import', + InvalidModuleSpecifier: 'Invalid module specifier', + NestedModule: 'Module declaration can not be nested', + NoUnintializedConst: 'Const must be initialized', + ComprehensionRequiresBlock: 'Comprehension must have at least one block', + ComprehensionError: 'Comprehension Error', + EachNotAllowed: 'Each is not supported' + }; + + // See also tools/generate-unicode-regex.py. + Regex = { + NonAsciiIdentifierStart: new RegExp('[\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u08a0\u08a2-\u08ac\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097f\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58\u0c59\u0c60\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d60\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191c\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19c1-\u19c7\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua697\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua793\ua7a0-\ua7aa\ua7f8-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa80-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]'), + NonAsciiIdentifierPart: new RegExp('[\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0300-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u0483-\u0487\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u05d0-\u05ea\u05f0-\u05f2\u0610-\u061a\u0620-\u0669\u066e-\u06d3\u06d5-\u06dc\u06df-\u06e8\u06ea-\u06fc\u06ff\u0710-\u074a\u074d-\u07b1\u07c0-\u07f5\u07fa\u0800-\u082d\u0840-\u085b\u08a0\u08a2-\u08ac\u08e4-\u08fe\u0900-\u0963\u0966-\u096f\u0971-\u0977\u0979-\u097f\u0981-\u0983\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bc-\u09c4\u09c7\u09c8\u09cb-\u09ce\u09d7\u09dc\u09dd\u09df-\u09e3\u09e6-\u09f1\u0a01-\u0a03\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a59-\u0a5c\u0a5e\u0a66-\u0a75\u0a81-\u0a83\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abc-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ad0\u0ae0-\u0ae3\u0ae6-\u0aef\u0b01-\u0b03\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3c-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b5c\u0b5d\u0b5f-\u0b63\u0b66-\u0b6f\u0b71\u0b82\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd0\u0bd7\u0be6-\u0bef\u0c01-\u0c03\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c58\u0c59\u0c60-\u0c63\u0c66-\u0c6f\u0c82\u0c83\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbc-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0cde\u0ce0-\u0ce3\u0ce6-\u0cef\u0cf1\u0cf2\u0d02\u0d03\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d-\u0d44\u0d46-\u0d48\u0d4a-\u0d4e\u0d57\u0d60-\u0d63\u0d66-\u0d6f\u0d7a-\u0d7f\u0d82\u0d83\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e01-\u0e3a\u0e40-\u0e4e\u0e50-\u0e59\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb9\u0ebb-\u0ebd\u0ec0-\u0ec4\u0ec6\u0ec8-\u0ecd\u0ed0-\u0ed9\u0edc-\u0edf\u0f00\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e-\u0f47\u0f49-\u0f6c\u0f71-\u0f84\u0f86-\u0f97\u0f99-\u0fbc\u0fc6\u1000-\u1049\u1050-\u109d\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u135d-\u135f\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176c\u176e-\u1770\u1772\u1773\u1780-\u17d3\u17d7\u17dc\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u1820-\u1877\u1880-\u18aa\u18b0-\u18f5\u1900-\u191c\u1920-\u192b\u1930-\u193b\u1946-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u19d0-\u19d9\u1a00-\u1a1b\u1a20-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1aa7\u1b00-\u1b4b\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1bf3\u1c00-\u1c37\u1c40-\u1c49\u1c4d-\u1c7d\u1cd0-\u1cd2\u1cd4-\u1cf6\u1d00-\u1de6\u1dfc-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u200c\u200d\u203f\u2040\u2054\u2071\u207f\u2090-\u209c\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d7f-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2de0-\u2dff\u2e2f\u3005-\u3007\u3021-\u302f\u3031-\u3035\u3038-\u303c\u3041-\u3096\u3099\u309a\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua62b\ua640-\ua66f\ua674-\ua67d\ua67f-\ua697\ua69f-\ua6f1\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua793\ua7a0-\ua7aa\ua7f8-\ua827\ua840-\ua873\ua880-\ua8c4\ua8d0-\ua8d9\ua8e0-\ua8f7\ua8fb\ua900-\ua92d\ua930-\ua953\ua960-\ua97c\ua980-\ua9c0\ua9cf-\ua9d9\uaa00-\uaa36\uaa40-\uaa4d\uaa50-\uaa59\uaa60-\uaa76\uaa7a\uaa7b\uaa80-\uaac2\uaadb-\uaadd\uaae0-\uaaef\uaaf2-\uaaf6\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabea\uabec\uabed\uabf0-\uabf9\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe00-\ufe0f\ufe20-\ufe26\ufe33\ufe34\ufe4d-\ufe4f\ufe70-\ufe74\ufe76-\ufefc\uff10-\uff19\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]') + }; + + // Ensure the condition is true, otherwise throw an error. + // This is only to have a better contract semantic, i.e. another safety net + // to catch a logic error. The condition shall be fulfilled in normal case. + // Do NOT use this to enforce a certain condition on any user input. + + function assert(condition, message) { + if (!condition) { + throw new Error('ASSERT: ' + message); + } + } + + function isDecimalDigit(ch) { + return (ch >= 48 && ch <= 57); // 0..9 + } + + function isHexDigit(ch) { + return '0123456789abcdefABCDEF'.indexOf(ch) >= 0; + } + + function isOctalDigit(ch) { + return '01234567'.indexOf(ch) >= 0; + } + + + // 7.2 White Space + + function isWhiteSpace(ch) { + return (ch === 32) || // space + (ch === 9) || // tab + (ch === 0xB) || + (ch === 0xC) || + (ch === 0xA0) || + (ch >= 0x1680 && '\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\uFEFF'.indexOf(String.fromCharCode(ch)) > 0); + } + + // 7.3 Line Terminators + + function isLineTerminator(ch) { + return (ch === 10) || (ch === 13) || (ch === 0x2028) || (ch === 0x2029); + } + + // 7.6 Identifier Names and Identifiers + + function isIdentifierStart(ch) { + return (ch === 36) || (ch === 95) || // $ (dollar) and _ (underscore) + (ch >= 65 && ch <= 90) || // A..Z + (ch >= 97 && ch <= 122) || // a..z + (ch === 92) || // \ (backslash) + ((ch >= 0x80) && Regex.NonAsciiIdentifierStart.test(String.fromCharCode(ch))); + } + + function isIdentifierPart(ch) { + return (ch === 36) || (ch === 95) || // $ (dollar) and _ (underscore) + (ch >= 65 && ch <= 90) || // A..Z + (ch >= 97 && ch <= 122) || // a..z + (ch >= 48 && ch <= 57) || // 0..9 + (ch === 92) || // \ (backslash) + ((ch >= 0x80) && Regex.NonAsciiIdentifierPart.test(String.fromCharCode(ch))); + } + + // 7.6.1.2 Future Reserved Words + + function isFutureReservedWord(id) { + switch (id) { + case 'class': + case 'enum': + case 'export': + case 'extends': + case 'import': + case 'super': + return true; + default: + return false; + } + } + + function isStrictModeReservedWord(id) { + switch (id) { + case 'implements': + case 'interface': + case 'package': + case 'private': + case 'protected': + case 'public': + case 'static': + case 'yield': + case 'let': + return true; + default: + return false; + } + } + + function isRestrictedWord(id) { + return id === 'eval' || id === 'arguments'; + } + + // 7.6.1.1 Keywords + + function isKeyword(id) { + if (strict && isStrictModeReservedWord(id)) { + return true; + } + + // 'const' is specialized as Keyword in V8. + // 'yield' is only treated as a keyword in strict mode. + // 'let' is for compatiblity with SpiderMonkey and ES.next. + // Some others are from future reserved words. + + switch (id.length) { + case 2: + return (id === 'if') || (id === 'in') || (id === 'do'); + case 3: + return (id === 'var') || (id === 'for') || (id === 'new') || + (id === 'try') || (id === 'let'); + case 4: + return (id === 'this') || (id === 'else') || (id === 'case') || + (id === 'void') || (id === 'with') || (id === 'enum'); + case 5: + return (id === 'while') || (id === 'break') || (id === 'catch') || + (id === 'throw') || (id === 'const') || + (id === 'class') || (id === 'super'); + case 6: + return (id === 'return') || (id === 'typeof') || (id === 'delete') || + (id === 'switch') || (id === 'export') || (id === 'import'); + case 7: + return (id === 'default') || (id === 'finally') || (id === 'extends'); + case 8: + return (id === 'function') || (id === 'continue') || (id === 'debugger'); + case 10: + return (id === 'instanceof'); + default: + return false; + } + } + + // 7.4 Comments + + function skipComment() { + var ch, blockComment, lineComment; + + blockComment = false; + lineComment = false; + + while (index < length) { + ch = source.charCodeAt(index); + + if (lineComment) { + ++index; + if (isLineTerminator(ch)) { + lineComment = false; + if (ch === 13 && source.charCodeAt(index) === 10) { + ++index; + } + ++lineNumber; + lineStart = index; + } + } else if (blockComment) { + if (isLineTerminator(ch)) { + if (ch === 13 && source.charCodeAt(index + 1) === 10) { + ++index; + } + ++lineNumber; + ++index; + lineStart = index; + if (index >= length) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } else { + ch = source.charCodeAt(index++); + if (index >= length) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + // Block comment ends with '*/' (char #42, char #47). + if (ch === 42) { + ch = source.charCodeAt(index); + if (ch === 47) { + ++index; + blockComment = false; + } + } + } + } else if (ch === 47) { + ch = source.charCodeAt(index + 1); + // Line comment starts with '//' (char #47, char #47). + if (ch === 47) { + index += 2; + lineComment = true; + } else if (ch === 42) { + // Block comment starts with '/*' (char #47, char #42). + index += 2; + blockComment = true; + if (index >= length) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } else { + break; + } + } else if (isWhiteSpace(ch)) { + ++index; + } else if (isLineTerminator(ch)) { + ++index; + if (ch === 13 && source.charCodeAt(index) === 10) { + ++index; + } + ++lineNumber; + lineStart = index; + } else { + break; + } + } + } + + function scanHexEscape(prefix) { + var i, len, ch, code = 0; + + len = (prefix === 'u') ? 4 : 2; + for (i = 0; i < len; ++i) { + if (index < length && isHexDigit(source[index])) { + ch = source[index++]; + code = code * 16 + '0123456789abcdef'.indexOf(ch.toLowerCase()); + } else { + return ''; + } + } + return String.fromCharCode(code); + } + + function scanUnicodeCodePointEscape() { + var ch, code, cu1, cu2; + + ch = source[index]; + code = 0; + + // At least, one hex digit is required. + if (ch === '}') { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + while (index < length) { + ch = source[index++]; + if (!isHexDigit(ch)) { + break; + } + code = code * 16 + '0123456789abcdef'.indexOf(ch.toLowerCase()); + } + + if (code > 0x10FFFF || ch !== '}') { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + // UTF-16 Encoding + if (code <= 0xFFFF) { + return String.fromCharCode(code); + } + cu1 = ((code - 0x10000) >> 10) + 0xD800; + cu2 = ((code - 0x10000) & 1023) + 0xDC00; + return String.fromCharCode(cu1, cu2); + } + + function getEscapedIdentifier() { + var ch, id; + + ch = source.charCodeAt(index++); + id = String.fromCharCode(ch); + + // '\u' (char #92, char #117) denotes an escaped character. + if (ch === 92) { + if (source.charCodeAt(index) !== 117) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + ++index; + ch = scanHexEscape('u'); + if (!ch || ch === '\\' || !isIdentifierStart(ch.charCodeAt(0))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + id = ch; + } + + while (index < length) { + ch = source.charCodeAt(index); + if (!isIdentifierPart(ch)) { + break; + } + ++index; + id += String.fromCharCode(ch); + + // '\u' (char #92, char #117) denotes an escaped character. + if (ch === 92) { + id = id.substr(0, id.length - 1); + if (source.charCodeAt(index) !== 117) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + ++index; + ch = scanHexEscape('u'); + if (!ch || ch === '\\' || !isIdentifierPart(ch.charCodeAt(0))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + id += ch; + } + } + + return id; + } + + function getIdentifier() { + var start, ch; + + start = index++; + while (index < length) { + ch = source.charCodeAt(index); + if (ch === 92) { + // Blackslash (char #92) marks Unicode escape sequence. + index = start; + return getEscapedIdentifier(); + } + if (isIdentifierPart(ch)) { + ++index; + } else { + break; + } + } + + return source.slice(start, index); + } + + function scanIdentifier() { + var start, id, type; + + start = index; + + // Backslash (char #92) starts an escaped character. + id = (source.charCodeAt(index) === 92) ? getEscapedIdentifier() : getIdentifier(); + + // There is no keyword or literal with only one character. + // Thus, it must be an identifier. + if (id.length === 1) { + type = Token.Identifier; + } else if (isKeyword(id)) { + type = Token.Keyword; + } else if (id === 'null') { + type = Token.NullLiteral; + } else if (id === 'true' || id === 'false') { + type = Token.BooleanLiteral; + } else { + type = Token.Identifier; + } + + return { + type: type, + value: id, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + + // 7.7 Punctuators + + function scanPunctuator() { + var start = index, + code = source.charCodeAt(index), + code2, + ch1 = source[index], + ch2, + ch3, + ch4; + + switch (code) { + // Check for most common single-character punctuators. + case 40: // ( open bracket + case 41: // ) close bracket + case 59: // ; semicolon + case 44: // , comma + case 123: // { open curly brace + case 125: // } close curly brace + case 91: // [ + case 93: // ] + case 58: // : + case 63: // ? + case 126: // ~ + ++index; + if (extra.tokenize) { + if (code === 40) { + extra.openParenToken = extra.tokens.length; + } else if (code === 123) { + extra.openCurlyToken = extra.tokens.length; + } + } + return { + type: Token.Punctuator, + value: String.fromCharCode(code), + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + + default: + code2 = source.charCodeAt(index + 1); + + // '=' (char #61) marks an assignment or comparison operator. + if (code2 === 61) { + switch (code) { + case 37: // % + case 38: // & + case 42: // *: + case 43: // + + case 45: // - + case 47: // / + case 60: // < + case 62: // > + case 94: // ^ + case 124: // | + index += 2; + return { + type: Token.Punctuator, + value: String.fromCharCode(code) + String.fromCharCode(code2), + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + + case 33: // ! + case 61: // = + index += 2; + + // !== and === + if (source.charCodeAt(index) === 61) { + ++index; + } + return { + type: Token.Punctuator, + value: source.slice(start, index), + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + default: + break; + } + } + break; + } + + // Peek more characters. + + ch2 = source[index + 1]; + ch3 = source[index + 2]; + ch4 = source[index + 3]; + + // 4-character punctuator: >>>= + + if (ch1 === '>' && ch2 === '>' && ch3 === '>') { + if (ch4 === '=') { + index += 4; + return { + type: Token.Punctuator, + value: '>>>=', + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + } + + // 3-character punctuators: === !== >>> <<= >>= + + if (ch1 === '>' && ch2 === '>' && ch3 === '>') { + index += 3; + return { + type: Token.Punctuator, + value: '>>>', + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + if (ch1 === '<' && ch2 === '<' && ch3 === '=') { + index += 3; + return { + type: Token.Punctuator, + value: '<<=', + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + if (ch1 === '>' && ch2 === '>' && ch3 === '=') { + index += 3; + return { + type: Token.Punctuator, + value: '>>=', + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + if (ch1 === '.' && ch2 === '.' && ch3 === '.') { + index += 3; + return { + type: Token.Punctuator, + value: '...', + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + // Other 2-character punctuators: ++ -- << >> && || + + if (ch1 === ch2 && ('+-<>&|'.indexOf(ch1) >= 0)) { + index += 2; + return { + type: Token.Punctuator, + value: ch1 + ch2, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + if (ch1 === '=' && ch2 === '>') { + index += 2; + return { + type: Token.Punctuator, + value: '=>', + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + if ('<>=!+-*%&|^/'.indexOf(ch1) >= 0) { + ++index; + return { + type: Token.Punctuator, + value: ch1, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + if (ch1 === '.') { + ++index; + return { + type: Token.Punctuator, + value: ch1, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + // 7.8.3 Numeric Literals + + function scanHexLiteral(start) { + var number = ''; + + while (index < length) { + if (!isHexDigit(source[index])) { + break; + } + number += source[index++]; + } + + if (number.length === 0) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + if (isIdentifierStart(source.charCodeAt(index))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + return { + type: Token.NumericLiteral, + value: parseInt('0x' + number, 16), + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + function scanOctalLiteral(prefix, start) { + var number, octal; + + if (isOctalDigit(prefix)) { + octal = true; + number = '0' + source[index++]; + } else { + octal = false; + ++index; + number = ''; + } + + while (index < length) { + if (!isOctalDigit(source[index])) { + break; + } + number += source[index++]; + } + + if (!octal && number.length === 0) { + // only 0o or 0O + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + if (isIdentifierStart(source.charCodeAt(index)) || isDecimalDigit(source.charCodeAt(index))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + return { + type: Token.NumericLiteral, + value: parseInt(number, 8), + octal: octal, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + function scanNumericLiteral() { + var number, start, ch, octal; + + ch = source[index]; + assert(isDecimalDigit(ch.charCodeAt(0)) || (ch === '.'), + 'Numeric literal must start with a decimal digit or a decimal point'); + + start = index; + number = ''; + if (ch !== '.') { + number = source[index++]; + ch = source[index]; + + // Hex number starts with '0x'. + // Octal number starts with '0'. + // Octal number in ES6 starts with '0o'. + // Binary number in ES6 starts with '0b'. + if (number === '0') { + if (ch === 'x' || ch === 'X') { + ++index; + return scanHexLiteral(start); + } + if (ch === 'b' || ch === 'B') { + ++index; + number = ''; + + while (index < length) { + ch = source[index]; + if (ch !== '0' && ch !== '1') { + break; + } + number += source[index++]; + } + + if (number.length === 0) { + // only 0b or 0B + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + if (index < length) { + ch = source.charCodeAt(index); + if (isIdentifierStart(ch) || isDecimalDigit(ch)) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } + return { + type: Token.NumericLiteral, + value: parseInt(number, 2), + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + if (ch === 'o' || ch === 'O' || isOctalDigit(ch)) { + return scanOctalLiteral(ch, start); + } + // decimal number starts with '0' such as '09' is illegal. + if (ch && isDecimalDigit(ch.charCodeAt(0))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } + + while (isDecimalDigit(source.charCodeAt(index))) { + number += source[index++]; + } + ch = source[index]; + } + + if (ch === '.') { + number += source[index++]; + while (isDecimalDigit(source.charCodeAt(index))) { + number += source[index++]; + } + ch = source[index]; + } + + if (ch === 'e' || ch === 'E') { + number += source[index++]; + + ch = source[index]; + if (ch === '+' || ch === '-') { + number += source[index++]; + } + if (isDecimalDigit(source.charCodeAt(index))) { + while (isDecimalDigit(source.charCodeAt(index))) { + number += source[index++]; + } + } else { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } + + if (isIdentifierStart(source.charCodeAt(index))) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + return { + type: Token.NumericLiteral, + value: parseFloat(number), + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + // 7.8.4 String Literals + + function scanStringLiteral() { + var str = '', quote, start, ch, code, unescaped, restore, octal = false; + + quote = source[index]; + assert((quote === '\'' || quote === '"'), + 'String literal must starts with a quote'); + + start = index; + ++index; + + while (index < length) { + ch = source[index++]; + + if (ch === quote) { + quote = ''; + break; + } else if (ch === '\\') { + ch = source[index++]; + if (!ch || !isLineTerminator(ch.charCodeAt(0))) { + switch (ch) { + case 'n': + str += '\n'; + break; + case 'r': + str += '\r'; + break; + case 't': + str += '\t'; + break; + case 'u': + case 'x': + if (source[index] === '{') { + ++index; + str += scanUnicodeCodePointEscape(); + } else { + restore = index; + unescaped = scanHexEscape(ch); + if (unescaped) { + str += unescaped; + } else { + index = restore; + str += ch; + } + } + break; + case 'b': + str += '\b'; + break; + case 'f': + str += '\f'; + break; + case 'v': + str += '\x0B'; + break; + + default: + if (isOctalDigit(ch)) { + code = '01234567'.indexOf(ch); + + // \0 is not octal escape sequence + if (code !== 0) { + octal = true; + } + + if (index < length && isOctalDigit(source[index])) { + octal = true; + code = code * 8 + '01234567'.indexOf(source[index++]); + + // 3 digits are only allowed when string starts + // with 0, 1, 2, 3 + if ('0123'.indexOf(ch) >= 0 && + index < length && + isOctalDigit(source[index])) { + code = code * 8 + '01234567'.indexOf(source[index++]); + } + } + str += String.fromCharCode(code); + } else { + str += ch; + } + break; + } + } else { + ++lineNumber; + if (ch === '\r' && source[index] === '\n') { + ++index; + } + } + } else if (isLineTerminator(ch.charCodeAt(0))) { + break; + } else { + str += ch; + } + } + + if (quote !== '') { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + return { + type: Token.StringLiteral, + value: str, + octal: octal, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + function scanTemplate() { + var cooked = '', ch, start, terminated, tail, restore, unescaped, code, octal; + + terminated = false; + tail = false; + start = index; + + ++index; + + while (index < length) { + ch = source[index++]; + if (ch === '`') { + tail = true; + terminated = true; + break; + } else if (ch === '$') { + if (source[index] === '{') { + ++index; + terminated = true; + break; + } + cooked += ch; + } else if (ch === '\\') { + ch = source[index++]; + if (!isLineTerminator(ch.charCodeAt(0))) { + switch (ch) { + case 'n': + cooked += '\n'; + break; + case 'r': + cooked += '\r'; + break; + case 't': + cooked += '\t'; + break; + case 'u': + case 'x': + if (source[index] === '{') { + ++index; + cooked += scanUnicodeCodePointEscape(); + } else { + restore = index; + unescaped = scanHexEscape(ch); + if (unescaped) { + cooked += unescaped; + } else { + index = restore; + cooked += ch; + } + } + break; + case 'b': + cooked += '\b'; + break; + case 'f': + cooked += '\f'; + break; + case 'v': + cooked += '\v'; + break; + + default: + if (isOctalDigit(ch)) { + code = '01234567'.indexOf(ch); + + // \0 is not octal escape sequence + if (code !== 0) { + octal = true; + } + + if (index < length && isOctalDigit(source[index])) { + octal = true; + code = code * 8 + '01234567'.indexOf(source[index++]); + + // 3 digits are only allowed when string starts + // with 0, 1, 2, 3 + if ('0123'.indexOf(ch) >= 0 && + index < length && + isOctalDigit(source[index])) { + code = code * 8 + '01234567'.indexOf(source[index++]); + } + } + cooked += String.fromCharCode(code); + } else { + cooked += ch; + } + break; + } + } else { + ++lineNumber; + if (ch === '\r' && source[index] === '\n') { + ++index; + } + } + } else if (isLineTerminator(ch.charCodeAt(0))) { + ++lineNumber; + if (ch === '\r' && source[index] === '\n') { + ++index; + } + cooked += '\n'; + } else { + cooked += ch; + } + } + + if (!terminated) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + return { + type: Token.Template, + value: { + cooked: cooked, + raw: source.slice(start + 1, index - ((tail) ? 1 : 2)) + }, + tail: tail, + octal: octal, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + + function scanTemplateElement(option) { + var startsWith, template; + + lookahead = null; + skipComment(); + + startsWith = (option.head) ? '`' : '}'; + + if (source[index] !== startsWith) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + + template = scanTemplate(); + + peek(); + + return template; + } + + function scanRegExp() { + var str, ch, start, pattern, flags, value, classMarker = false, restore, terminated = false; + + lookahead = null; + skipComment(); + + start = index; + ch = source[index]; + assert(ch === '/', 'Regular expression literal must start with a slash'); + str = source[index++]; + + while (index < length) { + ch = source[index++]; + str += ch; + if (classMarker) { + if (ch === ']') { + classMarker = false; + } + } else { + if (ch === '\\') { + ch = source[index++]; + // ECMA-262 7.8.5 + if (isLineTerminator(ch.charCodeAt(0))) { + throwError({}, Messages.UnterminatedRegExp); + } + str += ch; + } else if (ch === '/') { + terminated = true; + break; + } else if (ch === '[') { + classMarker = true; + } else if (isLineTerminator(ch.charCodeAt(0))) { + throwError({}, Messages.UnterminatedRegExp); + } + } + } + + if (!terminated) { + throwError({}, Messages.UnterminatedRegExp); + } + + // Exclude leading and trailing slash. + pattern = str.substr(1, str.length - 2); + + flags = ''; + while (index < length) { + ch = source[index]; + if (!isIdentifierPart(ch.charCodeAt(0))) { + break; + } + + ++index; + if (ch === '\\' && index < length) { + ch = source[index]; + if (ch === 'u') { + ++index; + restore = index; + ch = scanHexEscape('u'); + if (ch) { + flags += ch; + for (str += '\\u'; restore < index; ++restore) { + str += source[restore]; + } + } else { + index = restore; + flags += 'u'; + str += '\\u'; + } + } else { + str += '\\'; + } + } else { + flags += ch; + str += ch; + } + } + + try { + value = new RegExp(pattern, flags); + } catch (e) { + throwError({}, Messages.InvalidRegExp); + } + + peek(); + + + if (extra.tokenize) { + return { + type: Token.RegularExpression, + value: value, + lineNumber: lineNumber, + lineStart: lineStart, + range: [start, index] + }; + } + return { + literal: str, + value: value, + range: [start, index] + }; + } + + function isIdentifierName(token) { + return token.type === Token.Identifier || + token.type === Token.Keyword || + token.type === Token.BooleanLiteral || + token.type === Token.NullLiteral; + } + + function advanceSlash() { + var prevToken, + checkToken; + // Using the following algorithm: + // https://github.com/mozilla/sweet.js/wiki/design + prevToken = extra.tokens[extra.tokens.length - 1]; + if (!prevToken) { + // Nothing before that: it cannot be a division. + return scanRegExp(); + } + if (prevToken.type === 'Punctuator') { + if (prevToken.value === ')') { + checkToken = extra.tokens[extra.openParenToken - 1]; + if (checkToken && + checkToken.type === 'Keyword' && + (checkToken.value === 'if' || + checkToken.value === 'while' || + checkToken.value === 'for' || + checkToken.value === 'with')) { + return scanRegExp(); + } + return scanPunctuator(); + } + if (prevToken.value === '}') { + // Dividing a function by anything makes little sense, + // but we have to check for that. + if (extra.tokens[extra.openCurlyToken - 3] && + extra.tokens[extra.openCurlyToken - 3].type === 'Keyword') { + // Anonymous function. + checkToken = extra.tokens[extra.openCurlyToken - 4]; + if (!checkToken) { + return scanPunctuator(); + } + } else if (extra.tokens[extra.openCurlyToken - 4] && + extra.tokens[extra.openCurlyToken - 4].type === 'Keyword') { + // Named function. + checkToken = extra.tokens[extra.openCurlyToken - 5]; + if (!checkToken) { + return scanRegExp(); + } + } else { + return scanPunctuator(); + } + // checkToken determines whether the function is + // a declaration or an expression. + if (FnExprTokens.indexOf(checkToken.value) >= 0) { + // It is an expression. + return scanPunctuator(); + } + // It is a declaration. + return scanRegExp(); + } + return scanRegExp(); + } + if (prevToken.type === 'Keyword') { + return scanRegExp(); + } + return scanPunctuator(); + } + + function advance() { + var ch; + + skipComment(); + + if (index >= length) { + return { + type: Token.EOF, + lineNumber: lineNumber, + lineStart: lineStart, + range: [index, index] + }; + } + + ch = source.charCodeAt(index); + + // Very common: ( and ) and ; + if (ch === 40 || ch === 41 || ch === 58) { + return scanPunctuator(); + } + + // String literal starts with single quote (#39) or double quote (#34). + if (ch === 39 || ch === 34) { + return scanStringLiteral(); + } + + if (ch === 96) { + return scanTemplate(); + } + if (isIdentifierStart(ch)) { + return scanIdentifier(); + } + + // Dot (.) char #46 can also start a floating-point number, hence the need + // to check the next character. + if (ch === 46) { + if (isDecimalDigit(source.charCodeAt(index + 1))) { + return scanNumericLiteral(); + } + return scanPunctuator(); + } + + if (isDecimalDigit(ch)) { + return scanNumericLiteral(); + } + + // Slash (/) char #47 can also start a regex. + if (extra.tokenize && ch === 47) { + return advanceSlash(); + } + + return scanPunctuator(); + } + + function lex() { + var token; + + token = lookahead; + index = token.range[1]; + lineNumber = token.lineNumber; + lineStart = token.lineStart; + + lookahead = advance(); + + index = token.range[1]; + lineNumber = token.lineNumber; + lineStart = token.lineStart; + + return token; + } + + function peek() { + var pos, line, start; + + pos = index; + line = lineNumber; + start = lineStart; + lookahead = advance(); + index = pos; + lineNumber = line; + lineStart = start; + } + + function lookahead2() { + var adv, pos, line, start, result; + + // If we are collecting the tokens, don't grab the next one yet. + adv = (typeof extra.advance === 'function') ? extra.advance : advance; + + pos = index; + line = lineNumber; + start = lineStart; + + // Scan for the next immediate token. + if (lookahead === null) { + lookahead = adv(); + } + index = lookahead.range[1]; + lineNumber = lookahead.lineNumber; + lineStart = lookahead.lineStart; + + // Grab the token right after. + result = adv(); + index = pos; + lineNumber = line; + lineStart = start; + + return result; + } + + function markerCreate() { + if (!extra.loc && !extra.range) { + return undefined; + } + skipComment(); + return {offset: index, line: lineNumber, col: index - lineStart}; + } + + function markerApply(marker, node) { + if (extra.range) { + node.range = [marker.offset, index]; + } + if (extra.loc) { + node.loc = { + start: { + line: marker.line, + column: marker.col + }, + end: { + line: lineNumber, + column: index - lineStart + } + }; + node = delegate.postProcess(node); + } + return node; + } + + SyntaxTreeDelegate = { + + name: 'SyntaxTree', + + postProcess: function (node) { + return node; + }, + + createArrayExpression: function (elements) { + return { + type: Syntax.ArrayExpression, + elements: elements + }; + }, + + createAssignmentExpression: function (operator, left, right) { + return { + type: Syntax.AssignmentExpression, + operator: operator, + left: left, + right: right + }; + }, + + createBinaryExpression: function (operator, left, right) { + var type = (operator === '||' || operator === '&&') ? Syntax.LogicalExpression : + Syntax.BinaryExpression; + return { + type: type, + operator: operator, + left: left, + right: right + }; + }, + + createBlockStatement: function (body) { + return { + type: Syntax.BlockStatement, + body: body + }; + }, + + createBreakStatement: function (label) { + return { + type: Syntax.BreakStatement, + label: label + }; + }, + + createCallExpression: function (callee, args) { + return { + type: Syntax.CallExpression, + callee: callee, + 'arguments': args + }; + }, + + createCatchClause: function (param, body) { + return { + type: Syntax.CatchClause, + param: param, + body: body + }; + }, + + createConditionalExpression: function (test, consequent, alternate) { + return { + type: Syntax.ConditionalExpression, + test: test, + consequent: consequent, + alternate: alternate + }; + }, + + createContinueStatement: function (label) { + return { + type: Syntax.ContinueStatement, + label: label + }; + }, + + createDebuggerStatement: function () { + return { + type: Syntax.DebuggerStatement + }; + }, + + createDoWhileStatement: function (body, test) { + return { + type: Syntax.DoWhileStatement, + body: body, + test: test + }; + }, + + createEmptyStatement: function () { + return { + type: Syntax.EmptyStatement + }; + }, + + createExpressionStatement: function (expression) { + return { + type: Syntax.ExpressionStatement, + expression: expression + }; + }, + + createForStatement: function (init, test, update, body) { + return { + type: Syntax.ForStatement, + init: init, + test: test, + update: update, + body: body + }; + }, + + createForInStatement: function (left, right, body) { + return { + type: Syntax.ForInStatement, + left: left, + right: right, + body: body, + each: false + }; + }, + + createForOfStatement: function (left, right, body) { + return { + type: Syntax.ForOfStatement, + left: left, + right: right, + body: body + }; + }, + + createFunctionDeclaration: function (id, params, defaults, body, rest, generator, expression) { + return { + type: Syntax.FunctionDeclaration, + id: id, + params: params, + defaults: defaults, + body: body, + rest: rest, + generator: generator, + expression: expression + }; + }, + + createFunctionExpression: function (id, params, defaults, body, rest, generator, expression) { + return { + type: Syntax.FunctionExpression, + id: id, + params: params, + defaults: defaults, + body: body, + rest: rest, + generator: generator, + expression: expression + }; + }, + + createIdentifier: function (name) { + return { + type: Syntax.Identifier, + name: name + }; + }, + + createIfStatement: function (test, consequent, alternate) { + return { + type: Syntax.IfStatement, + test: test, + consequent: consequent, + alternate: alternate + }; + }, + + createLabeledStatement: function (label, body) { + return { + type: Syntax.LabeledStatement, + label: label, + body: body + }; + }, + + createLiteral: function (token) { + return { + type: Syntax.Literal, + value: token.value, + raw: source.slice(token.range[0], token.range[1]) + }; + }, + + createMemberExpression: function (accessor, object, property) { + return { + type: Syntax.MemberExpression, + computed: accessor === '[', + object: object, + property: property + }; + }, + + createNewExpression: function (callee, args) { + return { + type: Syntax.NewExpression, + callee: callee, + 'arguments': args + }; + }, + + createObjectExpression: function (properties) { + return { + type: Syntax.ObjectExpression, + properties: properties + }; + }, + + createPostfixExpression: function (operator, argument) { + return { + type: Syntax.UpdateExpression, + operator: operator, + argument: argument, + prefix: false + }; + }, + + createProgram: function (body) { + return { + type: Syntax.Program, + body: body + }; + }, + + createProperty: function (kind, key, value, method, shorthand) { + return { + type: Syntax.Property, + key: key, + value: value, + kind: kind, + method: method, + shorthand: shorthand + }; + }, + + createReturnStatement: function (argument) { + return { + type: Syntax.ReturnStatement, + argument: argument + }; + }, + + createSequenceExpression: function (expressions) { + return { + type: Syntax.SequenceExpression, + expressions: expressions + }; + }, + + createSwitchCase: function (test, consequent) { + return { + type: Syntax.SwitchCase, + test: test, + consequent: consequent + }; + }, + + createSwitchStatement: function (discriminant, cases) { + return { + type: Syntax.SwitchStatement, + discriminant: discriminant, + cases: cases + }; + }, + + createThisExpression: function () { + return { + type: Syntax.ThisExpression + }; + }, + + createThrowStatement: function (argument) { + return { + type: Syntax.ThrowStatement, + argument: argument + }; + }, + + createTryStatement: function (block, guardedHandlers, handlers, finalizer) { + return { + type: Syntax.TryStatement, + block: block, + guardedHandlers: guardedHandlers, + handlers: handlers, + finalizer: finalizer + }; + }, + + createUnaryExpression: function (operator, argument) { + if (operator === '++' || operator === '--') { + return { + type: Syntax.UpdateExpression, + operator: operator, + argument: argument, + prefix: true + }; + } + return { + type: Syntax.UnaryExpression, + operator: operator, + argument: argument, + prefix: true + }; + }, + + createVariableDeclaration: function (declarations, kind) { + return { + type: Syntax.VariableDeclaration, + declarations: declarations, + kind: kind + }; + }, + + createVariableDeclarator: function (id, init) { + return { + type: Syntax.VariableDeclarator, + id: id, + init: init + }; + }, + + createWhileStatement: function (test, body) { + return { + type: Syntax.WhileStatement, + test: test, + body: body + }; + }, + + createWithStatement: function (object, body) { + return { + type: Syntax.WithStatement, + object: object, + body: body + }; + }, + + createTemplateElement: function (value, tail) { + return { + type: Syntax.TemplateElement, + value: value, + tail: tail + }; + }, + + createTemplateLiteral: function (quasis, expressions) { + return { + type: Syntax.TemplateLiteral, + quasis: quasis, + expressions: expressions + }; + }, + + createSpreadElement: function (argument) { + return { + type: Syntax.SpreadElement, + argument: argument + }; + }, + + createTaggedTemplateExpression: function (tag, quasi) { + return { + type: Syntax.TaggedTemplateExpression, + tag: tag, + quasi: quasi + }; + }, + + createArrowFunctionExpression: function (params, defaults, body, rest, expression) { + return { + type: Syntax.ArrowFunctionExpression, + id: null, + params: params, + defaults: defaults, + body: body, + rest: rest, + generator: false, + expression: expression + }; + }, + + createMethodDefinition: function (propertyType, kind, key, value) { + return { + type: Syntax.MethodDefinition, + key: key, + value: value, + kind: kind, + 'static': propertyType === ClassPropertyType.static + }; + }, + + createClassBody: function (body) { + return { + type: Syntax.ClassBody, + body: body + }; + }, + + createClassExpression: function (id, superClass, body) { + return { + type: Syntax.ClassExpression, + id: id, + superClass: superClass, + body: body + }; + }, + + createClassDeclaration: function (id, superClass, body) { + return { + type: Syntax.ClassDeclaration, + id: id, + superClass: superClass, + body: body + }; + }, + + createExportSpecifier: function (id, name) { + return { + type: Syntax.ExportSpecifier, + id: id, + name: name + }; + }, + + createExportBatchSpecifier: function () { + return { + type: Syntax.ExportBatchSpecifier + }; + }, + + createExportDeclaration: function (declaration, specifiers, source) { + return { + type: Syntax.ExportDeclaration, + declaration: declaration, + specifiers: specifiers, + source: source + }; + }, + + createImportSpecifier: function (id, name) { + return { + type: Syntax.ImportSpecifier, + id: id, + name: name + }; + }, + + createImportDeclaration: function (specifiers, kind, source) { + return { + type: Syntax.ImportDeclaration, + specifiers: specifiers, + kind: kind, + source: source + }; + }, + + createYieldExpression: function (argument, delegate) { + return { + type: Syntax.YieldExpression, + argument: argument, + delegate: delegate + }; + }, + + createModuleDeclaration: function (id, source, body) { + return { + type: Syntax.ModuleDeclaration, + id: id, + source: source, + body: body + }; + }, + + createComprehensionExpression: function (filter, blocks, body) { + return { + type: Syntax.ComprehensionExpression, + filter: filter, + blocks: blocks, + body: body + }; + } + + }; + + // Return true if there is a line terminator before the next token. + + function peekLineTerminator() { + var pos, line, start, found; + + pos = index; + line = lineNumber; + start = lineStart; + skipComment(); + found = lineNumber !== line; + index = pos; + lineNumber = line; + lineStart = start; + + return found; + } + + // Throw an exception + + function throwError(token, messageFormat) { + var error, + args = Array.prototype.slice.call(arguments, 2), + msg = messageFormat.replace( + /%(\d)/g, + function (whole, index) { + assert(index < args.length, 'Message reference must be in range'); + return args[index]; + } + ); + + if (typeof token.lineNumber === 'number') { + error = new Error('Line ' + token.lineNumber + ': ' + msg); + error.index = token.range[0]; + error.lineNumber = token.lineNumber; + error.column = token.range[0] - lineStart + 1; + } else { + error = new Error('Line ' + lineNumber + ': ' + msg); + error.index = index; + error.lineNumber = lineNumber; + error.column = index - lineStart + 1; + } + + error.description = msg; + throw error; + } + + function throwErrorTolerant() { + try { + throwError.apply(null, arguments); + } catch (e) { + if (extra.errors) { + extra.errors.push(e); + } else { + throw e; + } + } + } + + + // Throw an exception because of the token. + + function throwUnexpected(token) { + if (token.type === Token.EOF) { + throwError(token, Messages.UnexpectedEOS); + } + + if (token.type === Token.NumericLiteral) { + throwError(token, Messages.UnexpectedNumber); + } + + if (token.type === Token.StringLiteral) { + throwError(token, Messages.UnexpectedString); + } + + if (token.type === Token.Identifier) { + throwError(token, Messages.UnexpectedIdentifier); + } + + if (token.type === Token.Keyword) { + if (isFutureReservedWord(token.value)) { + throwError(token, Messages.UnexpectedReserved); + } else if (strict && isStrictModeReservedWord(token.value)) { + throwErrorTolerant(token, Messages.StrictReservedWord); + return; + } + throwError(token, Messages.UnexpectedToken, token.value); + } + + if (token.type === Token.Template) { + throwError(token, Messages.UnexpectedTemplate, token.value.raw); + } + + // BooleanLiteral, NullLiteral, or Punctuator. + throwError(token, Messages.UnexpectedToken, token.value); + } + + // Expect the next token to match the specified punctuator. + // If not, an exception will be thrown. + + function expect(value) { + var token = lex(); + if (token.type !== Token.Punctuator || token.value !== value) { + throwUnexpected(token); + } + } + + // Expect the next token to match the specified keyword. + // If not, an exception will be thrown. + + function expectKeyword(keyword) { + var token = lex(); + if (token.type !== Token.Keyword || token.value !== keyword) { + throwUnexpected(token); + } + } + + // Return true if the next token matches the specified punctuator. + + function match(value) { + return lookahead.type === Token.Punctuator && lookahead.value === value; + } + + // Return true if the next token matches the specified keyword + + function matchKeyword(keyword) { + return lookahead.type === Token.Keyword && lookahead.value === keyword; + } + + + // Return true if the next token matches the specified contextual keyword + + function matchContextualKeyword(keyword) { + return lookahead.type === Token.Identifier && lookahead.value === keyword; + } + + // Return true if the next token is an assignment operator + + function matchAssign() { + var op; + + if (lookahead.type !== Token.Punctuator) { + return false; + } + op = lookahead.value; + return op === '=' || + op === '*=' || + op === '/=' || + op === '%=' || + op === '+=' || + op === '-=' || + op === '<<=' || + op === '>>=' || + op === '>>>=' || + op === '&=' || + op === '^=' || + op === '|='; + } + + function consumeSemicolon() { + var line; + + // Catch the very common case first: immediately a semicolon (char #59). + if (source.charCodeAt(index) === 59) { + lex(); + return; + } + + line = lineNumber; + skipComment(); + if (lineNumber !== line) { + return; + } + + if (match(';')) { + lex(); + return; + } + + if (lookahead.type !== Token.EOF && !match('}')) { + throwUnexpected(lookahead); + } + } + + // Return true if provided expression is LeftHandSideExpression + + function isLeftHandSide(expr) { + return expr.type === Syntax.Identifier || expr.type === Syntax.MemberExpression; + } + + function isAssignableLeftHandSide(expr) { + return isLeftHandSide(expr) || expr.type === Syntax.ObjectPattern || expr.type === Syntax.ArrayPattern; + } + + // 11.1.4 Array Initialiser + + function parseArrayInitialiser() { + var elements = [], blocks = [], filter = null, tmp, possiblecomprehension = true, body, + marker = markerCreate(); + + expect('['); + while (!match(']')) { + if (lookahead.value === 'for' && + lookahead.type === Token.Keyword) { + if (!possiblecomprehension) { + throwError({}, Messages.ComprehensionError); + } + matchKeyword('for'); + tmp = parseForStatement({ignoreBody: true}); + tmp.of = tmp.type === Syntax.ForOfStatement; + tmp.type = Syntax.ComprehensionBlock; + if (tmp.left.kind) { // can't be let or const + throwError({}, Messages.ComprehensionError); + } + blocks.push(tmp); + } else if (lookahead.value === 'if' && + lookahead.type === Token.Keyword) { + if (!possiblecomprehension) { + throwError({}, Messages.ComprehensionError); + } + expectKeyword('if'); + expect('('); + filter = parseExpression(); + expect(')'); + } else if (lookahead.value === ',' && + lookahead.type === Token.Punctuator) { + possiblecomprehension = false; // no longer allowed. + lex(); + elements.push(null); + } else { + tmp = parseSpreadOrAssignmentExpression(); + elements.push(tmp); + if (tmp && tmp.type === Syntax.SpreadElement) { + if (!match(']')) { + throwError({}, Messages.ElementAfterSpreadElement); + } + } else if (!(match(']') || matchKeyword('for') || matchKeyword('if'))) { + expect(','); // this lexes. + possiblecomprehension = false; + } + } + } + + expect(']'); + + if (filter && !blocks.length) { + throwError({}, Messages.ComprehensionRequiresBlock); + } + + if (blocks.length) { + if (elements.length !== 1) { + throwError({}, Messages.ComprehensionError); + } + return markerApply(marker, delegate.createComprehensionExpression(filter, blocks, elements[0])); + } + return markerApply(marker, delegate.createArrayExpression(elements)); + } + + // 11.1.5 Object Initialiser + + function parsePropertyFunction(options) { + var previousStrict, previousYieldAllowed, params, defaults, body, + marker = markerCreate(); + + previousStrict = strict; + previousYieldAllowed = state.yieldAllowed; + state.yieldAllowed = options.generator; + params = options.params || []; + defaults = options.defaults || []; + + body = parseConciseBody(); + if (options.name && strict && isRestrictedWord(params[0].name)) { + throwErrorTolerant(options.name, Messages.StrictParamName); + } + strict = previousStrict; + state.yieldAllowed = previousYieldAllowed; + + return markerApply(marker, delegate.createFunctionExpression( + null, + params, + defaults, + body, + options.rest || null, + options.generator, + body.type !== Syntax.BlockStatement + )); + } + + + function parsePropertyMethodFunction(options) { + var previousStrict, tmp, method; + + previousStrict = strict; + strict = true; + + tmp = parseParams(); + + if (tmp.stricted) { + throwErrorTolerant(tmp.stricted, tmp.message); + } + + + method = parsePropertyFunction({ + params: tmp.params, + defaults: tmp.defaults, + rest: tmp.rest, + generator: options.generator + }); + + strict = previousStrict; + + return method; + } + + + function parseObjectPropertyKey() { + var marker = markerCreate(), + token = lex(); + + // Note: This function is called only from parseObjectProperty(), where + // EOF and Punctuator tokens are already filtered out. + + if (token.type === Token.StringLiteral || token.type === Token.NumericLiteral) { + if (strict && token.octal) { + throwErrorTolerant(token, Messages.StrictOctalLiteral); + } + return markerApply(marker, delegate.createLiteral(token)); + } + + return markerApply(marker, delegate.createIdentifier(token.value)); + } + + function parseObjectProperty() { + var token, key, id, value, param, expr, + marker = markerCreate(); + + token = lookahead; + + if (token.type === Token.Identifier) { + + id = parseObjectPropertyKey(); + + // Property Assignment: Getter and Setter. + + if (token.value === 'get' && !(match(':') || match('('))) { + key = parseObjectPropertyKey(); + expect('('); + expect(')'); + return markerApply(marker, delegate.createProperty('get', key, parsePropertyFunction({ generator: false }), false, false)); + } + if (token.value === 'set' && !(match(':') || match('('))) { + key = parseObjectPropertyKey(); + expect('('); + token = lookahead; + param = [ parseVariableIdentifier() ]; + expect(')'); + return markerApply(marker, delegate.createProperty('set', key, parsePropertyFunction({ params: param, generator: false, name: token }), false, false)); + } + if (match(':')) { + lex(); + return markerApply(marker, delegate.createProperty('init', id, parseAssignmentExpression(), false, false)); + } + if (match('(')) { + return markerApply(marker, delegate.createProperty('init', id, parsePropertyMethodFunction({ generator: false }), true, false)); + } + return markerApply(marker, delegate.createProperty('init', id, id, false, true)); + } + if (token.type === Token.EOF || token.type === Token.Punctuator) { + if (!match('*')) { + throwUnexpected(token); + } + lex(); + + id = parseObjectPropertyKey(); + + if (!match('(')) { + throwUnexpected(lex()); + } + + return markerApply(marker, delegate.createProperty('init', id, parsePropertyMethodFunction({ generator: true }), true, false)); + } + key = parseObjectPropertyKey(); + if (match(':')) { + lex(); + return markerApply(marker, delegate.createProperty('init', key, parseAssignmentExpression(), false, false)); + } + if (match('(')) { + return markerApply(marker, delegate.createProperty('init', key, parsePropertyMethodFunction({ generator: false }), true, false)); + } + throwUnexpected(lex()); + } + + function parseObjectInitialiser() { + var properties = [], property, name, key, kind, map = {}, toString = String, + marker = markerCreate(); + + expect('{'); + + while (!match('}')) { + property = parseObjectProperty(); + + if (property.key.type === Syntax.Identifier) { + name = property.key.name; + } else { + name = toString(property.key.value); + } + kind = (property.kind === 'init') ? PropertyKind.Data : (property.kind === 'get') ? PropertyKind.Get : PropertyKind.Set; + + key = '$' + name; + if (Object.prototype.hasOwnProperty.call(map, key)) { + if (map[key] === PropertyKind.Data) { + if (strict && kind === PropertyKind.Data) { + throwErrorTolerant({}, Messages.StrictDuplicateProperty); + } else if (kind !== PropertyKind.Data) { + throwErrorTolerant({}, Messages.AccessorDataProperty); + } + } else { + if (kind === PropertyKind.Data) { + throwErrorTolerant({}, Messages.AccessorDataProperty); + } else if (map[key] & kind) { + throwErrorTolerant({}, Messages.AccessorGetSet); + } + } + map[key] |= kind; + } else { + map[key] = kind; + } + + properties.push(property); + + if (!match('}')) { + expect(','); + } + } + + expect('}'); + + return markerApply(marker, delegate.createObjectExpression(properties)); + } + + function parseTemplateElement(option) { + var marker = markerCreate(), + token = scanTemplateElement(option); + if (strict && token.octal) { + throwError(token, Messages.StrictOctalLiteral); + } + return markerApply(marker, delegate.createTemplateElement({ raw: token.value.raw, cooked: token.value.cooked }, token.tail)); + } + + function parseTemplateLiteral() { + var quasi, quasis, expressions, marker = markerCreate(); + + quasi = parseTemplateElement({ head: true }); + quasis = [ quasi ]; + expressions = []; + + while (!quasi.tail) { + expressions.push(parseExpression()); + quasi = parseTemplateElement({ head: false }); + quasis.push(quasi); + } + + return markerApply(marker, delegate.createTemplateLiteral(quasis, expressions)); + } + + // 11.1.6 The Grouping Operator + + function parseGroupExpression() { + var expr; + + expect('('); + + ++state.parenthesizedCount; + + expr = parseExpression(); + + expect(')'); + + return expr; + } + + + // 11.1 Primary Expressions + + function parsePrimaryExpression() { + var marker, type, token, expr; + + type = lookahead.type; + + if (type === Token.Identifier) { + marker = markerCreate(); + return markerApply(marker, delegate.createIdentifier(lex().value)); + } + + if (type === Token.StringLiteral || type === Token.NumericLiteral) { + if (strict && lookahead.octal) { + throwErrorTolerant(lookahead, Messages.StrictOctalLiteral); + } + marker = markerCreate(); + return markerApply(marker, delegate.createLiteral(lex())); + } + + if (type === Token.Keyword) { + if (matchKeyword('this')) { + marker = markerCreate(); + lex(); + return markerApply(marker, delegate.createThisExpression()); + } + + if (matchKeyword('function')) { + return parseFunctionExpression(); + } + + if (matchKeyword('class')) { + return parseClassExpression(); + } + + if (matchKeyword('super')) { + marker = markerCreate(); + lex(); + return markerApply(marker, delegate.createIdentifier('super')); + } + } + + if (type === Token.BooleanLiteral) { + marker = markerCreate(); + token = lex(); + token.value = (token.value === 'true'); + return markerApply(marker, delegate.createLiteral(token)); + } + + if (type === Token.NullLiteral) { + marker = markerCreate(); + token = lex(); + token.value = null; + return markerApply(marker, delegate.createLiteral(token)); + } + + if (match('[')) { + return parseArrayInitialiser(); + } + + if (match('{')) { + return parseObjectInitialiser(); + } + + if (match('(')) { + return parseGroupExpression(); + } + + if (match('/') || match('/=')) { + marker = markerCreate(); + return markerApply(marker, delegate.createLiteral(scanRegExp())); + } + + if (type === Token.Template) { + return parseTemplateLiteral(); + } + + throwUnexpected(lex()); + } + + // 11.2 Left-Hand-Side Expressions + + function parseArguments() { + var args = [], arg; + + expect('('); + + if (!match(')')) { + while (index < length) { + arg = parseSpreadOrAssignmentExpression(); + args.push(arg); + + if (match(')')) { + break; + } else if (arg.type === Syntax.SpreadElement) { + throwError({}, Messages.ElementAfterSpreadElement); + } + + expect(','); + } + } + + expect(')'); + + return args; + } + + function parseSpreadOrAssignmentExpression() { + if (match('...')) { + var marker = markerCreate(); + lex(); + return markerApply(marker, delegate.createSpreadElement(parseAssignmentExpression())); + } + return parseAssignmentExpression(); + } + + function parseNonComputedProperty() { + var marker = markerCreate(), + token = lex(); + + if (!isIdentifierName(token)) { + throwUnexpected(token); + } + + return markerApply(marker, delegate.createIdentifier(token.value)); + } + + function parseNonComputedMember() { + expect('.'); + + return parseNonComputedProperty(); + } + + function parseComputedMember() { + var expr; + + expect('['); + + expr = parseExpression(); + + expect(']'); + + return expr; + } + + function parseNewExpression() { + var callee, args, marker = markerCreate(); + + expectKeyword('new'); + callee = parseLeftHandSideExpression(); + args = match('(') ? parseArguments() : []; + + return markerApply(marker, delegate.createNewExpression(callee, args)); + } + + function parseLeftHandSideExpressionAllowCall() { + var expr, args, marker = markerCreate(); + + expr = matchKeyword('new') ? parseNewExpression() : parsePrimaryExpression(); + + while (match('.') || match('[') || match('(') || lookahead.type === Token.Template) { + if (match('(')) { + args = parseArguments(); + expr = markerApply(marker, delegate.createCallExpression(expr, args)); + } else if (match('[')) { + expr = markerApply(marker, delegate.createMemberExpression('[', expr, parseComputedMember())); + } else if (match('.')) { + expr = markerApply(marker, delegate.createMemberExpression('.', expr, parseNonComputedMember())); + } else { + expr = markerApply(marker, delegate.createTaggedTemplateExpression(expr, parseTemplateLiteral())); + } + } + + return expr; + } + + function parseLeftHandSideExpression() { + var expr, marker = markerCreate(); + + expr = matchKeyword('new') ? parseNewExpression() : parsePrimaryExpression(); + + while (match('.') || match('[') || lookahead.type === Token.Template) { + if (match('[')) { + expr = markerApply(marker, delegate.createMemberExpression('[', expr, parseComputedMember())); + } else if (match('.')) { + expr = markerApply(marker, delegate.createMemberExpression('.', expr, parseNonComputedMember())); + } else { + expr = markerApply(marker, delegate.createTaggedTemplateExpression(expr, parseTemplateLiteral())); + } + } + + return expr; + } + + // 11.3 Postfix Expressions + + function parsePostfixExpression() { + var marker = markerCreate(), + expr = parseLeftHandSideExpressionAllowCall(), + token; + + if (lookahead.type !== Token.Punctuator) { + return expr; + } + + if ((match('++') || match('--')) && !peekLineTerminator()) { + // 11.3.1, 11.3.2 + if (strict && expr.type === Syntax.Identifier && isRestrictedWord(expr.name)) { + throwErrorTolerant({}, Messages.StrictLHSPostfix); + } + + if (!isLeftHandSide(expr)) { + throwError({}, Messages.InvalidLHSInAssignment); + } + + token = lex(); + expr = markerApply(marker, delegate.createPostfixExpression(token.value, expr)); + } + + return expr; + } + + // 11.4 Unary Operators + + function parseUnaryExpression() { + var marker, token, expr; + + if (lookahead.type !== Token.Punctuator && lookahead.type !== Token.Keyword) { + return parsePostfixExpression(); + } + + if (match('++') || match('--')) { + marker = markerCreate(); + token = lex(); + expr = parseUnaryExpression(); + // 11.4.4, 11.4.5 + if (strict && expr.type === Syntax.Identifier && isRestrictedWord(expr.name)) { + throwErrorTolerant({}, Messages.StrictLHSPrefix); + } + + if (!isLeftHandSide(expr)) { + throwError({}, Messages.InvalidLHSInAssignment); + } + + return markerApply(marker, delegate.createUnaryExpression(token.value, expr)); + } + + if (match('+') || match('-') || match('~') || match('!')) { + marker = markerCreate(); + token = lex(); + expr = parseUnaryExpression(); + return markerApply(marker, delegate.createUnaryExpression(token.value, expr)); + } + + if (matchKeyword('delete') || matchKeyword('void') || matchKeyword('typeof')) { + marker = markerCreate(); + token = lex(); + expr = parseUnaryExpression(); + expr = markerApply(marker, delegate.createUnaryExpression(token.value, expr)); + if (strict && expr.operator === 'delete' && expr.argument.type === Syntax.Identifier) { + throwErrorTolerant({}, Messages.StrictDelete); + } + return expr; + } + + return parsePostfixExpression(); + } + + function binaryPrecedence(token, allowIn) { + var prec = 0; + + if (token.type !== Token.Punctuator && token.type !== Token.Keyword) { + return 0; + } + + switch (token.value) { + case '||': + prec = 1; + break; + + case '&&': + prec = 2; + break; + + case '|': + prec = 3; + break; + + case '^': + prec = 4; + break; + + case '&': + prec = 5; + break; + + case '==': + case '!=': + case '===': + case '!==': + prec = 6; + break; + + case '<': + case '>': + case '<=': + case '>=': + case 'instanceof': + prec = 7; + break; + + case 'in': + prec = allowIn ? 7 : 0; + break; + + case '<<': + case '>>': + case '>>>': + prec = 8; + break; + + case '+': + case '-': + prec = 9; + break; + + case '*': + case '/': + case '%': + prec = 11; + break; + + default: + break; + } + + return prec; + } + + // 11.5 Multiplicative Operators + // 11.6 Additive Operators + // 11.7 Bitwise Shift Operators + // 11.8 Relational Operators + // 11.9 Equality Operators + // 11.10 Binary Bitwise Operators + // 11.11 Binary Logical Operators + + function parseBinaryExpression() { + var expr, token, prec, previousAllowIn, stack, right, operator, left, i, + marker, markers; + + previousAllowIn = state.allowIn; + state.allowIn = true; + + marker = markerCreate(); + left = parseUnaryExpression(); + + token = lookahead; + prec = binaryPrecedence(token, previousAllowIn); + if (prec === 0) { + return left; + } + token.prec = prec; + lex(); + + markers = [marker, markerCreate()]; + right = parseUnaryExpression(); + + stack = [left, token, right]; + + while ((prec = binaryPrecedence(lookahead, previousAllowIn)) > 0) { + + // Reduce: make a binary expression from the three topmost entries. + while ((stack.length > 2) && (prec <= stack[stack.length - 2].prec)) { + right = stack.pop(); + operator = stack.pop().value; + left = stack.pop(); + expr = delegate.createBinaryExpression(operator, left, right); + markers.pop(); + marker = markers.pop(); + markerApply(marker, expr); + stack.push(expr); + markers.push(marker); + } + + // Shift. + token = lex(); + token.prec = prec; + stack.push(token); + markers.push(markerCreate()); + expr = parseUnaryExpression(); + stack.push(expr); + } + + state.allowIn = previousAllowIn; + + // Final reduce to clean-up the stack. + i = stack.length - 1; + expr = stack[i]; + markers.pop(); + while (i > 1) { + expr = delegate.createBinaryExpression(stack[i - 1].value, stack[i - 2], expr); + i -= 2; + marker = markers.pop(); + markerApply(marker, expr); + } + + return expr; + } + + + // 11.12 Conditional Operator + + function parseConditionalExpression() { + var expr, previousAllowIn, consequent, alternate, marker = markerCreate(); + expr = parseBinaryExpression(); + + if (match('?')) { + lex(); + previousAllowIn = state.allowIn; + state.allowIn = true; + consequent = parseAssignmentExpression(); + state.allowIn = previousAllowIn; + expect(':'); + alternate = parseAssignmentExpression(); + + expr = markerApply(marker, delegate.createConditionalExpression(expr, consequent, alternate)); + } + + return expr; + } + + // 11.13 Assignment Operators + + function reinterpretAsAssignmentBindingPattern(expr) { + var i, len, property, element; + + if (expr.type === Syntax.ObjectExpression) { + expr.type = Syntax.ObjectPattern; + for (i = 0, len = expr.properties.length; i < len; i += 1) { + property = expr.properties[i]; + if (property.kind !== 'init') { + throwError({}, Messages.InvalidLHSInAssignment); + } + reinterpretAsAssignmentBindingPattern(property.value); + } + } else if (expr.type === Syntax.ArrayExpression) { + expr.type = Syntax.ArrayPattern; + for (i = 0, len = expr.elements.length; i < len; i += 1) { + element = expr.elements[i]; + if (element) { + reinterpretAsAssignmentBindingPattern(element); + } + } + } else if (expr.type === Syntax.Identifier) { + if (isRestrictedWord(expr.name)) { + throwError({}, Messages.InvalidLHSInAssignment); + } + } else if (expr.type === Syntax.SpreadElement) { + reinterpretAsAssignmentBindingPattern(expr.argument); + if (expr.argument.type === Syntax.ObjectPattern) { + throwError({}, Messages.ObjectPatternAsSpread); + } + } else { + if (expr.type !== Syntax.MemberExpression && expr.type !== Syntax.CallExpression && expr.type !== Syntax.NewExpression) { + throwError({}, Messages.InvalidLHSInAssignment); + } + } + } + + + function reinterpretAsDestructuredParameter(options, expr) { + var i, len, property, element; + + if (expr.type === Syntax.ObjectExpression) { + expr.type = Syntax.ObjectPattern; + for (i = 0, len = expr.properties.length; i < len; i += 1) { + property = expr.properties[i]; + if (property.kind !== 'init') { + throwError({}, Messages.InvalidLHSInFormalsList); + } + reinterpretAsDestructuredParameter(options, property.value); + } + } else if (expr.type === Syntax.ArrayExpression) { + expr.type = Syntax.ArrayPattern; + for (i = 0, len = expr.elements.length; i < len; i += 1) { + element = expr.elements[i]; + if (element) { + reinterpretAsDestructuredParameter(options, element); + } + } + } else if (expr.type === Syntax.Identifier) { + validateParam(options, expr, expr.name); + } else { + if (expr.type !== Syntax.MemberExpression) { + throwError({}, Messages.InvalidLHSInFormalsList); + } + } + } + + function reinterpretAsCoverFormalsList(expressions) { + var i, len, param, params, defaults, defaultCount, options, rest; + + params = []; + defaults = []; + defaultCount = 0; + rest = null; + options = { + paramSet: {} + }; + + for (i = 0, len = expressions.length; i < len; i += 1) { + param = expressions[i]; + if (param.type === Syntax.Identifier) { + params.push(param); + defaults.push(null); + validateParam(options, param, param.name); + } else if (param.type === Syntax.ObjectExpression || param.type === Syntax.ArrayExpression) { + reinterpretAsDestructuredParameter(options, param); + params.push(param); + defaults.push(null); + } else if (param.type === Syntax.SpreadElement) { + assert(i === len - 1, 'It is guaranteed that SpreadElement is last element by parseExpression'); + reinterpretAsDestructuredParameter(options, param.argument); + rest = param.argument; + } else if (param.type === Syntax.AssignmentExpression) { + params.push(param.left); + defaults.push(param.right); + ++defaultCount; + validateParam(options, param.left, param.left.name); + } else { + return null; + } + } + + if (options.message === Messages.StrictParamDupe) { + throwError( + strict ? options.stricted : options.firstRestricted, + options.message + ); + } + + if (defaultCount === 0) { + defaults = []; + } + + return { + params: params, + defaults: defaults, + rest: rest, + stricted: options.stricted, + firstRestricted: options.firstRestricted, + message: options.message + }; + } + + function parseArrowFunctionExpression(options, marker) { + var previousStrict, previousYieldAllowed, body; + + expect('=>'); + + previousStrict = strict; + previousYieldAllowed = state.yieldAllowed; + state.yieldAllowed = false; + body = parseConciseBody(); + + if (strict && options.firstRestricted) { + throwError(options.firstRestricted, options.message); + } + if (strict && options.stricted) { + throwErrorTolerant(options.stricted, options.message); + } + + strict = previousStrict; + state.yieldAllowed = previousYieldAllowed; + + return markerApply(marker, delegate.createArrowFunctionExpression( + options.params, + options.defaults, + body, + options.rest, + body.type !== Syntax.BlockStatement + )); + } + + function parseAssignmentExpression() { + var marker, expr, token, params, oldParenthesizedCount; + + // Note that 'yield' is treated as a keyword in strict mode, but a + // contextual keyword (identifier) in non-strict mode, so we need + // to use matchKeyword and matchContextualKeyword appropriately. + if ((state.yieldAllowed && matchContextualKeyword('yield')) || (strict && matchKeyword('yield'))) { + return parseYieldExpression(); + } + + oldParenthesizedCount = state.parenthesizedCount; + + marker = markerCreate(); + + if (match('(')) { + token = lookahead2(); + if ((token.type === Token.Punctuator && token.value === ')') || token.value === '...') { + params = parseParams(); + if (!match('=>')) { + throwUnexpected(lex()); + } + return parseArrowFunctionExpression(params, marker); + } + } + + token = lookahead; + expr = parseConditionalExpression(); + + if (match('=>') && + (state.parenthesizedCount === oldParenthesizedCount || + state.parenthesizedCount === (oldParenthesizedCount + 1))) { + if (expr.type === Syntax.Identifier) { + params = reinterpretAsCoverFormalsList([ expr ]); + } else if (expr.type === Syntax.SequenceExpression) { + params = reinterpretAsCoverFormalsList(expr.expressions); + } + if (params) { + return parseArrowFunctionExpression(params, marker); + } + } + + if (matchAssign()) { + // 11.13.1 + if (strict && expr.type === Syntax.Identifier && isRestrictedWord(expr.name)) { + throwErrorTolerant(token, Messages.StrictLHSAssignment); + } + + // ES.next draf 11.13 Runtime Semantics step 1 + if (match('=') && (expr.type === Syntax.ObjectExpression || expr.type === Syntax.ArrayExpression)) { + reinterpretAsAssignmentBindingPattern(expr); + } else if (!isLeftHandSide(expr)) { + throwError({}, Messages.InvalidLHSInAssignment); + } + + expr = markerApply(marker, delegate.createAssignmentExpression(lex().value, expr, parseAssignmentExpression())); + } + + return expr; + } + + // 11.14 Comma Operator + + function parseExpression() { + var marker, expr, expressions, sequence, coverFormalsList, spreadFound, oldParenthesizedCount; + + oldParenthesizedCount = state.parenthesizedCount; + + marker = markerCreate(); + expr = parseAssignmentExpression(); + expressions = [ expr ]; + + if (match(',')) { + while (index < length) { + if (!match(',')) { + break; + } + + lex(); + expr = parseSpreadOrAssignmentExpression(); + expressions.push(expr); + + if (expr.type === Syntax.SpreadElement) { + spreadFound = true; + if (!match(')')) { + throwError({}, Messages.ElementAfterSpreadElement); + } + break; + } + } + + sequence = markerApply(marker, delegate.createSequenceExpression(expressions)); + } + + if (match('=>')) { + // Do not allow nested parentheses on the LHS of the =>. + if (state.parenthesizedCount === oldParenthesizedCount || state.parenthesizedCount === (oldParenthesizedCount + 1)) { + expr = expr.type === Syntax.SequenceExpression ? expr.expressions : expressions; + coverFormalsList = reinterpretAsCoverFormalsList(expr); + if (coverFormalsList) { + return parseArrowFunctionExpression(coverFormalsList, marker); + } + } + throwUnexpected(lex()); + } + + if (spreadFound && lookahead2().value !== '=>') { + throwError({}, Messages.IllegalSpread); + } + + return sequence || expr; + } + + // 12.1 Block + + function parseStatementList() { + var list = [], + statement; + + while (index < length) { + if (match('}')) { + break; + } + statement = parseSourceElement(); + if (typeof statement === 'undefined') { + break; + } + list.push(statement); + } + + return list; + } + + function parseBlock() { + var block, marker = markerCreate(); + + expect('{'); + + block = parseStatementList(); + + expect('}'); + + return markerApply(marker, delegate.createBlockStatement(block)); + } + + // 12.2 Variable Statement + + function parseVariableIdentifier() { + var marker = markerCreate(), + token = lex(); + + if (token.type !== Token.Identifier) { + throwUnexpected(token); + } + + return markerApply(marker, delegate.createIdentifier(token.value)); + } + + function parseVariableDeclaration(kind) { + var id, + marker = markerCreate(), + init = null; + if (match('{')) { + id = parseObjectInitialiser(); + reinterpretAsAssignmentBindingPattern(id); + } else if (match('[')) { + id = parseArrayInitialiser(); + reinterpretAsAssignmentBindingPattern(id); + } else { + id = state.allowKeyword ? parseNonComputedProperty() : parseVariableIdentifier(); + // 12.2.1 + if (strict && isRestrictedWord(id.name)) { + throwErrorTolerant({}, Messages.StrictVarName); + } + } + + if (kind === 'const') { + if (!match('=')) { + throwError({}, Messages.NoUnintializedConst); + } + expect('='); + init = parseAssignmentExpression(); + } else if (match('=')) { + lex(); + init = parseAssignmentExpression(); + } + + return markerApply(marker, delegate.createVariableDeclarator(id, init)); + } + + function parseVariableDeclarationList(kind) { + var list = []; + + do { + list.push(parseVariableDeclaration(kind)); + if (!match(',')) { + break; + } + lex(); + } while (index < length); + + return list; + } + + function parseVariableStatement() { + var declarations, marker = markerCreate(); + + expectKeyword('var'); + + declarations = parseVariableDeclarationList(); + + consumeSemicolon(); + + return markerApply(marker, delegate.createVariableDeclaration(declarations, 'var')); + } + + // kind may be `const` or `let` + // Both are experimental and not in the specification yet. + // see http://wiki.ecmascript.org/doku.php?id=harmony:const + // and http://wiki.ecmascript.org/doku.php?id=harmony:let + function parseConstLetDeclaration(kind) { + var declarations, marker = markerCreate(); + + expectKeyword(kind); + + declarations = parseVariableDeclarationList(kind); + + consumeSemicolon(); + + return markerApply(marker, delegate.createVariableDeclaration(declarations, kind)); + } + + // http://wiki.ecmascript.org/doku.php?id=harmony:modules + + function parseModuleDeclaration() { + var id, src, body, marker = markerCreate(); + + lex(); // 'module' + + if (peekLineTerminator()) { + throwError({}, Messages.NewlineAfterModule); + } + + switch (lookahead.type) { + + case Token.StringLiteral: + id = parsePrimaryExpression(); + body = parseModuleBlock(); + src = null; + break; + + case Token.Identifier: + id = parseVariableIdentifier(); + body = null; + if (!matchContextualKeyword('from')) { + throwUnexpected(lex()); + } + lex(); + src = parsePrimaryExpression(); + if (src.type !== Syntax.Literal) { + throwError({}, Messages.InvalidModuleSpecifier); + } + break; + } + + consumeSemicolon(); + return markerApply(marker, delegate.createModuleDeclaration(id, src, body)); + } + + function parseExportBatchSpecifier() { + var marker = markerCreate(); + expect('*'); + return markerApply(marker, delegate.createExportBatchSpecifier()); + } + + function parseExportSpecifier() { + var id, name = null, marker = markerCreate(); + + id = parseVariableIdentifier(); + if (matchContextualKeyword('as')) { + lex(); + name = parseNonComputedProperty(); + } + + return markerApply(marker, delegate.createExportSpecifier(id, name)); + } + + function parseExportDeclaration() { + var previousAllowKeyword, decl, def, src, specifiers, + marker = markerCreate(); + + expectKeyword('export'); + + if (lookahead.type === Token.Keyword) { + switch (lookahead.value) { + case 'let': + case 'const': + case 'var': + case 'class': + case 'function': + return markerApply(marker, delegate.createExportDeclaration(parseSourceElement(), null, null)); + } + } + + if (isIdentifierName(lookahead)) { + previousAllowKeyword = state.allowKeyword; + state.allowKeyword = true; + decl = parseVariableDeclarationList('let'); + state.allowKeyword = previousAllowKeyword; + return markerApply(marker, delegate.createExportDeclaration(decl, null, null)); + } + + specifiers = []; + src = null; + + if (match('*')) { + specifiers.push(parseExportBatchSpecifier()); + } else { + expect('{'); + do { + specifiers.push(parseExportSpecifier()); + } while (match(',') && lex()); + expect('}'); + } + + if (matchContextualKeyword('from')) { + lex(); + src = parsePrimaryExpression(); + if (src.type !== Syntax.Literal) { + throwError({}, Messages.InvalidModuleSpecifier); + } + } + + consumeSemicolon(); + + return markerApply(marker, delegate.createExportDeclaration(null, specifiers, src)); + } + + function parseImportDeclaration() { + var specifiers, kind, src, marker = markerCreate(); + + expectKeyword('import'); + specifiers = []; + + if (isIdentifierName(lookahead)) { + kind = 'default'; + specifiers.push(parseImportSpecifier()); + + if (!matchContextualKeyword('from')) { + throwError({}, Messages.NoFromAfterImport); + } + lex(); + } else if (match('{')) { + kind = 'named'; + lex(); + do { + specifiers.push(parseImportSpecifier()); + } while (match(',') && lex()); + expect('}'); + + if (!matchContextualKeyword('from')) { + throwError({}, Messages.NoFromAfterImport); + } + lex(); + } + + src = parsePrimaryExpression(); + if (src.type !== Syntax.Literal) { + throwError({}, Messages.InvalidModuleSpecifier); + } + + consumeSemicolon(); + + return markerApply(marker, delegate.createImportDeclaration(specifiers, kind, src)); + } + + function parseImportSpecifier() { + var id, name = null, marker = markerCreate(); + + id = parseNonComputedProperty(); + if (matchContextualKeyword('as')) { + lex(); + name = parseVariableIdentifier(); + } + + return markerApply(marker, delegate.createImportSpecifier(id, name)); + } + + // 12.3 Empty Statement + + function parseEmptyStatement() { + var marker = markerCreate(); + expect(';'); + return markerApply(marker, delegate.createEmptyStatement()); + } + + // 12.4 Expression Statement + + function parseExpressionStatement() { + var marker = markerCreate(), expr = parseExpression(); + consumeSemicolon(); + return markerApply(marker, delegate.createExpressionStatement(expr)); + } + + // 12.5 If statement + + function parseIfStatement() { + var test, consequent, alternate, marker = markerCreate(); + + expectKeyword('if'); + + expect('('); + + test = parseExpression(); + + expect(')'); + + consequent = parseStatement(); + + if (matchKeyword('else')) { + lex(); + alternate = parseStatement(); + } else { + alternate = null; + } + + return markerApply(marker, delegate.createIfStatement(test, consequent, alternate)); + } + + // 12.6 Iteration Statements + + function parseDoWhileStatement() { + var body, test, oldInIteration, marker = markerCreate(); + + expectKeyword('do'); + + oldInIteration = state.inIteration; + state.inIteration = true; + + body = parseStatement(); + + state.inIteration = oldInIteration; + + expectKeyword('while'); + + expect('('); + + test = parseExpression(); + + expect(')'); + + if (match(';')) { + lex(); + } + + return markerApply(marker, delegate.createDoWhileStatement(body, test)); + } + + function parseWhileStatement() { + var test, body, oldInIteration, marker = markerCreate(); + + expectKeyword('while'); + + expect('('); + + test = parseExpression(); + + expect(')'); + + oldInIteration = state.inIteration; + state.inIteration = true; + + body = parseStatement(); + + state.inIteration = oldInIteration; + + return markerApply(marker, delegate.createWhileStatement(test, body)); + } + + function parseForVariableDeclaration() { + var marker = markerCreate(), + token = lex(), + declarations = parseVariableDeclarationList(); + + return markerApply(marker, delegate.createVariableDeclaration(declarations, token.value)); + } + + function parseForStatement(opts) { + var init, test, update, left, right, body, operator, oldInIteration, + marker = markerCreate(); + init = test = update = null; + expectKeyword('for'); + + // http://wiki.ecmascript.org/doku.php?id=proposals:iterators_and_generators&s=each + if (matchContextualKeyword('each')) { + throwError({}, Messages.EachNotAllowed); + } + + expect('('); + + if (match(';')) { + lex(); + } else { + if (matchKeyword('var') || matchKeyword('let') || matchKeyword('const')) { + state.allowIn = false; + init = parseForVariableDeclaration(); + state.allowIn = true; + + if (init.declarations.length === 1) { + if (matchKeyword('in') || matchContextualKeyword('of')) { + operator = lookahead; + if (!((operator.value === 'in' || init.kind !== 'var') && init.declarations[0].init)) { + lex(); + left = init; + right = parseExpression(); + init = null; + } + } + } + } else { + state.allowIn = false; + init = parseExpression(); + state.allowIn = true; + + if (matchContextualKeyword('of')) { + operator = lex(); + left = init; + right = parseExpression(); + init = null; + } else if (matchKeyword('in')) { + // LeftHandSideExpression + if (!isAssignableLeftHandSide(init)) { + throwError({}, Messages.InvalidLHSInForIn); + } + operator = lex(); + left = init; + right = parseExpression(); + init = null; + } + } + + if (typeof left === 'undefined') { + expect(';'); + } + } + + if (typeof left === 'undefined') { + + if (!match(';')) { + test = parseExpression(); + } + expect(';'); + + if (!match(')')) { + update = parseExpression(); + } + } + + expect(')'); + + oldInIteration = state.inIteration; + state.inIteration = true; + + if (!(opts !== undefined && opts.ignoreBody)) { + body = parseStatement(); + } + + state.inIteration = oldInIteration; + + if (typeof left === 'undefined') { + return markerApply(marker, delegate.createForStatement(init, test, update, body)); + } + + if (operator.value === 'in') { + return markerApply(marker, delegate.createForInStatement(left, right, body)); + } + return markerApply(marker, delegate.createForOfStatement(left, right, body)); + } + + // 12.7 The continue statement + + function parseContinueStatement() { + var label = null, key, marker = markerCreate(); + + expectKeyword('continue'); + + // Optimize the most common form: 'continue;'. + if (source.charCodeAt(index) === 59) { + lex(); + + if (!state.inIteration) { + throwError({}, Messages.IllegalContinue); + } + + return markerApply(marker, delegate.createContinueStatement(null)); + } + + if (peekLineTerminator()) { + if (!state.inIteration) { + throwError({}, Messages.IllegalContinue); + } + + return markerApply(marker, delegate.createContinueStatement(null)); + } + + if (lookahead.type === Token.Identifier) { + label = parseVariableIdentifier(); + + key = '$' + label.name; + if (!Object.prototype.hasOwnProperty.call(state.labelSet, key)) { + throwError({}, Messages.UnknownLabel, label.name); + } + } + + consumeSemicolon(); + + if (label === null && !state.inIteration) { + throwError({}, Messages.IllegalContinue); + } + + return markerApply(marker, delegate.createContinueStatement(label)); + } + + // 12.8 The break statement + + function parseBreakStatement() { + var label = null, key, marker = markerCreate(); + + expectKeyword('break'); + + // Catch the very common case first: immediately a semicolon (char #59). + if (source.charCodeAt(index) === 59) { + lex(); + + if (!(state.inIteration || state.inSwitch)) { + throwError({}, Messages.IllegalBreak); + } + + return markerApply(marker, delegate.createBreakStatement(null)); + } + + if (peekLineTerminator()) { + if (!(state.inIteration || state.inSwitch)) { + throwError({}, Messages.IllegalBreak); + } + + return markerApply(marker, delegate.createBreakStatement(null)); + } + + if (lookahead.type === Token.Identifier) { + label = parseVariableIdentifier(); + + key = '$' + label.name; + if (!Object.prototype.hasOwnProperty.call(state.labelSet, key)) { + throwError({}, Messages.UnknownLabel, label.name); + } + } + + consumeSemicolon(); + + if (label === null && !(state.inIteration || state.inSwitch)) { + throwError({}, Messages.IllegalBreak); + } + + return markerApply(marker, delegate.createBreakStatement(label)); + } + + // 12.9 The return statement + + function parseReturnStatement() { + var argument = null, marker = markerCreate(); + + expectKeyword('return'); + + if (!state.inFunctionBody) { + throwErrorTolerant({}, Messages.IllegalReturn); + } + + // 'return' followed by a space and an identifier is very common. + if (source.charCodeAt(index) === 32) { + if (isIdentifierStart(source.charCodeAt(index + 1))) { + argument = parseExpression(); + consumeSemicolon(); + return markerApply(marker, delegate.createReturnStatement(argument)); + } + } + + if (peekLineTerminator()) { + return markerApply(marker, delegate.createReturnStatement(null)); + } + + if (!match(';')) { + if (!match('}') && lookahead.type !== Token.EOF) { + argument = parseExpression(); + } + } + + consumeSemicolon(); + + return markerApply(marker, delegate.createReturnStatement(argument)); + } + + // 12.10 The with statement + + function parseWithStatement() { + var object, body, marker = markerCreate(); + + if (strict) { + throwErrorTolerant({}, Messages.StrictModeWith); + } + + expectKeyword('with'); + + expect('('); + + object = parseExpression(); + + expect(')'); + + body = parseStatement(); + + return markerApply(marker, delegate.createWithStatement(object, body)); + } + + // 12.10 The swith statement + + function parseSwitchCase() { + var test, + consequent = [], + sourceElement, + marker = markerCreate(); + + if (matchKeyword('default')) { + lex(); + test = null; + } else { + expectKeyword('case'); + test = parseExpression(); + } + expect(':'); + + while (index < length) { + if (match('}') || matchKeyword('default') || matchKeyword('case')) { + break; + } + sourceElement = parseSourceElement(); + if (typeof sourceElement === 'undefined') { + break; + } + consequent.push(sourceElement); + } + + return markerApply(marker, delegate.createSwitchCase(test, consequent)); + } + + function parseSwitchStatement() { + var discriminant, cases, clause, oldInSwitch, defaultFound, marker = markerCreate(); + + expectKeyword('switch'); + + expect('('); + + discriminant = parseExpression(); + + expect(')'); + + expect('{'); + + cases = []; + + if (match('}')) { + lex(); + return markerApply(marker, delegate.createSwitchStatement(discriminant, cases)); + } + + oldInSwitch = state.inSwitch; + state.inSwitch = true; + defaultFound = false; + + while (index < length) { + if (match('}')) { + break; + } + clause = parseSwitchCase(); + if (clause.test === null) { + if (defaultFound) { + throwError({}, Messages.MultipleDefaultsInSwitch); + } + defaultFound = true; + } + cases.push(clause); + } + + state.inSwitch = oldInSwitch; + + expect('}'); + + return markerApply(marker, delegate.createSwitchStatement(discriminant, cases)); + } + + // 12.13 The throw statement + + function parseThrowStatement() { + var argument, marker = markerCreate(); + + expectKeyword('throw'); + + if (peekLineTerminator()) { + throwError({}, Messages.NewlineAfterThrow); + } + + argument = parseExpression(); + + consumeSemicolon(); + + return markerApply(marker, delegate.createThrowStatement(argument)); + } + + // 12.14 The try statement + + function parseCatchClause() { + var param, body, marker = markerCreate(); + + expectKeyword('catch'); + + expect('('); + if (match(')')) { + throwUnexpected(lookahead); + } + + param = parseExpression(); + // 12.14.1 + if (strict && param.type === Syntax.Identifier && isRestrictedWord(param.name)) { + throwErrorTolerant({}, Messages.StrictCatchVariable); + } + + expect(')'); + body = parseBlock(); + return markerApply(marker, delegate.createCatchClause(param, body)); + } + + function parseTryStatement() { + var block, handlers = [], finalizer = null, marker = markerCreate(); + + expectKeyword('try'); + + block = parseBlock(); + + if (matchKeyword('catch')) { + handlers.push(parseCatchClause()); + } + + if (matchKeyword('finally')) { + lex(); + finalizer = parseBlock(); + } + + if (handlers.length === 0 && !finalizer) { + throwError({}, Messages.NoCatchOrFinally); + } + + return markerApply(marker, delegate.createTryStatement(block, [], handlers, finalizer)); + } + + // 12.15 The debugger statement + + function parseDebuggerStatement() { + var marker = markerCreate(); + expectKeyword('debugger'); + + consumeSemicolon(); + + return markerApply(marker, delegate.createDebuggerStatement()); + } + + // 12 Statements + + function parseStatement() { + var type = lookahead.type, + marker, + expr, + labeledBody, + key; + + if (type === Token.EOF) { + throwUnexpected(lookahead); + } + + if (type === Token.Punctuator) { + switch (lookahead.value) { + case ';': + return parseEmptyStatement(); + case '{': + return parseBlock(); + case '(': + return parseExpressionStatement(); + default: + break; + } + } + + if (type === Token.Keyword) { + switch (lookahead.value) { + case 'break': + return parseBreakStatement(); + case 'continue': + return parseContinueStatement(); + case 'debugger': + return parseDebuggerStatement(); + case 'do': + return parseDoWhileStatement(); + case 'for': + return parseForStatement(); + case 'function': + return parseFunctionDeclaration(); + case 'class': + return parseClassDeclaration(); + case 'if': + return parseIfStatement(); + case 'return': + return parseReturnStatement(); + case 'switch': + return parseSwitchStatement(); + case 'throw': + return parseThrowStatement(); + case 'try': + return parseTryStatement(); + case 'var': + return parseVariableStatement(); + case 'while': + return parseWhileStatement(); + case 'with': + return parseWithStatement(); + default: + break; + } + } + + marker = markerCreate(); + expr = parseExpression(); + + // 12.12 Labelled Statements + if ((expr.type === Syntax.Identifier) && match(':')) { + lex(); + + key = '$' + expr.name; + if (Object.prototype.hasOwnProperty.call(state.labelSet, key)) { + throwError({}, Messages.Redeclaration, 'Label', expr.name); + } + + state.labelSet[key] = true; + labeledBody = parseStatement(); + delete state.labelSet[key]; + return markerApply(marker, delegate.createLabeledStatement(expr, labeledBody)); + } + + consumeSemicolon(); + + return markerApply(marker, delegate.createExpressionStatement(expr)); + } + + // 13 Function Definition + + function parseConciseBody() { + if (match('{')) { + return parseFunctionSourceElements(); + } + return parseAssignmentExpression(); + } + + function parseFunctionSourceElements() { + var sourceElement, sourceElements = [], token, directive, firstRestricted, + oldLabelSet, oldInIteration, oldInSwitch, oldInFunctionBody, oldParenthesizedCount, + marker = markerCreate(); + + expect('{'); + + while (index < length) { + if (lookahead.type !== Token.StringLiteral) { + break; + } + token = lookahead; + + sourceElement = parseSourceElement(); + sourceElements.push(sourceElement); + if (sourceElement.expression.type !== Syntax.Literal) { + // this is not directive + break; + } + directive = source.slice(token.range[0] + 1, token.range[1] - 1); + if (directive === 'use strict') { + strict = true; + if (firstRestricted) { + throwErrorTolerant(firstRestricted, Messages.StrictOctalLiteral); + } + } else { + if (!firstRestricted && token.octal) { + firstRestricted = token; + } + } + } + + oldLabelSet = state.labelSet; + oldInIteration = state.inIteration; + oldInSwitch = state.inSwitch; + oldInFunctionBody = state.inFunctionBody; + oldParenthesizedCount = state.parenthesizedCount; + + state.labelSet = {}; + state.inIteration = false; + state.inSwitch = false; + state.inFunctionBody = true; + state.parenthesizedCount = 0; + + while (index < length) { + if (match('}')) { + break; + } + sourceElement = parseSourceElement(); + if (typeof sourceElement === 'undefined') { + break; + } + sourceElements.push(sourceElement); + } + + expect('}'); + + state.labelSet = oldLabelSet; + state.inIteration = oldInIteration; + state.inSwitch = oldInSwitch; + state.inFunctionBody = oldInFunctionBody; + state.parenthesizedCount = oldParenthesizedCount; + + return markerApply(marker, delegate.createBlockStatement(sourceElements)); + } + + function validateParam(options, param, name) { + var key = '$' + name; + if (strict) { + if (isRestrictedWord(name)) { + options.stricted = param; + options.message = Messages.StrictParamName; + } + if (Object.prototype.hasOwnProperty.call(options.paramSet, key)) { + options.stricted = param; + options.message = Messages.StrictParamDupe; + } + } else if (!options.firstRestricted) { + if (isRestrictedWord(name)) { + options.firstRestricted = param; + options.message = Messages.StrictParamName; + } else if (isStrictModeReservedWord(name)) { + options.firstRestricted = param; + options.message = Messages.StrictReservedWord; + } else if (Object.prototype.hasOwnProperty.call(options.paramSet, key)) { + options.firstRestricted = param; + options.message = Messages.StrictParamDupe; + } + } + options.paramSet[key] = true; + } + + function parseParam(options) { + var token, rest, param, def; + + token = lookahead; + if (token.value === '...') { + token = lex(); + rest = true; + } + + if (match('[')) { + param = parseArrayInitialiser(); + reinterpretAsDestructuredParameter(options, param); + } else if (match('{')) { + if (rest) { + throwError({}, Messages.ObjectPatternAsRestParameter); + } + param = parseObjectInitialiser(); + reinterpretAsDestructuredParameter(options, param); + } else { + param = parseVariableIdentifier(); + validateParam(options, token, token.value); + if (match('=')) { + if (rest) { + throwErrorTolerant(lookahead, Messages.DefaultRestParameter); + } + lex(); + def = parseAssignmentExpression(); + ++options.defaultCount; + } + } + + if (rest) { + if (!match(')')) { + throwError({}, Messages.ParameterAfterRestParameter); + } + options.rest = param; + return false; + } + + options.params.push(param); + options.defaults.push(def); + return !match(')'); + } + + function parseParams(firstRestricted) { + var options, marker = markerCreate(); + + options = { + params: [], + defaultCount: 0, + defaults: [], + rest: null, + firstRestricted: firstRestricted + }; + + expect('('); + + if (!match(')')) { + options.paramSet = {}; + while (index < length) { + if (!parseParam(options)) { + break; + } + expect(','); + } + } + + expect(')'); + + if (options.defaultCount === 0) { + options.defaults = []; + } + + return markerApply(marker, options); + } + + function parseFunctionDeclaration() { + var id, body, token, tmp, firstRestricted, message, previousStrict, previousYieldAllowed, generator, + marker = markerCreate(); + + expectKeyword('function'); + + generator = false; + if (match('*')) { + lex(); + generator = true; + } + + token = lookahead; + + id = parseVariableIdentifier(); + + if (strict) { + if (isRestrictedWord(token.value)) { + throwErrorTolerant(token, Messages.StrictFunctionName); + } + } else { + if (isRestrictedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictFunctionName; + } else if (isStrictModeReservedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictReservedWord; + } + } + + tmp = parseParams(firstRestricted); + firstRestricted = tmp.firstRestricted; + if (tmp.message) { + message = tmp.message; + } + + previousStrict = strict; + previousYieldAllowed = state.yieldAllowed; + state.yieldAllowed = generator; + + body = parseFunctionSourceElements(); + + if (strict && firstRestricted) { + throwError(firstRestricted, message); + } + if (strict && tmp.stricted) { + throwErrorTolerant(tmp.stricted, message); + } + strict = previousStrict; + state.yieldAllowed = previousYieldAllowed; + + return markerApply(marker, delegate.createFunctionDeclaration(id, tmp.params, tmp.defaults, body, tmp.rest, generator, false)); + } + + function parseFunctionExpression() { + var token, id = null, firstRestricted, message, tmp, body, previousStrict, previousYieldAllowed, generator, + marker = markerCreate(); + + expectKeyword('function'); + + generator = false; + + if (match('*')) { + lex(); + generator = true; + } + + if (!match('(')) { + token = lookahead; + id = parseVariableIdentifier(); + if (strict) { + if (isRestrictedWord(token.value)) { + throwErrorTolerant(token, Messages.StrictFunctionName); + } + } else { + if (isRestrictedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictFunctionName; + } else if (isStrictModeReservedWord(token.value)) { + firstRestricted = token; + message = Messages.StrictReservedWord; + } + } + } + + tmp = parseParams(firstRestricted); + firstRestricted = tmp.firstRestricted; + if (tmp.message) { + message = tmp.message; + } + + previousStrict = strict; + previousYieldAllowed = state.yieldAllowed; + state.yieldAllowed = generator; + + body = parseFunctionSourceElements(); + + if (strict && firstRestricted) { + throwError(firstRestricted, message); + } + if (strict && tmp.stricted) { + throwErrorTolerant(tmp.stricted, message); + } + strict = previousStrict; + state.yieldAllowed = previousYieldAllowed; + + return markerApply(marker, delegate.createFunctionExpression(id, tmp.params, tmp.defaults, body, tmp.rest, generator, false)); + } + + function parseYieldExpression() { + var yieldToken, delegateFlag, expr, marker = markerCreate(); + + yieldToken = lex(); + assert(yieldToken.value === 'yield', 'Called parseYieldExpression with non-yield lookahead.'); + + if (!state.yieldAllowed) { + throwErrorTolerant({}, Messages.IllegalYield); + } + + delegateFlag = false; + if (match('*')) { + lex(); + delegateFlag = true; + } + + expr = parseAssignmentExpression(); + + return markerApply(marker, delegate.createYieldExpression(expr, delegateFlag)); + } + + // 14 Classes + + function parseMethodDefinition(existingPropNames) { + var token, key, param, propType, isValidDuplicateProp = false, + marker = markerCreate(); + + if (lookahead.value === 'static') { + propType = ClassPropertyType.static; + lex(); + } else { + propType = ClassPropertyType.prototype; + } + + if (match('*')) { + lex(); + return markerApply(marker, delegate.createMethodDefinition( + propType, + '', + parseObjectPropertyKey(), + parsePropertyMethodFunction({ generator: true }) + )); + } + + token = lookahead; + key = parseObjectPropertyKey(); + + if (token.value === 'get' && !match('(')) { + key = parseObjectPropertyKey(); + + // It is a syntax error if any other properties have a name + // duplicating this one unless they are a setter + if (existingPropNames[propType].hasOwnProperty(key.name)) { + isValidDuplicateProp = + // There isn't already a getter for this prop + existingPropNames[propType][key.name].get === undefined + // There isn't already a data prop by this name + && existingPropNames[propType][key.name].data === undefined + // The only existing prop by this name is a setter + && existingPropNames[propType][key.name].set !== undefined; + if (!isValidDuplicateProp) { + throwError(key, Messages.IllegalDuplicateClassProperty); + } + } else { + existingPropNames[propType][key.name] = {}; + } + existingPropNames[propType][key.name].get = true; + + expect('('); + expect(')'); + return markerApply(marker, delegate.createMethodDefinition( + propType, + 'get', + key, + parsePropertyFunction({ generator: false }) + )); + } + if (token.value === 'set' && !match('(')) { + key = parseObjectPropertyKey(); + + // It is a syntax error if any other properties have a name + // duplicating this one unless they are a getter + if (existingPropNames[propType].hasOwnProperty(key.name)) { + isValidDuplicateProp = + // There isn't already a setter for this prop + existingPropNames[propType][key.name].set === undefined + // There isn't already a data prop by this name + && existingPropNames[propType][key.name].data === undefined + // The only existing prop by this name is a getter + && existingPropNames[propType][key.name].get !== undefined; + if (!isValidDuplicateProp) { + throwError(key, Messages.IllegalDuplicateClassProperty); + } + } else { + existingPropNames[propType][key.name] = {}; + } + existingPropNames[propType][key.name].set = true; + + expect('('); + token = lookahead; + param = [ parseVariableIdentifier() ]; + expect(')'); + return markerApply(marker, delegate.createMethodDefinition( + propType, + 'set', + key, + parsePropertyFunction({ params: param, generator: false, name: token }) + )); + } + + // It is a syntax error if any other properties have the same name as a + // non-getter, non-setter method + if (existingPropNames[propType].hasOwnProperty(key.name)) { + throwError(key, Messages.IllegalDuplicateClassProperty); + } else { + existingPropNames[propType][key.name] = {}; + } + existingPropNames[propType][key.name].data = true; + + return markerApply(marker, delegate.createMethodDefinition( + propType, + '', + key, + parsePropertyMethodFunction({ generator: false }) + )); + } + + function parseClassElement(existingProps) { + if (match(';')) { + lex(); + return; + } + return parseMethodDefinition(existingProps); + } + + function parseClassBody() { + var classElement, classElements = [], existingProps = {}, marker = markerCreate(); + + existingProps[ClassPropertyType.static] = {}; + existingProps[ClassPropertyType.prototype] = {}; + + expect('{'); + + while (index < length) { + if (match('}')) { + break; + } + classElement = parseClassElement(existingProps); + + if (typeof classElement !== 'undefined') { + classElements.push(classElement); + } + } + + expect('}'); + + return markerApply(marker, delegate.createClassBody(classElements)); + } + + function parseClassExpression() { + var id, previousYieldAllowed, superClass = null, marker = markerCreate(); + + expectKeyword('class'); + + if (!matchKeyword('extends') && !match('{')) { + id = parseVariableIdentifier(); + } + + if (matchKeyword('extends')) { + expectKeyword('extends'); + previousYieldAllowed = state.yieldAllowed; + state.yieldAllowed = false; + superClass = parseAssignmentExpression(); + state.yieldAllowed = previousYieldAllowed; + } + + return markerApply(marker, delegate.createClassExpression(id, superClass, parseClassBody())); + } + + function parseClassDeclaration() { + var id, previousYieldAllowed, superClass = null, marker = markerCreate(); + + expectKeyword('class'); + + id = parseVariableIdentifier(); + + if (matchKeyword('extends')) { + expectKeyword('extends'); + previousYieldAllowed = state.yieldAllowed; + state.yieldAllowed = false; + superClass = parseAssignmentExpression(); + state.yieldAllowed = previousYieldAllowed; + } + + return markerApply(marker, delegate.createClassDeclaration(id, superClass, parseClassBody())); + } + + // 15 Program + + function matchModuleDeclaration() { + var id; + if (matchContextualKeyword('module')) { + id = lookahead2(); + return id.type === Token.StringLiteral || id.type === Token.Identifier; + } + return false; + } + + function parseSourceElement() { + if (lookahead.type === Token.Keyword) { + switch (lookahead.value) { + case 'const': + case 'let': + return parseConstLetDeclaration(lookahead.value); + case 'function': + return parseFunctionDeclaration(); + case 'export': + return parseExportDeclaration(); + case 'import': + return parseImportDeclaration(); + default: + return parseStatement(); + } + } + + if (matchModuleDeclaration()) { + throwError({}, Messages.NestedModule); + } + + if (lookahead.type !== Token.EOF) { + return parseStatement(); + } + } + + function parseProgramElement() { + if (lookahead.type === Token.Keyword) { + switch (lookahead.value) { + case 'export': + return parseExportDeclaration(); + case 'import': + return parseImportDeclaration(); + } + } + + if (matchModuleDeclaration()) { + return parseModuleDeclaration(); + } + + return parseSourceElement(); + } + + function parseProgramElements() { + var sourceElement, sourceElements = [], token, directive, firstRestricted; + + while (index < length) { + token = lookahead; + if (token.type !== Token.StringLiteral) { + break; + } + + sourceElement = parseProgramElement(); + sourceElements.push(sourceElement); + if (sourceElement.expression.type !== Syntax.Literal) { + // this is not directive + break; + } + directive = source.slice(token.range[0] + 1, token.range[1] - 1); + if (directive === 'use strict') { + strict = true; + if (firstRestricted) { + throwErrorTolerant(firstRestricted, Messages.StrictOctalLiteral); + } + } else { + if (!firstRestricted && token.octal) { + firstRestricted = token; + } + } + } + + while (index < length) { + sourceElement = parseProgramElement(); + if (typeof sourceElement === 'undefined') { + break; + } + sourceElements.push(sourceElement); + } + return sourceElements; + } + + function parseModuleElement() { + return parseSourceElement(); + } + + function parseModuleElements() { + var list = [], + statement; + + while (index < length) { + if (match('}')) { + break; + } + statement = parseModuleElement(); + if (typeof statement === 'undefined') { + break; + } + list.push(statement); + } + + return list; + } + + function parseModuleBlock() { + var block, marker = markerCreate(); + + expect('{'); + + block = parseModuleElements(); + + expect('}'); + + return markerApply(marker, delegate.createBlockStatement(block)); + } + + function parseProgram() { + var body, marker = markerCreate(); + strict = false; + peek(); + body = parseProgramElements(); + return markerApply(marker, delegate.createProgram(body)); + } + + // The following functions are needed only when the option to preserve + // the comments is active. + + function addComment(type, value, start, end, loc) { + var comment; + + assert(typeof start === 'number', 'Comment must have valid position'); + + // Because the way the actual token is scanned, often the comments + // (if any) are skipped twice during the lexical analysis. + // Thus, we need to skip adding a comment if the comment array already + // handled it. + if (state.lastCommentStart >= start) { + return; + } + state.lastCommentStart = start; + + comment = { + type: type, + value: value + }; + if (extra.range) { + comment.range = [start, end]; + } + if (extra.loc) { + comment.loc = loc; + } + extra.comments.push(comment); + } + + function scanComment() { + var comment, ch, loc, start, blockComment, lineComment; + + comment = ''; + blockComment = false; + lineComment = false; + + while (index < length) { + ch = source[index]; + + if (lineComment) { + ch = source[index++]; + if (isLineTerminator(ch.charCodeAt(0))) { + loc.end = { + line: lineNumber, + column: index - lineStart - 1 + }; + lineComment = false; + addComment('Line', comment, start, index - 1, loc); + if (ch === '\r' && source[index] === '\n') { + ++index; + } + ++lineNumber; + lineStart = index; + comment = ''; + } else if (index >= length) { + lineComment = false; + comment += ch; + loc.end = { + line: lineNumber, + column: length - lineStart + }; + addComment('Line', comment, start, length, loc); + } else { + comment += ch; + } + } else if (blockComment) { + if (isLineTerminator(ch.charCodeAt(0))) { + if (ch === '\r' && source[index + 1] === '\n') { + ++index; + comment += '\r\n'; + } else { + comment += ch; + } + ++lineNumber; + ++index; + lineStart = index; + if (index >= length) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } else { + ch = source[index++]; + if (index >= length) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + comment += ch; + if (ch === '*') { + ch = source[index]; + if (ch === '/') { + comment = comment.substr(0, comment.length - 1); + blockComment = false; + ++index; + loc.end = { + line: lineNumber, + column: index - lineStart + }; + addComment('Block', comment, start, index, loc); + comment = ''; + } + } + } + } else if (ch === '/') { + ch = source[index + 1]; + if (ch === '/') { + loc = { + start: { + line: lineNumber, + column: index - lineStart + } + }; + start = index; + index += 2; + lineComment = true; + if (index >= length) { + loc.end = { + line: lineNumber, + column: index - lineStart + }; + lineComment = false; + addComment('Line', comment, start, index, loc); + } + } else if (ch === '*') { + start = index; + index += 2; + blockComment = true; + loc = { + start: { + line: lineNumber, + column: index - lineStart - 2 + } + }; + if (index >= length) { + throwError({}, Messages.UnexpectedToken, 'ILLEGAL'); + } + } else { + break; + } + } else if (isWhiteSpace(ch.charCodeAt(0))) { + ++index; + } else if (isLineTerminator(ch.charCodeAt(0))) { + ++index; + if (ch === '\r' && source[index] === '\n') { + ++index; + } + ++lineNumber; + lineStart = index; + } else { + break; + } + } + } + + function collectToken() { + var start, loc, token, range, value; + + skipComment(); + start = index; + loc = { + start: { + line: lineNumber, + column: index - lineStart + } + }; + + token = extra.advance(); + loc.end = { + line: lineNumber, + column: index - lineStart + }; + + if (token.type !== Token.EOF) { + range = [token.range[0], token.range[1]]; + value = source.slice(token.range[0], token.range[1]); + extra.tokens.push({ + type: TokenName[token.type], + value: value, + range: range, + loc: loc + }); + } + + return token; + } + + function collectRegex() { + var pos, loc, regex, token; + + skipComment(); + + pos = index; + loc = { + start: { + line: lineNumber, + column: index - lineStart + } + }; + + regex = extra.scanRegExp(); + loc.end = { + line: lineNumber, + column: index - lineStart + }; + + if (!extra.tokenize) { + // Pop the previous token, which is likely '/' or '/=' + if (extra.tokens.length > 0) { + token = extra.tokens[extra.tokens.length - 1]; + if (token.range[0] === pos && token.type === 'Punctuator') { + if (token.value === '/' || token.value === '/=') { + extra.tokens.pop(); + } + } + } + + extra.tokens.push({ + type: 'RegularExpression', + value: regex.literal, + range: [pos, index], + loc: loc + }); + } + + return regex; + } + + function filterTokenLocation() { + var i, entry, token, tokens = []; + + for (i = 0; i < extra.tokens.length; ++i) { + entry = extra.tokens[i]; + token = { + type: entry.type, + value: entry.value + }; + if (extra.range) { + token.range = entry.range; + } + if (extra.loc) { + token.loc = entry.loc; + } + tokens.push(token); + } + + extra.tokens = tokens; + } + + function patch() { + if (extra.comments) { + extra.skipComment = skipComment; + skipComment = scanComment; + } + + if (typeof extra.tokens !== 'undefined') { + extra.advance = advance; + extra.scanRegExp = scanRegExp; + + advance = collectToken; + scanRegExp = collectRegex; + } + } + + function unpatch() { + if (typeof extra.skipComment === 'function') { + skipComment = extra.skipComment; + } + + if (typeof extra.scanRegExp === 'function') { + advance = extra.advance; + scanRegExp = extra.scanRegExp; + } + } + + // This is used to modify the delegate. + + function extend(object, properties) { + var entry, result = {}; + + for (entry in object) { + if (object.hasOwnProperty(entry)) { + result[entry] = object[entry]; + } + } + + for (entry in properties) { + if (properties.hasOwnProperty(entry)) { + result[entry] = properties[entry]; + } + } + + return result; + } + + function tokenize(code, options) { + var toString, + token, + tokens; + + toString = String; + if (typeof code !== 'string' && !(code instanceof String)) { + code = toString(code); + } + + delegate = SyntaxTreeDelegate; + source = code; + index = 0; + lineNumber = (source.length > 0) ? 1 : 0; + lineStart = 0; + length = source.length; + lookahead = null; + state = { + allowKeyword: true, + allowIn: true, + labelSet: {}, + inFunctionBody: false, + inIteration: false, + inSwitch: false, + lastCommentStart: -1 + }; + + extra = {}; + + // Options matching. + options = options || {}; + + // Of course we collect tokens here. + options.tokens = true; + extra.tokens = []; + extra.tokenize = true; + // The following two fields are necessary to compute the Regex tokens. + extra.openParenToken = -1; + extra.openCurlyToken = -1; + + extra.range = (typeof options.range === 'boolean') && options.range; + extra.loc = (typeof options.loc === 'boolean') && options.loc; + + if (typeof options.comment === 'boolean' && options.comment) { + extra.comments = []; + } + if (typeof options.tolerant === 'boolean' && options.tolerant) { + extra.errors = []; + } + + if (length > 0) { + if (typeof source[0] === 'undefined') { + // Try first to convert to a string. This is good as fast path + // for old IE which understands string indexing for string + // literals only and not for string object. + if (code instanceof String) { + source = code.valueOf(); + } + } + } + + patch(); + + try { + peek(); + if (lookahead.type === Token.EOF) { + return extra.tokens; + } + + token = lex(); + while (lookahead.type !== Token.EOF) { + try { + token = lex(); + } catch (lexError) { + token = lookahead; + if (extra.errors) { + extra.errors.push(lexError); + // We have to break on the first error + // to avoid infinite loops. + break; + } else { + throw lexError; + } + } + } + + filterTokenLocation(); + tokens = extra.tokens; + if (typeof extra.comments !== 'undefined') { + tokens.comments = extra.comments; + } + if (typeof extra.errors !== 'undefined') { + tokens.errors = extra.errors; + } + } catch (e) { + throw e; + } finally { + unpatch(); + extra = {}; + } + return tokens; + } + + function parse(code, options) { + var program, toString; + + toString = String; + if (typeof code !== 'string' && !(code instanceof String)) { + code = toString(code); + } + + delegate = SyntaxTreeDelegate; + source = code; + index = 0; + lineNumber = (source.length > 0) ? 1 : 0; + lineStart = 0; + length = source.length; + lookahead = null; + state = { + allowKeyword: false, + allowIn: true, + labelSet: {}, + parenthesizedCount: 0, + inFunctionBody: false, + inIteration: false, + inSwitch: false, + lastCommentStart: -1, + yieldAllowed: false + }; + + extra = {}; + if (typeof options !== 'undefined') { + extra.range = (typeof options.range === 'boolean') && options.range; + extra.loc = (typeof options.loc === 'boolean') && options.loc; + + if (extra.loc && options.source !== null && options.source !== undefined) { + delegate = extend(delegate, { + 'postProcess': function (node) { + node.loc.source = toString(options.source); + return node; + } + }); + } + + if (typeof options.tokens === 'boolean' && options.tokens) { + extra.tokens = []; + } + if (typeof options.comment === 'boolean' && options.comment) { + extra.comments = []; + } + if (typeof options.tolerant === 'boolean' && options.tolerant) { + extra.errors = []; + } + } + + if (length > 0) { + if (typeof source[0] === 'undefined') { + // Try first to convert to a string. This is good as fast path + // for old IE which understands string indexing for string + // literals only and not for string object. + if (code instanceof String) { + source = code.valueOf(); + } + } + } + + patch(); + try { + program = parseProgram(); + if (typeof extra.comments !== 'undefined') { + program.comments = extra.comments; + } + if (typeof extra.tokens !== 'undefined') { + filterTokenLocation(); + program.tokens = extra.tokens; + } + if (typeof extra.errors !== 'undefined') { + program.errors = extra.errors; + } + } catch (e) { + throw e; + } finally { + unpatch(); + extra = {}; + } + + return program; + } + + // Sync with *.json manifests. + exports.version = '1.1.0-dev-harmony'; + + exports.tokenize = tokenize; + + exports.parse = parse; + + // Deep copy. + exports.Syntax = (function () { + var name, types = {}; + + if (typeof Object.create === 'function') { + types = Object.create(null); + } + + for (name in Syntax) { + if (Syntax.hasOwnProperty(name)) { + types[name] = Syntax[name]; + } + } + + if (typeof Object.freeze === 'function') { + Object.freeze(types); + } + + return types; + }()); + +})); +/* vim: set sw=4 ts=4 et tw=80 : */ diff --git a/third_party/jsdoc/node_modules/esprima/package.json b/third_party/jsdoc/node_modules/esprima/package.json new file mode 100644 index 0000000000..c4c1b815a2 --- /dev/null +++ b/third_party/jsdoc/node_modules/esprima/package.json @@ -0,0 +1,61 @@ +{ + "name": "esprima", + "description": "ECMAScript parsing infrastructure for multipurpose analysis", + "homepage": "http://esprima.org", + "main": "esprima.js", + "bin": { + "esparse": "./bin/esparse.js", + "esvalidate": "./bin/esvalidate.js" + }, + "version": "1.1.0-dev-harmony", + "engines": { + "node": ">=0.4.0" + }, + "maintainers": [ + { + "name": "Ariya Hidayat", + "email": "ariya.hidayat@gmail.com", + "url": "http://ariya.ofilabs.com" + } + ], + "repository": { + "type": "git", + "url": "http://github.com/ariya/esprima.git" + }, + "licenses": [ + { + "type": "BSD", + "url": "http://github.com/ariya/esprima/raw/master/LICENSE.BSD" + } + ], + "devDependencies": { + "jslint": "~0.1.9", + "eslint": "~0.1.0", + "istanbul": "~0.1.27", + "complexity-report": "~0.6.1", + "regenerate": "~0.5.4", + "unicode-6.3.0": "~0.1.0", + "json-diff": "~0.3.1" + }, + "scripts": { + "test": "npm run-script lint && node test/run.js && npm run-script coverage && npm run-script complexity", + "lint": "node tools/check-version.js && node_modules/eslint/bin/eslint.js esprima.js && node_modules/jslint/bin/jslint.js esprima.js", + "coverage": "npm run-script analyze-coverage && npm run-script check-coverage", + "analyze-coverage": "node node_modules/istanbul/lib/cli.js cover test/runner.js", + "check-coverage": "node node_modules/istanbul/lib/cli.js check-coverage --statement -8 --branch -28 --function 99.69", + "complexity": "npm run-script analyze-complexity && npm run-script check-complexity", + "analyze-complexity": "node tools/list-complexity.js", + "check-complexity": "node node_modules/complexity-report/src/cli.js --maxcc 31 --silent -l -w esprima.js", + "benchmark": "node test/benchmarks.js", + "benchmark-quick": "node test/benchmarks.js quick" + }, + "readme": "**Esprima** ([esprima.org](http://esprima.org), BSD license) is a high performance,\nstandard-compliant [ECMAScript](http://www.ecma-international.org/publications/standards/Ecma-262.htm)\nparser written in ECMAScript (also popularly known as\n[JavaScript](http://en.wikipedia.org/wiki/JavaScript>JavaScript)).\nEsprima is created and maintained by [Ariya Hidayat](http://twitter.com/ariyahidayat),\nwith the help of [many contributors](https://github.com/ariya/esprima/contributors).\n\n### Features\n\n- Full support for ECMAScript 5.1 ([ECMA-262](http://www.ecma-international.org/publications/standards/Ecma-262.htm))\n- Sensible [syntax tree format](http://esprima.org/doc/index.html#ast) compatible with Mozilla\n[Parser AST](https://developer.mozilla.org/en/SpiderMonkey/Parser_API)\n- Optional tracking of syntax node location (index-based and line-column)\n- Heavily tested (> 600 [unit tests](http://esprima.org/test/) with solid statement and branch coverage)\n- Experimental support for ES6/Harmony (module, class, destructuring, ...)\n\nEsprima serves as a **building block** for some JavaScript\nlanguage tools, from [code instrumentation](http://esprima.org/demo/functiontrace.html)\nto [editor autocompletion](http://esprima.org/demo/autocomplete.html).\n\nEsprima runs on many popular web browsers, as well as other ECMAScript platforms such as\n[Rhino](http://www.mozilla.org/rhino) and [Node.js](https://npmjs.org/package/esprima).\n\nFor more information, check the web site [esprima.org](http://esprima.org).\n", + "readmeFilename": "README.md", + "bugs": { + "url": "https://github.com/ariya/esprima/issues" + }, + "_id": "esprima@1.1.0-dev-harmony", + "_shasum": "a03eaca83ec1125aa3d4acddd2636b4dd707db67", + "_from": "https://github.com/ariya/esprima/tarball/49a2eccb243f29bd653b11e9419241a9d726af7c", + "_resolved": "https://github.com/ariya/esprima/tarball/49a2eccb243f29bd653b11e9419241a9d726af7c" +} diff --git a/third_party/jsdoc/node_modules/js2xmlparser/LICENSE.md b/third_party/jsdoc/node_modules/js2xmlparser/LICENSE.md new file mode 100644 index 0000000000..1b76d2f6a5 --- /dev/null +++ b/third_party/jsdoc/node_modules/js2xmlparser/LICENSE.md @@ -0,0 +1,16 @@ +js2xmlparser is licensed under the MIT license: + +> Copyright © 2012 Michael Kourlas and other contributors +> +> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +> documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +> rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +> persons to whom the Software is furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +> Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +> WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +> COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +> OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/third_party/jsdoc/node_modules/js2xmlparser/lib/js2xmlparser.js b/third_party/jsdoc/node_modules/js2xmlparser/lib/js2xmlparser.js new file mode 100644 index 0000000000..f96c97b634 --- /dev/null +++ b/third_party/jsdoc/node_modules/js2xmlparser/lib/js2xmlparser.js @@ -0,0 +1,328 @@ +/* jshint node:true */ + +/** + * js2xmlparser + * Copyright © 2012 Michael Kourlas and other contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS + * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +(function() { + "use strict"; + + var xmlDeclaration = true; + var xmlVersion = "1.0"; + var xmlEncoding = "UTF-8"; + var attributeString = "@"; + var valueString = "#"; + var prettyPrinting = true; + var indentString = "\t"; + var convertMap = {}; + var useCDATA = false; + + module.exports = function (root, data, options) { + return toXML(init(root, data, options)); + }; + + // Initialization + var init = function(root, data, options) { + // Set option defaults + setOptionDefaults(); + + // Error checking for root element + if (typeof root !== "string") { + throw new Error("root element must be a string"); + } + else if (root === "") { + throw new Error("root element cannot be empty"); + } + + // Error checking and variable initialization for options + if (typeof options === "object" && options !== null) { + if ("declaration" in options) { + if ("include" in options.declaration) { + if (typeof options.declaration.include === "boolean") { + xmlDeclaration = options.declaration.include; + } + else { + throw new Error("declaration.include option must be a boolean"); + } + } + + if ("encoding" in options.declaration) { + if (typeof options.declaration.encoding === "string" || options.declaration.encoding === null) { + xmlEncoding = options.declaration.encoding; + } + else { + throw new Error("declaration.encoding option must be a string or null"); + } + } + } + if ("attributeString" in options) { + if (typeof options.attributeString === "string") { + attributeString = options.attributeString; + } + else { + throw new Error("attributeString option must be a string"); + } + } + if ("valueString" in options) { + if (typeof options.valueString === "string") { + valueString = options.valueString; + } + else { + throw new Error("valueString option must be a string"); + } + } + if ("prettyPrinting" in options) { + if ("enabled" in options.prettyPrinting) { + if (typeof options.prettyPrinting.enabled === "boolean") { + prettyPrinting = options.prettyPrinting.enabled; + } + else { + throw new Error("prettyPrinting.enabled option must be a boolean"); + } + } + + if ("indentString" in options.prettyPrinting) { + if (typeof options.prettyPrinting.indentString === "string") { + indentString = options.prettyPrinting.indentString; + } + else { + throw new Error("prettyPrinting.indentString option must be a string"); + } + } + } + if ("convertMap" in options) { + if (Object.prototype.toString.call(options.convertMap) === "[object Object]") { + convertMap = options.convertMap; + } + else { + throw new Error("convertMap option must be an object"); + } + } + if ("useCDATA" in options) { + if (typeof options.useCDATA === "boolean") { + useCDATA = options.useCDATA; + } + else { + throw new Error("useCDATA option must be a boolean"); + } + } + } + + // Error checking and variable initialization for data + if (typeof data !== "string" && typeof data !== "object" && typeof data !== "number" && + typeof data !== "boolean" && data !== null) { + throw new Error("data must be an object (excluding arrays) or a JSON string"); + } + + if (data === null) { + throw new Error("data must be an object (excluding arrays) or a JSON string"); + } + + if (Object.prototype.toString.call(data) === "[object Array]") { + throw new Error("data must be an object (excluding arrays) or a JSON string"); + } + + if (typeof data === "string") { + data = JSON.parse(data); + } + + var tempData = {}; + tempData[root] = data; // Add root element to object + + return tempData; + }; + + // Convert object to XML + var toXML = function(object) { + // Initialize arguments, if necessary + var xml = arguments[1] || ""; + var level = arguments[2] || 0; + + var i = null; + var tempObject = {}; + + for (var property in object) { + if (object.hasOwnProperty(property)) { + // Element name cannot start with a number + var elementName = property; + if (/^\d/.test(property)) { + elementName = "_" + property; + } + + // Arrays + if (Object.prototype.toString.call(object[property]) === "[object Array]") { + // Create separate XML elements for each array element + for (i = 0; i < object[property].length; i++) { + tempObject = {}; + tempObject[property] = object[property][i]; + + xml = toXML(tempObject, xml, level); + } + } + // JSON-type objects with properties + else if (Object.prototype.toString.call(object[property]) === "[object Object]") { + xml += addIndent("<" + elementName, level); + + // Add attributes + var lengthExcludingAttributes = Object.keys(object[property]).length; + if (Object.prototype.toString.call(object[property][attributeString]) === "[object Object]") { + lengthExcludingAttributes -= 1; + for (var attribute in object[property][attributeString]) { + if (object[property][attributeString].hasOwnProperty(attribute)) { + xml += " " + attribute + "=\"" + + toString(object[property][attributeString][attribute], true) + "\""; + } + } + } + else if (typeof object[property][attributeString] !== "undefined") { + // Fix for the case where an object contains a single property with the attribute string as its + // name, but this property contains no attributes; in that case, lengthExcludingAttributes + // should be set to zero to ensure that the object is considered an empty object for the + // purposes of the following if statement. + lengthExcludingAttributes -= 1; + } + + if (lengthExcludingAttributes === 0) { // Empty object + xml += addBreak("/>"); + } + else if (lengthExcludingAttributes === 1 && valueString in object[property]) { // Value string only + xml += addBreak(">" + toString(object[property][valueString], false) + ""); + } + else { // Object with properties + xml += addBreak(">"); + + // Create separate object for each property and pass to this function + for (var subProperty in object[property]) { + if (object[property].hasOwnProperty(subProperty) && subProperty !== attributeString && subProperty !== valueString) { + tempObject = {}; + tempObject[subProperty] = object[property][subProperty]; + + xml = toXML(tempObject, xml, level + 1); + } + } + + xml += addBreak(addIndent("", level)); + } + } + // Everything else + else { + xml += addBreak(addIndent("<" + elementName + ">" + toString(object[property], false) + "", level)); + } + } + } + + // Finalize XML at end of process + if (level === 0) { + // Strip trailing whitespace + xml = xml.replace(/\s+$/g, ""); + + // Add XML declaration + if (xmlDeclaration) { + if (xmlEncoding === null) { + xml = addBreak("") + xml; + } + else { + xml = addBreak("") + xml; + } + } + } + + return xml; + }; + + // Add indenting to data for pretty printing + var addIndent = function(data, level) { + if (prettyPrinting) { + + var indent = ""; + for (var i = 0; i < level; i++) { + indent += indentString; + } + data = indent + data; + } + + return data; + }; + + // Add line break to data for pretty printing + var addBreak = function(data) { + return prettyPrinting ? data + "\n" : data; + }; + + // Convert anything into a valid XML string representation + var toString = function(data, isAttribute) { + // Recursive function used to handle nested functions + var functionHelper = function(data) { + if (Object.prototype.toString.call(data) === "[object Function]") { + return functionHelper(data()); + } + else { + return data; + } + }; + + // Convert map + if (Object.prototype.toString.call(data) in convertMap) { + data = convertMap[Object.prototype.toString.call(data)](data); + } + else if ("*" in convertMap) { + data = convertMap["*"](data); + } + // Functions + else if (Object.prototype.toString.call(data) === "[object Function]") { + data = functionHelper(data()); + } + // Empty objects + else if (Object.prototype.toString.call(data) === "[object Object]" && Object.keys(data).length === 0) { + data = ""; + } + + // Cast data to string + if (typeof data !== "string") { + data = (data === null || typeof data === "undefined") ? "" : data.toString(); + } + + // Output as CDATA instead of escaping if option set (and only if not an attribute value) + if (useCDATA && !isAttribute) { + data = "/gm, "]]]]>") + "]]>"; + } + else { + // Escape illegal XML characters + data = data.replace(/&/gm, "&") + .replace(//gm, ">") + .replace(/"/gm, """) + .replace(/'/gm, "'"); + } + + return data; + }; + + // Revert options back to their default settings + var setOptionDefaults = function() { + useCDATA = false; + convertMap = {}; + xmlDeclaration = true; + xmlVersion = "1.0"; + xmlEncoding = "UTF-8"; + attributeString = "@"; + valueString = "#"; + prettyPrinting = true; + indentString = "\t"; + }; +})(); diff --git a/third_party/jsdoc/node_modules/js2xmlparser/package.json b/third_party/jsdoc/node_modules/js2xmlparser/package.json new file mode 100644 index 0000000000..a5a850fc08 --- /dev/null +++ b/third_party/jsdoc/node_modules/js2xmlparser/package.json @@ -0,0 +1,54 @@ +{ + "name": "js2xmlparser", + "description": "Parses JavaScript objects into XML", + "keywords": [ + "convert", + "converter", + "js", + "json", + "object", + "objects", + "parse", + "parser", + "xml" + ], + "homepage": "http://www.kourlas.net", + "version": "0.1.5", + "author": { + "name": "Michael Kourlas", + "email": "michael@kourlas.net" + }, + "main": "./lib/js2xmlparser.js", + "repository": { + "type": "git", + "url": "git://github.com/michaelkourlas/node-js2xmlparser.git" + }, + "devDependencies": { + "mocha": "*", + "should": "*" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/michaelkourlas/node-js2xmlparser/issues" + }, + "_id": "js2xmlparser@0.1.5", + "_shasum": "b42b3ac5a74bb282ff06cc93dfa67fb98a22959d", + "_from": "js2xmlparser@0.1.5", + "_npmVersion": "1.4.9", + "_npmUser": { + "name": "michaelkourlas", + "email": "michael@kourlas.net" + }, + "maintainers": [ + { + "name": "michaelkourlas", + "email": "michaelkourlas@gmail.com" + } + ], + "dist": { + "shasum": "b42b3ac5a74bb282ff06cc93dfa67fb98a22959d", + "tarball": "http://registry.npmjs.org/js2xmlparser/-/js2xmlparser-0.1.5.tgz" + }, + "directories": {}, + "_resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-0.1.5.tgz" +} diff --git a/third_party/jsdoc/node_modules/marked/LICENSE b/third_party/jsdoc/node_modules/marked/LICENSE new file mode 100644 index 0000000000..a7b812ed61 --- /dev/null +++ b/third_party/jsdoc/node_modules/marked/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011-2014, Christopher Jeffrey (https://github.com/chjj/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/jsdoc/node_modules/marked/index.js b/third_party/jsdoc/node_modules/marked/index.js new file mode 100644 index 0000000000..a12f90569f --- /dev/null +++ b/third_party/jsdoc/node_modules/marked/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/marked'); diff --git a/third_party/jsdoc/node_modules/marked/lib/marked.js b/third_party/jsdoc/node_modules/marked/lib/marked.js new file mode 100644 index 0000000000..e2f08c9983 --- /dev/null +++ b/third_party/jsdoc/node_modules/marked/lib/marked.js @@ -0,0 +1,1266 @@ +/** + * marked - a markdown parser + * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) + * https://github.com/chjj/marked + */ + +;(function() { + +/** + * Block-Level Grammar + */ + +var block = { + newline: /^\n+/, + code: /^( {4}[^\n]+\n*)+/, + fences: noop, + hr: /^( *[-*_]){3,} *(?:\n+|$)/, + heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, + nptable: noop, + lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, + blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/, + list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, + html: /^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/, + def: /^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, + table: noop, + paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/, + text: /^[^\n]+/ +}; + +block.bullet = /(?:[*+-]|\d+\.)/; +block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; +block.item = replace(block.item, 'gm') + (/bull/g, block.bullet) + (); + +block.list = replace(block.list) + (/bull/g, block.bullet) + ('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))') + ('def', '\\n+(?=' + block.def.source + ')') + (); + +block.blockquote = replace(block.blockquote) + ('def', block.def) + (); + +block._tag = '(?!(?:' + + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code' + + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo' + + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b'; + +block.html = replace(block.html) + ('comment', //) + ('closed', /<(tag)[\s\S]+?<\/\1>/) + ('closing', /])*?>/) + (/tag/g, block._tag) + (); + +block.paragraph = replace(block.paragraph) + ('hr', block.hr) + ('heading', block.heading) + ('lheading', block.lheading) + ('blockquote', block.blockquote) + ('tag', '<' + block._tag) + ('def', block.def) + (); + +/** + * Normal Block Grammar + */ + +block.normal = merge({}, block); + +/** + * GFM Block Grammar + */ + +block.gfm = merge({}, block.normal, { + fences: /^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/, + paragraph: /^/ +}); + +block.gfm.paragraph = replace(block.paragraph) + ('(?!', '(?!' + + block.gfm.fences.source.replace('\\1', '\\2') + '|' + + block.list.source.replace('\\1', '\\3') + '|') + (); + +/** + * GFM + Tables Block Grammar + */ + +block.tables = merge({}, block.gfm, { + nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/, + table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/ +}); + +/** + * Block Lexer + */ + +function Lexer(options) { + this.tokens = []; + this.tokens.links = {}; + this.options = options || marked.defaults; + this.rules = block.normal; + + if (this.options.gfm) { + if (this.options.tables) { + this.rules = block.tables; + } else { + this.rules = block.gfm; + } + } +} + +/** + * Expose Block Rules + */ + +Lexer.rules = block; + +/** + * Static Lex Method + */ + +Lexer.lex = function(src, options) { + var lexer = new Lexer(options); + return lexer.lex(src); +}; + +/** + * Preprocessing + */ + +Lexer.prototype.lex = function(src) { + src = src + .replace(/\r\n|\r/g, '\n') + .replace(/\t/g, ' ') + .replace(/\u00a0/g, ' ') + .replace(/\u2424/g, '\n'); + + return this.token(src, true); +}; + +/** + * Lexing + */ + +Lexer.prototype.token = function(src, top, bq) { + var src = src.replace(/^ +$/gm, '') + , next + , loose + , cap + , bull + , b + , item + , space + , i + , l; + + while (src) { + // newline + if (cap = this.rules.newline.exec(src)) { + src = src.substring(cap[0].length); + if (cap[0].length > 1) { + this.tokens.push({ + type: 'space' + }); + } + } + + // code + if (cap = this.rules.code.exec(src)) { + src = src.substring(cap[0].length); + cap = cap[0].replace(/^ {4}/gm, ''); + this.tokens.push({ + type: 'code', + text: !this.options.pedantic + ? cap.replace(/\n+$/, '') + : cap + }); + continue; + } + + // fences (gfm) + if (cap = this.rules.fences.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'code', + lang: cap[2], + text: cap[3] + }); + continue; + } + + // heading + if (cap = this.rules.heading.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'heading', + depth: cap[1].length, + text: cap[2] + }); + continue; + } + + // table no leading pipe (gfm) + if (top && (cap = this.rules.nptable.exec(src))) { + src = src.substring(cap[0].length); + + item = { + type: 'table', + header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + cells: cap[3].replace(/\n$/, '').split('\n') + }; + + for (i = 0; i < item.align.length; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = 'right'; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = 'center'; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = 'left'; + } else { + item.align[i] = null; + } + } + + for (i = 0; i < item.cells.length; i++) { + item.cells[i] = item.cells[i].split(/ *\| */); + } + + this.tokens.push(item); + + continue; + } + + // lheading + if (cap = this.rules.lheading.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'heading', + depth: cap[2] === '=' ? 1 : 2, + text: cap[1] + }); + continue; + } + + // hr + if (cap = this.rules.hr.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'hr' + }); + continue; + } + + // blockquote + if (cap = this.rules.blockquote.exec(src)) { + src = src.substring(cap[0].length); + + this.tokens.push({ + type: 'blockquote_start' + }); + + cap = cap[0].replace(/^ *> ?/gm, ''); + + // Pass `top` to keep the current + // "toplevel" state. This is exactly + // how markdown.pl works. + this.token(cap, top, true); + + this.tokens.push({ + type: 'blockquote_end' + }); + + continue; + } + + // list + if (cap = this.rules.list.exec(src)) { + src = src.substring(cap[0].length); + bull = cap[2]; + + this.tokens.push({ + type: 'list_start', + ordered: bull.length > 1 + }); + + // Get each top-level item. + cap = cap[0].match(this.rules.item); + + next = false; + l = cap.length; + i = 0; + + for (; i < l; i++) { + item = cap[i]; + + // Remove the list item's bullet + // so it is seen as the next token. + space = item.length; + item = item.replace(/^ *([*+-]|\d+\.) +/, ''); + + // Outdent whatever the + // list item contains. Hacky. + if (~item.indexOf('\n ')) { + space -= item.length; + item = !this.options.pedantic + ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') + : item.replace(/^ {1,4}/gm, ''); + } + + // Determine whether the next list item belongs here. + // Backpedal if it does not belong in this list. + if (this.options.smartLists && i !== l - 1) { + b = block.bullet.exec(cap[i + 1])[0]; + if (bull !== b && !(bull.length > 1 && b.length > 1)) { + src = cap.slice(i + 1).join('\n') + src; + i = l - 1; + } + } + + // Determine whether item is loose or not. + // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ + // for discount behavior. + loose = next || /\n\n(?!\s*$)/.test(item); + if (i !== l - 1) { + next = item.charAt(item.length - 1) === '\n'; + if (!loose) loose = next; + } + + this.tokens.push({ + type: loose + ? 'loose_item_start' + : 'list_item_start' + }); + + // Recurse. + this.token(item, false, bq); + + this.tokens.push({ + type: 'list_item_end' + }); + } + + this.tokens.push({ + type: 'list_end' + }); + + continue; + } + + // html + if (cap = this.rules.html.exec(src)) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: this.options.sanitize + ? 'paragraph' + : 'html', + pre: cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style', + text: cap[0] + }); + continue; + } + + // def + if ((!bq && top) && (cap = this.rules.def.exec(src))) { + src = src.substring(cap[0].length); + this.tokens.links[cap[1].toLowerCase()] = { + href: cap[2], + title: cap[3] + }; + continue; + } + + // table (gfm) + if (top && (cap = this.rules.table.exec(src))) { + src = src.substring(cap[0].length); + + item = { + type: 'table', + header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), + align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n') + }; + + for (i = 0; i < item.align.length; i++) { + if (/^ *-+: *$/.test(item.align[i])) { + item.align[i] = 'right'; + } else if (/^ *:-+: *$/.test(item.align[i])) { + item.align[i] = 'center'; + } else if (/^ *:-+ *$/.test(item.align[i])) { + item.align[i] = 'left'; + } else { + item.align[i] = null; + } + } + + for (i = 0; i < item.cells.length; i++) { + item.cells[i] = item.cells[i] + .replace(/^ *\| *| *\| *$/g, '') + .split(/ *\| */); + } + + this.tokens.push(item); + + continue; + } + + // top-level paragraph + if (top && (cap = this.rules.paragraph.exec(src))) { + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'paragraph', + text: cap[1].charAt(cap[1].length - 1) === '\n' + ? cap[1].slice(0, -1) + : cap[1] + }); + continue; + } + + // text + if (cap = this.rules.text.exec(src)) { + // Top-level should never reach here. + src = src.substring(cap[0].length); + this.tokens.push({ + type: 'text', + text: cap[0] + }); + continue; + } + + if (src) { + throw new + Error('Infinite loop on byte: ' + src.charCodeAt(0)); + } + } + + return this.tokens; +}; + +/** + * Inline-Level Grammar + */ + +var inline = { + escape: /^\\([\\`*{}\[\]()#+\-.!_>])/, + autolink: /^<([^ >]+(@|:\/)[^ >]+)>/, + url: noop, + tag: /^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/, + link: /^!?\[(inside)\]\(href\)/, + reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, + nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, + strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, + em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, + code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/, + br: /^ {2,}\n(?!\s*$)/, + del: noop, + text: /^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/; + +inline.link = replace(inline.link) + ('inside', inline._inside) + ('href', inline._href) + (); + +inline.reflink = replace(inline.reflink) + ('inside', inline._inside) + (); + +/** + * Normal Inline Grammar + */ + +inline.normal = merge({}, inline); + +/** + * Pedantic Inline Grammar + */ + +inline.pedantic = merge({}, inline.normal, { + strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, + em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/ +}); + +/** + * GFM Inline Grammar + */ + +inline.gfm = merge({}, inline.normal, { + escape: replace(inline.escape)('])', '~|])')(), + url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/, + del: /^~~(?=\S)([\s\S]*?\S)~~/, + text: replace(inline.text) + (']|', '~]|') + ('|', '|https?://|') + () +}); + +/** + * GFM + Line Breaks Inline Grammar + */ + +inline.breaks = merge({}, inline.gfm, { + br: replace(inline.br)('{2,}', '*')(), + text: replace(inline.gfm.text)('{2,}', '*')() +}); + +/** + * Inline Lexer & Compiler + */ + +function InlineLexer(links, options) { + this.options = options || marked.defaults; + this.links = links; + this.rules = inline.normal; + this.renderer = this.options.renderer || new Renderer; + this.renderer.options = this.options; + + if (!this.links) { + throw new + Error('Tokens array requires a `links` property.'); + } + + if (this.options.gfm) { + if (this.options.breaks) { + this.rules = inline.breaks; + } else { + this.rules = inline.gfm; + } + } else if (this.options.pedantic) { + this.rules = inline.pedantic; + } +} + +/** + * Expose Inline Rules + */ + +InlineLexer.rules = inline; + +/** + * Static Lexing/Compiling Method + */ + +InlineLexer.output = function(src, links, options) { + var inline = new InlineLexer(links, options); + return inline.output(src); +}; + +/** + * Lexing/Compiling + */ + +InlineLexer.prototype.output = function(src) { + var out = '' + , link + , text + , href + , cap; + + while (src) { + // escape + if (cap = this.rules.escape.exec(src)) { + src = src.substring(cap[0].length); + out += cap[1]; + continue; + } + + // autolink + if (cap = this.rules.autolink.exec(src)) { + src = src.substring(cap[0].length); + if (cap[2] === '@') { + text = cap[1].charAt(6) === ':' + ? this.mangle(cap[1].substring(7)) + : this.mangle(cap[1]); + href = this.mangle('mailto:') + text; + } else { + text = escape(cap[1]); + href = text; + } + out += this.renderer.link(href, null, text); + continue; + } + + // url (gfm) + if (!this.inLink && (cap = this.rules.url.exec(src))) { + src = src.substring(cap[0].length); + text = escape(cap[1]); + href = text; + out += this.renderer.link(href, null, text); + continue; + } + + // tag + if (cap = this.rules.tag.exec(src)) { + if (!this.inLink && /^/i.test(cap[0])) { + this.inLink = false; + } + src = src.substring(cap[0].length); + out += this.options.sanitize + ? escape(cap[0]) + : cap[0]; + continue; + } + + // link + if (cap = this.rules.link.exec(src)) { + src = src.substring(cap[0].length); + this.inLink = true; + out += this.outputLink(cap, { + href: cap[2], + title: cap[3] + }); + this.inLink = false; + continue; + } + + // reflink, nolink + if ((cap = this.rules.reflink.exec(src)) + || (cap = this.rules.nolink.exec(src))) { + src = src.substring(cap[0].length); + link = (cap[2] || cap[1]).replace(/\s+/g, ' '); + link = this.links[link.toLowerCase()]; + if (!link || !link.href) { + out += cap[0].charAt(0); + src = cap[0].substring(1) + src; + continue; + } + this.inLink = true; + out += this.outputLink(cap, link); + this.inLink = false; + continue; + } + + // strong + if (cap = this.rules.strong.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.strong(this.output(cap[2] || cap[1])); + continue; + } + + // em + if (cap = this.rules.em.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.em(this.output(cap[2] || cap[1])); + continue; + } + + // code + if (cap = this.rules.code.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.codespan(escape(cap[2], true)); + continue; + } + + // br + if (cap = this.rules.br.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.br(); + continue; + } + + // del (gfm) + if (cap = this.rules.del.exec(src)) { + src = src.substring(cap[0].length); + out += this.renderer.del(this.output(cap[1])); + continue; + } + + // text + if (cap = this.rules.text.exec(src)) { + src = src.substring(cap[0].length); + out += escape(this.smartypants(cap[0])); + continue; + } + + if (src) { + throw new + Error('Infinite loop on byte: ' + src.charCodeAt(0)); + } + } + + return out; +}; + +/** + * Compile Link + */ + +InlineLexer.prototype.outputLink = function(cap, link) { + var href = escape(link.href) + , title = link.title ? escape(link.title) : null; + + return cap[0].charAt(0) !== '!' + ? this.renderer.link(href, title, this.output(cap[1])) + : this.renderer.image(href, title, escape(cap[1])); +}; + +/** + * Smartypants Transformations + */ + +InlineLexer.prototype.smartypants = function(text) { + if (!this.options.smartypants) return text; + return text + // em-dashes + .replace(/--/g, '\u2014') + // opening singles + .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018') + // closing singles & apostrophes + .replace(/'/g, '\u2019') + // opening doubles + .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c') + // closing doubles + .replace(/"/g, '\u201d') + // ellipses + .replace(/\.{3}/g, '\u2026'); +}; + +/** + * Mangle Links + */ + +InlineLexer.prototype.mangle = function(text) { + var out = '' + , l = text.length + , i = 0 + , ch; + + for (; i < l; i++) { + ch = text.charCodeAt(i); + if (Math.random() > 0.5) { + ch = 'x' + ch.toString(16); + } + out += '&#' + ch + ';'; + } + + return out; +}; + +/** + * Renderer + */ + +function Renderer(options) { + this.options = options || {}; +} + +Renderer.prototype.code = function(code, lang, escaped) { + if (this.options.highlight) { + var out = this.options.highlight(code, lang); + if (out != null && out !== code) { + escaped = true; + code = out; + } + } + + if (!lang) { + return '
'
+      + (escaped ? code : escape(code, true))
+      + '\n
'; + } + + return '
'
+    + (escaped ? code : escape(code, true))
+    + '\n
\n'; +}; + +Renderer.prototype.blockquote = function(quote) { + return '
\n' + quote + '
\n'; +}; + +Renderer.prototype.html = function(html) { + return html; +}; + +Renderer.prototype.heading = function(text, level, raw) { + return '' + + text + + '\n'; +}; + +Renderer.prototype.hr = function() { + return this.options.xhtml ? '
\n' : '
\n'; +}; + +Renderer.prototype.list = function(body, ordered) { + var type = ordered ? 'ol' : 'ul'; + return '<' + type + '>\n' + body + '\n'; +}; + +Renderer.prototype.listitem = function(text) { + return '
  • ' + text + '
  • \n'; +}; + +Renderer.prototype.paragraph = function(text) { + return '

    ' + text + '

    \n'; +}; + +Renderer.prototype.table = function(header, body) { + return '\n' + + '\n' + + header + + '\n' + + '\n' + + body + + '\n' + + '
    \n'; +}; + +Renderer.prototype.tablerow = function(content) { + return '\n' + content + '\n'; +}; + +Renderer.prototype.tablecell = function(content, flags) { + var type = flags.header ? 'th' : 'td'; + var tag = flags.align + ? '<' + type + ' style="text-align:' + flags.align + '">' + : '<' + type + '>'; + return tag + content + '\n'; +}; + +// span level renderer +Renderer.prototype.strong = function(text) { + return '' + text + ''; +}; + +Renderer.prototype.em = function(text) { + return '' + text + ''; +}; + +Renderer.prototype.codespan = function(text) { + return '' + text + ''; +}; + +Renderer.prototype.br = function() { + return this.options.xhtml ? '
    ' : '
    '; +}; + +Renderer.prototype.del = function(text) { + return '' + text + ''; +}; + +Renderer.prototype.link = function(href, title, text) { + if (this.options.sanitize) { + try { + var prot = decodeURIComponent(unescape(href)) + .replace(/[^\w:]/g, '') + .toLowerCase(); + } catch (e) { + return ''; + } + if (prot.indexOf('javascript:') === 0) { + return ''; + } + } + var out = '
    '; + return out; +}; + +Renderer.prototype.image = function(href, title, text) { + var out = '' + text + '' : '>'; + return out; +}; + +/** + * Parsing & Compiling + */ + +function Parser(options) { + this.tokens = []; + this.token = null; + this.options = options || marked.defaults; + this.options.renderer = this.options.renderer || new Renderer; + this.renderer = this.options.renderer; + this.renderer.options = this.options; +} + +/** + * Static Parse Method + */ + +Parser.parse = function(src, options, renderer) { + var parser = new Parser(options, renderer); + return parser.parse(src); +}; + +/** + * Parse Loop + */ + +Parser.prototype.parse = function(src) { + this.inline = new InlineLexer(src.links, this.options, this.renderer); + this.tokens = src.reverse(); + + var out = ''; + while (this.next()) { + out += this.tok(); + } + + return out; +}; + +/** + * Next Token + */ + +Parser.prototype.next = function() { + return this.token = this.tokens.pop(); +}; + +/** + * Preview Next Token + */ + +Parser.prototype.peek = function() { + return this.tokens[this.tokens.length - 1] || 0; +}; + +/** + * Parse Text Tokens + */ + +Parser.prototype.parseText = function() { + var body = this.token.text; + + while (this.peek().type === 'text') { + body += '\n' + this.next().text; + } + + return this.inline.output(body); +}; + +/** + * Parse Current Token + */ + +Parser.prototype.tok = function() { + switch (this.token.type) { + case 'space': { + return ''; + } + case 'hr': { + return this.renderer.hr(); + } + case 'heading': { + return this.renderer.heading( + this.inline.output(this.token.text), + this.token.depth, + this.token.text); + } + case 'code': { + return this.renderer.code(this.token.text, + this.token.lang, + this.token.escaped); + } + case 'table': { + var header = '' + , body = '' + , i + , row + , cell + , flags + , j; + + // header + cell = ''; + for (i = 0; i < this.token.header.length; i++) { + flags = { header: true, align: this.token.align[i] }; + cell += this.renderer.tablecell( + this.inline.output(this.token.header[i]), + { header: true, align: this.token.align[i] } + ); + } + header += this.renderer.tablerow(cell); + + for (i = 0; i < this.token.cells.length; i++) { + row = this.token.cells[i]; + + cell = ''; + for (j = 0; j < row.length; j++) { + cell += this.renderer.tablecell( + this.inline.output(row[j]), + { header: false, align: this.token.align[j] } + ); + } + + body += this.renderer.tablerow(cell); + } + return this.renderer.table(header, body); + } + case 'blockquote_start': { + var body = ''; + + while (this.next().type !== 'blockquote_end') { + body += this.tok(); + } + + return this.renderer.blockquote(body); + } + case 'list_start': { + var body = '' + , ordered = this.token.ordered; + + while (this.next().type !== 'list_end') { + body += this.tok(); + } + + return this.renderer.list(body, ordered); + } + case 'list_item_start': { + var body = ''; + + while (this.next().type !== 'list_item_end') { + body += this.token.type === 'text' + ? this.parseText() + : this.tok(); + } + + return this.renderer.listitem(body); + } + case 'loose_item_start': { + var body = ''; + + while (this.next().type !== 'list_item_end') { + body += this.tok(); + } + + return this.renderer.listitem(body); + } + case 'html': { + var html = !this.token.pre && !this.options.pedantic + ? this.inline.output(this.token.text) + : this.token.text; + return this.renderer.html(html); + } + case 'paragraph': { + return this.renderer.paragraph(this.inline.output(this.token.text)); + } + case 'text': { + return this.renderer.paragraph(this.parseText()); + } + } +}; + +/** + * Helpers + */ + +function escape(html, encode) { + return html + .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function unescape(html) { + return html.replace(/&([#\w]+);/g, function(_, n) { + n = n.toLowerCase(); + if (n === 'colon') return ':'; + if (n.charAt(0) === '#') { + return n.charAt(1) === 'x' + ? String.fromCharCode(parseInt(n.substring(2), 16)) + : String.fromCharCode(+n.substring(1)); + } + return ''; + }); +} + +function replace(regex, opt) { + regex = regex.source; + opt = opt || ''; + return function self(name, val) { + if (!name) return new RegExp(regex, opt); + val = val.source || val; + val = val.replace(/(^|[^\[])\^/g, '$1'); + regex = regex.replace(name, val); + return self; + }; +} + +function noop() {} +noop.exec = noop; + +function merge(obj) { + var i = 1 + , target + , key; + + for (; i < arguments.length; i++) { + target = arguments[i]; + for (key in target) { + if (Object.prototype.hasOwnProperty.call(target, key)) { + obj[key] = target[key]; + } + } + } + + return obj; +} + + +/** + * Marked + */ + +function marked(src, opt, callback) { + if (callback || typeof opt === 'function') { + if (!callback) { + callback = opt; + opt = null; + } + + opt = merge({}, marked.defaults, opt || {}); + + var highlight = opt.highlight + , tokens + , pending + , i = 0; + + try { + tokens = Lexer.lex(src, opt) + } catch (e) { + return callback(e); + } + + pending = tokens.length; + + var done = function() { + var out, err; + + try { + out = Parser.parse(tokens, opt); + } catch (e) { + err = e; + } + + opt.highlight = highlight; + + return err + ? callback(err) + : callback(null, out); + }; + + if (!highlight || highlight.length < 3) { + return done(); + } + + delete opt.highlight; + + if (!pending) return done(); + + for (; i < tokens.length; i++) { + (function(token) { + if (token.type !== 'code') { + return --pending || done(); + } + return highlight(token.text, token.lang, function(err, code) { + if (code == null || code === token.text) { + return --pending || done(); + } + token.text = code; + token.escaped = true; + --pending || done(); + }); + })(tokens[i]); + } + + return; + } + try { + if (opt) opt = merge({}, marked.defaults, opt); + return Parser.parse(Lexer.lex(src, opt), opt); + } catch (e) { + e.message += '\nPlease report this to https://github.com/chjj/marked.'; + if ((opt || marked.defaults).silent) { + return '

    An error occured:

    '
    +        + escape(e.message + '', true)
    +        + '
    '; + } + throw e; + } +} + +/** + * Options + */ + +marked.options = +marked.setOptions = function(opt) { + merge(marked.defaults, opt); + return marked; +}; + +marked.defaults = { + gfm: true, + tables: true, + breaks: false, + pedantic: false, + sanitize: false, + smartLists: false, + silent: false, + highlight: null, + langPrefix: 'lang-', + smartypants: false, + headerPrefix: '', + renderer: new Renderer, + xhtml: false +}; + +/** + * Expose + */ + +marked.Parser = Parser; +marked.parser = Parser.parse; + +marked.Renderer = Renderer; + +marked.Lexer = Lexer; +marked.lexer = Lexer.lex; + +marked.InlineLexer = InlineLexer; +marked.inlineLexer = InlineLexer.output; + +marked.parse = marked; + +if (typeof exports === 'object') { + module.exports = marked; +} else if (typeof define === 'function' && define.amd) { + define(function() { return marked; }); +} else { + this.marked = marked; +} + +}).call(function() { + return this || (typeof window !== 'undefined' ? window : global); +}()); diff --git a/third_party/jsdoc/node_modules/marked/package.json b/third_party/jsdoc/node_modules/marked/package.json new file mode 100644 index 0000000000..bbea53418a --- /dev/null +++ b/third_party/jsdoc/node_modules/marked/package.json @@ -0,0 +1,50 @@ +{ + "name": "marked", + "description": "A markdown parser built for speed", + "author": { + "name": "Christopher Jeffrey" + }, + "version": "0.3.2", + "main": "./lib/marked.js", + "bin": { + "marked": "./bin/marked" + }, + "man": [ + "./man/marked.1" + ], + "preferGlobal": true, + "repository": { + "type": "git", + "url": "git://github.com/chjj/marked.git" + }, + "homepage": "https://github.com/chjj/marked", + "bugs": { + "url": "http://github.com/chjj/marked/issues" + }, + "license": "MIT", + "keywords": [ + "markdown", + "markup", + "html" + ], + "tags": [ + "markdown", + "markup", + "html" + ], + "devDependencies": { + "markdown": "*", + "showdown": "*", + "robotskirt": "*" + }, + "scripts": { + "test": "node test", + "bench": "node test --bench" + }, + "readme": "# marked\n\n> A full-featured markdown parser and compiler, written in JavaScript. Built\n> for speed.\n\n[![NPM version](https://badge.fury.io/js/marked.png)][badge]\n\n## Install\n\n``` bash\nnpm install marked --save\n```\n\n## Usage\n\nMinimal usage:\n\n```js\nvar marked = require('marked');\nconsole.log(marked('I am using __markdown__.'));\n// Outputs:

    I am using markdown.

    \n```\n\nExample setting options with default values:\n\n```js\nvar marked = require('marked');\nmarked.setOptions({\n renderer: new marked.Renderer(),\n gfm: true,\n tables: true,\n breaks: false,\n pedantic: false,\n sanitize: true,\n smartLists: true,\n smartypants: false\n});\n\nconsole.log(marked('I am using __markdown__.'));\n```\n\n## marked(markdownString [,options] [,callback])\n\n### markdownString\n\nType: `string`\n\nString of markdown source to be compiled.\n\n### options\n\nType: `object`\n\nHash of options. Can also be set using the `marked.setOptions` method as seen\nabove.\n\n### callback\n\nType: `function`\n\nFunction called when the `markdownString` has been fully parsed when using\nasync highlighting. If the `options` argument is omitted, this can be used as\nthe second argument.\n\n## Options\n\n### highlight\n\nType: `function`\n\nA function to highlight code blocks. The first example below uses async highlighting with\n[node-pygmentize-bundled][pygmentize], and the second is a synchronous example using\n[highlight.js][highlight]:\n\n```js\nvar marked = require('marked');\n\nvar markdownString = '```js\\n console.log(\"hello\"); \\n```';\n\n// Async highlighting with pygmentize-bundled\nmarked.setOptions({\n highlight: function (code, lang, callback) {\n require('pygmentize-bundled')({ lang: lang, format: 'html' }, code, function (err, result) {\n callback(err, result.toString());\n });\n }\n});\n\n// Using async version of marked\nmarked(markdownString, function (err, content) {\n if (err) throw err;\n console.log(content);\n});\n\n// Synchronous highlighting with highlight.js\nmarked.setOptions({\n highlight: function (code) {\n return require('highlight.js').highlightAuto(code).value;\n }\n});\n\nconsole.log(marked(markdownString));\n```\n\n#### highlight arguments\n\n`code`\n\nType: `string`\n\nThe section of code to pass to the highlighter.\n\n`lang`\n\nType: `string`\n\nThe programming language specified in the code block.\n\n`callback`\n\nType: `function`\n\nThe callback function to call when using an async highlighter.\n\n### renderer\n\nType: `object`\nDefault: `new Renderer()`\n\nAn object containing functions to render tokens to HTML.\n\n#### Overriding renderer methods\n\nThe renderer option allows you to render tokens in a custom manor. Here is an\nexample of overriding the default heading token rendering by adding an embedded anchor tag like on GitHub:\n\n```javascript\nvar marked = require('marked');\nvar renderer = new marked.Renderer();\n\nrenderer.heading = function (text, level) {\n var escapedText = text.toLowerCase().replace(/[^\\w]+/g, '-');\n\n return '
    ' +\n text + '';\n},\n\nconsole.log(marked('# heading+', { renderer: renderer }));\n```\nThis code will output the following HTML:\n```html\n

    \n \n \n \n heading+\n

    \n```\n\n#### Block level renderer methods\n\n- code(*string* code, *string* language)\n- blockquote(*string* quote)\n- html(*string* html)\n- heading(*string* text, *number* level)\n- hr()\n- list(*string* body, *boolean* ordered)\n- listitem(*string* text)\n- paragraph(*string* text)\n- table(*string* header, *string* body)\n- tablerow(*string* content)\n- tablecell(*string* content, *object* flags)\n\n`flags` has the following properties:\n\n```js\n{\n header: true || false,\n align: 'center' || 'left' || 'right'\n}\n```\n\n#### Inline level renderer methods\n\n- strong(*string* text)\n- em(*string* text)\n- codespan(*string* code)\n- br()\n- del(*string* text)\n- link(*string* href, *string* title, *string* text)\n- image(*string* href, *string* title, *string* text)\n\n### gfm\n\nType: `boolean`\nDefault: `true`\n\nEnable [GitHub flavored markdown][gfm].\n\n### tables\n\nType: `boolean`\nDefault: `true`\n\nEnable GFM [tables][tables].\nThis option requires the `gfm` option to be true.\n\n### breaks\n\nType: `boolean`\nDefault: `false`\n\nEnable GFM [line breaks][breaks].\nThis option requires the `gfm` option to be true.\n\n### pedantic\n\nType: `boolean`\nDefault: `false`\n\nConform to obscure parts of `markdown.pl` as much as possible. Don't fix any of\nthe original markdown bugs or poor behavior.\n\n### sanitize\n\nType: `boolean`\nDefault: `false`\n\nSanitize the output. Ignore any HTML that has been input.\n\n### smartLists\n\nType: `boolean`\nDefault: `true`\n\nUse smarter list behavior than the original markdown. May eventually be\ndefault with the old behavior moved into `pedantic`.\n\n### smartypants\n\nType: `boolean`\nDefault: `false`\n\nUse \"smart\" typograhic punctuation for things like quotes and dashes.\n\n## Access to lexer and parser\n\nYou also have direct access to the lexer and parser if you so desire.\n\n``` js\nvar tokens = marked.lexer(text, options);\nconsole.log(marked.parser(tokens));\n```\n\n``` js\nvar lexer = new marked.Lexer(options);\nvar tokens = lexer.lex(text);\nconsole.log(tokens);\nconsole.log(lexer.rules);\n```\n\n## CLI\n\n``` bash\n$ marked -o hello.html\nhello world\n^D\n$ cat hello.html\n

    hello world

    \n```\n\n## Philosophy behind marked\n\nThe point of marked was to create a markdown compiler where it was possible to\nfrequently parse huge chunks of markdown without having to worry about\ncaching the compiled output somehow...or blocking for an unnecesarily long time.\n\nmarked is very concise and still implements all markdown features. It is also\nnow fully compatible with the client-side.\n\nmarked more or less passes the official markdown test suite in its\nentirety. This is important because a surprising number of markdown compilers\ncannot pass more than a few tests. It was very difficult to get marked as\ncompliant as it is. It could have cut corners in several areas for the sake\nof performance, but did not in order to be exactly what you expect in terms\nof a markdown rendering. In fact, this is why marked could be considered at a\ndisadvantage in the benchmarks above.\n\nAlong with implementing every markdown feature, marked also implements [GFM\nfeatures][gfmf].\n\n## Benchmarks\n\nnode v0.8.x\n\n``` bash\n$ node test --bench\nmarked completed in 3411ms.\nmarked (gfm) completed in 3727ms.\nmarked (pedantic) completed in 3201ms.\nrobotskirt completed in 808ms.\nshowdown (reuse converter) completed in 11954ms.\nshowdown (new converter) completed in 17774ms.\nmarkdown-js completed in 17191ms.\n```\n\n__Marked is now faster than Discount, which is written in C.__\n\nFor those feeling skeptical: These benchmarks run the entire markdown test suite 1000 times. The test suite tests every feature. It doesn't cater to specific aspects.\n\n### Pro level\n\nYou also have direct access to the lexer and parser if you so desire.\n\n``` js\nvar tokens = marked.lexer(text, options);\nconsole.log(marked.parser(tokens));\n```\n\n``` js\nvar lexer = new marked.Lexer(options);\nvar tokens = lexer.lex(text);\nconsole.log(tokens);\nconsole.log(lexer.rules);\n```\n\n``` bash\n$ node\n> require('marked').lexer('> i am using marked.')\n[ { type: 'blockquote_start' },\n { type: 'paragraph',\n text: 'i am using marked.' },\n { type: 'blockquote_end' },\n links: {} ]\n```\n\n## Running Tests & Contributing\n\nIf you want to submit a pull request, make sure your changes pass the test\nsuite. If you're adding a new feature, be sure to add your own test.\n\nThe marked test suite is set up slightly strangely: `test/new` is for all tests\nthat are not part of the original markdown.pl test suite (this is where your\ntest should go if you make one). `test/original` is only for the original\nmarkdown.pl tests. `test/tests` houses both types of tests after they have been\ncombined and moved/generated by running `node test --fix` or `marked --test\n--fix`.\n\nIn other words, if you have a test to add, add it to `test/new/` and then\nregenerate the tests with `node test --fix`. Commit the result. If your test\nuses a certain feature, for example, maybe it assumes GFM is *not* enabled, you\ncan add `.nogfm` to the filename. So, `my-test.text` becomes\n`my-test.nogfm.text`. You can do this with any marked option. Say you want\nline breaks and smartypants enabled, your filename should be:\n`my-test.breaks.smartypants.text`.\n\nTo run the tests:\n\n``` bash\ncd marked/\nnode test\n```\n\n### Contribution and License Agreement\n\nIf you contribute code to this project, you are implicitly allowing your code\nto be distributed under the MIT license. You are also implicitly verifying that\nall code is your original work. ``\n\n## License\n\nCopyright (c) 2011-2014, Christopher Jeffrey. (MIT License)\n\nSee LICENSE for more info.\n\n[gfm]: https://help.github.com/articles/github-flavored-markdown\n[gfmf]: http://github.github.com/github-flavored-markdown/\n[pygmentize]: https://github.com/rvagg/node-pygmentize-bundled\n[highlight]: https://github.com/isagalaev/highlight.js\n[badge]: http://badge.fury.io/js/marked\n[tables]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#wiki-tables\n[breaks]: https://help.github.com/articles/github-flavored-markdown#newlines\n", + "readmeFilename": "README.md", + "_id": "marked@0.3.2", + "_shasum": "015db158864438f24a64bdd61a0428b418706d09", + "_from": "marked@0.3.2", + "_resolved": "https://registry.npmjs.org/marked/-/marked-0.3.2.tgz" +} diff --git a/third_party/jsdoc/node_modules/strip-json-comments/package.json b/third_party/jsdoc/node_modules/strip-json-comments/package.json new file mode 100644 index 0000000000..c55dc5f2a1 --- /dev/null +++ b/third_party/jsdoc/node_modules/strip-json-comments/package.json @@ -0,0 +1,61 @@ +{ + "name": "strip-json-comments", + "version": "0.1.3", + "description": "Strip comments from JSON. Lets you use comments in your JSON files!", + "keywords": [ + "json", + "strip", + "remove", + "delete", + "trim", + "comments", + "multiline", + "parse", + "config", + "configuration", + "conf", + "settings", + "util", + "env", + "environment", + "cli", + "bin" + ], + "license": "MIT", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "http://sindresorhus.com" + }, + "files": [ + "cli.js", + "strip-json-comments.js" + ], + "main": "strip-json-comments", + "bin": { + "strip-json-comments": "cli.js" + }, + "repository": { + "type": "git", + "url": "git://github.com/sindresorhus/strip-json-comments" + }, + "scripts": { + "test": "mocha" + }, + "devDependencies": { + "mocha": "*" + }, + "engines": { + "node": ">=0.8.0" + }, + "readme": "# strip-json-comments [![Build Status](https://travis-ci.org/sindresorhus/strip-json-comments.svg?branch=master)](https://travis-ci.org/sindresorhus/strip-json-comments)\n\n> Strip comments from JSON. Lets you use comments in your JSON files!\n\nThis is now possible:\n\n```js\n{\n\t// rainbows\n\t\"unicorn\": /* ❤ */ \"cake\"\n}\n```\n\nIt will remove single-line comments `//` and mult-line comments `/**/`.\n\nAlso available as a [gulp](https://github.com/sindresorhus/gulp-strip-json-comments)/[grunt](https://github.com/sindresorhus/grunt-strip-json-comments)/[broccoli](https://github.com/sindresorhus/broccoli-strip-json-comments) plugin and a [require hook](https://github.com/uTest/autostrip-json-comments).\n\n\n*There's already [json-comments](https://npmjs.org/package/json-comments), but it's only for Node.js and uses a naive regex to strip comments which fails on simple cases like `{\"a\":\"//\"}`. This module however parses out the comments.*\n\n\n## Install\n\n```sh\n$ npm install --save strip-json-comments\n```\n\n```sh\n$ bower install --save strip-json-comments\n```\n\n```sh\n$ component install sindresorhus/strip-json-comments\n```\n\n\n## Usage\n\n```js\nvar json = '{/*rainbows*/\"unicorn\":\"cake\"}';\nJSON.parse(stripJsonComments(json));\n//=> {unicorn: 'cake'}\n```\n\n\n## API\n\n### stripJsonComments(input)\n\n#### input\n\nType: `string`\n\nAccepts a string with JSON and returns a string without comments.\n\n\n## CLI\n\n```sh\n$ npm install --global strip-json-comments\n```\n\n```sh\n$ strip-json-comments --help\n\nstrip-json-comments > \n# or\ncat | strip-json-comments > \n```\n\n\n## License\n\nMIT © [Sindre Sorhus](http://sindresorhus.com)\n", + "readmeFilename": "readme.md", + "bugs": { + "url": "https://github.com/sindresorhus/strip-json-comments/issues" + }, + "homepage": "https://github.com/sindresorhus/strip-json-comments", + "_id": "strip-json-comments@0.1.3", + "_shasum": "164c64e370a8a3cc00c9e01b539e569823f0ee54", + "_from": "strip-json-comments@0.1.3", + "_resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-0.1.3.tgz" +} diff --git a/third_party/jsdoc/node_modules/strip-json-comments/strip-json-comments.js b/third_party/jsdoc/node_modules/strip-json-comments/strip-json-comments.js new file mode 100644 index 0000000000..2e7fdef27f --- /dev/null +++ b/third_party/jsdoc/node_modules/strip-json-comments/strip-json-comments.js @@ -0,0 +1,64 @@ +/*! + strip-json-comments + Strip comments from JSON. Lets you use comments in your JSON files! + https://github.com/sindresorhus/strip-json-comments + by Sindre Sorhus + MIT License +*/ +(function () { + 'use strict'; + + function stripJsonComments(str) { + var currentChar; + var nextChar; + var insideString = false; + var insideComment = false; + var ret = ''; + + for (var i = 0; i < str.length; i++) { + currentChar = str[i]; + nextChar = str[i + 1]; + + if (!insideComment && str[i - 1] !== '\\' && currentChar === '"') { + insideString = !insideString; + } + + if (insideString) { + ret += currentChar; + continue; + } + + if (!insideComment && currentChar + nextChar === '//') { + insideComment = 'single'; + i++; + } else if (insideComment === 'single' && currentChar + nextChar === '\r\n') { + insideComment = false; + i++; + } else if (insideComment === 'single' && currentChar === '\n') { + insideComment = false; + } else if (!insideComment && currentChar + nextChar === '/*') { + insideComment = 'multi'; + i++; + continue; + } else if (insideComment === 'multi' && currentChar + nextChar === '*/') { + insideComment = false; + i++; + continue; + } + + if (insideComment) { + continue; + } + + ret += currentChar; + } + + return ret; + } + + if (typeof module !== 'undefined' && module.exports) { + module.exports = stripJsonComments; + } else { + window.stripJsonComments = stripJsonComments; + } +})(); diff --git a/third_party/jsdoc/node_modules/taffydb/README.md b/third_party/jsdoc/node_modules/taffydb/README.md new file mode 100644 index 0000000000..52d14a3ae4 --- /dev/null +++ b/third_party/jsdoc/node_modules/taffydb/README.md @@ -0,0 +1 @@ +See [http://taffydb.com](http://taffydb.com). \ No newline at end of file diff --git a/third_party/jsdoc/node_modules/taffydb/package.json b/third_party/jsdoc/node_modules/taffydb/package.json new file mode 100644 index 0000000000..1165f2013c --- /dev/null +++ b/third_party/jsdoc/node_modules/taffydb/package.json @@ -0,0 +1,21 @@ +{ + "name": "taffydb", + "version": "2.6.2", + "description": "An open-source library that brings database features into your JavaScript applications.", + "main": "taffy.js", + "repository": { + "type": "git", + "url": "git://github.com/hegemonic/taffydb.git" + }, + "license": "BSD", + "readme": "See [http://taffydb.com](http://taffydb.com).", + "readmeFilename": "README.md", + "bugs": { + "url": "https://github.com/hegemonic/taffydb/issues" + }, + "homepage": "https://github.com/hegemonic/taffydb", + "_id": "taffydb@2.6.2", + "_shasum": "3c549d2f5712d7d1d109ad6bb1a4084f1b7add4e", + "_from": "https://github.com/hegemonic/taffydb/tarball/master", + "_resolved": "https://github.com/hegemonic/taffydb/tarball/master" +} diff --git a/third_party/jsdoc/node_modules/taffydb/taffy-test.html b/third_party/jsdoc/node_modules/taffydb/taffy-test.html new file mode 100644 index 0000000000..c4df78b489 --- /dev/null +++ b/third_party/jsdoc/node_modules/taffydb/taffy-test.html @@ -0,0 +1,84 @@ + + + + + taffy test + + + + + + +
    +Please open your javascript console to see test results +
    + + + + diff --git a/third_party/jsdoc/node_modules/taffydb/taffy.js b/third_party/jsdoc/node_modules/taffydb/taffy.js new file mode 100644 index 0000000000..b7ad88cdf9 --- /dev/null +++ b/third_party/jsdoc/node_modules/taffydb/taffy.js @@ -0,0 +1,1973 @@ +/* + + Software License Agreement (BSD License) + http://taffydb.com + Copyright (c) + All rights reserved. + + + Redistribution and use of this software in source and binary forms, with or without modification, are permitted provided that the following condition is met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +/*jslint browser : true, continue : true, + devel : true, indent : 2, maxerr : 500, + newcap : true, nomen : true, plusplus : true, + regexp : true, sloppy : true, vars : false, + white : true +*/ + +// BUILD 193d48d, modified by mmikowski to pass jslint + +// Setup TAFFY name space to return an object with methods +var TAFFY, exports, T; +(function () { + 'use strict'; + var + typeList, makeTest, idx, typeKey, + version, TC, idpad, cmax, + API, protectJSON, each, eachin, + isIndexable, returnFilter, runFilters, + numcharsplit, orderByCol, run + ; + + + if ( ! TAFFY ){ + // TC = Counter for Taffy DBs on page, used for unique IDs + // cmax = size of charnumarray conversion cache + // idpad = zeros to pad record IDs with + version = '2.6.2'; // proposed mmikowski 2012-08-06 + TC = 1; + idpad = '000000'; + cmax = 1000; + API = {}; + + protectJSON = function ( t ) { + // **************************************** + // * + // * Takes: a variable + // * Returns: the variable if object/array or the parsed variable if JSON + // * + // **************************************** + if ( TAFFY.isArray( t ) || TAFFY.isObject( t ) ){ + return t; + } + else { + return JSON.parse( t ); + } + }; + + each = function ( a, fun, u ) { + var r, i, x, y; + // **************************************** + // * + // * Takes: + // * a = an object/value or an array of objects/values + // * f = a function + // * u = optional flag to describe how to handle undefined values + // in array of values. True: pass them to the functions, + // False: skip. Default False; + // * Purpose: Used to loop over arrays + // * + // **************************************** + if ( a && ((T.isArray( a ) && a.length === 1) || (!T.isArray( a ))) ){ + fun( (T.isArray( a )) ? a[0] : a, 0 ); + } + else { + for ( r, i, x = 0, a = (T.isArray( a )) ? a : [a], y = a.length; + x < y; x++ ) + { + i = a[x]; + if ( !T.isUndefined( i ) || (u || false) ){ + r = fun( i, x ); + if ( r === T.EXIT ){ + break; + } + + } + } + } + }; + + eachin = function ( o, fun ) { + // **************************************** + // * + // * Takes: + // * o = an object + // * f = a function + // * Purpose: Used to loop over objects + // * + // **************************************** + var x = 0, r, i; + + for ( i in o ){ + if ( o.hasOwnProperty( i ) ){ + r = fun( o[i], i, x++ ); + if ( r === T.EXIT ){ + break; + } + } + } + + }; + + API.extend = function ( m, f ) { + // **************************************** + // * + // * Takes: method name, function + // * Purpose: Add a custom method to the API + // * + // **************************************** + API[m] = function () { + return f.apply( this, arguments ); + }; + }; + + isIndexable = function ( f ) { + var i; + // Check to see if record ID + if ( T.isString( f ) && /[t][0-9]*[r][0-9]*/i.test( f ) ){ + return true; + } + // Check to see if record + if ( T.isObject( f ) && f.___id && f.___s ){ + return true; + } + + // Check to see if array of indexes + if ( T.isArray( f ) ){ + i = true; + each( f, function ( r ) { + if ( !isIndexable( r ) ){ + i = false; + + return TAFFY.EXIT; + } + }); + return i; + } + + return false; + }; + + runFilters = function ( r, filter ) { + // **************************************** + // * + // * Takes: takes a record and a collection of filters + // * Returns: true if the record matches, false otherwise + // **************************************** + var match = true; + + + each( filter, function ( mf ) { + switch ( T.typeOf( mf ) ){ + case 'function': + // run function + if ( !mf.apply( r ) ){ + match = false; + return TAFFY.EXIT; + } + break; + case 'array': + // loop array and treat like a SQL or + match = (mf.length === 1) ? (runFilters( r, mf[0] )) : + (mf.length === 2) ? (runFilters( r, mf[0] ) || + runFilters( r, mf[1] )) : + (mf.length === 3) ? (runFilters( r, mf[0] ) || + runFilters( r, mf[1] ) || runFilters( r, mf[2] )) : + (mf.length === 4) ? (runFilters( r, mf[0] ) || + runFilters( r, mf[1] ) || runFilters( r, mf[2] ) || + runFilters( r, mf[3] )) : false; + if ( mf.length > 4 ){ + each( mf, function ( f ) { + if ( runFilters( r, f ) ){ + match = true; + } + }); + } + break; + } + }); + + return match; + }; + + returnFilter = function ( f ) { + // **************************************** + // * + // * Takes: filter object + // * Returns: a filter function + // * Purpose: Take a filter object and return a function that can be used to compare + // * a TaffyDB record to see if the record matches a query + // **************************************** + var nf = []; + if ( T.isString( f ) && /[t][0-9]*[r][0-9]*/i.test( f ) ){ + f = { ___id : f }; + } + if ( T.isArray( f ) ){ + // if we are working with an array + + each( f, function ( r ) { + // loop the array and return a filter func for each value + nf.push( returnFilter( r ) ); + }); + // now build a func to loop over the filters and return true if ANY of the filters match + // This handles logical OR expressions + f = function () { + var that = this, match = false; + each( nf, function ( f ) { + if ( runFilters( that, f ) ){ + match = true; + } + }); + return match; + }; + return f; + + } + // if we are dealing with an Object + if ( T.isObject( f ) ){ + if ( T.isObject( f ) && f.___id && f.___s ){ + f = { ___id : f.___id }; + } + + // Loop over each value on the object to prep match type and match value + eachin( f, function ( v, i ) { + + // default match type to IS/Equals + if ( !T.isObject( v ) ){ + v = { + 'is' : v + }; + } + // loop over each value on the value object - if any + eachin( v, function ( mtest, s ) { + // s = match type, e.g. is, hasAll, like, etc + var c = [], looper; + + // function to loop and apply filter + looper = (s === 'hasAll') ? + function ( mtest, func ) { + func( mtest ); + } : each; + + // loop over each test + looper( mtest, function ( mtest ) { + + // su = match success + // f = match false + var su = true, f = false, matchFunc; + + + // push a function onto the filter collection to do the matching + matchFunc = function () { + + // get the value from the record + var + mvalue = this[i], + eqeq = '==', + bangeq = '!=', + eqeqeq = '===', + lt = '<', + gt = '>', + lteq = '<=', + gteq = '>=', + bangeqeq = '!==', + r + ; + + + if ( (s.indexOf( '!' ) === 0) && s !== bangeq && + s !== bangeqeq ) + { + // if the filter name starts with ! as in '!is' then reverse the match logic and remove the ! + su = false; + s = s.substring( 1, s.length ); + } + // get the match results based on the s/match type + /*jslint eqeq : true */ + r = ( + (s === 'regex') ? (mtest.test( mvalue )) : (s === 'lt' || s === lt) + ? (mvalue < mtest) : (s === 'gt' || s === gt) + ? (mvalue > mtest) : (s === 'lte' || s === lteq) + ? (mvalue <= mtest) : (s === 'gte' || s === gteq) + ? (mvalue >= mtest) : (s === 'left') + ? (mvalue.indexOf( mtest ) === 0) : (s === 'leftnocase') + ? (mvalue.toLowerCase().indexOf( mtest.toLowerCase() ) + === 0) : (s === 'right') + ? (mvalue.substring( (mvalue.length - mtest.length) ) + === mtest) : (s === 'rightnocase') + ? (mvalue.toLowerCase().substring( + (mvalue.length - mtest.length) ) === mtest.toLowerCase()) + : (s === 'like') + ? (mvalue.indexOf( mtest ) >= 0) : (s === 'likenocase') + ? (mvalue.toLowerCase().indexOf(mtest.toLowerCase()) >= 0) + : (s === eqeqeq || s === 'is') + ? (mvalue === mtest) : (s === eqeq) + ? (mvalue == mtest) : (s === bangeqeq) + ? (mvalue !== mtest) : (s === bangeq) + ? (mvalue != mtest) : (s === 'isnocase') + ? (mvalue.toLowerCase + ? mvalue.toLowerCase() === mtest.toLowerCase() + : mvalue === mtest) : (s === 'has') + ? (T.has( mvalue, mtest )) : (s === 'hasall') + ? (T.hasAll( mvalue, mtest )) : ( + s.indexOf( 'is' ) === -1 + && !TAFFY.isNull( mvalue ) + && !TAFFY.isUndefined( mvalue ) + && !TAFFY.isObject( mtest ) + && !TAFFY.isArray( mtest ) + ) + ? (mtest === mvalue[s]) + : (T[s] && T.isFunction( T[s] ) + && s.indexOf( 'is' ) === 0) + ? T[s]( mvalue ) === mtest + : (T[s] && T.isFunction( T[s] )) + ? T[s]( mvalue, mtest ) : (false) + ); + /*jslint eqeq : false */ + r = (r && !su) ? false : (!r && !su) ? true : r; + + return r; + }; + c.push( matchFunc ); + + }); + // if only one filter in the collection push it onto the filter list without the array + if ( c.length === 1 ){ + + nf.push( c[0] ); + } + else { + // else build a function to loop over all the filters and return true only if ALL match + // this is a logical AND + nf.push( function () { + var that = this, match = false; + each( c, function ( f ) { + if ( f.apply( that ) ){ + match = true; + } + }); + return match; + }); + } + }); + }); + // finally return a single function that wraps all the other functions and will run a query + // where all functions have to return true for a record to appear in a query result + f = function () { + var that = this, match = true; + // faster if less than 4 functions + match = (nf.length === 1 && !nf[0].apply( that )) ? false : + (nf.length === 2 && + (!nf[0].apply( that ) || !nf[1].apply( that ))) ? false : + (nf.length === 3 && + (!nf[0].apply( that ) || !nf[1].apply( that ) || + !nf[2].apply( that ))) ? false : + (nf.length === 4 && + (!nf[0].apply( that ) || !nf[1].apply( that ) || + !nf[2].apply( that ) || !nf[3].apply( that ))) ? false + : true; + if ( nf.length > 4 ){ + each( nf, function ( f ) { + if ( !runFilters( that, f ) ){ + match = false; + } + }); + } + return match; + }; + return f; + } + + // if function + if ( T.isFunction( f ) ){ + return f; + } + }; + + orderByCol = function ( ar, o ) { + // **************************************** + // * + // * Takes: takes an array and a sort object + // * Returns: the array sorted + // * Purpose: Accept filters such as "[col], [col2]" or "[col] desc" and sort on those columns + // * + // **************************************** + + var sortFunc = function ( a, b ) { + // function to pass to the native array.sort to sort an array + var r = 0; + + T.each( o, function ( sd ) { + // loop over the sort instructions + // get the column name + var o, col, dir, c, d; + o = sd.split( ' ' ); + col = o[0]; + + // get the direction + dir = (o.length === 1) ? "logical" : o[1]; + + + if ( dir === 'logical' ){ + // if dir is logical than grab the charnum arrays for the two values we are looking at + c = numcharsplit( a[col] ); + d = numcharsplit( b[col] ); + // loop over the charnumarrays until one value is higher than the other + T.each( (c.length <= d.length) ? c : d, function ( x, i ) { + if ( c[i] < d[i] ){ + r = -1; + return TAFFY.EXIT; + } + else if ( c[i] > d[i] ){ + r = 1; + return TAFFY.EXIT; + } + } ); + } + else if ( dir === 'logicaldesc' ){ + // if logicaldesc than grab the charnum arrays for the two values we are looking at + c = numcharsplit( a[col] ); + d = numcharsplit( b[col] ); + // loop over the charnumarrays until one value is lower than the other + T.each( (c.length <= d.length) ? c : d, function ( x, i ) { + if ( c[i] > d[i] ){ + r = -1; + return TAFFY.EXIT; + } + else if ( c[i] < d[i] ){ + r = 1; + return TAFFY.EXIT; + } + } ); + } + else if ( dir === 'asec' && a[col] < b[col] ){ + // if asec - default - check to see which is higher + r = -1; + return T.EXIT; + } + else if ( dir === 'asec' && a[col] > b[col] ){ + // if asec - default - check to see which is higher + r = 1; + return T.EXIT; + } + else if ( dir === 'desc' && a[col] > b[col] ){ + // if desc check to see which is lower + r = -1; + return T.EXIT; + + } + else if ( dir === 'desc' && a[col] < b[col] ){ + // if desc check to see which is lower + r = 1; + return T.EXIT; + + } + // if r is still 0 and we are doing a logical sort than look to see if one array is longer than the other + if ( r === 0 && dir === 'logical' && c.length < d.length ){ + r = -1; + } + else if ( r === 0 && dir === 'logical' && c.length > d.length ){ + r = 1; + } + else if ( r === 0 && dir === 'logicaldesc' && c.length > d.length ){ + r = -1; + } + else if ( r === 0 && dir === 'logicaldesc' && c.length < d.length ){ + r = 1; + } + + if ( r !== 0 ){ + return T.EXIT; + } + + + } ); + return r; + }; + // call the sort function and return the newly sorted array + return (ar && ar.push) ? ar.sort( sortFunc ) : ar; + + + }; + + // **************************************** + // * + // * Takes: a string containing numbers and letters and turn it into an array + // * Returns: return an array of numbers and letters + // * Purpose: Used for logical sorting. String Example: 12ABC results: [12,'ABC'] + // **************************************** + (function () { + // creates a cache for numchar conversions + var cache = {}, cachcounter = 0; + // creates the numcharsplit function + numcharsplit = function ( thing ) { + // if over 1000 items exist in the cache, clear it and start over + if ( cachcounter > cmax ){ + cache = {}; + cachcounter = 0; + } + + // if a cache can be found for a numchar then return its array value + return cache['_' + thing] || (function () { + // otherwise do the conversion + // make sure it is a string and setup so other variables + var nthing = String( thing ), + na = [], + rv = '_', + rt = '', + x, xx, c; + + // loop over the string char by char + for ( x = 0, xx = nthing.length; x < xx; x++ ){ + // take the char at each location + c = nthing.charCodeAt( x ); + // check to see if it is a valid number char and append it to the array. + // if last char was a string push the string to the charnum array + if ( ( c >= 48 && c <= 57 ) || c === 46 ){ + if ( rt !== 'n' ){ + rt = 'n'; + na.push( rv.toLowerCase() ); + rv = ''; + } + rv = rv + nthing.charAt( x ); + } + else { + // check to see if it is a valid string char and append to string + // if last char was a number push the whole number to the charnum array + if ( rt !== 's' ){ + rt = 's'; + na.push( parseFloat( rv ) ); + rv = ''; + } + rv = rv + nthing.charAt( x ); + } + } + // once done, push the last value to the charnum array and remove the first uneeded item + na.push( (rt === 'n') ? parseFloat( rv ) : rv.toLowerCase() ); + na.shift(); + // add to cache + cache['_' + thing] = na; + cachcounter++; + // return charnum array + return na; + }()); + }; + }()); + + // **************************************** + // * + // * Runs a query + // **************************************** + + + run = function () { + this.context( { + results : this.getDBI().query( this.context() ) + }); + + }; + + API.extend( 'filter', function () { + // **************************************** + // * + // * Takes: takes unlimited filter objects as arguments + // * Returns: method collection + // * Purpose: Take filters as objects and cache functions for later lookup when a query is run + // **************************************** + var + nc = TAFFY.mergeObj( this.context(), { run : null } ), + nq = [] + ; + each( nc.q, function ( v ) { + nq.push( v ); + }); + nc.q = nq; + // Hadnle passing of ___ID or a record on lookup. + each( arguments, function ( f ) { + nc.q.push( returnFilter( f ) ); + nc.filterRaw.push( f ); + }); + + return this.getroot( nc ); + }); + + API.extend( 'order', function ( o ) { + // **************************************** + // * + // * Purpose: takes a string and creates an array of order instructions to be used with a query + // **************************************** + + o = o.split( ',' ); + var x = [], nc; + + each( o, function ( r ) { + x.push( r.replace( /^\s*/, '' ).replace( /\s*$/, '' ) ); + }); + + nc = TAFFY.mergeObj( this.context(), {sort : null} ); + nc.order = x; + + return this.getroot( nc ); + }); + + API.extend( 'limit', function ( n ) { + // **************************************** + // * + // * Purpose: takes a limit number to limit the number of rows returned by a query. Will update the results + // * of a query + // **************************************** + var nc = TAFFY.mergeObj( this.context(), {}), + limitedresults + ; + + nc.limit = n; + + if ( nc.run && nc.sort ){ + limitedresults = []; + each( nc.results, function ( i, x ) { + if ( (x + 1) > n ){ + return TAFFY.EXIT; + } + limitedresults.push( i ); + }); + nc.results = limitedresults; + } + + return this.getroot( nc ); + }); + + API.extend( 'start', function ( n ) { + // **************************************** + // * + // * Purpose: takes a limit number to limit the number of rows returned by a query. Will update the results + // * of a query + // **************************************** + var nc = TAFFY.mergeObj( this.context(), {} ), + limitedresults + ; + + nc.start = n; + + if ( nc.run && nc.sort && !nc.limit ){ + limitedresults = []; + each( nc.results, function ( i, x ) { + if ( (x + 1) > n ){ + limitedresults.push( i ); + } + }); + nc.results = limitedresults; + } + else { + nc = TAFFY.mergeObj( this.context(), {run : null, start : n} ); + } + + return this.getroot( nc ); + }); + + API.extend( 'update', function ( arg0, arg1, arg2 ) { + // **************************************** + // * + // * Takes: a object and passes it off DBI update method for all matched records + // **************************************** + var runEvent = true, o = {}, args = arguments, that; + if ( TAFFY.isString( arg0 ) && + (arguments.length === 2 || arguments.length === 3) ) + { + o[arg0] = arg1; + if ( arguments.length === 3 ){ + runEvent = arg2; + } + } + else { + o = arg0; + if ( args.length === 2 ){ + runEvent = arg1; + } + } + + that = this; + run.call( this ); + each( this.context().results, function ( r ) { + var c = o; + if ( TAFFY.isFunction( c ) ){ + c = c.apply( TAFFY.mergeObj( r, {} ) ); + } + else { + if ( T.isFunction( c ) ){ + c = c( TAFFY.mergeObj( r, {} ) ); + } + } + if ( TAFFY.isObject( c ) ){ + that.getDBI().update( r.___id, c, runEvent ); + } + }); + if ( this.context().results.length ){ + this.context( { run : null }); + } + return this; + }); + API.extend( 'remove', function ( runEvent ) { + // **************************************** + // * + // * Purpose: removes records from the DB via the remove and removeCommit DBI methods + // **************************************** + var that = this, c = 0; + run.call( this ); + each( this.context().results, function ( r ) { + that.getDBI().remove( r.___id ); + c++; + }); + if ( this.context().results.length ){ + this.context( { + run : null + }); + that.getDBI().removeCommit( runEvent ); + } + + return c; + }); + + + API.extend( 'count', function () { + // **************************************** + // * + // * Returns: The length of a query result + // **************************************** + run.call( this ); + return this.context().results.length; + }); + + API.extend( 'callback', function ( f, delay ) { + // **************************************** + // * + // * Returns null; + // * Runs a function on return of run.call + // **************************************** + if ( f ){ + var that = this; + setTimeout( function () { + run.call( that ); + f.call( that.getroot( that.context() ) ); + }, delay || 0 ); + } + + + return null; + }); + + API.extend( 'get', function () { + // **************************************** + // * + // * Returns: An array of all matching records + // **************************************** + run.call( this ); + return this.context().results; + }); + + API.extend( 'stringify', function () { + // **************************************** + // * + // * Returns: An JSON string of all matching records + // **************************************** + return JSON.stringify( this.get() ); + }); + API.extend( 'first', function () { + // **************************************** + // * + // * Returns: The first matching record + // **************************************** + run.call( this ); + return this.context().results[0] || false; + }); + API.extend( 'last', function () { + // **************************************** + // * + // * Returns: The last matching record + // **************************************** + run.call( this ); + return this.context().results[this.context().results.length - 1] || + false; + }); + + + API.extend( 'sum', function () { + // **************************************** + // * + // * Takes: column to sum up + // * Returns: Sums the values of a column + // **************************************** + var total = 0, that = this; + run.call( that ); + each( arguments, function ( c ) { + each( that.context().results, function ( r ) { + total = total + r[c]; + }); + }); + return total; + }); + + API.extend( 'min', function ( c ) { + // **************************************** + // * + // * Takes: column to find min + // * Returns: the lowest value + // **************************************** + var lowest = null; + run.call( this ); + each( this.context().results, function ( r ) { + if ( lowest === null || r[c] < lowest ){ + lowest = r[c]; + } + }); + return lowest; + }); + + // Taffy innerJoin Extension (OCD edition) + // ======================================= + // + // How to Use + // ********** + // + // left_table.innerJoin( right_table, condition1 <,... conditionN> ) + // + // A condition can take one of 2 forms: + // + // 1. An ARRAY with 2 or 3 values: + // A column name from the left table, an optional comparison string, + // and column name from the right table. The condition passes if the test + // indicated is true. If the condition string is omitted, '===' is assumed. + // EXAMPLES: [ 'last_used_time', '>=', 'current_use_time' ], [ 'user_id','id' ] + // + // 2. A FUNCTION: + // The function receives a left table row and right table row during the + // cartesian join. If the function returns true for the rows considered, + // the merged row is included in the result set. + // EXAMPLE: function (l,r){ return l.name === r.label; } + // + // Conditions are considered in the order they are presented. Therefore the best + // performance is realized when the least expensive and highest prune-rate + // conditions are placed first, since if they return false Taffy skips any + // further condition tests. + // + // Other notes + // *********** + // + // This code passes jslint with the exception of 2 warnings about + // the '==' and '!=' lines. We can't do anything about that short of + // deleting the lines. + // + // Credits + // ******* + // + // Heavily based upon the work of Ian Toltz. + // Revisions to API by Michael Mikowski. + // Code convention per standards in http://manning.com/mikowski + (function () { + var innerJoinFunction = (function () { + var fnCompareList, fnCombineRow, fnMain; + + fnCompareList = function ( left_row, right_row, arg_list ) { + var data_lt, data_rt, op_code, error; + + if ( arg_list.length === 2 ){ + data_lt = left_row[arg_list[0]]; + op_code = '==='; + data_rt = right_row[arg_list[1]]; + } + else { + data_lt = left_row[arg_list[0]]; + op_code = arg_list[1]; + data_rt = right_row[arg_list[2]]; + } + + /*jslint eqeq : true */ + switch ( op_code ){ + case '===' : + return data_lt === data_rt; + case '!==' : + return data_lt !== data_rt; + case '<' : + return data_lt < data_rt; + case '>' : + return data_lt > data_rt; + case '<=' : + return data_lt <= data_rt; + case '>=' : + return data_lt >= data_rt; + case '==' : + return data_lt == data_rt; + case '!=' : + return data_lt != data_rt; + default : + throw String( op_code ) + ' is not supported'; + } + // 'jslint eqeq : false' here results in + // "Unreachable '/*jslint' after 'return'". + // We don't need it though, as the rule exception + // is discarded at the end of this functional scope + }; + + fnCombineRow = function ( left_row, right_row ) { + var out_map = {}, i, prefix; + + for ( i in left_row ){ + if ( left_row.hasOwnProperty( i ) ){ + out_map[i] = left_row[i]; + } + } + for ( i in right_row ){ + if ( right_row.hasOwnProperty( i ) && i !== '___id' && + i !== '___s' ) + { + prefix = !TAFFY.isUndefined( out_map[i] ) ? 'right_' : ''; + out_map[prefix + String( i ) ] = right_row[i]; + } + } + return out_map; + }; + + fnMain = function ( table ) { + var + right_table, i, + arg_list = arguments, + arg_length = arg_list.length, + result_list = [] + ; + + if ( typeof table.filter !== 'function' ){ + if ( table.TAFFY ){ right_table = table(); } + else { + throw 'TAFFY DB or result not supplied'; + } + } + else { right_table = table; } + + this.context( { + results : this.getDBI().query( this.context() ) + } ); + + TAFFY.each( this.context().results, function ( left_row ) { + right_table.each( function ( right_row ) { + var arg_data, is_ok = true; + CONDITION: + for ( i = 1; i < arg_length; i++ ){ + arg_data = arg_list[i]; + if ( typeof arg_data === 'function' ){ + is_ok = arg_data( left_row, right_row ); + } + else if ( typeof arg_data === 'object' && arg_data.length ){ + is_ok = fnCompareList( left_row, right_row, arg_data ); + } + else { + is_ok = false; + } + + if ( !is_ok ){ break CONDITION; } // short circuit + } + + if ( is_ok ){ + result_list.push( fnCombineRow( left_row, right_row ) ); + } + } ); + } ); + return TAFFY( result_list )(); + }; + + return fnMain; + }()); + + API.extend( 'join', innerJoinFunction ); + }()); + + API.extend( 'max', function ( c ) { + // **************************************** + // * + // * Takes: column to find max + // * Returns: the highest value + // **************************************** + var highest = null; + run.call( this ); + each( this.context().results, function ( r ) { + if ( highest === null || r[c] > highest ){ + highest = r[c]; + } + }); + return highest; + }); + + API.extend( 'select', function () { + // **************************************** + // * + // * Takes: columns to select values into an array + // * Returns: array of values + // * Note if more than one column is given an array of arrays is returned + // **************************************** + + var ra = [], args = arguments; + run.call( this ); + if ( arguments.length === 1 ){ + + each( this.context().results, function ( r ) { + + ra.push( r[args[0]] ); + }); + } + else { + each( this.context().results, function ( r ) { + var row = []; + each( args, function ( c ) { + row.push( r[c] ); + }); + ra.push( row ); + }); + } + return ra; + }); + API.extend( 'distinct', function () { + // **************************************** + // * + // * Takes: columns to select unique alues into an array + // * Returns: array of values + // * Note if more than one column is given an array of arrays is returned + // **************************************** + var ra = [], args = arguments; + run.call( this ); + if ( arguments.length === 1 ){ + + each( this.context().results, function ( r ) { + var v = r[args[0]], dup = false; + each( ra, function ( d ) { + if ( v === d ){ + dup = true; + return TAFFY.EXIT; + } + }); + if ( !dup ){ + ra.push( v ); + } + }); + } + else { + each( this.context().results, function ( r ) { + var row = [], dup = false; + each( args, function ( c ) { + row.push( r[c] ); + }); + each( ra, function ( d ) { + var ldup = true; + each( args, function ( c, i ) { + if ( row[i] !== d[i] ){ + ldup = false; + return TAFFY.EXIT; + } + }); + if ( ldup ){ + dup = true; + return TAFFY.EXIT; + } + }); + if ( !dup ){ + ra.push( row ); + } + }); + } + return ra; + }); + API.extend( 'supplant', function ( template, returnarray ) { + // **************************************** + // * + // * Takes: a string template formated with key to be replaced with values from the rows, flag to determine if we want array of strings + // * Returns: array of values or a string + // **************************************** + var ra = []; + run.call( this ); + each( this.context().results, function ( r ) { + // TODO: The curly braces used to be unescaped + ra.push( template.replace( /\{([^\{\}]*)\}/g, function ( a, b ) { + var v = r[b]; + return typeof v === 'string' || typeof v === 'number' ? v : a; + } ) ); + }); + return (!returnarray) ? ra.join( "" ) : ra; + }); + + + API.extend( 'each', function ( m ) { + // **************************************** + // * + // * Takes: a function + // * Purpose: loops over every matching record and applies the function + // **************************************** + run.call( this ); + each( this.context().results, m ); + return this; + }); + API.extend( 'map', function ( m ) { + // **************************************** + // * + // * Takes: a function + // * Purpose: loops over every matching record and applies the function, returing the results in an array + // **************************************** + var ra = []; + run.call( this ); + each( this.context().results, function ( r ) { + ra.push( m( r ) ); + }); + return ra; + }); + + + + T = function ( d ) { + // **************************************** + // * + // * T is the main TAFFY object + // * Takes: an array of objects or JSON + // * Returns a new TAFFYDB + // **************************************** + var TOb = [], + ID = {}, + RC = 1, + settings = { + template : false, + onInsert : false, + onUpdate : false, + onRemove : false, + onDBChange : false, + storageName : false, + forcePropertyCase : null, + cacheSize : 100, + name : '' + }, + dm = new Date(), + CacheCount = 0, + CacheClear = 0, + Cache = {}, + DBI, runIndexes, root + ; + // **************************************** + // * + // * TOb = this database + // * ID = collection of the record IDs and locations within the DB, used for fast lookups + // * RC = record counter, used for creating IDs + // * settings.template = the template to merge all new records with + // * settings.onInsert = event given a copy of the newly inserted record + // * settings.onUpdate = event given the original record, the changes, and the new record + // * settings.onRemove = event given the removed record + // * settings.forcePropertyCase = on insert force the proprty case to be lower or upper. default lower, null/undefined will leave case as is + // * dm = the modify date of the database, used for query caching + // **************************************** + + + runIndexes = function ( indexes ) { + // **************************************** + // * + // * Takes: a collection of indexes + // * Returns: collection with records matching indexed filters + // **************************************** + + var records = [], UniqueEnforce = false; + + if ( indexes.length === 0 ){ + return TOb; + } + + each( indexes, function ( f ) { + // Check to see if record ID + if ( T.isString( f ) && /[t][0-9]*[r][0-9]*/i.test( f ) && + TOb[ID[f]] ) + { + records.push( TOb[ID[f]] ); + UniqueEnforce = true; + } + // Check to see if record + if ( T.isObject( f ) && f.___id && f.___s && + TOb[ID[f.___id]] ) + { + records.push( TOb[ID[f.___id]] ); + UniqueEnforce = true; + } + // Check to see if array of indexes + if ( T.isArray( f ) ){ + each( f, function ( r ) { + each( runIndexes( r ), function ( rr ) { + records.push( rr ); + }); + + }); + } + }); + if ( UniqueEnforce && records.length > 1 ){ + records = []; + } + + return records; + }; + + DBI = { + // **************************************** + // * + // * The DBI is the internal DataBase Interface that interacts with the data + // **************************************** + dm : function ( nd ) { + // **************************************** + // * + // * Takes: an optional new modify date + // * Purpose: used to get and set the DB modify date + // **************************************** + if ( nd ){ + dm = nd; + Cache = {}; + CacheCount = 0; + CacheClear = 0; + } + if ( settings.onDBChange ){ + setTimeout( function () { + settings.onDBChange.call( TOb ); + }, 0 ); + } + if ( settings.storageName ){ + setTimeout( function () { + localStorage.setItem( 'taffy_' + settings.storageName, + JSON.stringify( TOb ) ); + }); + } + return dm; + }, + insert : function ( i, runEvent ) { + // **************************************** + // * + // * Takes: a new record to insert + // * Purpose: merge the object with the template, add an ID, insert into DB, call insert event + // **************************************** + var columns = [], + records = [], + input = protectJSON( i ) + ; + each( input, function ( v, i ) { + var nv, o; + if ( T.isArray( v ) && i === 0 ){ + each( v, function ( av ) { + + columns.push( (settings.forcePropertyCase === 'lower') + ? av.toLowerCase() + : (settings.forcePropertyCase === 'upper') + ? av.toUpperCase() : av ); + }); + return true; + } + else if ( T.isArray( v ) ){ + nv = {}; + each( v, function ( av, ai ) { + nv[columns[ai]] = av; + }); + v = nv; + + } + else if ( T.isObject( v ) && settings.forcePropertyCase ){ + o = {}; + + eachin( v, function ( av, ai ) { + o[(settings.forcePropertyCase === 'lower') ? ai.toLowerCase() + : (settings.forcePropertyCase === 'upper') + ? ai.toUpperCase() : ai] = v[ai]; + }); + v = o; + } + + RC++; + v.___id = 'T' + String( idpad + TC ).slice( -6 ) + 'R' + + String( idpad + RC ).slice( -6 ); + v.___s = true; + records.push( v.___id ); + if ( settings.template ){ + v = T.mergeObj( settings.template, v ); + } + TOb.push( v ); + + ID[v.___id] = TOb.length - 1; + if ( settings.onInsert && + (runEvent || TAFFY.isUndefined( runEvent )) ) + { + settings.onInsert.call( v ); + } + DBI.dm( new Date() ); + }); + return root( records ); + }, + sort : function ( o ) { + // **************************************** + // * + // * Purpose: Change the sort order of the DB itself and reset the ID bucket + // **************************************** + TOb = orderByCol( TOb, o.split( ',' ) ); + ID = {}; + each( TOb, function ( r, i ) { + ID[r.___id] = i; + }); + DBI.dm( new Date() ); + return true; + }, + update : function ( id, changes, runEvent ) { + // **************************************** + // * + // * Takes: the ID of record being changed and the changes + // * Purpose: Update a record and change some or all values, call the on update method + // **************************************** + + var nc = {}, or, nr, tc, hasChange; + if ( settings.forcePropertyCase ){ + eachin( changes, function ( v, p ) { + nc[(settings.forcePropertyCase === 'lower') ? p.toLowerCase() + : (settings.forcePropertyCase === 'upper') ? p.toUpperCase() + : p] = v; + }); + changes = nc; + } + + or = TOb[ID[id]]; + nr = T.mergeObj( or, changes ); + + tc = {}; + hasChange = false; + eachin( nr, function ( v, i ) { + if ( TAFFY.isUndefined( or[i] ) || or[i] !== v ){ + tc[i] = v; + hasChange = true; + } + }); + if ( hasChange ){ + if ( settings.onUpdate && + (runEvent || TAFFY.isUndefined( runEvent )) ) + { + settings.onUpdate.call( nr, TOb[ID[id]], tc ); + } + TOb[ID[id]] = nr; + DBI.dm( new Date() ); + } + }, + remove : function ( id ) { + // **************************************** + // * + // * Takes: the ID of record to be removed + // * Purpose: remove a record, changes its ___s value to false + // **************************************** + TOb[ID[id]].___s = false; + }, + removeCommit : function ( runEvent ) { + var x; + // **************************************** + // * + // * + // * Purpose: loop over all records and remove records with ___s = false, call onRemove event, clear ID + // **************************************** + for ( x = TOb.length - 1; x > -1; x-- ){ + + if ( !TOb[x].___s ){ + if ( settings.onRemove && + (runEvent || TAFFY.isUndefined( runEvent )) ) + { + settings.onRemove.call( TOb[x] ); + } + ID[TOb[x].___id] = undefined; + TOb.splice( x, 1 ); + } + } + ID = {}; + each( TOb, function ( r, i ) { + ID[r.___id] = i; + }); + DBI.dm( new Date() ); + }, + query : function ( context ) { + // **************************************** + // * + // * Takes: the context object for a query and either returns a cache result or a new query result + // **************************************** + var returnq, cid, results, indexed, limitq, ni; + + if ( settings.cacheSize ) { + cid = ''; + each( context.filterRaw, function ( r ) { + if ( T.isFunction( r ) ){ + cid = 'nocache'; + return TAFFY.EXIT; + } + }); + if ( cid === '' ){ + cid = JSON.stringify( T.mergeObj( context, + {q : false, run : false, sort : false} ) ); + } + } + // Run a new query if there are no results or the run date has been cleared + if ( !context.results || !context.run || + (context.run && DBI.dm() > context.run) ) + { + results = []; + + // check Cache + + if ( settings.cacheSize && Cache[cid] ){ + + Cache[cid].i = CacheCount++; + return Cache[cid].results; + } + else { + // if no filter, return DB + if ( context.q.length === 0 && context.index.length === 0 ){ + each( TOb, function ( r ) { + results.push( r ); + }); + returnq = results; + } + else { + // use indexes + + indexed = runIndexes( context.index ); + + // run filters + each( indexed, function ( r ) { + // Run filter to see if record matches query + if ( context.q.length === 0 || runFilters( r, context.q ) ){ + results.push( r ); + } + }); + + returnq = results; + } + } + + + } + else { + // If query exists and run has not been cleared return the cache results + returnq = context.results; + } + // If a custom order array exists and the run has been clear or the sort has been cleared + if ( context.order.length > 0 && (!context.run || !context.sort) ){ + // order the results + returnq = orderByCol( returnq, context.order ); + } + + // If a limit on the number of results exists and it is less than the returned results, limit results + if ( returnq.length && + ((context.limit && context.limit < returnq.length) || + context.start) + ) { + limitq = []; + each( returnq, function ( r, i ) { + if ( !context.start || + (context.start && (i + 1) >= context.start) ) + { + if ( context.limit ){ + ni = (context.start) ? (i + 1) - context.start : i; + if ( ni < context.limit ){ + limitq.push( r ); + } + else if ( ni > context.limit ){ + return TAFFY.EXIT; + } + } + else { + limitq.push( r ); + } + } + }); + returnq = limitq; + } + + // update cache + if ( settings.cacheSize && cid !== 'nocache' ){ + CacheClear++; + + setTimeout( function () { + var bCounter, nc; + if ( CacheClear >= settings.cacheSize * 2 ){ + CacheClear = 0; + bCounter = CacheCount - settings.cacheSize; + nc = {}; + eachin( function ( r, k ) { + if ( r.i >= bCounter ){ + nc[k] = r; + } + }); + Cache = nc; + } + }, 0 ); + + Cache[cid] = { i : CacheCount++, results : returnq }; + } + return returnq; + } + }; + + + root = function () { + var iAPI, context; + // **************************************** + // * + // * The root function that gets returned when a new DB is created + // * Takes: unlimited filter arguments and creates filters to be run when a query is called + // **************************************** + // **************************************** + // * + // * iAPI is the the method collection valiable when a query has been started by calling dbname + // * Certain methods are or are not avaliable once you have started a query such as insert -- you can only insert into root + // **************************************** + iAPI = TAFFY.mergeObj( TAFFY.mergeObj( API, { insert : undefined } ), + { getDBI : function () { return DBI; }, + getroot : function ( c ) { return root.call( c ); }, + context : function ( n ) { + // **************************************** + // * + // * The context contains all the information to manage a query including filters, limits, and sorts + // **************************************** + if ( n ){ + context = TAFFY.mergeObj( context, + n.hasOwnProperty('results') + ? TAFFY.mergeObj( n, { run : new Date(), sort: new Date() }) + : n + ); + } + return context; + }, + extend : undefined + }); + + context = (this && this.q) ? this : { + limit : false, + start : false, + q : [], + filterRaw : [], + index : [], + order : [], + results : false, + run : null, + sort : null, + settings : settings + }; + // **************************************** + // * + // * Call the query method to setup a new query + // **************************************** + each( arguments, function ( f ) { + + if ( isIndexable( f ) ){ + context.index.push( f ); + } + else { + context.q.push( returnFilter( f ) ); + } + context.filterRaw.push( f ); + }); + + + return iAPI; + }; + + // **************************************** + // * + // * If new records have been passed on creation of the DB either as JSON or as an array/object, insert them + // **************************************** + TC++; + if ( d ){ + DBI.insert( d ); + } + + + root.insert = DBI.insert; + + root.merge = function ( i, key, runEvent ) { + var + search = {}, + finalSearch = [], + obj = {} + ; + + runEvent = runEvent || false; + key = key || 'id'; + + each( i, function ( o ) { + var existingObject; + search[key] = o[key]; + finalSearch.push( o[key] ); + existingObject = root( search ).first(); + if ( existingObject ){ + DBI.update( existingObject.___id, o, runEvent ); + } + else { + DBI.insert( o, runEvent ); + } + }); + + obj[key] = finalSearch; + return root( obj ); + }; + + root.TAFFY = true; + root.sort = DBI.sort; + // **************************************** + // * + // * These are the methods that can be accessed on off the root DB function. Example dbname.insert; + // **************************************** + root.settings = function ( n ) { + // **************************************** + // * + // * Getting and setting for this DB's settings/events + // **************************************** + if ( n ){ + settings = TAFFY.mergeObj( settings, n ); + if ( n.template ){ + + root().update( n.template ); + } + } + return settings; + }; + + // **************************************** + // * + // * These are the methods that can be accessed on off the root DB function. Example dbname.insert; + // **************************************** + root.store = function ( n ) { + // **************************************** + // * + // * Setup localstorage for this DB on a given name + // * Pull data into the DB as needed + // **************************************** + var r = false, i; + if ( localStorage ){ + if ( n ){ + i = localStorage.getItem( 'taffy_' + n ); + if ( i && i.length > 0 ){ + root.insert( i ); + r = true; + } + if ( TOb.length > 0 ){ + setTimeout( function () { + localStorage.setItem( 'taffy_' + settings.storageName, + JSON.stringify( TOb ) ); + }); + } + } + root.settings( {storageName : n} ); + } + return root; + }; + + // **************************************** + // * + // * Return root on DB creation and start having fun + // **************************************** + return root; + }; + // **************************************** + // * + // * Sets the global TAFFY object + // **************************************** + TAFFY = T; + + + // **************************************** + // * + // * Create public each method + // * + // **************************************** + T.each = each; + + // **************************************** + // * + // * Create public eachin method + // * + // **************************************** + T.eachin = eachin; + // **************************************** + // * + // * Create public extend method + // * Add a custom method to the API + // * + // **************************************** + T.extend = API.extend; + + + // **************************************** + // * + // * Creates TAFFY.EXIT value that can be returned to stop an each loop + // * + // **************************************** + TAFFY.EXIT = 'TAFFYEXIT'; + + // **************************************** + // * + // * Create public utility mergeObj method + // * Return a new object where items from obj2 + // * have replaced or been added to the items in + // * obj1 + // * Purpose: Used to combine objs + // * + // **************************************** + TAFFY.mergeObj = function ( ob1, ob2 ) { + var c = {}; + eachin( ob1, function ( v, n ) { c[n] = ob1[n]; }); + eachin( ob2, function ( v, n ) { c[n] = ob2[n]; }); + return c; + }; + + + // **************************************** + // * + // * Create public utility has method + // * Returns true if a complex object, array + // * or taffy collection contains the material + // * provided in the second argument + // * Purpose: Used to comare objects + // * + // **************************************** + TAFFY.has = function ( var1, var2 ) { + + var re = true, n; + + if ( (var1.TAFFY) ){ + re = var1( var2 ); + if ( re.length > 0 ){ + return true; + } + else { + return false; + } + } + else { + + switch ( T.typeOf( var1 ) ){ + case 'object': + if ( T.isObject( var2 ) ){ + eachin( var2, function ( v, n ) { + if ( re === true && !T.isUndefined( var1[n] ) && + var1.hasOwnProperty( n ) ) + { + re = T.has( var1[n], var2[n] ); + } + else { + re = false; + return TAFFY.EXIT; + } + }); + } + else if ( T.isArray( var2 ) ){ + each( var2, function ( v, n ) { + re = T.has( var1, var2[n] ); + if ( re ){ + return TAFFY.EXIT; + } + }); + } + else if ( T.isString( var2 ) ){ + if ( !TAFFY.isUndefined( var1[var2] ) ){ + return true; + } + else { + return false; + } + } + return re; + case 'array': + if ( T.isObject( var2 ) ){ + each( var1, function ( v, i ) { + re = T.has( var1[i], var2 ); + if ( re === true ){ + return TAFFY.EXIT; + } + }); + } + else if ( T.isArray( var2 ) ){ + each( var2, function ( v2, i2 ) { + each( var1, function ( v1, i1 ) { + re = T.has( var1[i1], var2[i2] ); + if ( re === true ){ + return TAFFY.EXIT; + } + }); + if ( re === true ){ + return TAFFY.EXIT; + } + }); + } + else if ( T.isString( var2 ) || T.isNumber( var2 ) ){ + for ( n = 0; n < var1.length; n++ ){ + re = T.has( var1[n], var2 ); + if ( re ){ + return true; + } + } + } + return re; + case 'string': + if ( T.isString( var2 ) && var2 === var1 ){ + return true; + } + break; + default: + if ( T.typeOf( var1 ) === T.typeOf( var2 ) && var1 === var2 ){ + return true; + } + break; + } + } + return false; + }; + + // **************************************** + // * + // * Create public utility hasAll method + // * Returns true if a complex object, array + // * or taffy collection contains the material + // * provided in the call - for arrays it must + // * contain all the material in each array item + // * Purpose: Used to comare objects + // * + // **************************************** + TAFFY.hasAll = function ( var1, var2 ) { + + var T = TAFFY, ar; + if ( T.isArray( var2 ) ){ + ar = true; + each( var2, function ( v ) { + ar = T.has( var1, v ); + if ( ar === false ){ + return TAFFY.EXIT; + } + }); + return ar; + } + else { + return T.has( var1, var2 ); + } + }; + + + // **************************************** + // * + // * typeOf Fixed in JavaScript as public utility + // * + // **************************************** + TAFFY.typeOf = function ( v ) { + var s = typeof v; + if ( s === 'object' ){ + if ( v ){ + if ( typeof v.length === 'number' && + !(v.propertyIsEnumerable( 'length' )) ) + { + s = 'array'; + } + } + else { + s = 'null'; + } + } + return s; + }; + + // **************************************** + // * + // * Create public utility getObjectKeys method + // * Returns an array of an objects keys + // * Purpose: Used to get the keys for an object + // * + // **************************************** + TAFFY.getObjectKeys = function ( ob ) { + var kA = []; + eachin( ob, function ( n, h ) { + kA.push( h ); + }); + kA.sort(); + return kA; + }; + + // **************************************** + // * + // * Create public utility isSameArray + // * Returns an array of an objects keys + // * Purpose: Used to get the keys for an object + // * + // **************************************** + TAFFY.isSameArray = function ( ar1, ar2 ) { + return (TAFFY.isArray( ar1 ) && TAFFY.isArray( ar2 ) && + ar1.join( ',' ) === ar2.join( ',' )) ? true : false; + }; + + // **************************************** + // * + // * Create public utility isSameObject method + // * Returns true if objects contain the same + // * material or false if they do not + // * Purpose: Used to comare objects + // * + // **************************************** + TAFFY.isSameObject = function ( ob1, ob2 ) { + var T = TAFFY, rv = true; + + if ( T.isObject( ob1 ) && T.isObject( ob2 ) ){ + if ( T.isSameArray( T.getObjectKeys( ob1 ), + T.getObjectKeys( ob2 ) ) ) + { + eachin( ob1, function ( v, n ) { + if ( ! ( (T.isObject( ob1[n] ) && T.isObject( ob2[n] ) && + T.isSameObject( ob1[n], ob2[n] )) || + (T.isArray( ob1[n] ) && T.isArray( ob2[n] ) && + T.isSameArray( ob1[n], ob2[n] )) || (ob1[n] === ob2[n]) ) + ) { + rv = false; + return TAFFY.EXIT; + } + }); + } + else { + rv = false; + } + } + else { + rv = false; + } + return rv; + }; + + // **************************************** + // * + // * Create public utility is[DataType] methods + // * Return true if obj is datatype, false otherwise + // * Purpose: Used to determine if arguments are of certain data type + // * + // * mmikowski 2012-08-06 refactored to make much less "magical": + // * fewer closures and passes jslint + // * + // **************************************** + + typeList = [ + 'String', 'Number', 'Object', 'Array', + 'Boolean', 'Null', 'Function', 'Undefined' + ]; + + makeTest = function ( thisKey ) { + return function ( data ) { + return TAFFY.typeOf( data ) === thisKey.toLowerCase() ? true : false; + }; + }; + + for ( idx = 0; idx < typeList.length; idx++ ){ + typeKey = typeList[idx]; + TAFFY['is' + typeKey] = makeTest( typeKey ); + } + } +}()); + +if ( typeof(exports) === 'object' ){ + exports.taffy = TAFFY; +} + diff --git a/third_party/jsdoc/node_modules/tv4/LICENSE.txt b/third_party/jsdoc/node_modules/tv4/LICENSE.txt new file mode 100644 index 0000000000..f421228be3 --- /dev/null +++ b/third_party/jsdoc/node_modules/tv4/LICENSE.txt @@ -0,0 +1,8 @@ +/* +Author: Geraint Luff and others +Year: 2013 + +This code is released into the "public domain" by its author(s). Anybody may use, alter and distribute the code without restriction. The author makes no guarantees, and takes no liability of any kind for use of this code. + +If you find a bug or make an improvement, it would be courteous to let the author know, but it is not compulsory. +*/ \ No newline at end of file diff --git a/third_party/jsdoc/node_modules/tv4/README.md b/third_party/jsdoc/node_modules/tv4/README.md new file mode 100644 index 0000000000..66f263f0f1 --- /dev/null +++ b/third_party/jsdoc/node_modules/tv4/README.md @@ -0,0 +1,453 @@ +# Tiny Validator (for v4 JSON Schema) + +[![Build Status](https://secure.travis-ci.org/geraintluff/tv4.png?branch=master)](http://travis-ci.org/geraintluff/tv4) [![Dependency Status](https://gemnasium.com/geraintluff/tv4.png)](https://gemnasium.com/geraintluff/tv4) [![NPM version](https://badge.fury.io/js/tv4.png)](http://badge.fury.io/js/tv4) + +Use [json-schema](http://json-schema.org/) [draft v4](http://json-schema.org/latest/json-schema-core.html) to validate simple values and complex objects using a rich [validation vocabulary](http://json-schema.org/latest/json-schema-validation.html) ([examples](http://json-schema.org/examples.html)). + +There is support for `$ref` with JSON Pointer fragment paths (```other-schema.json#/properties/myKey```). + +## Usage 1: Simple validation + +```javascript +var valid = tv4.validate(data, schema); +``` + +If validation returns ```false```, then an explanation of why validation failed can be found in ```tv4.error```. + +The error object will look something like: +```json +{ + "code": 0, + "message": "Invalid type: string", + "dataPath": "/intKey", + "schemaKey": "/properties/intKey/type" +} +``` + +The `"code"` property will refer to one of the values in `tv4.errorCodes` - in this case, `tv4.errorCodes.INVALID_TYPE`. + +To enable external schema to be referenced, you use: +```javascript +tv4.addSchema(url, schema); +``` + +If schemas are referenced (```$ref```) but not known, then validation will return ```true``` and the missing schema(s) will be listed in ```tv4.missing```. For more info see the API documentation below. + +## Usage 2: Multi-threaded validation + +Storing the error and missing schemas does not work well in multi-threaded environments, so there is an alternative syntax: + +```javascript +var result = tv4.validateResult(data, schema); +``` + +The result will look something like: +```json +{ + "valid": false, + "error": {...}, + "missing": [...] +} +``` + +## Usage 3: Multiple errors + +Normally, `tv4` stops when it encounters the first validation error. However, you can collect an array of validation errors using: + +```javascript +var result = tv4.validateMultiple(data, schema); +``` + +The result will look something like: +```json +{ + "valid": false, + "errors": [ + {...}, + ... + ], + "missing": [...] +} +``` + +## Asynchronous validation + +Support for asynchronous validation (where missing schemas are fetched) can be added by including an extra JavaScript file. Currently, the only version requires jQuery (`tv4.async-jquery.js`), but the code is very short and should be fairly easy to modify for other libraries (such as MooTools). + +Usage: + +```javascript +tv4.validate(data, schema, function (isValid, validationError) { ... }); +``` + +`validationFailure` is simply taken from `tv4.error`. + +## Options + +You can use several options to change tv4's behavior when validating objects. Pass in an object to set these options for any of the regular validation methods: + +```javascript +tv4.validate(data, schema, {checkRecursive: true}); +var result = tv4.validateResult(data, schema, {checkRecursive: true}); +var multiple = tv4.validateMultiple(data, schema, {checkRecursive: true}); +``` + +For backwards compatibility, you can also pass in two booleans to set the `checkRecursive` and `banUnknownProperties` options; this method signature is deprecated: + +```javascript +// Set checkRecursive to false (the default) and banUnknownProperties to true +tv4.validate(data, schema, false, true); +``` + +The following sections describe the validation options. + +### Cyclical JavaScript objects + +While they don't occur in proper JSON, JavaScript does support self-referencing objects. Any of the above calls support a checkRecursive option. If true, tv4 will handle self-referencing objects properly - this slows down validation slightly, but that's better than a hanging script. + +Consider this data, notice how both `a` and `b` refer to each other: + +```javascript +var a = {}; +var b = { a: a }; +a.b = b; +var aSchema = { properties: { b: { $ref: 'bSchema' }}}; +var bSchema = { properties: { a: { $ref: 'aSchema' }}}; +tv4.addSchema('aSchema', aSchema); +tv4.addSchema('bSchema', bSchema); +``` + +By default, this causes the validation methods to throw a "too much recursion" error. + +To enable support for self-referencing objects, set the checkRecursive option to `true`: + +```javascript +tv4.validate(a, aSchema, {checkRecursive: true}); +tv4.validate(a, schema, asynchronousFunction, {checkRecursive: true}); + +tv4.validateResult(a, aSchema, {checkRecursive: true}); +tv4.validateMultiple(a, aSchema, {checkRecursive: true}); +``` + +### Properties not defined in the schema + +An object's schema may include an additionalProperties setting. When additionalProperties is set to `false`, objects will fail validation if they include properties that are not defined in the schema. + +You can enforce this behavior for all object schema by setting tv4's banUnknownProperties option to `true`: + +```javascript +tv4.validate(data, schema, {banUnknownProperties: true}); +tv4.validate(data, schema, asynchronousFunction, {banUnknownProperties: true}); + +tv4.validateResult(data, schema, {banUnknownProperties: true}); +tv4.validateMultiple(data, schema, {banUnknownProperties: true}); +``` + +### Inherited properties + +By default, tv4 does not validate an object's inherited properties, which are ignored when you convert an object to JSON. This behavior differs from tv4 1.0.16 and earlier, which always validated inherited properties. + +To validate inherited properties, set tv4's checkInheritedProperties option to `true`: + +```javascript +tv4.validate(data, schema, {checkInheritedProperties: true}); +tv4.validate(data, schema, asynchronousFunction, {checkInheritedProperties: true}); + +tv4.validateResult(data, schema, {checkInheritedProperties: true}); +tv4.validateMultiple(data, schema, {checkInheritedProperties: true}); +``` + +### Non-enumerable properties + +By default, tv4 does not validate an object's own non-enumerable properties, which are ignored when you convert an object to JSON. This behavior differs from tv4 1.0.16 and earlier, which always validated an object's own non-enumerable properties. + +To validate an object's own non-enumerable properties, set tv4's checkNonEnumerableProperties option to `true`: + +```javascript +tv4.validate(data, schema, {checkNonEnumerableProperties: true}); +tv4.validate(data, schema, asynchronousFunction, {checkNonEnumerableProperties: true}); + +tv4.validateResult(data, schema, {checkNonEnumerableProperties: true}); +tv4.validateMultiple(data, schema, {checkNonEnumerableProperties: true}); +``` + +## API + +There are additional api commands available for more complex use-cases: + +##### addSchema(uri, schema) +Pre-register a schema for reference by other schema and synchronous validation. + +````js +tv4.addSchema('http://example.com/schema', { ... }); +```` + +* `uri` the uri to identify this schema. +* `schema` the schema object. + +Schemas that have their `id` property set can be added directly. + +````js +tv4.addSchema({ ... }); +```` + +##### getSchema(uri) + +Return a schema from the cache. + +* `uri` the uri of the schema (may contain a `#` fragment) + +````js +var schema = tv4.getSchema('http://example.com/schema'); +```` + +##### getSchemaMap() + +Return a shallow copy of the schema cache, mapping schema document URIs to schema objects. + +```` +var map = tv4.getSchemaMap(); + +var schema = map[uri]; +```` + +##### getSchemaUris(filter) + +Return an Array with known schema document URIs. + +* `filter` optional RegExp to filter URIs + +```` +var arr = tv4.getSchemaUris(); + +// optional filter using a RegExp +var arr = tv4.getSchemaUris(/^https?://example.com/); +```` + +##### getMissingUris(filter) + +Return an Array with schema document URIs that are used as `$ref` in known schemas but which currently have no associated schema data. + +Use this in combination with `tv4.addSchema(uri, schema)` to preload the cache for complete synchronous validation with. + +* `filter` optional RegExp to filter URIs + +```` +var arr = tv4.getMissingUris(); + +// optional filter using a RegExp +var arr = tv4.getMissingUris(/^https?://example.com/); +```` + +##### dropSchemas() + +Drop all known schema document URIs from the cache. + +```` +tv4.dropSchemas(); +```` + +##### freshApi() + +Return a new tv4 instance with no shared state. + +```` +var otherTV4 = tv4.freshApi(); +```` + +##### reset() + +Manually reset validation status from the simple `tv4.validate(data, schema)`. Although tv4 will self reset on each validation there are some implementation scenarios where this is useful. + +```` +tv4.reset(); +```` + +##### language(code) + +Select the language map used for reporting. + +* `code` is a language code, like `'en'` or `'en-gb'` + +```` +tv4.language('en-gb'); +```` + +##### addLanguage(code, map) + +Add a new language map for selection by `tv4.language(code)` + +* `code` is new language code +* `map` is an object mapping error IDs or constant names (e.g. `103` or `"NUMBER_MAXIMUM"`) to language strings. + +```` +tv4.addLanguage('fr', { ... }); + +// select for use +tv4.language('fr') +```` + +##### addFormat(format, validationFunction) + +Add a custom format validator. (There are no built-in format validators.) + +* `format` is a string, corresponding to the `"format"` value in schemas. +* `validationFunction` is a function that either returns: + * `null` (meaning no error) + * an error string (explaining the reason for failure) + +```` +tv4.addFormat('decimal-digits', function (data, schema) { + if (typeof data === 'string' && !/^[0-9]+$/.test(data)) { + return null; + } + return "must be string of decimal digits"; +}); +```` + +Alternatively, multiple formats can be added at the same time using an object: +```` +tv4.addFormat({ + 'my-format': function () {...}, + 'other-format': function () {...} +}); +```` + +## Demos + +### Basic usage +
    +
    +var schema = {
    +	"items": {
    +		"type": "boolean"
    +	}
    +};
    +var data1 = [true, false];
    +var data2 = [true, 123];
    +
    +alert("data 1: " + tv4.validate(data1, schema)); // true
    +alert("data 2: " + tv4.validate(data2, schema)); // false
    +alert("data 2 error: " + JSON.stringify(tv4.error, null, 4));
    +
    +
    + +### Use of $ref +
    +
    +var schema = {
    +	"type": "array",
    +	"items": {"$ref": "#"}
    +};
    +var data1 = [[], [[]]];
    +var data2 = [[], [true, []]];
    +
    +alert("data 1: " + tv4.validate(data1, schema)); // true
    +alert("data 2: " + tv4.validate(data2, schema)); // false
    +
    +
    + +### Missing schema +
    +
    +var schema = {
    +	"type": "array",
    +	"items": {"$ref": "http://example.com/schema" }
    +};
    +var data = [1, 2, 3];
    +
    +alert("Valid: " + tv4.validate(data, schema)); // true
    +alert("Missing schemas: " + JSON.stringify(tv4.missing));
    +
    +
    + +### Referencing remote schema +
    +
    +tv4.addSchema("http://example.com/schema", {
    +	"definitions": {
    +		"arrayItem": {"type": "boolean"}
    +	}
    +});
    +var schema = {
    +	"type": "array",
    +	"items": {"$ref": "http://example.com/schema#/definitions/arrayItem" }
    +};
    +var data1 = [true, false, true];
    +var data2 = [1, 2, 3];
    +
    +alert("data 1: " + tv4.validate(data1, schema)); // true
    +alert("data 2: " + tv4.validate(data2, schema)); // false
    +
    +
    + +## Supported platforms + +* Node.js +* All modern browsers +* IE >= 7 + +## Installation + +You can manually download [`tv4.js`](https://raw.github.com/geraintluff/tv4/master/tv4.js) or the minified [`tv4.min.js`](https://raw.github.com/geraintluff/tv4/master/tv4.min.js) and include it in your html to create the global `tv4` variable. + +Alternately use it as a CommonJS module: + +````js +var tv4 = require('tv4'); +```` + +#### npm + +```` +$ npm install tv4 +```` + +#### bower + +```` +$ bower install tv4 +```` + +#### component.io + +```` +$ component install geraintluff/tv4 +```` + +## Build and test + +You can rebuild and run the node and browser tests using node.js and [grunt](http://http://gruntjs.com/): + +Make sure you have the global grunt cli command: +```` +$ npm install grunt-cli -g +```` + +Clone the git repos, open a shell in the root folder and install the development dependencies: + +```` +$ npm install +```` + +Rebuild and run the tests: +```` +$ grunt +```` + +It will run a build and display one Spec-style report for the node.js and two Dot-style reports for both the plain and minified browser tests (via phantomJS). You can also use your own browser to manually run the suites by opening [`test/index.html`](http://geraintluff.github.io/tv4/test/index.html) and [`test/index-min.html`](http://geraintluff.github.io/tv4/test/index-min.html). + +## Contributing + +Pull-requests for fixes and expansions are welcome. Edit the partial files in `/source` and add your tests in a suitable suite or folder under `/test/tests` and run `grunt` to rebuild and run the test suite. Try to maintain an idiomatic coding style and add tests for any new features. It is recommend to discuss big changes in an Issue. + +## Packages using tv4 + +* [chai-json-schema](http://chaijs.com/plugins/chai-json-schema) is a [Chai Assertion Library](http://chaijs.com) plugin to assert values against json-schema. +* [grunt-tv4](http://www.github.com/Bartvds/grunt-tv4) is a plugin for [Grunt](http://http://gruntjs.com/) that uses tv4 to bulk validate json files. + +## License + +The code is available as "public domain", meaning that it is completely free to use, without any restrictions at all. Read the full license [here](http://geraintluff.github.com/tv4/LICENSE.txt). + +It's also available under an [MIT license](http://jsonary.com/LICENSE.txt). diff --git a/third_party/jsdoc/node_modules/tv4/lang/de.js b/third_party/jsdoc/node_modules/tv4/lang/de.js new file mode 100644 index 0000000000..aeb9f12fc6 --- /dev/null +++ b/third_party/jsdoc/node_modules/tv4/lang/de.js @@ -0,0 +1,29 @@ +tv4.addLanguage('de', { + INVALID_TYPE: "Ungültiger Typ: {type} (erwartet wurde: {expected})", + ENUM_MISMATCH: "Keine Übereinstimmung mit der Aufzählung (enum) für: {value}", + ANY_OF_MISSING: "Daten stimmen nicht überein mit einem der Schemas von \"anyOf\"", + ONE_OF_MISSING: "Daten stimmen nicht überein mit einem der Schemas von \"oneOf\"", + ONE_OF_MULTIPLE: "Daten sind valid in Bezug auf mehreren Schemas von \"oneOf\": index {index1} und {index2}", + NOT_PASSED: "Daten stimmen mit dem \"not\" Schema überein", + // Numeric errors + NUMBER_MULTIPLE_OF: "Wert {value} ist kein Vielfaches von {multipleOf}", + NUMBER_MINIMUM: "Wert {value} ist kleiner als das Minimum {minimum}", + NUMBER_MINIMUM_EXCLUSIVE: "Wert {value} ist gleich dem Exklusiven Minimum {minimum}", + NUMBER_MAXIMUM: "Wert {value} ist größer als das Maximum {maximum}", + NUMBER_MAXIMUM_EXCLUSIVE: "Wert {value} ist gleich dem Exklusiven Maximum {maximum}", + // String errors + STRING_LENGTH_SHORT: "Zeichenkette zu kurz ({length} chars), minimum {minimum}", + STRING_LENGTH_LONG: "Zeichenkette zu lang ({length} chars), maximum {maximum}", + STRING_PATTERN: "Zeichenkette entspricht nicht dem Muster: {pattern}", + // Object errors + OBJECT_PROPERTIES_MINIMUM: "Zu wenige Attribute definiert ({propertyCount}), minimum {minimum}", + OBJECT_PROPERTIES_MAXIMUM: "Zu viele Attribute definiert ({propertyCount}), maximum {maximum}", + OBJECT_REQUIRED: "Notwendiges Attribut fehlt: {key}", + OBJECT_ADDITIONAL_PROPERTIES: "Zusätzliche Attribute nicht erlaubt", + OBJECT_DEPENDENCY_KEY: "Abhängigkeit fehlt - Schlüssel nicht vorhanden: {missing} (wegen Schlüssel: {key})", + // Array errors + ARRAY_LENGTH_SHORT: "Array zu kurz ({length}), minimum {minimum}", + ARRAY_LENGTH_LONG: "Array zu lang ({length}), maximum {maximum}", + ARRAY_UNIQUE: "Array Einträge nicht eindeutig (Index {match1} und {match2})", + ARRAY_ADDITIONAL_ITEMS: "Zusätzliche Einträge nicht erlaubt" +}); diff --git a/third_party/jsdoc/node_modules/tv4/package.json b/third_party/jsdoc/node_modules/tv4/package.json new file mode 100644 index 0000000000..922346c8a0 --- /dev/null +++ b/third_party/jsdoc/node_modules/tv4/package.json @@ -0,0 +1,73 @@ +{ + "name": "tv4", + "version": "1.0.15", + "author": { + "name": "Geraint Luff" + }, + "description": "A public domain JSON Schema validator for JavaScript", + "keywords": [ + "json-schema", + "schema", + "validator", + "tv4" + ], + "maintainers": [ + { + "name": "Geraint Luff", + "email": "luffgd@gmail.com", + "url": "https://github.com/geraintluff/" + } + ], + "main": "tv4.js", + "repository": { + "type": "git", + "url": "https://github.com/geraintluff/tv4.git" + }, + "license:": [ + { + "type": "Public Domain", + "url": "http://geraintluff.github.io/tv4/LICENSE.txt" + }, + { + "type": "MIT", + "url": "http://jsonary.com/LICENSE.txt" + } + ], + "devDependencies": { + "grunt": "~0.4.1", + "grunt-contrib-clean": "~0.4.1", + "grunt-contrib-jshint": "~0.6.2", + "mocha": "~1.11.0", + "grunt-mocha": "~0.4", + "grunt-mocha-test": "~0.5.0", + "grunt-cli": "~0.1.9", + "grunt-contrib-uglify": "~0.2.2", + "grunt-contrib-copy": "~0.4.1", + "proclaim": "1.4", + "mocha-unfunk-reporter": "~0.2", + "jshint-path-reporter": "~0.1", + "grunt-concat-sourcemap": "~0.2", + "source-map-support": "~0.1", + "grunt-markdown": "~0.3.0", + "grunt-component": "~0.1.4", + "grunt-push-release": "~0.1.1", + "grunt-regex-replace": "~0.2.5" + }, + "engines": { + "node": ">= 0.8.0" + }, + "scripts": { + "test": "grunt test", + "prepublish": "grunt prepublish" + }, + "readme": "# Tiny Validator (for v4 JSON Schema)\n\n[![Build Status](https://secure.travis-ci.org/geraintluff/tv4.png?branch=master)](http://travis-ci.org/geraintluff/tv4) [![Dependency Status](https://gemnasium.com/geraintluff/tv4.png)](https://gemnasium.com/geraintluff/tv4) [![NPM version](https://badge.fury.io/js/tv4.png)](http://badge.fury.io/js/tv4)\n\nUse [json-schema](http://json-schema.org/) [draft v4](http://json-schema.org/latest/json-schema-core.html) to validate simple values and complex objects using a rich [validation vocabulary](http://json-schema.org/latest/json-schema-validation.html) ([examples](http://json-schema.org/examples.html)).\n\nThere is support for `$ref` with JSON Pointer fragment paths (```other-schema.json#/properties/myKey```).\n\n## Usage 1: Simple validation\n\n```javascript\nvar valid = tv4.validate(data, schema);\n```\n\nIf validation returns ```false```, then an explanation of why validation failed can be found in ```tv4.error```.\n\nThe error object will look something like:\n```json\n{\n \"code\": 0,\n \"message\": \"Invalid type: string\",\n \"dataPath\": \"/intKey\",\n \"schemaKey\": \"/properties/intKey/type\"\n}\n```\n\nThe `\"code\"` property will refer to one of the values in `tv4.errorCodes` - in this case, `tv4.errorCodes.INVALID_TYPE`.\n\nTo enable external schema to be referenced, you use:\n```javascript\ntv4.addSchema(url, schema);\n```\n\nIf schemas are referenced (```$ref```) but not known, then validation will return ```true``` and the missing schema(s) will be listed in ```tv4.missing```. For more info see the API documentation below.\n\n## Usage 2: Multi-threaded validation\n\nStoring the error and missing schemas does not work well in multi-threaded environments, so there is an alternative syntax:\n\n```javascript\nvar result = tv4.validateResult(data, schema);\n```\n\nThe result will look something like:\n```json\n{\n \"valid\": false,\n \"error\": {...},\n \"missing\": [...]\n}\n```\n\n## Usage 3: Multiple errors\n\nNormally, `tv4` stops when it encounters the first validation error. However, you can collect an array of validation errors using:\n\n```javascript\nvar result = tv4.validateMultiple(data, schema);\n```\n\nThe result will look something like:\n```json\n{\n \"valid\": false,\n \"errors\": [\n {...},\n ...\n ],\n \"missing\": [...]\n}\n```\n\n## Asynchronous validation\n\nSupport for asynchronous validation (where missing schemas are fetched) can be added by including an extra JavaScript file. Currently, the only version requires jQuery (`tv4.async-jquery.js`), but the code is very short and should be fairly easy to modify for other libraries (such as MooTools).\n\nUsage:\n\n```javascript\ntv4.validate(data, schema, function (isValid, validationError) { ... });\n```\n\n`validationFailure` is simply taken from `tv4.error`.\n\n## Options\n\nYou can use several options to change tv4's behavior when validating objects. Pass in an object to set these options for any of the regular validation methods:\n\n```javascript\ntv4.validate(data, schema, {checkRecursive: true});\nvar result = tv4.validateResult(data, schema, {checkRecursive: true});\nvar multiple = tv4.validateMultiple(data, schema, {checkRecursive: true});\n```\n\nFor backwards compatibility, you can also pass in two booleans to set the `checkRecursive` and `banUnknownProperties` options; this method signature is deprecated:\n\n```javascript\n// Set checkRecursive to false (the default) and banUnknownProperties to true\ntv4.validate(data, schema, false, true);\n```\n\nThe following sections describe the validation options.\n\n### Cyclical JavaScript objects\n\nWhile they don't occur in proper JSON, JavaScript does support self-referencing objects. Any of the above calls support a checkRecursive option. If true, tv4 will handle self-referencing objects properly - this slows down validation slightly, but that's better than a hanging script.\n\nConsider this data, notice how both `a` and `b` refer to each other:\n\n```javascript\nvar a = {};\nvar b = { a: a };\na.b = b;\nvar aSchema = { properties: { b: { $ref: 'bSchema' }}};\nvar bSchema = { properties: { a: { $ref: 'aSchema' }}};\ntv4.addSchema('aSchema', aSchema);\ntv4.addSchema('bSchema', bSchema);\n```\n\nBy default, this causes the validation methods to throw a \"too much recursion\" error.\n\nTo enable support for self-referencing objects, set the checkRecursive option to `true`:\n\n```javascript\ntv4.validate(a, aSchema, {checkRecursive: true});\ntv4.validate(a, schema, asynchronousFunction, {checkRecursive: true});\n\ntv4.validateResult(a, aSchema, {checkRecursive: true});\ntv4.validateMultiple(a, aSchema, {checkRecursive: true});\n```\n\n### Properties not defined in the schema\n\nAn object's schema may include an additionalProperties setting. When additionalProperties is set to `false`, objects will fail validation if they include properties that are not defined in the schema.\n\nYou can enforce this behavior for all object schema by setting tv4's banUnknownProperties option to `true`:\n\n```javascript\ntv4.validate(data, schema, {banUnknownProperties: true});\ntv4.validate(data, schema, asynchronousFunction, {banUnknownProperties: true});\n\ntv4.validateResult(data, schema, {banUnknownProperties: true});\ntv4.validateMultiple(data, schema, {banUnknownProperties: true});\n```\n\n### Inherited properties\n\nBy default, tv4 does not validate an object's inherited properties, which are ignored when you convert an object to JSON. This behavior differs from tv4 1.0.16 and earlier, which always validated inherited properties.\n\nTo validate inherited properties, set tv4's checkInheritedProperties option to `true`:\n\n```javascript\ntv4.validate(data, schema, {checkInheritedProperties: true});\ntv4.validate(data, schema, asynchronousFunction, {checkInheritedProperties: true});\n\ntv4.validateResult(data, schema, {checkInheritedProperties: true});\ntv4.validateMultiple(data, schema, {checkInheritedProperties: true});\n```\n\n### Non-enumerable properties\n\nBy default, tv4 does not validate an object's own non-enumerable properties, which are ignored when you convert an object to JSON. This behavior differs from tv4 1.0.16 and earlier, which always validated an object's own non-enumerable properties.\n\nTo validate an object's own non-enumerable properties, set tv4's checkNonEnumerableProperties option to `true`:\n\n```javascript\ntv4.validate(data, schema, {checkNonEnumerableProperties: true});\ntv4.validate(data, schema, asynchronousFunction, {checkNonEnumerableProperties: true});\n\ntv4.validateResult(data, schema, {checkNonEnumerableProperties: true});\ntv4.validateMultiple(data, schema, {checkNonEnumerableProperties: true});\n```\n\n## API\n\nThere are additional api commands available for more complex use-cases:\n\n##### addSchema(uri, schema)\nPre-register a schema for reference by other schema and synchronous validation.\n\n````js\ntv4.addSchema('http://example.com/schema', { ... });\n````\n\n* `uri` the uri to identify this schema.\n* `schema` the schema object.\n\nSchemas that have their `id` property set can be added directly.\n\n````js\ntv4.addSchema({ ... });\n````\n\n##### getSchema(uri)\n\nReturn a schema from the cache.\n\n* `uri` the uri of the schema (may contain a `#` fragment)\n\n````js\nvar schema = tv4.getSchema('http://example.com/schema');\n````\n\n##### getSchemaMap()\n\nReturn a shallow copy of the schema cache, mapping schema document URIs to schema objects.\n\n````\nvar map = tv4.getSchemaMap();\n\nvar schema = map[uri];\n````\n\n##### getSchemaUris(filter)\n\nReturn an Array with known schema document URIs.\n\n* `filter` optional RegExp to filter URIs\n\n````\nvar arr = tv4.getSchemaUris();\n\n// optional filter using a RegExp\nvar arr = tv4.getSchemaUris(/^https?://example.com/);\n````\n\n##### getMissingUris(filter)\n\nReturn an Array with schema document URIs that are used as `$ref` in known schemas but which currently have no associated schema data.\n\nUse this in combination with `tv4.addSchema(uri, schema)` to preload the cache for complete synchronous validation with.\n\n* `filter` optional RegExp to filter URIs\n\n````\nvar arr = tv4.getMissingUris();\n\n// optional filter using a RegExp\nvar arr = tv4.getMissingUris(/^https?://example.com/);\n````\n\n##### dropSchemas()\n\nDrop all known schema document URIs from the cache.\n\n````\ntv4.dropSchemas();\n````\n\n##### freshApi()\n\nReturn a new tv4 instance with no shared state.\n\n````\nvar otherTV4 = tv4.freshApi();\n````\n\n##### reset()\n\nManually reset validation status from the simple `tv4.validate(data, schema)`. Although tv4 will self reset on each validation there are some implementation scenarios where this is useful.\n\n````\ntv4.reset();\n````\n\n##### language(code)\n\nSelect the language map used for reporting.\n\n* `code` is a language code, like `'en'` or `'en-gb'`\n\n````\ntv4.language('en-gb');\n````\n\n##### addLanguage(code, map)\n\nAdd a new language map for selection by `tv4.language(code)`\n\n* `code` is new language code\n* `map` is an object mapping error IDs or constant names (e.g. `103` or `\"NUMBER_MAXIMUM\"`) to language strings.\n\n````\ntv4.addLanguage('fr', { ... });\n\n// select for use\ntv4.language('fr')\n````\n\n##### addFormat(format, validationFunction)\n\nAdd a custom format validator. (There are no built-in format validators.)\n\n* `format` is a string, corresponding to the `\"format\"` value in schemas.\n* `validationFunction` is a function that either returns:\n * `null` (meaning no error)\n * an error string (explaining the reason for failure)\n\n````\ntv4.addFormat('decimal-digits', function (data, schema) {\n\tif (typeof data === 'string' && !/^[0-9]+$/.test(data)) {\n\t\treturn null;\n\t}\n\treturn \"must be string of decimal digits\";\n});\n````\n\nAlternatively, multiple formats can be added at the same time using an object:\n````\ntv4.addFormat({\n\t'my-format': function () {...},\n\t'other-format': function () {...}\n});\n````\n\n## Demos\n\n### Basic usage\n
    \n
    \nvar schema = {\n\t\"items\": {\n\t\t\"type\": \"boolean\"\n\t}\n};\nvar data1 = [true, false];\nvar data2 = [true, 123];\n\nalert(\"data 1: \" + tv4.validate(data1, schema)); // true\nalert(\"data 2: \" + tv4.validate(data2, schema)); // false\nalert(\"data 2 error: \" + JSON.stringify(tv4.error, null, 4));\n
    \n
    \n\n### Use of $ref\n
    \n
    \nvar schema = {\n\t\"type\": \"array\",\n\t\"items\": {\"$ref\": \"#\"}\n};\nvar data1 = [[], [[]]];\nvar data2 = [[], [true, []]];\n\nalert(\"data 1: \" + tv4.validate(data1, schema)); // true\nalert(\"data 2: \" + tv4.validate(data2, schema)); // false\n
    \n
    \n\n### Missing schema\n
    \n
    \nvar schema = {\n\t\"type\": \"array\",\n\t\"items\": {\"$ref\": \"http://example.com/schema\" }\n};\nvar data = [1, 2, 3];\n\nalert(\"Valid: \" + tv4.validate(data, schema)); // true\nalert(\"Missing schemas: \" + JSON.stringify(tv4.missing));\n
    \n
    \n\n### Referencing remote schema\n
    \n
    \ntv4.addSchema(\"http://example.com/schema\", {\n\t\"definitions\": {\n\t\t\"arrayItem\": {\"type\": \"boolean\"}\n\t}\n});\nvar schema = {\n\t\"type\": \"array\",\n\t\"items\": {\"$ref\": \"http://example.com/schema#/definitions/arrayItem\" }\n};\nvar data1 = [true, false, true];\nvar data2 = [1, 2, 3];\n\nalert(\"data 1: \" + tv4.validate(data1, schema)); // true\nalert(\"data 2: \" + tv4.validate(data2, schema)); // false\n
    \n
    \n\n## Supported platforms\n\n* Node.js\n* All modern browsers\n* IE >= 7\n\n## Installation\n\nYou can manually download [`tv4.js`](https://raw.github.com/geraintluff/tv4/master/tv4.js) or the minified [`tv4.min.js`](https://raw.github.com/geraintluff/tv4/master/tv4.min.js) and include it in your html to create the global `tv4` variable.\n\nAlternately use it as a CommonJS module:\n\n````js\nvar tv4 = require('tv4');\n````\n\n#### npm\n\n````\n$ npm install tv4\n````\n\n#### bower\n\n````\n$ bower install tv4\n````\n\n#### component.io\n\n````\n$ component install geraintluff/tv4\n````\n\n## Build and test\n\nYou can rebuild and run the node and browser tests using node.js and [grunt](http://http://gruntjs.com/):\n\nMake sure you have the global grunt cli command:\n````\n$ npm install grunt-cli -g\n````\n\nClone the git repos, open a shell in the root folder and install the development dependencies:\n\n````\n$ npm install\n````\n\nRebuild and run the tests:\n````\n$ grunt\n````\n\nIt will run a build and display one Spec-style report for the node.js and two Dot-style reports for both the plain and minified browser tests (via phantomJS). You can also use your own browser to manually run the suites by opening [`test/index.html`](http://geraintluff.github.io/tv4/test/index.html) and [`test/index-min.html`](http://geraintluff.github.io/tv4/test/index-min.html).\n\n## Contributing\n\nPull-requests for fixes and expansions are welcome. Edit the partial files in `/source` and add your tests in a suitable suite or folder under `/test/tests` and run `grunt` to rebuild and run the test suite. Try to maintain an idiomatic coding style and add tests for any new features. It is recommend to discuss big changes in an Issue.\n\n## Packages using tv4\n\n* [chai-json-schema](http://chaijs.com/plugins/chai-json-schema) is a [Chai Assertion Library](http://chaijs.com) plugin to assert values against json-schema.\n* [grunt-tv4](http://www.github.com/Bartvds/grunt-tv4) is a plugin for [Grunt](http://http://gruntjs.com/) that uses tv4 to bulk validate json files.\n\n## License\n\nThe code is available as \"public domain\", meaning that it is completely free to use, without any restrictions at all. Read the full license [here](http://geraintluff.github.com/tv4/LICENSE.txt).\n\nIt's also available under an [MIT license](http://jsonary.com/LICENSE.txt).\n", + "readmeFilename": "README.md", + "bugs": { + "url": "https://github.com/geraintluff/tv4/issues" + }, + "homepage": "https://github.com/geraintluff/tv4", + "_id": "tv4@1.0.15", + "_shasum": "081e6823ee51d67aabe5b92ea3a00804902be155", + "_from": "https://github.com/hegemonic/tv4/tarball/own-properties", + "_resolved": "https://github.com/hegemonic/tv4/tarball/own-properties" +} diff --git a/third_party/jsdoc/node_modules/tv4/tv4.async-jquery.js b/third_party/jsdoc/node_modules/tv4/tv4.async-jquery.js new file mode 100644 index 0000000000..761f232ad7 --- /dev/null +++ b/third_party/jsdoc/node_modules/tv4/tv4.async-jquery.js @@ -0,0 +1,34 @@ +// Provides support for asynchronous validation (fetching schemas) using jQuery +// Callback is optional third argument to tv4.validate() - if not present, synchronous operation +// callback(result, error); +if (typeof (tv4.asyncValidate) === 'undefined') { + tv4.syncValidate = tv4.validate; + tv4.validate = function (data, schema, callback, checkRecursive, banUnknownProperties) { + if (typeof (callback) === 'undefined') { + return this.syncValidate(data, schema, checkRecursive, banUnknownProperties); + } else { + return this.asyncValidate(data, schema, callback, checkRecursive, banUnknownProperties); + } + }; + tv4.asyncValidate = function (data, schema, callback, checkRecursive, banUnknownProperties) { + var $ = jQuery; + var result = tv4.validate(data, schema, checkRecursive, banUnknownProperties); + if (!tv4.missing.length) { + callback(result, tv4.error); + } else { + // Make a request for each missing schema + var missingSchemas = $.map(tv4.missing, function (schemaUri) { + return $.getJSON(schemaUri).success(function (fetchedSchema) { + tv4.addSchema(schemaUri, fetchedSchema); + }).error(function () { + // If there's an error, just use an empty schema + tv4.addSchema(schemaUri, {}); + }); + }); + // When all requests done, try again + $.when.apply($, missingSchemas).done(function () { + var result = tv4.asyncValidate(data, schema, callback, checkRecursive, banUnknownProperties); + }); + } + }; +} diff --git a/third_party/jsdoc/node_modules/tv4/tv4.js b/third_party/jsdoc/node_modules/tv4/tv4.js new file mode 100644 index 0000000000..37a36241f1 --- /dev/null +++ b/third_party/jsdoc/node_modules/tv4/tv4.js @@ -0,0 +1,1388 @@ +/* +Author: Geraint Luff and others +Year: 2013 + +This code is released into the "public domain" by its author(s). Anybody may use, alter and distribute the code without restriction. The author makes no guarantees, and takes no liability of any kind for use of this code. + +If you find a bug or make an improvement, it would be courteous to let the author know, but it is not compulsory. +*/ +(function (global) { +'use strict'; + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys?redirectlocale=en-US&redirectslug=JavaScript%2FReference%2FGlobal_Objects%2FObject%2Fkeys +if (!Object.keys) { + Object.keys = (function () { + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function (obj) { + if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = []; + + for (var prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + + if (hasDontEnumBug) { + for (var i=0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + }; + })(); +} +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create +if (!Object.create) { + Object.create = (function(){ + function F(){} + + return function(o){ + if (arguments.length !== 1) { + throw new Error('Object.create implementation only accepts one parameter.'); + } + F.prototype = o; + return new F(); + }; + })(); +} +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray?redirectlocale=en-US&redirectslug=JavaScript%2FReference%2FGlobal_Objects%2FArray%2FisArray +if(!Array.isArray) { + Array.isArray = function (vArg) { + return Object.prototype.toString.call(vArg) === "[object Array]"; + }; +} +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf?redirectlocale=en-US&redirectslug=JavaScript%2FReference%2FGlobal_Objects%2FArray%2FindexOf +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { + if (this === null) { + throw new TypeError(); + } + var t = Object(this); + var len = t.length >>> 0; + + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 1) { + n = Number(arguments[1]); + if (n !== n) { // shortcut for verifying if it's NaN + n = 0; + } else if (n !== 0 && n !== Infinity && n !== -Infinity) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + }; +} + +// Grungey Object.isFrozen hack +if (!Object.isFrozen) { + Object.isFrozen = function (obj) { + var key = "tv4_test_frozen_key"; + while (obj.hasOwnProperty(key)) { + key += Math.random(); + } + try { + obj[key] = true; + delete obj[key]; + return false; + } catch (e) { + return true; + } + }; +} +var ValidatorContext = function ValidatorContext(parent, collectMultiple, errorMessages, options) { + this.missing = []; + this.missingMap = {}; + this.formatValidators = parent ? Object.create(parent.formatValidators) : {}; + this.schemas = parent ? Object.create(parent.schemas) : {}; + this.collectMultiple = collectMultiple; + this.errors = []; + this.handleError = collectMultiple ? this.collectError : this.returnError; + options = options || {}; + if (options.checkRecursive) { + this.checkRecursive = true; + this.scanned = []; + this.scannedFrozen = []; + this.scannedFrozenSchemas = []; + this.scannedFrozenValidationErrors = []; + this.validatedSchemasKey = 'tv4_validation_id'; + this.validationErrorsKey = 'tv4_validation_errors_id'; + } + if (options.banUnknownProperties) { + this.trackUnknownProperties = true; + this.knownPropertyPaths = {}; + this.unknownPropertyPaths = {}; + } + if (options.checkInheritedProperties) { + this.checkInheritedProperties = true; + } + if (options.checkNonEnumerableProperties) { + this.checkNonEnumerableProperties = true; + } + this.errorMessages = errorMessages; +}; +ValidatorContext.prototype.createError = function (code, messageParams, dataPath, schemaPath, subErrors) { + var messageTemplate = this.errorMessages[code] || ErrorMessagesDefault[code]; + if (typeof messageTemplate !== 'string') { + return new ValidationError(code, "Unknown error code " + code + ": " + JSON.stringify(messageParams), dataPath, schemaPath, subErrors); + } + // Adapted from Crockford's supplant() + var message = messageTemplate.replace(/\{([^{}]*)\}/g, function (whole, varName) { + var subValue = messageParams[varName]; + return typeof subValue === 'string' || typeof subValue === 'number' ? subValue : whole; + }); + return new ValidationError(code, message, dataPath, schemaPath, subErrors); +}; +ValidatorContext.prototype.returnError = function (error) { + return error; +}; +ValidatorContext.prototype.collectError = function (error) { + if (error) { + this.errors.push(error); + } + return null; +}; +ValidatorContext.prototype.prefixErrors = function (startIndex, dataPath, schemaPath) { + for (var i = startIndex; i < this.errors.length; i++) { + this.errors[i] = this.errors[i].prefixWith(dataPath, schemaPath); + } + return this; +}; +ValidatorContext.prototype.banUnknownProperties = function () { + if (this.trackUnknownProperties) { + var unknownPaths = Object.keys(this.unknownPropertyPaths); + for (var i = 0; i < unknownPaths.length; i++) { + var unknownPath = unknownPaths[i]; + var error = this.createError(ErrorCodes.UNKNOWN_PROPERTY, {path: unknownPath}, unknownPath, ""); + var result = this.handleError(error); + if (result) { + return result; + } + } + } + return null; +}; + +ValidatorContext.prototype.addFormat = function (format, validator) { + if (typeof format === 'object') { + var formatKeys = Object.keys(format); + for (var i = 0; i < formatKeys.length; i++) { + var key = formatKeys[i]; + this.addFormat(key, format[key]); + } + return this; + } + this.formatValidators[format] = validator; +}; +ValidatorContext.prototype.resolveRefs = function (schema, urlHistory) { + if (schema['$ref'] !== undefined) { + urlHistory = urlHistory || {}; + if (urlHistory[schema['$ref']]) { + return this.createError(ErrorCodes.CIRCULAR_REFERENCE, {urls: Object.keys(urlHistory).join(', ')}, '', ''); + } + urlHistory[schema['$ref']] = true; + schema = this.getSchema(schema['$ref'], urlHistory); + } + return schema; +}; +ValidatorContext.prototype.getSchema = function (url, urlHistory) { + var schema; + if (this.schemas[url] !== undefined) { + schema = this.schemas[url]; + return this.resolveRefs(schema, urlHistory); + } + var baseUrl = url; + var fragment = ""; + if (url.indexOf('#') !== -1) { + fragment = url.substring(url.indexOf("#") + 1); + baseUrl = url.substring(0, url.indexOf("#")); + } + if (typeof this.schemas[baseUrl] === 'object') { + schema = this.schemas[baseUrl]; + var pointerPath = decodeURIComponent(fragment); + if (pointerPath === "") { + return this.resolveRefs(schema, urlHistory); + } else if (pointerPath.charAt(0) !== "/") { + return undefined; + } + var parts = pointerPath.split("/").slice(1); + for (var i = 0; i < parts.length; i++) { + var component = parts[i].replace(/~1/g, "/").replace(/~0/g, "~"); + if (schema[component] === undefined) { + schema = undefined; + break; + } + schema = schema[component]; + } + if (schema !== undefined) { + return this.resolveRefs(schema, urlHistory); + } + } + if (this.missing[baseUrl] === undefined) { + this.missing.push(baseUrl); + this.missing[baseUrl] = baseUrl; + this.missingMap[baseUrl] = baseUrl; + } +}; +ValidatorContext.prototype.searchSchemas = function (schema, url) { + if (schema && typeof schema === "object") { + if (typeof schema.id === "string") { + if (isTrustedUrl(url, schema.id)) { + if (this.schemas[schema.id] === undefined) { + this.schemas[schema.id] = schema; + } + } + } + var schemaKeys = Object.keys(schema); + for (var i = 0; i < schemaKeys.length; i++) { + var key = schemaKeys[i]; + if (key !== "enum") { + if (typeof schema[key] === "object") { + this.searchSchemas(schema[key], url); + } else if (key === "$ref") { + var uri = getDocumentUri(schema[key]); + if (uri && this.schemas[uri] === undefined && this.missingMap[uri] === undefined) { + this.missingMap[uri] = uri; + } + } + } + } + } +}; +ValidatorContext.prototype.addSchema = function (url, schema) { + //overload + if (typeof url !== 'string' || typeof schema === 'undefined') { + if (typeof url === 'object' && typeof url.id === 'string') { + schema = url; + url = schema.id; + } + else { + return; + } + } + if (url = getDocumentUri(url) + "#") { + // Remove empty fragment + url = getDocumentUri(url); + } + this.schemas[url] = schema; + delete this.missingMap[url]; + normSchema(schema, url); + this.searchSchemas(schema, url); +}; + +ValidatorContext.prototype.getSchemaMap = function () { + var map = {}; + var schemaKeys = Object.keys(this.schemas); + for (var i = 0; i < schemaKeys.length; i++) { + var key = schemaKeys[i]; + map[key] = this.schemas[key]; + } + return map; +}; + +ValidatorContext.prototype.getSchemaUris = function (filterRegExp) { + var list = []; + var schemaKeys = Object.keys(this.schemas); + for (var i = 0; i < schemaKeys.length; i++) { + var key = schemaKeys[i]; + if (!filterRegExp || filterRegExp.test(key)) { + list.push(key); + } + } + return list; +}; + +ValidatorContext.prototype.getMissingUris = function (filterRegExp) { + var list = []; + var missingKeys = Object.keys(this.missingMap); + for (var i = 0; i < missingKeys.length; i++) { + var key = missingKeys[i]; + if (!filterRegExp || filterRegExp.test(key)) { + list.push(key); + } + } + return list; +}; + +ValidatorContext.prototype.dropSchemas = function () { + this.schemas = {}; + this.reset(); +}; +ValidatorContext.prototype.reset = function () { + this.missing = []; + this.missingMap = {}; + this.errors = []; +}; + +ValidatorContext.prototype.validateAll = function (data, schema, dataPathParts, schemaPathParts, dataPointerPath) { + var topLevel; + schema = this.resolveRefs(schema); + if (!schema) { + return null; + } else if (schema instanceof ValidationError) { + this.errors.push(schema); + return schema; + } + + var startErrorCount = this.errors.length; + var frozenIndex, scannedFrozenSchemaIndex = null, scannedSchemasIndex = null; + if (this.checkRecursive && (typeof data) === 'object') { + topLevel = !this.scanned.length; + if (data[this.validatedSchemasKey]) { + var schemaIndex = data[this.validatedSchemasKey].indexOf(schema); + if (schemaIndex !== -1) { + this.errors = this.errors.concat(data[this.validationErrorsKey][schemaIndex]); + return null; + } + } + if (Object.isFrozen(data)) { + frozenIndex = this.scannedFrozen.indexOf(data); + if (frozenIndex !== -1) { + var frozenSchemaIndex = this.scannedFrozenSchemas[frozenIndex].indexOf(schema); + if (frozenSchemaIndex !== -1) { + this.errors = this.errors.concat(this.scannedFrozenValidationErrors[frozenIndex][frozenSchemaIndex]); + return null; + } + } + } + this.scanned.push(data); + if (Object.isFrozen(data)) { + if (frozenIndex === -1) { + frozenIndex = this.scannedFrozen.length; + this.scannedFrozen.push(data); + this.scannedFrozenSchemas.push([]); + } + scannedFrozenSchemaIndex = this.scannedFrozenSchemas[frozenIndex].length; + this.scannedFrozenSchemas[frozenIndex][scannedFrozenSchemaIndex] = schema; + this.scannedFrozenValidationErrors[frozenIndex][scannedFrozenSchemaIndex] = []; + } else { + if (!data[this.validatedSchemasKey]) { + try { + Object.defineProperty(data, this.validatedSchemasKey, { + value: [], + configurable: true + }); + Object.defineProperty(data, this.validationErrorsKey, { + value: [], + configurable: true + }); + } catch (e) { + //IE 7/8 workaround + data[this.validatedSchemasKey] = []; + data[this.validationErrorsKey] = []; + } + } + scannedSchemasIndex = data[this.validatedSchemasKey].length; + data[this.validatedSchemasKey][scannedSchemasIndex] = schema; + data[this.validationErrorsKey][scannedSchemasIndex] = []; + } + } + + var errorCount = this.errors.length; + var error = this.validateBasic(data, schema, dataPointerPath) + || this.validateNumeric(data, schema, dataPointerPath) + || this.validateString(data, schema, dataPointerPath) + || this.validateArray(data, schema, dataPointerPath) + || this.validateObject(data, schema, dataPointerPath) + || this.validateCombinations(data, schema, dataPointerPath) + || this.validateFormat(data, schema, dataPointerPath) + || null; + + if (topLevel) { + while (this.scanned.length) { + var item = this.scanned.pop(); + delete item[this.validatedSchemasKey]; + } + this.scannedFrozen = []; + this.scannedFrozenSchemas = []; + } + + if (error || errorCount !== this.errors.length) { + while ((dataPathParts && dataPathParts.length) || (schemaPathParts && schemaPathParts.length)) { + var dataPart = (dataPathParts && dataPathParts.length) ? "" + dataPathParts.pop() : null; + var schemaPart = (schemaPathParts && schemaPathParts.length) ? "" + schemaPathParts.pop() : null; + if (error) { + error = error.prefixWith(dataPart, schemaPart); + } + this.prefixErrors(errorCount, dataPart, schemaPart); + } + } + + if (scannedFrozenSchemaIndex !== null) { + this.scannedFrozenValidationErrors[frozenIndex][scannedFrozenSchemaIndex] = this.errors.slice(startErrorCount); + } else if (scannedSchemasIndex !== null) { + data[this.validationErrorsKey][scannedSchemasIndex] = this.errors.slice(startErrorCount); + } + + return this.handleError(error); +}; +ValidatorContext.prototype.validateFormat = function (data, schema) { + if (typeof schema.format !== 'string' || !this.formatValidators[schema.format]) { + return null; + } + var errorMessage = this.formatValidators[schema.format].call(null, data, schema); + if (typeof errorMessage === 'string' || typeof errorMessage === 'number') { + return this.createError(ErrorCodes.FORMAT_CUSTOM, {message: errorMessage}).prefixWith(null, "format"); + } else if (errorMessage && typeof errorMessage === 'object') { + return this.createError(ErrorCodes.FORMAT_CUSTOM, {message: errorMessage.message || "?"}, errorMessage.dataPath || null, errorMessage.schemaPath || "/format"); + } + return null; +}; + +function recursiveCompare(A, B) { + if (A === B) { + return true; + } + if (typeof A === "object" && typeof B === "object") { + if (Array.isArray(A) !== Array.isArray(B)) { + return false; + } else if (Array.isArray(A)) { + if (A.length !== B.length) { + return false; + } + for (var i = 0; i < A.length; i++) { + if (!recursiveCompare(A[i], B[i])) { + return false; + } + } + } else { + var key; + for (key in A) { + if (B[key] === undefined && A[key] !== undefined) { + return false; + } + } + for (key in B) { + if (A[key] === undefined && B[key] !== undefined) { + return false; + } + } + for (key in A) { + if (!recursiveCompare(A[key], B[key])) { + return false; + } + } + } + return true; + } + return false; +} + +ValidatorContext.prototype.validateBasic = function validateBasic(data, schema, dataPointerPath) { + var error; + if (error = this.validateType(data, schema, dataPointerPath)) { + return error.prefixWith(null, "type"); + } + if (error = this.validateEnum(data, schema, dataPointerPath)) { + return error.prefixWith(null, "type"); + } + return null; +}; + +ValidatorContext.prototype.validateType = function validateType(data, schema) { + if (schema.type === undefined) { + return null; + } + var dataType = typeof data; + if (data === null) { + dataType = "null"; + } else if (Array.isArray(data)) { + dataType = "array"; + } + var allowedTypes = schema.type; + if (typeof allowedTypes !== "object") { + allowedTypes = [allowedTypes]; + } + + for (var i = 0; i < allowedTypes.length; i++) { + var type = allowedTypes[i]; + if (type === dataType || (type === "integer" && dataType === "number" && (data % 1 === 0))) { + return null; + } + } + return this.createError(ErrorCodes.INVALID_TYPE, {type: dataType, expected: allowedTypes.join("/")}); +}; + +ValidatorContext.prototype.validateEnum = function validateEnum(data, schema) { + if (schema["enum"] === undefined) { + return null; + } + for (var i = 0; i < schema["enum"].length; i++) { + var enumVal = schema["enum"][i]; + if (recursiveCompare(data, enumVal)) { + return null; + } + } + return this.createError(ErrorCodes.ENUM_MISMATCH, {value: (typeof JSON !== 'undefined') ? JSON.stringify(data) : data}); +}; + +ValidatorContext.prototype.validateNumeric = function validateNumeric(data, schema, dataPointerPath) { + return this.validateMultipleOf(data, schema, dataPointerPath) + || this.validateMinMax(data, schema, dataPointerPath) + || null; +}; + +ValidatorContext.prototype.validateMultipleOf = function validateMultipleOf(data, schema) { + var multipleOf = schema.multipleOf || schema.divisibleBy; + if (multipleOf === undefined) { + return null; + } + if (typeof data === "number") { + if (data % multipleOf !== 0) { + return this.createError(ErrorCodes.NUMBER_MULTIPLE_OF, {value: data, multipleOf: multipleOf}); + } + } + return null; +}; + +ValidatorContext.prototype.validateMinMax = function validateMinMax(data, schema) { + if (typeof data !== "number") { + return null; + } + if (schema.minimum !== undefined) { + if (data < schema.minimum) { + return this.createError(ErrorCodes.NUMBER_MINIMUM, {value: data, minimum: schema.minimum}).prefixWith(null, "minimum"); + } + if (schema.exclusiveMinimum && data === schema.minimum) { + return this.createError(ErrorCodes.NUMBER_MINIMUM_EXCLUSIVE, {value: data, minimum: schema.minimum}).prefixWith(null, "exclusiveMinimum"); + } + } + if (schema.maximum !== undefined) { + if (data > schema.maximum) { + return this.createError(ErrorCodes.NUMBER_MAXIMUM, {value: data, maximum: schema.maximum}).prefixWith(null, "maximum"); + } + if (schema.exclusiveMaximum && data === schema.maximum) { + return this.createError(ErrorCodes.NUMBER_MAXIMUM_EXCLUSIVE, {value: data, maximum: schema.maximum}).prefixWith(null, "exclusiveMaximum"); + } + } + return null; +}; + +ValidatorContext.prototype.validateString = function validateString(data, schema, dataPointerPath) { + return this.validateStringLength(data, schema, dataPointerPath) + || this.validateStringPattern(data, schema, dataPointerPath) + || null; +}; + +ValidatorContext.prototype.validateStringLength = function validateStringLength(data, schema) { + if (typeof data !== "string") { + return null; + } + if (schema.minLength !== undefined) { + if (data.length < schema.minLength) { + return this.createError(ErrorCodes.STRING_LENGTH_SHORT, {length: data.length, minimum: schema.minLength}).prefixWith(null, "minLength"); + } + } + if (schema.maxLength !== undefined) { + if (data.length > schema.maxLength) { + return this.createError(ErrorCodes.STRING_LENGTH_LONG, {length: data.length, maximum: schema.maxLength}).prefixWith(null, "maxLength"); + } + } + return null; +}; + +ValidatorContext.prototype.validateStringPattern = function validateStringPattern(data, schema) { + if (typeof data !== "string" || schema.pattern === undefined) { + return null; + } + var regexp = new RegExp(schema.pattern); + if (!regexp.test(data)) { + return this.createError(ErrorCodes.STRING_PATTERN, {pattern: schema.pattern}).prefixWith(null, "pattern"); + } + return null; +}; +ValidatorContext.prototype.validateArray = function validateArray(data, schema, dataPointerPath) { + if (!Array.isArray(data)) { + return null; + } + return this.validateArrayLength(data, schema, dataPointerPath) + || this.validateArrayUniqueItems(data, schema, dataPointerPath) + || this.validateArrayItems(data, schema, dataPointerPath) + || null; +}; + +ValidatorContext.prototype.validateArrayLength = function validateArrayLength(data, schema) { + var error; + if (schema.minItems !== undefined) { + if (data.length < schema.minItems) { + error = (this.createError(ErrorCodes.ARRAY_LENGTH_SHORT, {length: data.length, minimum: schema.minItems})).prefixWith(null, "minItems"); + if (this.handleError(error)) { + return error; + } + } + } + if (schema.maxItems !== undefined) { + if (data.length > schema.maxItems) { + error = (this.createError(ErrorCodes.ARRAY_LENGTH_LONG, {length: data.length, maximum: schema.maxItems})).prefixWith(null, "maxItems"); + if (this.handleError(error)) { + return error; + } + } + } + return null; +}; + +ValidatorContext.prototype.validateArrayUniqueItems = function validateArrayUniqueItems(data, schema) { + if (schema.uniqueItems) { + for (var i = 0; i < data.length; i++) { + for (var j = i + 1; j < data.length; j++) { + if (recursiveCompare(data[i], data[j])) { + var error = (this.createError(ErrorCodes.ARRAY_UNIQUE, {match1: i, match2: j})).prefixWith(null, "uniqueItems"); + if (this.handleError(error)) { + return error; + } + } + } + } + } + return null; +}; + +ValidatorContext.prototype.validateArrayItems = function validateArrayItems(data, schema, dataPointerPath) { + if (schema.items === undefined) { + return null; + } + var error, i; + if (Array.isArray(schema.items)) { + for (i = 0; i < data.length; i++) { + if (i < schema.items.length) { + if (error = this.validateAll(data[i], schema.items[i], [i], ["items", i], dataPointerPath + "/" + i)) { + return error; + } + } else if (schema.additionalItems !== undefined) { + if (typeof schema.additionalItems === "boolean") { + if (!schema.additionalItems) { + error = (this.createError(ErrorCodes.ARRAY_ADDITIONAL_ITEMS, {})).prefixWith("" + i, "additionalItems"); + if (this.handleError(error)) { + return error; + } + } + } else if (error = this.validateAll(data[i], schema.additionalItems, [i], ["additionalItems"], dataPointerPath + "/" + i)) { + return error; + } + } + } + } else { + for (i = 0; i < data.length; i++) { + if (error = this.validateAll(data[i], schema.items, [i], ["items"], dataPointerPath + "/" + i)) { + return error; + } + } + } + return null; +}; + +ValidatorContext.prototype.validateObject = function validateObject(data, schema, dataPointerPath) { + if (typeof data !== "object" || data === null || Array.isArray(data)) { + return null; + } + return this.validateObjectMinMaxProperties(data, schema, dataPointerPath) + || this.validateObjectRequiredProperties(data, schema, dataPointerPath) + || this.validateObjectProperties(data, schema, dataPointerPath) + || this.validateObjectDependencies(data, schema, dataPointerPath) + || null; +}; + +ValidatorContext.prototype.validateObjectMinMaxProperties = function validateObjectMinMaxProperties(data, schema) { + var keys = Object.keys(data); + var error; + if (schema.minProperties !== undefined) { + if (keys.length < schema.minProperties) { + error = this.createError(ErrorCodes.OBJECT_PROPERTIES_MINIMUM, {propertyCount: keys.length, minimum: schema.minProperties}).prefixWith(null, "minProperties"); + if (this.handleError(error)) { + return error; + } + } + } + if (schema.maxProperties !== undefined) { + if (keys.length > schema.maxProperties) { + error = this.createError(ErrorCodes.OBJECT_PROPERTIES_MAXIMUM, {propertyCount: keys.length, maximum: schema.maxProperties}).prefixWith(null, "maxProperties"); + if (this.handleError(error)) { + return error; + } + } + } + return null; +}; + +ValidatorContext.prototype.validateObjectRequiredProperties = function validateObjectRequiredProperties(data, schema) { + if (schema.required !== undefined) { + for (var i = 0; i < schema.required.length; i++) { + var key = schema.required[i]; + if (data[key] === undefined) { + var error = this.createError(ErrorCodes.OBJECT_REQUIRED, {key: key}).prefixWith(key, "" + i).prefixWith(null, "required"); + if (this.handleError(error)) { + return error; + } + } + } + } + return null; +}; + +function findProperties(obj, checkInheritedProperties, checkNonEnumerableProperties) { + // Start with the object's own enumerable properties + var properties = Object.keys(obj); + if (checkInheritedProperties) { + for (var key in obj) { + if (properties.indexOf(key) === -1) { + properties.push(key); + } + } + } + // Object.getOwnPropertyNames is not available in IE 8 and below (and cannot be polyfilled) + if (checkNonEnumerableProperties && Object.getOwnPropertyNames) { + Object.getOwnPropertyNames(obj).forEach(function (name) { + if (properties.indexOf(name) === -1) { + properties.push(name); + } + }); + } + return properties; +} + +ValidatorContext.prototype.validateObjectProperties = function validateObjectProperties(data, schema, dataPointerPath) { + var error; + var dataKeys = findProperties(data, this.checkInheritedProperties, this.checkNonEnumerableProperties); + for (var i = 0; i < dataKeys.length; i++) { + var key = dataKeys[i]; + var keyPointerPath = dataPointerPath + "/" + key.replace(/~/g, '~0').replace(/\//g, '~1'); + var foundMatch = false; + if (schema.properties !== undefined && schema.properties[key] !== undefined) { + foundMatch = true; + if (error = this.validateAll(data[key], schema.properties[key], [key], ["properties", key], keyPointerPath)) { + return error; + } + } + if (schema.patternProperties !== undefined) { + var patternKeys = Object.keys(schema.patternProperties); + for (var j = 0; j < patternKeys.length; j++) { + var patternKey = patternKeys[j]; + var regexp = new RegExp(patternKey); + if (regexp.test(key)) { + foundMatch = true; + if (error = this.validateAll(data[key], schema.patternProperties[patternKey], [key], ["patternProperties", patternKey], keyPointerPath)) { + return error; + } + } + } + } + if (!foundMatch) { + if (schema.additionalProperties !== undefined) { + if (this.trackUnknownProperties) { + this.knownPropertyPaths[keyPointerPath] = true; + delete this.unknownPropertyPaths[keyPointerPath]; + } + if (typeof schema.additionalProperties === "boolean") { + if (!schema.additionalProperties) { + error = this.createError(ErrorCodes.OBJECT_ADDITIONAL_PROPERTIES, {}).prefixWith(key, "additionalProperties"); + if (this.handleError(error)) { + return error; + } + } + } else { + if (error = this.validateAll(data[key], schema.additionalProperties, [key], ["additionalProperties"], keyPointerPath)) { + return error; + } + } + } else if (this.trackUnknownProperties && !this.knownPropertyPaths[keyPointerPath]) { + this.unknownPropertyPaths[keyPointerPath] = true; + } + } else if (this.trackUnknownProperties) { + this.knownPropertyPaths[keyPointerPath] = true; + delete this.unknownPropertyPaths[keyPointerPath]; + } + } + return null; +}; + +ValidatorContext.prototype.validateObjectDependencies = function validateObjectDependencies(data, schema, dataPointerPath) { + var error; + if (schema.dependencies !== undefined) { + var depKeys = Object.keys(schema.dependencies); + for (var i = 0; i < depKeys.length; i++) { + var depKey = depKeys[i]; + if (data[depKey] !== undefined) { + var dep = schema.dependencies[depKey]; + if (typeof dep === "string") { + if (data[dep] === undefined) { + error = this.createError(ErrorCodes.OBJECT_DEPENDENCY_KEY, {key: depKey, missing: dep}).prefixWith(null, depKey).prefixWith(null, "dependencies"); + if (this.handleError(error)) { + return error; + } + } + } else if (Array.isArray(dep)) { + for (var j = 0; j < dep.length; j++) { + var requiredKey = dep[j]; + if (data[requiredKey] === undefined) { + error = this.createError(ErrorCodes.OBJECT_DEPENDENCY_KEY, {key: depKey, missing: requiredKey}).prefixWith(null, "" + i).prefixWith(null, depKey).prefixWith(null, "dependencies"); + if (this.handleError(error)) { + return error; + } + } + } + } else { + if (error = this.validateAll(data, dep, [], ["dependencies", depKey], dataPointerPath)) { + return error; + } + } + } + } + } + return null; +}; + +ValidatorContext.prototype.validateCombinations = function validateCombinations(data, schema, dataPointerPath) { + return this.validateAllOf(data, schema, dataPointerPath) + || this.validateAnyOf(data, schema, dataPointerPath) + || this.validateOneOf(data, schema, dataPointerPath) + || this.validateNot(data, schema, dataPointerPath) + || null; +}; + +ValidatorContext.prototype.validateAllOf = function validateAllOf(data, schema, dataPointerPath) { + if (schema.allOf === undefined) { + return null; + } + var error; + for (var i = 0; i < schema.allOf.length; i++) { + var subSchema = schema.allOf[i]; + if (error = this.validateAll(data, subSchema, [], ["allOf", i], dataPointerPath)) { + return error; + } + } + return null; +}; + +ValidatorContext.prototype.validateAnyOf = function validateAnyOf(data, schema, dataPointerPath) { + if (schema.anyOf === undefined) { + return null; + } + var errors = []; + var startErrorCount = this.errors.length; + var oldUnknownPropertyPaths, oldKnownPropertyPaths; + if (this.trackUnknownProperties) { + oldUnknownPropertyPaths = this.unknownPropertyPaths; + oldKnownPropertyPaths = this.knownPropertyPaths; + } + var errorAtEnd = true; + for (var i = 0; i < schema.anyOf.length; i++) { + if (this.trackUnknownProperties) { + this.unknownPropertyPaths = {}; + this.knownPropertyPaths = {}; + } + var subSchema = schema.anyOf[i]; + + var errorCount = this.errors.length; + var error = this.validateAll(data, subSchema, [], ["anyOf", i], dataPointerPath); + + if (error === null && errorCount === this.errors.length) { + this.errors = this.errors.slice(0, startErrorCount); + + if (this.trackUnknownProperties) { + var knownKeys = Object.keys(this.knownPropertyPaths); + for (var j = 0; j < knownKeys.length; j++) { + var knownKey = knownKeys[j]; + oldKnownPropertyPaths[knownKey] = true; + delete oldUnknownPropertyPaths[knownKey]; + } + var unknownKeys = Object.keys(this.unknownPropertyPaths); + for (j = 0; j < unknownKeys.length; j++) { + var unknownKey = unknownKeys[j]; + if (!oldKnownPropertyPaths[unknownKey]) { + oldUnknownPropertyPaths[unknownKey] = true; + } + } + // We need to continue looping so we catch all the property definitions, but we don't want to return an error + errorAtEnd = false; + continue; + } + + return null; + } + if (error) { + errors.push(error.prefixWith(null, "" + i).prefixWith(null, "anyOf")); + } + } + if (this.trackUnknownProperties) { + this.unknownPropertyPaths = oldUnknownPropertyPaths; + this.knownPropertyPaths = oldKnownPropertyPaths; + } + if (errorAtEnd) { + errors = errors.concat(this.errors.slice(startErrorCount)); + this.errors = this.errors.slice(0, startErrorCount); + return this.createError(ErrorCodes.ANY_OF_MISSING, {}, "", "/anyOf", errors); + } +}; + +ValidatorContext.prototype.validateOneOf = function validateOneOf(data, schema, dataPointerPath) { + if (schema.oneOf === undefined) { + return null; + } + var validIndex = null; + var errors = []; + var startErrorCount = this.errors.length; + var oldUnknownPropertyPaths, oldKnownPropertyPaths; + if (this.trackUnknownProperties) { + oldUnknownPropertyPaths = this.unknownPropertyPaths; + oldKnownPropertyPaths = this.knownPropertyPaths; + } + for (var i = 0; i < schema.oneOf.length; i++) { + if (this.trackUnknownProperties) { + this.unknownPropertyPaths = {}; + this.knownPropertyPaths = {}; + } + var subSchema = schema.oneOf[i]; + + var errorCount = this.errors.length; + var error = this.validateAll(data, subSchema, [], ["oneOf", i], dataPointerPath); + + if (error === null && errorCount === this.errors.length) { + if (validIndex === null) { + validIndex = i; + } else { + this.errors = this.errors.slice(0, startErrorCount); + return this.createError(ErrorCodes.ONE_OF_MULTIPLE, {index1: validIndex, index2: i}, "", "/oneOf"); + } + if (this.trackUnknownProperties) { + var knownKeys = Object.keys(this.knownPropertyPaths); + for (var j = 0; j < knownKeys.length; j++) { + var knownKey = knownKeys[j]; + oldKnownPropertyPaths[knownKey] = true; + delete oldUnknownPropertyPaths[knownKey]; + } + var unknownKeys = Object.keys(this.unknownPropertyPaths); + for (j = 0; j < unknownKeys.length; j++) { + var unknownKey = unknownKeys[j]; + if (!oldKnownPropertyPaths[unknownKey]) { + oldUnknownPropertyPaths[unknownKey] = true; + } + } + } + } else if (error) { + errors.push(error.prefixWith(null, "" + i).prefixWith(null, "oneOf")); + } + } + if (this.trackUnknownProperties) { + this.unknownPropertyPaths = oldUnknownPropertyPaths; + this.knownPropertyPaths = oldKnownPropertyPaths; + } + if (validIndex === null) { + errors = errors.concat(this.errors.slice(startErrorCount)); + this.errors = this.errors.slice(0, startErrorCount); + return this.createError(ErrorCodes.ONE_OF_MISSING, {}, "", "/oneOf", errors); + } else { + this.errors = this.errors.slice(0, startErrorCount); + } + return null; +}; + +ValidatorContext.prototype.validateNot = function validateNot(data, schema, dataPointerPath) { + if (schema.not === undefined) { + return null; + } + var oldErrorCount = this.errors.length; + var oldUnknownPropertyPaths, oldKnownPropertyPaths; + if (this.trackUnknownProperties) { + oldUnknownPropertyPaths = this.unknownPropertyPaths; + oldKnownPropertyPaths = this.knownPropertyPaths; + this.unknownPropertyPaths = {}; + this.knownPropertyPaths = {}; + } + var error = this.validateAll(data, schema.not, null, null, dataPointerPath); + var notErrors = this.errors.slice(oldErrorCount); + this.errors = this.errors.slice(0, oldErrorCount); + if (this.trackUnknownProperties) { + this.unknownPropertyPaths = oldUnknownPropertyPaths; + this.knownPropertyPaths = oldKnownPropertyPaths; + } + if (error === null && notErrors.length === 0) { + return this.createError(ErrorCodes.NOT_PASSED, {}, "", "/not"); + } + return null; +}; + +// parseURI() and resolveUrl() are from https://gist.github.com/1088850 +// - released as public domain by author ("Yaffle") - see comments on gist + +function parseURI(url) { + var m = String(url).replace(/^\s+|\s+$/g, '').match(/^([^:\/?#]+:)?(\/\/(?:[^:@]*(?::[^:@]*)?@)?(([^:\/?#]*)(?::(\d*))?))?([^?#]*)(\?[^#]*)?(#[\s\S]*)?/); + // authority = '//' + user + ':' + pass '@' + hostname + ':' port + return (m ? { + href : m[0] || '', + protocol : m[1] || '', + authority: m[2] || '', + host : m[3] || '', + hostname : m[4] || '', + port : m[5] || '', + pathname : m[6] || '', + search : m[7] || '', + hash : m[8] || '' + } : null); +} + +function resolveUrl(base, href) {// RFC 3986 + + function removeDotSegments(input) { + var output = []; + input.replace(/^(\.\.?(\/|$))+/, '') + .replace(/\/(\.(\/|$))+/g, '/') + .replace(/\/\.\.$/, '/../') + .replace(/\/?[^\/]*/g, function (p) { + if (p === '/..') { + output.pop(); + } else { + output.push(p); + } + }); + return output.join('').replace(/^\//, input.charAt(0) === '/' ? '/' : ''); + } + + href = parseURI(href || ''); + base = parseURI(base || ''); + + return !href || !base ? null : (href.protocol || base.protocol) + + (href.protocol || href.authority ? href.authority : base.authority) + + removeDotSegments(href.protocol || href.authority || href.pathname.charAt(0) === '/' ? href.pathname : (href.pathname ? ((base.authority && !base.pathname ? '/' : '') + base.pathname.slice(0, base.pathname.lastIndexOf('/') + 1) + href.pathname) : base.pathname)) + + (href.protocol || href.authority || href.pathname ? href.search : (href.search || base.search)) + + href.hash; +} + +function getDocumentUri(uri) { + return uri.split('#')[0]; +} +function normSchema(schema, baseUri) { + if (schema && typeof schema === "object") { + var i; + if (baseUri === undefined) { + baseUri = schema.id; + } else if (typeof schema.id === "string") { + baseUri = resolveUrl(baseUri, schema.id); + schema.id = baseUri; + } + if (Array.isArray(schema)) { + for (i = 0; i < schema.length; i++) { + normSchema(schema[i], baseUri); + } + } else if (typeof schema['$ref'] === "string") { + schema['$ref'] = resolveUrl(baseUri, schema['$ref']); + } else { + var schemaKeys = Object.keys(schema); + for (i = 0; i < schemaKeys.length; i++) { + var key = schemaKeys[i]; + if (key !== "enum") { + normSchema(schema[key], baseUri); + } + } + } + } +} + +var ErrorCodes = { + INVALID_TYPE: 0, + ENUM_MISMATCH: 1, + ANY_OF_MISSING: 10, + ONE_OF_MISSING: 11, + ONE_OF_MULTIPLE: 12, + NOT_PASSED: 13, + // Numeric errors + NUMBER_MULTIPLE_OF: 100, + NUMBER_MINIMUM: 101, + NUMBER_MINIMUM_EXCLUSIVE: 102, + NUMBER_MAXIMUM: 103, + NUMBER_MAXIMUM_EXCLUSIVE: 104, + // String errors + STRING_LENGTH_SHORT: 200, + STRING_LENGTH_LONG: 201, + STRING_PATTERN: 202, + // Object errors + OBJECT_PROPERTIES_MINIMUM: 300, + OBJECT_PROPERTIES_MAXIMUM: 301, + OBJECT_REQUIRED: 302, + OBJECT_ADDITIONAL_PROPERTIES: 303, + OBJECT_DEPENDENCY_KEY: 304, + // Array errors + ARRAY_LENGTH_SHORT: 400, + ARRAY_LENGTH_LONG: 401, + ARRAY_UNIQUE: 402, + ARRAY_ADDITIONAL_ITEMS: 403, + // Format errors + FORMAT_CUSTOM: 500, + // Schema structure + CIRCULAR_REFERENCE: 600, + // Non-standard validation options + UNKNOWN_PROPERTY: 1000 +}; +var ErrorMessagesDefault = { + INVALID_TYPE: "invalid type: {type} (expected {expected})", + ENUM_MISMATCH: "No enum match for: {value}", + ANY_OF_MISSING: "Data does not match any schemas from \"anyOf\"", + ONE_OF_MISSING: "Data does not match any schemas from \"oneOf\"", + ONE_OF_MULTIPLE: "Data is valid against more than one schema from \"oneOf\": indices {index1} and {index2}", + NOT_PASSED: "Data matches schema from \"not\"", + // Numeric errors + NUMBER_MULTIPLE_OF: "Value {value} is not a multiple of {multipleOf}", + NUMBER_MINIMUM: "Value {value} is less than minimum {minimum}", + NUMBER_MINIMUM_EXCLUSIVE: "Value {value} is equal to exclusive minimum {minimum}", + NUMBER_MAXIMUM: "Value {value} is greater than maximum {maximum}", + NUMBER_MAXIMUM_EXCLUSIVE: "Value {value} is equal to exclusive maximum {maximum}", + // String errors + STRING_LENGTH_SHORT: "String is too short ({length} chars), minimum {minimum}", + STRING_LENGTH_LONG: "String is too long ({length} chars), maximum {maximum}", + STRING_PATTERN: "String does not match pattern: {pattern}", + // Object errors + OBJECT_PROPERTIES_MINIMUM: "Too few properties defined ({propertyCount}), minimum {minimum}", + OBJECT_PROPERTIES_MAXIMUM: "Too many properties defined ({propertyCount}), maximum {maximum}", + OBJECT_REQUIRED: "Missing required property: {key}", + OBJECT_ADDITIONAL_PROPERTIES: "Additional properties not allowed", + OBJECT_DEPENDENCY_KEY: "Dependency failed - key must exist: {missing} (due to key: {key})", + // Array errors + ARRAY_LENGTH_SHORT: "Array is too short ({length}), minimum {minimum}", + ARRAY_LENGTH_LONG: "Array is too long ({length}), maximum {maximum}", + ARRAY_UNIQUE: "Array items are not unique (indices {match1} and {match2})", + ARRAY_ADDITIONAL_ITEMS: "Additional items not allowed", + // Format errors + FORMAT_CUSTOM: "Format validation failed ({message})", + // Schema structure + CIRCULAR_REFERENCE: "Circular $refs: {urls}", + // Non-standard validation options + UNKNOWN_PROPERTY: "Unknown property (not in schema)" +}; + +function ValidationError(code, message, dataPath, schemaPath, subErrors) { + Error.call(this); + if (code === undefined) { + throw new Error ("No code supplied for error: "+ message); + } + this.message = message; + this.code = code; + this.dataPath = dataPath || ""; + this.schemaPath = schemaPath || ""; + this.subErrors = subErrors || null; + + var err = new Error(this.message); + this.stack = err.stack || err.stacktrace; + if (!this.stack) { + try { + throw err; + } + catch(err) { + this.stack = err.stack || err.stacktrace; + } + } +} +ValidationError.prototype = Object.create(Error.prototype); +ValidationError.prototype.constructor = ValidationError; +ValidationError.prototype.name = 'ValidationError'; + +ValidationError.prototype.prefixWith = function (dataPrefix, schemaPrefix) { + if (dataPrefix !== null) { + dataPrefix = dataPrefix.replace(/~/g, "~0").replace(/\//g, "~1"); + this.dataPath = "/" + dataPrefix + this.dataPath; + } + if (schemaPrefix !== null) { + schemaPrefix = schemaPrefix.replace(/~/g, "~0").replace(/\//g, "~1"); + this.schemaPath = "/" + schemaPrefix + this.schemaPath; + } + if (this.subErrors !== null) { + for (var i = 0; i < this.subErrors.length; i++) { + this.subErrors[i].prefixWith(dataPrefix, schemaPrefix); + } + } + return this; +}; + +function isTrustedUrl(baseUrl, testUrl) { + if(testUrl.substring(0, baseUrl.length) === baseUrl){ + var remainder = testUrl.substring(baseUrl.length); + if ((testUrl.length > 0 && testUrl.charAt(baseUrl.length - 1) === "/") + || remainder.charAt(0) === "#" + || remainder.charAt(0) === "?") { + return true; + } + } + return false; +} + +function makeOptionsObject(opts) { + var options = {}; + // old method signatures accepted checkRecursive and banUnknownProperties + if (opts[0] !== undefined) { + options.checkRecursive = opts[0]; + } + if (opts[1] !== undefined) { + options.banUnknownProperties = opts[1]; + } + return options; +} + +var languages = {}; +function createApi(language) { + var globalContext = new ValidatorContext(); + var currentLanguage = language || 'en'; + var api = { + addFormat: function () { + globalContext.addFormat.apply(globalContext, arguments); + }, + language: function (code) { + if (!code) { + return currentLanguage; + } + if (!languages[code]) { + code = code.split('-')[0]; // fall back to base language + } + if (languages[code]) { + currentLanguage = code; + return code; // so you can tell if fall-back has happened + } + return false; + }, + addLanguage: function (code, messageMap) { + var key; + for (key in ErrorCodes) { + if (messageMap[key] && !messageMap[ErrorCodes[key]]) { + messageMap[ErrorCodes[key]] = messageMap[key]; + } + } + var rootCode = code.split('-')[0]; + if (!languages[rootCode]) { // use for base language if not yet defined + languages[code] = messageMap; + languages[rootCode] = messageMap; + } else { + languages[code] = Object.create(languages[rootCode]); + for (key in messageMap) { + if (typeof languages[rootCode][key] === 'undefined') { + languages[rootCode][key] = messageMap[key]; + } + languages[code][key] = messageMap[key]; + } + } + return this; + }, + freshApi: function (language) { + var result = createApi(); + if (language) { + result.language(language); + } + return result; + }, + validate: function (data, schema, options) { + if (typeof schema === "string") { + schema = {"$ref": schema}; + } + if (typeof options !== "object" || options === null) { + options = makeOptionsObject(Array.prototype.slice.call(arguments, 2)); + } + var context = new ValidatorContext(globalContext, false, languages[currentLanguage], options); + context.addSchema("", schema); + var error = context.validateAll(data, schema, null, null, ""); + if (!error && options.banUnknownProperties) { + error = context.banUnknownProperties(); + } + this.error = error; + this.missing = context.missing; + this.valid = (error === null); + return this.valid; + }, + validateResult: function () { + var result = {}; + this.validate.apply(result, arguments); + return result; + }, + validateMultiple: function (data, schema, options) { + if (typeof schema === "string") { + schema = {"$ref": schema}; + } + if (typeof options !== "object" || options === null) { + options = makeOptionsObject(Array.prototype.slice.call(arguments, 2)); + } + var context = new ValidatorContext(globalContext, true, languages[currentLanguage], options); + context.addSchema("", schema); + context.validateAll(data, schema, null, null, ""); + if (options.banUnknownProperties) { + context.banUnknownProperties(); + } + var result = {}; + result.errors = context.errors; + result.missing = context.missing; + result.valid = (result.errors.length === 0); + return result; + }, + addSchema: function () { + return globalContext.addSchema.apply(globalContext, arguments); + }, + getSchema: function () { + return globalContext.getSchema.apply(globalContext, arguments); + }, + getSchemaMap: function () { + return globalContext.getSchemaMap.apply(globalContext, arguments); + }, + getSchemaUris: function () { + return globalContext.getSchemaUris.apply(globalContext, arguments); + }, + getMissingUris: function () { + return globalContext.getMissingUris.apply(globalContext, arguments); + }, + dropSchemas: function () { + globalContext.dropSchemas.apply(globalContext, arguments); + }, + reset: function () { + globalContext.reset(); + this.error = null; + this.missing = []; + this.valid = true; + }, + missing: [], + error: null, + valid: true, + normSchema: normSchema, + resolveUrl: resolveUrl, + getDocumentUri: getDocumentUri, + errorCodes: ErrorCodes + }; + return api; +} + +var tv4 = createApi(); +tv4.addLanguage('en-gb', ErrorMessagesDefault); + +//legacy property +tv4.tv4 = tv4; + +if (typeof module !== 'undefined' && module.exports){ + module.exports = tv4; +} +else { + global.tv4 = tv4; +} + +})(this); diff --git a/third_party/jsdoc/node_modules/underscore/LICENSE b/third_party/jsdoc/node_modules/underscore/LICENSE new file mode 100644 index 0000000000..0d6b8739d9 --- /dev/null +++ b/third_party/jsdoc/node_modules/underscore/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative +Reporters & Editors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/third_party/jsdoc/node_modules/underscore/package.json b/third_party/jsdoc/node_modules/underscore/package.json new file mode 100644 index 0000000000..d561286b9d --- /dev/null +++ b/third_party/jsdoc/node_modules/underscore/package.json @@ -0,0 +1,52 @@ +{ + "name": "underscore", + "description": "JavaScript's functional programming helper library.", + "homepage": "http://underscorejs.org", + "keywords": [ + "util", + "functional", + "server", + "client", + "browser" + ], + "author": { + "name": "Jeremy Ashkenas", + "email": "jeremy@documentcloud.org" + }, + "repository": { + "type": "git", + "url": "git://github.com/jashkenas/underscore.git" + }, + "main": "underscore.js", + "version": "1.6.0", + "devDependencies": { + "docco": "0.6.x", + "phantomjs": "1.9.0-1", + "uglify-js": "2.4.x" + }, + "scripts": { + "test": "phantomjs test/vendor/runner.js test/index.html?noglobals=true", + "build": "uglifyjs underscore.js -c \"evaluate=false\" --comments \"/ .*/\" -m --source-map underscore-min.map -o underscore-min.js", + "doc": "docco underscore.js" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://raw.github.com/jashkenas/underscore/master/LICENSE" + } + ], + "files": [ + "underscore.js", + "underscore-min.js", + "LICENSE" + ], + "readme": " __\n /\\ \\ __\n __ __ ___ \\_\\ \\ __ _ __ ____ ___ ___ _ __ __ /\\_\\ ____\n /\\ \\/\\ \\ /' _ `\\ /'_ \\ /'__`\\/\\ __\\/ ,__\\ / ___\\ / __`\\/\\ __\\/'__`\\ \\/\\ \\ /',__\\\n \\ \\ \\_\\ \\/\\ \\/\\ \\/\\ \\ \\ \\/\\ __/\\ \\ \\//\\__, `\\/\\ \\__//\\ \\ \\ \\ \\ \\//\\ __/ __ \\ \\ \\/\\__, `\\\n \\ \\____/\\ \\_\\ \\_\\ \\___,_\\ \\____\\\\ \\_\\\\/\\____/\\ \\____\\ \\____/\\ \\_\\\\ \\____\\/\\_\\ _\\ \\ \\/\\____/\n \\/___/ \\/_/\\/_/\\/__,_ /\\/____/ \\/_/ \\/___/ \\/____/\\/___/ \\/_/ \\/____/\\/_//\\ \\_\\ \\/___/\n \\ \\____/\n \\/___/\n\nUnderscore.js is a utility-belt library for JavaScript that provides\nsupport for the usual functional suspects (each, map, reduce, filter...)\nwithout extending any core JavaScript objects.\n\nFor Docs, License, Tests, and pre-packed downloads, see:\nhttp://underscorejs.org\n\nUnderscore is an open-sourced component of DocumentCloud:\nhttps://github.com/documentcloud\n\nMany thanks to our contributors:\nhttps://github.com/jashkenas/underscore/contributors\n", + "readmeFilename": "README.md", + "bugs": { + "url": "https://github.com/jashkenas/underscore/issues" + }, + "_id": "underscore@1.6.0", + "_shasum": "8b38b10cacdef63337b8b24e4ff86d45aea529a8", + "_from": "underscore@1.6.0", + "_resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz" +} diff --git a/third_party/jsdoc/node_modules/underscore/underscore.js b/third_party/jsdoc/node_modules/underscore/underscore.js new file mode 100644 index 0000000000..9a4cabecf7 --- /dev/null +++ b/third_party/jsdoc/node_modules/underscore/underscore.js @@ -0,0 +1,1343 @@ +// Underscore.js 1.6.0 +// http://underscorejs.org +// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `exports` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var + push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.6.0'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return obj; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, length = obj.length; i < length; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return; + } + } + return obj; + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results.push(iterator.call(context, value, index, list)); + }); + return results; + }; + + var reduceError = 'Reduce of empty array with no initial value'; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var length = obj.length; + if (length !== +length) { + var keys = _.keys(obj); + length = keys.length; + } + each(obj, function(value, index, list) { + index = keys ? keys[--length] : --length; + if (!initial) { + memo = obj[index]; + initial = true; + } else { + memo = iterator.call(context, memo, obj[index], index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, predicate, context) { + var result; + any(obj, function(value, index, list) { + if (predicate.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, predicate, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(predicate, context); + each(obj, function(value, index, list) { + if (predicate.call(context, value, index, list)) results.push(value); + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, predicate, context) { + return _.filter(obj, function(value, index, list) { + return !predicate.call(context, value, index, list); + }, context); + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, predicate, context) { + predicate || (predicate = _.identity); + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(predicate, context); + each(obj, function(value, index, list) { + if (!(result = result && predicate.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, predicate, context) { + predicate || (predicate = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(predicate, context); + each(obj, function(value, index, list) { + if (result || (result = predicate.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + if (obj == null) return false; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + return any(obj, function(value) { + return value === target; + }); + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + return (isFunc ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, _.property(key)); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs) { + return _.filter(obj, _.matches(attrs)); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.find(obj, _.matches(attrs)); + }; + + // Return the maximum element or (element-based computation). + // Can't optimize arrays of integers longer than 65,535 elements. + // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.max.apply(Math, obj); + } + var result = -Infinity, lastComputed = -Infinity; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + if (computed > lastComputed) { + result = value; + lastComputed = computed; + } + }); + return result; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.min.apply(Math, obj); + } + var result = Infinity, lastComputed = Infinity; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + if (computed < lastComputed) { + result = value; + lastComputed = computed; + } + }); + return result; + }; + + // Shuffle an array, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + _.shuffle = function(obj) { + var rand; + var index = 0; + var shuffled = []; + each(obj, function(value) { + rand = _.random(index++); + shuffled[index - 1] = shuffled[rand]; + shuffled[rand] = value; + }); + return shuffled; + }; + + // Sample **n** random values from a collection. + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (n == null || guard) { + if (obj.length !== +obj.length) obj = _.values(obj); + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + + // An internal function to generate lookup iterators. + var lookupIterator = function(value) { + if (value == null) return _.identity; + if (_.isFunction(value)) return value; + return _.property(value); + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, iterator, context) { + iterator = lookupIterator(iterator); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value: value, + index: index, + criteria: iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(behavior) { + return function(obj, iterator, context) { + var result = {}; + iterator = lookupIterator(iterator); + each(obj, function(value, index) { + var key = iterator.call(context, value, index, obj); + behavior(result, key, value); + }); + return result; + }; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = group(function(result, key, value) { + _.has(result, key) ? result[key].push(value) : result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, key, value) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, key) { + _.has(result, key) ? result[key]++ : result[key] = 1; + }); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator, context) { + iterator = lookupIterator(iterator); + var value = iterator.call(context, obj); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >>> 1; + iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + if ((n == null) || guard) return array[0]; + if (n < 0) return []; + return slice.call(array, 0, n); + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if ((n == null) || guard) return array[array.length - 1]; + return slice.call(array, Math.max(array.length - n, 0)); + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, (n == null) || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, output) { + if (shallow && _.every(input, _.isArray)) { + return concat.apply(output, input); + } + each(input, function(value) { + if (_.isArray(value) || _.isArguments(value)) { + shallow ? push.apply(output, value) : flatten(value, shallow, output); + } else { + output.push(value); + } + }); + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. + _.flatten = function(array, shallow) { + return flatten(array, shallow, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Split an array into two arrays: one whose elements all satisfy the given + // predicate, and one whose elements all do not satisfy the predicate. + _.partition = function(array, predicate) { + var pass = [], fail = []; + each(array, function(elem) { + (predicate(elem) ? pass : fail).push(elem); + }); + return [pass, fail]; + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } + var initial = iterator ? _.map(array, iterator, context) : array; + var results = []; + var seen = []; + each(initial, function(value, index) { + if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { + seen.push(value); + results.push(array[index]); + } + }); + return results; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(_.flatten(arguments, true)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.contains(other, item); + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.contains(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var length = _.max(_.pluck(arguments, 'length').concat(0)); + var results = new Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(arguments, '' + i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + if (list == null) return {}; + var result = {}; + for (var i = 0, length = list.length; i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, length = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted); + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); + for (; i < length; i++) if (array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var hasIndex = from != null; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { + return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); + } + var i = (hasIndex ? from : array.length); + while (i--) if (array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(length); + + while(idx < length) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Reusable constructor function for prototype setting. + var ctor = function(){}; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError; + args = slice.call(arguments, 2); + return bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + ctor.prototype = func.prototype; + var self = new ctor; + ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) return result; + return self; + }; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. _ acts + // as a placeholder, allowing any combination of arguments to be pre-filled. + _.partial = function(func) { + var boundArgs = slice.call(arguments, 1); + return function() { + var position = 0; + var args = boundArgs.slice(); + for (var i = 0, length = args.length; i < length; i++) { + if (args[i] === _) args[i] = arguments[position++]; + } + while (position < arguments.length) args.push(arguments[position++]); + return func.apply(this, args); + }; + }; + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length === 0) throw new Error('bindAll must be passed function names'); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(null, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + options || (options = {}); + var later = function() { + previous = options.leading === false ? 0 : _.now(); + timeout = null; + result = func.apply(context, args); + context = args = null; + }; + return function() { + var now = _.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = _.now() - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = _.now(); + var callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + memo = func.apply(this, arguments); + func = null; + return memo; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return _.partial(wrapper, func); + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = function(obj) { + if (!_.isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys.push(key); + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var values = new Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = new Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + each(keys, function(key) { + if (key in obj) copy[key] = obj[key]; + }); + return copy; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + for (var key in obj) { + if (!_.contains(keys, key)) copy[key] = obj[key]; + } + return copy; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + if (obj[prop] === void 0) obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) return bStack[length] == b; + } + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && + _.isFunction(bCtor) && (bCtor instanceof bCtor)) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) == '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Optimize `isFunction` if appropriate. + if (typeof (/./) !== 'function') { + _.isFunction = function(obj) { + return typeof obj === 'function'; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj != +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + _.constant = function(value) { + return function () { + return value; + }; + }; + + _.property = function(key) { + return function(obj) { + return obj[key]; + }; + }; + + // Returns a predicate for checking whether an object has a given set of `key:value` pairs. + _.matches = function(attrs) { + return function(obj) { + if (obj === attrs) return true; //avoid comparing an object to itself. + for (var key in attrs) { + if (attrs[key] !== obj[key]) + return false; + } + return true; + } + }; + + // Run a function **n** times. + _.times = function(n, iterator, context) { + var accum = Array(Math.max(0, n)); + for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // A (possibly faster) way to get the current timestamp as an integer. + _.now = Date.now || function() { return new Date().getTime(); }; + + // List of HTML entities for escaping. + var entityMap = { + escape: { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + } + }; + entityMap.unescape = _.invert(entityMap.escape); + + // Regexes containing the keys and values listed immediately above. + var entityRegexes = { + escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), + unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + _.each(['escape', 'unescape'], function(method) { + _[method] = function(string) { + if (string == null) return ''; + return ('' + string).replace(entityRegexes[method], function(match) { + return entityMap[method][match]; + }); + }; + }); + + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. + _.result = function(object, property) { + if (object == null) return void 0; + var value = object[property]; + return _.isFunction(value) ? value.call(object) : value; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + _.extend(_.prototype, { + + // Start chaining a wrapped Underscore object. + chain: function() { + this._chain = true; + return this; + }, + + // Extracts the result from a wrapped and chained object. + value: function() { + return this._wrapped; + } + + }); + + // AMD registration happens at the end for compatibility with AMD loaders + // that may not enforce next-turn semantics on modules. Even though general + // practice for AMD registration is to be anonymous, underscore registers + // as a named module because, like jQuery, it is a base library that is + // popular enough to be bundled in a third party lib, but not be part of + // an AMD load request. Those cases could generate an error when an + // anonymous define() is called outside of a loader request. + if (typeof define === 'function' && define.amd) { + define('underscore', [], function() { + return _; + }); + } +}).call(this); diff --git a/third_party/jsdoc/node_modules/wrench/LICENSE b/third_party/jsdoc/node_modules/wrench/LICENSE new file mode 100644 index 0000000000..a85a94a61b --- /dev/null +++ b/third_party/jsdoc/node_modules/wrench/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2010 Ryan McGrath + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/jsdoc/node_modules/wrench/lib/wrench.js b/third_party/jsdoc/node_modules/wrench/lib/wrench.js new file mode 100644 index 0000000000..00f4166bae --- /dev/null +++ b/third_party/jsdoc/node_modules/wrench/lib/wrench.js @@ -0,0 +1,399 @@ +/* wrench.js + * + * A collection of various utility functions I've found myself in need of + * for use with Node.js (http://nodejs.org/). This includes things like: + * + * - Recursively deleting directories in Node.js (Sync, not Async) + * - Recursively copying directories in Node.js (Sync, not Async) + * - Recursively chmoding a directory structure from Node.js (Sync, not Async) + * - Other things that I'll add here as time goes on. Shhhh... + * + * ~ Ryan McGrath (ryan [at] venodesigns.net) + */ + +var fs = require("fs"), + _path = require("path"); + + +/* wrench.readdirSyncRecursive("directory_path"); + * + * Recursively dives through directories and read the contents of all the + * children directories. + */ +exports.readdirSyncRecursive = function(baseDir) { + baseDir = baseDir.replace(/\/$/, ''); + + var readdirSyncRecursive = function(baseDir) { + var files = [], + curFiles, + nextDirs, + isDir = function(fname){ + return fs.statSync( _path.join(baseDir, fname) ).isDirectory(); + }, + prependBaseDir = function(fname){ + return _path.join(baseDir, fname); + }; + + curFiles = fs.readdirSync(baseDir); + nextDirs = curFiles.filter(isDir); + curFiles = curFiles.map(prependBaseDir); + + files = files.concat( curFiles ); + + while (nextDirs.length) { + files = files.concat( readdirSyncRecursive( _path.join(baseDir, nextDirs.shift()) ) ); + } + + return files; + }; + + // convert absolute paths to relative + var fileList = readdirSyncRecursive(baseDir).map(function(val){ + return _path.relative(baseDir, val); + }); + + return fileList; +}; + +/* wrench.readdirRecursive("directory_path", function(error, files) {}); + * + * Recursively dives through directories and read the contents of all the + * children directories. + * + * Asynchronous, so returns results/error in callback. + * Callback receives the of files in currently recursed directory. + * When no more directories are left, callback is called with null for all arguments. + * + */ +exports.readdirRecursive = function(baseDir, fn) { + baseDir = baseDir.replace(/\/$/, ''); + + var waitCount = 0; + + function readdirRecursive(curDir) { + var files = [], + curFiles, + nextDirs, + prependcurDir = function(fname){ + return _path.join(curDir, fname); + }; + + waitCount++; + fs.readdir(curDir, function(e, curFiles) { + waitCount--; + + curFiles = curFiles.map(prependcurDir); + + curFiles.forEach(function(it) { + waitCount++; + + fs.stat(it, function(e, stat) { + waitCount--; + + if (e) { + fn(e); + } else { + if (stat.isDirectory()) { + readdirRecursive(it); + } + } + + if (waitCount == 0) { + fn(null, null); + } + }); + }); + + fn(null, curFiles.map(function(val) { + // convert absolute paths to relative + return _path.relative(baseDir, val); + })); + + if (waitCount == 0) { + fn(null, null); + } + }); + }; + + readdirRecursive(baseDir); +}; + + + +/* wrench.rmdirSyncRecursive("directory_path", forceDelete, failSilent); + * + * Recursively dives through directories and obliterates everything about it. This is a + * Sync-function, which blocks things until it's done. No idea why anybody would want an + * Asynchronous version. :\ + */ +exports.rmdirSyncRecursive = function(path, failSilent) { + var files; + + try { + files = fs.readdirSync(path); + } catch (err) { + if(failSilent) return; + throw new Error(err.message); + } + + /* Loop through and delete everything in the sub-tree after checking it */ + for(var i = 0; i < files.length; i++) { + var currFile = fs.lstatSync(path + "/" + files[i]); + + if(currFile.isDirectory()) // Recursive function back to the beginning + exports.rmdirSyncRecursive(path + "/" + files[i]); + + else if(currFile.isSymbolicLink()) // Unlink symlinks + fs.unlinkSync(path + "/" + files[i]); + + else // Assume it's a file - perhaps a try/catch belongs here? + fs.unlinkSync(path + "/" + files[i]); + } + + /* Now that we know everything in the sub-tree has been deleted, we can delete the main + directory. Huzzah for the shopkeep. */ + return fs.rmdirSync(path); +}; + +/* wrench.copyDirSyncRecursive("directory_to_copy", "new_directory_location", opts); + * + * Recursively dives through a directory and moves all its files to a new location. This is a + * Synchronous function, which blocks things until it's done. If you need/want to do this in + * an Asynchronous manner, look at wrench.copyDirRecursively() below. + * + * Note: Directories should be passed to this function without a trailing slash. + */ +exports.copyDirSyncRecursive = function(sourceDir, newDirLocation, opts) { + + if (!opts || !opts.preserve) { + try { + if(fs.statSync(newDirLocation).isDirectory()) exports.rmdirSyncRecursive(newDirLocation); + } catch(e) { } + } + + /* Create the directory where all our junk is moving to; read the mode of the source directory and mirror it */ + var checkDir = fs.statSync(sourceDir); + try { + fs.mkdirSync(newDirLocation, checkDir.mode); + } catch (e) { + //if the directory already exists, that's okay + if (e.code !== 'EEXIST') throw e; + } + + var files = fs.readdirSync(sourceDir); + + for(var i = 0; i < files.length; i++) { + var currFile = fs.lstatSync(sourceDir + "/" + files[i]); + + if(currFile.isDirectory()) { + /* recursion this thing right on back. */ + exports.copyDirSyncRecursive(sourceDir + "/" + files[i], newDirLocation + "/" + files[i], opts); + } else if(currFile.isSymbolicLink()) { + var symlinkFull = fs.readlinkSync(sourceDir + "/" + files[i]); + fs.symlinkSync(symlinkFull, newDirLocation + "/" + files[i]); + } else { + /* At this point, we've hit a file actually worth copying... so copy it on over. */ + var contents = fs.readFileSync(sourceDir + "/" + files[i]); + fs.writeFileSync(newDirLocation + "/" + files[i], contents); + } + } +}; + +/* wrench.chmodSyncRecursive("directory", filemode); + * + * Recursively dives through a directory and chmods everything to the desired mode. This is a + * Synchronous function, which blocks things until it's done. + * + * Note: Directories should be passed to this function without a trailing slash. + */ +exports.chmodSyncRecursive = function(sourceDir, filemode) { + var files = fs.readdirSync(sourceDir); + + for(var i = 0; i < files.length; i++) { + var currFile = fs.lstatSync(sourceDir + "/" + files[i]); + + if(currFile.isDirectory()) { + /* ...and recursion this thing right on back. */ + exports.chmodSyncRecursive(sourceDir + "/" + files[i], filemode); + } else { + /* At this point, we've hit a file actually worth copying... so copy it on over. */ + fs.chmod(sourceDir + "/" + files[i], filemode); + } + } + + /* Finally, chmod the parent directory */ + fs.chmod(sourceDir, filemode); +}; + + +/* wrench.chownSyncRecursive("directory", uid, gid); + * + * Recursively dives through a directory and chowns everything to the desired user and group. This is a + * Synchronous function, which blocks things until it's done. + * + * Note: Directories should be passed to this function without a trailing slash. + */ +exports.chownSyncRecursive = function(sourceDir, uid, gid) { + var files = fs.readdirSync(sourceDir); + + for(var i = 0; i < files.length; i++) { + var currFile = fs.lstatSync(sourceDir + "/" + files[i]); + + if(currFile.isDirectory()) { + /* ...and recursion this thing right on back. */ + exports.chownSyncRecursive(sourceDir + "/" + files[i], uid, gid); + } else { + /* At this point, we've hit a file actually worth chowning... so own it. */ + fs.chownSync(sourceDir + "/" + files[i], uid, gid); + } + } + + /* Finally, chown the parent directory */ + fs.chownSync(sourceDir, uid, gid); +}; + + + +/* wrench.rmdirRecursive("directory_path", callback); + * + * Recursively dives through directories and obliterates everything about it. + */ +exports.rmdirRecursive = function rmdirRecursive(dir, clbk){ + fs.readdir(dir, function(err, files){ + if (err) return clbk(err); + (function rmFile(err){ + if (err) return clbk(err); + + var filename = files.shift(); + if (filename === null || typeof filename == 'undefined') + return fs.rmdir(dir, clbk); + + var file = dir+'/'+filename; + fs.stat(file, function(err, stat){ + if (err) return clbk(err); + if (stat.isDirectory()) + rmdirRecursive(file, rmFile); + else + fs.unlink(file, rmFile); + }); + })(); + }); +}; + +/* wrench.copyDirRecursive("directory_to_copy", "new_location", callback); + * + * Recursively dives through a directory and moves all its files to a new + * location. + * + * Note: Directories should be passed to this function without a trailing slash. + */ +exports.copyDirRecursive = function copyDirRecursive(srcDir, newDir, clbk) { + fs.stat(newDir, function(err, newDirStat){ + if (!err) return exports.rmdirRecursive(newDir, function(err){ + copyDirRecursive(srcDir, newDir, clbk); + }); + + fs.stat(srcDir, function(err, srcDirStat){ + if (err) return clbk(err); + fs.mkdir(newDir, srcDirStat.mode, function(err){ + if (err) return clbk(err); + fs.readdir(srcDir, function(err, files){ + if (err) return clbk(err); + (function copyFiles(err){ + if (err) return clbk(err); + + var filename = files.shift(); + if (filename === null || typeof filename == 'undefined') + return clbk(); + + var file = srcDir+'/'+filename, + newFile = newDir+'/'+filename; + + fs.stat(file, function(err, fileStat){ + if (fileStat.isDirectory()) + copyDirRecursive(file, newFile, copyFiles); + else if (fileStat.isSymbolicLink()) + fs.readlink(file, function(err, link){ + fs.symlink(link, newFile, copyFiles); + }); + else + fs.readFile(file, function(err, data){ + fs.writeFile(newFile, data, copyFiles); + }); + }); + })(); + }); + }); + }); + }); +}; + +var mkdirSyncRecursive = function(path, mode) { + var self = this; + + try { + fs.mkdirSync(path, mode); + } catch(err) { + if(err.code == "ENOENT") { + var slashIdx = path.lastIndexOf("/"); + if(slashIdx < 0) { + slashIdx = path.lastIndexOf("\\"); + } + + if(slashIdx > 0) { + var parentPath = path.substring(0, slashIdx); + mkdirSyncRecursive(parentPath, mode); + mkdirSyncRecursive(path, mode); + } else { + throw err; + } + } else if(err.code == "EEXIST") { + return; + } else { + throw err; + } + } +}; +exports.mkdirSyncRecursive = mkdirSyncRecursive; + +exports.LineReader = function(filename, bufferSize) { + this.bufferSize = bufferSize || 8192; + this.buffer = ""; + this.fd = fs.openSync(filename, "r"); + this.currentPosition = 0; +}; + +exports.LineReader.prototype = { + getBufferAndSetCurrentPosition: function(position) { + var res = fs.readSync(this.fd, this.bufferSize, position, "ascii"); + + this.buffer += res[0]; + if(res[1] === 0) { + this.currentPosition = -1; + } else { + this.currentPosition = position + res[1]; + } + + return this.currentPosition; + }, + + hasNextLine: function() { + while(this.buffer.indexOf('\n') === -1) { + this.getBufferAndSetCurrentPosition(this.currentPosition); + if(this.currentPosition === -1) return false; + } + + if(this.buffer.indexOf("\n") > -1) return true; + return false; + }, + + getNextLine: function() { + var lineEnd = this.buffer.indexOf("\n"), + result = this.buffer.substring(0, lineEnd); + + this.buffer = this.buffer.substring(result.length + 1, this.buffer.length); + return result; + } +}; + +// vim: et ts=4 sw=4 diff --git a/third_party/jsdoc/node_modules/wrench/package.json b/third_party/jsdoc/node_modules/wrench/package.json new file mode 100644 index 0000000000..80124ffe3c --- /dev/null +++ b/third_party/jsdoc/node_modules/wrench/package.json @@ -0,0 +1,43 @@ +{ + "name": "wrench", + "description": "Recursive filesystem (and other) operations that Node *should* have.", + "version": "1.3.9", + "author": { + "name": "Ryan McGrath", + "email": "ryan@venodesigns.net" + }, + "repository": { + "type": "git", + "url": "https://ryanmcgrath@github.com/ryanmcgrath/wrench-js.git" + }, + "bugs": { + "url": "http://github.com/ryanmcgrath/wrench-js/issues" + }, + "directories": { + "lib": "./lib/" + }, + "dependencies": {}, + "devDependencies": { + "nodeunit": ">= 0.6.4" + }, + "main": "./lib/wrench", + "engines": { + "node": ">=0.1.97" + }, + "scripts": { + "test": "nodeunit tests/runner.js" + }, + "licenses": [ + { + "type": "MIT", + "url": "http://github.com/ryanmcgrath/wrench-js/raw/master/LICENSE" + } + ], + "readme": "wrench.js - Recursive file operations in Node.js\n----------------------------------------------------------------------------\nWhile I love Node.js, I've found myself missing some functions. Things like\nrecursively deleting/chmodding a directory (or even deep copying a directory),\nor even a basic line reader, shouldn't need to be re-invented time and time again.\n\nThat said, here's my attempt at a re-usable solution, at least until something\nmore formalized gets integrated into Node.js (*hint hint*). wrench.js is fairly simple\nto use - check out the documentation/examples below:\n\nInstallation\n-----------------------------------------------------------------------------\n\n npm install wrench\n\nUsage\n-----------------------------------------------------------------------------\n``` javascript\nvar wrench = require('wrench'),\n\tutil = require('util');\n```\n\n### Synchronous operations\n``` javascript\n// Recursively create directories, sub-trees and all.\nwrench.mkdirSyncRecursive(dir, 0777);\n\n// Recursively delete the entire sub-tree of a directory, then kill the directory\nwrench.rmdirSyncRecursive('my_directory_name', failSilently);\n\n// Recursively read directories contents.\nwrench.readdirSyncRecursive('my_directory_name');\n\n// Recursively chmod the entire sub-tree of a directory\nwrench.chmodSyncRecursive('my_directory_name', 0755);\n\n// Recursively chown the entire sub-tree of a directory\nwrench.chownSyncRecursive(\"directory\", uid, gid);\n\n// Deep-copy an existing directory\nwrench.copyDirSyncRecursive('directory_to_copy', 'location_where_copy_should_end_up');\n\n// Read lines in from a file until you hit the end\nvar f = new wrench.LineReader('x.txt');\nwhile(f.hasNextLine()) {\n\tutil.puts(x.getNextLine());\n}\n```\n\n### Asynchronous operations\n``` javascript\n// Recursively read directories contents\nvar files = [];\nwrench.readdirRecursive('my_directory_name', function(error, curFiles) {\n // curFiles is what you want\n});\n\n```\n\nQuestions, comments? Hit me up. (ryan [at] venodesigns.net | http://twitter.com/ryanmcgrath)\n", + "readmeFilename": "readme.md", + "homepage": "https://github.com/ryanmcgrath/wrench-js", + "_id": "wrench@1.3.9", + "_shasum": "6f13ec35145317eb292ca5f6531391b244111411", + "_from": "wrench@1.3.9", + "_resolved": "https://registry.npmjs.org/wrench/-/wrench-1.3.9.tgz" +} diff --git a/third_party/jsdoc/package.json b/third_party/jsdoc/package.json new file mode 100644 index 0000000000..e64678ceda --- /dev/null +++ b/third_party/jsdoc/package.json @@ -0,0 +1,59 @@ +{ + "name": "jsdoc", + "version": "3.3.0-dev", + "revision": "1413822926905", + "description": "An API documentation generator for JavaScript.", + "keywords": [ + "documentation", + "javascript" + ], + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/jsdoc3/jsdoc" + }, + "dependencies": { + "async": "~0.1.22", + "catharsis": "~0.8.3", + "escape-string-regexp": "~1.0.0", + "esprima": "https://github.com/ariya/esprima/tarball/49a2eccb243f29bd653b11e9419241a9d726af7c", + "js2xmlparser": "~0.1.0", + "marked": "~0.3.1", + "requizzle": "~0.2.0", + "strip-json-comments": "~0.1.3", + "taffydb": "https://github.com/hegemonic/taffydb/tarball/master", + "underscore": "~1.6.0", + "wrench": "~1.3.9" + }, + "devDependencies": { + "eslint": "~0.9.1", + "gulp": "~3.8.5", + "gulp-eslint": "~0.1.7", + "gulp-json-editor": "~2.0.2", + "istanbul": "~0.2.1", + "tv4": "https://github.com/hegemonic/tv4/tarball/own-properties" + }, + "engines": { + "node": ">=0.10" + }, + "scripts": { + "test": "gulp test" + }, + "bin": { + "jsdoc": "./jsdoc.js" + }, + "bugs": "https://github.com/jsdoc3/jsdoc/issues", + "author": { + "name": "Michael Mathews", + "email": "micmath@gmail.com" + }, + "contributors": [ + { + "url": "https://github.com/jsdoc3/jsdoc/graphs/contributors" + } + ], + "maintainers": { + "name": "Jeff Williams", + "email": "jeffrey.l.williams@gmail.com" + } +} diff --git a/third_party/jsdoc/plugins/commentConvert.js b/third_party/jsdoc/plugins/commentConvert.js new file mode 100644 index 0000000000..64347e0d92 --- /dev/null +++ b/third_party/jsdoc/plugins/commentConvert.js @@ -0,0 +1,23 @@ +/** + @overview Demonstrate how to modify the source code before the parser sees it. + @module plugins/commentConvert + @author Michael Mathews + */ +'use strict'; + +/*eslint spaced-line-comment: 0 */ + +exports.handlers = { + /// + /// Convert ///-style comments into jsdoc comments. + /// @param e + /// @param e.filename + /// @param e.source + /// + beforeParse: function(e) { + e.source = e.source.replace(/(\n[ \t]*\/\/\/[^\n]*)+/g, function($) { + var replacement = '\n/**' + $.replace(/^[ \t]*\/\/\//mg, '').replace(/(\n$|$)/, '*/$1'); + return replacement; + }); + } +}; diff --git a/third_party/jsdoc/plugins/commentsOnly.js b/third_party/jsdoc/plugins/commentsOnly.js new file mode 100644 index 0000000000..a35e96f13e --- /dev/null +++ b/third_party/jsdoc/plugins/commentsOnly.js @@ -0,0 +1,18 @@ +/** + * @overview Remove everything in a file except JSDoc-style comments. By enabling this plugin, you + * can document source files that are not valid JavaScript (including source files for other + * languages). + * @module plugins/commentsOnly + * @author Jeff Williams + */ +'use strict'; + +exports.handlers = { + beforeParse: function(e) { + // a JSDoc comment looks like: /**[one or more chars]*/ + var comments = e.source.match(/\/\*\*[\s\S]+?\*\//g); + if (comments) { + e.source = comments.join('\n\n'); + } + } +}; diff --git a/third_party/jsdoc/plugins/escapeHtml.js b/third_party/jsdoc/plugins/escapeHtml.js new file mode 100644 index 0000000000..d587405277 --- /dev/null +++ b/third_party/jsdoc/plugins/escapeHtml.js @@ -0,0 +1,21 @@ +/** + @overview Escape HTML tags in descriptions. + @module plugins/escapeHtml + @author Michael Mathews + */ +'use strict'; + +exports.handlers = { + /** + Translate HTML tags in descriptions into safe entities. + Replaces <, & and newlines + */ + newDoclet: function(e) { + if (e.doclet.description) { + e.doclet.description = e.doclet.description + .replace(/&/g,'&') + .replace(/'); + } + } +}; diff --git a/third_party/jsdoc/plugins/eventDumper.js b/third_party/jsdoc/plugins/eventDumper.js new file mode 100644 index 0000000000..38ca84387a --- /dev/null +++ b/third_party/jsdoc/plugins/eventDumper.js @@ -0,0 +1,115 @@ +/*global env: true */ +/** + * @overview Dump information about parser events to the console. + * @module plugins/eventDumper + * @author Jeff Williams + */ +'use strict'; + +var _ = require('underscore'); +var util = require('util'); + +var conf = env.conf.eventDumper || {}; +var isRhino = require('jsdoc/util/runtime').isRhino(); + +// Dump the included parser events (defaults to all events) +var events = conf.include || [ + 'parseBegin', + 'fileBegin', + 'beforeParse', + 'jsdocCommentFound', + 'symbolFound', + 'newDoclet', + 'fileComplete', + 'parseComplete', + 'processingComplete' +]; +// Don't dump the excluded parser events +if (conf.exclude) { + events = _.difference(events, conf.exclude); +} + +/** + * Check whether a variable appears to be a Java native object. + * + * @param {*} o - The variable to check. + * @return {boolean} Set to `true` for Java native objects and `false` in all other cases. + */ +function isJavaNativeObject(o) { + if (!isRhino) { + return false; + } + + return o && typeof o === 'object' && typeof o.getClass === 'function'; +} + +/** + * Replace AST node objects in events with a placeholder. + * + * @param {Object} o - An object whose properties may contain AST node objects. + * @return {Object} The modified object. + */ +function replaceNodeObjects(o) { + var doop = require('jsdoc/util/doop'); + + var OBJECT_PLACEHOLDER = ''; + + if (o.code && o.code.node) { + // don't break the original object! + o.code = doop(o.code); + o.code.node = OBJECT_PLACEHOLDER; + } + + if (o.doclet && o.doclet.meta && o.doclet.meta.code && o.doclet.meta.code.node) { + // don't break the original object! + o.doclet.meta.code = doop(o.doclet.meta.code); + o.doclet.meta.code.node = OBJECT_PLACEHOLDER; + } + + if (o.astnode) { + o.astnode = OBJECT_PLACEHOLDER; + } + + return o; +} + +/** + * Get rid of unwanted crud in an event object. + * + * @param {object} e The event object. + * @return {object} The fixed-up object. + */ +function cleanse(e) { + var result = {}; + + Object.keys(e).forEach(function(prop) { + // by default, don't stringify properties that contain an array of functions + if (!conf.includeFunctions && util.isArray(e[prop]) && e[prop][0] && + String(typeof e[prop][0]) === 'function') { + result[prop] = 'function[' + e[prop].length + ']'; + } + // never include functions that belong to the object + else if (typeof e[prop] !== 'function') { + // don't call JSON.stringify() on Java native objects--Rhino will throw an exception + result[prop] = isJavaNativeObject(e[prop]) ? String(e[prop]) : e[prop]; + } + }); + + // allow users to omit node objects, which can be enormous + if (conf.omitNodes) { + result = replaceNodeObjects(result); + } + + return result; +} + +exports.handlers = {}; + +events.forEach(function(eventType) { + exports.handlers[eventType] = function(e) { + console.log( JSON.stringify({ + type: eventType, + content: cleanse(e) + }, null, 4) ); + }; +}); diff --git a/third_party/jsdoc/plugins/markdown.js b/third_party/jsdoc/plugins/markdown.js new file mode 100644 index 0000000000..91f918d807 --- /dev/null +++ b/third_party/jsdoc/plugins/markdown.js @@ -0,0 +1,89 @@ +/** + * @overview Translate doclet descriptions from MarkDown into HTML. + * @module plugins/markdown + * @author Michael Mathews + * @author Ben Blank + */ +'use strict'; + +var config = global.env.conf.markdown || {}; +var defaultTags = [ + 'classdesc', + 'description', + 'exceptions', + 'params', + 'properties', + 'returns', + 'see' +]; +var hasOwnProp = Object.prototype.hasOwnProperty; +var parse = require('jsdoc/util/markdown').getParser(); +var tags = []; +var excludeTags = []; + +function shouldProcessString(tagName, text) { + var shouldProcess = false; + + if (tagName !== 'see') { + shouldProcess = true; + } + // we only want to process `@see` tags that contain Markdown links + else if (tagName === 'see' && text.indexOf('[') !== -1) { + shouldProcess = true; + } + + return shouldProcess; +} + +/** + * Process the markdown source in a doclet. The properties that should be + * processed are configurable, but always include "classdesc", "description", + * "params", "properties", and "returns". Handled properties can be bare + * strings, objects, or arrays of objects. + */ +function process(doclet) { + tags.forEach(function(tag) { + if ( !hasOwnProp.call(doclet, tag) ) { + return; + } + + if (typeof doclet[tag] === 'string' && shouldProcessString(tag, doclet[tag]) ) { + doclet[tag] = parse(doclet[tag]); + } + else if ( Array.isArray(doclet[tag]) ) { + doclet[tag].forEach(function(value, index, original) { + var inner = {}; + inner[tag] = value; + process(inner); + original[index] = inner[tag]; + }); + } + else if (doclet[tag]) { + process(doclet[tag]); + } + }); +} + +// set up the list of "tags" (properties) to process +if (config.tags) { + tags = config.tags.slice(); +} +// set up the list of default tags to exclude from processing +if (config.excludeTags) { + excludeTags = config.excludeTags.slice(); +} +defaultTags.forEach(function(tag) { + if (excludeTags.indexOf(tag) === -1 && tags.indexOf(tag) === -1) { + tags.push(tag); + } +}); + +exports.handlers = { + /** + * Translate markdown syntax in a new doclet's description into HTML. Is run + * by JSDoc 3 whenever a "newDoclet" event fires. + */ + newDoclet: function(e) { + process(e.doclet); + } +}; diff --git a/third_party/jsdoc/plugins/overloadHelper.js b/third_party/jsdoc/plugins/overloadHelper.js new file mode 100644 index 0000000000..29c384d965 --- /dev/null +++ b/third_party/jsdoc/plugins/overloadHelper.js @@ -0,0 +1,185 @@ +/** + * The Overload Helper plugin automatically adds a signature-like string to the longnames of + * overloaded functions and methods. In JSDoc, this string is known as a _variation_. (The longnames + * of overloaded constructor functions are _not_ updated, so that JSDoc can identify the class' + * members correctly.) + * + * Using this plugin allows you to link to overloaded functions without manually adding `@variation` + * tags to your documentation. + * + * For example, suppose your code includes a function named `foo` that you can call in the + * following ways: + * + * + `foo()` + * + `foo(bar)` + * + `foo(bar, baz)` (where `baz` is repeatable) + * + * This plugin assigns the following variations and longnames to each version of `foo`: + * + * + `foo()` gets the variation `()` and the longname `foo()`. + * + `foo(bar)` gets the variation `(bar)` and the longname `foo(bar)`. + * + `foo(bar, baz)` (where `baz` is repeatable) gets the variation `(bar, ...baz)` and the longname + * `foo(bar, ...baz)`. + * + * You can then link to these functions with `{@link foo()}`, `{@link foo(bar)}`, and + * `{@link foo(bar, ...baz)`. Note that the variation is based on the names of the function + * parameters, _not_ their types. + * + * If you prefer to manually assign variations to certain functions, you can still do so with the + * `@variation` tag. This plugin will not change these variations or add more variations for that + * function, as long as the variations you've defined result in unique longnames. + * + * If an overloaded function includes multiple signatures with the same parameter names, the plugin + * will assign numeric variations instead, starting at `(1)` and counting upwards. + * + * @module plugins/overloadHelper + * @author Jeff Williams + * @license Apache License 2.0 + */ +'use strict'; + +// lookup table of function doclets by longname +var functionDoclets; + +function hasUniqueValues(obj) { + var isUnique = true; + var seen = []; + Object.keys(obj).forEach(function(key) { + if (seen.indexOf(obj[key]) !== -1) { + isUnique = false; + } + + seen.push(obj[key]); + }); + + return isUnique; +} + +function getParamNames(params) { + var names = []; + + params.forEach(function(param) { + var name = param.name || ''; + if (param.variable) { + name = '...' + name; + } + if (name !== '') { + names.push(name); + } + }); + + return names.length ? names.join(', ') : ''; +} + +function getParamVariation(doclet) { + return getParamNames(doclet.params || []); +} + +function getUniqueVariations(doclets) { + var counter = 0; + var variations = {}; + var docletKeys = Object.keys(doclets); + + function getUniqueNumbers() { + var format = require('util').format; + + docletKeys.forEach(function(doclet) { + var newLongname; + + while (true) { + counter++; + variations[doclet] = String(counter); + + // is this longname + variation unique? + newLongname = format('%s(%s)', doclets[doclet].longname, variations[doclet]); + if ( !functionDoclets[newLongname] ) { + break; + } + } + }); + } + + function getUniqueNames() { + // start by trying to preserve existing variations + docletKeys.forEach(function(doclet) { + variations[doclet] = doclets[doclet].variation || getParamVariation(doclets[doclet]); + }); + + // if they're identical, try again, without preserving existing variations + if ( !hasUniqueValues(variations) ) { + docletKeys.forEach(function(doclet) { + variations[doclet] = getParamVariation(doclets[doclet]); + }); + + // if they're STILL identical, switch to numeric variations + if ( !hasUniqueValues(variations) ) { + getUniqueNumbers(); + } + } + } + + // are we already using numeric variations? if so, keep doing that + if (functionDoclets[doclets.newDoclet.longname + '(1)']) { + getUniqueNumbers(); + } + else { + getUniqueNames(); + } + + return variations; +} + +function ensureUniqueLongname(newDoclet) { + var doclets = { + oldDoclet: functionDoclets[newDoclet.longname], + newDoclet: newDoclet + }; + var docletKeys = Object.keys(doclets); + var oldDocletLongname; + var variations = {}; + + if (doclets.oldDoclet) { + oldDocletLongname = doclets.oldDoclet.longname; + // if the shared longname has a variation, like MyClass#myLongname(variation), + // remove the variation + if (doclets.oldDoclet.variation || doclets.oldDoclet.variation === '') { + docletKeys.forEach(function(doclet) { + doclets[doclet].longname = doclets[doclet].longname.replace(/\([\s\S]*\)$/, ''); + doclets[doclet].variation = null; + }); + } + + variations = getUniqueVariations(doclets); + + // update the longnames/variations + docletKeys.forEach(function(doclet) { + doclets[doclet].longname += '(' + variations[doclet] + ')'; + doclets[doclet].variation = variations[doclet]; + }); + + // update the old doclet in the lookup table + functionDoclets[oldDocletLongname] = null; + functionDoclets[doclets.oldDoclet.longname] = doclets.oldDoclet; + } + + // always store the new doclet in the lookup table + functionDoclets[doclets.newDoclet.longname] = doclets.newDoclet; + + return doclets.newDoclet; +} + +exports.handlers = { + parseBegin: function() { + functionDoclets = {}; + }, + + newDoclet: function(e) { + if (e.doclet.kind === 'function') { + e.doclet = ensureUniqueLongname(e.doclet); + } + }, + + parseComplete: function() { + functionDoclets = null; + } +}; diff --git a/third_party/jsdoc/plugins/partial.js b/third_party/jsdoc/plugins/partial.js new file mode 100644 index 0000000000..9b5171579f --- /dev/null +++ b/third_party/jsdoc/plugins/partial.js @@ -0,0 +1,31 @@ +/** + @overview Adds support for reusable partial jsdoc files. + @module plugins/partial + @author Ludo Antonov + */ +'use strict'; + +var fs = require('jsdoc/fs'); +var path = require('path'); + +exports.handlers = { + /** + * Include a partial jsdoc + * + * @param e + * @param e.filename + * @param e.source + * @example + * @partial "partial_doc.jsdoc" + */ + beforeParse: function(e) { + e.source = e.source.replace(/(@partial \".*\")+/g, function($) { + var pathArg = $.match(/\".*\"/)[0].replace(/"/g,''); + var fullPath = path.join(e.filename , '..', pathArg); + + var partialData = fs.readFileSync(fullPath, global.env.opts.encoding); + + return partialData; + }); + } +}; diff --git a/third_party/jsdoc/plugins/railsTemplate.js b/third_party/jsdoc/plugins/railsTemplate.js new file mode 100644 index 0000000000..270381c6bb --- /dev/null +++ b/third_party/jsdoc/plugins/railsTemplate.js @@ -0,0 +1,20 @@ +/** + @overview Strips the rails template tags from a js.erb file + @module plugins/railsTemplate + @author Jannon Frank + */ +'use strict'; + +exports.handlers = { + /** + * Remove rails tags from the source input (e.g. <% foo bar %>) + * @param e + * @param e.filename + * @param e.source + */ + beforeParse: function(e) { + if (e.filename.match(/\.erb$/)) { + e.source = e.source.replace(/<%.*%>/g, ''); + } + } +}; diff --git a/third_party/jsdoc/plugins/shout.js b/third_party/jsdoc/plugins/shout.js new file mode 100644 index 0000000000..05c5d9f8aa --- /dev/null +++ b/third_party/jsdoc/plugins/shout.js @@ -0,0 +1,17 @@ +/** + @overview This is just an example. + @module plugins/shout + @author Michael Mathews + */ +'use strict'; + +exports.handlers = { + /** + Make your descriptions more shoutier. + */ + newDoclet: function(e) { + if (typeof e.doclet.description === 'string') { + e.doclet.description = e.doclet.description.toUpperCase(); + } + } +}; diff --git a/third_party/jsdoc/plugins/sourcetag.js b/third_party/jsdoc/plugins/sourcetag.js new file mode 100644 index 0000000000..c773283d81 --- /dev/null +++ b/third_party/jsdoc/plugins/sourcetag.js @@ -0,0 +1,51 @@ +/** + @module plugins/sourcetag + @author Michael Mathews + */ +'use strict'; + +var logger = require('jsdoc/util/logger'); + +exports.handlers = { + /** + Support @source tag. Expected value like: + { "filename": "myfile.js", "lineno": 123 } + Modifies the corresponding meta values on the given doclet. + + WARNING: If you are using a JSDoc template that generates pretty-printed source files, + such as JSDoc's default template, this plugin can cause JSDoc to crash. To fix this issue, + update your template settings to disable pretty-printed source files. + + @source { "filename": "sourcetag.js", "lineno": 13 } + */ + newDoclet: function(e) { + var tags = e.doclet.tags, + tag, + value; + + // any user-defined tags in this doclet? + if (typeof tags !== 'undefined') { + // only interested in the @source tags + tags = tags.filter(function($) { + return $.title === 'source'; + }); + + if (tags.length) { + // take the first one + tag = tags[0]; + + try { + value = JSON.parse(tag.value); + } + catch(e) { + logger.error('@source tag expects a valid JSON value, like { "filename": "myfile.js", "lineno": 123 }.'); + return; + } + + e.doclet.meta = e.doclet.meta || {}; + e.doclet.meta.filename = value.filename || ''; + e.doclet.meta.lineno = value.lineno || ''; + } + } + } +}; diff --git a/third_party/jsdoc/plugins/summarize.js b/third_party/jsdoc/plugins/summarize.js new file mode 100644 index 0000000000..da72f73869 --- /dev/null +++ b/third_party/jsdoc/plugins/summarize.js @@ -0,0 +1,61 @@ +/** + * @overview This plugin creates a summary tag, if missing, from the first sentence in the + * description. + * @module plugins/summarize + * @author Mads Bondo Dydensborg + */ +'use strict'; + +exports.handlers = { + /** + * Autogenerate summaries, if missing, from the description, if present. + */ + newDoclet: function(e) { + var endTag; + var tags; + var stack; + + // If the summary is missing, grab the first sentence from the description + // and use that. + if (e.doclet && !e.doclet.summary && e.doclet.description) { + // The summary may end with `.$`, `. `, or `.<` (a period followed by an HTML tag). + e.doclet.summary = e.doclet.description.split(/\.$|\.\s|\.]+>/g) || []; + stack = []; + + tags.forEach(function(tag) { + var idx = tag.indexOf('/'); + + if (idx === -1) { + // start tag -- push onto the stack + stack.push(tag); + } else if (idx === 1) { + // end tag -- pop off of the stack + stack.pop(); + } + + // otherwise, it's a self-closing tag; don't modify the stack + }); + + // stack should now contain only the start tags that lack end tags, + // with the most deeply nested start tag at the top + while (stack.length > 0) { + // pop the unmatched tag off the stack + endTag = stack.pop(); + // get just the tag name + endTag = endTag.substring(1, endTag.search(/[ >]/)); + // append the end tag + e.doclet.summary += ''; + } + + // and, finally, if the summary starts and ends with a

    tag, remove it; let the + // template decide whether to wrap the summary in a

    tag + e.doclet.summary = e.doclet.summary.replace(/^

    (.*)<\/p>$/i, '$1'); + } + } +}; diff --git a/third_party/jsdoc/plugins/test/fixtures/markdown.js b/third_party/jsdoc/plugins/test/fixtures/markdown.js new file mode 100644 index 0000000000..a727d65568 --- /dev/null +++ b/third_party/jsdoc/plugins/test/fixtures/markdown.js @@ -0,0 +1,24 @@ +'use strict'; + +/** + * @see [Nowhere](http://nowhere.com) + */ +function foo() {} + +/** + * @see AnObject#myProperty + */ +function bar() {} + +/** + * @classdesc My class. + * @description My class. + * @exception {Error} Some error. + * @param {string} myParam - My parameter. + * @property {string} value - Value of myParam. + * @return {MyClass} Class instance. + * @see [Example Inc.](http://example.com) + */ +function MyClass(myParam) { + this.value = myParam; +} diff --git a/third_party/jsdoc/plugins/test/fixtures/overloadHelper.js b/third_party/jsdoc/plugins/test/fixtures/overloadHelper.js new file mode 100644 index 0000000000..12c298c628 --- /dev/null +++ b/third_party/jsdoc/plugins/test/fixtures/overloadHelper.js @@ -0,0 +1,50 @@ +/** + * A bowl of non-spicy soup. + * @class + *//** + * A bowl of spicy soup. + * @class + * @param {number} spiciness - The spiciness of the soup, in Scoville heat units (SHU). + */ +function Soup(spiciness) {} + +/** + * Slurp the soup. + *//** + * Slurp the soup loudly. + * @param {number} dBA - The slurping volume, in A-weighted decibels. + */ +Soup.prototype.slurp = function(dBA) {}; + +/** + * Salt the soup as needed, using a highly optimized soup-salting heuristic. + *//** + * Salt the soup, specifying the amount of salt to add. + * @variation mg + * @param {number} amount - The amount of salt to add, in milligrams. + */ +Soup.prototype.salt = function(amount) {}; + +/** + * Heat the soup by the specified number of degrees. + * @param {number} degrees - The number of degrees, in Fahrenheit, by which to heat the soup. + *//** + * Heat the soup by the specified number of degrees. + * @variation 1 + * @param {string} degrees - The number of degrees, in Fahrenheit, by which to heat the soup, but + * as a string for some reason. + *//** + * Heat the soup by the specified number of degrees. + * @param {boolean} degrees - The number of degrees, as a boolean. Wait, what? + */ +Soup.prototype.heat = function(degrees) {}; + +/** + * Discard the soup. + * @variation discardSoup + *//** + * Discard the soup by pouring it into the specified container. + * @variation discardSoup + * @param {Object} container - The container in which to discard the soup. + */ +Soup.prototype.discard = function(container) {}; diff --git a/third_party/jsdoc/plugins/test/fixtures/railsTemplate.js.erb b/third_party/jsdoc/plugins/test/fixtures/railsTemplate.js.erb new file mode 100644 index 0000000000..c3df649549 --- /dev/null +++ b/third_party/jsdoc/plugins/test/fixtures/railsTemplate.js.erb @@ -0,0 +1,19 @@ +/** + @overview Strips the rails template tags from a js.erb file + @module plugins/railsTemplate + @author Jannon Frank + */ + +exports.handlers = { + /** + * Remove rails tags from the source input (e.g. <% foo bar %>) + * @param e + * @param e.filename + * @param e.source + */ + beforeParse: function(e) { + if (e.filename.match(/\.erb$/)) { + e.source = e.source.replace(/<%.*%> /g, ""); + } + } +}; \ No newline at end of file diff --git a/third_party/jsdoc/plugins/test/specs/commentConvert.js b/third_party/jsdoc/plugins/test/specs/commentConvert.js new file mode 100644 index 0000000000..dc29b80d72 --- /dev/null +++ b/third_party/jsdoc/plugins/test/specs/commentConvert.js @@ -0,0 +1,19 @@ +/*global describe: true, env: true, expect: true, it: true, jasmine: true */ +describe("commentConvert plugin", function() { + var parser = jasmine.createParser(); + var path = require('jsdoc/path'); + + var docSet; + + var pluginPath = 'plugins/commentConvert'; + var pluginPathResolved = path.join(env.dirname, pluginPath); + var plugin = require(pluginPathResolved); + + require('jsdoc/plugins').installPlugins([pluginPathResolved], parser); + docSet = jasmine.getDocSetFromFile(pluginPath + '.js', parser); + + it("should convert '///-style comments into jsdoc comments", function() { + var doclet = docSet.getByLongname("module:plugins/commentConvert.handlers.beforeParse"); + expect(doclet.length).toEqual(1); + }); +}); diff --git a/third_party/jsdoc/plugins/test/specs/escapeHtml.js b/third_party/jsdoc/plugins/test/specs/escapeHtml.js new file mode 100644 index 0000000000..06989389a0 --- /dev/null +++ b/third_party/jsdoc/plugins/test/specs/escapeHtml.js @@ -0,0 +1,19 @@ +/*global describe: true, env: true, expect: true, it: true, jasmine: true */ +describe("escapeHtml plugin", function() { + var parser = jasmine.createParser(); + var path = require('jsdoc/path'); + + var docSet; + + var pluginPath = 'plugins/escapeHtml'; + var pluginPathResolved = path.join(env.dirname,pluginPath); + var plugin = require(pluginPathResolved); + + require('jsdoc/plugins').installPlugins([pluginPathResolved], parser); + docSet = jasmine.getDocSetFromFile(pluginPath + '.js', parser); + + it("should escape '&', '<' and newlines in doclet descriptions", function() { + var doclet = docSet.getByLongname("module:plugins/escapeHtml.handlers.newDoclet"); + expect(doclet[0].description).toEqual("Translate HTML tags in descriptions into safe entities.
    Replaces <, & and newlines"); + }); +}); diff --git a/third_party/jsdoc/plugins/test/specs/markdown.js b/third_party/jsdoc/plugins/test/specs/markdown.js new file mode 100644 index 0000000000..af3a070646 --- /dev/null +++ b/third_party/jsdoc/plugins/test/specs/markdown.js @@ -0,0 +1,48 @@ +'use strict'; + +var path = require('jsdoc/path'); + +describe('markdown plugin', function() { + var pluginPath = 'plugins/markdown'; + var pluginPathResolved = path.join(global.env.dirname, pluginPath); + var plugin = require(pluginPathResolved); + + var docSet = jasmine.getDocSetFromFile('plugins/test/fixtures/markdown.js'); + + // TODO: more tests; refactor the plugin so multiple settings can be tested + + it('should process the correct tags by default', function() { + var myClass = docSet.getByLongname('MyClass')[0]; + + plugin.handlers.newDoclet({ doclet: myClass }); + [ + myClass.classdesc, + myClass.description, + myClass.exceptions[0].description, + myClass.params[0].description, + myClass.properties[0].description, + myClass.returns[0].description, + myClass.see + ].forEach(function(value) { + // if we processed the value, it should be wrapped in a

    tag + expect( /^

    (?:.+)<\/p>$/.test(value) ).toBe(true); + }); + }); + + describe('@see tag support', function() { + var foo = docSet.getByLongname('foo')[0]; + var bar = docSet.getByLongname('bar')[0]; + + it('should parse @see tags containing links', function() { + plugin.handlers.newDoclet({ doclet: foo }); + expect(typeof foo).toEqual('object'); + expect(foo.see[0]).toEqual('

    Nowhere

    '); + }); + + it('should not parse @see tags that do not contain links', function() { + plugin.handlers.newDoclet({ doclet: bar }); + expect(typeof bar).toEqual('object'); + expect(bar.see[0]).toEqual('AnObject#myProperty'); + }); + }); +}); diff --git a/third_party/jsdoc/plugins/test/specs/overloadHelper.js b/third_party/jsdoc/plugins/test/specs/overloadHelper.js new file mode 100644 index 0000000000..0538fa4cfa --- /dev/null +++ b/third_party/jsdoc/plugins/test/specs/overloadHelper.js @@ -0,0 +1,101 @@ +/*global describe: true, env: true, expect: true, it: true, jasmine: true, xit: true */ +describe('plugins/overloadHelper', function() { + var parser = jasmine.createParser(); + var path = require('jsdoc/path'); + + var docSet; + + var pluginPath = 'plugins/overloadHelper'; + var pluginPathResolved = path.resolve(env.dirname, pluginPath); + var plugin = require(pluginPathResolved); + + require('jsdoc/plugins').installPlugins([pluginPathResolved], parser); + docSet = jasmine.getDocSetFromFile('plugins/test/fixtures/overloadHelper.js', parser); + + it('should exist', function() { + expect(plugin).toBeDefined(); + expect(typeof plugin).toBe('object'); + }); + + it('should export handlers', function() { + expect(plugin.handlers).toBeDefined(); + expect(typeof plugin.handlers).toBe('object'); + }); + + it('should export a "newDoclet" handler', function() { + expect(plugin.handlers.newDoclet).toBeDefined(); + expect(typeof plugin.handlers.newDoclet).toBe('function'); + }); + + it('should export a "parseComplete" handler', function() { + expect(plugin.handlers.parseComplete).toBeDefined(); + expect(typeof plugin.handlers.parseComplete).toBe('function'); + }); + + describe('newDoclet handler', function() { + it('should not add unique longnames to constructors', function() { + var soup = docSet.getByLongname('Soup'); + var soup1 = docSet.getByLongname('Soup()'); + var soup2 = docSet.getByLongname('Soup(spiciness)'); + + expect(soup.length).toBe(2); + expect(soup1.length).toBe(0); + expect(soup2.length).toBe(0); + }); + + it('should add unique longnames to methods', function() { + var slurp = docSet.getByLongname('Soup#slurp'); + var slurp1 = docSet.getByLongname('Soup#slurp()'); + var slurp2 = docSet.getByLongname('Soup#slurp(dBA)'); + + expect(slurp.length).toBe(0); + expect(slurp1.length).toBe(1); + expect(slurp2.length).toBe(1); + }); + + it('should update the "variation" property of the method', function() { + var slurp1 = docSet.getByLongname('Soup#slurp()')[0]; + var slurp2 = docSet.getByLongname('Soup#slurp(dBA)')[0]; + + expect(slurp1.variation).toBe(''); + expect(slurp2.variation).toBe('dBA'); + }); + + it('should not add to or change existing variations that are unique', function() { + var salt1 = docSet.getByLongname('Soup#salt'); + var salt2 = docSet.getByLongname('Soup#salt(mg)'); + + expect(salt1.length).toBe(1); + expect(salt2.length).toBe(1); + }); + + it('should not duplicate the names of existing numeric variations', function() { + var heat1 = docSet.getByLongname('Soup#heat(1)'); + var heat2 = docSet.getByLongname('Soup#heat(2)'); + var heat3 = docSet.getByLongname('Soup#heat(3)'); + + expect(heat1.length).toBe(1); + expect(heat2.length).toBe(1); + expect(heat3.length).toBe(1); + }); + + it('should replace identical variations with new, unique variations', function() { + var discard1 = docSet.getByLongname('Soup#discard()'); + var discard2 = docSet.getByLongname('Soup#discard(container)'); + + expect(discard1.length).toBe(1); + expect(discard2.length).toBe(1); + }); + }); + + describe('parseComplete handler', function() { + // disabled because on the second run, each comment is being parsed twice; who knows why... + xit('should not retain parse results between parser runs', function() { + parser.clear(); + docSet = jasmine.getDocSetFromFile('plugins/test/fixtures/overloadHelper.js', parser); + var heat = docSet.getByLongname('Soup#heat(4)'); + + expect(heat.length).toBe(0); + }); + }); +}); diff --git a/third_party/jsdoc/plugins/test/specs/railsTemplate.js b/third_party/jsdoc/plugins/test/specs/railsTemplate.js new file mode 100644 index 0000000000..feebd74ab3 --- /dev/null +++ b/third_party/jsdoc/plugins/test/specs/railsTemplate.js @@ -0,0 +1,17 @@ +/*global describe: true, env: true, expect: true, it: true, jasmine: true */ +describe("railsTemplate plugin", function() { + var parser = jasmine.createParser(); + var path = require('jsdoc/path'); + + var pluginPath = path.join(env.dirname, 'plugins/railsTemplate'); + var plugin = require(pluginPath); + + require('jsdoc/plugins').installPlugins([pluginPath], parser); + require('jsdoc/src/handlers').attachTo(parser); + + it("should remove <% %> rails template tags from the source of *.erb files", function() { + var docSet = parser.parse([path.join(env.dirname, "plugins/test/fixtures/railsTemplate.js.erb")]); + + expect(docSet[2].description).toEqual("Remove rails tags from the source input (e.g. )"); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/plugins/test/specs/shout.js b/third_party/jsdoc/plugins/test/specs/shout.js new file mode 100644 index 0000000000..7b591843e8 --- /dev/null +++ b/third_party/jsdoc/plugins/test/specs/shout.js @@ -0,0 +1,19 @@ +/*global describe: true, env: true, expect: true, it: true, jasmine: true */ +describe("shout plugin", function() { + var parser = jasmine.createParser(); + var path = require('jsdoc/path'); + + var docSet; + + var pluginPath = 'plugins/shout'; + var pluginPathResolved = path.join(env.dirname, pluginPath); + var plugin = require(pluginPathResolved); + + require('jsdoc/plugins').installPlugins([pluginPathResolved], parser); + docSet = jasmine.getDocSetFromFile(pluginPath + '.js', parser); + + it("should make the description uppercase", function() { + var doclet = docSet.getByLongname("module:plugins/shout.handlers.newDoclet"); + expect(doclet[0].description).toEqual("MAKE YOUR DESCRIPTIONS MORE SHOUTIER."); + }); +}); diff --git a/third_party/jsdoc/plugins/test/specs/sourcetag.js b/third_party/jsdoc/plugins/test/specs/sourcetag.js new file mode 100644 index 0000000000..5c6568b1bd --- /dev/null +++ b/third_party/jsdoc/plugins/test/specs/sourcetag.js @@ -0,0 +1,23 @@ +/*global describe, expect, it, jasmine */ +'use strict'; + +describe('sourcetag plugin', function() { + var parser = jasmine.createParser(); + var path = require('jsdoc/path'); + + var docSet; + + var pluginPath = 'plugins/sourcetag'; + var pluginPathResolved = path.join(global.env.dirname, pluginPath); + var plugin = require(pluginPathResolved); + + require('jsdoc/plugins').installPlugins([pluginPathResolved], parser); + docSet = jasmine.getDocSetFromFile(pluginPath + '.js', parser); + + it("should set the lineno and filename of the doclet's meta property", function() { + var doclet = docSet.getByLongname('module:plugins/sourcetag.handlers.newDoclet'); + expect(doclet[0].meta).toBeDefined(); + expect(doclet[0].meta.filename).toEqual('sourcetag.js'); + expect(doclet[0].meta.lineno).toEqual(13); + }); +}); diff --git a/third_party/jsdoc/plugins/test/specs/summarize.js b/third_party/jsdoc/plugins/test/specs/summarize.js new file mode 100644 index 0000000000..9fb5ab80a0 --- /dev/null +++ b/third_party/jsdoc/plugins/test/specs/summarize.js @@ -0,0 +1,112 @@ +/*global describe, expect, it */ +'use strict'; + +var summarize = require('../../summarize'); + +describe('summarize', function() { + it('should export handlers', function() { + expect(summarize.handlers).toBeDefined(); + expect(typeof summarize.handlers).toBe('object'); + }); + + it('should export a newDoclet handler', function() { + expect(summarize.handlers.newDoclet).toBeDefined(); + expect(typeof summarize.handlers.newDoclet).toBe('function'); + }); + + describe('newDoclet handler', function() { + var handler = summarize.handlers.newDoclet; + + it('should not blow up if the doclet is missing', function() { + function noDoclet() { + return handler({}); + } + + expect(noDoclet).not.toThrow(); + }); + + it('should not change the summary if it is already defined', function() { + var doclet = { + summary: 'This is a summary.', + description: 'Descriptions are good.' + }; + handler({ doclet: doclet }); + + expect(doclet.summary).not.toBe(doclet.description); + }); + + it('should not do anything if the description is missing', function() { + var doclet = {}; + handler({ doclet: doclet }); + + expect(doclet.summary).not.toBeDefined(); + }); + + it('should use the first sentence as the summary', function() { + var doclet = { + description: 'This sentence is the summary. This sentence is not.' + }; + handler({ doclet: doclet }); + + expect(doclet.summary).toBe('This sentence is the summary.'); + }); + + it('should not add an extra period if there is only one sentence in the description', + function() { + var doclet = { + description: 'This description has only one sentence.' + }; + handler({ doclet: doclet }); + + expect(doclet.summary).toBe('This description has only one sentence.'); + }); + + it('should use the entire description, plus a period, as the summary if the description ' + + 'does not contain a period', function() { + var doclet = { + description: 'This is a description' + }; + handler({ doclet: doclet }); + + expect(doclet.summary).toBe('This is a description.'); + }); + + it('should use the entire description as the summary if the description contains only ' + + 'one sentence', function() { + var doclet = { + description: 'This is a description.' + }; + handler({ doclet: doclet }); + + expect(doclet.description).toBe('This is a description.'); + }); + + it('should work when an HTML tag immediately follows the first sentence', function() { + var doclet = { + description: 'This sentence is the summary.This sentence is small.' + }; + handler({ doclet: doclet }); + + expect(doclet.summary).toBe('This sentence is the summary.'); + }); + + it('should generate valid HTML if a tag is opened, but not closed, in the summary', + function() { + var doclet = { + description: 'This description has a tag. The tag straddles sentences.' + }; + handler({ doclet: doclet }); + + expect(doclet.summary).toBe('This description has a tag.'); + }); + + it('should not include a

    tag in the summary', function() { + var doclet = { + description: '

    This description contains HTML.

    And plenty of it!

    ' + }; + handler({ doclet: doclet }); + + expect(doclet.summary).toBe('This description contains HTML.'); + }); + }); +}); diff --git a/third_party/jsdoc/rhino/MPL_2.0.txt b/third_party/jsdoc/rhino/MPL_2.0.txt new file mode 100644 index 0000000000..a612ad9813 --- /dev/null +++ b/third_party/jsdoc/rhino/MPL_2.0.txt @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/third_party/jsdoc/rhino/assert.js b/third_party/jsdoc/rhino/assert.js new file mode 100644 index 0000000000..5c762d0727 --- /dev/null +++ b/third_party/jsdoc/rhino/assert.js @@ -0,0 +1,315 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// UTILITY +var util = require('util'); +var pSlice = Array.prototype.slice; + +// 1. The assert module provides functions that throw +// AssertionError's when particular conditions are not met. The +// assert module must conform to the following interface. + +var assert = module.exports = ok; + +// 2. The AssertionError is defined in assert. +// new assert.AssertionError({ message: message, +// actual: actual, +// expected: expected }) + +assert.AssertionError = function AssertionError(options) { + this.name = 'AssertionError'; + this.actual = options.actual; + this.expected = options.expected; + this.operator = options.operator; + this.message = options.message || getMessage(this); +}; + +// assert.AssertionError instanceof Error +util.inherits(assert.AssertionError, Error); + +function replacer(key, value) { + if (util.isUndefined(value)) { + return '' + value; + } + if (util.isNumber(value) && (isNaN(value) || !isFinite(value))) { + return value.toString(); + } + if (util.isFunction(value) || util.isRegExp(value)) { + return value.toString(); + } + return value; +} + +function truncate(s, n) { + if (util.isString(s)) { + return s.length < n ? s : s.slice(0, n); + } else { + return s; + } +} + +function getMessage(self) { + return truncate(JSON.stringify(self.actual, replacer), 128) + ' ' + + self.operator + ' ' + + truncate(JSON.stringify(self.expected, replacer), 128); +} + +// At present only the three keys mentioned above are used and +// understood by the spec. Implementations or sub modules can pass +// other keys to the AssertionError's constructor - they will be +// ignored. + +// 3. All of the following functions must throw an AssertionError +// when a corresponding condition is not met, with a message that +// may be undefined if not provided. All assertion methods provide +// both the actual and expected values to the assertion error for +// display purposes. + +function fail(actual, expected, message, operator, stackStartFunction) { + throw new assert.AssertionError({ + message: message, + actual: actual, + expected: expected, + operator: operator, + stackStartFunction: stackStartFunction + }); +} + +// EXTENSION! allows for well behaved errors defined elsewhere. +assert.fail = fail; + +// 4. Pure assertion tests whether a value is truthy, as determined +// by !!guard. +// assert.ok(guard, message_opt); +// This statement is equivalent to assert.equal(true, !!guard, +// message_opt);. To test strictly for the value true, use +// assert.strictEqual(true, guard, message_opt);. + +function ok(value, message) { + if (!value) fail(value, true, message, '==', assert.ok); +} +assert.ok = ok; + +// 5. The equality assertion tests shallow, coercive equality with +// ==. +// assert.equal(actual, expected, message_opt); + +assert.equal = function equal(actual, expected, message) { + if (actual != expected) fail(actual, expected, message, '==', assert.equal); +}; + +// 6. The non-equality assertion tests for whether two objects are not equal +// with != assert.notEqual(actual, expected, message_opt); + +assert.notEqual = function notEqual(actual, expected, message) { + if (actual == expected) { + fail(actual, expected, message, '!=', assert.notEqual); + } +}; + +// 7. The equivalence assertion tests a deep equality relation. +// assert.deepEqual(actual, expected, message_opt); + +assert.deepEqual = function deepEqual(actual, expected, message) { + if (!_deepEqual(actual, expected)) { + fail(actual, expected, message, 'deepEqual', assert.deepEqual); + } +}; + +function _deepEqual(actual, expected) { + // 7.1. All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; + + } else if (util.isBuffer(actual) && util.isBuffer(expected)) { + if (actual.length != expected.length) return false; + + for (var i = 0; i < actual.length; i++) { + if (actual[i] !== expected[i]) return false; + } + + return true; + + // 7.2. If the expected value is a Date object, the actual value is + // equivalent if it is also a Date object that refers to the same time. + } else if (util.isDate(actual) && util.isDate(expected)) { + return actual.getTime() === expected.getTime(); + + // 7.3 If the expected value is a RegExp object, the actual value is + // equivalent if it is also a RegExp object with the same source and + // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`). + } else if (util.isRegExp(actual) && util.isRegExp(expected)) { + return actual.source === expected.source && + actual.global === expected.global && + actual.multiline === expected.multiline && + actual.lastIndex === expected.lastIndex && + actual.ignoreCase === expected.ignoreCase; + + // 7.4. Other pairs that do not both pass typeof value == 'object', + // equivalence is determined by ==. + } else if (!util.isObject(actual) && !util.isObject(expected)) { + return actual == expected; + + // 7.5 For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical 'prototype' property. Note: this + // accounts for both named and indexed properties on Arrays. + } else { + return objEquiv(actual, expected); + } +} + +function isArguments(object) { + return Object.prototype.toString.call(object) == '[object Arguments]'; +} + +function objEquiv(a, b) { + if (util.isNullOrUndefined(a) || util.isNullOrUndefined(b)) + return false; + // an identical 'prototype' property. + if (a.prototype !== b.prototype) return false; + //~~~I've managed to break Object.keys through screwy arguments passing. + // Converting to array solves the problem. + if (isArguments(a)) { + if (!isArguments(b)) { + return false; + } + a = pSlice.call(a); + b = pSlice.call(b); + return _deepEqual(a, b); + } + try { + var ka = Object.keys(a), + kb = Object.keys(b), + key, i; + } catch (e) {//happens when one is a string literal and the other isn't + return false; + } + // having the same number of owned properties (keys incorporates + // hasOwnProperty) + if (ka.length != kb.length) + return false; + //the same set of keys (although not necessarily the same order), + ka.sort(); + kb.sort(); + //~~~cheap key test + for (i = ka.length - 1; i >= 0; i--) { + if (ka[i] != kb[i]) + return false; + } + //equivalent values for every corresponding key, and + //~~~possibly expensive deep test + for (i = ka.length - 1; i >= 0; i--) { + key = ka[i]; + if (!_deepEqual(a[key], b[key])) return false; + } + return true; +} + +// 8. The non-equivalence assertion tests for any deep inequality. +// assert.notDeepEqual(actual, expected, message_opt); + +assert.notDeepEqual = function notDeepEqual(actual, expected, message) { + if (_deepEqual(actual, expected)) { + fail(actual, expected, message, 'notDeepEqual', assert.notDeepEqual); + } +}; + +// 9. The strict equality assertion tests strict equality, as determined by ===. +// assert.strictEqual(actual, expected, message_opt); + +assert.strictEqual = function strictEqual(actual, expected, message) { + if (actual !== expected) { + fail(actual, expected, message, '===', assert.strictEqual); + } +}; + +// 10. The strict non-equality assertion tests for strict inequality, as +// determined by !==. assert.notStrictEqual(actual, expected, message_opt); + +assert.notStrictEqual = function notStrictEqual(actual, expected, message) { + if (actual === expected) { + fail(actual, expected, message, '!==', assert.notStrictEqual); + } +}; + +function expectedException(actual, expected) { + if (!actual || !expected) { + return false; + } + + if (Object.prototype.toString.call(expected) == '[object RegExp]') { + return expected.test(actual); + } else if (actual instanceof expected) { + return true; + } else if (expected.call({}, actual) === true) { + return true; + } + + return false; +} + +function _throws(shouldThrow, block, expected, message) { + var actual; + + if (util.isString(expected)) { + message = expected; + expected = null; + } + + try { + block(); + } catch (e) { + actual = e; + } + + message = (expected && expected.name ? ' (' + expected.name + ').' : '.') + + (message ? ' ' + message : '.'); + + if (shouldThrow && !actual) { + fail(actual, expected, 'Missing expected exception' + message); + } + + if (!shouldThrow && expectedException(actual, expected)) { + fail(actual, expected, 'Got unwanted exception' + message); + } + + if ((shouldThrow && actual && expected && + !expectedException(actual, expected)) || (!shouldThrow && actual)) { + throw actual; + } +} + +// 11. Expected to throw an error: +// assert.throws(block, Error_opt, message_opt); + +assert.throws = function(block, /*optional*/error, /*optional*/message) { + _throws.apply(this, [true].concat(pSlice.call(arguments))); +}; + +// EXTENSION! This is annoying to write outside this module. +assert.doesNotThrow = function(block, /*optional*/message) { + _throws.apply(this, [false].concat(pSlice.call(arguments))); +}; + +assert.ifError = function(err) { if (err) {throw err;}}; diff --git a/third_party/jsdoc/rhino/crypto.js b/third_party/jsdoc/rhino/crypto.js new file mode 100644 index 0000000000..35e162b7d0 --- /dev/null +++ b/third_party/jsdoc/rhino/crypto.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('crypto-browserify'); diff --git a/third_party/jsdoc/rhino/events.js b/third_party/jsdoc/rhino/events.js new file mode 100644 index 0000000000..488025a5d0 --- /dev/null +++ b/third_party/jsdoc/rhino/events.js @@ -0,0 +1,292 @@ +/** + * Shim for Node.js `events` module. + * @see https://github.com/Gozala/events + * @license MIT + */ +'use strict'; + +function EventEmitter() { + this._events = this._events || {}; + this._maxListeners = this._maxListeners || undefined; +} +module.exports = EventEmitter; +if (!process.EventEmitter) { + process.EventEmitter = EventEmitter; +} + +// Backwards-compat with node 0.10.x +EventEmitter.EventEmitter = EventEmitter; + +EventEmitter.prototype._events = undefined; +EventEmitter.prototype._maxListeners = undefined; + +// By default EventEmitters will print a warning if more than 10 listeners are +// added to it. This is a useful default which helps finding memory leaks. +EventEmitter.defaultMaxListeners = 10; + +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +EventEmitter.prototype.setMaxListeners = function(n) { + if (!isNumber(n) || n < 0 || isNaN(n)) + throw TypeError('n must be a positive number'); + this._maxListeners = n; + return this; +}; + +EventEmitter.prototype.emit = function(type) { + var er, handler, len, args, i, listeners; + + if (!this._events) + this._events = {}; + + // If there is no 'error' event listener then throw. + if (type === 'error') { + if (!this._events.error || + (isObject(this._events.error) && !this._events.error.length)) { + er = arguments[1]; + if (er instanceof Error) { + throw er; // Unhandled 'error' event + } else { + throw TypeError('Uncaught, unspecified "error" event.'); + } + return false; + } + } + + handler = this._events[type]; + + if (isUndefined(handler)) + return false; + + if (isFunction(handler)) { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + len = arguments.length; + args = new Array(len - 1); + for (i = 1; i < len; i++) + args[i - 1] = arguments[i]; + handler.apply(this, args); + } + } else if (isObject(handler)) { + len = arguments.length; + args = new Array(len - 1); + for (i = 1; i < len; i++) + args[i - 1] = arguments[i]; + + listeners = handler.slice(); + len = listeners.length; + for (i = 0; i < len; i++) + listeners[i].apply(this, args); + } + + return true; +}; + +EventEmitter.prototype.addListener = function(type, listener) { + var m; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events) + this._events = {}; + + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (this._events.newListener) + this.emit('newListener', type, + isFunction(listener.listener) ? + listener.listener : listener); + + if (!this._events[type]) + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + else if (isObject(this._events[type])) + // If we've already got an array, just append. + this._events[type].push(listener); + else + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + + // Check for listener leak + if (isObject(this._events[type]) && !this._events[type].warned) { + var m; + if (!isUndefined(this._maxListeners)) { + m = this._maxListeners; + } else { + m = EventEmitter.defaultMaxListeners; + } + + if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + this._events[type].length); + if (typeof console.trace === 'function') { + // not supported in IE 10 + console.trace(); + } + } + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener) { + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + var fired = false; + + function g() { + this.removeListener(type, g); + + if (!fired) { + fired = true; + listener.apply(this, arguments); + } + } + + g.listener = listener; + this.on(type, g); + + return this; +}; + +// emits a 'removeListener' event iff the listener was removed +EventEmitter.prototype.removeListener = function(type, listener) { + var list, position, length, i; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events || !this._events[type]) + return this; + + list = this._events[type]; + length = list.length; + position = -1; + + if (list === listener || + (isFunction(list.listener) && list.listener === listener)) { + delete this._events[type]; + if (this._events.removeListener) + this.emit('removeListener', type, listener); + + } else if (isObject(list)) { + for (i = length; i-- > 0;) { + if (list[i] === listener || + (list[i].listener && list[i].listener === listener)) { + position = i; + break; + } + } + + if (position < 0) + return this; + + if (list.length === 1) { + list.length = 0; + delete this._events[type]; + } else { + list.splice(position, 1); + } + + if (this._events.removeListener) + this.emit('removeListener', type, listener); + } + + return this; +}; + +EventEmitter.prototype.removeAllListeners = function(type) { + var key, listeners; + + if (!this._events) + return this; + + // not listening for removeListener, no need to emit + if (!this._events.removeListener) { + if (arguments.length === 0) + this._events = {}; + else if (this._events[type]) + delete this._events[type]; + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (key in this._events) { + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = {}; + return this; + } + + listeners = this._events[type]; + + if (isFunction(listeners)) { + this.removeListener(type, listeners); + } else { + // LIFO order + while (listeners.length) + this.removeListener(type, listeners[listeners.length - 1]); + } + delete this._events[type]; + + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + var ret; + if (!this._events || !this._events[type]) + ret = []; + else if (isFunction(this._events[type])) + ret = [this._events[type]]; + else + ret = this._events[type].slice(); + return ret; +}; + +EventEmitter.listenerCount = function(emitter, type) { + var ret; + if (!emitter._events || !emitter._events[type]) + ret = 0; + else if (isFunction(emitter._events[type])) + ret = 1; + else + ret = emitter._events[type].length; + return ret; +}; + +function isFunction(arg) { + return typeof arg === 'function'; +} + +function isNumber(arg) { + return typeof arg === 'number'; +} + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} + +function isUndefined(arg) { + return arg === void 0; +} \ No newline at end of file diff --git a/third_party/jsdoc/rhino/fs.js b/third_party/jsdoc/rhino/fs.js new file mode 100644 index 0000000000..8261e31991 --- /dev/null +++ b/third_party/jsdoc/rhino/fs.js @@ -0,0 +1,167 @@ +/** + * Partial Rhino shim for Node.js' `fs` module. + * @see http://nodejs.org/api/fs.html + */ +'use strict'; + +var path = require('path'); +var util = require('util'); + +var asyncify = path._asyncify; + +function checkEncoding(enc, name) { + // we require the `encoding` parameter for Node.js compatibility; on Node.js, if you omit the + // encoding, you get a stream instead of a string + if (!enc || typeof enc === 'function') { + throw new Error(name + ' requires an encoding on Rhino!'); + } + + // Node.js wants 'utf8', but Java wants 'utf-8' + if (enc === 'utf8') { + enc = 'utf-8'; + } + + return enc; +} + +// provide an error that's consistent with Node.js +function errorFactory(filepath) { + return new Error( util.format("ENOENT, no such file or directory '%s'", filepath) ); +} + +exports.readFileSync = function readFileSync(filename, encoding) { + encoding = checkEncoding(encoding, 'fs.readFile[Sync]'); + + return readFile(filename, encoding); +}; +exports.readFile = asyncify(exports.readFileSync); + +// in node 0.8, path.exists() and path.existsSync() moved to the "fs" module +exports.existsSync = path.existsSync; +exports.exists = path.exists; + +var statSync = exports.statSync = function statSync(_path) { + var f = new java.io.File(_path); + if (!f) { + throw errorFactory(_path); + } + + return { + isFile: function isFile() { + return f.isFile(); + }, + isDirectory: function isDirectory() { + return f.isDirectory(); + }, + isSymlink: function isSymlink() { + // java.io.File resolves symlinks + return false; + } + }; +}; +exports.stat = asyncify(statSync); + +// java.io.File resolves symlinks, so we can alias `lstat` to `stat` +var lstatSync = exports.lstatSync = statSync; +exports.lstat = asyncify(lstatSync); + +var readdirSync = exports.readdirSync = function readdirSync(_path) { + var dir; + var files; + + dir = new java.io.File(_path); + if (!dir.directory) { + throw errorFactory(_path); + } + + files = dir.list() + .map(function(fileName) { + return String(fileName); + }); + + return files; +}; +exports.readdir = asyncify(readdirSync); + +// JSDoc extension to `fs` module +var toDir = exports.toDir = function toDir(_path) { + var f; + + _path = path.normalize(_path); + f = new java.io.File( path.resolve(global.env.pwd, _path) ); + + if ( f.isDirectory() ){ + return _path; + } else { + return path.dirname(_path); + } +}; + +var mkdirSync = exports.mkdirSync = function mkdirSync(_path) { + var dir_path = toDir(_path); + ( new java.io.File(dir_path) ).mkdir(); +}; +exports.mkdir = asyncify(mkdirSync); + +// JSDoc extension to `fs` module +exports.mkPath = function mkPath(_path) { + if ( Array.isArray(_path) ) { + _path = _path.join(''); + } + + ( new java.io.File(path.resolve(global.env.pwd, _path)) ).mkdirs(); +}; + +// JSDoc extension to `fs` module +exports.copyFileSync = function copyFileSync(inFile, outDir, fileName) { + if (fileName === undefined || fileName === null) { + fileName = path.basename(inFile); + } + + outDir = toDir(outDir); + + inFile = new java.io.File(inFile); + var outFile = new java.io.File(outDir + '/' + fileName); + + var bis = new java.io.BufferedInputStream(new java.io.FileInputStream(inFile), 4096); + var bos = new java.io.BufferedOutputStream(new java.io.FileOutputStream(outFile), 4096); + var theChar = bis.read(); + while (theChar !== -1) { + bos.write(theChar); + theChar = bis.read(); + } + + bos.close(); + bis.close(); +}; +exports.copyFile = asyncify(exports.copyFileSync); + +exports.writeFileSync = function writeFileSync(filename, data, encoding) { + encoding = checkEncoding(encoding, 'fs.writeFile[Sync]'); + + var out = new java.io.PrintWriter( + new java.io.OutputStreamWriter( + new java.io.FileOutputStream(filename), + encoding + ) + ); + + try { + out.write(data); + } + finally { + out.flush(); + out.close(); + } +}; +exports.writeFile = asyncify(exports.writeFileSync); + +exports.rmdirSync = function rmdirSync(_path) { + throw new Error('not implemented'); +}; +exports.rmdir = asyncify(exports.rmdirSync); + +exports.unlinkSync = function unlinkSync(_path) { + throw new Error('not implemented'); +}; +exports.unlink = asyncify(exports.unlinkSync); diff --git a/third_party/jsdoc/rhino/js.jar b/third_party/jsdoc/rhino/js.jar new file mode 100644 index 0000000000..696a37d4f9 Binary files /dev/null and b/third_party/jsdoc/rhino/js.jar differ diff --git a/third_party/jsdoc/rhino/jsdoc/src/astbuilder.js b/third_party/jsdoc/rhino/jsdoc/src/astbuilder.js new file mode 100644 index 0000000000..1d62560e2f --- /dev/null +++ b/third_party/jsdoc/rhino/jsdoc/src/astbuilder.js @@ -0,0 +1,18 @@ +/*global Packages: true */ +/** + * Creates an Esprima-compatible AST using Rhino's JavaScript parser. + * @module rhino/jsdoc/src/astbuilder + */ +'use strict'; + +var AstBuilder = exports.AstBuilder = function() { + this._builder = new Packages.org.jsdoc.AstBuilder(); +}; + +AstBuilder.prototype.build = function(sourceCode, sourceName) { + return this._builder.build(sourceCode, sourceName); +}; + +AstBuilder.prototype.getRhinoNodes = function() { + return this._builder.getRhinoNodes(); +}; diff --git a/third_party/jsdoc/rhino/jsdoc/src/parser.js b/third_party/jsdoc/rhino/jsdoc/src/parser.js new file mode 100644 index 0000000000..1b1c05b216 --- /dev/null +++ b/third_party/jsdoc/rhino/jsdoc/src/parser.js @@ -0,0 +1,38 @@ +// TODO: module docs +'use strict'; + +// TODO: docs +exports.createParser = require('jsdoc/src/parser').createParser; + +// TODO: docs +var Parser = exports.Parser = function() { + var astBuilder; + var visitor; + + var runtime = require('jsdoc/util/runtime'); + if ( !runtime.isRhino() ) { + throw new Error('You must run JSDoc on Mozilla Rhino to use the Rhino parser.'); + } + + astBuilder = new ( require(runtime.getModulePath('jsdoc/src/astbuilder')) ).AstBuilder(); + visitor = new ( require(runtime.getModulePath('jsdoc/src/visitor')) ).Visitor(this); + + Parser.super_.call(this, astBuilder, visitor); +}; +require('util').inherits(Parser, require('jsdoc/src/parser').Parser); + +// TODO: update docs +/** + * Adds a node visitor to use in parsing + */ +Parser.prototype.addNodeVisitor = function(visitor) { + this._visitor.addRhinoNodeVisitor(visitor); +}; + +// TODO: docs +/** + * Get the node visitors used in parsing + */ +Parser.prototype.getNodeVisitors = function() { + return this._visitor.getRhinoNodeVisitors(); +}; diff --git a/third_party/jsdoc/rhino/jsdoc/src/visitor.js b/third_party/jsdoc/rhino/jsdoc/src/visitor.js new file mode 100644 index 0000000000..0295f2df15 --- /dev/null +++ b/third_party/jsdoc/rhino/jsdoc/src/visitor.js @@ -0,0 +1,57 @@ +// TODO: module docs +'use strict'; + +// TODO: docs +var Visitor = exports.Visitor = function(parser) { + var runtime = require('jsdoc/util/runtime'); + if ( !runtime.isRhino() ) { + throw new Error('You must run JSDoc on Mozilla Rhino to use the Rhino node visitor.'); + } + + Visitor.super_.call(this, parser); + + // Rhino node visitors added by plugins (deprecated in JSDoc 3.3) + this._rhinoNodeVisitors = []; + // Rhino nodes retrieved from the org.jsdoc.AstBuilder instance + this._rhinoNodes = null; + + this.addAstNodeVisitor({ + visitNode: this._visitRhinoNode.bind(this) + }); +}; +require('util').inherits(Visitor, require('jsdoc/src/visitor').Visitor); + +// TODO: docs (deprecated) +Visitor.prototype.addRhinoNodeVisitor = function(visitor) { + this._rhinoNodeVisitors.push(visitor); +}; + +// TODO: docs (deprecated) +Visitor.prototype.getRhinoNodeVisitors = function() { + return this._rhinoNodeVisitors; +}; + +// TODO: docs (deprecated) +Visitor.prototype._visitRhinoNode = function(astNode, e, parser, filename) { + var rhinoNode; + + var visitors = this._rhinoNodeVisitors; + // if there are no visitors, bail out before we retrieve all the nodes + if (!visitors.length) { + return; + } + + if (!this._rhinoNodes) { + this._rhinoNodes = parser.astBuilder.getRhinoNodes(); + } + + rhinoNode = this._rhinoNodes ? this._rhinoNodes.get(astNode.nodeId) : null; + if (rhinoNode) { + for (var i = 0, l = visitors.length; i < l; i++) { + visitors[i].visitNode(rhinoNode, e, parser, filename); + if (e.stopPropagation) { + break; + } + } + } +}; diff --git a/third_party/jsdoc/rhino/os.js b/third_party/jsdoc/rhino/os.js new file mode 100644 index 0000000000..e3d2b78719 --- /dev/null +++ b/third_party/jsdoc/rhino/os.js @@ -0,0 +1,19 @@ +/** + * Partial Rhino implementation of Node.js' `os` module. + * @module os + * @author Jeff Williams + * @see http://nodejs.org/api/os.html + */ +'use strict'; + +exports.EOL = String( java.lang.System.getProperty('line.separator') ); + +// clearly not accurate, but probably good enough +exports.platform = function() { + if ( String(java.lang.System.getProperty('os.name')).match(/^[Ww]in/) ) { + return 'win32'; + } + else { + return 'linux'; + } +}; \ No newline at end of file diff --git a/third_party/jsdoc/rhino/path.js b/third_party/jsdoc/rhino/path.js new file mode 100644 index 0000000000..c4218f939d --- /dev/null +++ b/third_party/jsdoc/rhino/path.js @@ -0,0 +1,432 @@ +'use strict'; + +var isWindows = java.lang.System.getProperty("os.name").toLowerCase().contains("windows"); +var fileSeparator = exports.sep = String( java.lang.System.getProperty("file.separator") ); + +function noOp() {} + +// exported for the benefit of our `fs` shim +var asyncify = exports._asyncify = function(func) { + return function() { + var args = Array.prototype.slice.call(arguments); + var callback = args.pop(); + var data; + + callback = typeof callback === 'function' ? callback : noOp; + + try { + data = func.apply(this, args); + process.nextTick(function() { + callback(null, data); + }); + } + catch (e) { + process.nextTick(function() { + callback(e); + }); + } + }; +}; + +/** + * Returns everything on a path except for the last item + * e.g. if the path was 'path/to/something', the return value would be 'path/to' + */ +exports.dirname = function(_path) { + var f = new java.io.File(_path); + return String(f.getParent()); +}; + +/** + * Returns the last item on a path + */ +exports.basename = function(_path, ext) { + var f = new java.io.File(_path); + var p = f.getParentFile(); + var base = String(f.getName()); + if (p != null) { + var idx = ext ? base.indexOf(ext) : -1; + if (idx !== -1) { + base = base.substring(0, base.length - ext.length); + } + } + return base; +}; + +exports.existsSync = function(_path) { + var f = new java.io.File(_path); + + if (f.isDirectory()){ + return true; + } + if (!f.exists()){ + return false; + } + if (!f.canRead()){ + return false; + } + return true; +}; + +exports.exists = asyncify(exports.existsSync); + +//Code below taken from node + +//resolves . and .. elements in a path array with directory names there +//must be no slashes, empty elements, or device names (c:\) in the array +//(so also no leading and trailing slashes - it does not distinguish +//relative and absolute paths) +function normalizeArray(parts, allowAboveRoot) { + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for ( var i = parts.length - 1; i >= 0; i--) { + var last = parts[i]; + if (last == '.') { + parts.splice(i, 1); + } else if (last === '..') { + parts.splice(i, 1); + up++; + } else if (up) { + parts.splice(i, 1); + up--; + } + } + + // if the path is allowed to go above the root, restore leading ..s + if (allowAboveRoot) { + for (; up--; up) { + parts.unshift('..'); + } + } + + return parts; +} + +exports.extname = function(path) { + return splitPath(path)[3]; +}; + +if (isWindows) { + // Regex to split a windows path into three parts: [*, device, slash, + // tail] windows-only + var splitDeviceRe = + /^([a-zA-Z]:|[\\\/]{2}[^\\\/]+[\\\/][^\\\/]+)?([\\\/])?([\s\S]*?)$/; + + // Regex to split the tail part of the above into [*, dir, basename, ext] + var splitTailRe = + /^([\s\S]*?)((?:\.{1,2}|[^\\\/]+?|)(\.[^.\/\\]*|))(?:[\\\/]*)$/; + + // Function to split a filename into [root, dir, basename, ext] + // windows version + var splitPath = function(filename) { + // Separate device+slash from tail + var result = splitDeviceRe.exec(filename), + device = (result[1] || '') + (result[2] || ''), + tail = result[3] || ''; + // Split the tail into dir, basename and extension + var result2 = splitTailRe.exec(tail), + dir = result2[1], + basename = result2[2], + ext = result2[3]; + return [device, dir, basename, ext]; + }; + + // path.resolve([from ...], to) + // windows version + exports.resolve = function() { + var resolvedDevice = '', + resolvedTail = '', + resolvedAbsolute = false; + + for (var i = arguments.length - 1; i >= -1; i--) { + var path; + if (i >= 0) { + path = arguments[i]; + } else if (!resolvedDevice) { + path = process.cwd(); + } else { + // Windows has the concept of drive-specific current working + // directories. If we've resolved a drive letter but not yet an + // absolute path, get cwd for that drive. We're sure the device is not + // an unc path at this points, because unc paths are always absolute. + path = process.env['=' + resolvedDevice]; + // Verify that a drive-local cwd was found and that it actually points + // to our drive. If not, default to the drive's root. + if (!path || path.substr(0, 3).toLowerCase() !== + resolvedDevice.toLowerCase() + '\\') { + path = resolvedDevice + '\\'; + } + } + + // Skip empty and invalid entries + if (typeof path !== 'string' || !path) { + continue; + } + + var result = splitDeviceRe.exec(path), + device = result[1] || '', + isUnc = device && device.charAt(1) !== ':', + isAbsolute = !!result[2] || isUnc, // UNC paths are always absolute + tail = result[3]; + + if (device && + resolvedDevice && + device.toLowerCase() !== resolvedDevice.toLowerCase()) { + // This path points to another device so it is not applicable + continue; + } + + if (!resolvedDevice) { + resolvedDevice = device; + } + if (!resolvedAbsolute) { + resolvedTail = tail + '\\' + resolvedTail; + resolvedAbsolute = isAbsolute; + } + + if (resolvedDevice && resolvedAbsolute) { + break; + } + } + + // Replace slashes (in UNC share name) by backslashes + resolvedDevice = resolvedDevice.replace(/\//g, '\\'); + + // At this point the path should be resolved to a full absolute path, + // but handle relative paths to be safe (might happen when process.cwd() + // fails) + + // Normalize the tail path + + function f(p) { + return !!p; + } + + resolvedTail = normalizeArray(resolvedTail.split(/[\\\/]+/).filter(f), + !resolvedAbsolute).join('\\'); + + return (resolvedDevice + (resolvedAbsolute ? '\\' : '') + resolvedTail) || + '.'; + }; + + // windows version + exports.normalize = function(_path) { + var result = splitDeviceRe.exec(_path), + device = result[1] || '', + isUnc = device && device.charAt(1) !== ':', + isAbsolute = !!result[2] || isUnc, // UNC paths are always absolute + tail = result[3], + trailingSlash = /[\\\/]$/.test(tail); + + // Normalize the tail path + tail = normalizeArray(tail.split(/[\\\/]+/).filter(function(p) { + return !!p; + }), !isAbsolute).join('\\'); + + if (!tail && !isAbsolute) { + tail = '.'; + } + if (tail && trailingSlash) { + tail += '\\'; + } + + return device + (isAbsolute ? '\\' : '') + tail; + }; + + //windows version + exports.join = function() { + function f(p) { + return p && typeof p === 'string'; + } + + var _paths = Array.prototype.slice.call(arguments, 0).filter(f); + var joined = _paths.join('\\'); + + // Make sure that the joined path doesn't start with two slashes + // - it will be mistaken for an unc path by normalize() - + // unless the _paths[0] also starts with two slashes + if (/^[\\\/]{2}/.test(joined) && !/^[\\\/]{2}/.test(_paths[0])) { + joined = joined.slice(1); + } + + return exports.normalize(joined); + }; + + // path.relative(from, to) + // it will solve the relative path from 'from' to 'to', for instance: + // from = 'C:\\orandea\\test\\aaa' + // to = 'C:\\orandea\\impl\\bbb' + // The output of the function should be: '..\\..\\impl\\bbb' + // windows version + exports.relative = function(from, to) { + from = exports.resolve(from); + to = exports.resolve(to); + + // windows is not case sensitive + var lowerFrom = from.toLowerCase(); + var lowerTo = to.toLowerCase(); + + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') { + break; + } + } + + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') { + break; + } + } + + if (start > end) { + return []; + } + return arr.slice(start, end - start + 1); + } + + var toParts = trim(to.split('\\')); + + var lowerFromParts = trim(lowerFrom.split('\\')); + var lowerToParts = trim(lowerTo.split('\\')); + + var length = Math.min(lowerFromParts.length, lowerToParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (lowerFromParts[i] !== lowerToParts[i]) { + samePartsLength = i; + break; + } + } + + if (samePartsLength === 0) { + return to; + } + + var outputParts = []; + for (i = samePartsLength; i < lowerFromParts.length; i++) { + outputParts.push('..'); + } + + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + + return outputParts.join('\\'); + }; +} else { + // Split a filename into [root, dir, basename, ext], unix version + // 'root' is just a slash, or nothing. + var splitPathRe = + /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; + var splitPath = function(filename) { + return splitPathRe.exec(filename).slice(1); + }; + + // path.resolve([from ...], to) + // posix version + exports.resolve = function() { + var resolvedPath = '', + resolvedAbsolute = false; + + for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path = (i >= 0) ? arguments[i] : process.cwd(); + + // Skip empty and invalid entries + if (typeof path !== 'string' || !path) { + continue; + } + + resolvedPath = path + '/' + resolvedPath; + resolvedAbsolute = path.charAt(0) === '/'; + } + + // At this point the path should be resolved to a full absolute path, but + // handle relative paths to be safe (might happen when process.cwd() fails) + + // Normalize the path + resolvedPath = normalizeArray(resolvedPath.split('/').filter(function(p) { + return !!p; + }), !resolvedAbsolute).join('/'); + + return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; + }; + + // path.normalize(_path) + // posix version + exports.normalize = function(_path) { + var isAbsolute = _path.charAt(0) === '/', + trailingSlash = _path.slice(-1) === '/'; + + // Normalize the path + _path = normalizeArray(_path.split('/').filter(function(p) { + return !!p; + }), !isAbsolute).join('/'); + + if (!_path && !isAbsolute) { + _path = '.'; + } + if (_path && trailingSlash) { + _path += '/'; + } + + return (isAbsolute ? '/' : '') + _path; + }; + + // posix version + exports.join = function() { + var _paths = Array.prototype.slice.call(arguments, 0); + return exports.normalize(_paths.filter(function(p, index) { + return p && typeof p === 'string'; + }).join('/')); + }; + + // path.relative(from, to) + // posix version + exports.relative = function(from, to) { + from = exports.resolve(from).substr(1); + to = exports.resolve(to).substr(1); + + function trim(arr) { + var start = 0; + for (; start < arr.length; start++) { + if (arr[start] !== '') { + break; + } + } + + var end = arr.length - 1; + for (; end >= 0; end--) { + if (arr[end] !== '') { + break; + } + } + + if (start > end) { + return []; + } + return arr.slice(start, end - start + 1); + } + + var fromParts = trim(from.split('/')); + var toParts = trim(to.split('/')); + + var length = Math.min(fromParts.length, toParts.length); + var samePartsLength = length; + for (var i = 0; i < length; i++) { + if (fromParts[i] !== toParts[i]) { + samePartsLength = i; + break; + } + } + + var outputParts = []; + for (i = samePartsLength; i < fromParts.length; i++) { + outputParts.push('..'); + } + + outputParts = outputParts.concat(toParts.slice(samePartsLength)); + + return outputParts.join('/'); + }; +} \ No newline at end of file diff --git a/third_party/jsdoc/rhino/querystring.js b/third_party/jsdoc/rhino/querystring.js new file mode 100644 index 0000000000..a24ec97ff9 --- /dev/null +++ b/third_party/jsdoc/rhino/querystring.js @@ -0,0 +1,121 @@ +/** + * Adapted version of Node.js' `querystring` module. + * @module querystring + * @see http://nodejs.org/api/querystring.html + * @see https://github.com/joyent/node/blob/f105f2f2/lib/querystring.js + * @license MIT + */ +'use strict'; + +var QueryString = exports; + +// If obj.hasOwnProperty has been overridden, then calling +// obj.hasOwnProperty(prop) will break. +// See: https://github.com/joyent/node/issues/1707 +function hasOwnProp(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); +} + +QueryString.unescape = function(s) { + return decodeURIComponent(s); +}; + +QueryString.escape = function(str) { + return encodeURIComponent(str); +}; + +var stringifyPrimitive = function(v) { + switch (typeof v) { + case 'string': + return v; + + case 'boolean': + return v ? 'true' : 'false'; + + case 'number': + return isFinite(v) ? v : ''; + + default: + return ''; + } +}; + +QueryString.stringify = QueryString.encode = function(obj, sep, eq, name) { + sep = sep || '&'; + eq = eq || '='; + if (obj === null) { + obj = undefined; + } + + if (typeof obj === 'object') { + return Object.keys(obj).map(function(k) { + var ks = QueryString.escape(stringifyPrimitive(k)) + eq; + if (Array.isArray(obj[k])) { + return obj[k].map(function(v) { + return ks + QueryString.escape(stringifyPrimitive(v)); + }).join(sep); + } else { + return ks + QueryString.escape(stringifyPrimitive(obj[k])); + } + }).join(sep); + + } + + if (!name) { + return ''; + } + return QueryString.escape(stringifyPrimitive(name)) + eq + + QueryString.escape(stringifyPrimitive(obj)); +}; + +// Parse a key=val string. +QueryString.parse = QueryString.decode = function(qs, sep, eq, options) { + sep = sep || '&'; + eq = eq || '='; + var obj = {}; + + if (typeof qs !== 'string' || qs.length === 0) { + return obj; + } + + var regexp = /\+/g; + qs = qs.split(sep); + + var maxKeys = 1000; + if (options && typeof options.maxKeys === 'number') { + maxKeys = options.maxKeys; + } + + var len = qs.length; + // maxKeys <= 0 means that we should not limit keys count + if (maxKeys > 0 && len > maxKeys) { + len = maxKeys; + } + + for (var i = 0; i < len; ++i) { + var x = qs[i].replace(regexp, '%20'), + idx = x.indexOf(eq), + kstr, vstr, k, v; + + if (idx >= 0) { + kstr = x.substr(0, idx); + vstr = x.substr(idx + 1); + } else { + kstr = x; + vstr = ''; + } + + k = QueryString.unescape(kstr); + v = QueryString.unescape(vstr); + + if (!hasOwnProp(obj, k)) { + obj[k] = v; + } else if (Array.isArray(obj[k])) { + obj[k].push(v); + } else { + obj[k] = [obj[k], v]; + } + } + + return obj; +}; diff --git a/third_party/jsdoc/rhino/rhino-shim.js b/third_party/jsdoc/rhino/rhino-shim.js new file mode 100644 index 0000000000..720202f9d1 --- /dev/null +++ b/third_party/jsdoc/rhino/rhino-shim.js @@ -0,0 +1,181 @@ +/*global env: true, Packages: true */ +/** + * @overview A minimal emulation of the standard features of Node.js necessary + * to get JSDoc to run. + */ + +// Set the JS version that the Rhino interpreter will use. +version(180); + +/** + * Emulate DOM timeout/interval functions. + * @see https://developer.mozilla.org/en-US/docs/DOM/window#Methods + */ +(function() { + 'use strict'; + + var timerPool = new java.util.concurrent.ScheduledThreadPoolExecutor(1); + var timers = {}; + var timerCount = 1; + var timerUnits = java.util.concurrent.TimeUnit.MILLISECONDS; + var queue = {}; + var queueActive = false; + + function getCallback(fn) { + return new java.lang.Runnable({ + run: Packages.org.mozilla.javascript.Context.call(fn) + }); + } + + global.setTimeout = function setTimeout(fn, delay) { + var timerId = timerCount++; + var callback = getCallback(fn); + timers[timerId] = timerPool.schedule(callback, delay, timerUnits); + return timerId; + }; + + global.clearTimeout = function clearTimeout(timerId) { + if (timers[timerId]) { + timerPool.remove(timers[timerId]); + delete timers[timerId]; + } + }; + + global.setInterval = function setInterval(fn, delay) { + var timerId = timerCount++; + var callback = getCallback(fn); + timers[timerId] = timerPool.scheduleAtFixedRate(callback, delay, delay, timerUnits); + return timerId; + }; + + global.clearInterval = global.clearTimeout; + + // adapted from https://github.com/alexgorbatchev/node-browser-builtins + // MIT license + global.setImmediate = (function() { + function drain() { + var key; + + var keys = Object.keys(queue); + + queueActive = false; + + for (var i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + var fn = queue[key]; + delete queue[key]; + fn(); + } + } + + return function setImmediate(fn) { + var timerId = timerCount++; + queue[timerId] = fn; + + if (!queueActive) { + queueActive = true; + global.setTimeout(drain, 0); + } + + return timerId; + }; + })(); + + global.clearImmediate = function clearImmediate(id) { + delete queue[id]; + }; +})(); + +/** + * Emulate Node.js console functions. + * @see http://nodejs.org/api/stdio.html + */ +global.console = (function() { + function println(stream, args) { + java.lang.System[stream].println( require('util').format.apply(this, args) ); + } + + return { + error: function error() { + println('err', arguments); + }, + info: function info() { + println('out', arguments); + }, + log: function log() { + println('out', arguments); + }, + trace: function trace(label) { + // this puts some extra junk at the top of the stack trace, but it's close enough + var e = new java.lang.Exception(label || 'Trace'); + e.printStackTrace(); + }, + warn: function warn() { + println('err', arguments); + } + }; +})(); + +/** + * Emulate Node.js process functions. + * @see http://nodejs.org/api/process.html + */ +global.process = { + // not quite right, but close enough + argv: ['java', env.dirname + '/jsdoc.js'] + .concat( Array.prototype.slice.call(arguments, 0) ), + // this depends on a hack in our version of Rhino + cwd: function cwd() { + var f = new java.io.File( java.lang.System.getProperty('user.dir') ); + return String( f.getAbsolutePath() ); + }, + env: (function() { + var result = {}; + + var env = java.lang.System.getenv(); + var keys = env.keySet().toArray(); + var key; + for (var i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + result[key + ''] = env.get(key) + ''; + } + + return result; + })(), + exit: function exit(n) { + n = n || 0; + java.lang.System.exit(n); + }, + nextTick: function nextTick(callback) { + setTimeout(callback, 0); + }, + stderr: { + // Java can't reliably find the terminal width across platforms, so we hard-code a + // reasonable value + columns: 80, + write: function write(str) { + java.lang.System.err.print(str); + } + }, + stdout: { + // Java can't reliably find the terminal width across platforms, so we hard-code a + // reasonable value + columns: 80, + write: function write(str) { + java.lang.System.out.print(str); + } + } +}; + +/** + * Emulate other Node.js globals. + * @see http://nodejs.org/docs/latest/api/globals.html + */ +Object.defineProperties(global, { + '__dirname': { + get: function() { + return global.process.cwd(); + }, + enumerable: true + } +}); diff --git a/third_party/jsdoc/rhino/util.js b/third_party/jsdoc/rhino/util.js new file mode 100644 index 0000000000..3dc3311353 --- /dev/null +++ b/third_party/jsdoc/rhino/util.js @@ -0,0 +1,532 @@ +/** + * Adapted version of Node.js' `util` module. + * @module util + * @see http://nodejs.org/api/util.html + * @see https://github.com/joyent/node/blob/85090734/lib/util.js + * @license MIT + */ + +function hasOwnProp(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); +} + +// placate JSHint +var stylizeNoColor, stylizeWithColor, formatValue, formatPrimitive; + +/** + * Echoes the value of a value. Trys to print the value out + * in the best way possible given the different types. + * + * @param {Object} obj The object to print out. + * @param {Object} opts Optional options object that alters the output. + */ +/* legacy: obj, showHidden, depth, colors*/ +function inspect(obj, opts) { + // default options + var ctx = { + seen: [], + stylize: stylizeNoColor + }; + // legacy... + if (arguments.length >= 3) { + ctx.depth = arguments[2]; + } + if (arguments.length >= 4) { + ctx.colors = arguments[3]; + } + if (typeof opts === 'boolean') { + // legacy... + ctx.showHidden = opts; + } else if (opts) { + // got an "options" object + exports._extend(ctx, opts); + } + // set default options + if (typeof ctx.showHidden === 'undefined') { + ctx.showHidden = false; + } + if (typeof ctx.depth === 'undefined') { + ctx.depth = 2; + } + if (typeof ctx.colors === 'undefined') { + ctx.colors = false; + } + if (typeof ctx.customInspect === 'undefined') { + ctx.customInspect = true; + } + if (ctx.colors) { + ctx.stylize = stylizeWithColor; + } + return formatValue(ctx, obj, ctx.depth); +} +exports.inspect = inspect; + +// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics +inspect.colors = { + 'bold' : [1, 22], + 'italic' : [3, 23], + 'underline' : [4, 24], + 'inverse' : [7, 27], + 'white' : [37, 39], + 'grey' : [90, 39], + 'black' : [30, 39], + 'blue' : [34, 39], + 'cyan' : [36, 39], + 'green' : [32, 39], + 'magenta' : [35, 39], + 'red' : [31, 39], + 'yellow' : [33, 39] +}; + +// Don't use 'blue' not visible on cmd.exe +inspect.styles = { + 'special': 'cyan', + 'number': 'yellow', + 'boolean': 'yellow', + 'undefined': 'grey', + 'null': 'bold', + 'string': 'green', + 'date': 'magenta', + // "name": intentionally not styling + 'regexp': 'red' +}; + +stylizeWithColor = function(str, styleType) { + var style = inspect.styles[styleType]; + + if (style) { + return '\u001b[' + inspect.colors[style][0] + 'm' + str + + '\u001b[' + inspect.colors[style][1] + 'm'; + } else { + return str; + } +}; + +stylizeNoColor = function(str, styleType) { + return str; +}; + +var formatRegExp = /%[sdj%]/g; +exports.format = function(f) { + var i, len; + + if (typeof f !== 'string') { + var objects = []; + for (i = 0, len = arguments.length; i < len; i++) { + objects.push(inspect(arguments[i])); + } + return objects.join(' '); + } + + i = 1; + var args = arguments; + len = args.length; + var str = String(f).replace(formatRegExp, function(x) { + if (x === '%%') { + return '%'; + } + if (i >= len) { + return x; + } + switch (x) { + case '%s': return String(args[i++]); + case '%d': return Number(args[i++]); + case '%j': return require('jsdoc/util/dumper').dump(args[i++]); + default: + return x; + } + }); + for (var x = args[i]; i < len; x = args[++i]) { + if (x === null || typeof x !== 'object') { + str += ' ' + x; + } else { + str += ' ' + inspect(x); + } + } + return str; +}; + +// Mark that a method should not be used. +// Returns a modified function which warns once by default. +// If --no-deprecation is set, then it is a no-op. +exports.deprecate = function(fn, msg) { + if (process.noDeprecation === true) { + return fn; + } + + var warned = false; + function deprecated() { + if (!warned) { + if (process.traceDeprecation) { + console.trace(msg); + } else { + console.error(msg); + } + warned = true; + } + return fn.apply(this, arguments); + } + + return deprecated; +}; + +exports.print = function() { + var args = Array.prototype.slice.call(arguments, 0); + for (var i = 0, len = args.length; i < len; ++i) { + process.stdout.write(String(args[i])); + } +}; + +exports.puts = function() { + var args = Array.prototype.slice.call(arguments, 0); + for (var i = 0, len = args.length; i < len; ++i) { + process.stdout.write(args[i] + '\n'); + } +}; + +exports.debug = function(x) { + process.stderr.write('DEBUG: ' + x + '\n'); +}; + +var error = exports.error = function(x) { + var args = Array.prototype.slice.call(arguments, 0); + for (var i = 0, len = args.length; i < len; ++i) { + process.stderr.write(args[i] + '\n'); + } +}; + +function arrayToHash(array) { + var hash = {}; + + array.forEach(function(val, idx) { + hash[val] = true; + }); + + return hash; +} + +function formatError(value) { + return '[' + Error.prototype.toString.call(value) + ']'; +} + +function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { + var name, str, desc; + desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; + if (desc.get) { + if (desc.set) { + str = ctx.stylize('[Getter/Setter]', 'special'); + } else { + str = ctx.stylize('[Getter]', 'special'); + } + } else { + if (desc.set) { + str = ctx.stylize('[Setter]', 'special'); + } + } + if (!hasOwnProp(visibleKeys, key)) { + name = '[' + key + ']'; + } + if (!str) { + if (ctx.seen.indexOf(desc.value) < 0) { + if (recurseTimes === null) { + str = formatValue(ctx, desc.value, null); + } else { + str = formatValue(ctx, desc.value, recurseTimes - 1); + } + if (str.indexOf('\n') > -1) { + if (array) { + str = str.split('\n').map(function(line) { + return ' ' + line; + }).join('\n').substr(2); + } else { + str = '\n' + str.split('\n').map(function(line) { + return ' ' + line; + }).join('\n'); + } + } + } else { + str = ctx.stylize('[Circular]', 'special'); + } + } + if (typeof name === 'undefined') { + if (array && key.match(/^\d+$/)) { + return str; + } + name = JSON.stringify('' + key); + if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { + name = name.substr(1, name.length - 2); + name = ctx.stylize(name, 'name'); + } else { + name = name.replace(/'/g, "\\'") + .replace(/\\"/g, '"') + .replace(/(^"|"$)/g, "'"); + name = ctx.stylize(name, 'string'); + } + } + + return name + ': ' + str; +} + +function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { + var output = []; + for (var i = 0, l = value.length; i < l; ++i) { + if (hasOwnProp(value, String(i))) { + output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, + String(i), true)); + } else { + output.push(''); + } + } + keys.forEach(function(key) { + if (!key.match(/^\d+$/)) { + output.push(formatProperty(ctx, value, recurseTimes, visibleKeys, + key, true)); + } + }); + return output; +} + +function reduceToSingleString(output, base, braces) { + var numLinesEst = 0; + var length = output.reduce(function(prev, cur) { + numLinesEst++; + if (cur.indexOf('\n') >= 0) { + numLinesEst++; + } + return prev + cur.length + 1; + }, 0); + + if (length > 60) { + return braces[0] + + (base === '' ? '' : base + '\n ') + + ' ' + + output.join(',\n ') + + ' ' + + braces[1]; + } + + return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; +} + +function objectToString(o) { + return Object.prototype.toString.call(o); +} + +// NOTE: These type checking functions intentionally don't use `instanceof` +// because it is fragile and can be easily faked with `Object.create()`. +function isArray(ar) { + return Array.isArray(ar) || + (typeof ar === 'object' && objectToString(ar) === '[object Array]'); +} +exports.isArray = isArray; + +function isRegExp(re) { + return typeof re === 'object' && objectToString(re) === '[object RegExp]'; +} +exports.isRegExp = isRegExp; + +function isDate(d) { + return typeof d === 'object' && objectToString(d) === '[object Date]'; +} +exports.isDate = isDate; + +function isError(e) { + return typeof e === 'object' && objectToString(e) === '[object Error]'; +} +exports.isError = isError; + +formatValue = function(ctx, value, recurseTimes) { + // Provide a hook for user-specified inspect functions. + // Check that value is an object with an inspect function on it + if (ctx.customInspect && value && typeof value.inspect === 'function' && + // Filter out the util module, it's inspect function is special + value.inspect !== exports.inspect && + // Also filter out any prototype objects using the circular check. + !(value.constructor && value.constructor.prototype === value)) { + return String(value.inspect(recurseTimes)); + } + + // Primitive types cannot have properties + var primitive = formatPrimitive(ctx, value); + if (primitive) { + return primitive; + } + + // Look up the keys of the object. + var keys = Object.keys(value); + var visibleKeys = arrayToHash(keys); + + if (ctx.showHidden) { + keys = Object.getOwnPropertyNames(value); + } + + // Some type of object without properties can be shortcutted. + if (keys.length === 0) { + if (typeof value === 'function') { + var name = value.name ? ': ' + value.name : ''; + return ctx.stylize('[Function' + name + ']', 'special'); + } + if (isRegExp(value)) { + return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); + } + if (isDate(value)) { + return ctx.stylize(Date.prototype.toString.call(value), 'date'); + } + if (isError(value)) { + return formatError(value); + } + } + + var base = '', array = false, braces = ['{', '}']; + + // Make Array say that they are Array + if (isArray(value)) { + array = true; + braces = ['[', ']']; + } + + // Make functions say that they are functions + if (typeof value === 'function') { + var n = value.name ? ': ' + value.name : ''; + base = ' [Function' + n + ']'; + } + + // Make RegExps say that they are RegExps + if (isRegExp(value)) { + base = ' ' + RegExp.prototype.toString.call(value); + } + + // Make dates with properties first say the date + if (isDate(value)) { + base = ' ' + Date.prototype.toUTCString.call(value); + } + + // Make error with message first say the error + if (isError(value)) { + base = ' ' + formatError(value); + } + + if (keys.length === 0 && (!array || value.length === 0)) { + return braces[0] + base + braces[1]; + } + + if (recurseTimes < 0) { + if (isRegExp(value)) { + return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp'); + } else { + return ctx.stylize('[Object]', 'special'); + } + } + + ctx.seen.push(value); + + var output; + if (array) { + output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); + } else { + output = keys.map(function(key) { + return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); + }); + } + + ctx.seen.pop(); + + return reduceToSingleString(output, base, braces); +}; + +formatPrimitive = function(ctx, value) { + switch (typeof value) { + case 'undefined': + return ctx.stylize('undefined', 'undefined'); + + case 'string': + var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') + .replace(/'/g, "\\'") + .replace(/\\"/g, '"') + '\''; + return ctx.stylize(simple, 'string'); + + case 'number': + return ctx.stylize('' + value, 'number'); + + case 'boolean': + return ctx.stylize('' + value, 'boolean'); + } + // For some reason typeof null is "object", so special case here. + if (value === null) { + return ctx.stylize('null', 'null'); + } +}; + +exports.p = exports.deprecate(function() { + var args = Array.prototype.slice.call(arguments, 0); + for (var i = 0, len = args.length; i < len; ++i) { + error(exports.inspect(args[i])); + } +}, 'util.p: Use console.error() instead.'); + +function pad(n) { + return n < 10 ? '0' + n.toString(10) : n.toString(10); +} + +var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', + 'Oct', 'Nov', 'Dec']; + +// 26 Feb 16:19:34 +function timestamp() { + var d = new Date(); + var time = [pad(d.getHours()), + pad(d.getMinutes()), + pad(d.getSeconds())].join(':'); + return [d.getDate(), months[d.getMonth()], time].join(' '); +} + +exports.log = function(msg) { + exports.puts(timestamp() + ' - ' + msg.toString()); +}; + +exports.exec = function() { + throw new Error('util.exec() is not implemented on Rhino (and was deprecated in Node.js 0.2)'); +}; + +exports.pump = function() { + throw new Error('util.pump() is not implemented on Rhino (and was deprecated in Node.js 0.8'); +}; + +/** + * Inherit the prototype methods from one constructor into another. + * + * The Function.prototype.inherits from lang.js rewritten as a standalone + * function (not on Function.prototype). NOTE: If this file is to be loaded + * during bootstrapping this function needs to be rewritten using some native + * functions as prototype setup using normal JavaScript does not work as + * expected during bootstrapping (see mirror.js in r114903). + * + * @param {function} ctor Constructor function which needs to inherit the + * prototype. + * @param {function} superCtor Constructor function to inherit prototype from. + */ +exports.inherits = function(ctor, superCtor) { + ctor.super_ = superCtor; + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); +}; + +exports._extend = function(origin, add) { + // Don't do anything if add isn't an object + if (!add || typeof add !== 'object') { + return origin; + } + + var keys = Object.keys(add); + var i = keys.length; + while (i--) { + origin[keys[i]] = add[keys[i]]; + } + return origin; +}; diff --git a/third_party/jsdoc/templates/README.md b/third_party/jsdoc/templates/README.md new file mode 100644 index 0000000000..7424e74142 --- /dev/null +++ b/third_party/jsdoc/templates/README.md @@ -0,0 +1,27 @@ +To create or use your own template: + +1. Create a folder with the same name as your template (for example, `mycooltemplate`). +2. Within the template folder, create a file named `publish.js`. This file must be a CommonJS module that exports a method named `publish`. + +For example: + +````javascript +/** @module publish */ + +/** + * Generate documentation output. + * + * @param {TAFFY} data - A TaffyDB collection representing + * all the symbols documented in your code. + * @param {object} opts - An object with options information. + */ +exports.publish = function(data, opts) { + // do stuff here to generate your output files +}; +```` + +To invoke JSDoc 3 with your own template, use the `-t` command line option, and specify the path to your template folder: + +```` +./jsdoc mycode.js -t /path/to/mycooltemplate +```` diff --git a/third_party/jsdoc/templates/default/README.md b/third_party/jsdoc/templates/default/README.md new file mode 100644 index 0000000000..a7bd96bfd5 --- /dev/null +++ b/third_party/jsdoc/templates/default/README.md @@ -0,0 +1 @@ +The default template for JSDoc 3 uses: [the Taffy Database library](http://taffydb.com/) and the [Underscore Template library](http://documentcloud.github.com/underscore/#template). diff --git a/third_party/jsdoc/templates/default/publish.js b/third_party/jsdoc/templates/default/publish.js new file mode 100644 index 0000000000..28028d6aeb --- /dev/null +++ b/third_party/jsdoc/templates/default/publish.js @@ -0,0 +1,697 @@ +/*global env: true */ +'use strict'; + +var doop = require('jsdoc/util/doop'); +var fs = require('jsdoc/fs'); +var helper = require('jsdoc/util/templateHelper'); +var logger = require('jsdoc/util/logger'); +var path = require('jsdoc/path'); +var taffy = require('taffydb').taffy; +var template = require('jsdoc/template'); +var util = require('util'); + +var htmlsafe = helper.htmlsafe; +var linkto = helper.linkto; +var resolveAuthorLinks = helper.resolveAuthorLinks; +var scopeToPunc = helper.scopeToPunc; +var hasOwnProp = Object.prototype.hasOwnProperty; + +var data; +var view; + +var outdir = path.normalize(env.opts.destination); + +function find(spec) { + return helper.find(data, spec); +} + +function tutoriallink(tutorial) { + return helper.toTutorial(tutorial, null, { tag: 'em', classname: 'disabled', prefix: 'Tutorial: ' }); +} + +function getAncestorLinks(doclet) { + return helper.getAncestorLinks(data, doclet); +} + +function hashToLink(doclet, hash) { + if ( !/^(#.+)/.test(hash) ) { return hash; } + + var url = helper.createLink(doclet); + + url = url.replace(/(#.+|$)/, hash); + return '' + hash + ''; +} + +function needsSignature(doclet) { + var needsSig = false; + + // function and class definitions always get a signature + if (doclet.kind === 'function' || doclet.kind === 'class') { + needsSig = true; + } + // typedefs that contain functions get a signature, too + else if (doclet.kind === 'typedef' && doclet.type && doclet.type.names && + doclet.type.names.length) { + for (var i = 0, l = doclet.type.names.length; i < l; i++) { + if (doclet.type.names[i].toLowerCase() === 'function') { + needsSig = true; + break; + } + } + } + + return needsSig; +} + +function getSignatureAttributes(item) { + var attributes = []; + + if (item.optional) { + attributes.push('opt'); + } + + if (item.nullable === true) { + attributes.push('nullable'); + } + else if (item.nullable === false) { + attributes.push('non-null'); + } + + return attributes; +} + +function updateItemName(item) { + var attributes = getSignatureAttributes(item); + var itemName = item.name || ''; + + if (item.variable) { + itemName = '…' + itemName; + } + + if (attributes && attributes.length) { + itemName = util.format( '%s%s', itemName, + attributes.join(', ') ); + } + + return itemName; +} + +function addParamAttributes(params) { + return params.filter(function(param) { + return param.name && param.name.indexOf('.') === -1; + }).map(updateItemName); +} + +function buildItemTypeStrings(item) { + var types = []; + + if (item && item.type && item.type.names) { + item.type.names.forEach(function(name) { + types.push( linkto(name, htmlsafe(name)) ); + }); + } + + return types; +} + +function buildAttribsString(attribs) { + var attribsString = ''; + + if (attribs && attribs.length) { + attribsString = htmlsafe( util.format('(%s) ', attribs.join(', ')) ); + } + + return attribsString; +} + +function addNonParamAttributes(items) { + var types = []; + + items.forEach(function(item) { + types = types.concat( buildItemTypeStrings(item) ); + }); + + return types; +} + +function addSignatureParams(f) { + var params = f.params ? addParamAttributes(f.params) : []; + + f.signature = util.format( '%s(%s)', (f.signature || ''), params.join(', ') ); +} + +function addSignatureReturns(f) { + var attribs = []; + var attribsString = ''; + var returnTypes = []; + var returnTypesString = ''; + + // jam all the return-type attributes into an array. this could create odd results (for example, + // if there are both nullable and non-nullable return types), but let's assume that most people + // who use multiple @return tags aren't using Closure Compiler type annotations, and vice-versa. + if (f.returns) { + f.returns.forEach(function(item) { + helper.getAttribs(item).forEach(function(attrib) { + if (attribs.indexOf(attrib) === -1) { + attribs.push(attrib); + } + }); + }); + + attribsString = buildAttribsString(attribs); + } + + if (f.returns) { + returnTypes = addNonParamAttributes(f.returns); + } + if (returnTypes.length) { + returnTypesString = util.format( ' → %s{%s}', attribsString, returnTypes.join('|') ); + } + + f.signature = '' + (f.signature || '') + '' + + '' + returnTypesString + ''; +} + +function addSignatureTypes(f) { + var types = f.type ? buildItemTypeStrings(f) : []; + + f.signature = (f.signature || '') + '' + + (types.length ? ' :' + types.join('|') : '') + ''; +} + +function addAttribs(f) { + var attribs = helper.getAttribs(f); + var attribsString = buildAttribsString(attribs); + + f.attribs = util.format('%s', attribsString); +} + +function shortenPaths(files, commonPrefix) { + Object.keys(files).forEach(function(file) { + files[file].shortened = files[file].resolved.replace(commonPrefix, '') + // always use forward slashes + .replace(/\\/g, '/'); + }); + + return files; +} + +function getPathFromDoclet(doclet) { + if (!doclet.meta) { + return null; + } + + return doclet.meta.path && doclet.meta.path !== 'null' ? + path.join(doclet.meta.path, doclet.meta.filename) : + doclet.meta.filename; +} + +function generate(title, docs, filename, resolveLinks) { + resolveLinks = resolveLinks === false ? false : true; + + var docData = { + title: title, + docs: docs + }; + + var outpath = path.join(outdir, filename), + html = view.render('container.tmpl', docData); + + if (resolveLinks) { + html = helper.resolveLinks(html); // turn {@link foo} into foo + } + + fs.writeFileSync(outpath, html, 'utf8'); +} + +function generateSourceFiles(sourceFiles, encoding) { + encoding = encoding || 'utf8'; + Object.keys(sourceFiles).forEach(function(file) { + var source; + // links are keyed to the shortened path in each doclet's `meta.shortpath` property + var sourceOutfile = helper.getUniqueFilename(sourceFiles[file].shortened); + helper.registerLink(sourceFiles[file].shortened, sourceOutfile); + + try { + source = { + kind: 'source', + code: helper.htmlsafe( fs.readFileSync(sourceFiles[file].resolved, encoding) ) + }; + } + catch(e) { + logger.error('Error while generating source file %s: %s', file, e.message); + } + + generate('Source: ' + sourceFiles[file].shortened, [source], sourceOutfile, + false); + }); +} + +/** + * Look for classes or functions with the same name as modules (which indicates that the module + * exports only that class or function), then attach the classes or functions to the `module` + * property of the appropriate module doclets. The name of each class or function is also updated + * for display purposes. This function mutates the original arrays. + * + * @private + * @param {Array.} doclets - The array of classes and functions to + * check. + * @param {Array.} modules - The array of module doclets to search. + */ +function attachModuleSymbols(doclets, modules) { + var symbols = {}; + + // build a lookup table + doclets.forEach(function(symbol) { + symbols[symbol.longname] = symbols[symbol.longname] || []; + symbols[symbol.longname].push(symbol); + }); + + return modules.map(function(module) { + if (symbols[module.longname]) { + module.modules = symbols[module.longname].map(function(symbol) { + symbol = doop(symbol); + + if (symbol.kind === 'class' || symbol.kind === 'function') { + symbol.name = symbol.name.replace('module:', '(require("') + '"))'; + } + + return symbol; + }); + } + }); +} + +/** + * Create the navigation sidebar. + * @param {object} members The members that will be used to create the sidebar. + * @param {array} members.classes + * @param {array} members.externals + * @param {array} members.globals + * @param {array} members.mixins + * @param {array} members.modules + * @param {array} members.namespaces + * @param {array} members.tutorials + * @param {array} members.events + * @param {array} members.interfaces + * @return {string} The HTML for the navigation sidebar. + */ +function buildNav(members) { + var nav = '

    Home

    ', + seen = {}, + hasClassList = false, + classNav = '', + globalNav = ''; + + if (members.tutorials.length) { + nav += '

    Tutorials

      '; + members.tutorials.forEach(function(t) { + nav += '
    • ' + tutoriallink(t.name) + '
    • '; + }); + + nav += '
    '; + } + + if (members.modules.length) { + nav += '

    Modules

      '; + members.modules.forEach(function(m) { + if ( !hasOwnProp.call(seen, m.longname) ) { + nav += '
    • ' + linkto(m.longname, m.longname) + '
    • '; + } + seen[m.longname] = true; + }); + + nav += '
    '; + } + + if (members.externals.length) { + nav += '

    Externals

      '; + members.externals.forEach(function(e) { + if ( !hasOwnProp.call(seen, e.longname) ) { + nav += '
    • ' + linkto( e.longname, e.name.replace(/(^"|"$)/g, '') ) + '
    • '; + } + seen[e.longname] = true; + }); + + nav += '
    '; + } + + if (members.namespaces.length) { + nav += '

    Namespaces

      '; + members.namespaces.forEach(function(n) { + if ( !hasOwnProp.call(seen, n.longname) ) { + nav += '
    • ' + linkto(n.longname, n.longname) + '
    • '; + } + seen[n.longname] = true; + }); + + nav += '
    '; + } + + if (members.classes.length) { + members.classes.forEach(function(c) { + if ( !hasOwnProp.call(seen, c.longname) ) { + var showLevel = (c.visibility ? '0' : '1'); + var classNames = 'visibility-' + (c.visibility || '') + + ' show' + showLevel; + classNav += '
  • ' + linkto(c.longname, c.longname) + '
  • '; + } + seen[c.longname] = true; + }); + + if (classNav !== '') { + nav += '

    Classes

      '; + nav += classNav; + nav += '
    '; + } + } + + if (members.mixins.length) { + nav += '

    Mixins

      '; + members.mixins.forEach(function(m) { + if ( !hasOwnProp.call(seen, m.longname) ) { + nav += '
    • ' + linkto(m.longname, m.longname) + '
    • '; + } + seen[m.longname] = true; + }); + + nav += '
    '; + } + + if (members.interfaces.length) { + nav += '

    Interfaces

      '; + members.interfaces.forEach(function(i) { + var showLevel = (i.visibility ? '0' : '1'); + var classNames = 'visibility-' + (i.visibility || '') + + ' show' + showLevel; + nav += '
    • ' + linkto(i.longname, i.longname) + '
    • '; + }); + nav += '
    '; + } + + if (members.events.length) { + nav += '

    Events

      '; + members.events.forEach(function(e) { + var showLevel = (e.visibility ? '0' : '1'); + var classNames = 'visibility-' + (e.visibility || '') + + ' show' + showLevel; + if ( !hasOwnProp.call(seen, e.longname) ) { + nav += '
    • ' + linkto(e.longname, e.longname) + '
    • '; + } + seen[e.longname] = true; + }); + + nav += '
    '; + } + + if (members.globals.length) { + members.globals.forEach(function(g) { + if ( g.kind !== 'typedef' && !hasOwnProp.call(seen, g.longname) ) { + globalNav += '
  • ' + linkto(g.longname, g.longname) + '
  • '; + } + seen[g.longname] = true; + }); + + if (!globalNav) { + // turn the heading into a link so you can actually get to the global page + nav += '

    ' + linkto('global', 'Global') + '

    '; + } + else { + nav += '

    Global

      ' + globalNav + '
    '; + } + } + + return nav; +} + +/** + @param {TAFFY} taffyData See . + @param {object} opts + @param {Tutorial} tutorials + */ +exports.publish = function(taffyData, opts, tutorials) { + data = taffyData; + + var conf = env.conf.templates || {}; + conf['default'] = conf['default'] || {}; + + var templatePath = path.normalize(opts.template); + view = new template.Template( path.join(templatePath, 'tmpl') ); + + // claim some special filenames in advance, so the All-Powerful Overseer of Filename Uniqueness + // doesn't try to hand them out later + var indexUrl = helper.getUniqueFilename('index'); + // don't call registerLink() on this one! 'index' is also a valid longname + + var globalUrl = helper.getUniqueFilename('global'); + helper.registerLink('global', globalUrl); + + // set up templating + view.layout = conf['default'].layoutFile ? + path.getResourcePath(path.dirname(conf['default'].layoutFile), + path.basename(conf['default'].layoutFile) ) : + 'layout.tmpl'; + + // set up tutorials for helper + helper.setTutorials(tutorials); + + data = helper.prune(data); + data.sort('longname, version, since'); + helper.addEventListeners(data); + + var sourceFiles = {}; + var sourceFilePaths = []; + data().each(function(doclet) { + doclet.attribs = ''; + + if (doclet.examples) { + doclet.examples = doclet.examples.map(function(example) { + var caption, code; + + if (example.match(/^\s*([\s\S]+?)<\/caption>(\s*[\n\r])([\s\S]+)$/i)) { + caption = RegExp.$1; + code = RegExp.$3; + } + + return { + caption: caption || '', + code: code || example + }; + }); + } + if (doclet.see) { + doclet.see.forEach(function(seeItem, i) { + doclet.see[i] = hashToLink(doclet, seeItem); + }); + } + + // build a list of source files + var sourcePath; + if (doclet.meta) { + sourcePath = getPathFromDoclet(doclet); + sourceFiles[sourcePath] = { + resolved: sourcePath, + shortened: null + }; + if (sourceFilePaths.indexOf(sourcePath) === -1) { + sourceFilePaths.push(sourcePath); + } + } + }); + + // update outdir if necessary, then create outdir + var packageInfo = ( find({kind: 'package'}) || [] ) [0]; + if (packageInfo && packageInfo.name) { + outdir = path.join( outdir, packageInfo.name, (packageInfo.version || '') ); + } + fs.mkPath(outdir); + + // copy the template's static files to outdir + var fromDir = path.join(templatePath, 'static'); + var staticFiles = fs.ls(fromDir, 3); + + staticFiles.forEach(function(fileName) { + var toDir = fs.toDir( fileName.replace(fromDir, outdir) ); + fs.mkPath(toDir); + fs.copyFileSync(fileName, toDir); + }); + + // copy user-specified static files to outdir + var staticFilePaths; + var staticFileFilter; + var staticFileScanner; + if (conf['default'].staticFiles) { + // The canonical property name is `include`. We accept `paths` for backwards compatibility + // with a bug in JSDoc 3.2.x. + staticFilePaths = conf['default'].staticFiles.include || + conf['default'].staticFiles.paths || + []; + staticFileFilter = new (require('jsdoc/src/filter')).Filter(conf['default'].staticFiles); + staticFileScanner = new (require('jsdoc/src/scanner')).Scanner(); + + staticFilePaths.forEach(function(filePath) { + var extraStaticFiles = staticFileScanner.scan([filePath], 10, staticFileFilter); + + extraStaticFiles.forEach(function(fileName) { + var sourcePath = fs.toDir(filePath); + var toDir = fs.toDir( fileName.replace(sourcePath, outdir) ); + fs.mkPath(toDir); + fs.copyFileSync(fileName, toDir); + }); + }); + } + + if (sourceFilePaths.length) { + sourceFiles = shortenPaths( sourceFiles, path.commonPrefix(sourceFilePaths) ); + } + data().each(function(doclet) { + var url = helper.createLink(doclet); + helper.registerLink(doclet.longname, url); + + // add a shortened version of the full path + var docletPath; + if (doclet.meta) { + docletPath = getPathFromDoclet(doclet); + docletPath = sourceFiles[docletPath].shortened; + if (docletPath) { + doclet.meta.shortpath = docletPath; + } + } + }); + + data().each(function(doclet) { + var url = helper.longnameToUrl[doclet.longname]; + + if (url.indexOf('#') > -1) { + doclet.id = helper.longnameToUrl[doclet.longname].split(/#/).pop(); + } + else { + doclet.id = doclet.name; + } + + if ( needsSignature(doclet) ) { + addSignatureParams(doclet); + addSignatureReturns(doclet); + addAttribs(doclet); + } + }); + + // do this after the urls have all been generated + data().each(function(doclet) { + doclet.ancestors = getAncestorLinks(doclet); + + if (doclet.kind === 'member') { + addSignatureTypes(doclet); + addAttribs(doclet); + } + + if (doclet.kind === 'constant') { + addSignatureTypes(doclet); + addAttribs(doclet); + doclet.kind = 'member'; + } + }); + + var members = helper.getMembers(data); + members.tutorials = tutorials.children; + + // output pretty-printed source files by default + var outputSourceFiles = conf['default'] && conf['default'].outputSourceFiles !== false ? true : + false; + + // add template helpers + view.find = find; + view.linkto = linkto; + view.resolveAuthorLinks = resolveAuthorLinks; + view.tutoriallink = tutoriallink; + view.htmlsafe = htmlsafe; + view.outputSourceFiles = outputSourceFiles; + + // once for all + view.nav = buildNav(members); + attachModuleSymbols( find({ longname: {left: 'module:'} }), members.modules ); + + // generate the pretty-printed source files first so other pages can link to them + if (outputSourceFiles) { + generateSourceFiles(sourceFiles, opts.encoding); + } + + if (members.globals.length) { generate('Global', [{kind: 'globalobj'}], globalUrl); } + + // index page displays information from package.json and lists files + var files = find({kind: 'file'}), + packages = find({kind: 'package'}); + + generate('Home', + packages.concat( + [{kind: 'mainpage', readme: opts.readme, longname: (opts.mainpagetitle) ? opts.mainpagetitle : 'Main Page'}] + ), + indexUrl); + + // set up the lists that we'll use to generate pages + var classes = taffy(members.classes); + var modules = taffy(members.modules); + var namespaces = taffy(members.namespaces); + var mixins = taffy(members.mixins); + var externals = taffy(members.externals); + var interfaces = taffy(members.interfaces); + + Object.keys(helper.longnameToUrl).forEach(function(longname) { + var myClasses = helper.find(classes, {longname: longname}); + if (myClasses.length) { + generate('Class: ' + myClasses[0].longname, myClasses, helper.longnameToUrl[longname]); + } + + var myModules = helper.find(modules, {longname: longname}); + if (myModules.length) { + generate('Module: ' + myModules[0].longname, myModules, helper.longnameToUrl[longname]); + } + + var myNamespaces = helper.find(namespaces, {longname: longname}); + if (myNamespaces.length) { + generate('Namespace: ' + myNamespaces[0].longname, myNamespaces, helper.longnameToUrl[longname]); + } + + var myMixins = helper.find(mixins, {longname: longname}); + if (myMixins.length) { + generate('Mixin: ' + myMixins[0].longname, myMixins, helper.longnameToUrl[longname]); + } + + var myExternals = helper.find(externals, {longname: longname}); + if (myExternals.length) { + generate('External: ' + myExternals[0].longname, myExternals, helper.longnameToUrl[longname]); + } + + var myInterfaces = helper.find(interfaces, {longname: longname}); + if (myInterfaces.length) { + generate('Interface: ' + myInterfaces[0].longname, myInterfaces, helper.longnameToUrl[longname]); + } + }); + + // TODO: move the tutorial functions to templateHelper.js + function generateTutorial(title, tutorial, filename) { + var tutorialData = { + title: title, + header: tutorial.title, + content: tutorial.parse(), + children: tutorial.children + }; + + var tutorialPath = path.join(outdir, filename), + html = view.render('tutorial.tmpl', tutorialData); + + // yes, you can use {@link} in tutorials too! + html = helper.resolveLinks(html); // turn {@link foo} into foo + + fs.writeFileSync(tutorialPath, html, 'utf8'); + } + + // tutorials can have only one parent so there is no risk for loops + function saveChildren(node) { + node.children.forEach(function(child) { + generateTutorial('Tutorial: ' + child.title, child, helper.tutorialToUrl(child.name)); + saveChildren(child); + }); + } + saveChildren(tutorials); +}; diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-Bold-webfont.eot b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Bold-webfont.eot new file mode 100644 index 0000000000..5d20d91633 Binary files /dev/null and b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Bold-webfont.eot differ diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-Bold-webfont.svg b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Bold-webfont.svg new file mode 100644 index 0000000000..3ed7be4bc5 --- /dev/null +++ b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Bold-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-Bold-webfont.woff b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Bold-webfont.woff new file mode 100644 index 0000000000..1205787b0e Binary files /dev/null and b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Bold-webfont.woff differ diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-BoldItalic-webfont.eot b/third_party/jsdoc/templates/default/static/fonts/OpenSans-BoldItalic-webfont.eot new file mode 100644 index 0000000000..1f639a15ff Binary files /dev/null and b/third_party/jsdoc/templates/default/static/fonts/OpenSans-BoldItalic-webfont.eot differ diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-BoldItalic-webfont.svg b/third_party/jsdoc/templates/default/static/fonts/OpenSans-BoldItalic-webfont.svg new file mode 100644 index 0000000000..6a2607b9da --- /dev/null +++ b/third_party/jsdoc/templates/default/static/fonts/OpenSans-BoldItalic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-BoldItalic-webfont.woff b/third_party/jsdoc/templates/default/static/fonts/OpenSans-BoldItalic-webfont.woff new file mode 100644 index 0000000000..ed760c0628 Binary files /dev/null and b/third_party/jsdoc/templates/default/static/fonts/OpenSans-BoldItalic-webfont.woff differ diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-Italic-webfont.eot b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Italic-webfont.eot new file mode 100644 index 0000000000..0c8a0ae06e Binary files /dev/null and b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Italic-webfont.eot differ diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-Italic-webfont.svg b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Italic-webfont.svg new file mode 100644 index 0000000000..e1075dcc24 --- /dev/null +++ b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Italic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-Italic-webfont.woff b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Italic-webfont.woff new file mode 100644 index 0000000000..ff652e6435 Binary files /dev/null and b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Italic-webfont.woff differ diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-Light-webfont.eot b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Light-webfont.eot new file mode 100644 index 0000000000..14868406aa Binary files /dev/null and b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Light-webfont.eot differ diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-Light-webfont.svg b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Light-webfont.svg new file mode 100644 index 0000000000..11a472ca8a --- /dev/null +++ b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Light-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-Light-webfont.woff b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Light-webfont.woff new file mode 100644 index 0000000000..e786074813 Binary files /dev/null and b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Light-webfont.woff differ diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-LightItalic-webfont.eot b/third_party/jsdoc/templates/default/static/fonts/OpenSans-LightItalic-webfont.eot new file mode 100644 index 0000000000..8f445929ff Binary files /dev/null and b/third_party/jsdoc/templates/default/static/fonts/OpenSans-LightItalic-webfont.eot differ diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-LightItalic-webfont.svg b/third_party/jsdoc/templates/default/static/fonts/OpenSans-LightItalic-webfont.svg new file mode 100644 index 0000000000..431d7e3546 --- /dev/null +++ b/third_party/jsdoc/templates/default/static/fonts/OpenSans-LightItalic-webfont.svg @@ -0,0 +1,1835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-LightItalic-webfont.woff b/third_party/jsdoc/templates/default/static/fonts/OpenSans-LightItalic-webfont.woff new file mode 100644 index 0000000000..43e8b9e6cc Binary files /dev/null and b/third_party/jsdoc/templates/default/static/fonts/OpenSans-LightItalic-webfont.woff differ diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-Regular-webfont.eot b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Regular-webfont.eot new file mode 100644 index 0000000000..6bbc3cf58c Binary files /dev/null and b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Regular-webfont.eot differ diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-Regular-webfont.svg b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Regular-webfont.svg new file mode 100644 index 0000000000..25a3952340 --- /dev/null +++ b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Regular-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/third_party/jsdoc/templates/default/static/fonts/OpenSans-Regular-webfont.woff b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Regular-webfont.woff new file mode 100644 index 0000000000..e231183dce Binary files /dev/null and b/third_party/jsdoc/templates/default/static/fonts/OpenSans-Regular-webfont.woff differ diff --git a/third_party/jsdoc/templates/default/static/scripts/linenumber.js b/third_party/jsdoc/templates/default/static/scripts/linenumber.js new file mode 100644 index 0000000000..8d52f7eafd --- /dev/null +++ b/third_party/jsdoc/templates/default/static/scripts/linenumber.js @@ -0,0 +1,25 @@ +/*global document */ +(function() { + var source = document.getElementsByClassName('prettyprint source linenums'); + var i = 0; + var lineNumber = 0; + var lineId; + var lines; + var totalLines; + var anchorHash; + + if (source && source[0]) { + anchorHash = document.location.hash.substring(1); + lines = source[0].getElementsByTagName('li'); + totalLines = lines.length; + + for (; i < totalLines; i++) { + lineNumber++; + lineId = 'line' + lineNumber; + lines[i].id = lineId; + if (lineId === anchorHash) { + lines[i].className += ' selected'; + } + } + } +})(); diff --git a/third_party/jsdoc/templates/default/static/scripts/prettify/Apache-License-2.0.txt b/third_party/jsdoc/templates/default/static/scripts/prettify/Apache-License-2.0.txt new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/third_party/jsdoc/templates/default/static/scripts/prettify/Apache-License-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third_party/jsdoc/templates/default/static/scripts/prettify/lang-css.js b/third_party/jsdoc/templates/default/static/scripts/prettify/lang-css.js new file mode 100644 index 0000000000..041e1f5906 --- /dev/null +++ b/third_party/jsdoc/templates/default/static/scripts/prettify/lang-css.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", +/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); diff --git a/third_party/jsdoc/templates/default/static/scripts/prettify/prettify.js b/third_party/jsdoc/templates/default/static/scripts/prettify/prettify.js new file mode 100644 index 0000000000..eef5ad7e6a --- /dev/null +++ b/third_party/jsdoc/templates/default/static/scripts/prettify/prettify.js @@ -0,0 +1,28 @@ +var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= +[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), +l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, +q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, +q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, +"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), +a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} +for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], +"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], +H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], +J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ +I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), +["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", +/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), +["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", +hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= +!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p= 1), + '.show2': (showLevel >= 2) + }; + + for (var selector in settings) { + var reveal = settings[selector]; + var list = document.querySelectorAll(selector); + list = Array.prototype.slice.call(list, 0); + list.forEach(function(e) { + if (reveal) { + e.classList.add('reveal'); + } else { + e.classList.remove('reveal'); + } + }); + } +} + +function initToggle() { + // get the previous setting from storage and populate the form. + var optionValue = localStorage.getItem('show'); + document.getElementById('show').value = optionValue; + + if (!document.getElementById('show').value) { + // fix nonsense, missing, or corrupted values. + document.getElementById('show').value = 'exported'; + } + + // enforce the setting. + toggle(); +} diff --git a/third_party/jsdoc/templates/default/static/styles/jsdoc-default.css b/third_party/jsdoc/templates/default/static/styles/jsdoc-default.css new file mode 100644 index 0000000000..5b84b05bd4 --- /dev/null +++ b/third_party/jsdoc/templates/default/static/styles/jsdoc-default.css @@ -0,0 +1,385 @@ +@font-face { + font-family: 'Open Sans'; + font-weight: normal; + font-style: normal; + src: url('../fonts/OpenSans-Regular-webfont.eot'); + src: + local('Open Sans'), + local('OpenSans'), + url('../fonts/OpenSans-Regular-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/OpenSans-Regular-webfont.woff') format('woff'), + url('../fonts/OpenSans-Regular-webfont.svg#open_sansregular') format('svg'); +} + +@font-face { + font-family: 'Open Sans Light'; + font-weight: normal; + font-style: normal; + src: url('../fonts/OpenSans-Light-webfont.eot'); + src: + local('Open Sans Light'), + local('OpenSans Light'), + url('../fonts/OpenSans-Light-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/OpenSans-Light-webfont.woff') format('woff'), + url('../fonts/OpenSans-Light-webfont.svg#open_sanslight') format('svg'); +} + +html +{ + overflow: auto; + background-color: #fff; + font-size: 14px; +} + +body +{ + font-family: 'Open Sans', sans-serif; + line-height: 1.5; + color: #4d4e53; + background-color: white; +} + +a, a:visited, a:active { + color: #0095dd; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +header +{ + display: block; + padding: 0px 4px; +} + +tt, code, kbd, samp { + font-family: Consolas, Monaco, 'Andale Mono', monospace; +} + +.class-description { + font-size: 130%; + line-height: 140%; + margin-bottom: 1em; + margin-top: 1em; +} + +.class-description:empty { + margin: 0; +} + +#main { + float: left; + width: 70%; +} + +article dl { + margin-bottom: 40px; +} + +section +{ + display: block; + background-color: #fff; + padding: 12px 24px; + border-bottom: 1px solid #ccc; + margin-right: 30px; +} + +.variation { + display: none; +} + +.signature-attributes { + font-size: 60%; + color: #aaa; + font-style: italic; + font-weight: lighter; +} + +nav +{ + display: block; + float: right; + margin-top: 28px; + width: 30%; + box-sizing: border-box; + border-left: 1px solid #ccc; + padding-left: 16px; +} + +nav ul { + font-family: 'Lucida Grande', 'Lucida Sans Unicode', arial, sans-serif; + font-size: 100%; + line-height: 17px; + padding: 0; + margin: 0; + list-style-type: none; +} + +nav ul a, nav ul a:visited, nav ul a:active { + font-family: Consolas, Monaco, 'Andale Mono', monospace; + line-height: 18px; + color: #4D4E53; +} + +nav h3 { + margin-top: 12px; +} + +nav li { + margin-top: 6px; +} + +footer { + display: block; + padding: 6px; + margin-top: 12px; + font-style: italic; + font-size: 90%; +} + +p { + margin-bottom: 24px; +} + +h1, h2, h3, h4 { + font-weight: 200; + margin: 0; +} + +h1 +{ + font-family: 'Open Sans Light', sans-serif; + font-size: 48px; + letter-spacing: -2px; + margin: 12px 24px 20px; +} + +h2, h3 +{ + font-size: 30px; + font-weight: 700; + letter-spacing: -1px; + margin-bottom: 12px; +} + +h4 +{ + font-size: 18px; + letter-spacing: -0.33px; + margin-bottom: 12px; + color: #4d4e53; +} + +h5, .container-overview .subsection-title +{ + font-size: 120%; + font-weight: bold; + letter-spacing: -0.01em; + margin: 8px 0 3px -16px; +} + +h6 +{ + font-size: 100%; + letter-spacing: -0.01em; + margin: 6px 0 3px 0; + font-style: italic; +} + +.ancestors { color: #999; } +.ancestors a +{ + color: #999 !important; + text-decoration: none; +} + +.important +{ + font-weight: bold; + color: #950B02; +} + +.yes-def { + text-indent: -1000px; +} + +.type-signature { + color: #aaa; +} + +.name, .signature { + font-family: Consolas, Monaco, 'Andale Mono', monospace; +} + +.details { margin-top: 14px; border-left: 2px solid #DDD; } +.details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } +.details dd { margin-left: 70px; } +.details ul { margin: 0; } +.details ul { list-style-type: none; } +.details li { margin-left: 30px; padding-top: 6px; } +.details pre.prettyprint { margin: 0 } +.details .object-value { padding-top: 0; } + +.description { + margin-bottom: 1em; + margin-top: 1em; +} + +.code-caption +{ + font-style: italic; + font-size: 107%; + margin: 0; +} + +.prettyprint +{ + border: 1px solid #ddd; + width: 80%; + overflow: auto; +} + +.prettyprint.source { + width: inherit; +} + +.prettyprint code +{ + font-size: 100%; + line-height: 18px; + display: block; + padding: 4px 12px; + margin: 0; + background-color: #fff; + color: #4D4E53; +} + +.prettyprint code span.line +{ + display: inline-block; +} + +.prettyprint.linenums +{ + padding-left: 70px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.prettyprint.linenums ol +{ + padding-left: 0; +} + +.prettyprint.linenums li +{ + border-left: 3px #ddd solid; +} + +.prettyprint.linenums li.selected, +.prettyprint.linenums li.selected * +{ + background-color: lightyellow; +} + +.prettyprint.linenums li * +{ + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.params, .props +{ + border-spacing: 0; + border: 0; + border-collapse: collapse; +} + +.params .name, .props .name, .name code { + color: #4D4E53; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 100%; +} + +.params td, .params th, .props td, .props th +{ + border: 1px solid #ddd; + margin: 0px; + text-align: left; + vertical-align: top; + padding: 4px 6px; + display: table-cell; +} + +.params thead tr, .props thead tr +{ + background-color: #ddd; + font-weight: bold; +} + +.params .params thead tr, .props .props thead tr +{ + background-color: #fff; + font-weight: bold; +} + +.params th, .props th { border-right: 1px solid #aaa; } +.params thead .last, .props thead .last { border-right: 1px solid #ddd; } + +.params td.description > p:first-child +{ + margin-top: 0; + padding-top: 0; +} + +.params td.description > p:last-child +{ + margin-bottom: 0; + padding-bottom: 0; +} + +.disabled { + color: #454545; +} + +.visibility-export .name, +li.visibility-export a, li.visibility-export a:active, li.visibility-export a:visited, +.visibility-expose .name, +li.visibility-expose a, li.visibility-expose a:active, li.visibility-expose a:visited { + color: #ad4353; +} + +.show1, .show2 { + display: none; +} + +.reveal { + display: block; +} + +#showContainer { + position: fixed; + right: 20px; + top: 20px; + padding: 5px; + + font-weight: bold; + background-color: rgba(240, 240, 240, 0.6); + border: black 1px solid; +} + +h3.tutorial-heading { + font-size: 20px; +} + +.newCode { + background-color: rgba(255, 255, 0, 0.25); +} + diff --git a/third_party/jsdoc/templates/default/static/styles/prettify-jsdoc.css b/third_party/jsdoc/templates/default/static/styles/prettify-jsdoc.css new file mode 100644 index 0000000000..5a2526e374 --- /dev/null +++ b/third_party/jsdoc/templates/default/static/styles/prettify-jsdoc.css @@ -0,0 +1,111 @@ +/* JSDoc prettify.js theme */ + +/* plain text */ +.pln { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* string content */ +.str { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a keyword */ +.kwd { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a comment */ +.com { + font-weight: normal; + font-style: italic; +} + +/* a type name */ +.typ { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a literal value */ +.lit { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* punctuation */ +.pun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp open bracket */ +.opn { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp close bracket */ +.clo { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a markup tag name */ +.tag { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute name */ +.atn { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute value */ +.atv { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a declaration */ +.dec { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a variable name */ +.var { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a function name */ +.fun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; +} diff --git a/third_party/jsdoc/templates/default/static/styles/prettify-tomorrow.css b/third_party/jsdoc/templates/default/static/styles/prettify-tomorrow.css new file mode 100644 index 0000000000..b6f92a78db --- /dev/null +++ b/third_party/jsdoc/templates/default/static/styles/prettify-tomorrow.css @@ -0,0 +1,132 @@ +/* Tomorrow Theme */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* Pretty printing styles. Used with prettify.js. */ +/* SPAN elements with the classes below are added by prettyprint. */ +/* plain text */ +.pln { + color: #4d4d4c; } + +@media screen { + /* string content */ + .str { + color: #718c00; } + + /* a keyword */ + .kwd { + color: #8959a8; } + + /* a comment */ + .com { + color: #8e908c; } + + /* a type name */ + .typ { + color: #4271ae; } + + /* a literal value */ + .lit { + color: #f5871f; } + + /* punctuation */ + .pun { + color: #4d4d4c; } + + /* lisp open bracket */ + .opn { + color: #4d4d4c; } + + /* lisp close bracket */ + .clo { + color: #4d4d4c; } + + /* a markup tag name */ + .tag { + color: #c82829; } + + /* a markup attribute name */ + .atn { + color: #f5871f; } + + /* a markup attribute value */ + .atv { + color: #3e999f; } + + /* a declaration */ + .dec { + color: #f5871f; } + + /* a variable name */ + .var { + color: #c82829; } + + /* a function name */ + .fun { + color: #4271ae; } } +/* Use higher contrast and text-weight for printable form. */ +@media print, projection { + .str { + color: #060; } + + .kwd { + color: #006; + font-weight: bold; } + + .com { + color: #600; + font-style: italic; } + + .typ { + color: #404; + font-weight: bold; } + + .lit { + color: #044; } + + .pun, .opn, .clo { + color: #440; } + + .tag { + color: #006; + font-weight: bold; } + + .atn { + color: #404; } + + .atv { + color: #060; } } +/* Style */ +/* +pre.prettyprint { + background: white; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 12px; + line-height: 1.5; + border: 1px solid #ccc; + padding: 10px; } +*/ + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; } + +/* IE indents via margin-left */ +li.L0, +li.L1, +li.L2, +li.L3, +li.L4, +li.L5, +li.L6, +li.L7, +li.L8, +li.L9 { + /* */ } + +/* Alternate shading for lines */ +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { + /* */ } diff --git a/third_party/jsdoc/templates/default/tmpl/augments.tmpl b/third_party/jsdoc/templates/default/tmpl/augments.tmpl new file mode 100644 index 0000000000..446d28aa53 --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/augments.tmpl @@ -0,0 +1,10 @@ + + + +
      +
    • +
    + diff --git a/third_party/jsdoc/templates/default/tmpl/container.tmpl b/third_party/jsdoc/templates/default/tmpl/container.tmpl new file mode 100644 index 0000000000..fa0b9c0bfb --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/container.tmpl @@ -0,0 +1,177 @@ + + + + + + + + + +
    + +
    + +

    + + + + + +

    + +
    + + + + +
    + + + +
    + +
    +
    + + +
    + + + + + + + + + +
    + + + + + +

    Example 1? 's':'' ?>

    + + + +
    + + +

    Requires

    + +
      +
    • +
    + + + +

    Classes

    + +
    +
    +
    +
    + + + +

    Mixins

    + +
    +
    +
    +
    + + + +

    Namespaces

    + +
    +
    +
    +
    + + + +

    Members

    + +
    + +
    + + + +

    Methods

    + +
    + +
    + + + +

    Type Definitions

    + +
    + + + +
    + + + +

    Events

    + +
    + +
    + +
    + +
    + + + diff --git a/third_party/jsdoc/templates/default/tmpl/details.tmpl b/third_party/jsdoc/templates/default/tmpl/details.tmpl new file mode 100644 index 0000000000..aa5ab98236 --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/details.tmpl @@ -0,0 +1,152 @@ +" + data.defaultvalue + ""; + defaultObjectClass = ' class="object-value"'; +} +?> + + +
    Properties:
    + +
    + + + +
    + + +
    Version:
    +
    + + + +
    Since:
    +
    + + + +
    Inherited From:
    +
    • + +
    + + + +
    Overrides:
    +
    • + +
    + + + +
    Implementations:
    +
      + +
    • + +
    + + + +
    Implements:
    +
      + +
    • + +
    + + + +
    Extends:
    +
      + +
    • + +
    + + + +
    Mixes In:
    + +
      + +
    • + +
    + + + +
    Deprecated:
    • Yes
      + + + +
      Author:
      +
      +
        +
      • +
      +
      + + + + + + + + +
      License:
      +
      + + + +
      Default Value:
      +
        + > +
      + + + +
      Source:
      +
      • + , +
      + + + +
      Tutorials:
      +
      +
        +
      • +
      +
      + + + +
      See:
      +
      +
        +
      • +
      +
      + + + +
      To Do:
      +
      +
        +
      • +
      +
      + +
      diff --git a/third_party/jsdoc/templates/default/tmpl/example.tmpl b/third_party/jsdoc/templates/default/tmpl/example.tmpl new file mode 100644 index 0000000000..e87caa5b72 --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/example.tmpl @@ -0,0 +1,2 @@ + +
      diff --git a/third_party/jsdoc/templates/default/tmpl/examples.tmpl b/third_party/jsdoc/templates/default/tmpl/examples.tmpl new file mode 100644 index 0000000000..04d975e96d --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/examples.tmpl @@ -0,0 +1,13 @@ + +

      + +
      + \ No newline at end of file diff --git a/third_party/jsdoc/templates/default/tmpl/exceptions.tmpl b/third_party/jsdoc/templates/default/tmpl/exceptions.tmpl new file mode 100644 index 0000000000..78c4e250d0 --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/exceptions.tmpl @@ -0,0 +1,30 @@ + + +
      +
      +
      + +
      +
      +
      +
      +
      + Type +
      +
      + +
      +
      +
      +
      + +
      + + + + + +
      + diff --git a/third_party/jsdoc/templates/default/tmpl/layout.tmpl b/third_party/jsdoc/templates/default/tmpl/layout.tmpl new file mode 100644 index 0000000000..4ef2273cea --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/layout.tmpl @@ -0,0 +1,47 @@ + + + + + JSDoc: <?js= title ?> + + + + + + + + + + + +
      + +

      + + +
      + + + +
      + +
      + Documentation generated by JSDoc on +
      + + + + + + diff --git a/third_party/jsdoc/templates/default/tmpl/mainpage.tmpl b/third_party/jsdoc/templates/default/tmpl/mainpage.tmpl new file mode 100644 index 0000000000..64e9e5943a --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/mainpage.tmpl @@ -0,0 +1,14 @@ + + + +

      + + + +
      +
      +
      + diff --git a/third_party/jsdoc/templates/default/tmpl/members.tmpl b/third_party/jsdoc/templates/default/tmpl/members.tmpl new file mode 100644 index 0000000000..2f0b4ac09e --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/members.tmpl @@ -0,0 +1,42 @@ + +
      +

      + + +

      + +
      +
      + +
      + +
      + + + +
      Type:
      +
        +
      • + +
      • +
      + + + + + +
      Fires:
      +
        +
      • +
      + + + +
      Example 1? 's':'' ?>
      + + +
      diff --git a/third_party/jsdoc/templates/default/tmpl/method.tmpl b/third_party/jsdoc/templates/default/tmpl/method.tmpl new file mode 100644 index 0000000000..e566554f88 --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/method.tmpl @@ -0,0 +1,111 @@ + +
      + + + +

      Constructor

      + + +

      + + + +

      + + +
      +
      + +
      + +
      + + + +
      Extends:
      + + + + +
      Type:
      +
        +
      • + +
      • +
      + + + +
      This:
      +
      + + + +
      Parameters:
      + + + + + + +
      Requires:
      +
        +
      • +
      + + + +
      Fires:
      +
        +
      • +
      + + + +
      Listens to Events:
      +
        +
      • +
      + + + +
      Listeners of This Event:
      +
        +
      • +
      + + + +
      Throws:
      + 1) { ?>
        +
      • +
      + + + + +
      Returns:
      + 1) { ?>
        +
      • +
      + + + + +
      Example 1? 's':'' ?>
      + + +
      diff --git a/third_party/jsdoc/templates/default/tmpl/params.tmpl b/third_party/jsdoc/templates/default/tmpl/params.tmpl new file mode 100644 index 0000000000..66ab04593c --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/params.tmpl @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeAttributesDefaultDescription
      + + + + + + <optional>
      + + + + <nullable>
      + + + + <repeatable>
      + +
      + + + + +
      Properties
      + +
      \ No newline at end of file diff --git a/third_party/jsdoc/templates/default/tmpl/properties.tmpl b/third_party/jsdoc/templates/default/tmpl/properties.tmpl new file mode 100644 index 0000000000..40e0909712 --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/properties.tmpl @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      NameTypeAttributesDefaultDescription
      + + + + + + <optional>
      + + + + <nullable>
      + +
      + + + + +
      Properties
      +
      diff --git a/third_party/jsdoc/templates/default/tmpl/returns.tmpl b/third_party/jsdoc/templates/default/tmpl/returns.tmpl new file mode 100644 index 0000000000..d07045920a --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/returns.tmpl @@ -0,0 +1,19 @@ + +
      + +
      + + + +
      +
      + Type +
      +
      + +
      +
      + \ No newline at end of file diff --git a/third_party/jsdoc/templates/default/tmpl/source.tmpl b/third_party/jsdoc/templates/default/tmpl/source.tmpl new file mode 100644 index 0000000000..e559b5d103 --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/source.tmpl @@ -0,0 +1,8 @@ + +
      +
      +
      +
      +
      \ No newline at end of file diff --git a/third_party/jsdoc/templates/default/tmpl/tutorial.tmpl b/third_party/jsdoc/templates/default/tmpl/tutorial.tmpl new file mode 100644 index 0000000000..9dfe909977 --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/tutorial.tmpl @@ -0,0 +1,17 @@ +
      + + 0) { ?> +
      +
        +
      • +
      +
      + + +
      + +
      + +
      diff --git a/third_party/jsdoc/templates/default/tmpl/type.tmpl b/third_party/jsdoc/templates/default/tmpl/type.tmpl new file mode 100644 index 0000000000..ec2c6c0df7 --- /dev/null +++ b/third_party/jsdoc/templates/default/tmpl/type.tmpl @@ -0,0 +1,7 @@ + + +| + \ No newline at end of file diff --git a/third_party/jsdoc/templates/haruki/README.md b/third_party/jsdoc/templates/haruki/README.md new file mode 100644 index 0000000000..ee6d36f1de --- /dev/null +++ b/third_party/jsdoc/templates/haruki/README.md @@ -0,0 +1,39 @@ +OVERVIEW +======== + +JSDoc 3 Haruki is an experimental template optimised for use with publishing processes that consume either JSON or XML. Whereas the default JSDoc template outputs an HTML representation of your API, Haruki will output a JSON, or optionally an XML, representation. + +Currently Haruki only supports a subset of the tags supported by the default template. Those are: + + * @name + * @desc + * @type + * @namespace + * @method (or @function) + * @member (or @var) + * @class + * @mixin + * @event + * @param + * @returns + * @throws + * @example + * @access (like @private or @public) + +This limited support set is intentional, as it is meant to be a usable set that could be shared with either JavaScript or PHP documentation -- another experimental tool, named "Vonnegut", can produce Haruki compatible JSON from PHPDoc tags. + +Note: `@link`s will appear in the output untransformed, there is no way to know at this stage what the file layout of your output will eventually be. It is assumed that whatever process emits the final output file/s will transform `@link` tags at that point. + +USAGE +===== + + ./jsdoc myscript.js -t templates/haruki -d console -q format=xml + +The results of this command will appear in `stdout` and can be piped into other tools for further processing. + +MORE +===== + +If you are interested in Haruki, you are encouraged to discuss your questions or ideas on the JSDoc-Users mailing list and fork/contribute to this project. + +For more information contact Michael Mathews at . \ No newline at end of file diff --git a/third_party/jsdoc/templates/haruki/publish.js b/third_party/jsdoc/templates/haruki/publish.js new file mode 100644 index 0000000000..657c859172 --- /dev/null +++ b/third_party/jsdoc/templates/haruki/publish.js @@ -0,0 +1,220 @@ +/*eslint no-nested-ternary:0, space-infix-ops: 0 */ +/** + @overview Builds a tree-like JSON string from the doclet data. + @version 0.0.3 + @example + ./jsdoc scratch/jsdoc_test.js -t templates/haruki -d console -q format=xml + */ +'use strict'; + +function graft(parentNode, childNodes, parentLongname, parentName) { + childNodes + .filter(function (element) { + return (element.memberof === parentLongname); + }) + .forEach(function (element, index) { + var i, + len; + + if (element.kind === 'namespace') { + if (! parentNode.namespaces) { + parentNode.namespaces = []; + } + + var thisNamespace = { + 'name': element.name, + 'description': element.description || '', + 'access': element.access || '', + 'virtual': !!element.virtual + }; + + parentNode.namespaces.push(thisNamespace); + + graft(thisNamespace, childNodes, element.longname, element.name); + } + else if (element.kind === 'mixin') { + if (! parentNode.mixins) { + parentNode.mixins = []; + } + + var thisMixin = { + 'name': element.name, + 'description': element.description || '', + 'access': element.access || '', + 'virtual': !!element.virtual + }; + + parentNode.mixins.push(thisMixin); + + graft(thisMixin, childNodes, element.longname, element.name); + } + else if (element.kind === 'function') { + if (! parentNode.functions) { + parentNode.functions = []; + } + + var thisFunction = { + 'name': element.name, + 'access': element.access || '', + 'virtual': !!element.virtual, + 'description': element.description || '', + 'parameters': [ ], + 'examples': [] + }; + + parentNode.functions.push(thisFunction); + + if (element.returns) { + thisFunction.returns = { + 'type': element.returns[0].type? (element.returns[0].type.names.length === 1? element.returns[0].type.names[0] : element.returns[0].type.names) : '', + 'description': element.returns[0].description || '' + }; + } + + if (element.examples) { + for (i = 0, len = element.examples.length; i < len; i++) { + thisFunction.examples.push(element.examples[i]); + } + } + + if (element.params) { + for (i = 0, len = element.params.length; i < len; i++) { + thisFunction.parameters.push({ + 'name': element.params[i].name, + 'type': element.params[i].type? (element.params[i].type.names.length === 1? element.params[i].type.names[0] : element.params[i].type.names) : '', + 'description': element.params[i].description || '', + 'default': element.params[i].defaultvalue || '', + 'optional': typeof element.params[i].optional === 'boolean'? element.params[i].optional : '', + 'nullable': typeof element.params[i].nullable === 'boolean'? element.params[i].nullable : '' + }); + } + } + } + else if (element.kind === 'member') { + if (! parentNode.properties) { + parentNode.properties = []; + } + parentNode.properties.push({ + 'name': element.name, + 'access': element.access || '', + 'virtual': !!element.virtual, + 'description': element.description || '', + 'type': element.type? (element.type.length === 1? element.type[0] : element.type) : '' + }); + } + + else if (element.kind === 'event') { + if (! parentNode.events) { + parentNode.events = []; + } + + var thisEvent = { + 'name': element.name, + 'access': element.access || '', + 'virtual': !!element.virtual, + 'description': element.description || '', + 'parameters': [], + 'examples': [] + }; + + parentNode.events.push(thisEvent); + + if (element.returns) { + thisEvent.returns = { + 'type': element.returns.type? (element.returns.type.names.length === 1? element.returns.type.names[0] : element.returns.type.names) : '', + 'description': element.returns.description || '' + }; + } + + if (element.examples) { + for (i = 0, len = element.examples.length; i < len; i++) { + thisEvent.examples.push(element.examples[i]); + } + } + + if (element.params) { + for (i = 0, len = element.params.length; i < len; i++) { + thisEvent.parameters.push({ + 'name': element.params[i].name, + 'type': element.params[i].type? (element.params[i].type.names.length === 1? element.params[i].type.names[0] : element.params[i].type.names) : '', + 'description': element.params[i].description || '', + 'default': element.params[i].defaultvalue || '', + 'optional': typeof element.params[i].optional === 'boolean'? element.params[i].optional : '', + 'nullable': typeof element.params[i].nullable === 'boolean'? element.params[i].nullable : '' + }); + } + } + } + else if (element.kind === 'class') { + if (! parentNode.classes) { + parentNode.classes = []; + } + + var thisClass = { + 'name': element.name, + 'description': element.classdesc || '', + 'extends': element.augments || [], + 'access': element.access || '', + 'virtual': !!element.virtual, + 'fires': element.fires || '', + 'constructor': { + 'name': element.name, + 'description': element.description || '', + 'parameters': [ + ], + 'examples': [] + } + }; + + parentNode.classes.push(thisClass); + + if (element.examples) { + for (i = 0, len = element.examples.length; i < len; i++) { + thisClass.constructor.examples.push(element.examples[i]); + } + } + + if (element.params) { + for (i = 0, len = element.params.length; i < len; i++) { + thisClass.constructor.parameters.push({ + 'name': element.params[i].name, + 'type': element.params[i].type? (element.params[i].type.names.length === 1? element.params[i].type.names[0] : element.params[i].type.names) : '', + 'description': element.params[i].description || '', + 'default': element.params[i].defaultvalue || '', + 'optional': typeof element.params[i].optional === 'boolean'? element.params[i].optional : '', + 'nullable': typeof element.params[i].nullable === 'boolean'? element.params[i].nullable : '' + }); + } + } + + graft(thisClass, childNodes, element.longname, element.name); + } + }); +} + +/** + @param {TAFFY} data + @param {object} opts + */ +exports.publish = function(data, opts) { + var root = {}, + docs; + + data({undocumented: true}).remove(); + docs = data().get(); // <-- an array of Doclet objects + + graft(root, docs); + + if (opts.destination === 'console') { + if (opts.query && opts.query.format === 'xml') { + var xml = require('js2xmlparser'); + console.log( xml('jsdoc', root) ); + } + else { + global.dump(root); + } + } + else { + console.log('This template only supports output to the console. Use the option "-d console" when you run JSDoc.'); + } +}; diff --git a/third_party/jsdoc/test/README.md b/third_party/jsdoc/test/README.md new file mode 100644 index 0000000000..464562264d --- /dev/null +++ b/third_party/jsdoc/test/README.md @@ -0,0 +1,45 @@ +Testing JSDoc 3 +=============== + +Running Tests +------------- + +Running tests is easy. Just change your working directory to the jsdoc folder +and run the following command on Windows: + + jsdoc -T + +Or on OS X, Linux, and other POSIX-compliant platforms: + + ./jsdoc -T + +If you can't get the short-form commands to work, try invoking Java directly: + + java -cp lib/js.jar org.mozilla.javascript.tools.shell.Main \ + -modules node_modules -modules rhino -modules lib -modules . \ + jsdoc.js -T + +Writing Tests +------------- + +Adding tests is pretty easy, too. You can write tests for JSDoc itself (to +make sure tags and the parser, etc. are working properly), tests for plugins, and/or +tests for templates. + +JSDoc 3 uses Jasmine (https://github.com/pivotal/jasmine) as its testing framework. +Take a look at that project's wiki for documentation on writing tests in general. + +### Tests for JSDoc + +Take a look at the files in the ```test``` directory for many examples of +writing tests for JSDoc itself. The ```test\fixtures``` directory hold fixtures +for use in the tests, and the ```test\specs``` directory holds the tests themselves. + +### Tests for plugins + +Tests for plugins are found in the ```plugins\test``` directory. Plugins containing +tests that were installed with the Jakefile install task will be run automatically. + +### Tests for templates + +TODO diff --git a/third_party/jsdoc/test/async-callback.js b/third_party/jsdoc/test/async-callback.js new file mode 100644 index 0000000000..d2388fa477 --- /dev/null +++ b/third_party/jsdoc/test/async-callback.js @@ -0,0 +1,57 @@ +/*global jasmine: true */ +(function() { + var withoutAsync = {}; + + ["it", "beforeEach", "afterEach"].forEach(function(jasmineFunction) { + withoutAsync[jasmineFunction] = jasmine.Env.prototype[jasmineFunction]; + return jasmine.Env.prototype[jasmineFunction] = function() { + var args = Array.prototype.slice.call(arguments, 0); + var timeout = null; + if (isLastArgumentATimeout(args)) { + timeout = args.pop(); + } + if (isLastArgumentAnAsyncSpecFunction(args)) + { + var specFunction = args.pop(); + args.push(function() { + return asyncSpec(specFunction, this, timeout); + }); + } + return withoutAsync[jasmineFunction].apply(this, args); + }; + }); + + function isLastArgumentATimeout(args) + { + return args.length > 0 && (typeof args[args.length-1]) === "number"; + } + + function isLastArgumentAnAsyncSpecFunction(args) + { + return args.length > 0 && (typeof args[args.length-1]) === "function" && args[args.length-1].length > 0; + } + + function asyncSpec(specFunction, spec, timeout) { + if (timeout == null){timeout = jasmine.DEFAULT_TIMEOUT_INTERVAL || 1000;} + var done = false; + spec.runs(function() { + try { + return specFunction(function(error) { + done = true; + if (error != null) { + return spec.fail(error); + } + }); + } catch (e) { + done = true; + throw e; + } + }); + return spec.waitsFor(function() { + if (done === true) { + return true; + } + }, "spec to complete", timeout); + } + +}).call(this); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/abstracttag.js b/third_party/jsdoc/test/fixtures/abstracttag.js new file mode 100644 index 0000000000..a54db3721f --- /dev/null +++ b/third_party/jsdoc/test/fixtures/abstracttag.js @@ -0,0 +1,17 @@ +/** @constructor */ +function Thingy() { + + /** @abstract */ + this.pez = 2; + +} + +// same as... + +/** @constructor */ +function OtherThingy() { + + /** @virtual */ + this.pez = 2; + +} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/accesstag.js b/third_party/jsdoc/test/fixtures/accesstag.js new file mode 100644 index 0000000000..7d1d8b52a9 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/accesstag.js @@ -0,0 +1,29 @@ +/** @constructor */ +function Thingy() { + + /** @access private */ + var foo = 0; + + /** @access protected */ + this._bar = 1; + + /** @access public */ + this.pez = 2; + +} + +// same as... + +/** @constructor */ +function OtherThingy() { + + /** @private */ + var foo = 0; + + /** @protected */ + this._bar = 1; + + /** @public */ + this.pez = 2; + +} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/alias.js b/third_party/jsdoc/test/fixtures/alias.js new file mode 100644 index 0000000000..924abb70c3 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/alias.js @@ -0,0 +1,13 @@ +var myObject = (function() { + + /** Give x another name. + @alias myObject + @namespace + */ + var x = { + /** document me */ + myProperty: 'foo' + } + + return x; +})(); diff --git a/third_party/jsdoc/test/fixtures/alias2.js b/third_party/jsdoc/test/fixtures/alias2.js new file mode 100644 index 0000000000..6c54c0a5c6 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/alias2.js @@ -0,0 +1,10 @@ +(function() { + + /** @alias ns.Myclass# */ + var x = { + /** document me */ + myProperty: 'foo' + } + + return x; +})(); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/alias3.js b/third_party/jsdoc/test/fixtures/alias3.js new file mode 100644 index 0000000000..673a82c20b --- /dev/null +++ b/third_party/jsdoc/test/fixtures/alias3.js @@ -0,0 +1,12 @@ +Klass('trackr.CookieManager', + + /** @class + @alias trackr.CookieManager + @param {object} kv + */ + function(kv) { + /** document me */ + this.value = kv; + } + +); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/alias4.js b/third_party/jsdoc/test/fixtures/alias4.js new file mode 100644 index 0000000000..dbc41dac24 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/alias4.js @@ -0,0 +1,12 @@ +/** @module jacket */ +define('jacket', function () { + /** + * Jacket constructor. + * + * @constructor + * @alias module:jacket + */ + function Jacket() {} + + return Jacket; +}); diff --git a/third_party/jsdoc/test/fixtures/aliasglobal.js b/third_party/jsdoc/test/fixtures/aliasglobal.js new file mode 100644 index 0000000000..0be1a61cd2 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/aliasglobal.js @@ -0,0 +1,7 @@ +(function() { + + /** @alias .log */ + var log = function() { + } + +})(); diff --git a/third_party/jsdoc/test/fixtures/aliasglobal2.js b/third_party/jsdoc/test/fixtures/aliasglobal2.js new file mode 100644 index 0000000000..00f6e8d04c --- /dev/null +++ b/third_party/jsdoc/test/fixtures/aliasglobal2.js @@ -0,0 +1,18 @@ +(function () { + /** + * Creates a new test object. + * @alias Test + * @constructor + */ + var Test = function(testName) { + /** Document me. */ + this.name = testName; + } + + /** Document me. */ + Test.prototype.run = function(message) { + }; + + /** Document me. */ + Test.counter = 1; +})(); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/aliasresolve.js b/third_party/jsdoc/test/fixtures/aliasresolve.js new file mode 100644 index 0000000000..8e9ec18689 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/aliasresolve.js @@ -0,0 +1,19 @@ +/** + * @namespace + */ +var A = {}; + +(function(ns) { + /** + * @namespace + * @alias A.F + */ + var f = {}; + + /** + * @return {String} + */ + f.method = function(){}; + + ns.F = f; +})(A); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/aliasresolve2.js b/third_party/jsdoc/test/fixtures/aliasresolve2.js new file mode 100644 index 0000000000..4ff8b2821c --- /dev/null +++ b/third_party/jsdoc/test/fixtures/aliasresolve2.js @@ -0,0 +1,19 @@ +/** + * @namespace + */ +var A = {}; + +/** + * @namespace + * @alias A.F + */ +var f = {}; + +(function(ns) { + /** + * @return {String} + */ + f.method = function(){}; + + ns.F = f; +})(A); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/also.js b/third_party/jsdoc/test/fixtures/also.js new file mode 100644 index 0000000000..96a109ef73 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/also.js @@ -0,0 +1,46 @@ +/** @class */ +function Asset() { + this._name = ''; + this._shape = ''; + this._shhhhKeepThisSecret = ''; +} + +/** + * + * Set the value of the name property. + * @param {string} newName + * + *//** + * + * Get the value of the name property. + * @returns {string} + * + */ +Asset.prototype.name = function(newName) { + if (newName) { this._name = newName; } + else { return this._name; } +}; + +/** + * Set the value of the shape property. + * @param {string} newShape + *//** + * Set the value of the shape property, plus some other property. + * @param {string} newShape + * @param {string} mysteryProperty + *//** + * Get the value of the shape property. + * @returns {string} + */ +Asset.prototype.shape = function(newShape, mysteryProperty) { + if (newShape && mysteryProperty) { + this._shape = newShape; + this._shhhhKeepThisSecret = mysteryProperty; + } + else if (newShape) { + this._shape = newShape; + } + else { + return this._shape; + } +}; diff --git a/third_party/jsdoc/test/fixtures/also2.js b/third_party/jsdoc/test/fixtures/also2.js new file mode 100644 index 0000000000..85abe4f698 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/also2.js @@ -0,0 +1,14 @@ +/** @class */ +function BowlingAlley() { + this._lanes = 0; +} + +/** + * Add a lane to the bowling alley. + * @also + * Add the specified number of lanes to the bowling alley. + * @param {number} [lanes=1] - The number of lanes to add. + */ +BowlingAlley.prototype.addLanes = function addLanes(lanes) { + this._lanes += (typeof lanes === undefined) ? 1 : lanes; +}; diff --git a/third_party/jsdoc/test/fixtures/augmentstag.js b/third_party/jsdoc/test/fixtures/augmentstag.js new file mode 100644 index 0000000000..b5e4418140 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/augmentstag.js @@ -0,0 +1,51 @@ +/** + * @constructor + */ +function Foo() { + /** First property */ + this.prop1 = true; +} + +/** + * Second property + * @type {String} + */ +Foo.prototype.prop2 = "parent prop2"; + +/** + * First parent method. + */ +Foo.prototype.method1 = function() {}; + +/** + * Second parent method. + */ +Foo.prototype.method2 = function() {}; + +/** + * @constructor + * @extends Foo + */ +function Bar() { + /** Third prop **/ + this.prop3 = true; +} + +/** + * Second child method. + */ +Bar.prototype.method2 = function() {}; + +/** + * @constructor + * @extends {Bar} + */ +function Baz() { + /** Override prop1 */ + this.prop1 = "new"; +} + +/** + * Third grandchild method. + */ +Baz.prototype.method3 = function() {}; diff --git a/third_party/jsdoc/test/fixtures/augmentstag2.js b/third_party/jsdoc/test/fixtures/augmentstag2.js new file mode 100644 index 0000000000..8def33a93f --- /dev/null +++ b/third_party/jsdoc/test/fixtures/augmentstag2.js @@ -0,0 +1,6 @@ +// Test for @augments/@extends tags that refer to undefined symbols +/** + * @constructor + * @extends UndocumentedThing + */ +function Qux() {} diff --git a/third_party/jsdoc/test/fixtures/augmentstag3.js b/third_party/jsdoc/test/fixtures/augmentstag3.js new file mode 100644 index 0000000000..80851817ad --- /dev/null +++ b/third_party/jsdoc/test/fixtures/augmentstag3.js @@ -0,0 +1,18 @@ +// test to see that we can @augment multiple things (code allows for it) +/** @class */ +function Foo() { +} +/** A method. */ +Foo.prototype.method1 = function () {}; + +/** @class */ +function Bar() { +} +/** Another method. */ +Bar.prototype.method2 = function () {} + +/** @class + * @augments Foo + * @augments Bar */ +function FooBar() { +} diff --git a/third_party/jsdoc/test/fixtures/augmentstag4.js b/third_party/jsdoc/test/fixtures/augmentstag4.js new file mode 100644 index 0000000000..3ead27520d --- /dev/null +++ b/third_party/jsdoc/test/fixtures/augmentstag4.js @@ -0,0 +1,21 @@ +// used to test jsdoc/augments module directly + +/** + * @constructor + * @classdesc Base class + */ +var Base = function() { + /** member */ + this.test1 = "base"; + /** another member */ + this.test2 = null; +}; + +/** + * @constructor + * @extends Base + * @classdesc Extension of Base + */ +var Derived = function() { + this.test1 = "derived"; +}; diff --git a/third_party/jsdoc/test/fixtures/augmentstag5.js b/third_party/jsdoc/test/fixtures/augmentstag5.js new file mode 100644 index 0000000000..dca225f14c --- /dev/null +++ b/third_party/jsdoc/test/fixtures/augmentstag5.js @@ -0,0 +1,36 @@ +/** @class */ +'use strict'; + +function Base0() {} + +Base0.prototype = /** @lends Base0# */ { + /** Description for {@link Base0#methodOfBaseCommon}. */ + methodOfBaseCommon: function() {}, + + /** Description for {@link Base0#methodOfBase0}. */ + methodOfBase0: function() {} +}; + +/** @class */ +function Base1() {} + +Base1.prototype = /** @lends Base1# */ { + /** Description for {@link Base1#methodOfBaseCommon}. */ + methodOfBaseCommon: function() {}, + + /** Description for {@link Base1#methodOfBase1}. */ + methodOfBase1: function() {} +}; + +/** + * @class + * @augments Base0 + * @augments Base1 + */ +function Class() {} + +Class.prototype = Object.create(Base0.prototype); + +Object.getOwnPropertyNames(Base1.prototype).forEach(function (prop) { + Object.defineProperty(Class.prototype, prop, Object.getOwnPropertyDescriptor(Base1.prototype, prop)); +}); diff --git a/third_party/jsdoc/test/fixtures/authortag.js b/third_party/jsdoc/test/fixtures/authortag.js new file mode 100644 index 0000000000..3bba0f0f9b --- /dev/null +++ b/third_party/jsdoc/test/fixtures/authortag.js @@ -0,0 +1,10 @@ +/** @constructor + @author Michael Mathews +*/ +function Thingy() { +} + +/** @author John Doe + * @author Jane Doe */ +function Thingy2() { +} diff --git a/third_party/jsdoc/test/fixtures/borrowstag.js b/third_party/jsdoc/test/fixtures/borrowstag.js new file mode 100644 index 0000000000..7a571adedc --- /dev/null +++ b/third_party/jsdoc/test/fixtures/borrowstag.js @@ -0,0 +1,13 @@ +/** @namespace + @borrows trstr as trim +*/ +var util = { + "trim": trstr +}; + +/** + Remove whitespace from around a string. + @param {string} str + */ +function trstr(str) { +} diff --git a/third_party/jsdoc/test/fixtures/borrowstag2.js b/third_party/jsdoc/test/fixtures/borrowstag2.js new file mode 100644 index 0000000000..22a16435f9 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/borrowstag2.js @@ -0,0 +1,20 @@ +/** @namespace + @borrows rtrim +*/ +var str = { + rtrim: util.rtrim +}; + +/** @namespace + @borrows rtrim +*/ +var util = { + rtrim: rtrim +}; + +/** + Remove whitespace from the right side of a string. + @param {string} str + */ +function rtrim(str) { +} diff --git a/third_party/jsdoc/test/fixtures/callbacktag.js b/third_party/jsdoc/test/fixtures/callbacktag.js new file mode 100644 index 0000000000..5dd468d5d7 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/callbacktag.js @@ -0,0 +1,21 @@ +/** + * @param {requestResponseCallback} cb + */ +function makeSpecialRequest(cb) { +} + +/** + * @param {wrongTypeCallback} cb + */ +function makeExtraSpecialRequest(cb) { +} + +/** + * @callback requestResponseCallback + * @param {number} responseCode + * @param {string} responseText + */ + +/** + * @callback {(object|string)} wrongTypeCallback + */ \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/classdesctag.js b/third_party/jsdoc/test/fixtures/classdesctag.js new file mode 100644 index 0000000000..707a79e62c --- /dev/null +++ b/third_party/jsdoc/test/fixtures/classdesctag.js @@ -0,0 +1,7 @@ +/** + * Asdf. + * @class + * @classdesc A description of the class. + */ +function Foo () { +} diff --git a/third_party/jsdoc/test/fixtures/classtag.js b/third_party/jsdoc/test/fixtures/classtag.js new file mode 100644 index 0000000000..1212b8b170 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/classtag.js @@ -0,0 +1,12 @@ +/** + Describe the Ticker class here. + @class + */ +var Ticker = function() { + +}; + +/** + Describe the NewsSource class here. + @class NewsSource + */ diff --git a/third_party/jsdoc/test/fixtures/classwithoutname.js b/third_party/jsdoc/test/fixtures/classwithoutname.js new file mode 100644 index 0000000000..7e9e42e4f3 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/classwithoutname.js @@ -0,0 +1,9 @@ +// JSDoc should not be able to identify the name of this class. + +var MyClass = Class.define({ + /** + * Create an instance of MyClass. + * @constructs + */ + initialize: function() {} +}); diff --git a/third_party/jsdoc/test/fixtures/constanttag.js b/third_party/jsdoc/test/fixtures/constanttag.js new file mode 100644 index 0000000000..3ca74aae8f --- /dev/null +++ b/third_party/jsdoc/test/fixtures/constanttag.js @@ -0,0 +1,15 @@ +/** @constant */ +var FOO = 1; + +/** @const BAR */ + +/** @const {string} BAZ */ + +/** @const {number} */ +var QUX = 0; + +/** @const {Object} SOCKET */ +var mySocket; + +/** @const ROCKET */ +var myRocket; diff --git a/third_party/jsdoc/test/fixtures/constructortag.js b/third_party/jsdoc/test/fixtures/constructortag.js new file mode 100644 index 0000000000..275ef3c62e --- /dev/null +++ b/third_party/jsdoc/test/fixtures/constructortag.js @@ -0,0 +1,15 @@ +/** + Describe your constructor function here. + @class Describe your class here. + @constructor + @param {string} url + @throws MalformedURL + */ +function Feed(url) { +} + +/** + Document your method here. +*/ +Feed.prototype.refresh = function() { +} diff --git a/third_party/jsdoc/test/fixtures/constructstag.js b/third_party/jsdoc/test/fixtures/constructstag.js new file mode 100644 index 0000000000..9b78dba926 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/constructstag.js @@ -0,0 +1,19 @@ +Classify('TextBlock', { + + /** + Document your constructor function here. + @constructs TextBlock + @classdesc Describe your class here + @param {object} opts + @throws MissingNode + */ + construct: function(node, opts) { + }, + + /** + Document your method here. + @memberof TextBlock# + */ + align: function() { + } +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/constructstag2.js b/third_party/jsdoc/test/fixtures/constructstag2.js new file mode 100644 index 0000000000..92b06fd594 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/constructstag2.js @@ -0,0 +1,16 @@ +Classify('Menu', + /** + @constructs Menu + @param items + */ + function (items) { + + }, + { + /** + @memberof Menu# + */ + show: function(){ + } + } +); diff --git a/third_party/jsdoc/test/fixtures/constructstag3.js b/third_party/jsdoc/test/fixtures/constructstag3.js new file mode 100644 index 0000000000..c992ddeebd --- /dev/null +++ b/third_party/jsdoc/test/fixtures/constructstag3.js @@ -0,0 +1,26 @@ +/** + A class that represents a person. + @class + */ +var Person = Class.create({ + + /** + @constructs Person + @param {string} name + */ + initialize: function(name) { + + /** The name of the person. */ + this.name = name; + }, + + /** + @memberof Person# + @param {string} message + */ + say: function(message) { + + /** The person's message. */ + this.message = message; + } +}); diff --git a/third_party/jsdoc/test/fixtures/constructstag4.js b/third_party/jsdoc/test/fixtures/constructstag4.js new file mode 100644 index 0000000000..a14f8dd921 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/constructstag4.js @@ -0,0 +1,23 @@ +var Person = Class.create(/** @lends Person# */{ + + /** + Describe the constructor. + @classdesc A class that represents a person. + @constructs + @param {string} name + */ + initialize: function(name) { + + /** The name of the person. */ + this.name = name; + }, + + /** + @param {string} message + */ + say: function(message) { + + /** The person's message. */ + this.message = message; + } +}); diff --git a/third_party/jsdoc/test/fixtures/constructstag5.js b/third_party/jsdoc/test/fixtures/constructstag5.js new file mode 100644 index 0000000000..caff0fe538 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/constructstag5.js @@ -0,0 +1,14 @@ +Duck = (function() { + return /** @lends Duck# */ { + /** + Constructs a duck. + @constructs + @param tog + */ + constructor: function(tog) { + }, + /** Say hello. */ + quack: function() { + } + } +})(); diff --git a/third_party/jsdoc/test/fixtures/copyrighttag.js b/third_party/jsdoc/test/fixtures/copyrighttag.js new file mode 100644 index 0000000000..b9eac48eec --- /dev/null +++ b/third_party/jsdoc/test/fixtures/copyrighttag.js @@ -0,0 +1,6 @@ +/** @constructor + @copyright (c) 2011 Michael Mathews +*/ +function Thingy() { + +} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/defaulttag.js b/third_party/jsdoc/test/fixtures/defaulttag.js new file mode 100644 index 0000000000..a4ec8488d5 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/defaulttag.js @@ -0,0 +1,57 @@ +/** + @default + */ +var request = null; + +/** + @default + */ +var response = 'ok'; + +/** + @default + */ +var rcode = 200; + +/** + @default + */ +var rvalid = true; + +/** + @default + */ +var rerrored = false; + +/** + @default the parent window + */ +var win = getParentWindow(); + +/** + @default + */ +var header = getHeaders(request); + +/** + @default + */ +var obj = {valueA: 'a', valueB: false, valueC: 7}; + +/** + * @default + */ +var multilineObject = { + valueA : 'a', + valueB : false, + valueC : 7 +}; + +/** @default */ +var arr = ['foo', true, 19]; + +/** + * @default + * @type {string} + */ +var defaultWithType = 'a'; diff --git a/third_party/jsdoc/test/fixtures/deprecatedtag.js b/third_party/jsdoc/test/fixtures/deprecatedtag.js new file mode 100644 index 0000000000..fed82082ac --- /dev/null +++ b/third_party/jsdoc/test/fixtures/deprecatedtag.js @@ -0,0 +1,11 @@ +/** @deprecated +*/ +function foo() { + +} + +/** @deprecated since version 2.0 +*/ +function bar() { + +} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/descriptiontag.js b/third_party/jsdoc/test/fixtures/descriptiontag.js new file mode 100644 index 0000000000..78d9d05a0e --- /dev/null +++ b/third_party/jsdoc/test/fixtures/descriptiontag.js @@ -0,0 +1,7 @@ +/** Blah Blah Blah + * @desc halb halb halb + */ +var x; + +/** @description lkjasdf */ +var y; diff --git a/third_party/jsdoc/test/fixtures/destructuring.js b/third_party/jsdoc/test/fixtures/destructuring.js new file mode 100644 index 0000000000..2b13f928e3 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/destructuring.js @@ -0,0 +1,5 @@ +/** + A builder function for the Stick application; + @var {function} Application + */ +var {Application} = require("stick"); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/doclet.js b/third_party/jsdoc/test/fixtures/doclet.js new file mode 100644 index 0000000000..ae7e485091 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/doclet.js @@ -0,0 +1,23 @@ +/** + Markdown asterisks in a doclet that does not use leading asterisks. + **Strong** is strong. + + * List item 1. + * List item 2. + @param {string} thingy - The thingy. + */ +function test1(thingy) { + +} + +/** + * Markdown asterisks in a doclet that uses leading asterisks. + * **Strong** is strong. + * + * * List item 1. + * * List item 2. + * @param {string} thingy - The thingy. + */ +function test2(thingy) { + +} diff --git a/third_party/jsdoc/test/fixtures/emptycomments.js b/third_party/jsdoc/test/fixtures/emptycomments.js new file mode 100644 index 0000000000..6ddd67eebe --- /dev/null +++ b/third_party/jsdoc/test/fixtures/emptycomments.js @@ -0,0 +1,27 @@ +/** */ +function first() {} + +/** */ +function second() {} + +// the next comment must contain a single hard tab (\t) character +/** */ +function third() {} + +// the next comment must contain at least two hard tab (\t) characters +/** */ +function fourth() {} + +// the next comment must contain one newline (\n) character +/** + */ +function fifth() {} + +// the next comment must contain multiple newline (\n) characters +/** + * + * + * + * + */ +function sixth() {} diff --git a/third_party/jsdoc/test/fixtures/enumtag.js b/third_party/jsdoc/test/fixtures/enumtag.js new file mode 100644 index 0000000000..2374f09e75 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/enumtag.js @@ -0,0 +1,11 @@ +/** + * Enum for tri-state values. + * @enum {number} + */ +var TriState = { + /** true */ + TRUE: 1, + FALSE: -1, + /** @type {boolean} */ + MAYBE: true +}; \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/enumtag2.js b/third_party/jsdoc/test/fixtures/enumtag2.js new file mode 100644 index 0000000000..2b0f9e630f --- /dev/null +++ b/third_party/jsdoc/test/fixtures/enumtag2.js @@ -0,0 +1,32 @@ +/** @module my/enums */ + +/** + * Enum for quad-state values. + * @enum {number} + * @memberof module:my/enums + */ +var QuadState = exports.QuadState = { + /** true */ + TRUE: 1, + FALSE: -1, + MAYBE: 0, + UNKNOWN: -99 +}; + +/** + * Enum for penta-state values. + * @enum {number} + */ +exports.PentaState = +/** + * Enum for penta-state values, BUT SHOUTIER. + * @enum {number} + */ +exports.PENTASTATE = { + /** true */ + TRUE: 1, + FALSE: -1, + MAYBE: 0, + UNKNOWN: -99, + DONTCARE: 99 +}; diff --git a/third_party/jsdoc/test/fixtures/es6.js b/third_party/jsdoc/test/fixtures/es6.js new file mode 100644 index 0000000000..355d2c4709 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/es6.js @@ -0,0 +1,48 @@ +'use strict'; + +// ArrowFunctionExpression +["Model", "View", "Controller"].forEach(name => console.log(name)); + +// ClassBody, ClassDeclaration, MethodDefinition +class Socket { + constructor(port) { + // ... + } + open() { + // ... + } + close() { + // ... + } +} + +// ClassExpression +var WebSocket = class extends Socket { + // ... +}; + +// ExportBatchSpecifier, ExportDeclaration +export * from 'lib/network'; + +// ExportSpecifier +export {Socket}; + +// ImportDeclaration, ImportSpecifier +import {Packet} from 'lib/data'; + +// ModuleDeclaration +module util from 'lib/util'; + +// SpreadElement +function logItems(...items) { + items.forEach(function(item) { + console.log(item); + }); +} +logItems(...['hello', 'world!']); + +// TaggedTemplateExpression +console.log`hello world!`; + +// TemplateElement, TemplateLiteral +var piMessage = `pi equals ${Math.PI}`; diff --git a/third_party/jsdoc/test/fixtures/eventfirestag.js b/third_party/jsdoc/test/fixtures/eventfirestag.js new file mode 100644 index 0000000000..331eaf8c3c --- /dev/null +++ b/third_party/jsdoc/test/fixtures/eventfirestag.js @@ -0,0 +1,30 @@ +/** + * @class + */ +var Hurl = function () { +}; + +/** + * Throw a snowball. + * + * @fires Hurl#snowball + * @fires Hurl#event:brick + */ +Hurl.prototype.snowball = function () { + /** + * @event Hurl#snowball + */ + this.emit('snowball', {}); +}; + +/** + * Throw a football match. + * + * @emits Hurl#footballMatch + */ +Hurl.prototype.footballMatch = function () { + /** + * @event Hurl#footballMatch + */ + this.emit('footballMatch', {}); +}; diff --git a/third_party/jsdoc/test/fixtures/exampletag.js b/third_party/jsdoc/test/fixtures/exampletag.js new file mode 100644 index 0000000000..b20f75305a --- /dev/null +++ b/third_party/jsdoc/test/fixtures/exampletag.js @@ -0,0 +1,14 @@ +/** @example + * console.log("foo"); + * console.log("bar"); + */ +var x; + +/** @example + * console.log("foo"); + * console.log("bar"); + * @example + * Example 2 + * 1 + 2; + */ +var y; diff --git a/third_party/jsdoc/test/fixtures/exceptiontag.js b/third_party/jsdoc/test/fixtures/exceptiontag.js new file mode 100644 index 0000000000..d210638b26 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/exceptiontag.js @@ -0,0 +1,27 @@ +/** + @throws {InvalidArgumentException} +*/ +function foo(x) { + +} + +/** + @exception Will throw an error if argument is null. +*/ +function bar(x) { + +} + +/** + @exception {DivideByZero} Argument x must be non-zero. +*/ +function pez(x) { + +} + +/** + * A description of the function. + * + * @exception {Object} A description of the exception. + */ +function cos(x) {} diff --git a/third_party/jsdoc/test/fixtures/exports.js b/third_party/jsdoc/test/fixtures/exports.js new file mode 100644 index 0000000000..21d11e768e --- /dev/null +++ b/third_party/jsdoc/test/fixtures/exports.js @@ -0,0 +1,24 @@ +/** + * An example of a server-side JavaScript module. + * @module hello/world + * @example + * var g = require('hello/world').sayHello('Gracie'); + */ + +/** + * Generate a greeting. + * @param {string} [subject="world"] To whom we say hello. + * @returns {string} + */ +exports.sayHello = function(subject) { + return 'Hello ' + (subject || 'World'); +}; + +/** + * Generate a morose farewell. + * @param {string} [subject="world"] To whom we say goodbye. + * @returns {string} + */ +module.exports.sayGoodbye = function(subject) { + return 'Goodbye Cruel ' + (subject || 'World'); +}; diff --git a/third_party/jsdoc/test/fixtures/exportstag.js b/third_party/jsdoc/test/fixtures/exportstag.js new file mode 100644 index 0000000000..e7eab02422 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/exportstag.js @@ -0,0 +1,20 @@ +define(function () { + /** + A module representing a shirt. + @exports my/shirt + @version 1.0 + */ + var shirt = { + + /** A property of the module. */ + color: "black", + + /** @constructor */ + Turtleneck: function(size) { + /** A property of the class. */ + this.size = size; + } + }; + + return shirt; +}); diff --git a/third_party/jsdoc/test/fixtures/exportstag2.js b/third_party/jsdoc/test/fixtures/exportstag2.js new file mode 100644 index 0000000000..4b89f65a47 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/exportstag2.js @@ -0,0 +1,18 @@ +define( + ["my/buttons"], + function () { + /** + A module representing a coat. + @exports my/coat + @requires my/buttons + @version 1.0 + */ + var myModule = function(wool) { + /** document me */ + this.wool = wool; + } + + return myModule; + + } +); diff --git a/third_party/jsdoc/test/fixtures/exportstag3.js b/third_party/jsdoc/test/fixtures/exportstag3.js new file mode 100644 index 0000000000..a26efd513a --- /dev/null +++ b/third_party/jsdoc/test/fixtures/exportstag3.js @@ -0,0 +1,22 @@ +define( + /** + Utility functions to ease working with DOM elements. + @exports html/utils + */ + function () { + + var exports = { + /** Get the value of a property on an element. */ + getStyleProperty: function(element, propertyName) { + // ... + } + }; + + /** Determine if an element is in the document head. */ + exports.isInHead = function(element) { + // ... + } + + return exports; + } +); diff --git a/third_party/jsdoc/test/fixtures/exportstag4.js b/third_party/jsdoc/test/fixtures/exportstag4.js new file mode 100644 index 0000000000..3097a5650d --- /dev/null +++ b/third_party/jsdoc/test/fixtures/exportstag4.js @@ -0,0 +1,12 @@ +define( + /** @exports some/module */ + function () { + /** @class */ + function myClass() {} + + /** Some method */ + myClass.prototype.myMethod = function () {}; + + return new myClass(); + } +); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/exportstag5.js b/third_party/jsdoc/test/fixtures/exportstag5.js new file mode 100644 index 0000000000..b2c5fc8e08 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/exportstag5.js @@ -0,0 +1,13 @@ +define(function() { + 'use strict'; + var bar = function() {}; + /** @exports Foo */ + var Foo = Object.create( + /** @lends module:Foo.prototype */ + { + /** This should be in the Foo module doc. */ + bar : function() {} + } + ); + return Foo; +}); diff --git a/third_party/jsdoc/test/fixtures/exportstag6.js b/third_party/jsdoc/test/fixtures/exportstag6.js new file mode 100644 index 0000000000..18bca55ede --- /dev/null +++ b/third_party/jsdoc/test/fixtures/exportstag6.js @@ -0,0 +1,18 @@ +define(function( +/** + * A module representing a shirt. + * @exports my/shirt + * @version 1.0 + */ +shirtModule) { + /** A property of the module. */ + shirtModule.color = 'black'; + + /** @constructor */ + shirtModule.Turtleneck = function(size) { + /** A property of the class. */ + this.size = size; + }; + + return shirtModule; +}); diff --git a/third_party/jsdoc/test/fixtures/exportstag7.js b/third_party/jsdoc/test/fixtures/exportstag7.js new file mode 100644 index 0000000000..8236f2c5cc --- /dev/null +++ b/third_party/jsdoc/test/fixtures/exportstag7.js @@ -0,0 +1,16 @@ +'use strict'; + +/** @exports my/shirt */ +var myShirt = exports; + +/** A property of the module. */ +myShirt.color = 'black'; + +/** @constructor */ +myShirt.Turtleneck = function(size) { + /** A property of the class. */ + this.size = size; +}; + +/** Iron the turtleneck. */ +myShirt.Turtleneck.prototype.iron = function() {}; diff --git a/third_party/jsdoc/test/fixtures/exportstag8.js b/third_party/jsdoc/test/fixtures/exportstag8.js new file mode 100644 index 0000000000..87b52aa6a8 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/exportstag8.js @@ -0,0 +1,4 @@ +'use strict'; + +/** @exports module:my/shirt */ +var myShirt = exports; diff --git a/third_party/jsdoc/test/fixtures/externaltag.js b/third_party/jsdoc/test/fixtures/externaltag.js new file mode 100644 index 0000000000..d68e05fb46 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/externaltag.js @@ -0,0 +1,24 @@ +/** + * The built in string object. + * @external String + * @see {@link https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String String} + */ + +/** + * Adds a new method to the built-in string. + * @function external:String#rot13 + * @example + * var greeting = new String('hello world'); + * console.log( greeting.rot13() ); // uryyb jbeyq + */ + +/** + * The jQuery plugin namespace. + * @external "jQuery.fn" + * @see {@link http://docs.jquery.com/Plugins/Authoring The jQuery Plugin Guide} + */ + +/** + * A jQuery plugin to make stars fly around your home page. + * @function external:"jQuery.fn".starfairy + */ diff --git a/third_party/jsdoc/test/fixtures/externaltag2.js b/third_party/jsdoc/test/fixtures/externaltag2.js new file mode 100644 index 0000000000..0099f0c1f3 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/externaltag2.js @@ -0,0 +1,11 @@ +/** + Namespace provided by the browser. + @external XMLHttpRequest + @see https://developer.mozilla.org/en/xmlhttprequest + */ + +/** + Extends the built in XMLHttpRequest to send data encoded with a secret key. + @class EncryptedRequest + @extends external:XMLHttpRequest +*/ \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/externaltag3.js b/third_party/jsdoc/test/fixtures/externaltag3.js new file mode 100644 index 0000000000..4948c8eb47 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/externaltag3.js @@ -0,0 +1,22 @@ +/** + * The built in string object. + * @external {String} + * @see {@link https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/String String} + */ + +/** + * Adds a new method to the built-in string. + * @function external:String#rot13 + * @example + * var greeting = new String('hello world'); + * console.log( greeting.rot13() ); // uryyb jbeyq + */ + +/** + * @external {"foo.bar.baz"} + */ + + /** + * Blah blah + * @function external:"foo.bar.baz"#blah + */ diff --git a/third_party/jsdoc/test/fixtures/file.js b/third_party/jsdoc/test/fixtures/file.js new file mode 100644 index 0000000000..e6d62cdc2d --- /dev/null +++ b/third_party/jsdoc/test/fixtures/file.js @@ -0,0 +1,7 @@ +/** + * @overview This is a file doclet. + * @copyright Michael Mathews 2011 + */ + +function ignoreMe() { +} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/funcExpression.js b/third_party/jsdoc/test/fixtures/funcExpression.js new file mode 100644 index 0000000000..924a659fb7 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/funcExpression.js @@ -0,0 +1,7 @@ +/** @class */ +var Foo = function Bar(a) { + /** document me */ + var var1 = 1; + /** document me */ + this.member1 = 2; +}; diff --git a/third_party/jsdoc/test/fixtures/funcExpression2.js b/third_party/jsdoc/test/fixtures/funcExpression2.js new file mode 100644 index 0000000000..0223092ab4 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/funcExpression2.js @@ -0,0 +1,7 @@ +/** @class */ +Foo = function Bar(a) { + /** document me */ + var var1 = 1; + /** document me */ + this.member1 = 2; +}; diff --git a/third_party/jsdoc/test/fixtures/funcExpression3.js b/third_party/jsdoc/test/fixtures/funcExpression3.js new file mode 100644 index 0000000000..8f1858161c --- /dev/null +++ b/third_party/jsdoc/test/fixtures/funcExpression3.js @@ -0,0 +1,9 @@ +ns = { + /** @class */ + Foo: function Bar(a) { + /** document me */ + var var1 = 1; + /** document me */ + this.member1 = 2; + } +}; \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/functiontag.js b/third_party/jsdoc/test/fixtures/functiontag.js new file mode 100644 index 0000000000..9783268fc8 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/functiontag.js @@ -0,0 +1,7 @@ +/** @func Foo */ +function Foo() { +} + +/** @method */ +function Bar() { +} diff --git a/third_party/jsdoc/test/fixtures/getset.js b/third_party/jsdoc/test/fixtures/getset.js new file mode 100644 index 0000000000..34a362f3d4 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/getset.js @@ -0,0 +1,37 @@ +/** @class */ +var Person = makeClass( + /** @lends Person# */ + { + /** Set up initial values. */ + initialize: function(name) { + }, + + /** Speak a message. */ + say: function(message) { + return this.name + " says: " + message; + }, + + /** + * The name of the person. + * @type {string} + */ + get name() { + return this._name; + }, + + /** + * @type {string} + * @param val + */ + set name(val) { + this._name = name; + }, + + /** + * @type {number} + */ + get age() { + return 25; + } + } +); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/globaltag.js b/third_party/jsdoc/test/fixtures/globaltag.js new file mode 100644 index 0000000000..935e7a136f --- /dev/null +++ b/third_party/jsdoc/test/fixtures/globaltag.js @@ -0,0 +1,16 @@ +/** + @global + @constructor + */ +window.Bar = new Function('', a, b, c); + +(function() { + + /** @global */ + var foo; + + foo = 'hello foo'; + + this.foo = foo; + +}).apply(window); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/ignoretag.js b/third_party/jsdoc/test/fixtures/ignoretag.js new file mode 100644 index 0000000000..64b21e1015 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/ignoretag.js @@ -0,0 +1,6 @@ +/** + @ignore +*/ +function foo(x) { + +} diff --git a/third_party/jsdoc/test/fixtures/ignoretag2.js b/third_party/jsdoc/test/fixtures/ignoretag2.js new file mode 100644 index 0000000000..1769e7443c --- /dev/null +++ b/third_party/jsdoc/test/fixtures/ignoretag2.js @@ -0,0 +1,6 @@ +/** + @ignore value that shouldn't be here +*/ +function foo(x) { + +} diff --git a/third_party/jsdoc/test/fixtures/inlinecomment.js b/third_party/jsdoc/test/fixtures/inlinecomment.js new file mode 100644 index 0000000000..883d5f0b9b --- /dev/null +++ b/third_party/jsdoc/test/fixtures/inlinecomment.js @@ -0,0 +1,2 @@ +/** Inline Comment 1 */ this.test = function(){} +/** Inline Comment 2 */ this.test2 = function(){}; diff --git a/third_party/jsdoc/test/fixtures/inner.js b/third_party/jsdoc/test/fixtures/inner.js new file mode 100644 index 0000000000..04ec2ca0d3 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/inner.js @@ -0,0 +1,7 @@ +function sendMessage(text) { + /** document me */ + var encoding = 'utf8'; + + /** document me */ + function encrypt(){} +} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/innerscope.js b/third_party/jsdoc/test/fixtures/innerscope.js new file mode 100644 index 0000000000..b9acc35fdf --- /dev/null +++ b/third_party/jsdoc/test/fixtures/innerscope.js @@ -0,0 +1,17 @@ +/** @constructor */ +function Message(to) { + + var headers = {}, + response; + + /** document me */ + headers.to = to; + + (function() { + /** document me */ + response.code = '200'; + + /** document me */ + headers.from = ''; + })() +} diff --git a/third_party/jsdoc/test/fixtures/innerscope2.js b/third_party/jsdoc/test/fixtures/innerscope2.js new file mode 100644 index 0000000000..4b82f10ecc --- /dev/null +++ b/third_party/jsdoc/test/fixtures/innerscope2.js @@ -0,0 +1,18 @@ +/** @constructor */ +function Message(to) { + + var headers = {}; + + /** document me */ + headers.to = to; + + (function() { + var headers = { + /** document me */ + cache: {} + }; + + /** document me */ + headers.from = ''; + })() +} diff --git a/third_party/jsdoc/test/fixtures/interface-implements.js b/third_party/jsdoc/test/fixtures/interface-implements.js new file mode 100644 index 0000000000..58b9a3d238 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/interface-implements.js @@ -0,0 +1,73 @@ +'use strict'; + +/** + * @interface + */ +function ITester() {} + +/** + * @type {string} + */ +ITester.prototype.hello = '123'; + +/** + * @enum + */ +ITester.type = { + KEYDOWN: 9, + KEYUP: 11 +}; + +/** + * before each method + */ +ITester.prototype.beforeEach = function() {}; + +/** + * it method. + */ +ITester.prototype.it = function() {}; + +/** + * @constructor + * @implements {ITester} + */ +function MyTester() {} + +/** @type {string} */ +MyTester.prototype.hello = '234'; + +/** @enum */ +MyTester.type = { + /** keyboard up */ + KEYDOWN: 9, + KEYUP: 11, + KEYLEFT: 10 +}; +/** + * my tester's beforeEach method. + */ +MyTester.prototype.beforeEach = function() {}; +MyTester.prototype.it = function() {}; + +/** + * @interface + */ +function IWorker() {} +/** Interface for doing some work. */ +IWorker.prototype.work = function() {}; + +/** + * @constructor + * @implements {IWorker} + */ +function MyWorker() {} +/** Do some work. */ +MyWorker.prototype.work = function() {}; +MyWorker.prototype.process = function() {}; + +/** + * @constructor + * @implements {IWorker} + */ +function MyIncompleteWorker() {} diff --git a/third_party/jsdoc/test/fixtures/jslangnames.js b/third_party/jsdoc/test/fixtures/jslangnames.js new file mode 100644 index 0000000000..a80d9439d0 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/jslangnames.js @@ -0,0 +1,23 @@ +/** @namespace */ +var constructor = { + /** document me */ + toString: function(){} +}; + +/** @namespace */ +var prototype = { + /** document me */ + valueOf: function(){} +}; + +/** + * This is Object + * @namespace Object + */ + +/** + * This is Object.hasOwnProperty + * @method Object.hasOwnProperty + */ + +// NOTE: you can't document a prototype of an object in JSDoc -- seriously, you just can't diff --git a/third_party/jsdoc/test/fixtures/kindtag.js b/third_party/jsdoc/test/fixtures/kindtag.js new file mode 100644 index 0000000000..72c5682f7c --- /dev/null +++ b/third_party/jsdoc/test/fixtures/kindtag.js @@ -0,0 +1,2 @@ +/** @kind function */ +var x; diff --git a/third_party/jsdoc/test/fixtures/lends.js b/third_party/jsdoc/test/fixtures/lends.js new file mode 100644 index 0000000000..0b8381509d --- /dev/null +++ b/third_party/jsdoc/test/fixtures/lends.js @@ -0,0 +1,16 @@ +/** @class */ +var Person = makeClass( + /** @lends Person# */ + { + /** Set up initial values. */ + initialize: function(name) { + /** The name of the person. */ + this.name = name; + }, + + /** Speak a message. */ + say: function(message) { + return this.name + " says: " + message; + } + } +); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/lends2.js b/third_party/jsdoc/test/fixtures/lends2.js new file mode 100644 index 0000000000..d2087512b0 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/lends2.js @@ -0,0 +1,18 @@ + +var Person = makeClass( + /** @lends Person# */ + { + /** Construct a Person. + @constructs Person + */ + initialize: function(name) { + /** The name of the person. */ + this.name = name; + }, + + /** Speak a message. */ + say: function(message) { + return this.name + " says: " + message; + } + } +); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/lends3.js b/third_party/jsdoc/test/fixtures/lends3.js new file mode 100644 index 0000000000..de42828c02 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/lends3.js @@ -0,0 +1,18 @@ +/** @class */ +var Person = makeClass( + /** + * @lends Person# + */ + { + /** Set up initial values. */ + initialize: function(name) { + /** The name of the person. */ + this.name = name; + }, + + /** Speak a message. */ + say: function(message) { + return this.name + " says: " + message; + } + } +); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/lends4.js b/third_party/jsdoc/test/fixtures/lends4.js new file mode 100644 index 0000000000..b3ffb759fd --- /dev/null +++ b/third_party/jsdoc/test/fixtures/lends4.js @@ -0,0 +1,16 @@ +define([], function () { + var Person = makeClass( + /** @lends Person.prototype */ + { + /** @constructs */ + initialize: function(name) { + this.name = name; + }, + /** Speak a message. */ + say: function(message) { + return this.name + " says: " + message; + } + } + ); + return Person; +}); diff --git a/third_party/jsdoc/test/fixtures/lends5.js b/third_party/jsdoc/test/fixtures/lends5.js new file mode 100644 index 0000000000..846cd17aa4 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/lends5.js @@ -0,0 +1,15 @@ +(function() { + /** + * @class Person + */ + function Person(name) {} + + Person.prototype = Object.create(null, /** @lends Person.prototype */ { + /** Speak a message. */ + say: function(message) { + return this.name + " says: " + message; + } + }); + + this.Person = Person; +}).call(this); diff --git a/third_party/jsdoc/test/fixtures/lends6.js b/third_party/jsdoc/test/fixtures/lends6.js new file mode 100644 index 0000000000..db6bb069fd --- /dev/null +++ b/third_party/jsdoc/test/fixtures/lends6.js @@ -0,0 +1,29 @@ +define([], function() { + var Person = makeClass( + /** @lends Person.prototype */ + { + /** @constructs */ + initialize: function(name) { + this.name = name; + }, + /** Speak a message. */ + say: function(message) { + return this.name + " says: " + message; + } + } + ); + + var Robot = makeClass( + /** @lends Robot.prototype */ + { + /** @constructs */ + initialize: function(name) { + this.name = name; + }, + /** Feign emotion. */ + emote: function() { + return this.name + " loves you!"; + } + } + ); +}); diff --git a/third_party/jsdoc/test/fixtures/lendsglobal.js b/third_party/jsdoc/test/fixtures/lendsglobal.js new file mode 100644 index 0000000000..48a41761ea --- /dev/null +++ b/third_party/jsdoc/test/fixtures/lendsglobal.js @@ -0,0 +1,14 @@ +declare({ + globals: /** @lends */ { + + /** document me */ + 'test': function() { }, + + /** @namespace */ + 'test1': { + + /** document me */ + 'test2': function() { } + } + } +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/letkeyword.js b/third_party/jsdoc/test/fixtures/letkeyword.js new file mode 100644 index 0000000000..3cbf88fd5b --- /dev/null +++ b/third_party/jsdoc/test/fixtures/letkeyword.js @@ -0,0 +1,17 @@ +/*global define: true */ +define( [], function() { + "use strict"; + + /** + * My example module. + * @exports exampleModule + */ + let myModule = { + /** + * My example method. + */ + exampleMethod: function() {} + }; + + return myModule; +} ); diff --git a/third_party/jsdoc/test/fixtures/licensetag.js b/third_party/jsdoc/test/fixtures/licensetag.js new file mode 100644 index 0000000000..e9d852f5b3 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/licensetag.js @@ -0,0 +1,2 @@ +/** @license GPL v2 */ +var x; diff --git a/third_party/jsdoc/test/fixtures/linktag.js b/third_party/jsdoc/test/fixtures/linktag.js new file mode 100644 index 0000000000..6f23ff1f7a --- /dev/null +++ b/third_party/jsdoc/test/fixtures/linktag.js @@ -0,0 +1,15 @@ +/** @namespace ns */ +var ns = {}; + +/** +* Similar to [the bar function]{@link bar}. +* @see {@link bar} +*/ +ns.foo = function () { +} + +/** +* @see {@link ns.foo} +*/ +function bar() { +} diff --git a/third_party/jsdoc/test/fixtures/listenstag.js b/third_party/jsdoc/test/fixtures/listenstag.js new file mode 100644 index 0000000000..2c72bdafe2 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/listenstag.js @@ -0,0 +1,33 @@ +/** @module myModule */ + +/** An event (has listeners). + * @event MyEvent + * @memberof module:myModule + * @param {number} foo - asdf. */ + +/** A handler. + * @listens module:myModule.MyEvent + * @listens module:myModule~Events.event:Event2 + * @listens fakeEvent + */ +function MyHandler() { +} + +/** Another handler. + * @listens module:myModule.MyEvent + */ +function AnotherHandler() { +} + +/** a namespace. + * @namespace */ +var Events = { +}; + +/** Another event (has listeners). + * @event Event2 + * @memberof module:myModule~Events + */ + +/** An event with no listeners. + * @event module:myModule#Event3 */ diff --git a/third_party/jsdoc/test/fixtures/markdowntest.md b/third_party/jsdoc/test/fixtures/markdowntest.md new file mode 100644 index 0000000000..276aada8f3 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/markdowntest.md @@ -0,0 +1,10 @@ +This is a header +---- + +This is some text. + + this is some code + +* this +* a +* list \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/memberoftag.js b/third_party/jsdoc/test/fixtures/memberoftag.js new file mode 100644 index 0000000000..33595c93df --- /dev/null +++ b/third_party/jsdoc/test/fixtures/memberoftag.js @@ -0,0 +1,11 @@ +/** @constructor + @memberof mathlib + */ +function Data() { + + /** @member */ + this.point = {}; +} + +/** @namespace */ +mathlib = {Data: Data}; diff --git a/third_party/jsdoc/test/fixtures/memberoftag2.js b/third_party/jsdoc/test/fixtures/memberoftag2.js new file mode 100644 index 0000000000..1f3d1480a2 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/memberoftag2.js @@ -0,0 +1,10 @@ +create( + 'Observable', + { + /** @memberof Observable */ + cache: [], + + /** @memberof Observable.prototype */ + publish: function(msg) {} + } +); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/memberoftag3.js b/third_party/jsdoc/test/fixtures/memberoftag3.js new file mode 100644 index 0000000000..a19f13573b --- /dev/null +++ b/third_party/jsdoc/test/fixtures/memberoftag3.js @@ -0,0 +1,19 @@ +/** @module terrain + @example + var terrain = require('terrain'), + forest = new terrain.Forest(), + tree = new forest.Tree(); +*/ + +/** @class */ +exports.Forest = function(){} +var Forest = exports.Forest; + +/** + @class + @memberof module:terrain +*/ +Forest.prototype.Tree = function() { + /** A leaf */ + this.leaf = 1; +} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/memberoftag4.js b/third_party/jsdoc/test/fixtures/memberoftag4.js new file mode 100644 index 0000000000..521343d80a --- /dev/null +++ b/third_party/jsdoc/test/fixtures/memberoftag4.js @@ -0,0 +1,16 @@ +/** + * Namespace doStuff. + * @namespace doStuff + */ + +/** + * Function with the same name as its namespace. + * @memberof doStuff + */ +function doStuff() {} + +/** + * Function with a different name than the namespace. + * @memberof doStuff + */ +function doOtherStuff() {} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/memberoftagforced.js b/third_party/jsdoc/test/fixtures/memberoftagforced.js new file mode 100644 index 0000000000..d116bf5e25 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/memberoftagforced.js @@ -0,0 +1,44 @@ + +/** @constructor +*/ +function Data() { + + /** + The current position. + @type {object} + @property {boolean} needsRevalidate Does this point need to be revalidated? + */ + this.point = { + /** + The x coordinate of the point. + @type {number} + @name point.x + @memberof! Data# + */ + x: 0, + + /** + The y coordinate of the point. + @type {number} + @name point.y + @memberof! Data# + @see {@link Data#point.x} + */ + y: 0, + + needsRevalidate: false + }; +} + +var map = { + /** + @type {Array} + @name map.routes + @memberof! + @property {Data#point} point + */ + routes: [] +} + +/** The current cursor. */ +var cursor = {}; diff --git a/third_party/jsdoc/test/fixtures/membertag.js b/third_party/jsdoc/test/fixtures/membertag.js new file mode 100644 index 0000000000..606b20ad3e --- /dev/null +++ b/third_party/jsdoc/test/fixtures/membertag.js @@ -0,0 +1,8 @@ +/** @member */ +var x; + +/** @var foobar */ +/** @var {string} baz */ + +/** @member {Object} */ +var y; diff --git a/third_party/jsdoc/test/fixtures/mixintag.js b/third_party/jsdoc/test/fixtures/mixintag.js new file mode 100644 index 0000000000..33845f9dfd --- /dev/null +++ b/third_party/jsdoc/test/fixtures/mixintag.js @@ -0,0 +1,26 @@ +/** + * This provides methods used for event handling. It's not meant to + * be used directly, except as a provider of related methods. + * + * @mixin + */ +var Eventful = { + /** fires something. */ + fires: function () {}, + /** handles a signal. */ + on: function () {} +}; + +/** + * @constructor + * @mixes Eventful + */ +var FormButton = function() { +}; + +/** @mixin AnotherMixin*/ + +/** I mix in multiple things + * @constructor MyClass + * @mixes Eventful + * @mixes AnotherMixin */ diff --git a/third_party/jsdoc/test/fixtures/moduleinner.js b/third_party/jsdoc/test/fixtures/moduleinner.js new file mode 100644 index 0000000000..8f0670881b --- /dev/null +++ b/third_party/jsdoc/test/fixtures/moduleinner.js @@ -0,0 +1,28 @@ +/** +* @module my/module +*/ +(function() { + +/** document fooIn */ +fooIn = function() { +}; + +/** @namespace */ +bar = { + /** document bar.Zop */ + zop: function() { + } +} + +/** @constructor */ +exports.Frotz = function() { + /** document exports.Frotz#quaz */ + this.quaz = 1; +} + +}) (); + +/** document fooOut +*/ +fooOut = function() { +}; diff --git a/third_party/jsdoc/test/fixtures/moduleisconstructor.js b/third_party/jsdoc/test/fixtures/moduleisconstructor.js new file mode 100644 index 0000000000..c7a22803e6 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/moduleisconstructor.js @@ -0,0 +1,19 @@ +/** + Describe the module here. + @module mymodule/config +*/ + +/** + Create a new configuration. + + @classdesc Describe the class here. + @class + @alias module:mymodule/config + @param {string} id +*/ +function Config(id) { + /** Document me. */ + this.id = id; +} + +module.exports = Config; \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/moduleisfunction.js b/third_party/jsdoc/test/fixtures/moduleisfunction.js new file mode 100644 index 0000000000..21c29bda5b --- /dev/null +++ b/third_party/jsdoc/test/fixtures/moduleisfunction.js @@ -0,0 +1,10 @@ +/** + * This is a module called foo. + * @module foo + */ + +/** + * The module exports a single function. + * @param {string} bar + */ +module.exports = function(bar) {}; diff --git a/third_party/jsdoc/test/fixtures/modules/data/mod-1.js b/third_party/jsdoc/test/fixtures/modules/data/mod-1.js new file mode 100644 index 0000000000..a5de108fe9 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/modules/data/mod-1.js @@ -0,0 +1,5 @@ +/** @module */ +define({ + property: "foo", + method: function() {} +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/modules/data/mod-2.js b/third_party/jsdoc/test/fixtures/modules/data/mod-2.js new file mode 100644 index 0000000000..1027fd9d3b --- /dev/null +++ b/third_party/jsdoc/test/fixtures/modules/data/mod-2.js @@ -0,0 +1,5 @@ +/** @module my/module/name */ +define({ + property: "foo", + method: function() {} +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/modules/data/mod-3.js b/third_party/jsdoc/test/fixtures/modules/data/mod-3.js new file mode 100644 index 0000000000..00c744f247 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/modules/data/mod-3.js @@ -0,0 +1,21 @@ +/** + My test module. + @module my/module + */ +define(function() { + + /** + @undocumented + @alias module:my/module + */ + var mod = { + + /** Document a property. */ + myProperty: "foo", + + /** Document a method. */ + myMethod: function() {} + }; + + return mod; +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/moduletag.js b/third_party/jsdoc/test/fixtures/moduletag.js new file mode 100644 index 0000000000..fb4c4665c5 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/moduletag.js @@ -0,0 +1,11 @@ +/** + * @module bookshelf + */ + +/** + * @class + */ +this.Book = function(title) { + /** document me */ + this.title = title; +} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/moduletag2.js b/third_party/jsdoc/test/fixtures/moduletag2.js new file mode 100644 index 0000000000..0dd0bc3ec1 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/moduletag2.js @@ -0,0 +1,9 @@ +/** @module color/mixer */ + +module.exports = { + /** Blend two colors together. */ + blend: function(color1, color2) { } +} + +/** Darken a color by the given shade. */ +exports.darken = function(color, shade) { } \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/moduletag3.js b/third_party/jsdoc/test/fixtures/moduletag3.js new file mode 100644 index 0000000000..5cea89bb10 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/moduletag3.js @@ -0,0 +1,19 @@ +/** +@module foo/Photo/manager +@desc Manage a collection of photos. +*/ + +/** +Construct a new Photo manager +@constructor module:foo/Photo/manager +@param {String} collectionId The identifier of the managed collection. +*/ +module.exports = function(collectionId) { + + /** + @function module:foo/Photo/manager#getPhoto + @param {String} photoName + */ + this.getPhoto = function() {} + +} diff --git a/third_party/jsdoc/test/fixtures/moduletag4.js b/third_party/jsdoc/test/fixtures/moduletag4.js new file mode 100644 index 0000000000..cbca4746af --- /dev/null +++ b/third_party/jsdoc/test/fixtures/moduletag4.js @@ -0,0 +1,37 @@ +/** @module M1 */ + +/** + * The differnet kind of mouse buttons. + * + * @summary Blah blah + * @readonly + * @enum {string} + */ +exports.MouseButtons = { + Left: "Left", + Middle: "Middle", + Right: "Right" +}; + +/** + * @typedef ClickProperties + * @type {object} + * @property {MouseButtons} Button - Which mouse button is clicking. + */ + +/** + * Virtual comment function + * + * @function VirtualComment + * @static + * @param {string} comment - comment + * + */ + +/** + * Virtual comment function 2 + * @function VirtualComment2 + * @instance + * @param {string} comment - comment + */ + diff --git a/third_party/jsdoc/test/fixtures/moduletag5.js b/third_party/jsdoc/test/fixtures/moduletag5.js new file mode 100644 index 0000000000..ff5b957c7e --- /dev/null +++ b/third_party/jsdoc/test/fixtures/moduletag5.js @@ -0,0 +1,3 @@ +/** + * @module module:bookshelf + */ diff --git a/third_party/jsdoc/test/fixtures/namespacetag.js b/third_party/jsdoc/test/fixtures/namespacetag.js new file mode 100644 index 0000000000..da20b05920 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/namespacetag.js @@ -0,0 +1,11 @@ +/** @namespace */ +var x = { +}; +/** @namespace Foo */ +/** @namespace {function} Bar */ + +/** @namespace */ +var S = { + /** Member of the namespace S. */ + Socket: function() {} +}; diff --git a/third_party/jsdoc/test/fixtures/nametag.js b/third_party/jsdoc/test/fixtures/nametag.js new file mode 100644 index 0000000000..fdf961eebc --- /dev/null +++ b/third_party/jsdoc/test/fixtures/nametag.js @@ -0,0 +1,24 @@ +/** + * A view. + * @name View + */ + +/** + * A controller. + * @name Controller + * @class + */ +function someOtherName() {} + +/** + * Helper methods for models, views, and controllers. + * @name MvcHelpers + * @namespace + */ + +/** + * Add the item to its parent. + * @name addToParent + * @memberof MvcHelpers + * @inner + */ diff --git a/third_party/jsdoc/test/fixtures/objectlit.js b/third_party/jsdoc/test/fixtures/objectlit.js new file mode 100644 index 0000000000..f976cfb446 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/objectlit.js @@ -0,0 +1,8 @@ +/** document me */ +var tools = { + /** document me */ + serialiser: { + /** document me */ + value: '' + } +}; diff --git a/third_party/jsdoc/test/fixtures/objectlit2.js b/third_party/jsdoc/test/fixtures/objectlit2.js new file mode 100644 index 0000000000..072d4eda5a --- /dev/null +++ b/third_party/jsdoc/test/fixtures/objectlit2.js @@ -0,0 +1,8 @@ +/** document me */ +var position = { + axis: { + /** document me */ + x: 0, + y: 0 + } +}; \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/objectlit3.js b/third_party/jsdoc/test/fixtures/objectlit3.js new file mode 100644 index 0000000000..10fb33ec39 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/objectlit3.js @@ -0,0 +1,10 @@ +/** Tokens that can appear in the stream. */ +var tokens = { + /** Open parenthesis. */ + '(': { + /** Executed before the token is processed. */ + before: function(token) {}, + /** Executed after the token is processed. */ + after: function(token) {} + } +}; diff --git a/third_party/jsdoc/test/fixtures/objectpropertykeys.js b/third_party/jsdoc/test/fixtures/objectpropertykeys.js new file mode 100644 index 0000000000..abc611f2d4 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/objectpropertykeys.js @@ -0,0 +1,22 @@ +Call( +{ + methodA: function() + { + this.id = this.createUUID(); + }, + + valueOf: function() + { + return this.id; + }, + + toString: function() + { + return this.id; + } +}); + +//Simple inheritance model with correct constructor +function Test() {} +function Test2() { Test.call(this); } +Test2.prototype = Object.create(Test.prototype, {constructor: {value: Test2}}); \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/paramtag.js b/third_party/jsdoc/test/fixtures/paramtag.js new file mode 100644 index 0000000000..247fdc99ff --- /dev/null +++ b/third_party/jsdoc/test/fixtures/paramtag.js @@ -0,0 +1,55 @@ +/** +* @param { String | Array} targetName The name (or names) of what to find. +*/ +function find(targetName) { +} + +/** +* @param {function} callback +*/ +function bind(callback) { +} + +/** +* @param {function} +*/ +function unbind(callback) { +} + +/** +* @param id The id of the element. +*/ +function getElement(id) { +} + +/** +* @param ... Two or more elements. +*/ +function combine() { +} + +/** +* @param delimiter - What to split on. +*/ +function split(delimiter) { +} + +/** +* @param - If true make the commit atomic. +*/ +function commit(atomic) { +} + +/** + * @param [async=true] - whether to be asynchronous + */ +function request(async) { +} + +/** @class */ +function MySocket() {} +/** + * @param {string} - Hostname. + * @param {number} - Port number. + */ +MySocket.prototype.open = function(hostname, port) {}; diff --git a/third_party/jsdoc/test/fixtures/paramtag2.js b/third_party/jsdoc/test/fixtures/paramtag2.js new file mode 100644 index 0000000000..7b5d3f97dd --- /dev/null +++ b/third_party/jsdoc/test/fixtures/paramtag2.js @@ -0,0 +1,7 @@ +/** @module mysocket */ + +/** + * @param {string} - Hostname. + * @param {number} - Port number. + */ +exports.open = function(hostname, port) {}; diff --git a/third_party/jsdoc/test/fixtures/paramtaginvalidtype.js b/third_party/jsdoc/test/fixtures/paramtaginvalidtype.js new file mode 100644 index 0000000000..2419d3f1a8 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/paramtaginvalidtype.js @@ -0,0 +1,9 @@ +/** + * @constructor + */ +var Test = function () {}; + +/** + * @param {string, number} a + */ +Test.prototype.test = function (a) {}; diff --git a/third_party/jsdoc/test/fixtures/plugins.js b/third_party/jsdoc/test/fixtures/plugins.js new file mode 100644 index 0000000000..d71e6e9dea --- /dev/null +++ b/third_party/jsdoc/test/fixtures/plugins.js @@ -0,0 +1,10 @@ +/** + * @name virtual + */ + +var foo = "bar"; + +/** + * @foo bar + */ +var test = "tada"; \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/privatetag.js b/third_party/jsdoc/test/fixtures/privatetag.js new file mode 100644 index 0000000000..e25b598ceb --- /dev/null +++ b/third_party/jsdoc/test/fixtures/privatetag.js @@ -0,0 +1,9 @@ +/** +* @constructor +* @private +*/ +function Foo() { + + /** document me */ + this.bar = 1; +} diff --git a/third_party/jsdoc/test/fixtures/privatetag2.js b/third_party/jsdoc/test/fixtures/privatetag2.js new file mode 100644 index 0000000000..f41b893aad --- /dev/null +++ b/third_party/jsdoc/test/fixtures/privatetag2.js @@ -0,0 +1,8 @@ +/** + * @private {Object.} + */ +var connectionPorts = { + 'devServer': 6464, + 'prodServer': 2232, + 'stagingServer': 4997 +}; diff --git a/third_party/jsdoc/test/fixtures/projecttag.js b/third_party/jsdoc/test/fixtures/projecttag.js new file mode 100644 index 0000000000..b00c8e2c68 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/projecttag.js @@ -0,0 +1,9 @@ +/** + An automated documentation generator for JavaScript. + @project JSDoc + @version 3.0.0 + @copyright 2011 (c) Michael Mathews + @license Apache Version 2 + */ +function blah(url) { +} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/propertytag.js b/third_party/jsdoc/test/fixtures/propertytag.js new file mode 100644 index 0000000000..6448182eb3 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/propertytag.js @@ -0,0 +1,15 @@ +/** + * @namespace + * @property {String} id=abc123 The identifier. + * @property {Object} defaults The default values. + * @property {Number} defaults.a The a property of the defaults. + * @property {String} defaults.b The b property of the defaults. + */ +var myobject = { + id: "abc123", + defaults: { + a: 1, + b: "Hit the light", + c: true + } +}; diff --git a/third_party/jsdoc/test/fixtures/protectedtag.js b/third_party/jsdoc/test/fixtures/protectedtag.js new file mode 100644 index 0000000000..68744ba0ad --- /dev/null +++ b/third_party/jsdoc/test/fixtures/protectedtag.js @@ -0,0 +1,20 @@ +/** @module uid */ + +/** @protected */ +var uidCounter = 1; + +/** @protected */ +var uidObjects = { + /** Root object. */ + root: {} +}; + +/** Obtain a unique ID. */ +exports.getUid = function getUid() { + return uidCounter++; +}; + +/** Associate an object with a unique ID. */ +exports.setObjectForUid = function setObjectForUid(obj, uid) { + uidObjects[uid] = obj; +}; diff --git a/third_party/jsdoc/test/fixtures/protectedtag2.js b/third_party/jsdoc/test/fixtures/protectedtag2.js new file mode 100644 index 0000000000..a67e0a8155 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/protectedtag2.js @@ -0,0 +1,13 @@ +/** @protected {number} */ +var uidCounter = 1; + +/** + * Unique ID generator. + * @constructor + */ +function UidGenerator() {} + +/** Generate a unique ID. */ +UidGenerator.prototype.generate = function generate() { + return uidCounter++; +}; diff --git a/third_party/jsdoc/test/fixtures/quotename.js b/third_party/jsdoc/test/fixtures/quotename.js new file mode 100644 index 0000000000..de907eaf12 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/quotename.js @@ -0,0 +1,18 @@ +/** @namespace */ +var chat = {}; + +/** + @namespace + */ +chat["#channel"] = {}; + +/** + @member + @type {boolean} + @defaultvalue + */ +chat["#channel"].open = true; + +/** + @event chat."#channel"."op:announce-motd" + */ \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/quotename2.js b/third_party/jsdoc/test/fixtures/quotename2.js new file mode 100644 index 0000000000..dd383f1ba1 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/quotename2.js @@ -0,0 +1,10 @@ +/** @namespace */ +var contacts = { + + /** @namespace */ + 'say-"hello"@example.com': { + + /** document me */ + "username": 'Sue Smart' + } +} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/readonlytag.js b/third_party/jsdoc/test/fixtures/readonlytag.js new file mode 100644 index 0000000000..9495617fb4 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/readonlytag.js @@ -0,0 +1,8 @@ +/** +* @constructor +*/ +function Collection() { + + /** @readonly */ + this.length = 0; +} diff --git a/third_party/jsdoc/test/fixtures/requirestag.js b/third_party/jsdoc/test/fixtures/requirestag.js new file mode 100644 index 0000000000..49b626fe8a --- /dev/null +++ b/third_party/jsdoc/test/fixtures/requirestag.js @@ -0,0 +1,20 @@ +/** + * @requires module:foo/helper + */ +function foo() { +} + +/** + * @requires foo + * @requires Pez#blat this text is ignored + */ +function bar() { +} + +/** + * @requires {@link module:zest} + * @requires {@linkplain module:zing} + * @requires {@linkstupid module:pizzazz} + */ +function baz() { +} diff --git a/third_party/jsdoc/test/fixtures/returnstag.js b/third_party/jsdoc/test/fixtures/returnstag.js new file mode 100644 index 0000000000..7c6d453543 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/returnstag.js @@ -0,0 +1,23 @@ +/** + * @returns {string|Array} The names of the found item(s). + */ +function find(targetName) { +} + +/** + * @returns {!string} The name, if defined. + */ +function getName() { +} + +/** + * @return The binding id. + */ +function bind(callback) { +} + +/** + * @return An object to be passed to {@link find}. + */ +function convert(name) { +} diff --git a/third_party/jsdoc/test/fixtures/scopetags.js b/third_party/jsdoc/test/fixtures/scopetags.js new file mode 100644 index 0000000000..1eeeb204e8 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/scopetags.js @@ -0,0 +1,10 @@ +/** (scope tags for global objects do not override globalness hence need a container class) + * @module scopetags */ +/** @inner */ +var myInner; + +/** @instance */ +var myInstance; + +/** @static */ +var myStatic; diff --git a/third_party/jsdoc/test/fixtures/seetag.js b/third_party/jsdoc/test/fixtures/seetag.js new file mode 100644 index 0000000000..b439c26183 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/seetag.js @@ -0,0 +1,11 @@ +/** +* @see {@link bar} +*/ +function foo() { +} + +/** +* @see http://example.com/someref +*/ +function bar() { +} diff --git a/third_party/jsdoc/test/fixtures/sincetag.js b/third_party/jsdoc/test/fixtures/sincetag.js new file mode 100644 index 0000000000..890ffb0f14 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/sincetag.js @@ -0,0 +1,6 @@ +/** + @since 1.2.3 +*/ +function foo(x) { + +} diff --git a/third_party/jsdoc/test/fixtures/specialnames.js b/third_party/jsdoc/test/fixtures/specialnames.js new file mode 100644 index 0000000000..d775a0b19f --- /dev/null +++ b/third_party/jsdoc/test/fixtures/specialnames.js @@ -0,0 +1,2 @@ +/** document me */ +var hasOwnProperty = Object.prototype.hasOwnProperty; \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/src/_dir2/four.js b/third_party/jsdoc/test/fixtures/src/_dir2/four.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/third_party/jsdoc/test/fixtures/src/_ignored.js b/third_party/jsdoc/test/fixtures/src/_ignored.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/third_party/jsdoc/test/fixtures/src/dir1/three.js b/third_party/jsdoc/test/fixtures/src/dir1/three.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/third_party/jsdoc/test/fixtures/src/ignored.txt b/third_party/jsdoc/test/fixtures/src/ignored.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/third_party/jsdoc/test/fixtures/src/one.js b/third_party/jsdoc/test/fixtures/src/one.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/third_party/jsdoc/test/fixtures/src/two.js b/third_party/jsdoc/test/fixtures/src/two.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/third_party/jsdoc/test/fixtures/starbangstar.js b/third_party/jsdoc/test/fixtures/starbangstar.js new file mode 100644 index 0000000000..262da969a2 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/starbangstar.js @@ -0,0 +1,12 @@ +/*!* +* Script that does something awesome +* +* @copyright (c) 2011 Rotorz Limited. All rights reserved. +* @author Lea Hayes +* @module myscript/core +*/ + +/*!********************************* + * This should be ignored by JSDoc + * @var x + */ \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/summarytag.js b/third_party/jsdoc/test/fixtures/summarytag.js new file mode 100644 index 0000000000..b1f4c496d9 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/summarytag.js @@ -0,0 +1,3 @@ +/** @summary I do not like green eggs and ham! */ +function Sam() { +} diff --git a/third_party/jsdoc/test/fixtures/testPlugin1.js b/third_party/jsdoc/test/fixtures/testPlugin1.js new file mode 100644 index 0000000000..282397c350 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/testPlugin1.js @@ -0,0 +1,31 @@ +global.jsdocPluginsTest.plugin1 = {}; + +exports.handlers = { + fileBegin: function() { + global.jsdocPluginsTest.plugin1.fileBegin = true; + }, + beforeParse: function() { + global.jsdocPluginsTest.plugin1.beforeParse = true; + }, + jsdocCommentFound: function() { + global.jsdocPluginsTest.plugin1.jsdocCommentFound = true; + }, + symbolFound: function() { + global.jsdocPluginsTest.plugin1.symbolFound = true; + }, + newDoclet: function() { + global.jsdocPluginsTest.plugin1.newDoclet = true; + }, + fileComplete: function() { + global.jsdocPluginsTest.plugin1.fileComplete = true; + } +}; + +exports.defineTags = function(dictionary) { + dictionary.defineTag("foo", { + canHaveName: true, + onTagged: function(doclet, tag) { + doclet.foo = true; + } + }); +}; diff --git a/third_party/jsdoc/test/fixtures/testPlugin2.js b/third_party/jsdoc/test/fixtures/testPlugin2.js new file mode 100644 index 0000000000..441bc3d511 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/testPlugin2.js @@ -0,0 +1,22 @@ +global.jsdocPluginsTest.plugin2 = {}; + +exports.handlers = { + fileBegin: function() { + global.jsdocPluginsTest.plugin2.fileBegin = true; + }, + beforeParse: function() { + global.jsdocPluginsTest.plugin2.beforeParse = true; + }, + jsdocCommentFound: function() { + global.jsdocPluginsTest.plugin2.jsdocCommentFound = true; + }, + symbolFound: function() { + global.jsdocPluginsTest.plugin2.symbolFound = true; + }, + newDoclet: function() { + global.jsdocPluginsTest.plugin2.newDoclet = true; + }, + fileComplete: function() { + global.jsdocPluginsTest.plugin2.fileComplete = true; + } +}; diff --git a/third_party/jsdoc/test/fixtures/this-and-objectlit.js b/third_party/jsdoc/test/fixtures/this-and-objectlit.js new file mode 100644 index 0000000000..66b22fe533 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/this-and-objectlit.js @@ -0,0 +1,11 @@ +/** @constructor */ +function Page(title) { + this.parts = { + title: title, + body: { + /** document me */ + heading: '', + main: '' + } + }; +} diff --git a/third_party/jsdoc/test/fixtures/this.js b/third_party/jsdoc/test/fixtures/this.js new file mode 100644 index 0000000000..7c167606da --- /dev/null +++ b/third_party/jsdoc/test/fixtures/this.js @@ -0,0 +1,10 @@ +/** + @constructor + */ +function Singer() { + + this.tralala = function() { // method of constructor Singer + /** document me */ + this.isSinging = true; // setting a member of constructor Singer + }; +} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/this2.js b/third_party/jsdoc/test/fixtures/this2.js new file mode 100644 index 0000000000..d443ed9cb4 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/this2.js @@ -0,0 +1,15 @@ +/** @constructor */ +function TemplateBuilder(templateType) { + //** document me */ + //this.templateType = templateType; + + /** @constructor */ + this.Template = function() { // nested constructor of constructor TemplateFactory + /** document me */ + this.render = function(data) { + /** document me */ + this.rendered = true; + } + }; + +} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/this3.js b/third_party/jsdoc/test/fixtures/this3.js new file mode 100644 index 0000000000..f78279bd5c --- /dev/null +++ b/third_party/jsdoc/test/fixtures/this3.js @@ -0,0 +1,4 @@ +function setPosition(newP) { + /** document me */ + this.position = newP; // sets global property +} diff --git a/third_party/jsdoc/test/fixtures/thistag.js b/third_party/jsdoc/test/fixtures/thistag.js new file mode 100644 index 0000000000..1ea79253fe --- /dev/null +++ b/third_party/jsdoc/test/fixtures/thistag.js @@ -0,0 +1,10 @@ +/** @constructor */ +function Foo(name) { + setName.apply(this, name); +} + +/** @this Foo */ +function setName(name) { + /** document me */ + this.name = name; +} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/todotag.js b/third_party/jsdoc/test/fixtures/todotag.js new file mode 100644 index 0000000000..5ecfac4b32 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/todotag.js @@ -0,0 +1,6 @@ +/** A function. + * @todo something + * @todo something else + */ +function x() { +} diff --git a/third_party/jsdoc/test/fixtures/tutorialtag.js b/third_party/jsdoc/test/fixtures/tutorialtag.js new file mode 100644 index 0000000000..b0124dbee4 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/tutorialtag.js @@ -0,0 +1,5 @@ +/** Some documentation. + * @tutorial tute1 + * @tutorial tute2 + */ +var x; diff --git a/third_party/jsdoc/test/fixtures/typedeftag.js b/third_party/jsdoc/test/fixtures/typedeftag.js new file mode 100644 index 0000000000..6b3512f111 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/typedeftag.js @@ -0,0 +1,13 @@ +/** @typedef {(string|number)} calc.NumberLike */ + +/** @typedef {string} */ +calc.Operator; + +/** @typedef {calc.NumberLike} calc.Result */ +calc.Outcome; + +/** @param {calc.NumberLike} x A number or a string. */ +calc.readNumber = function(x) { +}; + +/** @typedef {Object} CalculatorBattery */ diff --git a/third_party/jsdoc/test/fixtures/typekind.js b/third_party/jsdoc/test/fixtures/typekind.js new file mode 100644 index 0000000000..46d6c1ebf8 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/typekind.js @@ -0,0 +1,17 @@ +/** + @module {ConnectServer} blog/server +*/ + +module.exports = require('connect').createServer( + Connect.logger(), + Connect.conditionalGet(), + Connect.favicon(), + Connect.cache(), + Connect.gzip(), + require('wheat')(__dirname) +); + +/** + @member {number} module:blog/server.port + @default 8080 +*/ \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/typetag.js b/third_party/jsdoc/test/fixtures/typetag.js new file mode 100644 index 0000000000..12300eed0d --- /dev/null +++ b/third_party/jsdoc/test/fixtures/typetag.js @@ -0,0 +1,14 @@ +/** + * @type {string|Array} + */ +var foo; + +/** + * @type integer + */ +var bar = +(new Date()).getTime(); + +/** + * @type {!Array.} + */ +var baz = [1, 2, 3]; diff --git a/third_party/jsdoc/test/fixtures/typetag2.js b/third_party/jsdoc/test/fixtures/typetag2.js new file mode 100644 index 0000000000..25790895c5 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/typetag2.js @@ -0,0 +1,4 @@ +/** + * @type {(string|number)} A string or a number. + */ +var stringOrNumber; diff --git a/third_party/jsdoc/test/fixtures/typetaginline.js b/third_party/jsdoc/test/fixtures/typetaginline.js new file mode 100644 index 0000000000..a763776118 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/typetaginline.js @@ -0,0 +1,35 @@ +/** + * Inline type info only. + */ +function dispense(/** @type {string} */ candy) {} + +/** + * Inline type info that conflicts with `@param` tag. + * + * @class + * @param {number} candyId - The candy's identifier. + */ +function Dispenser(/** @type {string} */ candyId) {} + +/** + * Inline type info for leading param only. + * + * @param {string} item + */ +function restock(/** @type {Dispenser} */ dispenser, item) {} + +/** + * Inline type info for trailing param only. + * + * @param {Dispenser} dispenser + */ +function clean(dispenser, /** @type {string} */ cleaner) {} + +/** + * Inline type info for inner param only. + * + * @param {Dispenser} dispenser + * @param {number} shade + * @param {string} brand + */ +function paint(dispenser, /** @type {Color} */ color, shade, brand) {} diff --git a/third_party/jsdoc/test/fixtures/typetagwithnewline.js b/third_party/jsdoc/test/fixtures/typetagwithnewline.js new file mode 100644 index 0000000000..88a12f183d --- /dev/null +++ b/third_party/jsdoc/test/fixtures/typetagwithnewline.js @@ -0,0 +1,14 @@ +/** @class Matryoshka */ +function Matryoshka() {} + +/** + * @type {(!Array.| + * !Array.>)} + */ +Matryoshka.mini; + +/** + * @type (!Array.|!Array.>| + * !Array.>>) + */ +Matryoshka.mega; diff --git a/third_party/jsdoc/test/fixtures/undocumentedtag.js b/third_party/jsdoc/test/fixtures/undocumentedtag.js new file mode 100644 index 0000000000..01ab4fc350 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/undocumentedtag.js @@ -0,0 +1,3 @@ +/** Undocumented doclet. + * @undocumented */ +var x; diff --git a/third_party/jsdoc/test/fixtures/utf8.js b/third_party/jsdoc/test/fixtures/utf8.js new file mode 100644 index 0000000000..ec6eea741f --- /dev/null +++ b/third_party/jsdoc/test/fixtures/utf8.js @@ -0,0 +1,6 @@ +/** + * @constructor + * @desc Τεκμηρίωση είναι η επικοινωνία! + */ +Test = function() { +}; \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/var.js b/third_party/jsdoc/test/fixtures/var.js new file mode 100644 index 0000000000..30be5e826f --- /dev/null +++ b/third_party/jsdoc/test/fixtures/var.js @@ -0,0 +1,10 @@ +/** document me */ +const GREEN = 1, + RED = 0; + +/** document me */ +var validate = function(){}; + +var i, + /** document me */ + results; \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/variations.js b/third_party/jsdoc/test/fixtures/variations.js new file mode 100644 index 0000000000..68f4911061 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/variations.js @@ -0,0 +1,26 @@ +/** + * @namespace anim + */ + +/** + * @method anim.fadein(1) + * @desc Show the nodelist elements by fading them to opaque. + * @since 1.0 + * + * @param {number} [duration=400] How long the animation will run. + * @param {function} [callback] Called once the animation is complete. + * + * @returns {this} + */ + +/** + * @method anim.fadein(2) + * @desc Show the nodelist elements by fading them to opaque. + * @since 1.4.3 + * + * @param {number} [duration=400] How long the animation will run. + * @param {string} [easing=swing] The easing function for the transition. + * @param {function} [callback] Called once the animation is complete. + * + * @returns {this} + */ diff --git a/third_party/jsdoc/test/fixtures/variations2.js b/third_party/jsdoc/test/fixtures/variations2.js new file mode 100644 index 0000000000..2105c463fb --- /dev/null +++ b/third_party/jsdoc/test/fixtures/variations2.js @@ -0,0 +1,36 @@ +/** +* Describe the namespace. +* @namespace dollar(1) +*/ + +/** +* Describe the constructor. +* @constructor dollar(2) +* @param {object} [options] +*/ + +/** +* Describe the global function. +* @function dollar(3) +* @param {string} selector - The selector string. +*/ +dollar = function(a, b) { +}; + +/** +* Describe the instance method. +* @instance +* @function empty +* @memberof dollar(2) +*/ +dollar.prototype.empty = function() { +} + +/** +* Describe the static method. +* @function klass +* @memberof dollar(1) +* @param {string} name +*/ +dollar.klass = function(name) { +}; diff --git a/third_party/jsdoc/test/fixtures/variations3.js b/third_party/jsdoc/test/fixtures/variations3.js new file mode 100644 index 0000000000..0b3fdfc432 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/variations3.js @@ -0,0 +1,15 @@ +/** +* @constructor +*/ +someObject = function() {} + +/** +* @constructor +* @variation 2 +*/ +someObject = function() {} + +/** +* @memberof someObject(2) +*/ +someMethod = function() {} \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/versiontag.js b/third_party/jsdoc/test/fixtures/versiontag.js new file mode 100644 index 0000000000..c16dad66e4 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/versiontag.js @@ -0,0 +1,6 @@ +/** + @version 1.2.3 +*/ +function foo(x) { + +} diff --git a/third_party/jsdoc/test/fixtures/virtual.js b/third_party/jsdoc/test/fixtures/virtual.js new file mode 100644 index 0000000000..4eb25c1d20 --- /dev/null +++ b/third_party/jsdoc/test/fixtures/virtual.js @@ -0,0 +1,3 @@ +/** @name dimensions */ + +var width = 12 \ No newline at end of file diff --git a/third_party/jsdoc/test/fixtures/virtual2.js b/third_party/jsdoc/test/fixtures/virtual2.js new file mode 100644 index 0000000000..329b2ab3fd --- /dev/null +++ b/third_party/jsdoc/test/fixtures/virtual2.js @@ -0,0 +1,28 @@ +var Person = Klass.extend( +/** @lends Person.prototype */ +{ + /** @constructs Person */ + initialize: function(name) { + this.name = name; + }, + + /** + * Callback for `say`. + * + * @callback Person~sayCallback + * @param {?string} err - Information about the error, if any. + * @param {?string} message - The message. + */ + /** + * Speak a message asynchronously. + * + * @param {Person~sayCallback} cb + */ + say: function(message, cb) { + if (!message) { + cb('You forgot the message!'); + } + + cb(null, this.name + ' says: ' + message); + } +}); diff --git a/third_party/jsdoc/test/fixtures/virtual3.js b/third_party/jsdoc/test/fixtures/virtual3.js new file mode 100644 index 0000000000..9be56164ae --- /dev/null +++ b/third_party/jsdoc/test/fixtures/virtual3.js @@ -0,0 +1,11 @@ +/** @module connection + +/** + * @param {string} name - The connection name. + * @constructor module:connection + *//** + * @constructor module:connection + */ +module.exports = function() { + // ... +}; diff --git a/third_party/jsdoc/test/jasmine-jsdoc.js b/third_party/jsdoc/test/jasmine-jsdoc.js new file mode 100644 index 0000000000..ecf0a6acdf --- /dev/null +++ b/third_party/jsdoc/test/jasmine-jsdoc.js @@ -0,0 +1,187 @@ +/*global env: true, expect: true, runs: true, waits: true */ +var fs = require('jsdoc/fs'); +var path = require('jsdoc/path'); +var util = require('util'); + +var jsdoc = { + augment: require('jsdoc/augment'), + borrow: require('jsdoc/borrow'), + schema: require('jsdoc/schema'), + src: { + handlers: require('jsdoc/src/handlers'), + parser: require('jsdoc/src/parser') + }, + util: { + runtime: require('jsdoc/util/runtime') + } +}; + +var hasOwnProp = Object.prototype.hasOwnProperty; + +var jasmineAll = require('./lib/jasmine'); +var jasmine = jasmineAll.jasmine; +var jasmineNode = ( require('./reporter') )(jasmine); + +var reporter = null; + +var parseResults = []; + +jasmine.addParseResults = function(filename, doclets) { + parseResults.push({ + filename: filename, + doclets: doclets + }); +}; + +jasmine.getParseResults = function() { + return parseResults; +}; + +// use the requested parser, or default to Esprima (on Node.js) or Rhino (on Rhino) +jasmine.jsParser = (function() { + var parser = jsdoc.util.runtime.isRhino() ? 'rhino' : 'esprima'; + + if (env.opts.query && env.opts.query.parser) { + parser = env.opts.query.parser; + // remove this so the config tests don't complain + delete env.opts.query; + } + + return parser; +})(); + +jasmine.initialize = function(done, verbose) { + var jasmineEnv = jasmine.getEnv(); + + if (reporter !== null) { + // If we've run before, we need to reset the runner + jasmineEnv.currentRunner_ = new jasmine.Runner(jasmineEnv); + // And clear the reporter + jasmineEnv.reporter.subReporters_.splice(jasmineEnv.reporter.subReporters_.indexOf(reporter)); + } + + var reporterOpts = { + color: env.opts.nocolor === true ? false : true, + onComplete: done + }; + + reporter = env.opts.verbose ? new jasmineNode.TerminalVerboseReporter(reporterOpts) : + new jasmineNode.TerminalReporter(reporterOpts); + jasmineEnv.addReporter(reporter); + + // updateInterval is set to 0 because there were not-fully-understood + // issues with asynchronous behavior in jasmine otherwise. + jasmineEnv.updateInterval = 0; + + return jasmineEnv; +}; + +jasmine.createParser = function(type) { + return jsdoc.src.parser.createParser(type || jasmine.jsParser); +}; + +function capitalize(str) { + return str[0].toUpperCase() + str.slice(1); +} + +/** + * Execute the specs in the specified folder. + * + * @param {string} folder The folder in which the specs are to be found. + * @param {function?} done Callback function to execute when finished. + * @param {object} opts Options for executing the specs. + * @param {boolean} opts.verbose Whether or not to output verbose results. + * @param {RegExp} opts.matcher A regular expression to filter specs by. Only matching specs run. + */ +jasmine.executeSpecsInFolder = function(folder, done, opts) { + var fileMatcher = opts.matcher || new RegExp(".(js)$", "i"), + specs = require('./spec-collection'), + jasmineEnv = jasmine.initialize(done, opts.verbose); + + // Load the specs + specs.load(folder, fileMatcher, true); + + var specsList = specs.getSpecs(); + var filename; + + // Add the specs to the context + for (var i = 0, len = specsList.length; i < len; ++i) { + filename = specsList[i]; + require(filename.path().replace(/\\/g, '/'). + replace(new RegExp('^' + env.dirname + '/test'), './'). + replace(/\.\w+$/, '')); + } + + // Run Jasmine + jasmineEnv.execute(); +}; + +function now() { + return new Date().getTime(); +} + +jasmine.asyncSpecWait = function() { + var wait = this.asyncSpecWait; + wait.start = now(); + wait.done = false; + (function innerWait() { + waits(10); + runs(function() { + if (wait.start + wait.timeout < now()) { + expect('timeout waiting for spec').toBeNull(); + } else if (wait.done) { + wait.done = false; + } else { + innerWait(); + } + }); + })(); +}; +jasmine.asyncSpecWait.timeout = 4 * 1000; +jasmine.asyncSpecDone = function() { + jasmine.asyncSpecWait.done = true; +}; + +jasmine.getDocSetFromFile = function(filename, parser, validate) { + var doclets; + var validationResult; + + var sourceCode = fs.readFileSync( path.join(env.dirname, filename), 'utf8' ); + var testParser = parser || jasmine.createParser(); + + jsdoc.src.handlers.attachTo(testParser); + + doclets = testParser.parse('javascript:' + sourceCode); + jsdoc.borrow.indexAll(doclets); + + jsdoc.augment.addInherited(doclets); + jsdoc.augment.addImplemented(doclets); + + // test assume borrows have not yet been resolved + // require('jsdoc/borrow').resolveBorrows(doclets); + + // store the parse results for later validation + if (validate !== false) { + jasmine.addParseResults(filename, doclets); + } + + return { + doclets: doclets, + getByLongname: function(longname) { + return doclets.filter(function(doclet) { + return (doclet.longname || doclet.name) === longname; + }); + } + }; +}; + +// set up jasmine's global functions +Object.keys(jasmine).forEach(function(key) { + exports[key] = global[key] = jasmine[key]; +}); +global.jasmine = jasmine; +require('./async-callback'); +['spyOn', 'it', 'xit', 'expect', 'runs', 'waitsFor', 'beforeEach', 'afterEach', 'describe', + 'xdescribe'].forEach(function(item) { + global[item] = jasmineAll[item]; +}); diff --git a/third_party/jsdoc/test/lib/jasmine.js b/third_party/jsdoc/test/lib/jasmine.js new file mode 100644 index 0000000000..a261473dc7 --- /dev/null +++ b/third_party/jsdoc/test/lib/jasmine.js @@ -0,0 +1,2537 @@ +var isCommonJS = typeof window == "undefined"; + +/** + * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework. + * + * @namespace + */ +var jasmine = {}; +if (isCommonJS){exports.jasmine = jasmine;} +/** + * @private + */ +jasmine.unimplementedMethod_ = function() { + throw new Error("unimplemented method"); +}; + +/** + * Use jasmine.undefined instead of undefined, since undefined is just + * a plain old variable and may be redefined by somebody else. + * + * @private + */ +jasmine.undefined = jasmine.___undefined___; + +/** + * Show diagnostic messages in the console if set to true + * + */ +jasmine.VERBOSE = false; + +/** + * Default interval in milliseconds for event loop yields (e.g. to allow network activity or to refresh the screen with the HTML-based runner). Small values here may result in slow test running. Zero means no updates until all tests have completed. + * + */ +jasmine.DEFAULT_UPDATE_INTERVAL = 250; + +/** + * Default timeout interval in milliseconds for waitsFor() blocks. + */ +jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; + +jasmine.getGlobal = function() { + function getGlobal() { + return this; + } + + return getGlobal(); +}; + +/** + * Allows for bound functions to be compared. Internal use only. + * + * @ignore + * @private + * @param base {Object} bound 'this' for the function + * @param name {Function} function to find + */ +jasmine.bindOriginal_ = function(base, name) { + var original = base[name]; + if (original.apply) { + return function() { + return original.apply(base, arguments); + }; + } else { + // IE support + return jasmine.getGlobal()[name]; + } +}; + +jasmine.setTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'setTimeout'); +jasmine.clearTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearTimeout'); +jasmine.setInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'setInterval'); +jasmine.clearInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearInterval'); + +jasmine.MessageResult = function(values) { + this.type = 'log'; + this.values = values; + this.trace = new Error(); // todo: test better +}; + +jasmine.MessageResult.prototype.toString = function() { + var text = ""; + for (var i = 0; i < this.values.length; i++) { + if (i > 0){text += " ";} + if (jasmine.isString_(this.values[i])) { + text += this.values[i]; + } else { + text += jasmine.pp(this.values[i]); + } + } + return text; +}; + +jasmine.ExpectationResult = function(params) { + this.type = 'expect'; + this.matcherName = params.matcherName; + this.passed_ = params.passed; + this.expected = params.expected; + this.actual = params.actual; + this.message = this.passed_ ? 'Passed.' : params.message; + + var trace = (params.trace || new Error(this.message)); + this.trace = this.passed_ ? '' : trace; +}; + +jasmine.ExpectationResult.prototype.toString = function () { + return this.message; +}; + +jasmine.ExpectationResult.prototype.passed = function () { + return this.passed_; +}; + +/** + * Getter for the Jasmine environment. Ensures one gets created + */ +jasmine.getEnv = function() { + var env = jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env(); + return env; +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isArray_ = function(value) { + return jasmine.isA_("Array", value); +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isString_ = function(value) { + return jasmine.isA_("String", value); +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isNumber_ = function(value) { + return jasmine.isA_("Number", value); +}; + +/** + * @ignore + * @private + * @param {String} typeName + * @param value + * @returns {Boolean} + */ +jasmine.isA_ = function(typeName, value) { + return Object.prototype.toString.apply(value) === '[object ' + typeName + ']'; +}; + +/** + * Pretty printer for expecations. Takes any object and turns it into a human-readable string. + * + * @param value {Object} an object to be outputted + * @returns {String} + */ +jasmine.pp = function(value) { + var stringPrettyPrinter = new jasmine.StringPrettyPrinter(); + stringPrettyPrinter.format(value); + return stringPrettyPrinter.string; +}; + +/** + * Returns true if the object is a DOM Node. + * + * @param {Object} obj object to check + * @returns {Boolean} + */ +jasmine.isDomNode = function(obj) { + return obj.nodeType > 0; +}; + +/** + * Returns a matchable 'generic' object of the class type. For use in expecations of type when values don't matter. + * + * @example + * // don't care about which function is passed in, as long as it's a function + * expect(mySpy).toHaveBeenCalledWith(jasmine.any(Function)); + * + * @param {Class} clazz + * @returns matchable object of the type clazz + */ +jasmine.any = function(clazz) { + return new jasmine.Matchers.Any(clazz); +}; + +/** + * Returns a matchable subset of a JSON object. For use in expectations when you don't care about all of the + * attributes on the object. + * + * @example + * // don't care about any other attributes than foo. + * expect(mySpy).toHaveBeenCalledWith(jasmine.objectContaining({foo: "bar"}); + * + * @param sample {Object} sample + * @returns matchable object for the sample + */ +jasmine.objectContaining = function (sample) { + return new jasmine.Matchers.ObjectContaining(sample); +}; + +/** + * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks. + * + * Spies should be created in test setup, before expectations. They can then be checked, using the standard Jasmine + * expectation syntax. Spies can be checked if they were called or not and what the calling params were. + * + * A Spy has the following fields: wasCalled, callCount, mostRecentCall, and argsForCall (see docs). + * + * Spies are torn down at the end of every spec. + * + * Note: Do not call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj. + * + * @example + * // a stub + * var myStub = jasmine.createSpy('myStub'); // can be used anywhere + * + * // spy example + * var foo = { + * not: function(bool) { return !bool; } + * } + * + * // actual foo.not will not be called, execution stops + * spyOn(foo, 'not'); + + // foo.not spied upon, execution will continue to implementation + * spyOn(foo, 'not').andCallThrough(); + * + * // fake example + * var foo = { + * not: function(bool) { return !bool; } + * } + * + * // foo.not(val) will return val + * spyOn(foo, 'not').andCallFake(function(value) {return value;}); + * + * // mock example + * foo.not(7 == 7); + * expect(foo.not).toHaveBeenCalled(); + * expect(foo.not).toHaveBeenCalledWith(true); + * + * @constructor + * @see spyOn, jasmine.createSpy, jasmine.createSpyObj + * @param {String} name + */ +jasmine.Spy = function(name) { + /** + * The name of the spy, if provided. + */ + this.identity = name || 'unknown'; + /** + * Is this Object a spy? + */ + this.isSpy = true; + /** + * The actual function this spy stubs. + */ + this.plan = function() { + }; + /** + * Tracking of the most recent call to the spy. + * @example + * var mySpy = jasmine.createSpy('foo'); + * mySpy(1, 2); + * mySpy.mostRecentCall.args = [1, 2]; + */ + this.mostRecentCall = {}; + + /** + * Holds arguments for each call to the spy, indexed by call count + * @example + * var mySpy = jasmine.createSpy('foo'); + * mySpy(1, 2); + * mySpy(7, 8); + * mySpy.mostRecentCall.args = [7, 8]; + * mySpy.argsForCall[0] = [1, 2]; + * mySpy.argsForCall[1] = [7, 8]; + */ + this.argsForCall = []; + this.calls = []; +}; + +/** + * Tells a spy to call through to the actual implemenatation. + * + * @example + * var foo = { + * bar: function() { // do some stuff } + * } + * + * // defining a spy on an existing property: foo.bar + * spyOn(foo, 'bar').andCallThrough(); + */ +jasmine.Spy.prototype.andCallThrough = function() { + this.plan = this.originalValue; + return this; +}; + +/** + * For setting the return value of a spy. + * + * @example + * // defining a spy from scratch: foo() returns 'baz' + * var foo = jasmine.createSpy('spy on foo').andReturn('baz'); + * + * // defining a spy on an existing property: foo.bar() returns 'baz' + * spyOn(foo, 'bar').andReturn('baz'); + * + * @param {Object} value + */ +jasmine.Spy.prototype.andReturn = function(value) { + this.plan = function() { + return value; + }; + return this; +}; + +/** + * For throwing an exception when a spy is called. + * + * @example + * // defining a spy from scratch: foo() throws an exception w/ message 'ouch' + * var foo = jasmine.createSpy('spy on foo').andThrow('baz'); + * + * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch' + * spyOn(foo, 'bar').andThrow('baz'); + * + * @param {String} exceptionMsg + */ +jasmine.Spy.prototype.andThrow = function(exceptionMsg) { + this.plan = function() { + throw exceptionMsg; + }; + return this; +}; + +/** + * Calls an alternate implementation when a spy is called. + * + * @example + * var baz = function() { + * // do some stuff, return something + * } + * // defining a spy from scratch: foo() calls the function baz + * var foo = jasmine.createSpy('spy on foo').andCall(baz); + * + * // defining a spy on an existing property: foo.bar() calls an anonymnous function + * spyOn(foo, 'bar').andCall(function() { return 'baz';} ); + * + * @param {Function} fakeFunc + */ +jasmine.Spy.prototype.andCallFake = function(fakeFunc) { + this.plan = fakeFunc; + return this; +}; + +/** + * Resets all of a spy's the tracking variables so that it can be used again. + * + * @example + * spyOn(foo, 'bar'); + * + * foo.bar(); + * + * expect(foo.bar.callCount).toEqual(1); + * + * foo.bar.reset(); + * + * expect(foo.bar.callCount).toEqual(0); + */ +jasmine.Spy.prototype.reset = function() { + this.wasCalled = false; + this.callCount = 0; + this.argsForCall = []; + this.calls = []; + this.mostRecentCall = {}; +}; + +jasmine.createSpy = function(name) { + + var spyObj = function() { + spyObj.wasCalled = true; + spyObj.callCount++; + var args = jasmine.util.argsToArray(arguments); + spyObj.mostRecentCall.object = this; + spyObj.mostRecentCall.args = args; + spyObj.argsForCall.push(args); + spyObj.calls.push({object: this, args: args}); + return spyObj.plan.apply(this, arguments); + }; + + var spy = new jasmine.Spy(name); + + for (var prop in spy) { + spyObj[prop] = spy[prop]; + } + + spyObj.reset(); + + return spyObj; +}; + +/** + * Determines whether an object is a spy. + * + * @param {jasmine.Spy|Object} putativeSpy + * @returns {Boolean} + */ +jasmine.isSpy = function(putativeSpy) { + return putativeSpy && putativeSpy.isSpy; +}; + +/** + * Creates a more complicated spy: an Object that has every property a function that is a spy. Used for stubbing something + * large in one call. + * + * @param {String} baseName name of spy class + * @param {Array} methodNames array of names of methods to make spies + */ +jasmine.createSpyObj = function(baseName, methodNames) { + if (!jasmine.isArray_(methodNames) || methodNames.length === 0) { + throw new Error('createSpyObj requires a non-empty array of method names to create spies for'); + } + var obj = {}; + for (var i = 0; i < methodNames.length; i++) { + obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]); + } + return obj; +}; + +/** + * All parameters are pretty-printed and concatenated together, then written to the current spec's output. + * + * Be careful not to leave calls to jasmine.log in production code. + */ +jasmine.log = function() { + var spec = jasmine.getEnv().currentSpec; + spec.log.apply(spec, arguments); +}; + +/** + * Function that installs a spy on an existing object's method name. Used within a Spec to create a spy. + * + * @example + * // spy example + * var foo = { + * not: function(bool) { return !bool; } + * } + * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops + * + * @see jasmine.createSpy + * @param obj + * @param methodName + * @returns a Jasmine spy that can be chained with all spy methods + */ +var spyOn = function(obj, methodName) { + return jasmine.getEnv().currentSpec.spyOn(obj, methodName); +}; +if (isCommonJS){exports.spyOn = spyOn;} + +/** + * Creates a Jasmine spec that will be added to the current suite. + * + * // TODO: pending tests + * + * @example + * it('should be true', function() { + * expect(true).toEqual(true); + * }); + * + * @param {String} desc description of this specification + * @param {Function} func defines the preconditions and expectations of the spec + */ +var it = function(desc, func) { + return jasmine.getEnv().it(desc, func); +}; +if (isCommonJS){exports.it = it;} + +/** + * Creates a disabled Jasmine spec. + * + * A convenience method that allows existing specs to be disabled temporarily during development. + * + * @param {String} desc description of this specification + * @param {Function} func defines the preconditions and expectations of the spec + */ +var xit = function(desc, func) { + return jasmine.getEnv().xit(desc, func); +}; +if (isCommonJS){exports.xit = xit;} + +/** + * Starts a chain for a Jasmine expectation. + * + * It is passed an Object that is the actual value and should chain to one of the many + * jasmine.Matchers functions. + * + * @param {Object} actual Actual value to test against and expected value + */ +var expect = function(actual) { + return jasmine.getEnv().currentSpec.expect(actual); +}; +if (isCommonJS){exports.expect = expect;} + +/** + * Defines part of a jasmine spec. Used in cominbination with waits or waitsFor in asynchrnous specs. + * + * @param {Function} func Function that defines part of a jasmine spec. + */ +var runs = function(func) { + jasmine.getEnv().currentSpec.runs(func); +}; +if (isCommonJS){exports.runs = runs;} + +/** + * Waits a fixed time period before moving to the next block. + * + * @deprecated Use waitsFor() instead + * @param {Number} timeout milliseconds to wait + */ +var waits = function(timeout) { + jasmine.getEnv().currentSpec.waits(timeout); +}; +if (isCommonJS){exports.waits = waits;} + +/** + * Waits for the latchFunction to return true before proceeding to the next block. + * + * @param {Function} latchFunction + * @param {String} optional_timeoutMessage + * @param {Number} optional_timeout + */ +var waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { + jasmine.getEnv().currentSpec.waitsFor.apply(jasmine.getEnv().currentSpec, arguments); +}; +if (isCommonJS){exports.waitsFor = waitsFor;} + +/** + * A function that is called before each spec in a suite. + * + * Used for spec setup, including validating assumptions. + * + * @param {Function} beforeEachFunction + */ +var beforeEach = function(beforeEachFunction) { + jasmine.getEnv().beforeEach(beforeEachFunction); +}; +if (isCommonJS){exports.beforeEach = beforeEach;} + +/** + * A function that is called after each spec in a suite. + * + * Used for restoring any state that is hijacked during spec execution. + * + * @param {Function} afterEachFunction + */ +var afterEach = function(afterEachFunction) { + jasmine.getEnv().afterEach(afterEachFunction); +}; +if (isCommonJS){exports.afterEach = afterEach;} + +/** + * Defines a suite of specifications. + * + * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared + * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization + * of setup in some tests. + * + * @example + * // TODO: a simple suite + * + * // TODO: a simple suite with a nested describe block + * + * @param {String} description A string, usually the class under test. + * @param {Function} specDefinitions function that defines several specs. + */ +var describe = function(description, specDefinitions) { + return jasmine.getEnv().describe(description, specDefinitions); +}; +if (isCommonJS){exports.describe = describe;} + +/** + * Disables a suite of specifications. Used to disable some suites in a file, or files, temporarily during development. + * + * @param {String} description A string, usually the class under test. + * @param {Function} specDefinitions function that defines several specs. + */ +var xdescribe = function(description, specDefinitions) { + return jasmine.getEnv().xdescribe(description, specDefinitions); +}; +if (isCommonJS){exports.xdescribe = xdescribe;} + +// Provide the XMLHttpRequest class for IE 5.x-6.x: +jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() { + function tryIt(f) { + try { + return f(); + } catch(e) { + } + return null; + } + + var xhr = tryIt(function() { + return new ActiveXObject("Msxml2.XMLHTTP.6.0"); + }) || + tryIt(function() { + return new ActiveXObject("Msxml2.XMLHTTP.3.0"); + }) || + tryIt(function() { + return new ActiveXObject("Msxml2.XMLHTTP"); + }) || + tryIt(function() { + return new ActiveXObject("Microsoft.XMLHTTP"); + }); + + if (!xhr) { + throw new Error("This browser does not support XMLHttpRequest."); +} + + return xhr; +} : XMLHttpRequest; +/** + * @namespace + */ +jasmine.util = {}; + +/** + * Declare that a child class inherit it's prototype from the parent class. + * + * @private + * @param {Function} childClass + * @param {Function} parentClass + */ +jasmine.util.inherit = function(childClass, parentClass) { + /** + * @private + */ + var subclass = function() { + }; + subclass.prototype = parentClass.prototype; + childClass.prototype = new subclass(); +}; + +jasmine.util.formatException = function(e) { + var lineNumber; + if (e.line) { + lineNumber = e.line; + } + else if (e.lineNumber) { + lineNumber = e.lineNumber; + } + + var file; + + if (e.sourceURL) { + file = e.sourceURL; + } + else if (e.fileName) { + file = e.fileName; + } + + var message = (e.name && e.message) ? (e.name + ': ' + e.message) : e.toString(); + + if (file && lineNumber) { + message += ' in ' + file + ' (line ' + lineNumber + ')'; + } + + return message; +}; + +jasmine.util.htmlEscape = function(str) { + if (!str) { + return str; +} + return str.replace(/&/g, '&') + .replace(//g, '>'); +}; + +jasmine.util.argsToArray = function(args) { + var arrayOfArgs = []; + for (var i = 0; i < args.length; i++) {arrayOfArgs.push(args[i]);} + return arrayOfArgs; +}; + +jasmine.util.extend = function(destination, source) { + for (var property in source) destination[property] = source[property]; + return destination; +}; + +/** + * Environment for Jasmine + * + * @constructor + */ +jasmine.Env = function() { + this.currentSpec = null; + this.currentSuite = null; + this.currentRunner_ = new jasmine.Runner(this); + + this.reporter = new jasmine.MultiReporter(); + + this.updateInterval = jasmine.DEFAULT_UPDATE_INTERVAL; + this.defaultTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL; + this.lastUpdate = 0; + this.specFilter = function() { + return true; + }; + + this.nextSpecId_ = 0; + this.nextSuiteId_ = 0; + this.equalityTesters_ = []; + + // wrap matchers + this.matchersClass = function() { + jasmine.Matchers.apply(this, arguments); + }; + jasmine.util.inherit(this.matchersClass, jasmine.Matchers); + + jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass); +}; + +jasmine.Env.prototype.setTimeout = jasmine.setTimeout; +jasmine.Env.prototype.clearTimeout = jasmine.clearTimeout; +jasmine.Env.prototype.setInterval = jasmine.setInterval; +jasmine.Env.prototype.clearInterval = jasmine.clearInterval; + +/** + * @returns an object containing jasmine version build info, if set. + */ +jasmine.Env.prototype.version = function () { + if (jasmine.version_) { + return jasmine.version_; + } else { + throw new Error('Version not set'); + } +}; + +/** + * @returns string containing jasmine version build info, if set. + */ +jasmine.Env.prototype.versionString = function() { + if (!jasmine.version_) { + return "version unknown"; + } + + var version = this.version(); + var versionString = version.major + "." + version.minor + "." + version.build; + if (version.release_candidate) { + versionString += ".rc" + version.release_candidate; + } + versionString += " revision " + version.revision; + return versionString; +}; + +/** + * @returns a sequential integer starting at 0 + */ +jasmine.Env.prototype.nextSpecId = function () { + return this.nextSpecId_++; +}; + +/** + * @returns a sequential integer starting at 0 + */ +jasmine.Env.prototype.nextSuiteId = function () { + return this.nextSuiteId_++; +}; + +/** + * Register a reporter to receive status updates from Jasmine. + * @param {jasmine.Reporter} reporter An object which will receive status updates. + */ +jasmine.Env.prototype.addReporter = function(reporter) { + this.reporter.addReporter(reporter); +}; + +jasmine.Env.prototype.execute = function() { + this.currentRunner_.execute(); +}; + +jasmine.Env.prototype.describe = function(description, specDefinitions) { + var suite = new jasmine.Suite(this, description, specDefinitions, this.currentSuite); + + var parentSuite = this.currentSuite; + if (parentSuite) { + parentSuite.add(suite); + } else { + this.currentRunner_.add(suite); + } + + this.currentSuite = suite; + + var declarationError = null; + try { + specDefinitions.call(suite); + } catch(e) { + declarationError = e; + } + + if (declarationError) { + this.it("encountered a declaration exception", function() { + throw declarationError; + }); + } + + this.currentSuite = parentSuite; + + return suite; +}; + +jasmine.Env.prototype.beforeEach = function(beforeEachFunction) { + if (this.currentSuite) { + this.currentSuite.beforeEach(beforeEachFunction); + } else { + this.currentRunner_.beforeEach(beforeEachFunction); + } +}; + +jasmine.Env.prototype.currentRunner = function () { + return this.currentRunner_; +}; + +jasmine.Env.prototype.afterEach = function(afterEachFunction) { + if (this.currentSuite) { + this.currentSuite.afterEach(afterEachFunction); + } else { + this.currentRunner_.afterEach(afterEachFunction); + } + +}; + +jasmine.Env.prototype.xdescribe = function(desc, specDefinitions) { + return { + execute: function() { + } + }; +}; + +jasmine.Env.prototype.it = function(description, func) { + var spec = new jasmine.Spec(this, this.currentSuite, description); + this.currentSuite.add(spec); + this.currentSpec = spec; + + if (func) { + spec.runs(func); + } + + return spec; +}; + +jasmine.Env.prototype.xit = function(desc, func) { + return { + id: this.nextSpecId(), + runs: function() { + } + }; +}; + +jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) { + if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) { + return true; + } + + a.__Jasmine_been_here_before__ = b; + b.__Jasmine_been_here_before__ = a; + + var hasKey = function(obj, keyName) { + return obj !== null && obj[keyName] !== jasmine.undefined; + }; + + for (var property in b) { + if (!hasKey(a, property) && hasKey(b, property)) { + mismatchKeys.push("expected has key '" + property + "', but missing from actual."); + } + } + for (property in a) { + if (!hasKey(b, property) && hasKey(a, property)) { + mismatchKeys.push("expected missing key '" + property + "', but present in actual."); + } + } + for (property in b) { + if (property == '__Jasmine_been_here_before__') { + continue; + } + if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) { + mismatchValues.push("'" + property + "' was '" + (b[property] ? jasmine.util.htmlEscape(b[property].toString()) : b[property]) + "' in expected, but was '" + (a[property] ? jasmine.util.htmlEscape(a[property].toString()) : a[property]) + "' in actual."); + } + } + + if (jasmine.isArray_(a) && jasmine.isArray_(b) && a.length != b.length) { + mismatchValues.push("arrays were not the same length"); + } + + delete a.__Jasmine_been_here_before__; + delete b.__Jasmine_been_here_before__; + return (mismatchKeys.length === 0 && mismatchValues.length === 0); +}; + +jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) { + mismatchKeys = mismatchKeys || []; + mismatchValues = mismatchValues || []; + + for (var i = 0; i < this.equalityTesters_.length; i++) { + var equalityTester = this.equalityTesters_[i]; + var result = equalityTester(a, b, this, mismatchKeys, mismatchValues); + if (result !== jasmine.undefined) { + return result; + } + } + + if (a === b) { + return true; +} + + if (a === jasmine.undefined || a === null || b === jasmine.undefined || b === null) { + return (a == jasmine.undefined && b == jasmine.undefined); + } + + if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) { + return a === b; + } + + if (a instanceof Date && b instanceof Date) { + return a.getTime() == b.getTime(); + } + + if (a.jasmineMatches) { + return a.jasmineMatches(b); + } + + if (b.jasmineMatches) { + return b.jasmineMatches(a); + } + + if (a instanceof jasmine.Matchers.ObjectContaining) { + return a.matches(b); + } + + if (b instanceof jasmine.Matchers.ObjectContaining) { + return b.matches(a); + } + + if (jasmine.isString_(a) && jasmine.isString_(b)) { + return (a == b); + } + + if (jasmine.isNumber_(a) && jasmine.isNumber_(b)) { + return (a == b); + } + + if (typeof a === "object" && typeof b === "object") { + return this.compareObjects_(a, b, mismatchKeys, mismatchValues); + } + + //Straight check + return (a === b); +}; + +jasmine.Env.prototype.contains_ = function(haystack, needle) { + if (jasmine.isArray_(haystack)) { + for (var i = 0; i < haystack.length; i++) { + if (this.equals_(haystack[i], needle)) { + return true; + } + } + return false; + } + return haystack.indexOf(needle) >= 0; +}; + +jasmine.Env.prototype.addEqualityTester = function(equalityTester) { + this.equalityTesters_.push(equalityTester); +}; +/** No-op base class for Jasmine reporters. + * + * @constructor + */ +jasmine.Reporter = function() { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportRunnerStarting = function(runner) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportRunnerResults = function(runner) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportSuiteResults = function(suite) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportSpecStarting = function(spec) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportSpecResults = function(spec) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.log = function(str) { +}; + +/** + * Blocks are functions with executable code that make up a spec. + * + * @constructor + * @param {jasmine.Env} env + * @param {Function} func + * @param {jasmine.Spec} spec + */ +jasmine.Block = function(env, func, spec) { + this.env = env; + this.func = func; + this.spec = spec; +}; + +jasmine.Block.prototype.execute = function(onComplete) { + try { + this.func.apply(this.spec); + } catch (e) { + this.spec.fail(e); + } + onComplete(); +}; +/** JavaScript API reporter. + * + * @constructor + */ +jasmine.JsApiReporter = function() { + this.started = false; + this.finished = false; + this.suites_ = []; + this.results_ = {}; +}; + +jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) { + this.started = true; + var suites = runner.topLevelSuites(); + for (var i = 0; i < suites.length; i++) { + var suite = suites[i]; + this.suites_.push(this.summarize_(suite)); + } +}; + +jasmine.JsApiReporter.prototype.suites = function() { + return this.suites_; +}; + +jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) { + var isSuite = suiteOrSpec instanceof jasmine.Suite; + var summary = { + id: suiteOrSpec.id, + name: suiteOrSpec.description, + type: isSuite ? 'suite' : 'spec', + children: [] + }; + + if (isSuite) { + var children = suiteOrSpec.children(); + for (var i = 0; i < children.length; i++) { + summary.children.push(this.summarize_(children[i])); + } + } + return summary; +}; + +jasmine.JsApiReporter.prototype.results = function() { + return this.results_; +}; + +jasmine.JsApiReporter.prototype.resultsForSpec = function(specId) { + return this.results_[specId]; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) { + this.finished = true; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) { + this.results_[spec.id] = { + messages: spec.results().getItems(), + result: spec.results().failedCount > 0 ? "failed" : "passed" + }; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.log = function(str) { +}; + +jasmine.JsApiReporter.prototype.resultsForSpecs = function(specIds){ + var results = {}; + for (var i = 0; i < specIds.length; i++) { + var specId = specIds[i]; + results[specId] = this.summarizeResult_(this.results_[specId]); + } + return results; +}; + +jasmine.JsApiReporter.prototype.summarizeResult_ = function(result){ + var summaryMessages = []; + var messagesLength = result.messages.length; + for (var messageIndex = 0; messageIndex < messagesLength; messageIndex++) { + var resultMessage = result.messages[messageIndex]; + summaryMessages.push({ + text: resultMessage.type == 'log' ? resultMessage.toString() : jasmine.undefined, + passed: resultMessage.passed ? resultMessage.passed() : true, + type: resultMessage.type, + message: resultMessage.message, + trace: { + stack: resultMessage.passed && !resultMessage.passed() ? resultMessage.trace.stack : jasmine.undefined + } + }); + } + + return { + result : result.result, + messages : summaryMessages + }; +}; + +/** + * @constructor + * @param {jasmine.Env} env + * @param actual + * @param {jasmine.Spec} spec + */ +jasmine.Matchers = function(env, actual, spec, opt_isNot) { + this.env = env; + this.actual = actual; + this.spec = spec; + this.isNot = opt_isNot || false; + this.reportWasCalled_ = false; +}; + +// todo: @deprecated as of Jasmine 0.11, remove soon [xw] +jasmine.Matchers.pp = function(str) { + throw new Error("jasmine.Matchers.pp() is no longer supported, please use jasmine.pp() instead!"); +}; + +// todo: @deprecated Deprecated as of Jasmine 0.10. Rewrite your custom matchers to return true or false. [xw] +jasmine.Matchers.prototype.report = function(result, failing_message, details) { + throw new Error("As of jasmine 0.11, custom matchers must be implemented differently -- please see jasmine docs"); +}; + +jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) { + for (var methodName in prototype) { + if (methodName == 'report') { + continue; + } + var orig = prototype[methodName]; + matchersClass.prototype[methodName] = jasmine.Matchers.matcherFn_(methodName, orig); + } +}; + +jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) { + return function() { + var matcherArgs = jasmine.util.argsToArray(arguments); + var result = matcherFunction.apply(this, arguments); + + if (this.isNot) { + result = !result; + } + + if (this.reportWasCalled_) { + return result; + } + + var message; + if (!result) { + if (this.message) { + message = this.message.apply(this, arguments); + if (jasmine.isArray_(message)) { + message = message[this.isNot ? 1 : 0]; + } + } else { + var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); }); + message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate; + if (matcherArgs.length > 0) { + for (var i = 0; i < matcherArgs.length; i++) { + if (i > 0){message += ",";} + message += " " + jasmine.pp(matcherArgs[i]); + } + } + message += "."; + } + } + var expectationResult = new jasmine.ExpectationResult({ + matcherName: matcherName, + passed: result, + expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0], + actual: this.actual, + message: message + }); + this.spec.addMatcherResult(expectationResult); + return jasmine.undefined; + }; +}; + +/** + * toBe: compares the actual to the expected using === + * @param expected + */ +jasmine.Matchers.prototype.toBe = function(expected) { + return this.actual === expected; +}; + +/** + * toNotBe: compares the actual to the expected using !== + * @param expected + * @deprecated as of 1.0. Use not.toBe() instead. + */ +jasmine.Matchers.prototype.toNotBe = function(expected) { + return this.actual !== expected; +}; + +/** + * toEqual: compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc. + * + * @param expected + */ +jasmine.Matchers.prototype.toEqual = function(expected) { + return this.env.equals_(this.actual, expected); +}; + +/** + * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual + * @param expected + * @deprecated as of 1.0. Use not.toEqual() instead. + */ +jasmine.Matchers.prototype.toNotEqual = function(expected) { + return !this.env.equals_(this.actual, expected); +}; + +/** + * Matcher that compares the actual to the expected using a regular expression. Constructs a RegExp, so takes + * a pattern or a String. + * + * @param expected + */ +jasmine.Matchers.prototype.toMatch = function(expected) { + return new RegExp(expected).test(this.actual); +}; + +/** + * Matcher that compares the actual to the expected using the boolean inverse of jasmine.Matchers.toMatch + * @param expected + * @deprecated as of 1.0. Use not.toMatch() instead. + */ +jasmine.Matchers.prototype.toNotMatch = function(expected) { + return !(new RegExp(expected).test(this.actual)); +}; + +/** + * Matcher that compares the actual to jasmine.undefined. + */ +jasmine.Matchers.prototype.toBeDefined = function() { + return (this.actual !== jasmine.undefined); +}; + +/** + * Matcher that compares the actual to jasmine.undefined. + */ +jasmine.Matchers.prototype.toBeUndefined = function() { + return (this.actual === jasmine.undefined); +}; + +/** + * Matcher that compares the actual to null. + */ +jasmine.Matchers.prototype.toBeNull = function() { + return (this.actual === null); +}; + +/** + * Matcher that boolean not-nots the actual. + */ +jasmine.Matchers.prototype.toBeTruthy = function() { + return !!this.actual; +}; + +/** + * Matcher that boolean nots the actual. + */ +jasmine.Matchers.prototype.toBeFalsy = function() { + return !this.actual; +}; + +/** + * Matcher that checks to see if the actual, a Jasmine spy, was called. + */ +jasmine.Matchers.prototype.toHaveBeenCalled = function() { + if (arguments.length > 0) { + throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith'); + } + + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + + this.message = function() { + return [ + "Expected spy " + this.actual.identity + " to have been called.", + "Expected spy " + this.actual.identity + " not to have been called." + ]; + }; + + return this.actual.wasCalled; +}; + +/** @deprecated Use expect(xxx).toHaveBeenCalled() instead */ +jasmine.Matchers.prototype.wasCalled = jasmine.Matchers.prototype.toHaveBeenCalled; + +/** + * Matcher that checks to see if the actual, a Jasmine spy, was not called. + * + * @deprecated Use expect(xxx).not.toHaveBeenCalled() instead + */ +jasmine.Matchers.prototype.wasNotCalled = function() { + if (arguments.length > 0) { + throw new Error('wasNotCalled does not take arguments'); + } + + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + + this.message = function() { + return [ + "Expected spy " + this.actual.identity + " to not have been called.", + "Expected spy " + this.actual.identity + " to have been called." + ]; + }; + + return !this.actual.wasCalled; +}; + +/** + * Matcher that checks to see if the actual, a Jasmine spy, was called with a set of parameters. + * + * @example + * + */ +jasmine.Matchers.prototype.toHaveBeenCalledWith = function() { + var expectedArgs = jasmine.util.argsToArray(arguments); + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + this.message = function() { + if (this.actual.callCount === 0) { + // todo: what should the failure message for .not.toHaveBeenCalledWith() be? is this right? test better. [xw] + return [ + "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called.", + "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was." + ]; + } else { + return [ + "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall), + "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but was called with " + jasmine.pp(this.actual.argsForCall) + ]; + } + }; + + return this.env.contains_(this.actual.argsForCall, expectedArgs); +}; + +/** @deprecated Use expect(xxx).toHaveBeenCalledWith() instead */ +jasmine.Matchers.prototype.wasCalledWith = jasmine.Matchers.prototype.toHaveBeenCalledWith; + +/** @deprecated Use expect(xxx).not.toHaveBeenCalledWith() instead */ +jasmine.Matchers.prototype.wasNotCalledWith = function() { + var expectedArgs = jasmine.util.argsToArray(arguments); + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + + this.message = function() { + return [ + "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was", + "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was" + ]; + }; + + return !this.env.contains_(this.actual.argsForCall, expectedArgs); +}; + +/** + * Matcher that checks that the expected item is an element in the actual Array. + * + * @param {Object} expected + */ +jasmine.Matchers.prototype.toContain = function(expected) { + return this.env.contains_(this.actual, expected); +}; + +/** + * Matcher that checks that the expected item is NOT an element in the actual Array. + * + * @param {Object} expected + * @deprecated as of 1.0. Use not.toContain() instead. + */ +jasmine.Matchers.prototype.toNotContain = function(expected) { + return !this.env.contains_(this.actual, expected); +}; + +jasmine.Matchers.prototype.toBeLessThan = function(expected) { + return this.actual < expected; +}; + +jasmine.Matchers.prototype.toBeGreaterThan = function(expected) { + return this.actual > expected; +}; + +/** + * Matcher that checks that the expected item is equal to the actual item + * up to a given level of decimal precision (default 2). + * + * @param {Number} expected + * @param {Number} precision + */ +jasmine.Matchers.prototype.toBeCloseTo = function(expected, precision) { + if (!(precision === 0)) { + precision = precision || 2; + } + var multiplier = Math.pow(10, precision); + var actual = Math.round(this.actual * multiplier); + expected = Math.round(expected * multiplier); + return expected == actual; +}; + +/** + * Matcher that checks that the expected exception was thrown by the actual. + * + * @param {String} expected + */ +jasmine.Matchers.prototype.toThrow = function(expected) { + var result = false; + var exception; + if (typeof this.actual != 'function') { + throw new Error('Actual is not a function'); + } + try { + this.actual(); + } catch (e) { + exception = e; + } + if (exception) { + result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected)); + } + + var not = this.isNot ? "not " : ""; + + this.message = function() { + if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) { + return ["Expected function " + not + "to throw", expected ? expected.message || expected : "an exception", ", but it threw", exception.message || exception].join(' '); + } else { + return "Expected function to throw an exception."; + } + }; + + return result; +}; + +jasmine.Matchers.Any = function(expectedClass) { + this.expectedClass = expectedClass; +}; + +jasmine.Matchers.Any.prototype.jasmineMatches = function(other) { + if (this.expectedClass == String) { + return typeof other == 'string' || other instanceof String; + } + + if (this.expectedClass == Number) { + return typeof other == 'number' || other instanceof Number; + } + + if (this.expectedClass == Function) { + return typeof other == 'function' || other instanceof Function; + } + + if (this.expectedClass == Object) { + return typeof other == 'object'; + } + + return other instanceof this.expectedClass; +}; + +jasmine.Matchers.Any.prototype.jasmineToString = function() { + return ''; +}; + +jasmine.Matchers.ObjectContaining = function (sample) { + this.sample = sample; +}; + +jasmine.Matchers.ObjectContaining.prototype.jasmineMatches = function(other, mismatchKeys, mismatchValues) { + mismatchKeys = mismatchKeys || []; + mismatchValues = mismatchValues || []; + + var env = jasmine.getEnv(); + + var hasKey = function(obj, keyName) { + return obj != null && obj[keyName] !== jasmine.undefined; + }; + + for (var property in this.sample) { + if (!hasKey(other, property) && hasKey(this.sample, property)) { + mismatchKeys.push("expected has key '" + property + "', but missing from actual."); + } + else if (!env.equals_(this.sample[property], other[property], mismatchKeys, mismatchValues)) { + mismatchValues.push("'" + property + "' was '" + (other[property] ? jasmine.util.htmlEscape(other[property].toString()) : other[property]) + "' in expected, but was '" + (this.sample[property] ? jasmine.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + "' in actual."); + } + } + + return (mismatchKeys.length === 0 && mismatchValues.length === 0); +}; + +jasmine.Matchers.ObjectContaining.prototype.jasmineToString = function () { + return ""; +}; +// Mock setTimeout, clearTimeout +// Contributed by Pivotal Computer Systems, www.pivotalsf.com + +jasmine.FakeTimer = function() { + this.reset(); + + var self = this; + self.setTimeout = function(funcToCall, millis) { + self.timeoutsMade++; + self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false); + return self.timeoutsMade; + }; + + self.setInterval = function(funcToCall, millis) { + self.timeoutsMade++; + self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true); + return self.timeoutsMade; + }; + + self.clearTimeout = function(timeoutKey) { + self.scheduledFunctions[timeoutKey] = jasmine.undefined; + }; + + self.clearInterval = function(timeoutKey) { + self.scheduledFunctions[timeoutKey] = jasmine.undefined; + }; + +}; + +jasmine.FakeTimer.prototype.reset = function() { + this.timeoutsMade = 0; + this.scheduledFunctions = {}; + this.nowMillis = 0; +}; + +jasmine.FakeTimer.prototype.tick = function(millis) { + var oldMillis = this.nowMillis; + var newMillis = oldMillis + millis; + this.runFunctionsWithinRange(oldMillis, newMillis); + this.nowMillis = newMillis; +}; + +jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) { + var scheduledFunc; + var funcsToRun = []; + for (var timeoutKey in this.scheduledFunctions) { + scheduledFunc = this.scheduledFunctions[timeoutKey]; + if (scheduledFunc != jasmine.undefined && + scheduledFunc.runAtMillis >= oldMillis && + scheduledFunc.runAtMillis <= nowMillis) { + funcsToRun.push(scheduledFunc); + this.scheduledFunctions[timeoutKey] = jasmine.undefined; + } + } + + if (funcsToRun.length > 0) { + funcsToRun.sort(function(a, b) { + return a.runAtMillis - b.runAtMillis; + }); + for (var i = 0; i < funcsToRun.length; ++i) { + try { + var funcToRun = funcsToRun[i]; + this.nowMillis = funcToRun.runAtMillis; + funcToRun.funcToCall(); + if (funcToRun.recurring) { + this.scheduleFunction(funcToRun.timeoutKey, + funcToRun.funcToCall, + funcToRun.millis, + true); + } + } catch(e) { + } + } + this.runFunctionsWithinRange(oldMillis, nowMillis); + } +}; + +jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) { + this.scheduledFunctions[timeoutKey] = { + runAtMillis: this.nowMillis + millis, + funcToCall: funcToCall, + recurring: recurring, + timeoutKey: timeoutKey, + millis: millis + }; +}; + +/** + * @namespace + */ +jasmine.Clock = { + defaultFakeTimer: new jasmine.FakeTimer(), + + reset: function() { + jasmine.Clock.assertInstalled(); + jasmine.Clock.defaultFakeTimer.reset(); + }, + + tick: function(millis) { + jasmine.Clock.assertInstalled(); + jasmine.Clock.defaultFakeTimer.tick(millis); + }, + + runFunctionsWithinRange: function(oldMillis, nowMillis) { + jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis); + }, + + scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) { + jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring); + }, + + useMock: function() { + if (!jasmine.Clock.isInstalled()) { + var spec = jasmine.getEnv().currentSpec; + spec.after(jasmine.Clock.uninstallMock); + + jasmine.Clock.installMock(); + } + }, + + installMock: function() { + jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer; + }, + + uninstallMock: function() { + jasmine.Clock.assertInstalled(); + jasmine.Clock.installed = jasmine.Clock.real; + }, + + real: { + setTimeout: jasmine.getGlobal().setTimeout, + clearTimeout: jasmine.getGlobal().clearTimeout, + setInterval: jasmine.getGlobal().setInterval, + clearInterval: jasmine.getGlobal().clearInterval + }, + + assertInstalled: function() { + if (!jasmine.Clock.isInstalled()) { + throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()"); + } + }, + + isInstalled: function() { + return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer; + }, + + installed: null +}; +jasmine.Clock.installed = jasmine.Clock.real; + +//else for IE support +jasmine.getGlobal().setTimeout = function(funcToCall, millis) { + if (jasmine.Clock.installed.setTimeout.apply) { + return jasmine.Clock.installed.setTimeout.apply(this, arguments); + } else { + return jasmine.Clock.installed.setTimeout(funcToCall, millis); + } +}; + +jasmine.getGlobal().setInterval = function(funcToCall, millis) { + if (jasmine.Clock.installed.setInterval.apply) { + return jasmine.Clock.installed.setInterval.apply(this, arguments); + } else { + return jasmine.Clock.installed.setInterval(funcToCall, millis); + } +}; + +jasmine.getGlobal().clearTimeout = function(timeoutKey) { + if (jasmine.Clock.installed.clearTimeout.apply) { + return jasmine.Clock.installed.clearTimeout.apply(this, arguments); + } else { + return jasmine.Clock.installed.clearTimeout(timeoutKey); + } +}; + +jasmine.getGlobal().clearInterval = function(timeoutKey) { + if (jasmine.Clock.installed.clearTimeout.apply) { + return jasmine.Clock.installed.clearInterval.apply(this, arguments); + } else { + return jasmine.Clock.installed.clearInterval(timeoutKey); + } +}; + +/** + * @constructor + */ +jasmine.MultiReporter = function() { + this.subReporters_ = []; +}; +jasmine.util.inherit(jasmine.MultiReporter, jasmine.Reporter); + +jasmine.MultiReporter.prototype.addReporter = function(reporter) { + this.subReporters_.push(reporter); +}; + +(function() { + var functionNames = [ + "reportRunnerStarting", + "reportRunnerResults", + "reportSuiteResults", + "reportSpecStarting", + "reportSpecResults", + "log" + ]; + for (var i = 0; i < functionNames.length; i++) { + var functionName = functionNames[i]; + jasmine.MultiReporter.prototype[functionName] = (function(functionName) { + return function() { + for (var j = 0; j < this.subReporters_.length; j++) { + var subReporter = this.subReporters_[j]; + if (subReporter[functionName]) { + subReporter[functionName].apply(subReporter, arguments); + } + } + }; + })(functionName); + } +})(); +/** + * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults + * + * @constructor + */ +jasmine.NestedResults = function() { + /** + * The total count of results + */ + this.totalCount = 0; + /** + * Number of passed results + */ + this.passedCount = 0; + /** + * Number of failed results + */ + this.failedCount = 0; + /** + * Was this suite/spec skipped? + */ + this.skipped = false; + /** + * @ignore + */ + this.items_ = []; +}; + +/** + * Roll up the result counts. + * + * @param result + */ +jasmine.NestedResults.prototype.rollupCounts = function(result) { + this.totalCount += result.totalCount; + this.passedCount += result.passedCount; + this.failedCount += result.failedCount; +}; + +/** + * Adds a log message. + * @param values Array of message parts which will be concatenated later. + */ +jasmine.NestedResults.prototype.log = function(values) { + this.items_.push(new jasmine.MessageResult(values)); +}; + +/** + * Getter for the results: message & results. + */ +jasmine.NestedResults.prototype.getItems = function() { + return this.items_; +}; + +/** + * Adds a result, tracking counts (total, passed, & failed) + * @param {jasmine.ExpectationResult|jasmine.NestedResults} result + */ +jasmine.NestedResults.prototype.addResult = function(result) { + if (result.type != 'log') { + if (result.items_) { + this.rollupCounts(result); + } else { + this.totalCount++; + if (result.passed()) { + this.passedCount++; + } else { + this.failedCount++; + } + } + } + this.items_.push(result); +}; + +/** + * @returns {Boolean} True if everything below passed + */ +jasmine.NestedResults.prototype.passed = function() { + return this.passedCount === this.totalCount; +}; +/** + * Base class for pretty printing for expectation results. + */ +jasmine.PrettyPrinter = function() { + this.ppNestLevel_ = 0; +}; + +/** + * Formats a value in a nice, human-readable string. + * + * @param value + */ +jasmine.PrettyPrinter.prototype.format = function(value) { + if (this.ppNestLevel_ > 40) { + throw new Error('jasmine.PrettyPrinter: format() nested too deeply!'); + } + + this.ppNestLevel_++; + try { + if (value === jasmine.undefined) { + this.emitScalar('undefined'); + } else if (value === null) { + this.emitScalar('null'); + } else if (value === jasmine.getGlobal()) { + this.emitScalar(''); + } else if (value.jasmineToString) { + this.emitScalar(value.jasmineToString()); + } else if (typeof value === 'string') { + this.emitString(value); + } else if (jasmine.isSpy(value)) { + this.emitScalar("spy on " + value.identity); + } else if (value instanceof RegExp) { + this.emitScalar(value.toString()); + } else if (typeof value === 'function') { + this.emitScalar('Function'); + } else if (typeof value.nodeType === 'number') { + this.emitScalar('HTMLNode'); + } else if (value instanceof Date) { + this.emitScalar('Date(' + value + ')'); + } else if (value.__Jasmine_been_here_before__) { + this.emitScalar(''); + } else if (jasmine.isArray_(value) || typeof value == 'object') { + value.__Jasmine_been_here_before__ = true; + if (jasmine.isArray_(value)) { + this.emitArray(value); + } else { + this.emitObject(value); + } + delete value.__Jasmine_been_here_before__; + } else { + this.emitScalar(value.toString()); + } + } finally { + this.ppNestLevel_--; + } +}; + +jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) { + for (var property in obj) { + if (property == '__Jasmine_been_here_before__') { + continue; + } + fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) !== jasmine.undefined && + obj.__lookupGetter__(property) !== null) : false); + } +}; + +jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_; +jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_; +jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_; +jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_; + +jasmine.StringPrettyPrinter = function() { + jasmine.PrettyPrinter.call(this); + + this.string = ''; +}; +jasmine.util.inherit(jasmine.StringPrettyPrinter, jasmine.PrettyPrinter); + +jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) { + this.append(value); +}; + +jasmine.StringPrettyPrinter.prototype.emitString = function(value) { + this.append("'" + value + "'"); +}; + +jasmine.StringPrettyPrinter.prototype.emitArray = function(array) { + this.append('[ '); + for (var i = 0; i < array.length; i++) { + if (i > 0) { + this.append(', '); + } + this.format(array[i]); + } + this.append(' ]'); +}; + +jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) { + var self = this; + this.append('{ '); + var first = true; + + this.iterateObject(obj, function(property, isGetter) { + if (first) { + first = false; + } else { + self.append(', '); + } + + self.append(property); + self.append(' : '); + if (isGetter) { + self.append(''); + } else { + self.format(obj[property]); + } + }); + + this.append(' }'); +}; + +jasmine.StringPrettyPrinter.prototype.append = function(value) { + this.string += value; +}; +jasmine.Queue = function(env) { + this.env = env; + this.blocks = []; + this.running = false; + this.index = 0; + this.offset = 0; + this.abort = false; +}; + +jasmine.Queue.prototype.addBefore = function(block) { + this.blocks.unshift(block); +}; + +jasmine.Queue.prototype.add = function(block) { + this.blocks.push(block); +}; + +jasmine.Queue.prototype.insertNext = function(block) { + this.blocks.splice((this.index + this.offset + 1), 0, block); + this.offset++; +}; + +jasmine.Queue.prototype.start = function(onComplete) { + this.running = true; + this.onComplete = onComplete; + this.next_(); +}; + +jasmine.Queue.prototype.isRunning = function() { + return this.running; +}; + +jasmine.Queue.LOOP_DONT_RECURSE = true; + +jasmine.Queue.prototype.next_ = function() { + var self = this; + var goAgain = true; + + while (goAgain) { + goAgain = false; + if (self.index < self.blocks.length && !this.abort) { + var calledSynchronously = true; + var completedSynchronously = false; + + var onComplete = function () { + if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) { + completedSynchronously = true; + return; + } + + if (self.blocks[self.index].abort) { + self.abort = true; + } + + self.offset = 0; + self.index++; + + var now = new Date().getTime(); + if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) { + self.env.lastUpdate = now; + self.env.setTimeout(function() { + self.next_(); + }, 0); + } else { + if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) { + goAgain = true; + } else { + self.next_(); + } + } + }; + self.blocks[self.index].execute(onComplete); + + calledSynchronously = false; + if (completedSynchronously) { + onComplete(); + } + + } else { + self.running = false; + if (self.onComplete) { + self.onComplete(); + } + } + } +}; + +jasmine.Queue.prototype.results = function() { + var results = new jasmine.NestedResults(); + for (var i = 0; i < this.blocks.length; i++) { + if (this.blocks[i].results) { + results.addResult(this.blocks[i].results()); + } + } + return results; +}; + +/** + * Runner + * + * @constructor + * @param {jasmine.Env} env + */ +jasmine.Runner = function(env) { + var self = this; + self.env = env; + self.queue = new jasmine.Queue(env); + self.before_ = []; + self.after_ = []; + self.suites_ = []; +}; + +jasmine.Runner.prototype.execute = function() { + var self = this; + if (self.env.reporter.reportRunnerStarting) { + self.env.reporter.reportRunnerStarting(this); + } + self.queue.start(function () { + self.finishCallback(); + }); +}; + +jasmine.Runner.prototype.beforeEach = function(beforeEachFunction) { + beforeEachFunction.typeName = 'beforeEach'; + this.before_.splice(0,0,beforeEachFunction); +}; + +jasmine.Runner.prototype.afterEach = function(afterEachFunction) { + afterEachFunction.typeName = 'afterEach'; + this.after_.splice(0,0,afterEachFunction); +}; + +jasmine.Runner.prototype.finishCallback = function() { + this.env.reporter.reportRunnerResults(this); +}; + +jasmine.Runner.prototype.addSuite = function(suite) { + this.suites_.push(suite); +}; + +jasmine.Runner.prototype.add = function(block) { + if (block instanceof jasmine.Suite) { + this.addSuite(block); + } + this.queue.add(block); +}; + +jasmine.Runner.prototype.specs = function () { + var suites = this.suites(); + var specs = []; + for (var i = 0; i < suites.length; i++) { + specs = specs.concat(suites[i].specs()); + } + return specs; +}; + +jasmine.Runner.prototype.suites = function() { + return this.suites_; +}; + +jasmine.Runner.prototype.topLevelSuites = function() { + var topLevelSuites = []; + for (var i = 0; i < this.suites_.length; i++) { + if (!this.suites_[i].parentSuite) { + topLevelSuites.push(this.suites_[i]); + } + } + return topLevelSuites; +}; + +jasmine.Runner.prototype.results = function() { + return this.queue.results(); +}; +/** + * Internal representation of a Jasmine specification, or test. + * + * @constructor + * @param {jasmine.Env} env + * @param {jasmine.Suite} suite + * @param {String} description + */ +jasmine.Spec = function(env, suite, description) { + if (!env) { + throw new Error('jasmine.Env() required'); + } + if (!suite) { + throw new Error('jasmine.Suite() required'); + } + var spec = this; + spec.id = env.nextSpecId ? env.nextSpecId() : null; + spec.env = env; + spec.suite = suite; + spec.description = description; + spec.queue = new jasmine.Queue(env); + + spec.afterCallbacks = []; + spec.spies_ = []; + + spec.results_ = new jasmine.NestedResults(); + spec.results_.description = description; + spec.matchersClass = null; +}; + +jasmine.Spec.prototype.getFullName = function() { + return this.suite.getFullName() + ' ' + this.description + '.'; +}; + +jasmine.Spec.prototype.results = function() { + return this.results_; +}; + +/** + * All parameters are pretty-printed and concatenated together, then written to the spec's output. + * + * Be careful not to leave calls to jasmine.log in production code. + */ +jasmine.Spec.prototype.log = function() { + return this.results_.log(arguments); +}; + +jasmine.Spec.prototype.runs = function (func) { + var block = new jasmine.Block(this.env, func, this); + this.addToQueue(block); + return this; +}; + +jasmine.Spec.prototype.addToQueue = function (block) { + if (this.queue.isRunning()) { + this.queue.insertNext(block); + } else { + this.queue.add(block); + } +}; + +/** + * @param {jasmine.ExpectationResult} result + */ +jasmine.Spec.prototype.addMatcherResult = function(result) { + this.results_.addResult(result); +}; + +jasmine.Spec.prototype.expect = function(actual) { + var positive = new (this.getMatchersClass_())(this.env, actual, this); + positive.not = new (this.getMatchersClass_())(this.env, actual, this, true); + return positive; +}; + +/** + * Waits a fixed time period before moving to the next block. + * + * @deprecated Use waitsFor() instead + * @param {Number} timeout milliseconds to wait + */ +jasmine.Spec.prototype.waits = function(timeout) { + var waitsFunc = new jasmine.WaitsBlock(this.env, timeout, this); + this.addToQueue(waitsFunc); + return this; +}; + +/** + * Waits for the latchFunction to return true before proceeding to the next block. + * + * @param {Function} latchFunction + * @param {String} optional_timeoutMessage + * @param {Number} optional_timeout + */ +jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { + var latchFunction_ = null; + var optional_timeoutMessage_ = null; + var optional_timeout_ = null; + + for (var i = 0; i < arguments.length; i++) { + var arg = arguments[i]; + switch (typeof arg) { + case 'function': + latchFunction_ = arg; + break; + case 'string': + optional_timeoutMessage_ = arg; + break; + case 'number': + optional_timeout_ = arg; + break; + } + } + + var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this); + this.addToQueue(waitsForFunc); + return this; +}; + +jasmine.Spec.prototype.fail = function (e) { + var expectationResult = new jasmine.ExpectationResult({ + passed: false, + message: e ? jasmine.util.formatException(e) : 'Exception', + trace: { stack: e.stack } + }); + this.results_.addResult(expectationResult); +}; + +jasmine.Spec.prototype.getMatchersClass_ = function() { + return this.matchersClass || this.env.matchersClass; +}; + +jasmine.Spec.prototype.addMatchers = function(matchersPrototype) { + var parent = this.getMatchersClass_(); + var newMatchersClass = function() { + parent.apply(this, arguments); + }; + jasmine.util.inherit(newMatchersClass, parent); + jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass); + this.matchersClass = newMatchersClass; +}; + +jasmine.Spec.prototype.finishCallback = function() { + this.env.reporter.reportSpecResults(this); +}; + +jasmine.Spec.prototype.finish = function(onComplete) { + this.removeAllSpies(); + this.finishCallback(); + if (onComplete) { + onComplete(); + } +}; + +jasmine.Spec.prototype.after = function(doAfter) { + if (this.queue.isRunning()) { + this.queue.add(new jasmine.Block(this.env, doAfter, this)); + } else { + this.afterCallbacks.unshift(doAfter); + } +}; + +jasmine.Spec.prototype.execute = function(onComplete) { + var spec = this; + if (!spec.env.specFilter(spec)) { + spec.results_.skipped = true; + spec.finish(onComplete); + return; + } + + this.env.reporter.reportSpecStarting(this); + + spec.env.currentSpec = spec; + + spec.addBeforesAndAftersToQueue(); + + spec.queue.start(function () { + spec.finish(onComplete); + }); +}; + +jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() { + var runner = this.env.currentRunner(); + var i; + + for (var suite = this.suite; suite; suite = suite.parentSuite) { + for (i = 0; i < suite.before_.length; i++) { + this.queue.addBefore(new jasmine.Block(this.env, suite.before_[i], this)); + } + } + for (i = 0; i < runner.before_.length; i++) { + this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this)); + } + for (i = 0; i < this.afterCallbacks.length; i++) { + this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this)); + } + for (suite = this.suite; suite; suite = suite.parentSuite) { + for (i = 0; i < suite.after_.length; i++) { + this.queue.add(new jasmine.Block(this.env, suite.after_[i], this)); + } + } + for (i = 0; i < runner.after_.length; i++) { + this.queue.add(new jasmine.Block(this.env, runner.after_[i], this)); + } +}; + +jasmine.Spec.prototype.explodes = function() { + throw 'explodes function should not have been called'; +}; + +jasmine.Spec.prototype.spyOn = function(obj, methodName, ignoreMethodDoesntExist) { + if (obj == jasmine.undefined) { + throw "spyOn could not find an object to spy upon for " + methodName + "()"; + } + + if (!ignoreMethodDoesntExist && obj[methodName] === jasmine.undefined) { + throw methodName + '() method does not exist'; + } + + if (!ignoreMethodDoesntExist && obj[methodName] && obj[methodName].isSpy) { + throw new Error(methodName + ' has already been spied upon'); + } + + var spyObj = jasmine.createSpy(methodName); + + this.spies_.push(spyObj); + spyObj.baseObj = obj; + spyObj.methodName = methodName; + spyObj.originalValue = obj[methodName]; + + obj[methodName] = spyObj; + + return spyObj; +}; + +jasmine.Spec.prototype.removeAllSpies = function() { + for (var i = 0; i < this.spies_.length; i++) { + var spy = this.spies_[i]; + spy.baseObj[spy.methodName] = spy.originalValue; + } + this.spies_ = []; +}; + +/** + * Internal representation of a Jasmine suite. + * + * @constructor + * @param {jasmine.Env} env + * @param {String} description + * @param {Function} specDefinitions + * @param {jasmine.Suite} parentSuite + */ +jasmine.Suite = function(env, description, specDefinitions, parentSuite) { + var self = this; + self.id = env.nextSuiteId ? env.nextSuiteId() : null; + self.description = description; + self.queue = new jasmine.Queue(env); + self.parentSuite = parentSuite; + self.env = env; + self.before_ = []; + self.after_ = []; + self.children_ = []; + self.suites_ = []; + self.specs_ = []; +}; + +jasmine.Suite.prototype.getFullName = function() { + var fullName = this.description; + for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) { + fullName = parentSuite.description + ' ' + fullName; + } + return fullName; +}; + +jasmine.Suite.prototype.finish = function(onComplete) { + this.env.reporter.reportSuiteResults(this); + this.finished = true; + if (typeof(onComplete) == 'function') { + onComplete(); + } +}; + +jasmine.Suite.prototype.beforeEach = function(beforeEachFunction) { + beforeEachFunction.typeName = 'beforeEach'; + this.before_.unshift(beforeEachFunction); +}; + +jasmine.Suite.prototype.afterEach = function(afterEachFunction) { + afterEachFunction.typeName = 'afterEach'; + this.after_.unshift(afterEachFunction); +}; + +jasmine.Suite.prototype.results = function() { + return this.queue.results(); +}; + +jasmine.Suite.prototype.add = function(suiteOrSpec) { + this.children_.push(suiteOrSpec); + if (suiteOrSpec instanceof jasmine.Suite) { + this.suites_.push(suiteOrSpec); + this.env.currentRunner().addSuite(suiteOrSpec); + } else { + this.specs_.push(suiteOrSpec); + } + this.queue.add(suiteOrSpec); +}; + +jasmine.Suite.prototype.specs = function() { + return this.specs_; +}; + +jasmine.Suite.prototype.suites = function() { + return this.suites_; +}; + +jasmine.Suite.prototype.children = function() { + return this.children_; +}; + +jasmine.Suite.prototype.execute = function(onComplete) { + var self = this; + this.queue.start(function () { + self.finish(onComplete); + }); +}; +jasmine.WaitsBlock = function(env, timeout, spec) { + this.timeout = timeout; + jasmine.Block.call(this, env, null, spec); +}; + +jasmine.util.inherit(jasmine.WaitsBlock, jasmine.Block); + +jasmine.WaitsBlock.prototype.execute = function (onComplete) { + if (jasmine.VERBOSE) { + this.env.reporter.log('>> Jasmine waiting for ' + this.timeout + ' ms...'); + } + this.env.setTimeout(function () { + onComplete(); + }, this.timeout); +}; +/** + * A block which waits for some condition to become true, with timeout. + * + * @constructor + * @extends jasmine.Block + * @param {jasmine.Env} env The Jasmine environment. + * @param {Number} timeout The maximum time in milliseconds to wait for the condition to become true. + * @param {Function} latchFunction A function which returns true when the desired condition has been met. + * @param {String} message The message to display if the desired condition hasn't been met within the given time period. + * @param {jasmine.Spec} spec The Jasmine spec. + */ +jasmine.WaitsForBlock = function(env, timeout, latchFunction, message, spec) { + this.timeout = timeout || env.defaultTimeoutInterval; + this.latchFunction = latchFunction; + this.message = message; + this.totalTimeSpentWaitingForLatch = 0; + jasmine.Block.call(this, env, null, spec); +}; +jasmine.util.inherit(jasmine.WaitsForBlock, jasmine.Block); + +jasmine.WaitsForBlock.TIMEOUT_INCREMENT = 10; + +jasmine.WaitsForBlock.prototype.execute = function(onComplete) { + if (jasmine.VERBOSE) { + this.env.reporter.log('>> Jasmine waiting for ' + (this.message || 'something to happen')); + } + var latchFunctionResult; + try { + latchFunctionResult = this.latchFunction.apply(this.spec); + } catch (e) { + this.spec.fail(e); + onComplete(); + return; + } + + if (latchFunctionResult) { + onComplete(); + } else if (this.totalTimeSpentWaitingForLatch >= this.timeout) { + var message = 'timed out after ' + this.timeout + ' msec waiting for ' + (this.message || 'something to happen'); + this.spec.fail({ + name: 'timeout', + message: message + }); + + this.abort = true; + onComplete(); + } else { + this.totalTimeSpentWaitingForLatch += jasmine.WaitsForBlock.TIMEOUT_INCREMENT; + var self = this; + this.env.setTimeout(function() { + self.execute(onComplete); + }, jasmine.WaitsForBlock.TIMEOUT_INCREMENT); + } +}; + +jasmine.version_= { + "major": 1, + "minor": 2, + "build": 0, + "revision": 1333557965, + "release_candidate": 3 +}; diff --git a/third_party/jsdoc/test/reporter.js b/third_party/jsdoc/test/reporter.js new file mode 100644 index 0000000000..b4d0ca0648 --- /dev/null +++ b/third_party/jsdoc/test/reporter.js @@ -0,0 +1,292 @@ +module.exports = function(jasmine) { + var util = require('util'); + + var jasmineNode = {}; + + // + // Helpers + // + function noop() { + } + + jasmineNode.ANSIColors = { + pass : function() { + return '\033[32m'; + }, // Green + fail : function() { + return '\033[31m'; + }, // Red + neutral : function() { + return '\033[0m'; + } // Normal + }; + + jasmineNode.NoColors = { + pass : function() { + return ''; + }, + fail : function() { + return ''; + }, + neutral : function() { + return ''; + } + }; + + jasmineNode.TerminalReporter = function(config) { + this.print_ = config.print || function (str) { process.stdout.write(util.format(str)); }; + this.color_ = config.color ? jasmineNode.ANSIColors : jasmineNode.NoColors; + + this.started_ = false; + this.finished_ = false; + + this.callback_ = config.onComplete || false; + + this.suites_ = []; + this.specResults_ = {}; + this.failures_ = {}; + this.failures_.length = 0; + }; + + jasmineNode.TerminalReporter.prototype = { + reportRunnerStarting : function(runner) { + this.started_ = true; + this.startedAt = new Date(); + var suites = runner.topLevelSuites(); + for ( var i = 0; i < suites.length; i++) { + var suite = suites[i]; + this.suites_.push(this.summarize_(suite)); + } + }, + + summarize_ : function(suiteOrSpec) { + var isSuite = suiteOrSpec instanceof jasmine.Suite; + + // We could use a separate object for suite and spec + var summary = { + id : suiteOrSpec.id, + name : suiteOrSpec.description, + type : isSuite ? 'suite' : 'spec', + suiteNestingLevel : 0, + children : [] + }; + + if (isSuite) { + var calculateNestingLevel = function(examinedSuite) { + var nestingLevel = 0; + while (examinedSuite.parentSuite !== null) { + nestingLevel += 1; + examinedSuite = examinedSuite.parentSuite; + } + return nestingLevel; + }; + + summary.suiteNestingLevel = calculateNestingLevel(suiteOrSpec); + + var children = suiteOrSpec.children(); + for ( var i = 0; i < children.length; i++) { + summary.children.push(this.summarize_(children[i])); + } + } + + return summary; + }, + + // This is heavily influenced by Jasmine's Html/Trivial Reporter + reportRunnerResults : function(runner) { + this.reportFailures_(); + + var results = runner.results(); + var resultColor = (results.failedCount > 0) ? this.color_.fail() : this.color_.pass(); + + var specs = runner.specs(); + var specCount = specs.length; + + var message = "\n\nFinished in " + + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + + " seconds"; + this.printLine_(message); + + // This is what jasmine-html.js has + // message = "" + specCount + " spec" + ( specCount === 1 ? "" : "s") + ", " + results.failedCount + " failure" + ((results.failedCount === 1) ? "" : "s"); + + this.printLine_(this.stringWithColor_(this.printRunnerResults_(runner), resultColor)); + + this.finished_ = true; + if (this.callback_) { + this.callback_(runner); + } + }, + + reportFailures_ : function() { + if (this.failures_.length === 0) { + return; + } + + var indent = ' ', failure, failures; + this.printLine_('\n'); + + this.print_('Failures:'); + + for ( var suite in this.failures_) { + if (this.failures_.hasOwnProperty(suite) && suite !== "length") { + this.printLine_('\n'); + this.printLine_(suite); + failures = this.failures_[suite]; + for ( var i = 0; i < failures.length; i++) { + failure = failures[i]; + this.printLine_('\n'); + this.printLine_(indent + (i + 1) + ') ' + failure.spec); + this.printLine_(indent + 'Message:'); + this.printLine_(indent + indent + this.stringWithColor_(failure.message, this.color_.fail())); + this.printLine_(indent + 'Stacktrace:'); + this.print_(indent + indent + failure.stackTrace); + } + } + } + + }, + + reportSuiteResults : function(suite) { + // Not used in this context + }, + + reportSpecResults : function(spec) { + var result = spec.results(); + var msg = ''; + if (result.skipped) { + msg = this.stringWithColor_('-', this.color_.ignore()); + } else if (result.passed()) { + msg = this.stringWithColor_('.', this.color_.pass()); + } else { + msg = this.stringWithColor_('F', this.color_.fail()); + this.addFailureToFailures_(spec); + } + this.spec_results += msg; + this.print_(msg); + }, + + addFailureToFailures_ : function(spec) { + var result = spec.results(); + var failureItem = null; + var suite = spec.suite.getFullName(); + var failures = null; + var items_length = result.items_.length; + for ( var i = 0; i < items_length; i++) { + if (result.items_[i].passed_ === false) { + failureItem = result.items_[i]; + var failure = { + spec : spec.description, + message : failureItem.message, + stackTrace : failureItem.trace.stack + }; + failures = this.failures_[suite]; + if (!failures) { + this.failures_[suite] = []; + } + this.failures_[suite].push(failure); + this.failures_.length++; + } + } + }, + + printRunnerResults_ : function(runner) { + var results = runner.results(); + var specs = runner.specs(); + var msg = ''; + msg += specs.length + ' test' + ((specs.length === 1) ? '' : 's') + ', '; + msg += results.totalCount + ' assertion' + ((results.totalCount === 1) ? '' : 's') + ', '; + msg += results.failedCount + ' failure' + ((results.failedCount === 1) ? '' : 's') + '\n'; + return msg; + }, + + // Helper Methods // + stringWithColor_ : function(stringValue, color) { + return (color || this.color_.neutral()) + stringValue + this.color_.neutral(); + }, + + printLine_ : function(stringValue) { + this.print_(stringValue); + this.print_('\n'); + } + }; + + // *************************************************************** + // TerminalVerboseReporter uses the TerminalReporter's constructor + // *************************************************************** + jasmineNode.TerminalVerboseReporter = function(config) { + jasmineNode.TerminalReporter.call(this, config); + // The extra field in this object + this.indent_ = 0; + }; + + jasmineNode.TerminalVerboseReporter.prototype = { + reportSpecResults : function(spec) { + if (spec.results().failedCount > 0) { + this.addFailureToFailures_(spec); + } + + this.specResults_[spec.id] = { + messages : spec.results().getItems(), + result : spec.results().failedCount > 0 ? 'failed' : 'passed' + }; + }, + + reportRunnerResults : function(runner) { + var messages = new Array(); + this.buildMessagesFromResults_(messages, this.suites_); + + var messages_length = messages.length; + for ( var i = 0; i < messages_length - 1; i++) { + this.printLine_(messages[i]); + } + + this.print_(messages[messages_length - 1]); + + // Call the parent object's method + jasmineNode.TerminalReporter.prototype.reportRunnerResults.call(this, runner); + }, + + buildMessagesFromResults_ : function(messages, results) { + var element, specResult, specIndentSpaces, msg = ''; + + var results_length = results.length; + for ( var i = 0; i < results_length; i++) { + element = results[i]; + + if (element.type === 'spec') { + specResult = this.specResults_[element.id.toString()]; + + specIndentSpaces = this.indent_ + 2; + if (specResult.result === 'passed') { + msg = this.stringWithColor_(this.indentMessage_(element.name, specIndentSpaces), this.color_.pass()); + } else { + msg = this.stringWithColor_(this.indentMessage_(element.name, specIndentSpaces), this.color_.fail()); + } + + messages.push(msg); + } else { + this.indent_ = element.suiteNestingLevel * 2; + + messages.push(''); + messages.push(this.indentMessage_(element.name,this.indent_)); + } + + this.buildMessagesFromResults_(messages, element.children); + } + }, + + indentMessage_ : function(message, indentCount) { + var _indent = ''; + for ( var i = 0; i < indentCount; i++) { + _indent += ' '; + } + return (_indent + message); + } + }; + + // Inherit from TerminalReporter + jasmineNode.TerminalVerboseReporter.prototype.__proto__ = jasmineNode.TerminalReporter.prototype; + + return jasmineNode; +}; \ No newline at end of file diff --git a/third_party/jsdoc/test/runner.js b/third_party/jsdoc/test/runner.js new file mode 100644 index 0000000000..bd65b47dd3 --- /dev/null +++ b/third_party/jsdoc/test/runner.js @@ -0,0 +1,65 @@ +/*global env: true, jasmine: true */ +/* + * Test Steps: + * 1. Get Jasmine + * 2. Get the test options + * 3. Get the list of directories to run tests from + * 4. Run Jasmine on each directory + */ +var fs = require('jsdoc/fs'); +var logger = require('jsdoc/util/logger'); +var path = require('path'); + +fs.existsSync = fs.existsSync || path.existsSync; + +require( path.join(env.dirname, 'test/jasmine-jsdoc') ); + +var hasOwnProp = Object.prototype.hasOwnProperty; + +var opts = { + verbose: env.opts.verbose || false, + showColors: env.opts.nocolor === true ? false : true +}; + +var extensions = 'js'; +var match = env.opts.match || '.'; +if (match instanceof Array) { + match = match.join("|"); +} +opts.matcher = new RegExp("(" + match + ")\\.(" + extensions + ")$", 'i'); + +var specFolders = [ + path.join(env.dirname, 'test/specs'), + path.join(env.dirname, 'plugins/test/specs') +]; + +var failedCount = 0; +var index = 0; + +var testsCompleteCallback; +var onComplete; + +var runNextFolder = module.exports = function(callback) { + testsCompleteCallback = testsCompleteCallback || callback; + + // silence the logger while we run the tests + logger.setLevel(logger.LEVELS.SILENT); + + if (index < specFolders.length) { + jasmine.executeSpecsInFolder(specFolders[index], onComplete, opts); + } + else { + process.nextTick(function() { + testsCompleteCallback(failedCount); + }); + } +}; + +onComplete = function(runner, log) { + if (runner.results().failedCount !== 0) { + failedCount += runner.results().failedCount; + } + + index++; + runNextFolder(); +}; diff --git a/third_party/jsdoc/test/spec-collection.js b/third_party/jsdoc/test/spec-collection.js new file mode 100644 index 0000000000..493d909029 --- /dev/null +++ b/third_party/jsdoc/test/spec-collection.js @@ -0,0 +1,93 @@ +/*global env: true */ +var fs = require('jsdoc/fs'); +var path = require('jsdoc/path'); +var runtime = require('jsdoc/util/runtime'); +var wrench = require('wrench'); + +var specs = []; +var finalSpecs = []; + +var createSpecObj = function(_path, root) { + function relativePath() { + return _path.replace(root, '').replace(/^[\/\\]/, '').replace(/\\/g, '/'); + } + + return { + path: function() { + return _path; + }, + relativePath: relativePath, + directory: function() { + return _path.replace(/[\/\\][\s\w\.\-]*$/, "").replace(/\\/g, '/'); + }, + relativeDirectory: function() { + return relativePath().replace(/[\/\\][\s\w\.\-]*$/, "").replace(/\\/g, '/'); + }, + filename: function() { + return _path.replace(/^.*[\\\/]/, ''); + } + }; +}; + +var clearSpecs = exports.clearSpecs = function() { + specs.splice(0, specs.length); +}; + +function addSpec(file, target) { + target = target || specs; + + target.push( createSpecObj(file) ); +} + +function isValidSpec(file, matcher) { + var result; + + var skipPath = runtime.isRhino() ? runtime.NODE : runtime.RHINO; + + // valid specs must... + try { + // ...be a file + result = fs.statSync(file).isFile() && + // ...match the matcher + matcher.test( path.basename(file) ) && + // ...be relevant to the current runtime + file.indexOf(skipPath) === -1; + } + catch(e) { + result = false; + } + + return result; +} + +function shouldLoad(file, matcher) { + var result = false; + + // should this spec run at the end? + if ( /schema\.js$/.test(file) && isValidSpec(file, matcher) ) { + addSpec(file, finalSpecs); + } + else { + result = isValidSpec(file, matcher); + } + + return result; +} + +exports.load = function(loadpath, matcher, clear) { + if (clear === true) { + clearSpecs(); + } + + var wannaBeSpecs = wrench.readdirSyncRecursive(loadpath); + for (var i = 0; i < wannaBeSpecs.length; i++) { + var file = path.join(loadpath, wannaBeSpecs[i]); + if ( shouldLoad(file, matcher) ) { + addSpec(file); + } + } +}; + +exports.getSpecs = function() { + return specs.concat(finalSpecs); +}; diff --git a/third_party/jsdoc/test/specs/documentation/alias.js b/third_party/jsdoc/test/specs/documentation/alias.js new file mode 100644 index 0000000000..ce84f59d34 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/alias.js @@ -0,0 +1,80 @@ +/*global describe, expect, it, jasmine */ +'use strict'; + +describe('aliases', function() { + describe('standard', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/alias.js'); + var found = docSet.getByLongname('myObject').filter(function($) { + return ! $.undocumented; + }); + var foundMember = docSet.getByLongname('myObject.myProperty'); + + it('When a symbol is given an alias it is documented as if the name is the alias value.', function() { + expect(found[0].longname).toEqual('myObject'); + }); + + it('When a symbol is a member of an alias it is documented as if the memberof is the alias value.', function() { + expect(foundMember[0].longname).toEqual('myObject.myProperty'); + expect(foundMember[0].memberof).toEqual('myObject'); + }); + }); + + it('When a symbol is a member of an alias of a nested name it is documented as if the memberof is the nested alias value.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/alias2.js'); + var foundMember = docSet.getByLongname('ns.Myclass#myProperty'); + + expect(foundMember[0].longname).toEqual('ns.Myclass#myProperty'); + expect(foundMember[0].name).toEqual('myProperty'); + expect(foundMember[0].memberof).toEqual('ns.Myclass'); + expect(foundMember[0].scope).toEqual('instance'); + }); + + it('When a symbol is a member of an aliased class, a this-variable is documented as if it were a member that class.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/alias3.js'); + var tcm = docSet.getByLongname('trackr.CookieManager')[0]; + var tcmValue = docSet.getByLongname('trackr.CookieManager#value')[0]; + + expect(tcmValue.memberof).toEqual('trackr.CookieManager'); + }); + + it('When a symbol is a function expression that has an alias, the symbol should get the correct longname', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/alias4.js'); + var jacketClass = docSet.getByLongname('module:jacket').filter(function($) { + return $.kind === 'class'; + }); + + expect(jacketClass.length).toBe(1); + expect(jacketClass[0].longname).toBe('module:jacket'); + }); + + it('When a symbol is documented as a static member of , its scope is "global" and not "static".', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/aliasglobal.js'); + var log = docSet.getByLongname('log')[0]; + + expect(log.scope).toEqual('global'); + }); + + it('When a symbol is documented as an instance member of , its scope is "instance" and not "static".', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/aliasglobal2.js'); + var run = docSet.getByLongname('Test#run')[0]; + + expect(run.scope).toEqual('instance'); + expect(run.memberof).toEqual('Test'); + }); + + describe('resolving', function() { + it('When a local reference has alias, put all members into aliased definition. Local modifications should be visible to outside.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/aliasresolve.js'); + var method = docSet.getByLongname('A.F.method'); + + expect(method.length).toEqual(1); + }); + + it('When a reference in an outer scope has alias, put all members into aliased definition. Local modifications are visible to outside.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/aliasresolve2.js'); + var method = docSet.getByLongname('A.F.method'); + + expect(method.length).toEqual(1); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/also.js b/third_party/jsdoc/test/specs/documentation/also.js new file mode 100644 index 0000000000..5afc578340 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/also.js @@ -0,0 +1,69 @@ +/*global describe, expect, it, jasmine, spyOn */ +describe("multiple doclets per symbol", function() { + function undocumented($) { + return ! $.undocumented; + } + + function checkInequality(doclets, property) { + for (var l = doclets.length - 1; l > 0; l--) { + if (doclets[l][property] !== undefined && doclets[l - 1][property] !== undefined) { + expect(doclets[l][property]).not.toBe(doclets[l - 1][property]); + } + } + } + + var docSet = jasmine.getDocSetFromFile('test/fixtures/also.js'); + var name = docSet.getByLongname('Asset#name').filter(undocumented); + var shape = docSet.getByLongname('Asset#shape').filter(undocumented); + + it('When a symbol has multiple adjacent JSDoc comments, both apply to the symbol.', function() { + expect(name.length).toBe(2); + expect(shape.length).toBe(3); + }); + + it('When a symbol has multiple adjacent JSDoc comments that are not identical, the doclets ' + + 'have different comments.', function() { + checkInequality(name, 'comment'); + checkInequality(shape, 'comment'); + }); + + it('When a symbol has multiple adjacent JSDoc comments with different descriptions, ' + + 'the doclets have different descriptions.', function() { + checkInequality(name, 'description'); + checkInequality(shape, 'description'); + }); + + it('When a symbol has multiple adjacent JSDoc comments with different numbers of ' + + '@param tags, the doclets have different parameter lists.', function() { + checkInequality(name, 'params.length'); + checkInequality(shape, 'params.length'); + }); + + it('When a symbol has multiple adjacent JSDoc comments with different numbers of ' + + '@returns tags, the doclets have different lists of return values.', function() { + checkInequality(name, 'returns.length'); + checkInequality(shape, 'returns.length'); + }); + + it('When a file contains a JSDoc comment with an @also tag, and the "tags.allowUnknownTags" ' + + 'option is set to false, the file can be parsed without errors.', function() { + var logger = require('jsdoc/util/logger'); + + var allowUnknownTags = !!global.env.conf.tags.allowUnknownTags; + var docs; + var errors = []; + + function errorListener(err) { + errors.push(err); + } + + logger.addListener('logger:error', errorListener); + global.env.conf.tags.allowUnknownTags = false; + + docs = jasmine.getDocSetFromFile('test/fixtures/also2.js'); + expect(errors[0]).not.toBeDefined(); + + logger.removeListener('logger:error', errorListener); + global.env.conf.tags.allowUnknownTags = allowUnknownTags; + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/callback.js b/third_party/jsdoc/test/specs/documentation/callback.js new file mode 100644 index 0000000000..d8e1441290 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/callback.js @@ -0,0 +1,27 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ +describe('callback tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/callbacktag.js'); + + function callbackTests(callback) { + expect(callback).toBeDefined(); + + expect(callback.type).toBeDefined(); + expect(typeof callback.type).toEqual('object'); + + expect(callback.type.names).toBeDefined(); + expect(callback.type.names instanceof Array).toEqual(true); + expect(callback.type.names.length).toEqual(1); + + expect(callback.type.names[0]).toEqual('function'); + } + + it('correctly handles callbacks that do not define a {type}', function() { + var callback = docSet.getByLongname('requestResponseCallback')[0]; + callbackTests(callback); + }); + + it('correctly handles callbacks that define an incorrect {type}', function() { + var callback = docSet.getByLongname('wrongTypeCallback')[0]; + callbackTests(callback); + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/classwithoutname.js b/third_party/jsdoc/test/specs/documentation/classwithoutname.js new file mode 100644 index 0000000000..844ddbacb4 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/classwithoutname.js @@ -0,0 +1,14 @@ +/*global describe, expect, it, jasmine */ +describe('class without a name', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/classwithoutname.js').doclets + .filter(function(doclet) { + return doclet.name === ''; + }); + + it('When the doclet for a class has an empty name, it should also have an empty longname', function() { + expect(docSet).toBeDefined(); + expect(docSet.length).toBe(1); + expect(docSet[0].description).toBe('Create an instance of MyClass.'); + expect(docSet[0].longname).toBe(''); + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/emptycomments.js b/third_party/jsdoc/test/specs/documentation/emptycomments.js new file mode 100644 index 0000000000..6747f1ad8b --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/emptycomments.js @@ -0,0 +1,15 @@ +/*global describe, expect, it, jasmine, spyOn */ +'use strict'; + +var logger = require('jsdoc/util/logger'); + +describe('empty JSDoc comments', function() { + it('should not report an error when a JSDoc comment contains only whitespace', function() { + var doclets; + + spyOn(logger, 'error'); + doclets = jasmine.getDocSetFromFile('test/fixtures/emptycomments.js'); + + expect(logger.error).not.toHaveBeenCalled(); + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/exports.js b/third_party/jsdoc/test/specs/documentation/exports.js new file mode 100644 index 0000000000..eec64597be --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/exports.js @@ -0,0 +1,20 @@ +/*global describe, expect, it, jasmine */ +describe("'exports' symbol in modules", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/exports.js'); + var sayHello = docSet.getByLongname('module:hello/world.sayHello')[0]; + var sayGoodbye = docSet.getByLongname('module:hello/world.sayGoodbye')[0]; + + it('When a symbol starts with the special name "exports" and is in a file with a ' + + '@module tag, the symbol is documented as a member of that module.', function() { + expect(typeof sayHello).toBe('object'); + expect(sayHello.kind).toBe('function'); + expect(sayHello.memberof).toBe('module:hello/world'); + }); + + it('When a symbol starts with the special name "module.exports" and is in a file with a ' + + '@module tag, the symbol is documented as a member of that module.', function() { + expect(typeof sayGoodbye).toBe('object'); + expect(sayGoodbye.kind).toBe('function'); + expect(sayGoodbye.memberof).toBe('module:hello/world'); + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/funcExpression.js b/third_party/jsdoc/test/specs/documentation/funcExpression.js new file mode 100644 index 0000000000..7cb29eec8a --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/funcExpression.js @@ -0,0 +1,29 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ +describe('function expressions', function() { + function checkLongnames(docSet, namespace) { + var memberName = (namespace || '') + 'Foo#member1'; + var variableName = (namespace || '') + 'Foo~var1'; + var fooMember = docSet.getByLongname(memberName)[0]; + var fooVariable = docSet.getByLongname(variableName)[0]; + + it('should assign the correct longname to members of a function expression', function() { + expect(fooMember.longname).toBe(memberName); + }); + + it('should assign the correct longname to variables in a function expression', function() { + expect(fooVariable.longname).toBe(variableName); + }); + } + + describe('standard', function() { + checkLongnames( jasmine.getDocSetFromFile('test/fixtures/funcExpression.js') ); + }); + + describe('global', function() { + checkLongnames( jasmine.getDocSetFromFile('test/fixtures/funcExpression2.js') ); + }); + + describe('as object literal property', function() { + checkLongnames( jasmine.getDocSetFromFile('test/fixtures/funcExpression3.js'), 'ns.' ); + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/getset.js b/third_party/jsdoc/test/specs/documentation/getset.js new file mode 100644 index 0000000000..a73773ceaf --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/getset.js @@ -0,0 +1,24 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ +describe("When a getter or setter is the child of an object literal", function () { + var docSet = jasmine.getDocSetFromFile("test/fixtures/getset.js"); + var foundName = docSet.getByLongname("Person#name"); + var foundAge = docSet.getByLongname("Person#age"); + + it("should have a doclet with the correct longname", function () { + expect(foundName.length).toEqual(2); + expect(foundAge.length).toEqual(1); + }); + + it("should have a doclet with the correct name", function () { + expect(foundName[0].name).toEqual("name"); + expect(foundName[1].name).toEqual("name"); + expect(foundAge[0].name).toEqual("age"); + }); + + it("should have the correct memberof", function () { + expect(foundName[0].memberof).toEqual("Person"); + expect(foundName[1].memberof).toEqual("Person"); + expect(foundAge[0].memberof).toEqual("Person"); + }); + +}); diff --git a/third_party/jsdoc/test/specs/documentation/inlinecomment.js b/third_party/jsdoc/test/specs/documentation/inlinecomment.js new file mode 100644 index 0000000000..d2692ec164 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/inlinecomment.js @@ -0,0 +1,12 @@ +describe("inline comments", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/inlinecomment.js'), + t = docSet.getByLongname('test'), + t2 = docSet.getByLongname('test2'); + + it('When there is an inline comment on a line ending with no semicolon, that comment and the next comment are still captured', function() { + //Inline comment on line without semicolon is captured + expect(t.length).toEqual(1); + //Inline comment on line after line without semicolon is captured + expect(t2.length).toEqual(1); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/documentation/inner.js b/third_party/jsdoc/test/specs/documentation/inner.js new file mode 100644 index 0000000000..d69594ea2c --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/inner.js @@ -0,0 +1,24 @@ +describe("when a documented var memeber is inside a named function", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/inner.js'), + found1 = docSet.getByLongname('sendMessage~encoding'), + found2 = docSet.getByLongname('sendMessage~encrypt'); + + it("A Doclet with the correct longname should be found", function() { + expect(found1.length).toEqual(1); + expect(found2.length).toEqual(1); + }); + + it("The short name should be correct", function() { + expect(found1[0].name).toEqual('encoding'); + expect(found2[0].name).toEqual('encrypt'); + }); + + it("The member of should be correct", function() { + expect(found1[0].memberof).toEqual('sendMessage'); + expect(found2[0].memberof).toEqual('sendMessage'); + }); + it("The scope should default to 'inner'", function() { + expect(found1[0].scope).toEqual('inner'); + expect(found2[0].scope).toEqual('inner'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/documentation/innerscope.js b/third_party/jsdoc/test/specs/documentation/innerscope.js new file mode 100644 index 0000000000..41526ae013 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/innerscope.js @@ -0,0 +1,41 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ +describe("inner scope", function() { + describe("Outer~inner.member cases", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/innerscope.js'); + var to = docSet.getByLongname('Message~headers.to'); + var from = docSet.getByLongname('Message~headers.from'); + var response = docSet.getByLongname('Message~response.code'); + + it('should occur when a member of a var member is documented.', function() { + expect(to.length).toEqual(1); + }); + + it('should occur when a second member of a var member is documented.', function() { + expect(response.length).toEqual(1); + }); + + it('should occur when a deeply nested member of a var member is documented.', function() { + expect(from.length).toEqual(1); + }); + }); + + describe("other cases", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/innerscope2.js'); + var to = docSet.getByLongname('Message~headers.to'); + var from = docSet.getByLongname('~headers.from'); + var cache = docSet.getByLongname('~headers.cache'); + + it('When a var is declared in a function, It is like Inner~member', function() { + expect(cache.length).toEqual(1); + }); + + it('When a var is masked by an inner var and a member of the inner is documented, it is like Inner~inner.member', function() { + expect(from.length).toEqual(1); + }); + + it('When a documented member is assigned to a var that masks an outer var.', function() { + expect(from[0].name).toEqual('from'); + expect(from[0].memberof).toEqual('~headers'); + }); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/documentation/lends.js b/third_party/jsdoc/test/specs/documentation/lends.js new file mode 100644 index 0000000000..85253c808c --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/lends.js @@ -0,0 +1,137 @@ +/*global describe, expect, it, jasmine */ +describe("lends", function() { + describe("when a documented member is inside an object literal associated with a @lends tag", function() { + function removeUndocumented($) { + return !$.undocumented; + } + + describe("standard case", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/lends.js'), + init = docSet.getByLongname('Person#initialize'), + name = docSet.getByLongname('Person#name'); + + it("The member should be documented as a member of the lendee", function() { + expect(init.length, 1); + }); + + it("The this member should be documented as a member of the lendee", function() { + expect(name.length, 1); + }); + }); + + describe("case containing constructor", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/lends2.js'), + person = docSet.getByLongname('Person').filter(function($) { + return ! $.undocumented; + })[0], + name = docSet.getByLongname('Person#name'); + + it("A tag with a @constructs tag is documented as a constructor.", function() { + expect(person.description).toEqual('Construct a Person.'); + }); + + it("The member should be documented as a member of the lendee", function() { + expect(person.length, 1); + }); + + it("The this member should be documented as a member of the lendee", function() { + expect(name.length, 1); + }); + }); + + describe("case that uses @lends in a multiline doclet", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/lends3.js'), + init = docSet.getByLongname('Person#initialize'), + name = docSet.getByLongname('Person#name'); + + it("The member should be documented as a member of the lendee", function() { + expect(init.length, 1); + }); + + it("The this member should be documented as a member of the lendee", function() { + expect(name.length, 1); + }); + }); + + describe("case that uses @lends within a closure", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/lends4.js'); + var person = docSet.getByLongname('Person'); + var say = docSet.getByLongname('Person#say'); + + it("The class constructor should be documented with the name of the lendee", function() { + expect(person.length).toBe(1); + expect(person[0].name).toBe('Person'); + expect(person[0].kind).toBe('class'); + }); + + it("A class' instance method should be documented as a member of the lendee", function() { + expect(say.length).toBe(1); + }); + }); + + describe("case that uses @lends within nested function calls", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/lends5.js'); + var person = docSet.getByLongname('Person').filter(removeUndocumented)[0]; + var say = docSet.getByLongname('Person#say').filter(removeUndocumented)[0]; + + it("The class constructor should be documented with the name of the lendee", function() { + expect(person).toBeDefined(); + expect(person.name).toBe('Person'); + expect(person.kind).toBe('class'); + }); + + it("A class' instance method should be documented as a member of the lendee", function() { + expect(say).toBeDefined(); + }); + }); + + describe('case that uses @lends twice within a closure', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/lends6.js'); + + it('The first class with a @lends tag should appear in the parse results', function() { + var person = docSet.getByLongname('Person').filter(removeUndocumented)[0]; + var say = docSet.getByLongname('Person#say').filter(removeUndocumented)[0]; + + expect(person).toBeDefined(); + expect(person.name).toBe('Person'); + expect(person.kind).toBe('class'); + + expect(say).toBeDefined(); + expect(say.name).toBe('say'); + expect(say.kind).toBe('function'); + }); + + it('The second class with a @lends tag should appear in the parse results', function() { + var robot = docSet.getByLongname('Robot').filter(removeUndocumented)[0]; + var emote = docSet.getByLongname('Robot#emote').filter(removeUndocumented)[0]; + + expect(robot).toBeDefined(); + expect(robot.name).toBe('Robot'); + expect(robot.kind).toBe('class'); + + expect(emote).toBeDefined(); + expect(emote.name).toBe('emote'); + expect(emote.kind).toBe('function'); + }); + }); + }); + + describe("when a documented member is inside an objlit associated with a @lends tag that has no value.", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/lendsglobal.js'), + testf = docSet.getByLongname('test')[0], + test1 = docSet.getByLongname('test1')[0], + test12 = docSet.getByLongname('test1.test2')[0]; + + it("The members of the objlit are not members of any symbol", function() { + expect(typeof testf.memberof).toEqual('undefined'); + }); + + it("The members of the objlit are documented as global.", function() { + expect(testf.longname).toEqual('test'); + }); + + it("The nested members of the objlit are members of a global symbol", function() { + expect(test12.memberof).toEqual('test1'); + }); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/documentation/letkeyword.js b/third_party/jsdoc/test/specs/documentation/letkeyword.js new file mode 100644 index 0000000000..43d120638b --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/letkeyword.js @@ -0,0 +1,28 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ +describe('let keyword', function() { + var docSet; + var exampleModule; + var exampleMethod; + + function getDocSet() { + docSet = jasmine.getDocSetFromFile('test/fixtures/letkeyword.js'); + exampleModule = docSet.getByLongname('module:exampleModule'); + exampleMethod = docSet.getByLongname('module:exampleModule.exampleMethod'); + } + + it('should be able to compile JS files that contain the "let" keyword', function() { + expect(getDocSet).not.toThrow(); + }); + + it('should correctly recognize a module defined with the "let" keyword', function() { + expect(exampleModule).toBeDefined(); + expect( Array.isArray(exampleModule) ).toBe(true); + expect(exampleModule.length).toBe(1); + }); + + it('should correctly recognize members of a module defined with the "let" keyword', function() { + expect(exampleMethod).toBeDefined(); + expect( Array.isArray(exampleMethod) ).toBe(true); + expect(exampleMethod.length).toBe(1); + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/moduleinner.js b/third_party/jsdoc/test/specs/documentation/moduleinner.js new file mode 100644 index 0000000000..1a783c96c2 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/moduleinner.js @@ -0,0 +1,13 @@ +describe("inner scope for modules", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/moduleinner.js'), + fooIn = docSet.getByLongname('module:my/module~fooIn')[0], + fooOut = docSet.getByLongname('module:my/module~fooOut')[0]; + + it('When a function appears in the topscope of a module, the symbol is documented as an inner member of that module.', function() { + expect(typeof fooOut).toEqual('object'); + expect(fooOut.longname).toEqual('module:my/module~fooOut'); + + expect(typeof fooIn).toEqual('object'); + expect(fooIn.longname).toEqual('module:my/module~fooIn'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/documentation/moduleisconstructor.js b/third_party/jsdoc/test/specs/documentation/moduleisconstructor.js new file mode 100644 index 0000000000..45a1ac0b67 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/moduleisconstructor.js @@ -0,0 +1,37 @@ +/*global describe, expect, it, jasmine */ + +describe('module that exports a constructor', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/moduleisconstructor.js'); + var modules = docSet.doclets.filter(function(doclet) { + return doclet.kind === 'module'; + }); + var classes = docSet.doclets.filter(function(doclet) { + return doclet.kind === 'class'; + }); + + it('should include one doclet whose kind is "module"', function() { + expect(modules.length).toBe(1); + expect(modules[0].kind).toBe('module'); + }); + + it('should include one doclet whose kind is "class"', function() { + expect(classes.length).toBe(1); + expect(classes[0].kind).toBe('class'); + }); + + describe('class doclet', function() { + it('should include a "description" property that contains the constructor description', function() { + expect(classes[0].description).toEqual('Create a new configuration.'); + }); + + it('should include a "class-description" property', function() { + expect(classes[0].classdesc).toEqual('Describe the class here.'); + }); + }); + + describe('module doclet', function() { + it('should include a "description" property that contains the module description', function() { + expect(modules[0].description).toEqual('Describe the module here.'); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/moduleisfunction.js b/third_party/jsdoc/test/specs/documentation/moduleisfunction.js new file mode 100644 index 0000000000..1aeb783d07 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/moduleisfunction.js @@ -0,0 +1,23 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ + +describe('module that exports a function that is not a constructor', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/moduleisfunction.js'); + var functions = docSet.doclets.filter(function(doclet) { + return doclet.kind === 'function'; + }); + + it('should include one doclet whose kind is "function"', function() { + expect(functions.length).toBe(1); + expect(functions[0].kind).toBe('function'); + }); + + describe('function doclet', function() { + it('should not include a "scope" property', function() { + expect(functions[0].scope).not.toBeDefined(); + }); + + it('should not include a "memberof" property', function() { + expect(functions[0].memberof).not.toBeDefined(); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/modules.js b/third_party/jsdoc/test/specs/documentation/modules.js new file mode 100644 index 0000000000..fd61cda749 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/modules.js @@ -0,0 +1,71 @@ +/*global afterEach: true, beforeEach: true, describe: true, env: true, expect: true, it: true, +jasmine: true, spyOn: true */ +describe("module names", function() { + var path = require('jsdoc/path'); + var runtime = require('jsdoc/util/runtime'); + + var doclets; + + var pwd = env.pwd; + var srcParser = null; + var sourceFiles = env.sourceFiles.slice(0); + var sourcePaths = env.opts._.slice(0); + + beforeEach(function() { + env.opts._ = [path.normalize(env.pwd + '/test/fixtures/modules/data/')]; + env.pwd = env.dirname; + env.sourceFiles = []; + srcParser = jasmine.createParser(); + require('jsdoc/src/handlers').attachTo(srcParser); + }); + + afterEach(function() { + env.opts._ = sourcePaths; + env.pwd = pwd; + env.sourceFiles = sourceFiles; + }); + + it("should create a name from the file path when no documented module name exists", function() { + var filename = 'test/fixtures/modules/data/mod-1.js'; + + env.sourceFiles.push(filename); + doclets = srcParser.parse( + path.normalize( path.join(env.pwd, filename) ) + ); + expect(doclets.length).toBeGreaterThan(1); + expect(doclets[0].longname).toEqual('module:mod-1'); + }); + + // only works on Windows (which is fine) + if ( /^win/.test(require('os').platform()) ) { + it("should always use forward slashes when creating a name from the file path", function() { + var Doclet = require('jsdoc/doclet').Doclet; + var doclet; + + env.sourceFiles = [ + 'C:\\Users\\Jane Smith\\myproject\\index.js', + 'C:\\Users\\Jane Smith\\myproject\\lib\\mymodule.js' + ]; + env.opts._ = []; + + doclet = new Doclet('/** @module */', { + lineno: 1, + filename: 'C:\\Users\\Jane Smith\\myproject\\lib\\mymodule.js' + }); + + expect(doclet.name).toBe('lib/mymodule'); + }); + } + + it("should use the documented module name if available", function() { + var filename = 'test/fixtures/modules/data/mod-2.js'; + + env.sourceFiles.push(filename); + doclets = srcParser.parse( + path.normalize( path.join(env.pwd, filename) ) + ); + + expect(doclets.length).toBeGreaterThan(1); + expect(doclets[0].longname).toEqual('module:my/module/name'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/documentation/objectlit.js b/third_party/jsdoc/test/specs/documentation/objectlit.js new file mode 100644 index 0000000000..39bca2f11c --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/objectlit.js @@ -0,0 +1,67 @@ +/*global describe, expect, it, jasmine */ + +describe('object literals', function() { + describe('When a child of an objlit has no @name or @memberof tags', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/objectlit.js'); + var found = docSet.getByLongname('tools.serialiser.value'); + + it('should have a doclet with the correct longname', function() { + expect(found.length).toBe(1); + }); + + it('should have a doclet with the correct name', function() { + expect(found[0].name).toBe('value'); + }); + + it('should have the correct memberof', function() { + expect(found[0].memberof).toBe('tools.serialiser'); + }); + + it('should have a static scope', function() { + expect(found[0].scope).toBe('static'); + }); + }); + + describe('When a parent of an objlit has no documentation', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/objectlit2.js'); + var found = docSet.getByLongname('position.axis.x'); + + it('should have a doclet with the correct longname', function() { + expect(found.length).toBe(1); + }); + + it('should have a doclet with the correct name', function() { + expect(found[0].name).toBe('x'); + }); + + it('should have the correct memberof', function() { + expect(found[0].memberof).toBe('position.axis'); + }); + + it('should have a static scope', function() { + expect(found[0].scope).toBe('static'); + }); + }); + + describe('When an object literal\'s property names must be escaped in a regexp', function() { + var docSet; + var found; + + function loadDocSet() { + docSet = jasmine.getDocSetFromFile('test/fixtures/objectlit3.js'); + found = docSet.getByLongname('tokens.(.before'); + } + + it('should not throw an error when creating a doclet', function() { + expect(loadDocSet).not.toThrow(); + }); + + it('should have a doclet with the correct name', function() { + expect(found[0].name).toBe('before'); + }); + + it('should have a doclet with the correct memberof', function() { + expect(found[0].memberof).toBe('tokens.('); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/objectpropertykeys.js b/third_party/jsdoc/test/specs/documentation/objectpropertykeys.js new file mode 100644 index 0000000000..0cb39e22a3 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/objectpropertykeys.js @@ -0,0 +1,6 @@ +describe("using existing Object properties as object literal keys", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/objectpropertykeys.js'); + it("should not crash", function() { + expect(true).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/documentation/quotename.js b/third_party/jsdoc/test/specs/documentation/quotename.js new file mode 100644 index 0000000000..16b24f575a --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/quotename.js @@ -0,0 +1,23 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ +describe("quoted names", function() { + + describe("when found in square brackets", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/quotename.js'); + var found1 = docSet.getByLongname('chat.\"#channel\".open')[0]; + + it('should have correct name and memberof', function() { + expect(found1.name).toEqual('open'); + expect(found1.memberof).toEqual('chat.\"#channel\"'); + }); + }); + + describe("when found in an object literal", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/quotename2.js'); + var found1 = docSet.getByLongname('contacts.say-"hello"@example.com.username')[0]; + + it('should have correct name and memberof', function() { + expect(found1.name).toEqual('username'); + expect(found1.memberof).toEqual('contacts.say-"hello"@example.com'); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/specialnames.js b/third_party/jsdoc/test/specs/documentation/specialnames.js new file mode 100644 index 0000000000..f6d8b62bce --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/specialnames.js @@ -0,0 +1,10 @@ +describe("documenting symbols with special names", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/specialnames.js'), + name = docSet.getByLongname('hasOwnProperty').filter(function($) { + return ! $.undocumented; + }); + + it('When a symbol has the documented name of "hasOwnProperty," JSDoc should correctly include it in the docs.', function() { + expect(name.length).toEqual(1); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/documentation/starbangstar.js b/third_party/jsdoc/test/specs/documentation/starbangstar.js new file mode 100644 index 0000000000..0a875bb41f --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/starbangstar.js @@ -0,0 +1,13 @@ +describe("starbangstar", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/starbangstar.js'), + mod = docSet.getByLongname('module:myscript/core')[0], + x = docSet.getByLongname('module:myscript/core.x')[0]; + + it('should not treat a doclet starting with /*!* as a JSDoc comment.', function() { + expect(mod.description).toEqual('Script that does something awesome'); + }); + + it('should not treat a doclet starting with /*!** as a JSDoc comment.', function() { + expect(x).toBeUndefined(); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/documentation/this.js b/third_party/jsdoc/test/specs/documentation/this.js new file mode 100644 index 0000000000..6393dba4aa --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/this.js @@ -0,0 +1,102 @@ +describe("this", function() { + describe("attaching members to 'this'", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/this.js'), + found1 = docSet.getByLongname('Singer#tralala'), + found2 = docSet.getByLongname('Singer#isSinging'); + + describe("in a contructor", function() { + it("should have a longname like Constructor#member", function() { + expect(found1.length).toEqual(1); + }); + + it("should havea correct short name", function() { + expect(found1[0].name).toEqual('tralala'); + }); + + it("should havea correct memberof", function() { + expect(found1[0].memberof).toEqual('Singer'); + }); + + it("should default to a 'instance' scope", function() { + expect(found1[0].scope).toEqual('instance'); + }); + }); + + describe("in a method of a constructor", function() { + it("should have a longname like Constructor#member", function() { + expect(found2.length).toEqual(1); + }); + + it("should havea correct short name", function() { + expect(found2[0].name).toEqual('isSinging'); + }); + + it("should havea correct memberof", function() { + expect(found2[0].memberof).toEqual('Singer'); + }); + + it("should default to a 'instance' scope", function() { + expect(found2[0].scope).toEqual('instance'); + }); + }); + }); + + describe("when a contructor is nested inside another constructor", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/this2.js'), + found = docSet.getByLongname('TemplateBuilder#Template#rendered'); + + it("should have a longname like Constructor#Constructor#member", function() { + expect(found.length).toEqual(1); + }); + + it("should havea correct short name", function() { + expect(found[0].name).toEqual('rendered'); + }); + + it("should havea correct memberof", function() { + expect(found[0].memberof).toEqual('TemplateBuilder#Template'); + }); + + it("should default to a 'instance' scope", function() { + expect(found[0].scope).toEqual('instance'); + }); + }); + + describe("When a this is assigned to inside a non-constructor function", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/this3.js'), + found = docSet.getByLongname('position'); + + it("should have a global member name like 'member'", function() { + expect(found.length).toEqual(1); + }); + + it("should havea correct short name", function() { + expect(found[0].name).toEqual('position'); + }); + + it("should havea correct memberof", function() { + expect(found[0].memberof).toBeUndefined(); + }); + }); + + describe("When a member is nested inside an objectlit 'this' property inside a constructor", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/this-and-objectlit.js'), + found = docSet.getByLongname('Page#parts.body.heading'); + + it("should have a longname like Constructor#objlit.member", function() { + expect(found.length).toEqual(1); + }); + + it("should havea correct short name", function() { + expect(found[0].name).toEqual('heading'); + }); + + it("should havea correct memberof", function() { + expect(found[0].memberof).toEqual('Page#parts.body'); + }); + + it("should default to a 'static' scope", function() { + expect(found[0].scope).toEqual('static'); + }); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/documentation/typetaginline.js b/third_party/jsdoc/test/specs/documentation/typetaginline.js new file mode 100644 index 0000000000..2b1aec49b9 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/typetaginline.js @@ -0,0 +1,71 @@ +/*global beforeEach, describe, expect, it, jasmine */ +describe('@type tag inline with function parameters', function() { + var info; + + var docSet = jasmine.getDocSetFromFile('test/fixtures/typetaginline.js'); + + function checkParams(doclet, paramInfo) { + expect(doclet.params).toBeDefined(); + expect(doclet.params.length).toBe(paramInfo.length); + + doclet.params.forEach(function(param, i) { + expect(param.name).toBe(paramInfo[i].name); + expect(param.type.names[0]).toBe(paramInfo[i].typeName); + if (paramInfo[i].description !== undefined) { + expect(param.description).toBe(paramInfo[i].description); + } + }); + } + + beforeEach(function() { + info = []; + }); + + it('When a function parameter has an inline @type tag, the parameter type is documented', + function() { + var dispense = docSet.getByLongname('dispense')[0]; + info[0] = { name: 'candy', typeName: 'string' }; + + checkParams(dispense, info); + }); + + it('When a function parameter has a standard JSDoc comment and an inline @type tag, the docs ' + + 'reflect the standard JSDoc comment', function() { + var Dispenser = docSet.getByLongname('Dispenser')[0]; + info[0] = { name: 'candyId', typeName: 'number', description: 'The candy\'s identifier.' }; + + checkParams(Dispenser, info); + }); + + it('When a function accepts multiple parameters, and only the first parameter is documented ' + + 'with an inline @type tag, the function parameters are documented in the correct order', + function() { + var restock = docSet.getByLongname('restock')[0]; + info[0] = { name: 'dispenser', typeName: 'Dispenser' }; + info[1] = { name: 'item', typeName: 'string' }; + + checkParams(restock, info); + }); + + it('When a function accepts multiple parameters, and only the last parameter is documented ' + + 'with an inline @type tag, the function parameters are documented in the correct order', + function() { + var clean = docSet.getByLongname('clean')[0]; + info[0] = { name: 'dispenser', typeName: 'Dispenser' }; + info[1] = { name: 'cleaner', typeName: 'string' }; + + checkParams(clean, info); + }); + + it('When a function accepts multiple parameters, and a parameter in the middle is documented ' + + 'with an inline @type tag, the function parameters are documented in the correct order', + function() { + var paint = docSet.getByLongname('paint')[0]; + info[0] = { name: 'dispenser', typeName: 'Dispenser' }; + info[1] = { name: 'color', typeName: 'Color' }; + info[2] = { name: 'shade', typeName: 'number' }; + info[3] = { name: 'brand', typeName: 'string' }; + + checkParams(paint, info); + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/typetagwithnewline.js b/third_party/jsdoc/test/specs/documentation/typetagwithnewline.js new file mode 100644 index 0000000000..f1198e9722 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/typetagwithnewline.js @@ -0,0 +1,28 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ + +describe('@type tag containing a newline character', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/typetagwithnewline.js'); + var mini = docSet.getByLongname('Matryoshka.mini')[0]; + var mega = docSet.getByLongname('Matryoshka.mega')[0]; + + it('When the type expression for a @type tag contains a newline character and is not ' + + 'enclosed in braces, the type expression is parsed correctly.', function() { + expect(mini).toBeDefined(); + expect(mini.type).toBeDefined(); + expect(mini.type.names).toBeDefined(); + expect(mini.type.names.length).toBe(2); + expect(mini.type.names[0]).toBe('!Array.'); + expect(mini.type.names[1]).toBe('!Array.>'); + }); + + it('When the type expression for a @type tag contains a newline character and is enclosed ' + + 'in braces, the type expression is parsed correctly.', function() { + expect(mega).toBeDefined(); + expect(mega.type).toBeDefined(); + expect(mega.type.names).toBeDefined(); + expect(mega.type.names.length).toBe(3); + expect(mega.type.names[0]).toBe('!Array.'); + expect(mega.type.names[1]).toBe('!Array.>'); + expect(mega.type.names[2]).toBe('!Array.>>'); + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/var.js b/third_party/jsdoc/test/specs/documentation/var.js new file mode 100644 index 0000000000..4417435b93 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/var.js @@ -0,0 +1,61 @@ +/*global describe, expect, it, jasmine */ +'use strict'; + +describe('var statements', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/var.js'); + var found = [ + docSet.getByLongname('GREEN'), + docSet.getByLongname('RED'), + docSet.getByLongname('validate'), + docSet.getByLongname('i'), + docSet.getByLongname('results') + ]; + + describe('when a series of constants is documented', function() { + it('should find the first constant', function() { + expect(found[0].length).toBe(1); + }); + + it('should attach the docs to the first constant', function() { + expect(found[0][0].comment).toBe('/** document me */'); + }); + + it('should have the correct name', function() { + expect(found[0][0].name).toBe('GREEN'); + }); + + it('should have the correct memberof', function() { + expect(found[0][0].memberof).toBeUndefined(); + }); + + it('should give the constant a global scope', function() { + expect(found[0][0].scope).toBe('global'); + }); + + it('should find the second constant', function() { + expect(found[1].length).toBe(1); + }); + + it('should not attach the docs to the second constant', function() { + expect(found[1][0].undocumented).toBe(true); + }); + }); + + describe('when a member of a series of vars is documented', function() { + it('should attach the docs to the correct var', function() { + expect(found[4][0].comment).toBe('/** document me */'); + }); + + it('should have the correct name', function() { + expect(found[4][0].name).toBe('results'); + }); + + it('should leave memberof undefined', function() { + expect(found[4][0].memberof).toBeUndefined(); + }); + + it('should give the var a global scope', function() { + expect(found[4][0].scope).toBe('global'); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/documentation/variations.js b/third_party/jsdoc/test/specs/documentation/variations.js new file mode 100644 index 0000000000..3bf133c8e1 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/variations.js @@ -0,0 +1,38 @@ +describe("variations", function() { + + describe("by name", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/variations.js'), + fadein1 = docSet.getByLongname('anim.fadein(1)')[0], + fadein2 = docSet.getByLongname('anim.fadein(2)')[0]; + + it('When a symbol has a name with a variation, the doclet has a variation property.', function() { + expect(fadein1.variation).toEqual('1'); + expect(fadein2.variation).toEqual('2'); + }); + + it('When a symbol has a name with a variation in the name, the doclet name has no variation in it.', function() { + expect(fadein1.name).toEqual('fadein'); + expect(fadein2.name).toEqual('fadein'); + }); + + it('When a symbol has a name with a variation in the name, the doclet longname has the variation in it.', function() { + expect(fadein1.longname).toEqual('anim.fadein(1)'); + expect(fadein2.longname).toEqual('anim.fadein(2)'); + }); + }); + + describe("by tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/variations3.js'), + someObject = docSet.getByLongname('someObject')[0], + someObject2 = docSet.getByLongname('someObject(2)')[0], + someObject2method = docSet.getByLongname('someObject(2).someMethod')[0]; + + it('When a symbol has a variation tag, the longname includes that variation.', function() { + expect(someObject2.longname).toEqual('someObject(2)'); + }); + + it('When a symbol is a member of a variation, the longname includes the variation.', function() { + expect(someObject2method.longname).toEqual('someObject(2).someMethod'); + }); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/documentation/virtual.js b/third_party/jsdoc/test/specs/documentation/virtual.js new file mode 100644 index 0000000000..0e55bb7cb4 --- /dev/null +++ b/third_party/jsdoc/test/specs/documentation/virtual.js @@ -0,0 +1,53 @@ +/*global describe, expect, it, jasmine */ +describe('virtual symbols', function() { + + describe('simple cases', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/virtual.js'); + var dimensions = docSet.getByLongname('dimensions'); + var width = docSet.getByLongname('width'); + + it('should document virtual symbols', function() { + expect(dimensions.length).toBe(1); + }); + + it('should document an undocumented symbol found after a comment for a virtual symbol', function() { + expect(width.length).toBe(1); + }); + }); + + describe('complex cases', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/virtual2.js'); + var say = docSet.getByLongname('Person#say')[0]; + var sayCallback = docSet.getByLongname('Person~sayCallback')[0]; + + it('should document virtual symbols inside an object literal', function() { + expect(sayCallback).toBeDefined(); + expect(sayCallback.undocumented).not.toBeDefined(); + }); + + it('should attach the comment to a documented symbol that follows a virtual symbol', function() { + expect(say).toBeDefined(); + expect(say.undocumented).not.toBeDefined(); + }); + }); + + describe('overloaded virtual symbols', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/virtual3.js'); + var constructors = docSet.getByLongname('module:connection'); + + it('should create multiple doclets for overloaded virtual symbols', function() { + expect(constructors).toBeDefined(); + expect(constructors.length).toBe(2); + }); + + it('should use the correct signature for each virtual symbol', function() { + expect(constructors[0]).toBeDefined(); + expect(constructors[0].params).toBeDefined(); + expect(Array.isArray(constructors[0].params)).toBe(true); + expect(constructors[0].params[0].name).toBe('name'); + + expect(constructors[1]).toBeDefined(); + expect(constructors[1].params).not.toBeDefined(); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/augment.js b/third_party/jsdoc/test/specs/jsdoc/augment.js new file mode 100644 index 0000000000..7b2daf927f --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/augment.js @@ -0,0 +1,4 @@ +/*global describe: true, env: true, it: true */ +describe("jsdoc/augment", function() { + // TODO +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/borrow.js b/third_party/jsdoc/test/specs/jsdoc/borrow.js new file mode 100644 index 0000000000..4085d9b194 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/borrow.js @@ -0,0 +1,3 @@ +describe("jsdoc/borrow", function() { + //TODO +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/jsdoc/config.js b/third_party/jsdoc/test/specs/jsdoc/config.js new file mode 100644 index 0000000000..1ea42dbd2e --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/config.js @@ -0,0 +1,47 @@ +/*global describe: true, expect: true, it: true */ +describe("jsdoc/config", function() { + var jsdoc = {config: require('jsdoc/config') }; + + it("should exist", function() { + expect(jsdoc.config).toBeDefined(); + expect(typeof jsdoc.config).toEqual("function"); + }); + + it("should provide a 'get' instance function", function() { + var config = new jsdoc.config(); + expect(config.get).toBeDefined(); + expect(typeof config.get).toEqual("function"); + }); + + describe ("constructor with empty", function() { + it('should be possible to construct a Config with an empty arguments', function() { + var config = new jsdoc.config().get(); + + expect(config.plugins).toEqual([]); + }); + }); + + describe ("constructor with {}", function() { + it('should be possible to construct a Config with JSON of an object literal that is emptys', function() { + var config = new jsdoc.config('{}').get(); + + expect(config.plugins).toEqual([]); + }); + }); + + describe ("constructor with plugins value", function() { + it('should be possible to construct a Config with JSON of an object literal that has a plugin value', function() { + var config = new jsdoc.config('{"plugins":[42]}').get(); + + expect(config.plugins).toEqual([42]); + }); + }); + + describe ("constructor with source value", function() { + it('should be possible to construct a Config with JSON of an object literal that has a source value', function() { + var config = new jsdoc.config('{"source":{"includePattern":"hello"}}').get(); + + expect(config.source.includePattern).toEqual('hello'); + }); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/jsdoc/doclet.js b/third_party/jsdoc/test/specs/jsdoc/doclet.js new file mode 100644 index 0000000000..edbbc5a235 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/doclet.js @@ -0,0 +1,64 @@ +/*global afterEach, describe, env, expect, it, jasmine */ +'use strict'; + +describe('jsdoc/doclet', function() { + // TODO: more tests + var _ = require('underscore'); + var Doclet = require('jsdoc/doclet').Doclet; + + var debug = !!env.opts.debug; + var docSet = jasmine.getDocSetFromFile('test/fixtures/doclet.js'); + var test1 = docSet.getByLongname('test1')[0]; + var test2 = docSet.getByLongname('test2')[0]; + + var expectList = '* List item 1'; + var expectStrong = '**Strong** is strong'; + + afterEach(function() { + env.opts.debug = debug; + }); + + it('does not mangle Markdown in a description that uses leading asterisks', function() { + expect(test2.description.indexOf(expectList)).toBeGreaterThan(-1); + expect(test2.description.indexOf(expectStrong)).toBeGreaterThan(-1); + }); + + it('adds the AST node as a non-enumerable property by default', function() { + var descriptor = Object.getOwnPropertyDescriptor(test1.meta.code, 'node'); + + expect(descriptor.enumerable).toBe(false); + }); + + it('adds the AST node as an enumerable property in debug mode', function() { + var descriptor; + var doclet; + + env.opts.debug = true; + doclet = jasmine.getDocSetFromFile('test/fixtures/doclet.js').getByLongname('test1')[0]; + descriptor = Object.getOwnPropertyDescriptor(doclet.meta.code, 'node'); + + expect(descriptor.enumerable).toBe(true); + }); + + describe('setScope', function() { + it('should accept the correct scope names', function() { + function setScope(scopeName) { + var doclet = new Doclet('/** Huzzah, a doclet! */', {}); + doclet.setScope(scopeName); + } + + _.values(require('jsdoc/name').SCOPE.NAMES).forEach(function(scopeName) { + expect( setScope.bind(null, scopeName) ).not.toThrow(); + }); + }); + + it('should throw an error for invalid scope names', function() { + function setScope() { + var doclet = new Doclet('/** Woe betide this doclet. */', {}); + doclet.setScope('fiddlesticks'); + } + + expect(setScope).toThrow(); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/name.js b/third_party/jsdoc/test/specs/jsdoc/name.js new file mode 100644 index 0000000000..e20e86d2bf --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/name.js @@ -0,0 +1,421 @@ +/*global describe, expect, it */ +'use strict'; + +describe('jsdoc/name', function() { + var jsdoc = { + doclet: require('jsdoc/doclet'), + name: require('jsdoc/name') + }; + + it('should exist', function() { + expect(jsdoc.name).toBeDefined(); + expect(typeof jsdoc.name).toBe('object'); + }); + + it("should export a 'resolve' function", function() { + expect(jsdoc.name.resolve).toBeDefined(); + expect(typeof jsdoc.name.resolve).toBe('function'); + }); + + it("should export an 'applyNamespace' function", function() { + expect(jsdoc.name.applyNamespace).toBeDefined(); + expect(typeof jsdoc.name.applyNamespace).toBe('function'); + }); + + // TODO: add tests for other exported constants + it('should export a SCOPE enum', function() { + expect(jsdoc.name.SCOPE).toBeDefined(); + expect(typeof jsdoc.name.SCOPE).toBe('object'); + }); + + it("should export a 'shorten' function", function() { + expect(jsdoc.name.shorten).toBeDefined(); + expect(typeof jsdoc.name.shorten).toBe('function'); + }); + + it("should export a 'splitName' function", function() { + expect(jsdoc.name.splitName).toBeDefined(); + expect(typeof jsdoc.name.splitName).toBe('function'); + }); + + describe('SCOPE', function() { + var SCOPE = jsdoc.name.SCOPE; + + it('should have a "NAMES" property', function() { + expect(SCOPE.NAMES).toBeDefined(); + expect(typeof SCOPE.NAMES).toBe('object'); + }); + + it('should have a "PUNC" property', function() { + expect(SCOPE.PUNC).toBeDefined(); + expect(typeof SCOPE.PUNC).toBe('object'); + }); + + describe('NAMES', function() { + it('should have a "GLOBAL" property', function() { + expect(SCOPE.NAMES.GLOBAL).toBeDefined(); + expect(typeof SCOPE.NAMES.GLOBAL).toBe('string'); + }); + + it('should have an "INNER" property', function() { + expect(SCOPE.NAMES.INNER).toBeDefined(); + expect(typeof SCOPE.NAMES.INNER).toBe('string'); + }); + + it('should have an "INSTANCE" property', function() { + expect(SCOPE.NAMES.INSTANCE).toBeDefined(); + expect(typeof SCOPE.NAMES.INSTANCE).toBe('string'); + }); + + it('should have a "STATIC" property', function() { + expect(SCOPE.NAMES.STATIC).toBeDefined(); + expect(typeof SCOPE.NAMES.STATIC).toBe('string'); + }); + }); + + describe('PUNC', function() { + it('should have an "INNER" property', function() { + expect(SCOPE.PUNC.INNER).toBeDefined(); + expect(typeof SCOPE.PUNC.INNER).toBe('string'); + }); + + it('should have an "INSTANCE" property', function() { + expect(SCOPE.PUNC.INSTANCE).toBeDefined(); + expect(typeof SCOPE.PUNC.INSTANCE).toBe('string'); + }); + + it('should have a "STATIC" property', function() { + expect(SCOPE.PUNC.STATIC).toBeDefined(); + expect(typeof SCOPE.PUNC.STATIC).toBe('string'); + }); + }); + }); + + describe('shorten', function() { + it('should break up a longname into the correct memberof, name and scope parts', function() { + var startName = 'lib.Panel#open'; + var parts = jsdoc.name.shorten(startName); + + expect(parts.name).toEqual('open'); + expect(parts.memberof).toEqual('lib.Panel'); + expect(parts.scope).toEqual('#'); + }); + + it('should work on static names', function() { + var startName = 'elements.selected.getVisible'; + var parts = jsdoc.name.shorten(startName); + + expect(parts.name).toEqual('getVisible'); + expect(parts.memberof).toEqual('elements.selected'); + expect(parts.scope).toEqual('.'); + }); + + it('should work on protoyped names', function() { + var startName = 'Validator.prototype.$element'; + var parts = jsdoc.name.shorten(startName); + + expect(parts.name).toEqual('$element'); + expect(parts.memberof).toEqual('Validator'); + expect(parts.scope).toEqual('#'); + }); + + it('should work on inner names', function() { + var startName = 'Button~_onclick'; + var parts = jsdoc.name.shorten(startName); + + expect(parts.name).toEqual('_onclick'); + expect(parts.memberof).toEqual('Button'); + expect(parts.scope).toEqual('~'); + }); + + it('should work on global names', function() { + var startName = 'close'; + var parts = jsdoc.name.shorten(startName); + + expect(parts.name).toEqual('close'); + expect(parts.memberof).toEqual(''); + expect(parts.scope).toEqual(''); + }); + + it('should work when a single property uses bracket notation', function() { + var startName = 'channels["#ops"]#open'; + var parts = jsdoc.name.shorten(startName); + + expect(parts.name).toEqual('open'); + expect(parts.memberof).toEqual('channels."#ops"'); + expect(parts.scope).toEqual('#'); + }); + + it('should work when consecutive properties use bracket notation', function() { + var startName = 'channels["#bots"]["log.max"]'; + var parts = jsdoc.name.shorten(startName); + + expect(parts.name).toEqual('"log.max"'); + expect(parts.memberof).toEqual('channels."#bots"'); + expect(parts.scope).toEqual('.'); + }); + + it('should work when a property uses single-quoted bracket notation', function() { + var startName = "channels['#ops']"; + var parts = jsdoc.name.shorten(startName); + + expect(parts.name).toBe("'#ops'"); + expect(parts.memberof).toBe('channels'); + expect(parts.scope).toBe('.'); + }); + + it('should work on double-quoted strings', function() { + var startName = '"foo.bar"'; + var parts = jsdoc.name.shorten(startName); + + expect(parts.name).toEqual('"foo.bar"'); + expect(parts.longname).toEqual('"foo.bar"'); + expect(parts.memberof).toEqual(''); + expect(parts.scope).toEqual(''); + }); + + it('should work on single-quoted strings', function() { + var startName = "'foo.bar'"; + var parts = jsdoc.name.shorten(startName); + + expect(parts.name).toBe("'foo.bar'"); + expect(parts.longname).toBe("'foo.bar'"); + expect(parts.memberof).toBe(''); + expect(parts.scope).toBe(''); + }); + + it('should find the variation', function() { + var startName = 'anim.fadein(2)'; + var parts = jsdoc.name.shorten(startName); + + expect(parts.variation).toEqual('2'); + expect(parts.name).toEqual('fadein'); + expect(parts.longname).toEqual('anim.fadein(2)'); + }); + }); + + describe('applyNamespace', function() { + it('should insert the namespace only before the name part of the longname', function() { + var startName = 'lib.Panel#open'; + var endName = jsdoc.name.applyNamespace(startName, 'event'); + + expect(endName, 'lib.Panel#event:open'); + }); + + it('should insert the namespace before a global name', function() { + var startName = 'maths/bigint'; + var endName = jsdoc.name.applyNamespace(startName, 'module'); + + expect(endName, 'module:maths/bigint'); + }); + + it('should treat quoted parts of the name as atomic and insert namespace before a quoted shortname', function() { + var startName = 'foo."*dont\'t.look~in#here!"'; + var endName = jsdoc.name.applyNamespace(startName, 'event'); + + expect(endName, 'foo.event:"*dont\'t.look~in#here!"'); + }); + + it('should not add another namespace if one already exists.', function() { + var startName = 'lib.Panel#event:open'; + var endName = jsdoc.name.applyNamespace(startName, 'event'); + + expect(endName, 'lib.Panel#event:open'); + }); + }); + + describe('splitName', function() { + it('should find the name and description.', function() { + var startName = 'ns.Page#"last \\"sentence\\"".words~sort(2) - This is a description. '; + var parts = jsdoc.name.splitName(startName); + + expect(parts.name, 'ns.Page#"last \\"sentence\\"".words~sort(2)'); + expect(parts.description, 'This is a description.'); + }); + + it('should strip the separator when the separator starts on the same line as the name', function() { + var startName = 'socket - The networking kind, not the wrench.'; + var parts = jsdoc.name.splitName(startName); + + expect(parts.name).toBe('socket'); + expect(parts.description).toBe('The networking kind, not the wrench.'); + }); + + it('should not strip a separator that is preceded by a line break', function() { + var startName = 'socket\n - The networking kind, not the wrench.'; + var parts = jsdoc.name.splitName(startName); + + expect(parts.name).toBe('socket'); + expect(parts.description).toBe('- The networking kind, not the wrench.'); + }); + + it('should allow default values to have brackets', function() { + var startName = '[path=["home", "user"]] - Path split into components' + var parts = jsdoc.name.splitName(startName); + + expect(parts.name).toBe('[path=["home", "user"]]'); + expect(parts.description).toBe('Path split into components'); + }); + + it('should allow default values to have unmatched brackets inside strings', function() { + var startName = '[path=["Unmatched begin: ["]] - Path split into components' + var parts = jsdoc.name.splitName(startName); + + expect(parts.name).toBe('[path=["Unmatched begin: ["]]'); + expect(parts.description).toBe('Path split into components'); + }); + + it('should fail gracefully when the default value has an unmatched bracket', function() { + var startName = '[path=["home", "user"] - Path split into components' + var parts = jsdoc.name.splitName(startName); + + expect(parts).not.toBe(null); + expect(parts.name).toBe('[path=["home", "user"]'); + expect(parts.description).toBe('Path split into components'); + }); + + it('should fail gracefully when the default value has an unmatched quote', function() { + var startName = '[path=["home", "user] - Path split into components' + var parts = jsdoc.name.splitName(startName); + + expect(parts).not.toBe(null); + expect(parts.name).toBe('[path=["home", "user]'); + expect(parts.description).toBe('Path split into components'); + }); + }); + + describe('resolve', function() { + // TODO: further tests (namespaces, modules, ...) + + function makeDoclet(tagStrings) { + var comment = '/**\n' + tagStrings.join('\n') + '\n*/'; + return new jsdoc.doclet.Doclet(comment, {}); + } + + // @event testing. + var event = '@event'; + var memberOf = '@memberof MyClass'; + var name = '@name A'; + + // Test the basic @event that is not nested. + it('unnested @event gets resolved correctly', function() { + var doclet = makeDoclet([event, name]); + var out = jsdoc.name.resolve(doclet); + + expect(doclet.name).toEqual('A'); + expect(doclet.memberof).toBeUndefined(); + expect(doclet.longname).toEqual('event:A'); + }); + + // test all permutations of @event @name [name] @memberof. + it('@event @name @memberof resolves correctly', function() { + var doclet = makeDoclet([event, name, memberOf]); + var out = jsdoc.name.resolve(doclet); + + expect(doclet.name).toEqual('A'); + expect(doclet.memberof).toEqual('MyClass'); + expect(doclet.longname).toEqual('MyClass.event:A'); + }); + it('@event @memberof @name resolves correctly', function() { + var doclet = makeDoclet([event, memberOf, name]); + var out = jsdoc.name.resolve(doclet); + + expect(doclet.name).toEqual('A'); + expect(doclet.memberof).toEqual('MyClass'); + expect(doclet.longname).toEqual('MyClass.event:A'); + }); + it('@name @event @memberof resolves correctly', function() { + var doclet = makeDoclet([name, event, memberOf]); + var out = jsdoc.name.resolve(doclet); + + expect(doclet.name).toEqual('A'); + expect(doclet.memberof).toEqual('MyClass'); + expect(doclet.longname).toEqual('MyClass.event:A'); + }); + it('@name @memberof @event resolves correctly', function() { + var doclet = makeDoclet([name, memberOf, event]); + var out = jsdoc.name.resolve(doclet); + + expect(doclet.name).toEqual('A'); + expect(doclet.memberof).toEqual('MyClass'); + expect(doclet.longname).toEqual('MyClass.event:A'); + }); + it('@memberof @event @name resolves correctly', function() { + var doclet = makeDoclet([memberOf, event, name]); + var out = jsdoc.name.resolve(doclet); + + expect(doclet.name).toEqual('A'); + expect(doclet.memberof).toEqual('MyClass'); + expect(doclet.longname).toEqual('MyClass.event:A'); + }); + it('@memberof @name @event resolves correctly', function() { + var doclet = makeDoclet([memberOf, name, event]); + var out = jsdoc.name.resolve(doclet); + + expect(doclet.name).toEqual('A'); + expect(doclet.memberof).toEqual('MyClass'); + expect(doclet.longname).toEqual('MyClass.event:A'); + }); + + // test all permutations of @event [name] @memberof + it('@event [name] @memberof resolves correctly', function() { + var doclet = makeDoclet(['@event A', memberOf]); + var out = jsdoc.name.resolve(doclet); + + expect(doclet.name).toEqual('A'); + expect(doclet.memberof).toEqual('MyClass'); + expect(doclet.longname).toEqual('MyClass.event:A'); + }); + it('@memberof @event [name] resolves correctly', function() { + var doclet = makeDoclet([memberOf, '@event A']); + var out = jsdoc.name.resolve(doclet); + + expect(doclet.name).toEqual('A'); + expect(doclet.memberof).toEqual('MyClass'); + expect(doclet.longname).toEqual('MyClass.event:A'); + }); + + // test full @event A.B + it('full @event definition works', function() { + var doclet = makeDoclet(['@event MyClass.A']); + var out = jsdoc.name.resolve(doclet); + + expect(doclet.name).toEqual('A'); + expect(doclet.memberof).toEqual('MyClass'); + expect(doclet.longname).toEqual('MyClass.event:A'); + }); + it('full @event definition with event: works', function() { + var doclet = makeDoclet(['@event MyClass.event:A']); + var out = jsdoc.name.resolve(doclet); + + expect(doclet.name).toEqual('event:A'); + expect(doclet.memberof).toEqual('MyClass'); + expect(doclet.longname).toEqual('MyClass.event:A'); + }); + + // a double-nested one just in case + it('@event @name MyClass.EventName @memberof somethingelse works', function() { + var doclet = makeDoclet([event, '@name MyClass.A', '@memberof MyNamespace']); + var out = jsdoc.name.resolve(doclet); + + expect(doclet.name).toEqual('A'); + expect(doclet.memberof).toEqual('MyNamespace.MyClass'); + expect(doclet.longname).toEqual('MyNamespace.MyClass.event:A'); + }); + + // other cases + it('correctly handles a function parameter named "prototype"', function() { + var doclet = makeDoclet([ + '@name Bar.prototype.baz', + '@function', + '@memberof module:foo', + '@param {string} qux' + ]); + var out = jsdoc.name.resolve(doclet); + + expect(doclet.name).toBe('baz'); + expect(doclet.memberof).toBe('module:foo.Bar'); + expect(doclet.longname).toBe('module:foo.Bar#baz'); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/opts/argparser.js b/third_party/jsdoc/test/specs/jsdoc/opts/argparser.js new file mode 100644 index 0000000000..3156a1852b --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/opts/argparser.js @@ -0,0 +1,170 @@ +/*global afterEach, beforeEach, describe, expect, it */ +'use strict'; + +describe('jsdoc/opts/argparser', function() { + var ArgParser = require('jsdoc/opts/argparser'); + var argParser; + var ourOptions; + + function trueFalse(v) { + var r = false; + if (v) { + if (v === 'true') { r = true; } + else if (v === 'false') { r = false; } + else { v = !!r; } + } + + return r; + } + + beforeEach(function() { + argParser = new ArgParser() + .addOption('s', 'strict', true, 'Throw error on invalid input.', false, trueFalse) + .addOption('n', 'name', true, 'The name of the project.', false); + + ourOptions = argParser.parse(['-s', 'true', '-n', 'true']); + }); + + it('should exist', function() { + expect(ArgParser).toBeDefined(); + }); + + it('should be a constructor', function() { + expect(typeof ArgParser).toBe('function'); + expect(new ArgParser() instanceof ArgParser).toBe(true); + }); + + describe('ArgParser', function() { + it('should provide an "addIgnoredOption" method', function() { + expect(argParser.addIgnoredOption).toBeDefined(); + expect(typeof argParser.addIgnoredOption).toBe('function'); + }); + + it('should provide an "addOption" method', function() { + expect(argParser.addOption).toBeDefined(); + expect(typeof argParser.addOption).toBe('function'); + }); + + it('should provide a "help" method', function() { + expect(argParser.help).toBeDefined(); + expect(typeof argParser.help).toBe('function'); + }); + + it('should provide a "parse" method', function() { + expect(argParser.parse).toBeDefined(); + expect(typeof argParser.parse).toBe('function'); + }); + + describe('addIgnoredOption', function() { + it('should be chainable', function() { + expect(argParser.addIgnoredOption({})).toBe(argParser); + }); + }); + + describe('addOption', function() { + it('should be chainable', function() { + expect(argParser.addOption('a', null, false, 'Option')).toBe(argParser); + }); + }); + + describe('help', function() { + var columns = process.stdout.columns; + + beforeEach(function() { + process.stdout.columns = 80; + }); + + afterEach(function() { + process.stdout.columns = columns; + }); + + it('should format the help text correctly', function() { + var helpText = [ + 'Options:', + ' --noshortname Just a long name.', + ' -o Only a short name.', + ' -s, --supercalifragilisticexpialidocious If you say it loud enough,', + " you'll always sound", + ' precocious.' + ].join('\n'); + + argParser = new ArgParser() + .addIgnoredOption('m', 'meh', false, 'Ignore me.') + .addOption(null, 'noshortname', false, 'Just a long name.') + .addOption('o', null, true, 'Only a short name.') + .addOption('s', 'supercalifragilisticexpialidocious', false, + "If you say it loud enough, you'll always sound precocious."); + + expect(argParser.help()).toBe(helpText); + }); + }); + + describe('parse', function() { + it('should return an object with information about the options', function() { + expect(typeof ourOptions).toBe('object'); + expect(ourOptions.strict).toBe(true); + expect(ourOptions.name).toBe('true'); + }); + + it('should merge the defaults into the command-line options', function() { + var defaults = { + strict: false, + name: 'Hello world!' + }; + + ourOptions = argParser.parse(['-s', true], defaults); + + expect(ourOptions.strict).toBe(true); + expect(ourOptions.name).toBe(defaults.name); + }); + + it('should recognize options that can be used more than once', function() { + argParser.addOption(null, 'multi', true, '', true); + + ourOptions = argParser.parse(['--multi', 'value1', '--multi', 'value2', + '--multi', 'value3']); + expect(Array.isArray(ourOptions.multi)).toBe(true); + expect(ourOptions.multi.length).toBe(3); + expect(ourOptions.multi[0]).toBe('value1'); + expect(ourOptions.multi[1]).toBe('value2'); + expect(ourOptions.multi[2]).toBe('value3'); + }); + + it('should throw an error if an unrecognized short option is used', function() { + function badShortOption() { + argParser.parse(['-w']); + } + + expect(badShortOption).toThrow(); + }); + + it('should throw an error if an unrecognized long option is used', function() { + function badLongOption() { + argParser.parse(['--whatever']); + } + + expect(badLongOption).toThrow(); + }); + + it('should throw an error if a required value is missing', function() { + function missingValue() { + argParser.parse(['--requires-value']); + } + + argParser.addOption(null, 'requires-value', true, ''); + + expect(missingValue).toThrow(); + }); + + it('should coerce a true value if a coercer is provided', function() { + expect(ourOptions.strict).toBeDefined(); + expect(ourOptions.strict).toBe(true); + }); + + it('should coerce a string value if no coercer is provided', function() { + expect(ourOptions.name).toBeDefined(); + expect(ourOptions.name).toBe('true'); + }); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/opts/args.js b/third_party/jsdoc/test/specs/jsdoc/opts/args.js new file mode 100644 index 0000000000..16cd77baff --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/opts/args.js @@ -0,0 +1,303 @@ +/*global describe, expect, it */ +'use strict'; + +describe('jsdoc/opts/args', function() { + var args = require('jsdoc/opts/args'); + var querystring = require('querystring'); + + it('should exist', function() { + expect(args).toBeDefined(); + expect(typeof args).toBe('object'); + }); + + it('should export a "parse" function', function() { + expect(args.parse).toBeDefined(); + expect(typeof args.parse).toBe('function'); + }); + + it('should export a "help" function', function() { + expect(args.help).toBeDefined(); + expect(typeof args.help).toBe('function'); + }); + + it('should export a "get" function', function() { + expect(args.get).toBeDefined(); + expect(typeof args.get).toBe('function'); + }); + + describe('parse', function() { + it('should accept a "-t" option and return an object with a "template" property', function() { + args.parse(['-t', 'mytemplate']); + var r = args.get(); + + expect(r.template).toBe('mytemplate'); + }); + + it('should accept a "--template" option and return an object with a "template" property', function() { + args.parse(['--template', 'mytemplate']); + var r = args.get(); + + expect(r.template).toBe('mytemplate'); + }); + + it('should accept a "-c" option and return an object with a "configure" property', function() { + args.parse(['-c', 'myconf.json']); + var r = args.get(); + + expect(r.configure).toBe('myconf.json'); + }); + + it('should accept a "--configure" option and return an object with a "configure" property', function() { + args.parse(['--configure', 'myconf.json']); + var r = args.get(); + + expect(r.configure).toBe('myconf.json'); + }); + + it('should accept an "-e" option and return an object with a "encoding" property', function() { + args.parse(['-e', 'ascii']); + var r = args.get(); + + expect(r.encoding).toBe('ascii'); + }); + + it('should accept an "--encoding" option and return an object with an "encoding" property', function() { + args.parse(['--encoding', 'ascii']); + var r = args.get(); + + expect(r.encoding).toBe('ascii'); + }); + + it('should accept a "-T" option and return an object with a "test" property', function() { + args.parse(['-T']); + var r = args.get(); + + expect(r.test).toBe(true); + }); + + it('should accept a "--test" option and return an object with a "test" property', function() { + args.parse(['--test']); + var r = args.get(); + + expect(r.test).toBe(true); + }); + + it('should accept a "-d" option and return an object with a "destination" property', function() { + args.parse(['-d', 'mydestination']); + var r = args.get(); + + expect(r.destination).toBe('mydestination'); + }); + + it('should accept a "--destination" option and return an object with a "destination" property', function() { + args.parse(['--destination', 'mydestination']); + var r = args.get(); + + expect(r.destination).toBe('mydestination'); + }); + + it('should accept a "-p" option and return an object with a "private" property', function() { + args.parse(['-p']); + var r = args.get(); + + expect(r.private).toBe(true); + }); + + it('should accept a "--private" option and return an object with a "private" property', function() { + args.parse(['--private']); + var r = args.get(); + + expect(r.private).toBe(true); + }); + + it('should accept a "-r" option and return an object with a "recurse" property', function() { + args.parse(['-r']); + var r = args.get(); + + expect(r.recurse).toBe(true); + }); + + it('should accept a "--recurse" option and return an object with a "recurse" property', function() { + args.parse(['--recurse']); + var r = args.get(); + + expect(r.recurse).toBe(true); + }); + + it('should accept a "-l" option and ignore it', function() { + args.parse(['-l']); + var r = args.get(); + + expect(r.lenient).not.toBeDefined(); + }); + + it('should accept a "--lenient" option and ignore it', function() { + args.parse(['--lenient']); + var r = args.get(); + + expect(r.lenient).not.toBeDefined(); + }); + + it('should accept a "-h" option and return an object with a "help" property', function() { + args.parse(['-h']); + var r = args.get(); + + expect(r.help).toBe(true); + }); + + it('should accept a "--help" option and return an object with a "help" property', function() { + args.parse(['--help']); + var r = args.get(); + + expect(r.help).toBe(true); + }); + + it('should accept an "-X" option and return an object with an "explain" property', function() { + args.parse(['-X']); + var r = args.get(); + + expect(r.explain).toBe(true); + }); + + it('should accept an "--explain" option and return an object with an "explain" property', function() { + args.parse(['--explain']); + var r = args.get(); + + expect(r.explain).toBe(true); + }); + + it('should accept a "-q" option and return an object with a "query" property', function() { + args.parse(['-q', 'foo=bar&fab=baz']); + var r = args.get(); + + expect(r.query).toEqual({ foo: 'bar', fab: 'baz' }); + }); + + it('should accept a "--query" option and return an object with a "query" property', function() { + args.parse(['--query', 'foo=bar&fab=baz']); + var r = args.get(); + + expect(r.query).toEqual({ foo: 'bar', fab: 'baz' }); + }); + + it('should use type coercion on the "query" property so it has real booleans and numbers', function() { + var obj = { + foo: 'fab', + bar: true, + baz: false, + qux: [1, -97] + }; + args.parse(['-q', querystring.stringify(obj)]); + var r = args.get(); + + expect(r.query).toEqual(obj); + }); + + it('should accept a "-u" option and return an object with a "tutorials" property', function() { + args.parse(['-u', 'mytutorials']); + var r = args.get(); + + expect(r.tutorials).toBe('mytutorials'); + }); + + it('should accept a "--tutorials" option and return an object with a "tutorials" property', function() { + args.parse(['--tutorials', 'mytutorials']); + var r = args.get(); + + expect(r.tutorials).toBe('mytutorials'); + }); + + it('should accept a "--debug" option and return an object with a "debug" property', function() { + args.parse(['--debug']); + var r = args.get(); + + expect(r.debug).toBe(true); + }); + + it('should accept a "--verbose" option and return an object with a "verbose" property', function() { + args.parse(['--verbose']); + var r = args.get(); + + expect(r.verbose).toBe(true); + }); + + it('should accept a "--pedantic" option and return an object with a "pedantic" property', function() { + args.parse(['--pedantic']); + var r = args.get(); + + expect(r.pedantic).toBe(true); + }); + + it('should accept a "--match" option and return an object with a "match" property', function() { + args.parse(['--match', '.*tag']); + var r = args.get(); + + expect(r.match).toBe('.*tag'); + }); + + it('should accept multiple "--match" options and return an object with a "match" property', function() { + args.parse(['--match', '.*tag', '--match', 'parser']); + var r = args.get(); + + expect(r.match).toEqual(['.*tag', 'parser']); + }); + + it('should accept a "--nocolor" option and return an object with a "nocolor" property', function() { + args.parse(['--nocolor']); + var r = args.get(); + + expect(r.nocolor).toBe(true); + }); + + it('should accept a "-P" option and return an object with a "package" property', function() { + args.parse(['-P', 'path/to/package/file.json']); + var r = args.get(); + + expect(r.package).toBe('path/to/package/file.json'); + }); + + it('should accept a "--package" option and return an object with a "package" property', function() { + args.parse(['--package', 'path/to/package/file.json']); + var r = args.get(); + + expect(r.package).toBe('path/to/package/file.json'); + }); + + it('should accept a "-R" option and return an object with a "readme" property', function() { + args.parse(['-R', 'path/to/readme/file.md']); + var r = args.get(); + + expect(r.readme).toBe('path/to/readme/file.md'); + }); + + it('should accept a "--readme" option and return an object with a "readme" property', function() { + args.parse(['--readme', 'path/to/readme/file.md']); + var r = args.get(); + + expect(r.readme).toBe('path/to/readme/file.md'); + }); + + it('should accept a "-v" option and return an object with a "version" property', function() { + args.parse(['-v']); + var r = args.get(); + + expect(r.version).toBe(true); + }); + + it('should accept a "--version" option and return an object with a "version" property', function() { + args.parse(['--version']); + var r = args.get(); + + expect(r.version).toBe(true); + }); + + it('should accept a naked option (with no "-") and return an object with a "_" property', function() { + args.parse(['myfile1', 'myfile2']); + var r = args.get(); + + expect(r._).toEqual(['myfile1', 'myfile2']); + }); + + //TODO: tests for args that must have values + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/package.js b/third_party/jsdoc/test/specs/jsdoc/package.js new file mode 100644 index 0000000000..090cef6599 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/package.js @@ -0,0 +1,264 @@ +/*global beforeEach, describe, expect, it, jasmine, spyOn */ +'use strict'; + +var hasOwnProp = Object.prototype.hasOwnProperty; + +describe('jsdoc/package', function() { + var emptyPackage; + var jsdocPackage = require('jsdoc/package'); + var logger = require('jsdoc/util/logger'); + var Package = jsdocPackage.Package; + + function checkPackageProperty(name, value) { + var myPackage; + var obj = {}; + + obj[name] = value; + myPackage = new Package( JSON.stringify(obj) ); + // add the package object to the cached parse results, so we can validate it against the + // doclet schema + jasmine.addParseResults('package-property-' + name + '.js', [myPackage]); + + // use toEqual so we can test array/object values + expect(myPackage[name]).toEqual(value); + } + + it('should exist', function() { + expect(jsdocPackage).toBeDefined(); + expect(typeof jsdocPackage).toBe('object'); + }); + + it('should export a "Package" constructor', function() { + expect(Package).toBeDefined(); + expect(typeof Package).toBe('function'); + }); + + describe('Package', function() { + beforeEach(function() { + emptyPackage = new Package(); + }); + + it('should accept a JSON-format string', function() { + function newPackage() { + return new Package('{"foo": "bar"}'); + } + + expect(newPackage).not.toThrow(); + }); + + it('should work when called with no arguments', function() { + function newPackage() { + return new Package(); + } + + expect(newPackage).not.toThrow(); + }); + + it('should log an error when called with bad input', function() { + function newPackage() { + return new Package('abcdefg'); + } + + spyOn(logger, 'error'); + + expect(newPackage).not.toThrow(); + expect(logger.error).toHaveBeenCalled(); + }); + + describe('author', function() { + it('should not exist by default', function() { + expect( hasOwnProp.call(emptyPackage, 'author') ).toBe(false); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('author', { name: 'Jane Smith', email: 'jsmith@example.com' }); + }); + }); + + describe('bugs', function() { + it('should not exist by default', function() { + expect( hasOwnProp.call(emptyPackage, 'bugs') ).toBe(false); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('bugs', { url: 'http://example.com/bugs' }); + }); + }); + + describe('contributors', function() { + it('should not exist by default', function() { + expect( hasOwnProp.call(emptyPackage, 'contributors') ).toBe(false); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('contributors', [{ + name: 'Jane Smith', + email: 'jsmith@example.com' + }]); + }); + }); + + describe('dependencies', function() { + it('should not exist by default', function() { + expect( hasOwnProp.call(emptyPackage, 'dependencies') ).toBe(false); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('dependencies', { bar: '~1.1.0' }); + }); + }); + + describe('description', function() { + it('should not exist by default', function() { + expect( hasOwnProp.call(emptyPackage, 'description') ).toBe(false); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('description', 'My package.'); + }); + }); + + describe('devDependencies', function() { + it('should not exist by default', function() { + expect( hasOwnProp.call(emptyPackage, 'devDependencies') ).toBe(false); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('devDependencies', { baz: '~3.4.5' }); + }); + }); + + describe('engines', function() { + it('should not exist by default', function() { + expect( hasOwnProp.call(emptyPackage, 'engines') ).toBe(false); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('engines', { node: '>=0.10.3' }); + }); + }); + + describe('files', function() { + it('should contain an empty array by default', function() { + expect(emptyPackage.files).toBeDefined(); + expect(emptyPackage.files).toEqual([]); + }); + + it('should ignore the value from the package file', function() { + var myPackage = new Package('{"files": ["foo", "bar"]}'); + + expect(myPackage.files.length).toBe(0); + }); + }); + + describe('homepage', function() { + it('should not exist by default', function() { + expect( hasOwnProp.call(emptyPackage, 'homepage') ).toBe(false); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('homepage', 'http://example.com/'); + }); + }); + + describe('keywords', function() { + it('should not exist by default', function() { + expect( hasOwnProp.call(emptyPackage, 'keywords') ).toBe(false); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('keywords', ['foo', 'bar']); + }); + }); + + describe('licenses', function() { + it('should not exist by default', function() { + expect( hasOwnProp.call(emptyPackage, 'licenses') ).toBe(false); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('licenses', [{ + type: 'My Open-Source License', + url: 'http://example.com/oss' + }]); + }); + + it('should contain the value of "license" from the package file', function() { + var myPackage = new Package('{"license": "My-OSS-License"}'); + + expect(myPackage.license).not.toBeDefined(); + expect(myPackage.licenses).toBeDefined(); + expect(myPackage.licenses.length).toBe(1); + expect(myPackage.licenses[0].type).toBe('My-OSS-License'); + }); + + it('should combine the "license" and "licenses" properties', function() { + var packageInfo = { + license: 'My-OSS-License', + licenses: [{ + type: 'My Open-Source License', + url: 'http://example.com/oss' + }] + }; + var myPackage = new Package( JSON.stringify(packageInfo) ); + + expect(myPackage.licenses.length).toBe(2); + }); + }); + + describe('longname', function() { + it('should default to "package:undefined"', function() { + expect(emptyPackage.longname).toBe('package:undefined'); + }); + + it('should reflect the value of the "name" property', function() { + var myPackage = new Package('{"name": "foo"}'); + + expect(myPackage.longname).toBe('package:foo'); + }); + }); + + describe('main', function() { + it('should not exist by default', function() { + expect( hasOwnProp.call(emptyPackage, 'main') ).toBe(false); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('main', 'foo'); + }); + }); + + describe('name', function() { + it('should not exist by default', function() { + expect( hasOwnProp.call(emptyPackage, 'name') ).toBe(false); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('name', 'foo'); + }); + }); + + describe('repository', function() { + it('should not exist by default', function() { + expect( hasOwnProp.call(emptyPackage, 'repository') ).toBe(false); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('repository', { + type: 'git', + url: 'git@example.org:foo/bar/baz.git' + }); + }); + }); + + describe('version', function() { + it('should not exist by default', function() { + expect( hasOwnProp.call(emptyPackage, 'version') ).toBe(false); + }); + + it('should contain the value from the package file', function() { + checkPackageProperty('version', '0.1.2'); + }); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/path.js b/third_party/jsdoc/test/specs/jsdoc/path.js new file mode 100644 index 0000000000..24ed9aeb09 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/path.js @@ -0,0 +1,137 @@ +/*global afterEach: true, beforeEach: true, describe: true, expect: true, it: true, spyOn: true, +xdescribe: true */ + +describe('jsdoc/path', function() { + var os = require('os'); + var path = require('jsdoc/path'); + var standardPath = require('path'); + + var isWindows = /^win/.test( os.platform() ); + + it('should exist', function() { + expect(path).toBeDefined(); + expect(typeof path).toEqual('object'); + }); + + it('should export all functions in the "path" module', function() { + Object.keys(standardPath).forEach(function(item) { + if (typeof standardPath[item] === 'function') { + expect(path[item]).toBeDefined(); + expect(typeof path[item]).toEqual('function'); + } + }); + }); + + it('should export a "commonPrefix" function', function() { + expect(path.commonPrefix).toBeDefined(); + expect(typeof path.commonPrefix).toEqual('function'); + }); + + it('should export a "getResourcePath" function', function() { + expect(path.getResourcePath).toBeDefined(); + expect(typeof path.getResourcePath).toEqual('function'); + }); + + describe('commonPrefix', function() { + var oldPwd; + var cwd; + + beforeEach(function() { + oldPwd = global.env.pwd; + global.env.pwd = isWindows ? 'C:\\Users\\jsdoc' : '/Users/jsdoc'; + cwd = global.env.pwd.split(path.sep); + }); + + afterEach(function() { + global.env.pwd = oldPwd; + }); + + it('finds the correct prefix for a single relative path', function() { + var paths = [path.join('foo', 'bar', 'baz', 'qux.js')]; + // we expect a trailing slash + var expected = cwd.concat('foo', 'bar', 'baz', '').join(path.sep); + + expect( path.commonPrefix(paths) ).toBe(expected); + }); + + it('finds the correct prefix for a group of relative paths', function() { + var paths = [ + path.join('foo', 'bar', 'baz', 'qux.js'), + path.join('foo', 'bar', 'baz', 'quux.js'), + path.join('foo', 'bar', 'baz.js') + ]; + // we expect a trailing slash + var expected = cwd.concat('foo', 'bar', '').join(path.sep); + + expect( path.commonPrefix(paths) ).toEqual(expected); + }); + + it('finds the correct prefix for a single absolute path', function() { + var paths = [cwd.concat('foo', 'bar', 'baz', 'qux.js').join(path.sep)]; + // we expect a trailing slash + var expected = cwd.concat('foo', 'bar', 'baz', '').join(path.sep); + + expect( path.commonPrefix(paths) ).toBe(expected); + }); + + it('finds the correct prefix for a group of absolute paths', function() { + var paths = [ + cwd.concat('foo', 'bar', 'baz', 'qux.js').join(path.sep), + cwd.concat('foo', 'bar', 'baz', 'quux.js').join(path.sep), + cwd.concat('foo', 'bar', 'baz.js').join(path.sep) + ]; + // we expect a trailing slash + var expected = cwd.concat('foo', 'bar', '').join(path.sep); + + expect( path.commonPrefix(paths) ).toEqual(expected); + }); + + it('finds the correct prefix for a group of absolute paths and dotted relative paths', + function() { + var paths = [ + path.join('..', 'jsdoc', 'foo', 'bar', 'baz', 'qux', 'quux', 'test.js'), + cwd.concat('foo', 'bar', 'bazzy.js').join(path.sep), + path.join('..', '..', 'Users', 'jsdoc', 'foo', 'bar', 'foobar.js') + ]; + // we expect a trailing slash + var expected = cwd.concat('foo', 'bar', '').join(path.sep); + + expect( path.commonPrefix(paths) ).toEqual(expected); + }); + + it('returns an empty string when the paths array is empty', function() { + var paths = []; + + expect( path.commonPrefix(paths) ).toBe(''); + }); + + // skip on Windows, since the paths share a drive letter at the start + if (!isWindows) { + it('returns an empty string when there is no common prefix', function() { + var paths = [ + path.join('foo', 'bar', 'baz', 'qux.js'), + path.join('..', '..', 'Library', 'foo', 'bar', 'baz.js') + ]; + + expect( path.commonPrefix(paths) ).toEqual(''); + }); + } + + // only test Windows paths on Windows + if (isWindows) { + it('works with Windows paths that contain spaces', function() { + var prefix = 'C:\\Users\\Jane Smith\\myproject\\'; + var paths = [ + prefix + 'index.js', + prefix + 'lib\\mymodule.js' + ]; + + expect( path.commonPrefix(paths) ).toBe(prefix); + }); + } + }); + + xdescribe('getResourcePath', function() { + // TODO + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/plugins.js b/third_party/jsdoc/test/specs/jsdoc/plugins.js new file mode 100644 index 0000000000..d634ddabbd --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/plugins.js @@ -0,0 +1,19 @@ +/*global describe: true, expect: true, it: true, xdescribe: true */ + +describe("jsdoc/plugins", function() { + var plugins = require('jsdoc/plugins'); + + it("should exist", function() { + expect(plugins).toBeDefined(); + expect(typeof plugins).toEqual('object'); + }); + + it("should export an 'installPlugins' function", function() { + expect(plugins.installPlugins).toBeDefined(); + expect(typeof plugins.installPlugins).toEqual('function'); + }); + + xdescribe("installPlugins", function() { + // TODO + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/readme.js b/third_party/jsdoc/test/specs/jsdoc/readme.js new file mode 100644 index 0000000000..51af2f7513 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/readme.js @@ -0,0 +1,14 @@ +describe("jsdoc/readme", function() { + var jsdoc = {readme: require('jsdoc/readme') }, + html = (new jsdoc.readme('test/fixtures/markdowntest.md')).html; + + it("should parse html out of markdown", function() { + expect(html).toBeDefined(); + expect(typeof html).toEqual("string"); + expect(html).toContain(''); + expect(html).toContain('

      '); + expect(html).toContain('

      '); + expect(html).toContain('

    • '); + }); + +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/jsdoc/schema.js b/third_party/jsdoc/test/specs/jsdoc/schema.js new file mode 100644 index 0000000000..8f52d8751b --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/schema.js @@ -0,0 +1,87 @@ +/*global describe, expect, it, jasmine */ +'use strict'; + +describe('jsdoc/schema', function() { + var schema = require('jsdoc/schema'); + + it('should exist', function() { + expect(schema).toBeDefined(); + expect(typeof schema).toBe('object'); + }); + + it('should export a "BUGS_SCHEMA" object', function() { + expect(schema.BUGS_SCHEMA).toBeDefined(); + expect(typeof schema.BUGS_SCHEMA).toBe('object'); + }); + + it('should export a "CONTACT_INFO_SCHEMA" object', function() { + expect(schema.CONTACT_INFO_SCHEMA).toBeDefined(); + expect(typeof schema.CONTACT_INFO_SCHEMA).toBe('object'); + }); + + it('should export a "DOCLET_SCHEMA" object', function() { + expect(schema.DOCLET_SCHEMA).toBeDefined(); + expect(typeof schema.DOCLET_SCHEMA).toBe('object'); + }); + + it('should export a "DOCLETS_SCHEMA" object', function() { + expect(schema.DOCLETS_SCHEMA).toBeDefined(); + expect(typeof schema.DOCLETS_SCHEMA).toBe('object'); + }); + + it('should export an "ENUM_PROPERTY_SCHEMA" object', function() { + expect(schema.ENUM_PROPERTY_SCHEMA).toBeDefined(); + expect(typeof schema.ENUM_PROPERTY_SCHEMA).toBe('object'); + }); + + it('should export a "META_SCHEMA" object', function() { + expect(schema.META_SCHEMA).toBeDefined(); + expect(typeof schema.META_SCHEMA).toBe('object'); + }); + + it('should export a "PACKAGE_SCHEMA" object', function() { + expect(schema.PACKAGE_SCHEMA).toBeDefined(); + expect(typeof schema.PACKAGE_SCHEMA).toBe('object'); + }); + + it('should export a "PARAM_SCHEMA" object', function() { + expect(schema.PARAM_SCHEMA).toBeDefined(); + expect(typeof schema.PARAM_SCHEMA).toBe('object'); + }); + + it('should export a "TYPE_PROPERTY_SCHEMA" object', function() { + expect(schema.TYPE_PROPERTY_SCHEMA).toBeDefined(); + expect(typeof schema.TYPE_PROPERTY_SCHEMA).toBe('object'); + }); + + describe('validation', function() { + var validate = require('tv4').validateMultiple; + + it('should find validation errors in bogus input', function() { + var doclets = [ + { + foo: 'bar' + } + ]; + var validationResult = validate(doclets, schema.DOCLETS_SCHEMA); + + expect(validationResult.valid).toBe(false); + }); + + it('should not find any validation errors in the JSDoc parse results', function() { + jasmine.getParseResults().forEach(function(doclets) { + var validationResult; + validationResult = validate(doclets.doclets, schema.DOCLETS_SCHEMA); + + // hack to get the filename/errors in the test results + if (validationResult.errors.length) { + expect(doclets.filename).toBe(''); + expect(validationResult.errors).toEqual([]); + } + else { + expect(validationResult.errors.length).toBe(0); + } + }); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/src/astbuilder.js b/third_party/jsdoc/test/specs/jsdoc/src/astbuilder.js new file mode 100644 index 0000000000..9d6392135f --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/src/astbuilder.js @@ -0,0 +1,46 @@ +/*global beforeEach, describe, expect, it, spyOn, xdescribe */ +describe('jsdoc/src/astbuilder', function() { + var astbuilder = require('jsdoc/src/astbuilder'); + + it('should exist', function() { + expect(astbuilder).toBeDefined(); + expect(typeof astbuilder).toBe('object'); + }); + + it('should export an AstBuilder class', function() { + expect(astbuilder.AstBuilder).toBeDefined(); + expect(typeof astbuilder.AstBuilder).toBe('function'); + }); + + describe('AstBuilder', function() { + // TODO: more tests + var builder; + + beforeEach(function() { + builder = new astbuilder.AstBuilder(); + }); + + it('should provide a "build" method', function() { + expect(builder.build).toBeDefined(); + expect(typeof builder.build).toBe('function'); + }); + + describe('build', function() { + // TODO: more tests + var logger = require('jsdoc/util/logger'); + + beforeEach(function() { + spyOn(logger, 'error'); + }); + + it('should log (not throw) an error when a file cannot be parsed', function() { + function parse() { + builder.build('if (true) { return; }', 'bad.js'); + } + + expect(parse).not.toThrow(); + expect(logger.error).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/src/astnode.js b/third_party/jsdoc/test/specs/jsdoc/src/astnode.js new file mode 100644 index 0000000000..7f17ed60eb --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/src/astnode.js @@ -0,0 +1,584 @@ +/*global afterEach, beforeEach, describe, env, expect, it */ +'use strict'; + +describe('jsdoc/src/astnode', function() { + var astnode = require('jsdoc/src/astnode'); + var doop = require('jsdoc/util/doop'); + var esprima = require('esprima'); + var Syntax = require('jsdoc/src/syntax').Syntax; + + // we need this in Esprima <1.1.0 for some node types + var opts = { + raw: true + }; + + // create the AST nodes we'll be testing + var arrayExpression = esprima.parse('[,]').body[0].expression; + var assignmentExpression = esprima.parse('foo = 1;').body[0].expression; + var binaryExpression = esprima.parse('foo & foo;').body[0].expression; + var functionDeclaration1 = esprima.parse('function foo() {}').body[0]; + var functionDeclaration1a = esprima.parse('function bar() {}').body[0]; + var functionDeclaration2 = esprima.parse('function foo(bar) {}').body[0]; + var functionDeclaration3 = esprima.parse('function foo(bar, baz, qux) {}').body[0]; + var functionExpression1 = esprima.parse('var foo = function() {};').body[0].declarations[0] + .init; + var functionExpression2 = esprima.parse('var foo = function(bar) {};').body[0].declarations[0] + .init; + var identifier = esprima.parse('foo;').body[0].expression; + var literal = esprima.parse('1;').body[0].expression; + var memberExpression = esprima.parse('foo.bar;').body[0].expression; + var memberExpressionComputed1 = esprima.parse('foo["bar"];', opts).body[0].expression; + var memberExpressionComputed2 = esprima.parse('foo[\'bar\'];', opts).body[0].expression; + var propertyGet = esprima.parse('var foo = { get bar() {} };').body[0].declarations[0].init + .properties[0]; + var propertyInit = esprima.parse('var foo = { bar: {} };').body[0].declarations[0].init + .properties[0]; + var propertySet = esprima.parse('var foo = { set bar(a) {} };').body[0].declarations[0].init + .properties[0]; + var thisExpression = esprima.parse('this;').body[0].expression; + var unaryExpression1 = esprima.parse('+1;').body[0].expression; + var unaryExpression2 = esprima.parse('+foo;').body[0].expression; + var variableDeclaration1 = esprima.parse('var foo = 1;').body[0]; + var variableDeclaration2 = esprima.parse('var foo = 1, bar = 2;').body[0]; + var variableDeclarator1 = esprima.parse('var foo = 1;').body[0].declarations[0]; + var variableDeclarator2 = esprima.parse('var foo;').body[0].declarations[0]; + + it('should exist', function() { + expect(astnode).toBeDefined(); + expect(typeof astnode).toBe('object'); + }); + + it('should export a nodeToString method', function() { + expect(astnode.nodeToString).toBeDefined(); + expect(typeof astnode.nodeToString).toBe('function'); + }); + + it('should export an addNodeProperties method', function() { + expect(astnode.addNodeProperties).toBeDefined(); + expect(typeof astnode.addNodeProperties).toBe('function'); + }); + + it('should export a getInfo method', function() { + expect(astnode.getInfo).toBeDefined(); + expect(typeof astnode.getInfo).toBe('function'); + }); + + it('should export a getParamNames method', function() { + expect(astnode.getParamNames).toBeDefined(); + expect(typeof astnode.getParamNames).toBe('function'); + }); + + it('should export an isAccessor method', function() { + expect(astnode.isAccessor).toBeDefined(); + expect(typeof astnode.isAccessor).toBe('function'); + }); + + it('should export an isAssignment method', function() { + expect(astnode.isAssignment).toBeDefined(); + expect(typeof astnode.isAssignment).toBe('function'); + }); + + it('should export an isScope method', function() { + expect(astnode.isScope).toBeDefined(); + expect(typeof astnode.isScope).toBe('function'); + }); + + describe('addNodeProperties', function() { + var debugEnabled; + + beforeEach(function() { + debugEnabled = !!global.env.opts.debug; + }); + + afterEach(function() { + global.env.opts.debug = debugEnabled; + }); + + it('should return null for undefined input', function() { + expect( astnode.addNodeProperties() ).toBe(null); + }); + + it('should return null if the input is not an object', function() { + expect( astnode.addNodeProperties('foo') ).toBe(null); + }); + + it('should preserve existing properties that are not "node properties"', function() { + var node = astnode.addNodeProperties({foo: 1}); + + expect(typeof node).toBe('object'); + expect(node.foo).toBe(1); + }); + + it('should add a non-enumerable nodeId if necessary', function() { + var node = astnode.addNodeProperties({}); + var descriptor = Object.getOwnPropertyDescriptor(node, 'nodeId'); + + expect(descriptor).toBeDefined(); + expect(typeof descriptor.value).toBe('string'); + expect(descriptor.enumerable).toBe(false); + }); + + it('should not overwrite an existing nodeId', function() { + var nodeId = 'foo'; + var node = astnode.addNodeProperties({nodeId: nodeId}); + + expect(node.nodeId).toBe(nodeId); + }); + + it('should add an enumerable nodeId in debug mode', function() { + var descriptor; + var node; + + global.env.opts.debug = true; + node = astnode.addNodeProperties({}); + descriptor = Object.getOwnPropertyDescriptor(node, 'nodeId'); + + expect(descriptor.enumerable).toBe(true); + }); + + it('should add a non-enumerable, writable parent if necessary', function() { + var node = astnode.addNodeProperties({}); + var descriptor = Object.getOwnPropertyDescriptor(node, 'parent'); + + expect(descriptor).toBeDefined(); + expect(descriptor.value).toBe(undefined); + expect(descriptor.enumerable).toBe(false); + expect(descriptor.writable).toBe(true); + }); + + it('should not overwrite an existing parent', function() { + var parent = {}; + var node = astnode.addNodeProperties({parent: parent}); + + expect(node.parent).toBe(parent); + }); + + it('should not overwrite a null parent', function() { + var node = astnode.addNodeProperties({parent: null}); + + expect(node.parent).toBe(null); + }); + + it('should add an enumerable parentId in debug mode', function() { + var descriptor; + var node; + + global.env.opts.debug = true; + node = astnode.addNodeProperties({}); + descriptor = Object.getOwnPropertyDescriptor(node, 'parentId'); + + expect(descriptor).toBeDefined(); + expect(descriptor.enumerable).toBe(true); + }); + + it('should provide a null parentId in debug mode for nodes with no parent', function() { + var descriptor; + var node; + + global.env.opts.debug = true; + node = astnode.addNodeProperties({}); + + expect(node.parentId).toBe(null); + }); + + it('should provide a non-null parentId in debug mode for nodes with a parent', function() { + var descriptor; + var node; + var parent; + + global.env.opts.debug = true; + node = astnode.addNodeProperties({}); + parent = astnode.addNodeProperties({}); + node.parent = parent; + + expect(node.parentId).toBe(parent.nodeId); + }); + + it('should add a non-enumerable, writable enclosingScope if necessary', function() { + var node = astnode.addNodeProperties({}); + var descriptor = Object.getOwnPropertyDescriptor(node, 'enclosingScope'); + + expect(descriptor).toBeDefined(); + expect(descriptor.value).toBe(undefined); + expect(descriptor.enumerable).toBe(false); + expect(descriptor.writable).toBe(true); + }); + + it('should not overwrite an existing enclosingScope', function() { + var enclosingScope = {}; + var node = astnode.addNodeProperties({enclosingScope: enclosingScope}); + + expect(node.enclosingScope).toBe(enclosingScope); + }); + + it('should not overwrite a null enclosingScope', function() { + var node = astnode.addNodeProperties({enclosingScope: null}); + + expect(node.enclosingScope).toBe(null); + }); + + it('should add an enumerable enclosingScopeId in debug mode', function() { + var descriptor; + var node; + + global.env.opts.debug = true; + node = astnode.addNodeProperties({}); + descriptor = Object.getOwnPropertyDescriptor(node, 'enclosingScopeId'); + + expect(descriptor).toBeDefined(); + expect(descriptor.enumerable).toBe(true); + }); + + it('should provide a null enclosingScopeId in debug mode for nodes with no enclosing scope', + function() { + var descriptor; + var node; + + global.env.opts.debug = true; + node = astnode.addNodeProperties({}); + + expect(node.enclosingScopeId).toBe(null); + }); + + it('should provide a non-null enclosingScopeId in debug mode for nodes with an enclosing ' + + 'scope', function() { + var descriptor; + var enclosingScope; + var node; + + global.env.opts.debug = true; + node = astnode.addNodeProperties({}); + enclosingScope = astnode.addNodeProperties({}); + node.enclosingScope = enclosingScope; + + expect(node.enclosingScopeId).toBe(enclosingScope.nodeId); + }); + + }); + + describe('getInfo', function() { + it('should throw an error for undefined input', function() { + function noNode() { + astnode.getInfo(); + } + + expect(noNode).toThrow(); + }); + + it('should return the correct info for an AssignmentExpression', function() { + var info = astnode.getInfo(assignmentExpression); + + expect(info).toBeDefined(); + + expect(info.node).toBeDefined(); + expect(info.node.type).toBe(Syntax.Literal); + expect(info.node.value).toBe(1); + + expect(info.name).toBe('foo'); + expect(info.type).toBe(Syntax.Literal); + expect(info.value).toBe('1'); + }); + + it('should return the correct info for a FunctionDeclaration', function() { + var info = astnode.getInfo(functionDeclaration2); + + expect(info).toBeDefined(); + + expect(info.node).toBeDefined(); + expect(info.node.type).toBe(Syntax.FunctionDeclaration); + + expect(info.name).toBe('foo'); + expect(info.type).toBe(Syntax.FunctionDeclaration); + expect(info.value).not.toBeDefined(); + expect( Array.isArray(info.paramnames) ).toBe(true); + expect(info.paramnames.length).toBe(1); + expect(info.paramnames[0]).toBe('bar'); + }); + + it('should return the correct info for a FunctionExpression', function() { + var info = astnode.getInfo(functionExpression2); + + expect(info).toBeDefined(); + + expect(info.node).toBeDefined(); + expect(info.node.type).toBe(Syntax.FunctionExpression); + + expect(info.name).toBe(''); + expect(info.type).toBe(Syntax.FunctionExpression); + expect(info.value).not.toBeDefined(); + expect( Array.isArray(info.paramnames) ).toBe(true); + expect(info.paramnames.length).toBe(1); + expect(info.paramnames[0]).toBe('bar'); + }); + + it('should return the correct info for a MemberExpression', function() { + var info = astnode.getInfo(memberExpression); + + expect(info).toBeDefined(); + + expect(info.node).toBeDefined(); + expect(info.node.type).toBe(Syntax.MemberExpression); + + expect(info.name).toBe('foo.bar'); + expect(info.type).toBe(Syntax.MemberExpression); + }); + + it('should return the correct info for a computed MemberExpression', function() { + var info = astnode.getInfo(memberExpressionComputed1); + + expect(info).toBeDefined(); + + expect(info.node).toBeDefined(); + expect(info.node.type).toBe(Syntax.MemberExpression); + + expect(info.name).toBe('foo["bar"]'); + expect(info.type).toBe(Syntax.MemberExpression); + }); + + it('should return the correct info for a Property initializer', function() { + var info = astnode.getInfo(propertyInit); + + expect(info).toBeDefined(); + + expect(info.node).toBeDefined(); + expect(info.node.type).toBe(Syntax.ObjectExpression); + + expect(info.name).toBe('bar'); + expect(info.type).toBe(Syntax.ObjectExpression); + }); + + it('should return the correct info for a Property setter', function() { + var info = astnode.getInfo(propertySet); + + expect(info).toBeDefined(); + + expect(info.node).toBeDefined(); + expect(info.node.type).toBe(Syntax.FunctionExpression); + + expect(info.name).toBe('bar'); + expect(info.type).toBe('function'); + expect(info.value).toBe('function'); + expect( Array.isArray(info.paramnames) ).toBe(true); + expect(info.paramnames.length).toBe(1); + expect(info.paramnames[0]).toBe('a'); + }); + + it('should return the correct info for a VariableDeclarator with a value', function() { + var info = astnode.getInfo(variableDeclarator1); + + expect(info).toBeDefined(); + + expect(info.node).toBeDefined(); + expect(info.node.type).toBe(Syntax.Literal); + + expect(info.name).toBe('foo'); + expect(info.type).toBe(Syntax.Literal); + expect(info.value).toBe('1'); + }); + + it('should return the correct info for a VariableDeclarator with no value', function() { + var info = astnode.getInfo(variableDeclarator2); + + expect(info).toBeDefined(); + + expect(info.node).toBeDefined(); + expect(info.node.type).toBe(Syntax.Identifier); + + expect(info.name).toBe('foo'); + expect(info.type).not.toBeDefined(); + expect(info.value).not.toBeDefined(); + }); + + it('should return the correct info for other node types', function() { + var info = astnode.getInfo(binaryExpression); + + expect(info).toBeDefined(); + + expect(info.node).toBe(binaryExpression); + expect(info.type).toBe(Syntax.BinaryExpression); + }); + }); + + describe('getParamNames', function() { + it('should return an empty array for undefined input', function() { + var params = astnode.getParamNames(); + + expect( Array.isArray(params) ).toBe(true); + expect(params.length).toBe(0); + }); + + it('should return an empty array if the input has no params property', function() { + var params = astnode.getParamNames({}); + + expect( Array.isArray(params) ).toBe(true); + expect(params.length).toBe(0); + }); + + it('should return an empty array if the input has no params', function() { + var params = astnode.getParamNames(functionDeclaration1); + + expect( Array.isArray(params) ).toBe(true); + expect(params.length).toBe(0); + }); + + it('should return a single-item array if the input has a single param', function() { + var params = astnode.getParamNames(functionDeclaration2); + + expect( Array.isArray(params) ).toBe(true); + expect(params.length).toBe(1); + expect(params[0]).toBe('bar'); + }); + + it('should return a multi-item array if the input has multiple params', function() { + var params = astnode.getParamNames(functionDeclaration3); + + expect( Array.isArray(params) ).toBe(true); + expect(params.length).toBe(3); + expect(params[0]).toBe('bar'); + expect(params[1]).toBe('baz'); + expect(params[2]).toBe('qux'); + }); + }); + + describe('isAccessor', function() { + it('should return false for undefined values', function() { + expect( astnode.isAccessor() ).toBe(false); + }); + + it('should return false if the parameter is not an object', function() { + expect( astnode.isAccessor('foo') ).toBe(false); + }); + + it('should return false for non-Property nodes', function() { + expect( astnode.isAccessor(binaryExpression) ).toBe(false); + }); + + it('should return false for Property nodes whose kind is "init"', function() { + expect( astnode.isAccessor(propertyInit) ).toBe(false); + }); + + it('should return true for Property nodes whose kind is "get"', function() { + expect( astnode.isAccessor(propertyGet) ).toBe(true); + }); + + it('should return true for Property nodes whose kind is "set"', function() { + expect( astnode.isAccessor(propertySet) ).toBe(true); + }); + }); + + describe('isAssignment', function() { + it('should return false for undefined values', function() { + expect( astnode.isAssignment() ).toBe(false); + }); + + it('should return false if the parameter is not an object', function() { + expect( astnode.isAssignment('foo') ).toBe(false); + }); + + it('should return false for nodes that are not assignments', function() { + expect( astnode.isAssignment(binaryExpression) ).toBe(false); + }); + + it('should return true for AssignmentExpression nodes', function() { + expect( astnode.isAssignment(assignmentExpression) ).toBe(true); + }); + + it('should return true for VariableDeclarator nodes', function() { + expect( astnode.isAssignment(variableDeclarator1) ).toBe(true); + }); + }); + + describe('isScope', function() { + it('should return false for undefined values', function() { + expect( astnode.isScope() ).toBe(false); + }); + + it('should return false if the parameter is not an object', function() { + expect( astnode.isScope('foo') ).toBe(false); + }); + + it('should return true for CatchClause nodes', function() { + expect( astnode.isScope({type: Syntax.CatchClause}) ).toBe(true); + }); + + it('should return true for FunctionDeclaration nodes', function() { + expect( astnode.isScope({type: Syntax.FunctionDeclaration}) ).toBe(true); + }); + + it('should return true for FunctionExpression nodes', function() { + expect( astnode.isScope({type: Syntax.FunctionExpression}) ).toBe(true); + }); + + it('should return false for other nodes', function() { + expect( astnode.isScope({type: Syntax.NameExpression}) ).toBe(false); + }); + }); + + describe('nodeToString', function() { + it('should return `[null]` for the sparse array `[,]`', function() { + expect( astnode.nodeToString(arrayExpression) ).toBe('[null]'); + }); + + it('should return the variable name for assignment expressions', function() { + expect( astnode.nodeToString(assignmentExpression) ).toBe('foo'); + }); + + it('should return "function" for function declarations', function() { + expect( astnode.nodeToString(functionDeclaration1) ).toBe('function'); + }); + + it('should return "function" for function expressions', function() { + expect( astnode.nodeToString(functionExpression1) ).toBe('function'); + }); + + it('should return the identifier name for identifiers', function() { + expect( astnode.nodeToString(identifier) ).toBe('foo'); + }); + + it('should return the literal value (as a string) for literals', function() { + expect( astnode.nodeToString(literal) ).toBe('1'); + }); + + it('should return the object and property for noncomputed member expressions', function() { + expect( astnode.nodeToString(memberExpression) ).toBe('foo.bar'); + }); + + it('should return the object and property, with a computed property that uses the same ' + + 'quote character as the original source, for computed member expressions', function() { + expect( astnode.nodeToString(memberExpressionComputed1) ).toBe('foo["bar"]'); + expect( astnode.nodeToString(memberExpressionComputed2) ).toBe('foo[\'bar\']'); + }); + + it('should return "this" for this expressions', function() { + expect( astnode.nodeToString(thisExpression) ).toBe('this'); + }); + + it('should return the operator and nodeToString value for prefix unary expressions', + function() { + expect( astnode.nodeToString(unaryExpression1) ).toBe('+1'); + expect( astnode.nodeToString(unaryExpression2) ).toBe('+foo'); + }); + + it('should throw an error for postfix unary expressions', function() { + function postfixNodeToString() { + // there's no valid source representation for this one, so we fake it + var unaryExpressionPostfix = (function() { + var node = esprima.parse('+1;').body[0].expression; + node.prefix = false; + return node; + })(); + return astnode.nodeToString(unaryExpressionPostfix); + } + + expect(postfixNodeToString).toThrow(); + }); + + it('should return the variable name for variable declarators', function() { + expect( astnode.nodeToString(variableDeclarator1) ).toBe('foo'); + }); + + it('should return an empty string for all other nodes', function() { + expect( astnode.nodeToString(binaryExpression) ).toBe(''); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/src/filter.js b/third_party/jsdoc/test/specs/jsdoc/src/filter.js new file mode 100644 index 0000000000..a6104ab94d --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/src/filter.js @@ -0,0 +1,167 @@ +/*global beforeEach, describe, expect, it, jasmine */ +describe('jsdoc/src/filter', function() { + var filter = require('jsdoc/src/filter'); + var path = require('jsdoc/path'); + + it('should exist', function() { + expect(filter).toBeDefined(); + expect(typeof filter).toBe('object'); + }); + + it('should export a "Filter" class', function() { + expect(filter.Filter).toBeDefined(); + expect(typeof filter.Filter).toBe('function'); + }); + + describe('Filter', function() { + var myFilter; + + var defaultIncludePattern = new RegExp('.+\\.js(doc)?$'); + var defaultExcludePattern = new RegExp('(^|\\/|\\\\)_'); + + beforeEach(function() { + myFilter = new filter.Filter({}); + }); + it('should have an "exclude" property', function() { + expect(myFilter.exclude).toBeDefined(); + }); + + it('should have an "excludePattern" property', function() { + expect(myFilter.excludePattern).toBeDefined(); + }); + + it('should have an "includePattern" property', function() { + expect(myFilter.includePattern).toBeDefined(); + }); + + it('should have an "isIncluded" method', function() { + expect(myFilter.isIncluded).toBeDefined(); + expect(typeof myFilter.isIncluded).toBe('function'); + }); + + describe('exclude', function() { + it('should default to null', function() { + expect(myFilter.exclude).toBe(null); + }); + + it('should be null if the value passed to the constructor was not an array', + function() { + myFilter = new filter.Filter({ + exclude: 'foo' + }); + + expect(myFilter.exclude).toBe(null); + }); + + it('should resolve paths relative to the user\'s working directory', function() { + var filename = 'bar.js'; + myFilter = new filter.Filter({ + exclude: [filename] + }); + + expect(myFilter.exclude).toEqual([path.resolve(global.env.pwd, filename)]); + }); + }); + + function testRegExpProperty(name) { + it('should default to null', function() { + expect(myFilter[name]).toBe(null); + }); + + it('should contain the regexp passed to the constructor', function() { + var regExp = new RegExp('^foo$'); + var options = {}; + options[name] = regExp; + myFilter = new filter.Filter(options); + + expect(myFilter[name]).toBe(regExp); + }); + + it('should contain a regexp if a string was passed to the constructor', function() { + var regExpString = '^foo$'; + var options = {}; + options[name] = regExpString; + myFilter = new filter.Filter(options); + + expect(myFilter[name] instanceof RegExp).toBe(true); + expect(myFilter[name].source).toBe(regExpString); + }); + } + + describe( 'excludePattern', testRegExpProperty.bind(jasmine, 'excludePattern') ); + + describe( 'includePattern', testRegExpProperty.bind(jasmine, 'includePattern') ); + + describe('isIncluded', function() { + it('should return the correct source files', function() { + var files = [ + 'yes.js', + '/yes.jsdoc', + '/_nope.js', + '.ignore', + path.normalize(global.env.pwd + '/scratch/conf.js') + ]; + myFilter = new filter.Filter({ + includePattern: defaultIncludePattern, + excludePattern: defaultExcludePattern, + exclude: ['.ignore', 'scratch/conf.js'] + }); + + files = files.filter(function($) { + return myFilter.isIncluded($); + }); + + expect(files.length).toEqual(2); + expect( files.indexOf('yes.js') ).toBeGreaterThan(-1); + expect( files.indexOf('/yes.jsdoc') ).toBeGreaterThan(-1); + }); + + it('should be able to exclude specific subdirectories', function() { + var files = [ + 'yes.js', + 'topsecret/nope.js', + 'module/yes.js', + 'module/topsecret/nope.js' + ]; + myFilter = new filter.Filter({ + includePattern: defaultIncludePattern, + excludePattern: defaultExcludePattern, + exclude: ['topsecret', 'module/topsecret'] + }); + + files = files.filter(function($) { + return myFilter.isIncluded($); + }); + + expect(files.length).toBe(2); + expect( files.indexOf('yes.js') ).toBeGreaterThan(-1); + expect( files.indexOf('module/yes.js') ).toBeGreaterThan(-1); + }); + + it('should be able to exclude descendants of excluded subdirectories', function() { + var files = [ + 'yes.js', + 'topsecret/nested/nope.js', + 'module/yes.js', + 'module/topsecret/nested/nope.js' + ]; + myFilter = new filter.Filter({ + includePattern: defaultIncludePattern, + excludePattern: defaultExcludePattern, + exclude: ['topsecret', 'module/topsecret'] + }); + + files = files.filter(function($) { + return myFilter.isIncluded($); + }); + + expect(files.length).toBe(2); + expect( files.indexOf('yes.js') ).toBeGreaterThan(-1); + expect( files.indexOf('module/yes.js') ).toBeGreaterThan(-1); + expect( files.indexOf('topsecret/nested/nope.js') ).toBe(-1); + expect( files.indexOf('module/topsecret/nested/nope.js') ).toBe(-1); + }); + }); + }); + +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/src/handlers.js b/third_party/jsdoc/test/specs/jsdoc/src/handlers.js new file mode 100644 index 0000000000..d1d42d6236 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/src/handlers.js @@ -0,0 +1,55 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ +describe("jsdoc/src/handlers", function() { + var handlers = require('jsdoc/src/handlers'); + var runtime = require('jsdoc/util/runtime'); + + var testParser = jasmine.createParser(); + handlers.attachTo(testParser); + + it("should exist", function() { + expect(handlers).toBeDefined(); + expect(typeof handlers).toEqual("object"); + }); + + it("should export an 'attachTo' function", function() { + expect(handlers.attachTo).toBeDefined(); + expect(typeof handlers.attachTo).toEqual("function"); + }); + + describe("attachTo", function() { + it("should attach a 'jsdocCommentFound' handler to the parser", function() { + var callbacks = testParser.listeners("jsdocCommentFound"); + expect(callbacks).toBeDefined(); + expect(callbacks.length).toEqual(1); + expect(typeof callbacks[0]).toEqual("function"); + }); + + it("should attach a 'symbolFound' handler to the parser", function() { + var callbacks = testParser.listeners("symbolFound"); + expect(callbacks).toBeDefined(); + expect(callbacks.length).toEqual(1); + expect(typeof callbacks[0]).toEqual("function"); + }); + + it("should attach a 'fileComplete' handler to the parser", function() { + var callbacks = testParser.listeners("fileComplete"); + expect(callbacks).toBeDefined(); + expect(callbacks.length).toEqual(1); + expect(typeof callbacks[0]).toEqual("function"); + }); + }); + + describe("jsdocCommentFound handler", function() { + var sourceCode = 'javascript:/** @name bar */', + result = testParser.parse(sourceCode); + + it("should create a doclet for comments with '@name' tags", function() { + expect(result.length).toEqual(1); + expect(result[0].name).toEqual('bar'); + }); + }); + + describe("symbolFound handler", function() { + //TODO + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/src/parser.js b/third_party/jsdoc/test/specs/jsdoc/src/parser.js new file mode 100644 index 0000000000..405d4464e2 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/src/parser.js @@ -0,0 +1,388 @@ +/*global beforeEach, describe, expect, it, jasmine, spyOn */ +/*eslint no-script-url: 0 */ +'use strict'; + +describe('jsdoc/src/parser', function() { + var fs = require('jsdoc/fs'); + var jsdoc = { + src: { + handlers: require('jsdoc/src/handlers'), + parser: require('jsdoc/src/parser') + } + }; + var path = require('jsdoc/path'); + + it('should exist', function() { + expect(jsdoc.src.parser).toBeDefined(); + expect(typeof jsdoc.src.parser).toBe('object'); + }); + + it('should export a "Parser" constructor', function() { + expect(jsdoc.src.parser.Parser).toBeDefined(); + expect(typeof jsdoc.src.parser.Parser).toBe('function'); + }); + + describe('Parser', function() { + var parser; + + function newParser() { + parser = new jsdoc.src.parser.Parser(); + } + + newParser(); + + it('should have an "astBuilder" property', function() { + expect(parser.astBuilder).toBeDefined(); + }); + + it('should have a "visitor" property', function() { + expect(parser.visitor).toBeDefined(); + }); + + it('should have a "walker" property', function() { + expect(parser.walker).toBeDefined(); + }); + + it('should accept an astBuilder, visitor, and walker as arguments', function() { + var astBuilder = {}; + var visitor = {}; + var walker = {}; + + var myParser = new jsdoc.src.parser.Parser(astBuilder, visitor, walker); + + expect(myParser.astBuilder).toBe(astBuilder); + expect(myParser.visitor).toBe(visitor); + expect(myParser.walker).toBe(walker); + }); + + it('should have a "parse" method', function() { + expect(parser.parse).toBeDefined(); + expect(typeof parser.parse).toBe('function'); + }); + + it('should have a "results" method', function() { + expect(parser.results).toBeDefined(); + expect(typeof parser.results).toBe('function'); + }); + + it('should have an "addAstNodeVisitor" method', function() { + expect(parser.addAstNodeVisitor).toBeDefined(); + expect(typeof parser.addAstNodeVisitor).toBe('function'); + }); + + it('should have a "getAstNodeVisitors" method', function() { + expect(parser.getAstNodeVisitors).toBeDefined(); + expect(typeof parser.getAstNodeVisitors).toBe('function'); + }); + + describe('astBuilder', function() { + it('should contain an appropriate astBuilder by default', function() { + expect(parser.astBuilder instanceof (require('jsdoc/src/astbuilder')).AstBuilder).toBe(true); + }); + }); + + describe('visitor', function() { + it('should contain an appropriate visitor by default', function() { + expect(parser.visitor instanceof (require('jsdoc/src/visitor')).Visitor).toBe(true); + }); + }); + + describe('walker', function() { + it('should contain an appropriate walker by default', function() { + expect(parser.walker instanceof (require('jsdoc/src/walker')).Walker).toBe(true); + }); + }); + + describe('parse', function() { + beforeEach(newParser); + + it('should fire "parseBegin" events before it parses any files', function() { + var spy = jasmine.createSpy(); + var sourceFiles = ['javascript:/** @name foo */']; + + parser.on('parseBegin', spy).parse(sourceFiles); + expect(spy).toHaveBeenCalled(); + expect(spy.mostRecentCall.args[0].sourcefiles).toBe(sourceFiles); + }); + + it("should allow 'parseBegin' handlers to modify the list of source files", function() { + var sourceCode = 'javascript:/** @name foo */'; + var newFiles = ['[[replaced]]']; + var evt; + + function handler(e) { + e.sourcefiles = newFiles; + evt = e; + } + + parser.on('parseBegin', handler).parse(sourceCode); + expect(evt.sourcefiles).toBe(newFiles); + }); + + it('should fire "jsdocCommentFound" events when a source file contains JSDoc comments', function() { + var spy = jasmine.createSpy(); + var sourceCode = ['javascript:/** @name bar */']; + + parser.on('jsdocCommentFound', spy).parse(sourceCode); + + expect(spy).toHaveBeenCalled(); + expect(spy.mostRecentCall.args[0].comment).toBe('/** @name bar */'); + }); + + it('should fire "symbolFound" events when a source file contains named symbols', function() { + var spy = jasmine.createSpy(); + var sourceCode = 'javascript:var foo = 1'; + + parser.on('symbolFound', spy).parse(sourceCode); + + expect(spy).toHaveBeenCalled(); + }); + + it('should fire "newDoclet" events after creating a new doclet', function() { + var spy = jasmine.createSpy(); + var sourceCode = 'javascript:var foo = 1'; + + parser.on('symbolFound', spy).parse(sourceCode); + + expect(spy).toHaveBeenCalled(); + }); + + it('should allow "newDoclet" handlers to modify doclets', function() { + var results; + + var sourceCode = 'javascript:/** @class */function Foo() {}'; + + function handler(e) { + var doop = require('jsdoc/util/doop'); + e.doclet = doop(e.doclet); + e.doclet.foo = 'bar'; + } + + jsdoc.src.handlers.attachTo(parser); + parser.on('newDoclet', handler).parse(sourceCode); + results = parser.results(); + + expect(results[0].foo).toBe('bar'); + }); + + it('should call AST node visitors', function() { + var Syntax = require('jsdoc/src/syntax').Syntax; + + var args; + + var sourceCode = ['javascript:/** foo */var foo;']; + var visitor = { + visitNode: function(node, e, parser, sourceName) { + if (e && e.code && !args) { + args = Array.prototype.slice.call(arguments); + } + } + }; + + jsdoc.src.handlers.attachTo(parser); + parser.addAstNodeVisitor(visitor); + parser.parse(sourceCode); + + expect(args).toBeDefined(); + expect( Array.isArray(args) ).toBe(true); + expect(args.length).toBe(4); + + // args[0]: AST node + expect(args[0].type).toBeDefined(); + expect(args[0].type).toBe(Syntax.VariableDeclarator); + + // args[1]: JSDoc event + expect(typeof args[1]).toBe('object'); + expect(args[1].code).toBeDefined(); + expect(args[1].code.name).toBeDefined(); + expect(args[1].code.name).toBe('foo'); + + // args[2]: parser + expect(typeof args[2]).toBe('object'); + expect(args[2] instanceof jsdoc.src.parser.Parser).toBe(true); + + // args[3]: current source name + expect( String(args[3]) ).toBe('[[string0]]'); + }); + + it('should reflect changes made by AST node visitors', function() { + var doclet; + + var sourceCode = ['javascript:/** foo */var foo;']; + var visitor = { + visitNode: function(node, e, parser, sourceName) { + if (e && e.code && e.code.name === 'foo') { + e.code.name = 'bar'; + } + } + }; + + jsdoc.src.handlers.attachTo(parser); + parser.addAstNodeVisitor(visitor); + parser.parse(sourceCode); + + doclet = parser.results()[0]; + + expect(doclet).toBeDefined(); + expect(typeof doclet).toBe('object'); + expect(doclet.name).toBeDefined(); + expect(doclet.name).toBe('bar'); + }); + + it('should fire "parseComplete" events after it finishes parsing files', function() { + var eventObject; + + var spy = jasmine.createSpy(); + var sourceCode = ['javascript:/** @class */function Foo() {}']; + + jsdoc.src.handlers.attachTo(parser); + parser.on('parseComplete', spy).parse(sourceCode); + + expect(spy).toHaveBeenCalled(); + + eventObject = spy.mostRecentCall.args[0]; + expect(eventObject).toBeDefined(); + expect( Array.isArray(eventObject.sourcefiles) ).toBe(true); + expect(eventObject.sourcefiles.length).toBe(1); + expect(eventObject.sourcefiles[0]).toBe('[[string0]]'); + expect( Array.isArray(eventObject.doclets) ).toBe(true); + expect(eventObject.doclets.length).toBe(1); + expect(eventObject.doclets[0].kind).toBe('class'); + expect(eventObject.doclets[0].longname).toBe('Foo'); + }); + + it('should fire a "processingComplete" event when fireProcessingComplete is called', function() { + var spy = jasmine.createSpy(); + var doclets = ['a','b']; + + parser.on('processingComplete', spy).fireProcessingComplete(doclets); + + expect(spy).toHaveBeenCalled(); + expect(typeof spy.mostRecentCall.args[0]).toBe('object'); + expect(spy.mostRecentCall.args[0].doclets).toBeDefined(); + expect(spy.mostRecentCall.args[0].doclets).toBe(doclets); + }); + + // Rhino can't parse ES6 + if (jasmine.jsParser !== 'rhino') { + it('should not throw errors when parsing files with ES6 syntax', function() { + function parse() { + var parserSrc = 'javascript:' + fs.readFileSync( + path.join(global.env.dirname, 'test/fixtures/es6.js'), 'utf8'); + parser.parse(parserSrc); + } + + expect(parse).not.toThrow(); + }); + } + + it('should be able to parse its own source file', function() { + var parserSrc = 'javascript:' + fs.readFileSync(path.join(global.env.dirname, + 'lib/jsdoc/src/parser.js'), 'utf8'); + + function parse() { + parser.parse(parserSrc); + } + + expect(parse).not.toThrow(); + }); + + it('should comment out a POSIX hashbang at the start of the file', function() { + var parserSrc = 'javascript:#!/usr/bin/env node\n/** class */function Foo() {}'; + + function parse() { + parser.parse(parserSrc); + } + + expect(parse).not.toThrow(); + }); + }); + + describe('results', function() { + beforeEach(newParser); + + it('returns an empty array before files are parsed', function() { + var results = parser.results(); + + expect(results).toBeDefined(); + expect( Array.isArray(results) ).toBe(true); + expect(results.length).toBe(0); + }); + + it('returns an array of doclets after files are parsed', function() { + var source = 'javascript:var foo;'; + var results; + + jsdoc.src.handlers.attachTo(parser); + + parser.parse(source); + results = parser.results(); + + expect(results).toBeDefined(); + expect(results[0]).toBeDefined(); + expect(typeof results[0]).toBe('object'); + expect(results[0].name).toBeDefined(); + expect(results[0].name).toBe('foo'); + }); + + it('should reflect comment changes made by "jsdocCommentFound" handlers', function() { + // we test both POSIX and Windows line endings + var source = 'javascript:/**\n * replaceme\r\n * @module foo\n */\n\n' + + '/**\n * replaceme\n */\nvar bar;'; + + parser.on('jsdocCommentFound', function(e) { + e.comment = e.comment.replace('replaceme', 'REPLACED!'); + }); + jsdoc.src.handlers.attachTo(parser); + + parser.parse(source); + parser.results().forEach(function(doclet) { + expect(doclet.comment).not.toMatch('replaceme'); + expect(doclet.comment).toMatch('REPLACED!'); + }); + }); + }); + + describe('addAstNodeVisitor', function() { + function visitorA() {} + function visitorB() {} + + var visitors; + + beforeEach(newParser); + + it('should work with a single node visitor', function() { + parser.addAstNodeVisitor(visitorA); + + visitors = parser.getAstNodeVisitors(); + + expect(visitors.length).toBe(1); + expect(visitors[0]).toBe(visitorA); + }); + + it('should work with multiple node visitors', function() { + parser.addAstNodeVisitor(visitorA); + parser.addAstNodeVisitor(visitorB); + + visitors = parser.getAstNodeVisitors(); + + expect(visitors.length).toBe(2); + expect(visitors[0]).toBe(visitorA); + expect(visitors[1]).toBe(visitorB); + }); + }); + + describe('getAstNodeVisitors', function() { + beforeEach(newParser); + + it('should return an empty array by default', function() { + var visitors = parser.getAstNodeVisitors(); + + expect( Array.isArray(visitors) ).toBe(true); + expect(visitors.length).toBe(0); + }); + + // other functionality is covered by the addNodeVisitors tests + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/src/scanner.js b/third_party/jsdoc/test/specs/jsdoc/src/scanner.js new file mode 100644 index 0000000000..ed184c9095 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/src/scanner.js @@ -0,0 +1,56 @@ +/*global describe, env, expect, it */ +describe('jsdoc/src/scanner', function() { + var path = require('jsdoc/path'); + var scanner = require('jsdoc/src/scanner'); + + var filter = new (require('jsdoc/src/filter').Filter)({ + includePattern: new RegExp('.+\\.js(doc)?$'), + excludePattern: new RegExp('(^|\\/|\\\\)_') + }); + var sourcePath = path.normalize(env.pwd + '/test/fixtures/src'); + + it('should exist', function() { + expect(scanner).toBeDefined(); + expect(typeof scanner).toBe('object'); + }); + + it('should export a "Scanner" class', function() { + expect(scanner.Scanner).toBeDefined(); + expect(typeof scanner.Scanner).toBe('function'); + }); + + describe('Scanner', function() { + it('should inherit from EventEmitter', function() { + var EventEmitter = require('events').EventEmitter; + var testScanner = new scanner.Scanner(); + + expect(testScanner instanceof EventEmitter).toBe(true); + }); + + it('should have a "scan" method', function() { + var testScanner = new scanner.Scanner(); + + expect(testScanner.scan).toBeDefined(); + expect(typeof testScanner.scan).toBe('function'); + }); + + describe('scan', function() { + it('should return the correct source files', function() { + var testScanner = new scanner.Scanner(); + var sourceFiles = testScanner.scan([sourcePath], 3, filter); + + sourceFiles = sourceFiles.map(function($) { + return path.relative(env.pwd, $); + }); + + expect(sourceFiles.length).toEqual(3); + expect( sourceFiles.indexOf(path.join('test', 'fixtures', 'src', 'one.js')) ) + .toBeGreaterThan(-1); + expect( sourceFiles.indexOf(path.join('test', 'fixtures', 'src', 'two.js')) ) + .toBeGreaterThan(-1); + expect( sourceFiles.indexOf(path.join('test', 'fixtures', 'src', 'dir1', 'three.js')) ) + .toBeGreaterThan(-1); + }); + }); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/jsdoc/src/syntax.js b/third_party/jsdoc/test/specs/jsdoc/src/syntax.js new file mode 100644 index 0000000000..d14113760b --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/src/syntax.js @@ -0,0 +1,23 @@ +/*global describe, expect, it */ +describe('jsdoc/src/syntax', function() { + var Syntax = require('jsdoc/src/syntax').Syntax; + + it('should exist', function() { + expect(Syntax).toBeDefined(); + expect(typeof Syntax).toBe('object'); + }); + + it('should define all of the node types that are defined by Esprima', function() { + var esprimaSyntax = require('esprima').Syntax; + + Object.keys(esprimaSyntax).forEach(function(nodeType) { + expect(Syntax[nodeType]).toBeDefined(); + expect(Syntax[nodeType]).toBe(esprimaSyntax[nodeType]); + }); + }); + + it('should define the LetStatement node type', function() { + expect(Syntax.LetStatement).toBeDefined(); + expect(Syntax.LetStatement).toBe('LetStatement'); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/src/visitor.js b/third_party/jsdoc/test/specs/jsdoc/src/visitor.js new file mode 100644 index 0000000000..4c5d293803 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/src/visitor.js @@ -0,0 +1,4 @@ +/*global describe: true, env: true, expect: true, it: true, xdescribe: true */ +xdescribe('jsdoc/src/visitor', function() { + // TODO +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/src/walker.js b/third_party/jsdoc/test/specs/jsdoc/src/walker.js new file mode 100644 index 0000000000..be7528c216 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/src/walker.js @@ -0,0 +1,36 @@ +/*global describe, expect, it, xdescribe */ +describe('jsdoc/src/walker', function() { + var walker = require('jsdoc/src/walker'); + + it('should exist', function() { + expect(walker).toBeDefined(); + expect(typeof walker).toBe('object'); + }); + + it('should export a "walkers" object', function() { + expect(walker.walkers).toBeDefined(); + expect(typeof walker.walkers).toBe('object'); + }); + + it('should export a "Walker" class', function() { + expect(walker.Walker).toBeDefined(); + expect(typeof walker.Walker).toBe('function'); + }); + + describe('walkers', function() { + var Syntax = require('jsdoc/src/syntax').Syntax; + + // TODO: tests for default functions + + it('should contain a function for each known node type', function() { + Object.keys(Syntax).forEach(function(nodeType) { + expect(walker.walkers[nodeType]).toBeDefined(); + expect(typeof walker.walkers[nodeType]).toBe('function'); + }); + }); + }); + + xdescribe('Walker', function() { + // TODO + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/tag.js b/third_party/jsdoc/test/specs/jsdoc/tag.js new file mode 100644 index 0000000000..f3dfea2d5c --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/tag.js @@ -0,0 +1,237 @@ +'use strict'; + +var hasOwnProp = Object.prototype.hasOwnProperty; + +describe('jsdoc/tag', function() { + var jsdoc = { + tag: require('jsdoc/tag'), + dictionary: require('jsdoc/tag/dictionary'), + type: require('jsdoc/tag/type') + }; + var logger = require('jsdoc/util/logger'); + + it('should exist', function() { + expect(jsdoc.tag).toBeDefined(); + expect(typeof jsdoc.tag).toBe('object'); + }); + + it('should export a Tag function', function() { + expect(jsdoc.tag.Tag).toBeDefined(); + expect(typeof jsdoc.tag.Tag).toBe('function'); + }); + + describe('Tag', function() { + var meta = {lineno: 1, filename: 'asdf.js'}; + var desc = 'lalblakd lkjasdlib\n lija'; + var text = '{!number} [foo=1] - ' + desc; + var exampleRaw = [ + 'Asdf\n', + ' myFunction(1, 2); // returns 3\n', + ' myFunction(3, 4); // returns 7\n' + ]; + var textExample = exampleRaw.join(''); + var exampleIndentedRaw = [ + ' var firstLine;\n', + ' function secondLine() {\n', + ' // comment\n', + ' }\n' + ]; + var textExampleIndented = exampleIndentedRaw.join(''); + + var tagArg; + var tagExample; + var tagExampleIndented; + var tagParam; + var tagParamWithType; + var tagType; + + // allow each test to recreate the tags (for example, after enabling debug mode) + function createTags() { + // synonym for @param; space in the title + tagArg = new jsdoc.tag.Tag('arg ', text, meta); + // @param with no type, but with optional and defaultvalue + tagParam = new jsdoc.tag.Tag('param', '[foo=1]', meta); + // @param with type and no type modifiers (such as optional) + tagParamWithType = new jsdoc.tag.Tag('param', '{string} foo', meta); + // @example that does not need indentation to be removed + tagExample = new jsdoc.tag.Tag('example', textExample, meta); + // @example that needs indentation to be removed + tagExampleIndented = new jsdoc.tag.Tag('example', textExampleIndented, meta); + // for testing that onTagText is run when necessary + tagType = new jsdoc.tag.Tag('type', 'MyType ', meta); + } + + beforeEach(function() { + createTags(); + }); + + it("should have a 'originalTitle' property, a string", function() { + expect(tagArg.originalTitle).toBeDefined(); + expect(typeof tagArg.originalTitle).toBe('string'); + }); + + it("'originalTitle' property should be the initial tag title, trimmed of whitespace", function() { + expect(tagArg.originalTitle).toBe('arg'); + expect(tagExample.originalTitle).toBe('example'); + }); + + it("should have a 'title' property, a string", function() { + expect(tagArg.title).toBeDefined(); + expect(typeof tagArg.title).toBe('string'); + }); + + it("'title' property should be the normalised tag title", function() { + expect(tagArg.title).toBe(jsdoc.dictionary.normalise(tagArg.originalTitle)); + expect(tagExample.title).toBe(jsdoc.dictionary.normalise(tagExample.originalTitle)); + }); + + it("should have a 'text' property, a string", function() { + expect(tagArg.text).toBeDefined(); + expect(typeof tagArg.text).toBe('string'); + }); + + it("should have a 'value' property", function() { + expect(tagArg.value).toBeDefined(); + expect(tagExample.value).toBeDefined(); + expect(tagType.value).toBeDefined(); + }); + + describe("'text' property", function() { + it("'text' property should be the trimmed tag text, with all leading and trailing space removed unless tagDef.keepsWhitespace", function() { + // @example has keepsWhitespace and removesIndent, @param doesn't + expect(tagArg.text).toBe( text.replace(/^\s+|\n$/g, '') ); + expect(tagExample.text).toBe( textExample.replace(/\n$/, '') ); + expect(tagExampleIndented.text).toBe( textExampleIndented.replace(/^ {5}/gm, '') + .replace(/\n$/, '') ); + }); + + it("'text' property should have onTagText run on it if it has it.", function() { + var def = jsdoc.dictionary.lookUp('type'); + expect(def.onTagText).toBeDefined(); + expect(typeof def.onTagText).toBe('function'); + + // @type adds {} around the type if necessary. + expect(tagType.text).toBeDefined(); + expect(tagType.text).toBe(def.onTagText('MyType')); + }); + + it('should be enclosed in quotes, with no whitespace trimming, if it is a symbol name with leading or trailing whitespace', function() { + var wsBoth; + var wsLeading; + var wsOnly; + var wsTrailing; + + spyOn(logger, 'error'); + + wsOnly = new jsdoc.tag.Tag('name', ' ', { code: { name: ' ' } }); + wsLeading = new jsdoc.tag.Tag('name', ' foo', { code: { name: ' foo' } }); + wsTrailing = new jsdoc.tag.Tag('name', 'foo ', { code: { name: 'foo ' } }); + wsBoth = new jsdoc.tag.Tag('name', ' foo ', { code: { name: ' foo ' } }); + + expect(logger.error).not.toHaveBeenCalled(); + expect(wsOnly.text).toBe('" "'); + expect(wsLeading.text).toBe('" foo"'); + expect(wsTrailing.text).toBe('"foo "'); + expect(wsBoth.text).toBe('" foo "'); + }); + }); + + describe("'value' property", function() { + it("'value' property should equal tag text if tagDef.canHaveType and canHaveName are both false", function() { + // @example can't have type or name + expect(typeof tagExample.value).toBe('string'); + expect(tagExample.value).toBe(tagExample.text); + }); + + it("'value' property should be an object if tagDef can have type or name", function () { + expect(typeof tagType.value).toBe('object'); + expect(typeof tagArg.value).toBe('object'); + }); + + function verifyTagType(tag) { + var def; + var descriptor; + var info; + + def = jsdoc.dictionary.lookUp(tag.title); + expect(def).not.toBe(false); + + info = jsdoc.type.parse(tag.text, def.canHaveName, def.canHaveType); + + ['optional', 'nullable', 'variable', 'defaultvalue'].forEach(function(prop) { + if (hasOwnProp.call(info, prop)) { + expect(tag.value[prop]).toBe(info[prop]); + } + }); + + if (info.type && info.type.length) { + expect(tag.value.type).toBeDefined(); + expect(typeof tag.value.type).toBe('object'); + expect(tag.value.type.names).toBeDefined(); + expect(tag.value.type.names).toEqual(info.type); + + expect(tag.value.type.parsedType).toBeDefined(); + expect(typeof tag.value.type.parsedType).toBe('object'); + + descriptor = Object.getOwnPropertyDescriptor(tag.value.type, 'parsedType'); + expect(descriptor.enumerable).toBe(!!global.env.opts.debug); + } + } + + it('if the tag has a type, tag.value should contain the type information', function() { + // we assume jsdoc/tag/type.parse works (it has its own tests to verify this); + var debug = !!global.env.opts.debug; + + [true, false].forEach(function(bool) { + global.env.opts.debug = bool; + createTags(); + + verifyTagType(tagType); + verifyTagType(tagArg); + verifyTagType(tagParam); + }); + + global.env.opts.debug = debug; + }); + + it('if the tag has a description beyond the name/type, this should be in tag.value.description', function() { + expect(tagType.value.description).not.toBeDefined(); + + expect(tagArg.value.description).toBeDefined(); + expect(tagArg.value.description).toBe(desc); + }); + + it('if the tag can have a name, it should be stored in tag.value.name', function() { + expect(tagArg.value.name).toBeDefined(); + expect(tagArg.value.name).toBe('foo'); + + expect(tagType.value.name).not.toBeDefined(); + }); + + it('if the tag has a type without modifiers, tag.value should not include properties for the modifiers', function() { + ['optional', 'nullable', 'variable', 'defaultvalue'].forEach(function(modifier) { + expect( hasOwnProp.call(tagParamWithType.value, modifier) ).toBe(false); + }); + }); + }); + + // further tests for this sort of thing are in jsdoc/tag/validator.js tests. + describe('tag validating', function() { + beforeEach(function() { + spyOn(logger, 'error'); + }); + + it('logs an error for bad tags', function() { + var tag = new jsdoc.tag.Tag('param', '{!*!*!*!} foo'); + + expect(logger.error).toHaveBeenCalled(); + }); + + it('validates tags with no text', function() { + var tag = new jsdoc.tag.Tag('copyright'); + + expect(logger.error).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/tag/dictionary.js b/third_party/jsdoc/test/specs/jsdoc/tag/dictionary.js new file mode 100644 index 0000000000..c815817631 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/tag/dictionary.js @@ -0,0 +1,144 @@ +/*global describe, expect, it, xdescribe */ +'use strict'; + +describe('jsdoc/tag/dictionary', function() { + var dictionary = require('jsdoc/tag/dictionary'); + var testDictionary = new dictionary.Dictionary(); + + var tagOptions = { + canHaveValue: true, + isNamespace: true + }; + var tagTitle = '!!!testTag!!!'; + var tagSynonym = '!!!testTagSynonym!!!'; + var tagDef = testDictionary.defineTag(tagTitle, tagOptions).synonym(tagSynonym); + + it('should exist', function() { + expect(dictionary).toBeDefined(); + expect(typeof dictionary).toBe('object'); + }); + + it('should be an instance of dictionary.Dictionary', function() { + expect(dictionary instanceof dictionary.Dictionary).toBe(true); + }); + + it('should export a defineSynonym method', function() { + expect(dictionary.defineSynonym).toBeDefined(); + expect(typeof dictionary.defineSynonym).toBe('function'); + }); + + it('should export a defineTag method', function() { + expect(dictionary.defineTag).toBeDefined(); + expect(typeof dictionary.defineTag).toBe('function'); + }); + + it('should export a lookUp method', function() { + expect(dictionary.lookUp).toBeDefined(); + expect(typeof dictionary.lookUp).toBe('function'); + }); + + it('should export an isNamespace method', function() { + expect(dictionary.isNamespace).toBeDefined(); + expect(typeof dictionary.isNamespace).toBe('function'); + }); + + it('should export a normalise method', function() { + expect(dictionary.normalise).toBeDefined(); + expect(typeof dictionary.normalise).toBe('function'); + }); + + it('should export a Dictionary constructor', function() { + expect(dictionary.Dictionary).toBeDefined(); + expect(typeof dictionary.Dictionary).toBe('function'); + }); + + describe('defineSynonym', function() { + it('adds a synonym for the specified tag', function() { + var synonymDict = new dictionary.Dictionary(); + + dictionary.defineTag('foo', {}); + dictionary.defineSynonym('foo', 'bar'); + + expect(dictionary.normalise('bar')).toBe('foo'); + }); + }); + + describe('defineTag', function() { + it('returns an object with the correct "title" property', function() { + expect(typeof tagDef).toBe('object'); + expect(tagDef.title).toBeDefined(); + expect(typeof tagDef.title).toBe('string'); + expect(tagDef.title).toBe(testDictionary.normalise(tagTitle)); + }); + + it('returns an object that contains all of the tag properties', function() { + Object.keys(tagOptions).forEach(function(opt) { + expect(tagDef[opt]).toBe(tagOptions[opt]); + }); + }); + + it('works correctly without an options object', function() { + var title = '!!!testTagNoOptions!!!'; + + function makeTag() { + return testDictionary.defineTag(title); + } + + expect(makeTag).not.toThrow(); + expect(makeTag().title).toBe(testDictionary.normalise(title)); + }); + }); + + describe('lookUp', function() { + it("retrieves the definition using the tag's canonical name", function() { + expect(testDictionary.lookUp(tagTitle)).toBe(tagDef); + }); + + it('retrieves the definition using a synonym for the tag', function() { + expect(testDictionary.lookUp(tagSynonym)).toBe(tagDef); + }); + + it('returns `false` when a tag is not found', function() { + expect(testDictionary.lookUp('lkjas1l24jk')).toBe(false); + }); + }); + + describe('isNamespace', function() { + it("returns whether a tag is a namespace using the tag's canonical name", function() { + expect(testDictionary.isNamespace(tagTitle)).toBe(true); + }); + + it('returns whether a tag is a namespace when using a synonym for the tag', function() { + expect(testDictionary.isNamespace(tagSynonym)).toBe(true); + }); + + it('returns `false` for nonexistent tags', function() { + expect(testDictionary.isNamespace('lkjasd90034')).toBe(false); + }); + + it('returns `false` for non-namespace tags', function() { + expect(testDictionary.isNamespace('see')).toBe(false); + }); + }); + + describe('normalise', function() { + it("should return the tag's title if it is not a synonym", function() { + expect(testDictionary.normalise('FooBar')).toBe('foobar'); + expect(testDictionary.normalise(tagTitle)).toBe(tagDef.title); + }); + + it('should return the canonical name of a tag if the synonym is normalised', function() { + expect(testDictionary.normalise(tagSynonym)).toBe(tagDef.title); + }); + }); + + describe('Dictionary', function() { + it('should be a constructor', function() { + function newDictionary() { + return new dictionary.Dictionary(); + } + + expect(newDictionary).not.toThrow(); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/tag/dictionary/definitions.js b/third_party/jsdoc/test/specs/jsdoc/tag/dictionary/definitions.js new file mode 100644 index 0000000000..fb1b57fda5 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/tag/dictionary/definitions.js @@ -0,0 +1,231 @@ +/*global afterEach, beforeEach, describe, expect, it, spyOn */ +'use strict'; + +describe('jsdoc/tag/dictionary/definitions', function() { + var definitions = require('jsdoc/tag/dictionary/definitions'); + var Dictionary = require('jsdoc/tag/dictionary').Dictionary; + var logger = require('jsdoc/util/logger'); + + it('should exist', function() { + expect(definitions).toBeDefined(); + expect(typeof definitions).toBe('object'); + }); + + it('should export a baseTags object', function() { + expect(definitions.baseTags).toBeDefined(); + expect(typeof definitions.baseTags).toBe('object'); + }); + + it('should export a closureTags object', function() { + expect(definitions.closureTags).toBeDefined(); + expect(typeof definitions.closureTags).toBe('object'); + }); + + it('should export a defineTags method', function() { + expect(definitions.defineTags).toBeDefined(); + expect(typeof definitions.defineTags).toBe('function'); + }); + + it('should export a jsdocTags object', function() { + expect(definitions.jsdocTags).toBeDefined(); + expect(typeof definitions.jsdocTags).toBe('object'); + }); + + describe('baseTags', function() { + it('should be identical to jsdocTags', function() { + expect(definitions.baseTags).toBe(definitions.jsdocTags); + }); + }); + + describe('closureTags', function() { + // this test just makes sure all the definitions are here; we have other tests for tag + // behavior + it('should contain the expected tag definitions', function() { + var expectedTagNames = [ + 'const', + 'constructor', + 'deprecated', + 'enum', + 'extends', + 'final', + 'implements', + 'interface', + 'lends', + 'license', + 'param', + 'private', + 'protected', + 'return', + 'this', + 'throws', + 'type', + 'typedef' + ].sort(); + var actualTagNames = Object.keys(definitions.closureTags).sort(); + + expect(expectedTagNames).toEqual(actualTagNames); + }); + }); + + describe('defineTags', function() { + var dictionaryConfig = global.env.conf.tags.dictionaries.slice(0); + var tagDict; + + beforeEach(function() { + global.env.conf.tags.dictionaries = []; + tagDict = new Dictionary(); + }); + + afterEach(function() { + global.env.conf.tags.dictionaries = dictionaryConfig.slice(0); + }); + + it('should log an error if `env.conf.tags.dictionaries` is undefined', function() { + global.env.conf.tags.dictionaries = undefined; + spyOn(logger, 'error'); + definitions.defineTags(tagDict); + + expect(logger.error).toHaveBeenCalled(); + }); + + it('should log an error if an unknown dictionary is requested', function() { + global.env.conf.tags.dictionaries = ['jsmarmoset']; + spyOn(logger, 'error'); + definitions.defineTags(tagDict); + + expect(logger.error).toHaveBeenCalled(); + }); + + it('should add both JSDoc and Closure tags by default', function() { + global.env.conf.tags.dictionaries = dictionaryConfig.slice(0); + definitions.defineTags(tagDict); + + // Check for one tag from the JSDoc tagdefs and another tag from the Closure tagdefs. + // Not thorough, but probably good enough. + expect(tagDict.lookUp('abstract')).not.toBe(false); + expect(tagDict.lookUp('final')).not.toBe(false); + }); + + it('should add only the JSDoc tags if requested', function() { + global.env.conf.tags.dictionaries = ['jsdoc']; + definitions.defineTags(tagDict); + + // Check for one tag from the JSDoc tagdefs and another tag from another set of tagdefs. + // Not thorough, but probably good enough. + expect(tagDict.lookUp('abstract')).not.toBe(false); + expect(tagDict.lookUp('final')).toBe(false); + }); + + it('should add only the Closure tags if requested', function() { + global.env.conf.tags.dictionaries = ['closure']; + definitions.defineTags(tagDict); + + // Check for one tag from the Closure tagdefs and another tag from another set of + // tagdefs. Not thorough, but probably good enough. + expect(tagDict.lookUp('final')).not.toBe(false); + expect(tagDict.lookUp('abstract')).toBe(false); + }); + + it('should prefer tagdefs from the first dictionary on the list', function() { + global.env.conf.tags.dictionaries = ['closure', 'jsdoc']; + definitions.defineTags(tagDict); + + expect(tagDict.lookUp('deprecated').synonyms).not.toBeDefined(); + }); + + it('should add tag synonyms', function() { + global.env.conf.tags.dictionaries = ['jsdoc']; + definitions.defineTags(tagDict); + + expect(tagDict.lookUp('extends')).not.toBe(false); + expect(tagDict.normalise('extends')).toBe('augments'); + }); + + it('should ignore the config settings if tagdefs are passed in', function() { + var tagDefs = { + foo: { + mustHaveValue: false + } + }; + + global.env.conf.tags.dictionaries = ['jsdoc']; + definitions.defineTags(tagDict, tagDefs); + + expect(tagDict.lookUp('foo')).not.toBe(false); + expect(tagDict.lookUp('abstract')).toBe(false); + }); + }); + + describe('jsdocTags', function() { + // this test just makes sure all the definitions are here; we have other tests for tag + // behavior + it('should contain the expected tag definitions', function() { + var expectedTagNames = [ + 'abstract', + 'access', + 'alias', + 'also', + 'augments', + 'author', + 'borrows', + 'class', + 'classdesc', + 'constant', + 'constructs', + 'copyright', + 'default', + 'deprecated', + 'description', + 'enum', + 'event', + 'example', + 'exports', + 'external', + 'file', + 'fires', + 'function', + 'global', + 'ignore', + 'inner', + 'instance', + 'implements', + 'interface', + 'kind', + 'lends', + 'license', + 'listens', + 'member', + 'memberof', + 'mixes', + 'mixin', + 'module', + 'name', + 'namespace', + 'param', + 'private', + 'property', + 'protected', + 'public', + 'readonly', + 'requires', + 'returns', + 'see', + 'since', + 'static', + 'summary', + 'this', + 'throws', + 'todo', + 'tutorial', + 'type', + 'typedef', + 'undocumented', + 'variation', + 'version' + ].sort(); + var actualTagNames = Object.keys(definitions.jsdocTags).sort(); + + expect(expectedTagNames).toEqual(actualTagNames); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/tag/inline.js b/third_party/jsdoc/test/specs/jsdoc/tag/inline.js new file mode 100644 index 0000000000..3670ae545a --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/tag/inline.js @@ -0,0 +1,254 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ +'use strict'; + +describe('jsdoc/tag/inline', function() { + var jsdoc = { + tag: { + inline: require('jsdoc/tag/inline') + } + }; + + it('should exist', function() { + expect(jsdoc.tag.inline).toBeDefined(); + expect(typeof jsdoc.tag.inline).toBe('object'); + }); + + it('should export an isInlineTag function', function() { + expect(jsdoc.tag.inline.isInlineTag).toBeDefined(); + expect(typeof jsdoc.tag.inline.isInlineTag).toBe('function'); + }); + + it('should export a replaceInlineTag function', function() { + expect(jsdoc.tag.inline.replaceInlineTag).toBeDefined(); + expect(typeof jsdoc.tag.inline.replaceInlineTag).toBe('function'); + }); + + it('should export an extractInlineTag function', function() { + expect(jsdoc.tag.inline.extractInlineTag).toBeDefined(); + expect(typeof jsdoc.tag.inline.replaceInlineTag).toBe('function'); + }); + + describe('isInlineTag', function() { + var isInlineTag = jsdoc.tag.inline.isInlineTag; + + it('should correctly identify an inline tag', function() { + expect( isInlineTag('{@mytag hooray}', 'mytag') ).toBe(true); + }); + + it('should correctly identify a non-inline tag', function() { + expect( isInlineTag('mytag hooray', 'mytag') ).toBe(false); + }); + + it('should report that a string containing an inline tag is not an inline tag', function() { + expect( isInlineTag('this is {@mytag hooray}', 'mytag') ).toBe(false); + }); + + it('should default to allowing any inline tag', function() { + expect( isInlineTag('{@anyoldtag will do}') ).toBe(true); + }); + + it('should still identify non-inline tags when a tag name is not provided', function() { + expect( isInlineTag('mytag hooray') ).toBe(false); + }); + + it('should allow regexp characters in the tag name', function() { + expect( isInlineTag('{@mytags hooray}', 'mytag\\S') ).toBe(true); + }); + + it('should return false (rather than throwing) with invalid input', function() { + function badInput() { + return isInlineTag(); + } + + expect(badInput).not.toThrow(); + expect( badInput() ).toBe(false); + }); + }); + + describe('replaceInlineTag', function() { + it('should throw if the tag is matched and the replacer is invalid', function() { + function badReplacerUndefined() { + jsdoc.tag.inline.replaceInlineTag('{@foo tag}', 'foo'); + } + + function badReplacerString() { + jsdoc.tag.inline.replaceInlineTag('{@foo tag}', 'foo', 'hello'); + } + + expect(badReplacerUndefined).toThrow(); + expect(badReplacerString).toThrow(); + }); + + it('should not find anything if there is no text in braces', function() { + var replacer = jasmine.createSpy('replacer'); + var result = jsdoc.tag.inline.replaceInlineTag('braceless text', 'foo', replacer); + expect(replacer).not.toHaveBeenCalled(); + }); + + it('should cope with bad escapement at the end of the string', function() { + var replacer = jasmine.createSpy('replacer'); + var result = jsdoc.tag.inline.replaceInlineTag('bad {@foo escapement \\', 'foo', + replacer); + expect(replacer).not.toHaveBeenCalled(); + }); + + it('should work if the tag is the entire string', function() { + function replacer(string, tagInfo) { + expect(string).toBe('{@foo text in braces}'); + expect(tagInfo.completeTag).toBe('{@foo text in braces}'); + expect(tagInfo.text).toBe('text in braces'); + + return tagInfo.completeTag; + } + + var result = jsdoc.tag.inline.replaceInlineTag('{@foo text in braces}', 'foo', + replacer); + expect(result.tags[0]).toBeDefined(); + expect(typeof result.tags[0]).toBe('object'); + expect(result.tags[0].tag).toBe('foo'); + expect(result.tags[0].text).toBe('text in braces'); + expect(result.newString).toBe('{@foo text in braces}'); + }); + + it('should work if the tag is at the beginning of the string', function() { + function replacer(string, tagInfo) { + expect(string).toBe('{@foo test string} ahoy'); + expect(tagInfo.completeTag).toBe('{@foo test string}'); + expect(tagInfo.text).toBe('test string'); + + return string; + } + + var result = jsdoc.tag.inline.replaceInlineTag('{@foo test string} ahoy', 'foo', + replacer); + expect(result.tags[0]).toBeDefined(); + expect(typeof result.tags[0]).toBe('object'); + expect(result.tags[0].tag).toBe('foo'); + expect(result.tags[0].text).toBe('test string'); + expect(result.newString).toBe('{@foo test string} ahoy'); + }); + + it('should work if the tag is in the middle of the string', function() { + function replacer(string, tagInfo) { + expect(string).toBe('a {@foo test string} yay'); + expect(tagInfo.completeTag).toBe('{@foo test string}'); + expect(tagInfo.text).toBe('test string'); + + return string; + } + + var result = jsdoc.tag.inline.replaceInlineTag('a {@foo test string} yay', 'foo', + replacer); + expect(result.tags[0]).toBeDefined(); + expect(typeof result.tags[0]).toBe('object'); + expect(result.tags[0].tag).toBe('foo'); + expect(result.tags[0].text).toBe('test string'); + expect(result.newString).toBe('a {@foo test string} yay'); + }); + + it('should work if the tag is at the end of the string', function() { + function replacer(string, tagInfo) { + expect(string).toBe('a {@foo test string}'); + expect(tagInfo.completeTag).toBe('{@foo test string}'); + expect(tagInfo.text).toBe('test string'); + + return string; + } + + var result = jsdoc.tag.inline.replaceInlineTag('a {@foo test string}', 'foo', replacer); + expect(result.tags[0]).toBeDefined(); + expect(typeof result.tags[0]).toBe('object'); + expect(result.tags[0].tag).toBe('foo'); + expect(result.tags[0].text).toBe('test string'); + expect(result.newString).toBe('a {@foo test string}'); + }); + + it('should replace the string with the specified value', function() { + function replacer() { + return 'REPLACED!'; + } + + var result = jsdoc.tag.inline.replaceInlineTag('a {@foo test string}', 'foo', replacer); + expect(result.newString).toBe('REPLACED!'); + }); + + it('should process all occurrences of a tag', function() { + function replacer(string, tagInfo) { + return string.replace(tagInfo.completeTag, 'stuff'); + } + + var result = jsdoc.tag.inline.replaceInlineTag('some {@foo text} with multiple ' + + '{@foo tags}', 'foo', replacer); + + expect(result.tags[0]).toBeDefined(); + expect(typeof result.tags[0]).toBe('object'); + expect(result.tags[0].tag).toBe('foo'); + expect(result.tags[0].text).toBe('text'); + + expect(result.tags[1]).toBeDefined(); + expect(typeof result.tags[1]).toBe('object'); + expect(result.tags[1].tag).toBe('foo'); + expect(result.tags[1].text).toBe('tags'); + + expect(result.newString).toBe('some stuff with multiple stuff'); + }); + + }); + + // largely covered by the replaceInlineTag tests + describe('replaceInlineTags', function() { + it('should work with an empty replacer object', function() { + var replacers = {}; + var text = 'some {@foo text} to parse'; + + var result = jsdoc.tag.inline.replaceInlineTags(text, replacers); + expect(result.newString).toBe(text); + }); + + it('should work with an object with one replacer', function() { + var text = 'some {@foo text} with {@bar multiple} tags'; + var replacers = { + foo: function(string, tagInfo) { + expect(tagInfo.completeTag).toBe('{@foo text}'); + expect(tagInfo.text).toBe('text'); + return string.replace(tagInfo.completeTag, 'stuff'); + } + }; + + var result = jsdoc.tag.inline.replaceInlineTags(text, replacers); + expect(result.newString).toBe('some stuff with {@bar multiple} tags'); + + }); + + it('should work with an object with multiple replacers', function() { + var text = 'some {@foo text} with {@bar multiple} tags'; + var replacers = { + foo: function(string, tagInfo) { + expect(tagInfo.completeTag).toBe('{@foo text}'); + expect(tagInfo.text).toBe('text'); + return string.replace(tagInfo.completeTag, 'stuff'); + }, + bar: function(string, tagInfo) { + expect(tagInfo.completeTag).toBe('{@bar multiple}'); + expect(tagInfo.text).toBe('multiple'); + return string.replace(tagInfo.completeTag, 'awesome'); + } + }; + + var result = jsdoc.tag.inline.replaceInlineTags(text, replacers); + expect(result.newString).toBe('some stuff with awesome tags'); + }); + }); + + // largely covered by the replaceInlineTag tests + describe('extractInlineTag', function() { + it('should work when a tag is specified', function() { + var result = jsdoc.tag.inline.extractInlineTag('some {@tagged text}', 'tagged'); + expect(result.tags[0]).toBeDefined(); + expect(typeof result.tags[0]).toBe('object'); + expect(result.tags[0].tag).toBe('tagged'); + expect(result.tags[0].text).toBe('text'); + expect(result.newString).toBe('some'); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/tag/type.js b/third_party/jsdoc/test/specs/jsdoc/tag/type.js new file mode 100644 index 0000000000..4add5b0072 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/tag/type.js @@ -0,0 +1,255 @@ +/*global describe, expect, it */ +'use strict'; + +function buildText(type, name, desc) { + var text = ''; + if (type) { + text += '{' + type + '}'; + if (name || desc) { + text += ' '; + } + } + + if (name) { + text += name; + if (desc) { + text += ' '; + } + } + + if (desc) { + text += desc; + } + + return text; +} + +describe('jsdoc/tag/type', function() { + var jsdoc = { + tag: { + type: require('jsdoc/tag/type') + } + }; + + it('should exist', function() { + expect(jsdoc.tag.type).toBeDefined(); + expect(typeof jsdoc.tag.type).toBe('object'); + }); + + it('should export a parse function', function() { + expect(jsdoc.tag.type.parse).toBeDefined(); + expect(typeof jsdoc.tag.type.parse).toBe('function'); + }); + + describe('parse', function() { + it('should return an object with name, type, and text properties', function() { + var info = jsdoc.tag.type.parse(''); + expect(info.name).toBeDefined(); + expect(info.type).toBeDefined(); + expect(info.text).toBeDefined(); + }); + + it('should not extract a name or type if canHaveName and canHaveType are not set', function() { + var desc = '{number} foo The foo parameter.'; + var info = jsdoc.tag.type.parse(desc); + expect(info.type).toEqual([]); + expect(info.name).toBe(''); + expect(info.text).toBe(desc); + }); + + it('should extract a name, but not a type, if canHaveName === true and canHaveType === false', function() { + var name = 'bar'; + var desc = 'The bar parameter.'; + var info = jsdoc.tag.type.parse( buildText(null, name, desc), true, false ); + expect(info.type).toEqual([]); + expect(info.name).toBe(name); + expect(info.text).toBe(desc); + }); + + it('should extract a type, but not a name, if canHaveName === false and canHaveType === true', function() { + var type = 'boolean'; + var desc = 'Set to true on alternate Thursdays.'; + var info = jsdoc.tag.type.parse( buildText(type, null, desc), false, true ); + expect(info.type).toEqual([type]); + expect(info.name).toBe(''); + expect(info.text).toBe(desc); + }); + + it('should extract a name and type if canHaveName and canHaveType are true', function() { + var type = 'string'; + var name = 'baz'; + var desc = 'The baz parameter.'; + var info = jsdoc.tag.type.parse( buildText(type, name, desc), true, true ); + expect(info.type).toEqual([type]); + expect(info.name).toBe(name); + expect(info.text).toBe(desc); + }); + + it('should report optional types correctly no matter which syntax we use', function() { + var desc = '{string} [foo]'; + var info = jsdoc.tag.type.parse(desc, true, true); + expect(info.optional).toBe(true); + + desc = '{string=} [foo]'; + info = jsdoc.tag.type.parse(desc, true, true); + expect(info.optional).toBe(true); + + desc = '[foo]'; + info = jsdoc.tag.type.parse(desc, true, true); + expect(info.optional).toBe(true); + }); + + it('should return the types as an array', function() { + var desc = '{string} foo'; + var info = jsdoc.tag.type.parse(desc, true, true); + expect(info.type).toEqual( ['string'] ); + }); + + it('should recognize the entire list of possible types', function() { + var desc = '{(string|number)} foo'; + var info = jsdoc.tag.type.parse(desc, true, true); + expect(info.type).toEqual( ['string', 'number'] ); + + desc = '{ ( string | number ) } foo'; + info = jsdoc.tag.type.parse(desc, true, true); + expect(info.type).toEqual( ['string', 'number'] ); + + desc = '{ ( string | number)} foo'; + info = jsdoc.tag.type.parse(desc, true, true); + expect(info.type).toEqual( ['string', 'number'] ); + + desc = '{(string|number|boolean|function)} foo'; + info = jsdoc.tag.type.parse(desc, true, true); + expect(info.type).toEqual( ['string', 'number', 'boolean', 'function'] ); + }); + + it('should not find any type if there is no text in braces', function() { + var desc = 'braceless text'; + var info = jsdoc.tag.type.parse(desc, false, true); + expect(info.type).toEqual([]); + }); + + it('should cope with bad escapement at the end of the string', function() { + var desc = 'bad {escapement \\'; + var info = jsdoc.tag.type.parse(desc, false, true); + expect(info.type).toEqual([]); + expect(info.text).toBe(desc); + }); + + it('should handle escaped braces correctly', function() { + var desc = '{weirdObject."with\\}AnnoyingProperty"}'; + var info = jsdoc.tag.type.parse(desc, false, true); + expect(info.type[0]).toBe('weirdObject."with}AnnoyingProperty"'); + }); + + it('should work if the type expression is the entire string', function() { + var desc = '{textInBraces}'; + var info = jsdoc.tag.type.parse(desc, false, true); + expect(info.type[0]).toBe('textInBraces'); + }); + + it('should work if the type expression is at the beginning of the string', function() { + var desc = '{testString} ahoy'; + var info = jsdoc.tag.type.parse(desc, false, true); + expect(info.type[0]).toBe('testString'); + expect(info.text).toBe('ahoy'); + }); + + it('should work if the type expression is in the middle of the string', function() { + var desc = 'a {testString} yay'; + var info = jsdoc.tag.type.parse(desc, false, true); + expect(info.type[0]).toBe('testString'); + expect(info.text).toBe('a yay'); + }); + + it('should work if the tag is at the end of the string', function() { + var desc = 'a {testString}'; + var info = jsdoc.tag.type.parse(desc, false, true); + expect(info.type[0]).toBe('testString'); + expect(info.text).toBe('a'); + }); + + it('should work when there are nested braces', function() { + var desc = 'some {{double}} braces'; + var info = jsdoc.tag.type.parse(desc, false, true); + // we currently stringify all record types as 'Object' + expect(info.type[0]).toBe('Object'); + expect(info.text).toBe('some braces'); + }); + + it('should override the type expression if an inline @type tag is specified', function() { + var desc = '{Object} cookie {@type Monster}'; + var info = jsdoc.tag.type.parse(desc, true, true); + expect(info.type).toEqual( ['Monster'] ); + expect(info.text).toBe(''); + + desc = '{Object} cookie - {@type Monster}'; + info = jsdoc.tag.type.parse(desc, true, true); + expect(info.type).toEqual( ['Monster'] ); + expect(info.text).toBe(''); + + desc = '{Object} cookie - The cookie parameter. {@type Monster}'; + info = jsdoc.tag.type.parse(desc, true, true); + expect(info.type).toEqual( ['Monster'] ); + expect(info.text).toBe('The cookie parameter.'); + + desc = '{Object} cookie - The cookie parameter. {@type (Monster|Jar)}'; + info = jsdoc.tag.type.parse(desc, true, true); + expect(info.type).toEqual( ['Monster', 'Jar'] ); + expect(info.text).toBe('The cookie parameter.'); + + desc = '{Object} cookie - The cookie parameter. {@type (Monster|Jar)} Mmm, cookie.'; + info = jsdoc.tag.type.parse(desc, true, true); + expect(info.type).toEqual( ['Monster', 'Jar'] ); + expect(info.text).toBe('The cookie parameter. Mmm, cookie.'); + }); + + describe('JSDoc-style type info', function() { + it('should parse JSDoc-style optional parameters', function() { + var name = '[qux]'; + var desc = 'The qux parameter.'; + var info = jsdoc.tag.type.parse( buildText(null, name, desc), true, false ); + expect(info.name).toBe('qux'); + expect(info.text).toBe(desc); + expect(info.optional).toBe(true); + + name = '[ qux ]'; + info = jsdoc.tag.type.parse( buildText(null, name, desc), true, false ); + expect(info.name).toBe('qux'); + expect(info.text).toBe(desc); + expect(info.optional).toBe(true); + + name = '[qux=hooray]'; + info = jsdoc.tag.type.parse( buildText(null, name, desc), true, false ); + expect(info.name).toBe('qux'); + expect(info.text).toBe(desc); + expect(info.optional).toBe(true); + expect(info.defaultvalue).toBe('hooray'); + + name = '[ qux = hooray ]'; + info = jsdoc.tag.type.parse( buildText(null, name, desc), true, false ); + expect(info.name).toBe('qux'); + expect(info.text).toBe(desc); + expect(info.optional).toBe(true); + expect(info.defaultvalue).toBe('hooray'); + }); + }); + + // TODO: add more tests related to how JSDoc mangles the Catharsis parse results + describe('Closure Compiler-style type info', function() { + it('should recognize variable (repeatable) parameters', function() { + var desc = '{...string} foo - Foo.'; + var info = jsdoc.tag.type.parse(desc, true, true); + expect(info.type).toEqual( ['string'] ); + expect(info.variable).toBe(true); + }); + + it('should set the type correctly for type applications that contain type unions', + function() { + var desc = '{Array.<(string|number)>} foo - Foo.'; + var info = jsdoc.tag.type.parse(desc, true, true); + expect(info.type).toEqual(['Array.<(string|number)>']); + }); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/tag/validator.js b/third_party/jsdoc/test/specs/jsdoc/tag/validator.js new file mode 100644 index 0000000000..2e39994597 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/tag/validator.js @@ -0,0 +1,98 @@ +/*global afterEach, beforeEach, describe, env, expect, it, spyOn */ +'use strict'; + +describe('jsdoc/tag/validator', function() { + var doop = require('jsdoc/util/doop'); + var logger = require('jsdoc/util/logger'); + var tag = require('jsdoc/tag'); + var validator = require('jsdoc/tag/validator'); + + it('should exist', function() { + expect(validator).toBeDefined(); + expect(typeof validator).toBe('object'); + }); + + it('should export a validate function', function() { + expect(validator.validate).toBeDefined(); + expect(typeof validator.validate).toBe('function'); + }); + + describe('validate', function() { + var dictionary = require('jsdoc/tag/dictionary'); + + var allowUnknown = !!env.conf.tags.allowUnknownTags; + var badTag = { title: 'lkjasdlkjfb' }; + var badTag2 = new tag.Tag('type', '{string} I am a string!'); + var meta = { + filename: 'asdf.js', + lineno: 1, + comment: 'Better luck next time.' + }; + var goodTag = new tag.Tag('name', 'MyDocletName', meta); // mustHaveValue + var goodTag2 = new tag.Tag('ignore', '', meta); // mustNotHaveValue + + function validateTag(tag) { + validator.validate(tag, dictionary.lookUp(tag.title), meta); + } + + beforeEach(function() { + spyOn(logger, 'error'); + spyOn(logger, 'warn'); + }); + + afterEach(function() { + env.conf.tags.allowUnknownTags = allowUnknown; + }); + + it('logs an error if the tag is not in the dictionary and conf.tags.allowUnknownTags is false', function() { + env.conf.tags.allowUnknownTags = false; + validateTag(badTag); + + expect(logger.error).toHaveBeenCalled(); + }); + + it('does not log an error if the tag is not in the dictionary and conf.tags.allowUnknownTags is true', function() { + env.conf.tags.allowUnknownTags = true; + validateTag(badTag); + + expect(logger.error).not.toHaveBeenCalled(); + }); + + it('does not log an error for valid tags', function() { + validateTag(goodTag); + validateTag(goodTag2); + + expect(logger.error).not.toHaveBeenCalled(); + }); + + it('logs an error if the tag has no text but mustHaveValue is true', function() { + var missingName = doop(goodTag); + missingName.text = null; + validateTag(missingName); + + expect(logger.error).toHaveBeenCalled(); + }); + + it('logs a warning if the tag has text but mustNotHaveValue is true', function() { + var missingText = doop(goodTag2); + missingText.mustNotHaveValue = true; + missingText.text = missingText.text || 'asdf'; + validateTag(missingText); + + expect(logger.warn).toHaveBeenCalled(); + }); + + it('logs a warning if the tag has a description but mustNotHaveDescription is true', function() { + validateTag(badTag2); + + expect(logger.warn).toHaveBeenCalled(); + }); + + it('logs meta.comment when present', function() { + env.conf.tags.allowUnknownTags = false; + validateTag(badTag); + + expect(logger.error.mostRecentCall.args[0]).toContain(meta.comment); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/template.js b/third_party/jsdoc/test/specs/jsdoc/template.js new file mode 100644 index 0000000000..97dedb3c75 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/template.js @@ -0,0 +1,4 @@ +/*global describe: true, expect: true, it: true, xdescribe: true, xit: true */ +xdescribe('jsdoc/template', function() { + // TODO +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/tutorial.js b/third_party/jsdoc/test/specs/jsdoc/tutorial.js new file mode 100644 index 0000000000..0ab8d98a68 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/tutorial.js @@ -0,0 +1,284 @@ +'use strict'; + +describe('jsdoc/tutorial', function() { + var tutorial = require('jsdoc/tutorial'); + + var name = 'tuteID'; + var content = 'Tutorial content blah blah blah & <'; + var tute = new tutorial.Tutorial(name, content, tutorial.TYPES.NOTAVALUE); + var par = new tutorial.Tutorial('parent', + "# This is the parent tutorial's content & stuff A_B X_Y", + tutorial.TYPES.MARKDOWN); + var par2 = new tutorial.Tutorial('parent2', '

      This is the second parent tutorial

      ', + tutorial.TYPES.HTML); + var markdownEntities = new tutorial.Tutorial('markdown-entities', + '
      This Markdown tutorial contains HTML entities: & < >
      ', + tutorial.TYPES.MARKDOWN); + + it('module should exist', function() { + expect(tutorial).toBeDefined(); + expect(typeof tutorial).toBe('object'); + }); + + it('should export a Tutorial function', function() { + expect(tutorial.Tutorial).toBeDefined(); + expect(typeof tutorial.Tutorial).toBe('function'); + }); + + it('should export a RootTutorial function', function() { + expect(tutorial.RootTutorial).toBeDefined(); + expect(typeof tutorial.RootTutorial).toBe('function'); + }); + + it('should export a TYPES object', function() { + expect(tutorial.TYPES).toBeDefined(); + expect(typeof tutorial.TYPES).toBe('object'); + }); + + describe('tutorial.TYPES', function() { + it('should have a HTML property', function() { + expect(tutorial.TYPES.HTML).toBeDefined(); + }); + + it('should have a MARKDOWN property', function() { + expect(tutorial.TYPES.MARKDOWN).toBeDefined(); + }); + }); + + describe('Tutorial', function() { + it('should have a "setParent" method', function() { + expect(tutorial.Tutorial.prototype.setParent).toBeDefined(); + expect(typeof tutorial.Tutorial.prototype.setParent).toBe('function'); + }); + + it('should have a "removeChild" method', function() { + expect(tutorial.Tutorial.prototype.removeChild).toBeDefined(); + expect(typeof tutorial.Tutorial.prototype.removeChild).toBe('function'); + }); + + it('should have an "addChild" method', function() { + expect(tutorial.Tutorial.prototype.addChild).toBeDefined(); + expect(typeof tutorial.Tutorial.prototype.addChild).toBe('function'); + }); + + it('should have a "parse" method', function() { + expect(tutorial.Tutorial.prototype.parse).toBeDefined(); + expect(typeof tutorial.Tutorial.prototype.parse).toBe('function'); + }); + + it('should have a "name" property', function() { + expect(tute.name).toBeDefined(); + expect(typeof tute.name).toBe('string'); + expect(tute.name).toBe(name); + }); + + it("should have a 'title' property, by default set to to the tutorial's name", function() { + expect(tute.title).toBeDefined(); + expect(typeof tute.title).toBe('string'); + expect(tute.title).toBe(name); + // Testing of overriding a tutorial's title in its JSON file is + // covered in tutorial/resolver.js tests. + }); + + it("should have a 'content' property set to the tutorial's content", function() { + expect(tute.content).toBeDefined(); + expect(typeof tute.content).toBe('string'); + expect(tute.content).toBe(content); + }); + + it("should have a 'type' property set to the tutorial's type", function() { + expect(par.type).toBeDefined(); + expect(typeof par.type).toBe(typeof tutorial.TYPES.MARKDOWN); + expect(par.type).toBe(tutorial.TYPES.MARKDOWN); + }); + + it("should have a 'parent' property, initially null", function() { + expect(tute.parent).toBeDefined(); + expect(tute.parent).toBe(null); + }); + + it("should have a 'children' property, an empty array", function() { + expect(tute.children).toBeDefined(); + expect(Array.isArray(tute.children)).toBe(true); + expect(tute.children.length).toBe(0); + }); + + describe('setParent', function() { + it("adding a parent sets the child's 'parent' property", function() { + tute.setParent(par); + expect(tute.parent).toBe(par); + }); + + it("adding a parent adds the child to the parent's 'children' property", function() { + expect(par.children).toContain(tute); + }); + + it('re-parenting removes the child from the previous parent', function() { + tute.setParent(par2); + + expect(tute.parent).toBe(par2); + expect(par2.children).toContain(tute); + expect(par.children).not.toContain(tute); + }); + + it("calling setParent with a null parent unsets the child's parent and removes the child from its previous parent", function() { + expect(par2.children).toContain(tute); + tute.setParent(null); + + expect(tute.parent).toBe(null); + expect(par2.children).not.toContain(tute); + }); + }); + + describe('addChild', function() { + it("adding a child tutorial adds the child to the parent's 'children' property", function() { + tute.setParent(null); + var n = par.children.length; + + par.addChild(tute); + + expect(par.children.length).toBe(n + 1); + expect(par.children).toContain(tute); + }); + + it("adding a child tutorial sets the child's parent to to the parent tutorial", function() { + expect(tute.parent).toBe(par); + }); + + it('adding a child tutorial removes the child from its old parent', function() { + // tue is currently owned by par; we reparent it to par2 + expect(tute.parent).toBe(par); + par2.addChild(tute); + + expect(tute.parent).toBe(par2); + expect(par.children).not.toContain(tute); + expect(par2.children).toContain(tute); + }); + }); + + describe('removeChild', function() { + function removeChild() { + par2.removeChild(par); + } + + it('removing a tutorial that is not a child silently passes', function() { + var n = par2.children.length; + expect(removeChild).not.toThrow(); + expect(par2.children.length).toBe(n); + }); + + it("removing a child removes the child from the parent's 'children' property", function() { + tute.setParent(par2); + expect(par2.children.length).toBe(1); + + par2.removeChild(tute); + + expect(par2.children).not.toContain(tute); + expect(par2.children.length).toBe(0); + }); + + it("removing a child unsets the child's 'parent' property", function() { + expect(tute.parent).toBe(null); + }); + }); + + describe('various inheritance tests with addChild, setParent and removeChild', function() { + it('parenting and unparenting via addChild, setParent and removeChild makes sure inheritance is set accordingly', function() { + // unparent everything. + tute.setParent(null); + par.setParent(null); + par2.setParent(null); + + // let tute belong to par + tute.setParent(par); + expect(tute.parent).toBe(par); + expect(par2.children.length).toBe(0); + expect(par.children.length).toBe(1); + expect(par.children[0]).toBe(tute); + + // addChild tute to par2. its parent should now be par2, and + // it can't be the child of two parents + par2.addChild(tute); + expect(tute.parent).toBe(par2); + expect(par.children.length).toBe(0); + expect(par2.children.length).toBe(1); + expect(par2.children[0]).toBe(tute); + + // removeChild tute from par2. tute should now be unparented. + par2.removeChild(tute); + expect(tute.parent).toBe(null); + expect(par.children.length).toBe(0); + expect(par2.children.length).toBe(0); + }); + }); + + describe('parse', function() { + var markdownConfig = global.env.conf.markdown; + + function setMarkdownConfig(config) { + global.env.conf.markdown = config; + } + + beforeEach(function() { + setMarkdownConfig({parser: 'marked'}); + }); + + afterEach(function() { + global.env.conf.markdown = markdownConfig; + }); + + it('Tutorials with HTML type return content as-is', function() { + expect(par2.parse()).toBe('

      This is the second parent tutorial

      '); + }); + + it('Tutorials with MARKDOWN type go through the markdown parser, respecting configuration options', function() { + expect(par.parse()).toBe("

      This is the parent tutorial's content & stuff A_B X_Y

      "); + }); + + it('Tutorials with MARKDOWN type preserve &/</> entities', function() { + expect(markdownEntities.parse()) + .toBe('
      This Markdown tutorial contains HTML entities: & < >
      '); + }); + + it('Tutorials with unrecognised type are returned as-is', function() { + expect(tute.parse()).toBe(content); + }); + }); + }); + + describe('RootTutorial', function() { + it('should inherit from Tutorial', function() { + var root = new tutorial.RootTutorial(); + + expect(root instanceof tutorial.Tutorial).toBe(true); + }); + + it('should have a "getByName" method', function() { + expect(tutorial.RootTutorial.prototype.getByName).toBeDefined(); + expect(typeof tutorial.RootTutorial.prototype.getByName).toBe('function'); + }); + + describe('getByName', function() { + var root; + + beforeEach(function() { + root = new tutorial.RootTutorial(); + }); + + it('can retrieve tutorials by name', function() { + var myTutorial = new tutorial.Tutorial('myTutorial', '', tutorial.TYPES.HTML); + root._addTutorial(myTutorial); + + expect(root.getByName('myTutorial')).toBe(myTutorial); + }); + + it('returns nothing for non-existent tutorials', function() { + expect(root.getByName('asdf')).toBeFalsy(); + }); + + it('uses hasOwnProperty when it checks for the tutorial', function() { + expect(root.getByName('prototype')).toBeFalsy(); + }); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/tutorial/resolver.js b/third_party/jsdoc/test/specs/jsdoc/tutorial/resolver.js new file mode 100644 index 0000000000..cea68eee16 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/tutorial/resolver.js @@ -0,0 +1,232 @@ +'use strict'; + +describe('jsdoc/tutorial/resolver', function() { + var logger = require('jsdoc/util/logger'); + var resolver = require('jsdoc/tutorial/resolver'); + var tutorial = require('jsdoc/tutorial'); + + var childNames; + var constr; + var test; + var test2; + var test3; + var test4; + var test6; + + function resetRootTutorial() { + resolver.root = new tutorial.RootTutorial(); + } + + function loadTutorials() { + resetRootTutorial(); + + resolver.load(global.env.dirname + '/test/tutorials/tutorials'); + + childNames = resolver.root.children.map(function (t) { return t.name; }); + test = resolver.root.getByName('test'); + test2 = resolver.root.getByName('test2'); + test3 = resolver.root.getByName('test3'); + test4 = resolver.root.getByName('test4'); + test6 = resolver.root.getByName('test6'); + constr = resolver.root.getByName('constructor'); + } + + it('should exist', function() { + expect(resolver).toBeDefined(); + expect(typeof resolver).toBe('object'); + }); + + it('should export an "addTutorial" function', function() { + expect(resolver.addTutorial).toBeDefined(); + expect(typeof resolver.addTutorial).toBe('function'); + }); + + it('should export a "load" function', function() { + expect(resolver.load).toBeDefined(); + expect(typeof resolver.load).toBe('function'); + }); + + it('should export a "resolve" function', function() { + expect(resolver.resolve).toBeDefined(); + expect(typeof resolver.resolve).toBe('function'); + }); + + it('should export a "root" tutorial', function() { + expect(resolver.root).toBeDefined(); + expect(resolver.root instanceof tutorial.RootTutorial).toBe(true); + }); + + it('exported "root" tutorial should export a "getByName" function', function() { + expect(resolver.root.getByName).toBeDefined(); + expect(typeof resolver.root.getByName).toBe('function'); + }); + + // note: every time we addTutorial or run the resolver, we are *adding* + // to the root tutorial. + describe('addTutorial', function() { + var tute; + + beforeEach(function() { + resetRootTutorial(); + + tute = new tutorial.Tutorial('myTutorial', '', tutorial.TYPES.HTML); + resolver.addTutorial(tute); + }); + + afterEach(resetRootTutorial); + + it('should add a default parent of the root tutorial', function() { + expect(tute.parent).toBe(resolver.root); + }); + + it('should be added to the root tutorial as a child', function() { + expect(resolver.root.children).toContain(tute); + }); + }); + + describe('load', function() { + beforeEach(loadTutorials); + + afterEach(resetRootTutorial); + + it('does not, by default, recurse into subdirectories', function() { + expect(resolver.root.getByName('test_recursive')).toBeFalsy(); + }); + + it('recurses into subdirectories when the --recurse flag is used', function() { + var recurse = global.env.opts.recurse; + var recursiveTute; + + global.env.opts.recurse = true; + loadTutorials(); + recursiveTute = resolver.root.getByName('test_recursive'); + + expect(recursiveTute).toBeDefined(); + expect(recursiveTute instanceof tutorial.Tutorial).toBe(true); + + global.env.opts.recurse = recurse; + }); + + it('all tutorials are added, initially as top-level tutorials', function() { + // check they were added + expect(test).toBeDefined(); + expect(test2).toBeDefined(); + expect(test3).toBeDefined(); + expect(test4).toBeDefined(); + expect(test6).toBeDefined(); + expect(constr).toBeDefined(); + // check they are top-level in resolver.root + expect(childNames).toContain('test'); + expect(childNames).toContain('test2'); + expect(childNames).toContain('test3'); + expect(childNames).toContain('test4'); + expect(childNames).toContain('test6'); + }); + + it('tutorials with names equal to reserved keywords in JS still function as expected', function() { + expect(constr instanceof tutorial.Tutorial).toBe(true); + }); + + it('non-tutorials are skipped', function() { + expect(resolver.root.getByName('multiple')).toBeFalsy(); + expect(resolver.root.getByName('test5')).toBeFalsy(); + }); + + it('tutorial types are determined correctly', function() { + // test.html, test2.markdown, test3.html, test4.md, test6.xml + expect(test.type).toBe(tutorial.TYPES.HTML); + expect(test2.type).toBe(tutorial.TYPES.MARKDOWN); + expect(test3.type).toBe(tutorial.TYPES.HTML); + expect(test4.type).toBe(tutorial.TYPES.MARKDOWN); + expect(test6.type).toBe(tutorial.TYPES.HTML); + expect(constr.type).toBe(tutorial.TYPES.MARKDOWN); + }); + }); + + // resolve + // myTutorial + // constructor + // test + // |- test2 + // |- test6 + // |- test3 + // |- test4 + describe('resolve', function() { + beforeEach(function() { + spyOn(logger, 'error'); + spyOn(logger, 'warn'); + loadTutorials(); + resolver.resolve(); + }); + + afterEach(resetRootTutorial); + + it('hierarchy is resolved properly no matter how the children property is defined', function() { + // root has child 'test' + expect(resolver.root.children.length).toBe(2); + expect(resolver.root.children).toContain(test); + expect(resolver.root.children).toContain(constr); + expect(test.parent).toBe(resolver.root); + expect(constr.parent).toBe(resolver.root); + + // test has child 'test2' + expect(test.children.length).toBe(1); + expect(test.children).toContain(test2); + expect(test2.parent).toBe(test); + + // test2 has children test3, test6 + expect(test2.children.length).toBe(2); + expect(test2.children).toContain(test3); + expect(test2.children).toContain(test6); + expect(test3.parent).toBe(test2); + expect(test6.parent).toBe(test2); + + // test3 has child test4 + expect(test3.children.length).toBe(1); + expect(test3.children).toContain(test4); + expect(test4.parent).toBe(test3); + }); + + it('tutorials without configuration files have titles matching filenames', function() { + // test6.xml didn't have a metadata + expect(test6.title).toBe('test6'); + }); + + it('tutorials with configuration files have titles as specified in configuration', function() { + // test.json had info for just test.json + expect(test.title).toBe('Test tutorial'); + }); + + it('multiple tutorials can appear in a configuration file', function() { + expect(test2.title).toBe('Test 2'); + expect(test3.title).toBe('Test 3'); + expect(test4.title).toBe('Test 4'); + }); + + it('logs an error for missing tutorials', function() { + resolver.load(global.env.dirname + '/test/tutorials/incomplete'); + resolver.resolve(); + + expect(logger.error).toHaveBeenCalled(); + }); + + it('logs a warning for duplicate-named tutorials (e.g. test.md, test.html)', function() { + var tute = new tutorial.Tutorial('myTutorial', '', tutorial.TYPES.HTML); + resolver.addTutorial(tute); + resolver.addTutorial(tute); + + expect(logger.warn).toHaveBeenCalled(); + }); + + it('allows tutorials to be defined in one .json file and redefined in another', function() { + resolver.load(global.env.dirname + '/test/tutorials/duplicateDefined'); + resolver.resolve(); + + expect(logger.error).not.toHaveBeenCalled(); + expect(logger.warn).toHaveBeenCalled(); + // we don't check to see which one wins; it depends on the order in which the JS engine + // iterates over object keys + expect(resolver.root.getByName('asdf')).toBeDefined(); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/util/doop.js b/third_party/jsdoc/test/specs/jsdoc/util/doop.js new file mode 100644 index 0000000000..ebff9d9f79 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/util/doop.js @@ -0,0 +1,109 @@ +/*global describe, expect, it */ +'use strict'; + +describe('jsdoc/util/doop', function() { + var doop = require('jsdoc/util/doop'); + + it('should exist', function() { + expect(doop).toBeDefined(); + expect(typeof doop).toBe('function'); + }); + + it('should export a doop function for backwards compatibility', function() { + expect(doop.doop).toBeDefined(); + expect(typeof doop.doop).toBe('function'); + }); + + // deep-clones a simple object. + describe('doop', function() { + it("should return the input object if it's a value type or a function", function() { + // test a number... + expect(doop.doop(3)).toBe(3); + // test a string... + expect(doop.doop('asdf')).toBe('asdf'); + // test a boolean... + expect(doop.doop(true)).toBe(true); + // test a function... + var f = function () {}; + expect(doop.doop(f)).toBe(f); + }); + + it("should return a clone of an array", function() { + var inp = [1,2,3], + out = doop.doop(inp); + // toEqual is a comparison on properties; toBe is === comparison. + expect(inp).toEqual(out); + expect(inp).not.toBe(out); + }); + + it("should return a clone of an object", function() { + var inp = {a:1, b:2, 'asdf-fdsa': 3}; + var out = doop.doop(inp); + // toEqual is a comparison on properties; toBe is === comparison. + expect(inp).toEqual(out); + expect(inp).not.toBe(out); + }); + + it("should return an object with the same prototype as the original object", function() { + function Foo() {} + + var foo = new Foo(); + var bar = doop(foo); + expect( Object.getPrototypeOf(foo) ).toBe( Object.getPrototypeOf(bar) ); + }); + + // checks that a === b if it's not an object or array (or it's af function); + // otherwise recurses down into keys and compares them. + function compareForEquality(a, b) { + if (a instanceof Object && a.constructor != Function) { + // if it's an object and not a function, it should clone. + var keysA = Object.keys(a).sort(); + var keysB = Object.keys(b).sort(); + expect(keysA).toEqual(keysB); + for (var i = 0; i < keysA.length; ++i) { + compareForEquality(a[keysA[i]], b[keysB[i]]); + } + } else { + // otherwise, it should be exactly equal. + expect(a).toBe(b); + } + } + + it("should clone recursively", function() { + var inp = {a:1, b:2, 'asdf-fdsa': {a: 'fdsa', b: [1,2,3]}}; + var out = doop.doop(inp); + // toEqual is a comparison on properties; toBe is === comparison. + expect(inp).toEqual(out); + expect(inp).not.toBe(out); + // double-check + compareForEquality(inp, out); + }); + + it('should not clone non-enumerable properties', function() { + var clone; + var obj = { a: 1 }; + + Object.defineProperty(obj, 'foo', { + value: 2 + }); + + clone = doop(obj); + + expect(clone.foo).not.toBeDefined(); + }); + + it('should not create a circular reference if an object is seen more than once', function() { + var input = { a: {} }; + var output; + + function stringify() { + return JSON.stringify(output); + } + + input.a.circular = input.a; + output = doop(input); + + expect(stringify).not.toThrow(); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/util/dumper.js b/third_party/jsdoc/test/specs/jsdoc/util/dumper.js new file mode 100644 index 0000000000..e08d25c8e3 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/util/dumper.js @@ -0,0 +1,103 @@ +/*global describe: true, expect: true, it: true */ +describe("jsdoc/util/dumper", function() { + var common = {dumper: require('jsdoc/util/dumper')}; + + it("should exist", function() { + expect(common.dumper).toBeDefined(); + expect(typeof common.dumper).toEqual("object"); + }); + + it("should export a 'dump' function", function() { + expect(common.dumper.dump).toBeDefined(); + expect(typeof common.dumper.dump).toEqual("function"); + }); + + it("can dump string values", function() { + expect(common.dumper.dump('hello')).toEqual('"hello"'); + }); + + it("escapes double quotes in string values", function() { + expect(common.dumper.dump('hello "world"')).toEqual('"hello \\"world\\""', 'Double quotes should be escaped.'); + }); + + it("escapes newlines in string values", function() { + expect(common.dumper.dump('hello\nworld')).toEqual('"hello\\nworld"', 'Newlines should be escaped.'); + }); + + it("can dump number values", function() { + expect(common.dumper.dump(1)).toEqual('1'); + expect(common.dumper.dump(0.1)).toEqual('0.1'); + }); + + it("can dump boolean values", function() { + expect(common.dumper.dump(true)).toEqual('true'); + expect(common.dumper.dump(false)).toEqual('false'); + }); + + it("can dump null values", function() { + expect(common.dumper.dump(null)).toEqual('null'); + }); + + it("can dump undefined values", function() { + expect(common.dumper.dump(undefined)).toEqual('null'); + }); + + it("can dump regex values", function() { + expect(common.dumper.dump(/^[Ff]oo$/gi)).toEqual('""'); + }); + + it("can dump date values", function() { + expect(common.dumper.dump(new Date('January 1, 1901 GMT'))) + .toEqual('""'); + }); + + it("can dump function values", function() { + expect(common.dumper.dump(function myFunc(){})).toEqual('""'); + expect(common.dumper.dump(function(){})).toEqual('""'); + }); + + it("can dump array values", function() { + var actual = common.dumper.dump(["hello", "world"]), + expected = '[\n "hello",\n "world"\n]'; + + expect(actual).toEqual(expected); + }); + + it("can dump simple object values", function() { + var actual = common.dumper.dump({hello: "world"}), + expected = '{\n "hello": "world"\n}'; + + expect(actual).toEqual(expected); + }); + + it("can dump constructed instance values, not displaying prototype members", function() { + function Foo(name){ this.name = name; } + Foo.prototype.sayHello = function(){}; + + var actual = common.dumper.dump(new Foo('hello')), + expected = '{\n "name": "hello"\n}'; + + expect(actual).toEqual(expected); + }); + + it("can dump complex mixed values", function() { + function Foo(){} + + var actual = common.dumper.dump( + [undefined, null, new Foo(), 1, true, 'hello\n"world', new Error('oops'), /foo/gi, new Date('December 26, 2010 GMT'), {f: function myFunc(){}, o: {a:1}}] + ), + expected = '[\n null,\n null,\n {},\n 1,\n true,\n "hello\\n\\"world",\n {\n "message": "oops"\n },\n "",\n "",\n {\n "f": "",\n "o": {\n "a": 1\n }\n }\n]'; + + expect(actual).toEqual(expected); + }); + + it("doesn't crash on circular references", function() { + var a = {}; + a.b = a; + + var actual = common.dumper.dump(a), + expected = '{\n "b": ""\n}'; + + expect(actual).toEqual(expected); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/jsdoc/util/error.js b/third_party/jsdoc/test/specs/jsdoc/util/error.js new file mode 100644 index 0000000000..1d21b5f33d --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/util/error.js @@ -0,0 +1,38 @@ +/*global beforeEach, describe, expect, it, spyOn */ +'use strict'; + +describe('jsdoc/util/error', function() { + var error = require('jsdoc/util/error'); + var handle = error.handle; + var logger = require('jsdoc/util/logger'); + + it('should exist', function() { + expect(error).toBeDefined(); + expect(typeof error).toBe('object'); + }); + + it('should export a "handle" function', function() { + expect(handle).toBeDefined(); + expect(typeof handle).toBe('function'); + }); + + describe('handle', function() { + it('should not throw', function() { + expect(handle).not.toThrow(); + }); + + it('should log messages with logger.error()', function() { + spyOn(logger, 'error'); + handle('test'); + + expect(logger.error).toHaveBeenCalled(); + }); + + it('should use special formatting for Error instances', function() { + spyOn(logger, 'error'); + handle( new Error('Oh no!') ); + + expect(logger.error).toHaveBeenCalledWith('Error: Oh no!'); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/util/logger.js b/third_party/jsdoc/test/specs/jsdoc/util/logger.js new file mode 100644 index 0000000000..f335a25f7c --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/util/logger.js @@ -0,0 +1,200 @@ +/*global afterEach, describe, expect, it, jasmine */ +describe('jsdoc/util/logger', function() { + var logger = require('jsdoc/util/logger'); + + var loggerArgs = ['foo bar %s', 'hello']; + + it('should exist', function() { + expect(logger).toBeDefined(); + expect(typeof logger).toBe('object'); + }); + + it('should inherit from EventEmitter', function() { + var EventEmitter = require('events').EventEmitter; + + expect(logger instanceof EventEmitter).toBe(true); + }); + + it('should export a "debug" method', function() { + expect(logger.debug).toBeDefined(); + expect(typeof logger.debug).toBe('function'); + }); + + it('should export an "error" method', function() { + expect(logger.error).toBeDefined(); + expect(typeof logger.error).toBe('function'); + }); + + it('should export a "fatal" method', function() { + expect(logger.fatal).toBeDefined(); + expect(typeof logger.fatal).toBe('function'); + }); + + it('should export a "getLevel" method', function() { + expect(logger.getLevel).toBeDefined(); + expect(typeof logger.getLevel).toBe('function'); + }); + + it('should export an "info" method', function() { + expect(logger.info).toBeDefined(); + expect(typeof logger.info).toBe('function'); + }); + + it('should export a "LEVELS" object', function() { + expect(logger.LEVELS).toBeDefined(); + expect(typeof logger.LEVELS).toBe('object'); + }); + + it('should export a "setLevel" method', function() { + expect(logger.setLevel).toBeDefined(); + expect(typeof logger.setLevel).toBe('function'); + }); + + it('should export a "verbose" method', function() { + expect(logger.verbose).toBeDefined(); + expect(typeof logger.verbose).toBe('function'); + }); + + it('should export a "warn" method', function() { + expect(logger.warn).toBeDefined(); + expect(typeof logger.warn).toBe('function'); + }); + + // helpers for testing logger methods + function eventIsEmitted(name) { + var called = false; + + logger.once('logger:' + name, function() { + called = true; + }); + logger[name](); + + expect(called).toBe(true); + } + + function eventGetsArguments(name) { + var args; + + logger.once('logger:' + name, function() { + args = Array.prototype.slice.call(arguments, 0); + }); + logger[name](loggerArgs[0], loggerArgs[1]); + + expect(args).toBeDefined(); + expect( Array.isArray(args) ).toBe(true); + expect(args[0]).toBe(loggerArgs[0]); + expect(args[1]).toBe(loggerArgs[1]); + } + + describe('debug', function() { + var methodName = 'debug'; + + it('should cause the logger to emit the correct event', function() { + eventIsEmitted(methodName); + }); + + it('should pass its arguments to listeners', function() { + eventGetsArguments(methodName); + }); + }); + + describe('error', function() { + var methodName = 'error'; + + it('should cause the logger to emit the correct event', function() { + eventIsEmitted(methodName); + }); + + it('should pass its arguments to listeners', function() { + eventGetsArguments(methodName); + }); + }); + + describe('fatal', function() { + var methodName = 'fatal'; + + it('should cause the logger to emit the correct event', function() { + eventIsEmitted(methodName); + }); + + it('should pass its arguments to listeners', function() { + eventGetsArguments(methodName); + }); + }); + + describe('getLevel', function() { + it('should return LEVELS.SILENT when we are running tests', function() { + expect( logger.getLevel() ).toBe(logger.LEVELS.SILENT); + }); + }); + + describe('info', function() { + var methodName = 'info'; + + it('should cause the logger to emit the correct event', function() { + eventIsEmitted(methodName); + }); + + it('should pass its arguments to listeners', function() { + eventGetsArguments(methodName); + }); + }); + + describe('LEVELS', function() { + var LEVELS = logger.LEVELS; + + it('should include the correct properties', function() { + expect(LEVELS.VERBOSE).toBeDefined(); + expect(LEVELS.DEBUG).toBeDefined(); + expect(LEVELS.INFO).toBeDefined(); + expect(LEVELS.WARN).toBeDefined(); + expect(LEVELS.ERROR).toBeDefined(); + expect(LEVELS.SILENT).toBeDefined(); + }); + + it('should weight the logging levels correctly relative to one another', function() { + expect(LEVELS.VERBOSE).toBeGreaterThan(LEVELS.DEBUG); + expect(LEVELS.DEBUG).toBeGreaterThan(LEVELS.INFO); + expect(LEVELS.INFO).toBeGreaterThan(LEVELS.WARN); + expect(LEVELS.WARN).toBeGreaterThan(LEVELS.ERROR); + expect(LEVELS.ERROR).toBeGreaterThan(LEVELS.SILENT); + }); + }); + + describe('setLevel', function() { + var oldLevel = logger.getLevel(); + + afterEach(function() { + logger.setLevel(oldLevel); + }); + + it('should update the log level', function() { + logger.setLevel(logger.LEVELS.VERBOSE); + expect( logger.getLevel() ).toBe(logger.LEVELS.VERBOSE); + }); + }); + + describe('verbose', function() { + var methodName = 'verbose'; + + it('should cause the logger to emit the correct event', function() { + eventIsEmitted(methodName); + }); + + it('should pass its arguments to listeners', function() { + eventGetsArguments(methodName); + }); + }); + + describe('warn', function() { + var methodName = 'warn'; + + it('should cause the logger to emit the correct event', function() { + eventIsEmitted(methodName); + }); + + it('should pass its arguments to listeners', function() { + eventGetsArguments(methodName); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/util/markdown.js b/third_party/jsdoc/test/specs/jsdoc/util/markdown.js new file mode 100644 index 0000000000..70062ed60b --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/util/markdown.js @@ -0,0 +1,101 @@ +/*global afterEach, describe, env, expect, it, spyOn */ +'use strict'; + +describe('jsdoc/util/markdown', function() { + var markdown = require('jsdoc/util/markdown'); + + it('should exist', function() { + expect(markdown).toBeDefined(); + expect(typeof markdown).toEqual('object'); + }); + + it('should export a "getParser" function', function() { + expect(markdown.getParser).toBeDefined(); + expect(typeof markdown.getParser).toEqual('function'); + }); + + describe('getParser', function() { + var originalMarkdownConf = env.conf.markdown; + + function setMarkdownConf(hash) { + env.conf.markdown = hash; + } + + afterEach(function() { + env.conf.markdown = originalMarkdownConf; + }); + + it('should retrieve a function when called with default settings', function() { + var storage = setMarkdownConf({}); + + var parser = markdown.getParser(); + expect(typeof parser).toEqual('function'); + + setMarkdownConf({parser: 'marked'}); + parser = markdown.getParser(); + expect(typeof parser).toEqual('function'); + }); + + it('should use the marked parser when evilstreak is requested', function() { + var storage = setMarkdownConf({parser: 'evilstreak'}); + var parser = markdown.getParser(); + expect(parser._parser).toEqual('marked'); + }); + + it('should use the marked parser when requested', function() { + var storage = setMarkdownConf({parser: 'marked'}); + var parser = markdown.getParser(); + expect(parser._parser).toEqual('marked'); + }); + + it('should use the marked parser when GFM is requested', function() { + var storage = setMarkdownConf({parser: 'gfm'}); + var parser = markdown.getParser(); + expect(parser._parser).toEqual('marked'); + }); + + it('should log an error if an unrecognized Markdown parser is requested', function() { + var logger = require('jsdoc/util/logger'); + var parser; + var storage = setMarkdownConf({parser: 'not-a-real-markdown-parser'}); + + spyOn(logger, 'error'); + + parser = markdown.getParser(); + + expect(logger.error).toHaveBeenCalled(); + }); + + it('should not apply formatting to inline tags when the marked parser is enabled', function() { + var storage = setMarkdownConf({parser: 'marked'}); + var parser = markdown.getParser(); + + // get the marked parser and do the test + expect(parser('{@link MyClass#_x} and {@link MyClass#_y}')).toEqual( + '

      {@link MyClass#_x} and {@link MyClass#_y}

      '); + }); + + it('should not automatically convert HTTP/HTTPS URLs to links', function() { + var parser = markdown.getParser(); + + expect(parser('Visit {@link http://usejsdoc.com}.')) + .toBe('

      Visit {@link http://usejsdoc.com}.

      '); + expect(parser('Visit {@link https://google.com}.')) + .toBe('

      Visit {@link https://google.com}.

      '); + }); + + it('should escape characters in code blocks as needed', function() { + var parser = markdown.getParser(); + var markdownText = '' + + '```html\n' + + '

      Sample \'HTML.\'

      \n' + + '```'; + var convertedText = '' + + '
      ' +
      +                '<p><a href="#">Sample \'HTML.\'</a></p>' +
      +                '
      '; + + expect(parser(markdownText)).toBe(convertedText); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/util/runtime.js b/third_party/jsdoc/test/specs/jsdoc/util/runtime.js new file mode 100644 index 0000000000..2efb1a816f --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/util/runtime.js @@ -0,0 +1,62 @@ +/*global describe: true, expect: true, it: true, xit: true */ +describe("jsdoc/util/runtime", function() { + var runtime = require('jsdoc/util/runtime'); + var isRhino; + var isNode; + + it("should exist", function() { + expect(runtime).toBeDefined(); + expect(typeof runtime).toEqual('object'); + }); + + it("should export a 'RHINO' constant", function() { + expect(runtime.RHINO).toBeDefined(); + expect(typeof runtime.RHINO).toEqual('string'); + }); + + it("should export a 'NODE' constant", function() { + expect(runtime.NODE).toBeDefined(); + expect(typeof runtime.NODE).toEqual('string'); + }); + it("should export an 'isRhino' function", function() { + expect(runtime.isRhino).toBeDefined(); + expect(typeof runtime.isRhino).toEqual('function'); + }); + + it("should export an 'isNode' function", function() { + expect(runtime.isNode).toBeDefined(); + expect(typeof runtime.isNode).toEqual('function'); + }); + + describe("isRhino", function() { + isRhino = runtime.isRhino(); + + it("should return a boolean", function() { + expect(typeof isRhino).toEqual('boolean'); + }); + + it("should return the opposite value from isNode()", function() { + if (isNode === undefined) { + isNode = runtime.isNode(); + } + + expect(!isRhino).toBe(isNode); + }); + }); + + describe("isNode", function() { + isNode = runtime.isNode(); + + it("should return a boolean", function() { + expect(typeof isNode).toEqual('boolean'); + }); + + it("should return the opposite value from isRhino()", function() { + if (isRhino === undefined) { + isRhino = runtime.isRhino(); + } + + expect(!isNode).toBe(isRhino); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/jsdoc/util/templateHelper.js b/third_party/jsdoc/test/specs/jsdoc/util/templateHelper.js new file mode 100644 index 0000000000..c4586b1027 --- /dev/null +++ b/third_party/jsdoc/test/specs/jsdoc/util/templateHelper.js @@ -0,0 +1,1473 @@ +/*eslint quotes:0 */ +'use strict'; + +var hasOwnProp = Object.prototype.hasOwnProperty; + +describe("jsdoc/util/templateHelper", function() { + var helper = require('jsdoc/util/templateHelper'); + var doclet = require('jsdoc/doclet'); + var doop = require('jsdoc/util/doop'); + var logger = require('jsdoc/util/logger'); + var resolver = require('jsdoc/tutorial/resolver'); + var taffy = require('taffydb').taffy; + + helper.registerLink('test', 'path/to/test.html'); + + it("should exist", function() { + expect(helper).toBeDefined(); + expect(typeof helper).toBe('object'); + }); + + it("should export a 'setTutorials' function", function() { + expect(helper.setTutorials).toBeDefined(); + expect(typeof helper.setTutorials).toBe("function"); + }); + + it("should export a 'globalName' property", function() { + expect(helper.globalName).toBeDefined(); + expect(typeof helper.globalName).toBe("string"); + }); + + it("should export a 'fileExtension' property", function() { + expect(helper.fileExtension).toBeDefined(); + expect(typeof helper.fileExtension).toBe("string"); + }); + + it("should export a 'scopeToPunc' property", function() { + expect(helper.scopeToPunc).toBeDefined(); + expect(typeof helper.scopeToPunc).toBe("object"); + }); + + it("should export a 'getUniqueFilename' function", function() { + expect(helper.getUniqueFilename).toBeDefined(); + expect(typeof helper.getUniqueFilename).toBe("function"); + }); + + it("should export a 'getUniqueId' function", function() { + expect(helper.getUniqueId).toBeDefined(); + expect(typeof helper.getUniqueId).toBe('function'); + }); + + it("should export a 'longnameToUrl' property", function() { + expect(helper.longnameToUrl).toBeDefined(); + expect(typeof helper.longnameToUrl).toBe("object"); + }); + + it("should export a 'linkto' function", function() { + expect(helper.linkto).toBeDefined(); + expect(typeof helper.linkto).toBe("function"); + }); + + it("should export an 'htmlsafe' function", function() { + expect(helper.htmlsafe).toBeDefined(); + expect(typeof helper.htmlsafe).toBe("function"); + }); + + it("should export a 'find' function", function() { + expect(helper.find).toBeDefined(); + expect(typeof helper.find).toBe("function"); + }); + + it("should export a 'getMembers' function", function() { + expect(helper.getMembers).toBeDefined(); + expect(typeof helper.getMembers).toBe("function"); + }); + + it("should export a 'getAttribs' function", function() { + expect(helper.getAttribs).toBeDefined(); + expect(typeof helper.getAttribs).toBe("function"); + }); + + it("should export a 'getSignatureTypes' function", function() { + expect(helper.getSignatureTypes).toBeDefined(); + expect(typeof helper.getSignatureTypes).toBe("function"); + }); + + it("should export a 'getSignatureParams' function", function() { + expect(helper.getSignatureParams).toBeDefined(); + expect(typeof helper.getSignatureParams).toBe("function"); + }); + + it("should export a 'getSignatureReturns' function", function() { + expect(helper.getSignatureReturns).toBeDefined(); + expect(typeof helper.getSignatureReturns).toBe("function"); + }); + + it("should export a 'getAncestors' function", function() { + expect(helper.getAncestors).toBeDefined(); + expect(typeof helper.getAncestors).toBe('function'); + }); + + it("should export a 'getAncestorLinks' function", function() { + expect(helper.getAncestorLinks).toBeDefined(); + expect(typeof helper.getAncestorLinks).toBe("function"); + }); + + it("should export a 'addEventListeners' function", function() { + expect(helper.addEventListeners).toBeDefined(); + expect(typeof helper.addEventListeners).toBe("function"); + }); + + it("should export a 'prune' function", function() { + expect(helper.prune).toBeDefined(); + expect(typeof helper.prune).toBe("function"); + }); + + it("should export a 'registerLink' function", function() { + expect(helper.registerLink).toBeDefined(); + expect(typeof helper.registerLink).toBe("function"); + }); + + it("should export a 'tutorialToUrl' function", function() { + expect(helper.tutorialToUrl).toBeDefined(); + expect(typeof helper.tutorialToUrl).toBe("function"); + }); + + it("should export a 'toTutorial' function", function() { + expect(helper.toTutorial).toBeDefined(); + expect(typeof helper.toTutorial).toBe("function"); + }); + + it("should export a 'resolveLinks' function", function() { + expect(helper.resolveLinks).toBeDefined(); + expect(typeof helper.resolveLinks).toBe("function"); + }); + + it("should export a 'resolveAuthorLinks' function", function() { + expect(helper.resolveAuthorLinks).toBeDefined(); + expect(typeof helper.resolveAuthorLinks).toBe("function"); + }); + + it("should export a 'createLink' function", function() { + expect(helper.createLink).toBeDefined(); + expect(typeof helper.createLink).toBe("function"); + }); + + it('should export a "longnamesToTree" function', function() { + expect(helper.longnamesToTree).toBeDefined(); + expect(typeof helper.longnamesToTree).toBe('function'); + }); + + describe("setTutorials", function() { + // used in tutorialToUrl, toTutorial. + it("setting tutorials to null causes all tutorial lookups to fail", function() { + // bit of a dodgy test but the best I can manage. setTutorials doesn't do much. + helper.setTutorials(null); + // should throw error: no 'getByName' in tutorials. + expect(function () { return helper.tutorialToUrl('asdf'); }).toThrow(); + }); + + it("setting tutorials to the root tutorial object lets lookups work", function() { + helper.setTutorials(resolver.root); + spyOn(resolver.root, 'getByName'); + helper.tutorialToUrl('asdf'); + + expect(resolver.root.getByName).toHaveBeenCalled(); + }); + }); + + describe("globalName", function() { + it("should equal 'global'", function() { + expect(helper.globalName).toBe('global'); + }); + }); + + describe("fileExtension", function() { + it("should equal '.html'", function() { + expect(helper.fileExtension).toBe('.html'); + }); + }); + + describe("scopeToPunc", function() { + it("should map 'static' to '.', 'inner', to '~', 'instance' to '#'", function() { + expect(helper.scopeToPunc).toEqual({static: '.', inner: '~', instance: '#'}); + }); + }); + + describe("getUniqueFilename", function() { + // TODO: needs more tests for unusual values and things that get special treatment (such as + // inner members) + it('should convert a simple string into the string plus the default extension', function() { + var filename = helper.getUniqueFilename('BackusNaur'); + expect(filename).toBe('BackusNaur.html'); + }); + + it('should replace slashes with underscores', function() { + var filename = helper.getUniqueFilename('tick/tock'); + expect(filename).toBe('tick_tock.html'); + }); + + it('should replace other problematic characters with underscores', function() { + var filename = helper.getUniqueFilename('a very strange \\/?*:|\'"<> filename'); + expect(filename).toBe('a very strange __________ filename.html'); + }); + + it('should not allow a filename to start with an underscore', function() { + expect( helper.getUniqueFilename('') ).toBe('X_.html'); + }); + + it('should not return the same filename twice', function() { + var name = 'polymorphic'; + var filename1 = helper.getUniqueFilename(name); + var filename2 = helper.getUniqueFilename(name); + + expect(filename1).not.toBe(filename2); + }); + + it('should not consider the same name with different letter case to be unique', function() { + var camel = 'myJavaScriptIdentifier'; + var pascal = 'MyJavaScriptIdentifier'; + var filename1 = helper.getUniqueFilename(camel); + var filename2 = helper.getUniqueFilename(pascal); + + expect( filename1.toLowerCase() ).not.toBe( filename2.toLowerCase() ); + }); + + it('should remove variations from the longname before generating the filename', function() { + var filename = helper.getUniqueFilename('MyClass(foo, bar)'); + expect(filename).toBe('MyClass.html'); + }); + }); + + xdescribe('getUniqueId', function() { + // TODO + }); + + describe("longnameToUrl", function() { + it("is an object", function() { + expect(typeof helper.longnameToUrl).toBe('object'); + }); + + it("has an entry added into it by calling registerLink", function() { + helper.registerLink('MySymbol', 'asdf.html'); + expect(helper.longnameToUrl.MySymbol).toBeDefined(); + expect(helper.longnameToUrl.MySymbol).toBe('asdf.html'); + + delete helper.longnameToUrl.MySymbol; + }); + + it("adding an entry to it allows me to link with linkto", function() { + helper.longnameToUrl.foo2 = 'bar.html'; + expect(helper.linkto('foo2')).toBe('foo2'); + delete helper.longnameToUrl.foo2; + }); + }); + + describe("linkto", function() { + beforeEach(function() { + helper.longnameToUrl.linktoTest = 'test.html'; + helper.longnameToUrl.LinktoFakeClass = 'fakeclass.html'; + }); + + afterEach(function() { + delete helper.longnameToUrl.linktoTest; + delete helper.longnameToUrl.LinktoFakeClass; + }); + + it('returns the longname if only the longname is specified and has no URL', function() { + var link = helper.linkto('example'); + expect(link).toBe('example'); + }); + + it('returns the link text if only the link text is specified', function() { + var link = helper.linkto(null, 'link text'); + expect(link).toBe('link text'); + }); + + it('returns the link text if the longname does not have a URL, and both the longname and ' + + 'link text are specified', function() { + var link = helper.linkto('example', 'link text'); + expect(link).toBe('link text'); + }); + + it('uses the longname as the link text if no link text is provided', function() { + var link = helper.linkto('linktoTest'); + expect(link).toBe('linktoTest'); + }); + + it('uses the link text if it is specified', function() { + var link = helper.linkto('linktoTest', 'link text'); + expect(link).toBe('link text'); + }); + + it('includes a "class" attribute in the link if a class is specified', function() { + var link = helper.linkto('linktoTest', 'link text', 'myclass'); + expect(link).toBe('link text'); + }); + + it('is careful with longnames that are reserved words in JS', function() { + // we don't have a registered link for 'constructor' so it should return the text 'link text'. + var link = helper.linkto('constructor', 'link text'); + expect(typeof link).toBe('string'); + expect(link).toBe('link text'); + }); + + it('works correctly with type applications if only the longname is specified', function() { + var link = helper.linkto('Array.'); + expect(link).toBe('Array.<LinktoFakeClass>'); + }); + + it('works correctly with type applications if a class is not specified', function() { + var link = helper.linkto('Array.', 'link text'); + expect(link).toBe('Array.<LinktoFakeClass>'); + }); + + it('works correctly with type applications if a class is specified', function() { + var link = helper.linkto('Array.', 'link text', 'myclass'); + expect(link).toBe('Array.<LinktoFakeClass' + + '>'); + }); + + it('works correctly with type applications that include a type union', function() { + var link = helper.linkto('Array.<(linktoTest|LinktoFakeClass)>', 'link text'); + expect(link).toBe('Array.<(linktoTest|' + + 'LinktoFakeClass)>'); + }); + + it('works correctly with type unions that are not enclosed in parentheses', function() { + var link = helper.linkto('linktoTest|LinktoFakeClass', 'link text'); + expect(link).toBe('(linktoTest|' + + 'LinktoFakeClass)'); + }); + + it('does not try to parse a longname starting with as a type application', + function() { + spyOn(logger, 'error'); + + helper.linkto('~foo'); + expect(logger.error).not.toHaveBeenCalled(); + }); + + it('returns a link when a URL is specified', function() { + var link = helper.linkto('http://example.com'); + expect(link).toBe('http://example.com'); + }); + + it('returns a link if a URL wrapped in angle brackets is specified', function() { + var link = helper.linkto(''); + expect(link).toBe('http://example.com'); + }); + + it('returns a link with link text if a URL and link text are specified', function() { + var link = helper.linkto('http://example.com', 'text'); + expect(link).toBe('text'); + }); + + it('returns a link with a fragment ID if a URL and fragment ID are specified', function() { + var link = helper.linkto('LinktoFakeClass', null, null, 'fragment'); + expect(link).toBe('LinktoFakeClass'); + }); + + it('returns the original text if an inline {@link} tag is specified', function() { + var link; + var text = '{@link Foo}'; + + function getLink() { + link = helper.linkto(text); + } + + // make sure we're not trying to parse the inline link as a type expression + expect(getLink).not.toThrow(); + // linkto doesn't process {@link} tags + expect(link).toBe(text); + }); + }); + + describe("htmlsafe", function() { + it('should convert all occurences of < to <', function() { + var inp = '

      Potentially dangerous.

      ', + out = helper.htmlsafe(inp); + expect(out).toBe('<h1>Potentially dangerous.</h1>'); + }); + + it('should convert all occurrences of & to &', function() { + var input = 'foo && bar & baz;'; + expect( helper.htmlsafe(input) ).toBe('foo && bar & baz;'); + }); + + it('should not double-convert ampersands', function() { + var input = '

      Foo & Friends

      '; + expect( helper.htmlsafe(input) ).toBe('<h1>Foo & Friends</h1>'); + }); + }); + + describe("find", function() { + var array = [ + // match + { number: 2, A: true }, + // match + { number: 1, A: true, D: 'hello', Q: false }, + // match + { number: 3, A: 'maybe', squiggle: '?' }, + // no match (number not in spec) + { number: 4, A: true }, + // no match (missing top-level property) + { A: true } + ]; + var matches = array.slice(0, 3); + var spec = { number: [1, 2, 3], A: [true, 'maybe'] }; + + it('should find the requested items', function() { + expect( helper.find(taffy(array), spec) ).toEqual(matches); + }); + }); + + // we can't use toEqual() because TaffyDB adds its own stuff to the array it returns. + // instead, we make sure arrays a and b are the same length, and that each object in + // array b has all the properties of the corresponding object in array a + // used for getMembers and prune tests. + function compareObjectArrays(a, b) { + expect(a.length).toEqual(b.length); + + for (var i = 0, l = a.length; i < l; i++) { + for (var prop in a[i]) { + if ( hasOwnProp.call(a[i], prop) ) { + expect(b[i][prop]).toBeDefined(); + expect(a[i][prop]).toEqual(b[i][prop]); + } + } + } + } + describe("getMembers", function() { + // instead parse a file from fixtures and verify it? + var classes = [ + {kind: 'class'}, // global + {kind: 'class', memberof: 'SomeNamespace'} // not global + ]; + var externals = [ + {kind: 'external', name: 'foo'} + ]; + var events = [ + {kind: 'event'} + ]; + var mixins = [ + {kind: 'mixin'} + ]; + var modules = [ + {kind: 'module'} + ]; + var namespaces = [ + {kind: 'namespace'} + ]; + var miscGlobal = [ + {kind: 'function'}, + {kind: 'member'}, + {kind: 'constant'}, + {kind: 'typedef'} + ]; + var miscNonGlobal = [ + {kind: 'constant', memberof: 'module:one/two'}, + {kind: 'function', name: 'module:foo', longname: 'module:foo'}, + {kind: 'member', name: 'module:bar', longname: 'module:bar'} + ]; + var misc = miscGlobal.concat(miscNonGlobal); + var array = classes.concat(externals.concat(events.concat(mixins.concat(modules.concat(namespaces.concat(misc)))))); + var data = taffy(array); + var members = helper.getMembers(data); + + // check the output object has properties as expected. + it("should have a 'classes' property", function() { + expect(members.classes).toBeDefined(); + }); + + it("should have a 'externals' property", function() { + expect(members.externals).toBeDefined(); + }); + + it("should have a 'events' property", function() { + expect(members.events).toBeDefined(); + }); + + it("should have a 'globals' property", function() { + expect(members.globals).toBeDefined(); + }); + + it("should have a 'mixins' property", function() { + expect(members.mixins).toBeDefined(); + }); + + it("should have a 'modules' property", function() { + expect(members.modules).toBeDefined(); + }); + + it("should have a 'namespaces' property", function() { + expect(members.namespaces).toBeDefined(); + }); + + // check that things were found properly. + it("classes are detected", function() { + compareObjectArrays(classes, members.classes); + }); + + it("externals are detected", function() { + compareObjectArrays(externals, members.externals); + }); + + it("events are detected", function() { + compareObjectArrays(events, members.events); + }); + + it("mixins are detected", function() { + compareObjectArrays(mixins, members.mixins); + }); + + it("modules are detected", function() { + compareObjectArrays(modules, members.modules); + }); + + it("namespaces are detected", function() { + compareObjectArrays(namespaces, members.namespaces); + }); + + it("globals are detected", function() { + compareObjectArrays(miscGlobal, members.globals); + }); + }); + + describe("getAttribs", function() { + var doc, attribs; + + it('should return an array of strings', function() { + doc = new doclet.Doclet('/** ljklajsdf */', {}); + attribs = helper.getAttribs(doc); + expect(Array.isArray(attribs)).toBe(true); + }); + + // tests is an object of test[doclet src] = + // if false, we expect attribs to either not contain anything in whatNotToContain, + // or be empty (if whatNotToContain was not provided). + function doTests(tests, whatNotToContain) { + for (var src in tests) { + if (tests.hasOwnProperty(src)) { + doc = new doclet.Doclet('/** ' + src + ' */', {}); + attribs = helper.getAttribs(doc); + if (tests[src]) { + expect(attribs).toContain(tests[src]); + } else { + if (Array.isArray(whatNotToContain)) { + for (var i = 0; i < whatNotToContain.length; ++i) { + expect(attribs).not.toContain(whatNotToContain[i]); + } + } else { + expect(attribs.length).toBe(0); + } + } + } + } + } + + it('should detect if a doclet is virtual', function() { + var tests = { + 'My constant. \n @virtual': 'abstract', + 'asdf': false + }; + doTests(tests); + }); + + it("should detect if a doclet's access is not public", function() { + var tests = {'@private': 'private', + '@access private': 'private', + '@protected': 'protected', + '@access protected': 'protected', + '@public': false, + '@access public': false, + 'asdf': false + }; + doTests(tests); + }); + + it("should detect if a doclet's scope is inner or static AND it is a function or member or constant", function() { + var tests = { + // by default these are members + '@inner': 'inner', + '@instance': false, + '@global': false, + '@static': 'static', + '@name Asdf.fdsa': 'static', + '@name Outer~inner': 'inner', + '@name Fdsa#asdf': false, + '@name .log': false, + // some tests with functions and constants + '@const Asdf#FOO': false, + '@const Asdf\n@inner': 'inner', + '@function Asdf#myFunction': false, + '@function Fdsa.MyFunction': 'static', + '@function Fdsa': false, + // these are not functions or members or constants, they should not have their scope recorded. + '@namespace Fdsa\n@inner': false, + '@class asdf': false + }; + doTests(tests, ['inner', 'static', 'global', 'instance']); + }); + + it("should detect if a doclet is readonly (and its kind is 'member')", function() { + var tests = { + 'asdf\n @readonly': 'readonly', + 'asdf': false, + '@name Fdsa#foo\n@readonly': 'readonly', + // kind is not 'member'. + '@const asdf\n@readonly': 'constant', + '@function asdf\n@readonly': false, + '@function Asdf#bar\n@readonly': false + }; + doTests(tests, 'readonly'); + }); + + it("should detect if the doclet is a for constant", function() { + var tests = { + 'Enum. @enum\n@constant': 'constant', + '@function Foo#BAR\n@const': 'constant', + '@const Asdf': 'constant' + }; + doTests(tests, 'constant'); + }); + + it("should detect multiple attributes", function() { + var doc = new doclet.Doclet('/** @const module:fdsa~FOO\n@readonly\n@private */', {}); + attribs = helper.getAttribs(doc); + expect(attribs).toContain('private'); + //expect(attribs).toContain('readonly'); // kind is 'constant' not 'member'. + expect(attribs).toContain('constant'); + expect(attribs).toContain('inner'); + }); + + it('should return an empty array for null values', function() { + var emptyAttribs; + + function attribs() { + return helper.getAttribs(); + } + + expect(attribs).not.toThrow(); + + emptyAttribs = attribs(); + expect( Array.isArray(emptyAttribs) ).toBe(true); + expect(emptyAttribs.length).toBe(0); + }); + }); + + describe("getSignatureTypes", function() { + // returns links to allowed types for a doclet. + it("returns an empty array if the doclet has no specified type", function() { + var doc = new doclet.Doclet('/** @const ASDF */', {}), + types = helper.getSignatureTypes(doc); + + expect(Array.isArray(types)).toBe(true); + expect(types.length).toBe(0); + }); + + it("returns a string array of the doclet's types", function() { + var doc = new doclet.Doclet('/** @const {number|Array.} ASDF */', {}), + types = helper.getSignatureTypes(doc); + + expect(types.length).toBe(2); + expect(types).toContain('number'); + expect(types).toContain(helper.htmlsafe('Array.')); // should be HTML safe + }); + + it("creates links for types if relevant", function() { + // make some links. + helper.longnameToUrl.MyClass = 'MyClass.html'; + + var doc = new doclet.Doclet('/** @const {MyClass} ASDF */', {}), + types = helper.getSignatureTypes(doc); + expect(types.length).toBe(1); + expect(types).toContain('MyClass'); + + delete helper.longnameToUrl.MyClass; + }); + + it("uses the cssClass parameter for links if it is provided", function() { + // make some links. + helper.longnameToUrl.MyClass = 'MyClass.html'; + + var doc = new doclet.Doclet('/** @const {MyClass} ASDF */', {}), + types = helper.getSignatureTypes(doc, 'myCSSClass'); + expect(types.length).toBe(1); + expect(types).toContain('MyClass'); + + delete helper.longnameToUrl.MyClass; + }); + }); + + describe("getSignatureParams", function() { + // retrieves parameter names. + // if css class is provided, optional parameters are wrapped in a with that class. + it("returns an empty array if the doclet has no specified type", function() { + var doc = new doclet.Doclet('/** @function myFunction */', {}), + params = helper.getSignatureParams(doc); + expect(Array.isArray(params)).toBe(true); + expect(params.length).toBe(0); + }); + + it("returns a string array of the doclet's parameter names", function() { + var doc = new doclet.Doclet('/** @function myFunction\n @param {string} foo - asdf. */', {}), + params = helper.getSignatureParams(doc); + expect(params.length).toBe(1); + expect(params).toContain('foo'); + }); + + it("wraps optional parameters in if optClass is provided", function() { + var doc = new doclet.Doclet( + '/** @function myFunction\n' + + ' * @param {boolean} foo - explanation.\n' + + ' * @param {number} [bar=1] - another explanation.\n' + + ' * @param {string} [baz] - another explanation.\n' + + ' */', {}), + params = helper.getSignatureParams(doc, 'cssClass'); + + expect(params.length).toBe(3); + expect(params).toContain('foo'); + expect(params).toContain('bar'); + expect(params).toContain('baz'); + }); + + it("doesn't wrap optional parameters in if optClass is not provided", function() { + var doc = new doclet.Doclet( + '/** @function myFunction\n' + + ' * @param {boolean} foo - explanation.\n' + + ' * @param {number} [bar=1] - another explanation.\n' + + ' * @param {string} [baz] - another explanation.\n' + + ' */', {}), + params = helper.getSignatureParams(doc); + + expect(params.length).toBe(3); + expect(params).toContain('foo'); + expect(params).toContain('bar'); + expect(params).toContain('baz'); + }); + }); + + describe("getSignatureReturns", function() { + // retrieves links to types that the member can return. + + it("returns a value with correctly escaped HTML", function() { + var mockDoclet = { + returns: [ + { + type: { + names: [ + 'Array.' + ] + } + } + ] + }; + + var html = helper.getSignatureReturns(mockDoclet); + expect(html).not.toContain('Array.'); + expect(html).toContain('Array.<string>'); + }); + + it("returns an empty array if the doclet has no returns", function() { + var doc = new doclet.Doclet('/** @function myFunction */', {}), + returns = helper.getSignatureReturns(doc); + + expect(Array.isArray(returns)).toBe(true); + expect(returns.length).toBe(0); + }); + + it("returns an empty array if the doclet has @returns but with no type", function() { + var doc = new doclet.Doclet('/** @function myFunction\n@returns an interesting result.*/', {}), + returns = helper.getSignatureReturns(doc); + + expect(Array.isArray(returns)).toBe(true); + expect(returns.length).toBe(0); + }); + + it("creates links for return types if relevant", function() { + // make some links. + helper.longnameToUrl.MyClass = 'MyClass.html'; + + var doc = new doclet.Doclet('/** @function myFunction\n@returns {number|MyClass} an interesting result.*/', {}), + returns = helper.getSignatureReturns(doc); + + expect(returns.length).toBe(2); + expect(returns).toContain('MyClass'); + expect(returns).toContain('number'); + + delete helper.longnameToUrl.MyClass; + }); + + it("uses the cssClass parameter for links if it is provided", function() { + // make some links. + helper.longnameToUrl.MyClass = 'MyClass.html'; + + var doc = new doclet.Doclet('/** @function myFunction\n@returns {number|MyClass} an interesting result.*/', {}), + returns = helper.getSignatureReturns(doc, 'myCssClass'); + + expect(returns.length).toBe(2); + expect(returns).toContain('MyClass'); + expect(returns).toContain('number'); + + delete helper.longnameToUrl.MyClass; + }); + }); + + xdescribe('getAncestors', function() { + // TODO + }); + + describe("getAncestorLinks", function() { + // make a hierarchy. + var lackeys = new doclet.Doclet('/** @member lackeys\n@memberof module:mafia/gangs.Sharks~Henchman\n@instance*/', {}), + henchman = new doclet.Doclet('/** @class Henchman\n@memberof module:mafia/gangs.Sharks\n@inner */', {}), + gang = new doclet.Doclet('/** @namespace module:mafia/gangs.Sharks */', {}), + mafia = new doclet.Doclet('/** @module mafia/gangs */', {}), + data = taffy([lackeys, henchman, gang, mafia]); + + // register some links + it("returns an empty array if there are no ancestors", function() { + var links = helper.getAncestorLinks(data, mafia); + expect(Array.isArray(links)).toBe(true); + expect(links.length).toBe(0); + }); + + it("returns an array of ancestor names (with preceding punctuation) if there are ancestors, the direct ancestor with following punctuation too", function() { + var links = helper.getAncestorLinks(data, lackeys); + expect(links.length).toBe(3); + expect(links).toContain('~Henchman#'); + expect(links).toContain('.Sharks'); + expect(links).toContain('mafia/gangs'); + + links = helper.getAncestorLinks(data, henchman); + expect(links.length).toBe(2); + expect(links).toContain('.Sharks~'); + expect(links).toContain('mafia/gangs'); + + links = helper.getAncestorLinks(data, gang); + expect(links.length).toBe(1); + expect(links).toContain('mafia/gangs.'); + }); + + it("adds links if they exist", function() { + // register some links + helper.longnameToUrl['module:mafia/gangs'] = 'mafia_gangs.html'; + helper.longnameToUrl['module:mafia/gangs.Sharks~Henchman'] = 'henchman.html'; + + var links = helper.getAncestorLinks(data, lackeys); + expect(links.length).toBe(3); + expect(links).toContain('~Henchman#'); + expect(links).toContain('.Sharks'); + expect(links).toContain('mafia/gangs'); + + delete helper.longnameToUrl['module:mafia/gangs']; + delete helper.longnameToUrl['module:mafia/gangs.Sharks~Henchman']; + }); + + it("adds cssClass to any link", function() { + // register some links + helper.longnameToUrl['module:mafia/gangs'] = 'mafia_gangs.html'; + helper.longnameToUrl['module:mafia/gangs.Sharks~Henchman'] = 'henchman.html'; + + var links = helper.getAncestorLinks(data, lackeys, 'myClass'); + expect(links.length).toBe(3); + expect(links).toContain('~Henchman#'); + expect(links).toContain('.Sharks'); + expect(links).toContain('mafia/gangs'); + + delete helper.longnameToUrl['module:mafia/gangs']; + delete helper.longnameToUrl['module:mafia/gangs.Sharks~Henchman']; + }); + }); + + describe("addEventListeners", function() { + var doclets = ( taffy(doop(jasmine.getDocSetFromFile('test/fixtures/listenstag.js').doclets)) ), + ev = helper.find(doclets, {longname: 'module:myModule.event:MyEvent'})[0], + ev2 = helper.find(doclets, {longname: 'module:myModule~Events.event:Event2'})[0], + ev3 = helper.find(doclets, {longname: 'module:myModule#event:Event3'})[0]; + + helper.addEventListeners(doclets); + + it("adds a 'listeners' array to events with the longnames of the listeners", function() { + expect(Array.isArray(ev.listeners)).toBe(true); + expect(Array.isArray(ev2.listeners)).toBe(true); + + expect(ev.listeners.length).toBe(2); + expect(ev.listeners).toContain('module:myModule~MyHandler'); + expect(ev.listeners).toContain('module:myModule~AnotherHandler'); + + expect(ev2.listeners.length).toBe(1); + expect(ev2.listeners).toContain('module:myModule~MyHandler'); + }); + + it("does not add listeners for events with no listeners", function() { + expect(ev3.listeners).not.toBeDefined(); + }); + + it("does not make spurious doclets if something @listens to a non-existent symbol", function() { + expect(helper.find(doclets, {longname: 'event:fakeEvent'}).length).toBe(0); + }); + }); + + describe("prune", function() { + var priv = !!env.opts.private; + + afterEach(function() { + env.opts.private = priv; + }); + + var array = [ + // keep + {undocumented: false}, + // keep + {ignore: false}, + // keep + {memberof: 'SomeClass'}, + // prune + {undocumented: true}, + // prune + {ignore: true}, + // prune + {memberof: ''} + ]; + var arrayPrivate = [ + // prune (unless env.opts.private is truthy) + {access: 'private'} + ]; + var keep = array.slice(0, 3); + + it('should prune the correct members', function() { + var pruned = helper.prune( taffy(array) )().get(); + compareObjectArrays(keep, pruned); + }); + + it('should prune private members if env.opts.private is falsy', function() { + var pruned; + + env.opts.private = false; + pruned = helper.prune( taffy(arrayPrivate) )().get(); + compareObjectArrays([], pruned); + }); + + it('should not prune private members if env.opts.private is truthy', function() { + var pruned; + + env.opts.private = true; + pruned = helper.prune( taffy(arrayPrivate) )().get(); + compareObjectArrays(arrayPrivate, pruned); + }); + }); + + describe("registerLink", function() { + it("adds an entry to exports.longnameToUrl", function() { + helper.longnameToUrl.MySymbol = 'asdf.html'; + + expect(helper.longnameToUrl.MySymbol).toBeDefined(); + expect(helper.longnameToUrl.MySymbol).toBe('asdf.html'); + + delete helper.longnameToUrl.MySymbol; + }); + + it("allows linkto to work", function() { + helper.registerLink('MySymbol', 'asdf.html'); + + expect(helper.linkto('MySymbol')).toBe('MySymbol'); + + delete helper.longnameToUrl.MySymbol; + }); + }); + + describe("tutorialToUrl", function() { + function missingTutorial() { + var url = helper.tutorialToUrl("be-a-perfect-person-in-just-three-days"); + } + + beforeEach(function() { + spyOn(logger, 'error'); + helper.setTutorials(resolver.root); + }); + + afterEach(function() { + helper.setTutorials(null); + }); + + it('logs an error if the tutorial is missing', function() { + helper.tutorialToUrl('be-a-perfect-person-in-just-three-days'); + + expect(logger.error).toHaveBeenCalled(); + }); + + it("logs an error if the tutorial's name is a reserved JS keyword and it doesn't exist", function() { + helper.tutorialToUrl('prototype'); + + expect(logger.error).toHaveBeenCalled(); + }); + + it("creates links to tutorials if they exist", function() { + // load the tutorials we already have for the tutorials tests + resolver.load(env.dirname + "/test/tutorials/tutorials"); + resolver.resolve(); + + var url = helper.tutorialToUrl('test'); + expect(typeof url).toBe('string'); + expect(url).toBe('tutorial-test.html'); + }); + + it("creates links for tutorials where the name is a reserved JS keyword", function() { + var url = helper.tutorialToUrl('constructor'); + expect(typeof url).toBe('string'); + expect(url).toBe('tutorial-constructor.html'); + }); + + it("returns the same link if called multiple times on the same tutorial", function() { + expect(helper.tutorialToUrl('test2')).toBe(helper.tutorialToUrl('test2')); + }); + }); + + describe("toTutorial", function() { + beforeEach(function () { + spyOn(logger, 'error'); + helper.setTutorials(resolver.root); + }); + + afterEach(function() { + helper.setTutorials(null); + }); + + it('logs an error if the first param is missing', function() { + helper.toTutorial(); + + expect(logger.error).toHaveBeenCalled(); + }); + + // missing tutorials + it("returns the tutorial name if it's missing and no missingOpts is provided", function() { + helper.setTutorials(resolver.root); + var link = helper.toTutorial('qwerty'); + expect(link).toBe('qwerty'); + }); + + it("returns the tutorial name wrapped in missingOpts.tag if provided and the tutorial is missing", function() { + var link = helper.toTutorial('qwerty', 'lkjklqwerty', {tag: 'span'}); + expect(link).toBe('qwerty'); + }); + + it("returns the tutorial name wrapped in missingOpts.tag with class missingOpts.classname if provided and the tutorial is missing", function() { + var link = helper.toTutorial('qwerty', 'lkjklqwerty', {classname: 'missing'}); + expect(link).toBe('qwerty'); + + link = helper.toTutorial('qwerty', 'lkjklqwerty', {tag: 'span', classname: 'missing'}); + expect(link).toBe('qwerty'); + }); + + it("prefixes the tutorial name with missingOpts.prefix if provided and the tutorial is missing", function() { + var link = helper.toTutorial('qwerty', 'lkjklqwerty', {tag: 'span', classname: 'missing', prefix: 'TODO-'}); + expect(link).toBe('TODO-qwerty'); + + link = helper.toTutorial('qwerty', 'lkjklqwerty', {prefix: 'TODO-'}); + expect(link).toBe('TODO-qwerty'); + + link = helper.toTutorial('qwerty', 'lkjklqwerty', {prefix: 'TODO-', classname: 'missing'}); + expect(link).toBe('TODO-qwerty'); + }); + + // now we do non-missing tutorials. + it("returns a link to the tutorial if not missing", function() { + // load the tutorials we already have for the tutorials tests + resolver.load(env.dirname + "/test/tutorials/tutorials"); + resolver.resolve(); + + var link = helper.toTutorial('constructor', 'The Constructor tutorial'); + expect(link).toBe('The Constructor tutorial'); + }); + + it("uses the tutorial's title for the link text if no content parameter is provided", function() { + var link = helper.toTutorial('test'); + expect(link).toBe('Test tutorial'); + }); + + it("does not apply any of missingOpts if the tutorial was found", function() { + var link = helper.toTutorial('test', '', {tag: 'span', classname: 'missing', prefix: 'TODO-'}); + expect(link).toBe('Test tutorial'); + }); + }); + + // couple of convenience functions letting me set conf variables and restore + // them back to the originals later. + function setConfTemplatesVariables(hash) { + var keys = Object.keys(hash); + var storage = {}; + for (var i = 0; i < keys.length; ++i) { + storage[keys[i]] = env.conf.templates[keys[i]]; + // works because hash[key] is a scalar not an array/object + env.conf.templates[keys[i]] = hash[keys[i]]; + } + return storage; + } + + function restoreConfTemplates(storage) { + var keys = Object.keys(storage); + for (var i = 0; i < keys.length; ++i) { + env.conf.templates[keys[i]] = storage[keys[i]]; + } + } + + describe("resolveLinks", function() { + it('should translate {@link test} into a HTML link.', function() { + var input = 'This is a {@link test}.', + output = helper.resolveLinks(input); + + expect(output).toBe('This is a test.'); + }); + + it('should translate {@link unknown} into a simple text.', function() { + var input = 'This is a {@link unknown}.', + output = helper.resolveLinks(input); + + expect(output).toBe('This is a unknown.'); + }); + + it('should translate {@link test} into a HTML links multiple times.', function() { + var input = 'This is a {@link test} and {@link test}.', + output = helper.resolveLinks(input); + + expect(output).toBe('This is a test and test.'); + }); + + it('should translate [hello there]{@link test} into a HTML link with the custom content.', function() { + var input = 'This is a [hello there]{@link test}.', + output = helper.resolveLinks(input); + + expect(output).toBe('This is a hello there.'); + }); + + it('should translate [dummy text] and [hello there]{@link test} into an HTML link with the custom content.', function() { + var input = 'This is [dummy text] and [hello there]{@link test}.', + output = helper.resolveLinks(input); + + expect(output).toBe('This is [dummy text] and hello there.'); + }); + + it('should translate [dummy text] and [more] and [hello there]{@link test} into an HTML link with the custom content.', function() { + var input = 'This is [dummy text] and [more] and [hello there]{@link test}.', + output = helper.resolveLinks(input); + + expect(output).toBe('This is [dummy text] and [more] and hello there.'); + }); + + it('should ignore [hello there].', function() { + var input = 'This is a [hello there].', + output = helper.resolveLinks(input); + + expect(output).toBe(input); + }); + + it('should translate http links in the tag', function() { + var input = 'Link to {@link http://github.com}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to http://github.com'); + }); + + it('should translate ftp links in the tag', function() { + var input = 'Link to {@link ftp://foo.bar}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to ftp://foo.bar'); + }); + + it('should allow pipe to be used as delimiter between href and text (external link)', function() { + var input = 'Link to {@link http://github.com|Github}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to Github'); + }); + + it('should allow pipe to be used as delimiter between href and text (symbol link)', function() { + var input = 'Link to {@link test|Test}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to Test'); + }); + + it('should allow first space to be used as delimiter between href and text (external link)', function() { + var input = 'Link to {@link http://github.com Github}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to Github'); + }); + + it('should allow first space to be used as delimiter between href and text (symbol link)', function() { + var input = 'Link to {@link test My Caption}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to My Caption'); + }); + + it('if pipe and space are present in link tag, use pipe as the delimiter', function() { + var input = 'Link to {@link test|My Caption}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to My Caption'); + }); + + it('Test of {@linkcode } which should be in monospace', function() { + var input = 'Link to {@linkcode test}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to test'); + }); + + it('Test of {@linkplain } which should be in normal font', function() { + var input = 'Link to {@linkplain test}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to test'); + }); + + it('should be careful with linking to links whose names are reserved JS keywords', function() { + var input = 'Link to {@link constructor}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to constructor'); + }); + + it('should allow linebreaks between link tag and content', function() { + var input = 'This is a {@link\ntest}.', + output = helper.resolveLinks(input); + + expect(output).toBe('This is a test.'); + }); + + it('should allow linebreaks to separate url from link text', function() { + var input = 'This is a {@link\ntest\ntest}.', + output = helper.resolveLinks(input); + + expect(output).toBe('This is a test.'); + }); + + it('should normalize additional newlines to spaces', function() { + var input = 'This is a {@link\ntest\ntest\n\ntest}.', + output = helper.resolveLinks(input); + + expect(output).toBe('This is a test test.'); + }); + + it('should allow tabs between link tag and content', function() { + var input = 'This is a {@link\ttest}.', + output = helper.resolveLinks(input); + + expect(output).toBe('This is a test.'); + }); + + // conf.monospaceLinks. check that + // a) it works + it('if conf.monospaceLinks is true, all {@link} should be monospace', function () { + var storage = setConfTemplatesVariables({monospaceLinks: true}); + var input = 'Link to {@link test}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to test'); + restoreConfTemplates(storage); + }); + + // b) linkcode and linkplain are still respected + it('if conf.monospaceLinks is true, all {@linkcode} should still be monospace', function () { + var storage = setConfTemplatesVariables({monospaceLinks: true}); + var input = 'Link to {@linkcode test}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to test'); + restoreConfTemplates(storage); + }); + + it('if conf.monospaceLinks is true, all {@linkplain} should still be plain', function () { + var storage = setConfTemplatesVariables({monospaceLinks: true}); + var input = 'Link to {@linkplain test}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to test'); + restoreConfTemplates(storage); + }); + + // conf.cleverLinks. check that + // a) it works + it('if conf.cleverLinks is true, {@link symbol} should be in monospace', function () { + var storage = setConfTemplatesVariables({cleverLinks: true}); + var input = 'Link to {@link test}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to test'); + restoreConfTemplates(storage); + }); + + it('if conf.cleverLinks is true, {@link URL} should be in plain text', function () { + var storage = setConfTemplatesVariables({cleverLinks: true}); + var input = 'Link to {@link http://github.com}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to http://github.com'); + restoreConfTemplates(storage); + }); + + // b) linkcode and linkplain are still respected + it('if conf.cleverLinks is true, all {@linkcode} should still be clever', function () { + var storage = setConfTemplatesVariables({cleverLinks: true}); + var input = 'Link to {@linkcode test}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to test'); + restoreConfTemplates(storage); + }); + + it('if conf.cleverLinks is true, all {@linkplain} should still be plain', function () { + var storage = setConfTemplatesVariables({cleverLinks: true}); + var input = 'Link to {@linkplain test}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to test'); + restoreConfTemplates(storage); + }); + + // c) if monospaceLinks is additionally `true` it is ignored in favour + // of cleverLinks + it('if conf.cleverLinks is true and so is conf.monospaceLinks, cleverLinks overrides', function () { + var storage = setConfTemplatesVariables({cleverLinks: true, monospaceLinks: true}); + var input = 'Link to {@link test} and {@link http://github.com}', + output = helper.resolveLinks(input); + expect(output).toBe('Link to test and http://github.com'); + restoreConfTemplates(storage); + }); + + }); + + describe("createLink", function() { + it('should create a url for a simple global.', function() { + var mockDoclet = { + kind: 'function', + longname: 'foo', + name: 'foo' + }, + url = helper.createLink(mockDoclet); + + expect(url).toBe('global.html#foo'); + }); + + it('should create a url for a namespace.', function() { + var mockDoclet = { + kind: 'namespace', + longname: 'foo', + name: 'foo' + }, + url = helper.createLink(mockDoclet); + + expect(url).toBe('foo.html'); + }); + + it('should create a url for a member of a namespace.', function() { + var mockDoclet = { + kind: 'function', + longname: 'ns.foo', + name: 'foo', + memberof: 'ns' + }, + url = helper.createLink(mockDoclet); + + expect(url).toBe('ns.html#foo'); + }); + + var nestedNamespaceDoclet = { + kind: 'function', + longname: 'ns1.ns2.foo', + name: 'foo', + memberof: 'ns1.ns2' + }; + var nestedNamespaceUrl; + + it('should create a url for a member of a nested namespace.', function() { + nestedNamespaceUrl = helper.createLink(nestedNamespaceDoclet); + + expect(nestedNamespaceUrl).toBe('ns1.ns2.html#foo'); + }); + + it('should return the same value when called twice with the same doclet.', function() { + var newUrl = helper.createLink(nestedNamespaceDoclet); + expect(newUrl).toBe(nestedNamespaceUrl); + }); + + it('should create a url for a name with invalid characters.', function() { + var mockDoclet = { + kind: 'function', + longname: 'ns1."!"."*foo"', + name: '"*foo"', + memberof: 'ns1."!"' + }, + url = helper.createLink(mockDoclet); + + expect(url).toEqual('ns1._!_.html#%22*foo%22'); + }); + + it('should create a url for a function that is the only symbol exported by a module.', + function() { + var mockDoclet = { + kind: 'function', + longname: 'module:bar', + name: 'module:bar' + }; + var url = helper.createLink(mockDoclet); + + expect(url).toEqual('module-bar.html'); + }); + + it('should create a url for a doclet with the wrong kind (caused by incorrect JSDoc tags', function() { + var moduleDoclet = { + kind: 'module', + longname: 'module:baz', + name: 'module:baz' + }; + var badDoclet = { + kind: 'member', + longname: 'module:baz', + name: 'module:baz' + }; + + var moduleDocletUrl = helper.createLink(moduleDoclet); + var badDocletUrl = helper.createLink(badDoclet); + + expect(moduleDocletUrl).toBe('module-baz.html'); + expect(badDocletUrl).toBe('module-baz.html'); + }); + + it('should create a url for a function that is a member of a doclet with the wrong kind', function() { + var badModuleDoclet = { + kind: 'member', + longname: 'module:qux', + name: 'module:qux' + }; + var memberDoclet = { + kind: 'function', + name: 'frozzle', + memberof: 'module:qux', + scope: 'instance', + longname: 'module:qux#frozzle' + }; + + var badModuleDocletUrl = helper.createLink(badModuleDoclet); + var memberDocletUrl = helper.createLink(memberDoclet); + + expect(badModuleDocletUrl).toBe('module-qux.html'); + expect(memberDocletUrl).toBe('module-qux.html#frozzle'); + }); + + it('should create a url for an empty package definition', function() { + var packageDoclet = { + kind: 'package', + name: undefined, + longname: 'package:undefined' + }; + + var packageDocletUrl = helper.createLink(packageDoclet); + + expect(packageDocletUrl).toBe('global.html'); + }); + }); + + describe("resolveAuthorLinks", function() { + // convert Jane Doe to a mailto link. + it('should convert email addresses in angle brackets *after* a name to mailto links', function() { + var str = ' John Doe ', + out = helper.resolveAuthorLinks(str); + expect(out).toBe('John Doe'); + }); + + it('should HTML-safe author names', function() { + var str = ' John ', + out = helper.resolveAuthorLinks(str); + expect(out).toBe('' + helper.htmlsafe('John'); + }); + + it('should simply return the input string, HTML-safe, if no email is detected', function() { + var str = 'John Doe ', + out = helper.resolveAuthorLinks(str); + expect(out).toBe(helper.htmlsafe(str)); + }); + }); + + xdescribe('longnamesToTree', function() { + // TODO + }); +}); diff --git a/third_party/jsdoc/test/specs/plugins/plugins.js b/third_party/jsdoc/test/specs/plugins/plugins.js new file mode 100644 index 0000000000..82eb3e37c0 --- /dev/null +++ b/third_party/jsdoc/test/specs/plugins/plugins.js @@ -0,0 +1,57 @@ +/*global afterEach: true, app: true, beforeEach: true, describe: true, env: true, expect: true, +it: true, jasmine: true */ +// TODO: consolidate with specs/jsdoc/parser and specs/jsdoc/plugins +describe("plugins", function() { + var path = require('jsdoc/path'); + + var docSet; + + var pluginPaths = [ + path.normalize(env.dirname + '/test/fixtures/testPlugin1'), + path.normalize(env.dirname + '/test/fixtures/testPlugin2') + ]; + + // TODO: decouple this from the global parser + app.jsdoc.parser = jasmine.createParser(); + + global.jsdocPluginsTest = global.jsdocPluginsTest || {}; + + require('jsdoc/plugins').installPlugins(pluginPaths, app.jsdoc.parser); + + docSet = jasmine.getDocSetFromFile('test/fixtures/plugins.js', app.jsdoc.parser, false); + + it("should fire the plugin's event handlers", function() { + expect(global.jsdocPluginsTest.plugin1.fileBegin).toBeDefined(); + expect(global.jsdocPluginsTest.plugin1.fileBegin).toEqual(true); + expect(global.jsdocPluginsTest.plugin1.beforeParse).toBeDefined(); + expect(global.jsdocPluginsTest.plugin1.beforeParse).toEqual(true); + expect(global.jsdocPluginsTest.plugin1.jsdocCommentFound).toBeDefined(); + expect(global.jsdocPluginsTest.plugin1.jsdocCommentFound).toEqual(true); + expect(global.jsdocPluginsTest.plugin1.symbolFound).toBeDefined(); + expect(global.jsdocPluginsTest.plugin1.symbolFound).toEqual(true); + expect(global.jsdocPluginsTest.plugin1.newDoclet).toBeDefined(); + expect(global.jsdocPluginsTest.plugin1.newDoclet).toEqual(true); + expect(global.jsdocPluginsTest.plugin1.fileComplete).toBeDefined(); + expect(global.jsdocPluginsTest.plugin1.fileComplete).toEqual(true); + + expect(global.jsdocPluginsTest.plugin2.fileBegin).toBeDefined(); + expect(global.jsdocPluginsTest.plugin2.fileBegin).toEqual(true); + expect(global.jsdocPluginsTest.plugin2.beforeParse).toBeDefined(); + expect(global.jsdocPluginsTest.plugin2.beforeParse).toEqual(true); + expect(global.jsdocPluginsTest.plugin2.jsdocCommentFound).toBeDefined(); + expect(global.jsdocPluginsTest.plugin2.jsdocCommentFound).toEqual(true); + expect(global.jsdocPluginsTest.plugin2.symbolFound).toBeDefined(); + expect(global.jsdocPluginsTest.plugin2.symbolFound).toEqual(true); + expect(global.jsdocPluginsTest.plugin2.newDoclet).toBeDefined(); + expect(global.jsdocPluginsTest.plugin2.newDoclet).toEqual(true); + expect(global.jsdocPluginsTest.plugin2.fileComplete).toBeDefined(); + expect(global.jsdocPluginsTest.plugin2.fileComplete).toEqual(true); + }); + + it("should add the plugin's tag definitions to the dictionary", function() { + var test = docSet.getByLongname("test"); + + expect(test[0].longname).toEqual("test"); + expect(test[0].foo).toEqual(true); + }); +}); diff --git a/third_party/jsdoc/test/specs/rhino/fs.js b/third_party/jsdoc/test/specs/rhino/fs.js new file mode 100644 index 0000000000..3e45a658ea --- /dev/null +++ b/third_party/jsdoc/test/specs/rhino/fs.js @@ -0,0 +1,4 @@ +/*global describe: true */ +describe("fs", function() { + // TODO +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/rhino/os.js b/third_party/jsdoc/test/specs/rhino/os.js new file mode 100644 index 0000000000..653e6b59c5 --- /dev/null +++ b/third_party/jsdoc/test/specs/rhino/os.js @@ -0,0 +1,4 @@ +/*global describe: true */ +describe("os", function() { + // TODO +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/rhino/path.js b/third_party/jsdoc/test/specs/rhino/path.js new file mode 100644 index 0000000000..cf21795dee --- /dev/null +++ b/third_party/jsdoc/test/specs/rhino/path.js @@ -0,0 +1,35 @@ +/*global describe: true, expect: true, it: true */ +describe("path", function() { + // TODO: more tests + var path = require('path'); + + var pathChunks = [ + "foo", + "bar", + "baz", + "qux.html" + ]; + var joinedPath = path.join.apply(this, pathChunks); + + describe("basename", function() { + it("should exist", function() { + expect(path.basename).toBeDefined(); + }); + + it("should be a function", function() { + expect(typeof path.basename).toEqual("function"); + }); + + it("should work correctly without an 'ext' parameter", function() { + expect( path.basename(joinedPath) ).toEqual( pathChunks[pathChunks.length - 1] ); + }); + + it("should work correctly with an 'ext' parameter", function() { + var fn = pathChunks[pathChunks.length - 1], + ext = Array.prototype.slice.call( fn, fn.indexOf(".") ).join(""); + bn = Array.prototype.slice.call( fn, 0, fn.indexOf(".") ).join(""); + + expect( path.basename(joinedPath, ext) ).toEqual(bn); + }); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/rhino/src/parser.js b/third_party/jsdoc/test/specs/rhino/src/parser.js new file mode 100644 index 0000000000..c47a488938 --- /dev/null +++ b/third_party/jsdoc/test/specs/rhino/src/parser.js @@ -0,0 +1,183 @@ +/*global beforeEach: true, describe: true, expect: true, it: true, jasmine: true, spyOn: true */ +describe('rhino/jsdoc/src/parser', function() { + var jsdoc = { + src: { + parser: (function() { + var runtime = require('jsdoc/util/runtime'); + return require( runtime.getModulePath('jsdoc/src/parser') ); + })() + } + }; + + // don't run this spec if we're currently testing another parser + if (jasmine.jsParser !== 'rhino') { + return; + } + + it('should exist', function() { + expect(jsdoc.src.parser).toBeDefined(); + expect(typeof jsdoc.src.parser).toBe('object'); + }); + + it('should export a "Parser" constructor', function() { + expect(jsdoc.src.parser.Parser).toBeDefined(); + expect(typeof jsdoc.src.parser.Parser).toBe('function'); + }); + + describe('Parser', function() { + var parser; + + function newParser() { + parser = jsdoc.src.parser.createParser('rhino'); + } + + newParser(); + + it('should inherit from jsdoc/src/parser', function() { + var parent = require('jsdoc/src/parser').Parser; + expect(parser instanceof parent).toBe(true); + }); + + it('should have an "addNodeVisitor" method', function() { + expect(parser.addNodeVisitor).toBeDefined(); + expect(typeof parser.addNodeVisitor).toBe('function'); + }); + + it('should have a "getNodeVisitors" method', function() { + expect(parser.getNodeVisitors).toBeDefined(); + expect(typeof parser.getNodeVisitors).toBe('function'); + }); + + describe('addNodeVisitor', function() { + function visitorA() {} + function visitorB() {} + + var visitors; + + beforeEach(newParser); + + it('should work with a single Rhino node visitor', function() { + parser.addNodeVisitor(visitorA); + + visitors = parser.getNodeVisitors(); + + expect(visitors.length).toBe(1); + expect(visitors[0]).toBe(visitorA); + }); + + it('should work with multiple Rhino node visitors', function() { + parser.addNodeVisitor(visitorA); + parser.addNodeVisitor(visitorB); + + visitors = parser.getNodeVisitors(); + + expect(visitors.length).toBe(2); + expect(visitors[0]).toBe(visitorA); + expect(visitors[1]).toBe(visitorB); + }); + }); + + describe('getNodeVisitors', function() { + beforeEach(newParser); + + it('should return an empty array by default', function() { + var visitors = parser.getNodeVisitors(); + + expect( Array.isArray(visitors) ).toBe(true); + expect(visitors.length).toBe(0); + }); + + // other functionality is covered by the addNodeVisitors tests + }); + + describe('parse', function() { + beforeEach(newParser); + + var sourceCode = ['javascript:/** foo */var foo;']; + + it('should call Rhino node visitors', function() { + var args; + + var visitor = { + visitNode: function(rhinoNode, e, parser, sourceName) { + if (e && e.code && !args) { + args = Array.prototype.slice.call(arguments); + } + } + }; + + require('jsdoc/src/handlers').attachTo(parser); + parser.addNodeVisitor(visitor); + parser.parse(sourceCode); + + expect(args).toBeDefined(); + expect( Array.isArray(args) ).toBe(true); + expect(args.length).toBe(4); + + // args[0]: Rhino node + expect(args[0].toSource).toBeDefined(); + expect( String(args[0].toSource()) ).toBe('foo'); + + // args[1]: JSDoc event + expect(typeof args[1]).toBe('object'); + expect(args[1].code).toBeDefined(); + expect(args[1].code.name).toBeDefined(); + expect( String(args[1].code.name) ).toBe('foo'); + + // args[2]: parser + expect(typeof args[2]).toBe('object'); + + // args[3]: current source name + expect( String(args[3]) ).toBe('[[string0]]'); + }); + + it('should reflect changes made by Rhino node visitors', function() { + var doclet; + + var visitor = { + visitNode: function(rhinoNode, e, parser, sourceName) { + if (e && e.code && e.code.name === 'foo') { + e.code.name = 'bar'; + } + } + }; + + require('jsdoc/src/handlers').attachTo(parser); + parser.addNodeVisitor(visitor); + parser.parse(sourceCode); + + doclet = parser.results()[0]; + + expect(doclet).toBeDefined(); + expect(typeof doclet).toBe('object'); + expect(doclet.name).toBeDefined(); + expect(doclet.name).toBe('bar'); + }); + + it('should not call a second Rhino node visitor if the first visitor stopped ' + + 'propagation', function() { + var doclet; + + var visitor1 = { + visitNode: function(rhinoNode, e, parser, sourceName) { + e.stopPropagation = true; + } + }; + var visitor2 = { + visitNode: function(rhinoNode, e, parser, sourceName) { + e.propertyThatWillNeverBeSet = ':('; + } + }; + + require('jsdoc/src/handlers').attachTo(parser); + parser.addNodeVisitor(visitor1); + parser.addNodeVisitor(visitor2); + parser.parse(sourceCode); + + doclet = parser.results()[0]; + + expect(doclet.propertyThatWillNeverBeSet).not.toBeDefined(); + }); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/rhino/src/visitor.js b/third_party/jsdoc/test/specs/rhino/src/visitor.js new file mode 100644 index 0000000000..ea71084d6f --- /dev/null +++ b/third_party/jsdoc/test/specs/rhino/src/visitor.js @@ -0,0 +1,88 @@ +/*global beforeEach: true, describe: true, expect: true, it: true */ +describe('rhino/jsdoc/src/visitor', function() { + var runtime = require('jsdoc/util/runtime'); + var jsdoc = { + src: { + visitor: require( runtime.getModulePath('jsdoc/src/visitor') ) + } + }; + + it('should exist', function() { + expect(jsdoc.src.visitor).toBeDefined(); + expect(typeof jsdoc.src.visitor).toBe('object'); + }); + + it('should export a "Visitor" constructor', function() { + expect(jsdoc.src.visitor.Visitor).toBeDefined(); + expect(typeof jsdoc.src.visitor.Visitor).toBe('function'); + }); + + describe('Visitor', function() { + var parser; + var visitor; + + function newVisitor() { + parser = require('jsdoc/src/parser').createParser('rhino'); + visitor = new jsdoc.src.visitor.Visitor(parser); + } + + newVisitor(); + + it('should inherit from jsdoc/src/visitor', function() { + var parent = require('jsdoc/src/visitor').Visitor; + expect(visitor instanceof parent).toBe(true); + }); + + it('should have an "addRhinoNodeVisitor" method', function() { + expect(visitor.addRhinoNodeVisitor).toBeDefined(); + expect(typeof visitor.addRhinoNodeVisitor).toBe('function'); + }); + + it('should have a "getRhinoNodeVisitors" method', function() { + expect(visitor.getRhinoNodeVisitors).toBeDefined(); + expect(typeof visitor.getRhinoNodeVisitors).toBe('function'); + }); + + describe('addRhinoNodeVisitor', function() { + function visitorA() {} + function visitorB() {} + + var visitors; + + beforeEach(newVisitor); + + it('should work with a single Rhino node visitor', function() { + visitor.addRhinoNodeVisitor(visitorA); + + visitors = visitor.getRhinoNodeVisitors(); + + expect(visitors.length).toBe(1); + expect(visitors[0]).toBe(visitorA); + }); + + it('should work with multiple Rhino node visitors', function() { + visitor.addRhinoNodeVisitor(visitorA); + visitor.addRhinoNodeVisitor(visitorB); + + visitors = visitor.getRhinoNodeVisitors(); + + expect(visitors.length).toBe(2); + expect(visitors[0]).toBe(visitorA); + expect(visitors[1]).toBe(visitorB); + }); + }); + + describe('getRhinoNodeVisitors', function() { + beforeEach(newVisitor); + + it('should return an empty array by default', function() { + var visitors = visitor.getRhinoNodeVisitors(); + + expect( Array.isArray(visitors) ).toBe(true); + expect(visitors.length).toBe(0); + }); + + // other functionality is covered by the addNodeVisitors tests + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/abstracttag.js b/third_party/jsdoc/test/specs/tags/abstracttag.js new file mode 100644 index 0000000000..be2b786205 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/abstracttag.js @@ -0,0 +1,20 @@ +describe("@abstract tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/abstracttag.js'), + type = docSet.getByLongname('Thingy')[0], + pez = docSet.getByLongname('Thingy#pez')[0]; + + it("should have an undefined 'virtual' property with no '@abstract' tag", function() { + expect(type.virtual).toBeUndefined(); + }); + + it("should set the doclet's 'virtual' property to true when ' @abstract tag is present", function() { + expect(pez.virtual).toBe(true); + }); + + // same as... + + it("should set the doclet's 'virtual' property to true when ' @abstract tag is present", function() { + pez = docSet.getByLongname('OtherThingy#pez')[0]; + expect(pez.virtual).toBe(true); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/accesstag.js b/third_party/jsdoc/test/specs/tags/accesstag.js new file mode 100644 index 0000000000..3bfa397305 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/accesstag.js @@ -0,0 +1,24 @@ +describe("@access tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/accesstag.js'), + foo = docSet.getByLongname('Thingy~foo')[0], + _bar = docSet.getByLongname('Thingy#_bar')[0], + pez = docSet.getByLongname('Thingy#pez')[0], + foo2 = docSet.getByLongname('OtherThingy~foo')[0], + _bar2 = docSet.getByLongname('OtherThingy#_bar')[0], + pez2 = docSet.getByLongname('OtherThingy#pez')[0]; + + it("should set the doclet's 'access' property to 'private' when there is an @access private tag", function() { + expect(foo.access).toBe('private'); + expect(foo2.access).toBe('private'); + }); + + it("should set the doclet's 'access' property to 'protected' when there is an @access protected tag", function() { + expect(_bar.access).toBe('protected'); + expect(_bar2.access).toBe('protected'); + }); + + it("should set no 'access' property on the doclet when there is an @access public tag", function() { + expect(pez.access).toBeUndefined(); + expect(pez2.access).toBeUndefined(); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/aliastag.js b/third_party/jsdoc/test/specs/tags/aliastag.js new file mode 100644 index 0000000000..4b2396ef26 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/aliastag.js @@ -0,0 +1,11 @@ +describe("@alias tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/alias.js'), + // there are two doclets with longname myObject, we want the second one + myObject = docSet.getByLongname('myObject')[1]; + + it("adds an 'alias' property to the doclet with the tag's value", function() { + expect(myObject.alias).toBeDefined(); + expect(myObject.alias).toBe('myObject'); + }); + // further tests (ensuring alias has the proper effect): documentation/alias.js +}); diff --git a/third_party/jsdoc/test/specs/tags/augmentstag.js b/third_party/jsdoc/test/specs/tags/augmentstag.js new file mode 100644 index 0000000000..819212165e --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/augmentstag.js @@ -0,0 +1,152 @@ +'use strict'; + + describe('@augments tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/augmentstag.js'); + var docSet2 = jasmine.getDocSetFromFile('test/fixtures/augmentstag2.js'); + var docSet3 = jasmine.getDocSetFromFile('test/fixtures/augmentstag3.js'); + var docSet4 = jasmine.getDocSetFromFile('test/fixtures/augmentstag4.js'); + var docSet5 = jasmine.getDocSetFromFile('test/fixtures/augmentstag5.js'); + + it('When a symbol has an @augments tag, the doclet has a augments property that includes that value.', function() { + var bar = docSet.getByLongname('Bar')[0]; + + expect(typeof bar.augments).toBe('object'); + expect(bar.augments[0]).toBe('Foo'); + }); + + it('When an object is extended, the original is not modified', function() { + var fooProp3 = docSet.getByLongname('Foo#prop3')[0]; + + expect(fooProp3).toBeUndefined(); + }); + + it('When an object is extended, it inherits properties set in parent constructor', function() { + var fooProp1 = docSet.getByLongname('Foo#prop1')[0]; + var barProp1 = docSet.getByLongname('Bar#prop1')[0]; + + expect(fooProp1.memberof).toBe('Foo'); + expect(barProp1.memberof).toBe('Bar'); + expect(barProp1.description).toBe(fooProp1.description); + }); + + it('When an object is extended, it inherits properties set on parent prototype', function() { + var fooProp2 = docSet.getByLongname('Foo#prop2')[0]; + var barProp2 = docSet.getByLongname('Bar#prop2')[0]; + + expect(fooProp2.memberof).toBe('Foo'); + expect(barProp2.memberof).toBe('Bar'); + expect(barProp2.description).toBe(fooProp2.description); + }); + + it('When an object is extended, it inherits methods set on parent prototype', function() { + var fooMethod1 = docSet.getByLongname('Foo#method1')[0]; + var barMethod1 = docSet.getByLongname('Bar#method1')[0]; + + expect(fooMethod1.memberof).toBe('Foo'); + expect(barMethod1.memberof).toBe('Bar'); + expect(barMethod1.description).toBe(fooMethod1.description); + }); + + it('When an object is extended, it may override methods set on parent prototype', function() { + var fooMethod2 = docSet.getByLongname('Foo#method2')[0]; + var barMethod2 = docSet.getByLongname('Bar#method2')[0]; + + expect(fooMethod2.memberof).toBe('Foo'); + expect(fooMethod2.description).toBe('Second parent method.'); + expect(barMethod2.memberof).toBe('Bar'); + expect(barMethod2.description).toBe('Second child method.'); + }); + + it('When an object is extended, and it overrides an ancestor method, the child does not include docs for the ancestor method.', function() { + var barMethod2All = docSet.getByLongname('Bar#method2'); + + expect(barMethod2All.length).toBe(1); + }); + + it('When an object is extended, and it overrides an ancestor, the child has an "overrides" property', function() { + var barMethod2 = docSet.getByLongname('Bar#method2')[0]; + + expect(barMethod2.overrides).toBeDefined(); + expect(barMethod2.overrides).toBe('Foo#method2'); + }); + + it('When an object is extended, it inherits properties set on grandparent prototype', function() { + var fooProp1 = docSet.getByLongname('Foo#prop1')[0]; + var barProp1 = docSet.getByLongname('Bar#prop1')[0]; + var bazProp1 = docSet.getByLongname('Baz#prop1')[0]; + var bazMethod1 = docSet.getByLongname('Baz#method1')[0]; + + expect(fooProp1.memberof).toBe('Foo'); + expect(barProp1.memberof).toBe('Bar'); + expect(bazProp1.memberof).toBe('Baz'); + expect(bazProp1.description).toBe('Override prop1'); + expect(bazMethod1.memberof).toBe('Baz'); + }); + + it('(Grand)children correctly identify the original source of inherited members', function() { + var fooProp1 = docSet.getByLongname('Foo#prop1')[0]; + var barProp1 = docSet.getByLongname('Bar#prop1')[0]; + var barProp3 = docSet.getByLongname('Bar#prop3')[0]; + var bazProp2 = docSet.getByLongname('Baz#prop2')[0]; + var bazProp3 = docSet.getByLongname('Baz#prop3')[0]; + var bazMethod1 = docSet.getByLongname('Baz#method1')[0]; + var bazMethod2 = docSet.getByLongname('Baz#method2')[0]; + + expect(fooProp1.inherits).not.toBeDefined(); + expect(barProp3.inherits).not.toBeDefined(); + expect(barProp1.inherits).toBe('Foo#prop1'); + expect(bazProp2.inherits).toBe('Foo#prop2'); + expect(bazProp3.inherits).toBe('Bar#prop3'); + expect(bazMethod1.inherits).toBe('Foo#method1'); + expect(bazMethod2.inherits).toBe('Bar#method2'); + }); + + it('When the grandparent has a method, and the parent overrides it, the child should not say it overrides the grandparent', function() { + var bazMethod2 = docSet.getByLongname('Baz#method2')[0]; + + expect(bazMethod2.overrides).not.toBeDefined(); + }); + + it('When an object is extended, and it overrides an ancestor property, the child does not include docs for the ancestor property.', function() { + var bazProp1All = docSet.getByLongname('Baz#prop1'); + + expect(bazProp1All.length).toBe(1); + }); + + it('When a symbol has an @augments tag, and the parent is not documented, the doclet still has an augments property', function() { + var qux = docSet2.getByLongname('Qux')[0]; + + expect(typeof qux.augments).toBe('object'); + expect(qux.augments[0]).toBe('UndocumentedThing'); + }); + + it('When a symbol @augments multiple parents, it inherits methods from all parents', function() { + var fooMethod1 = docSet3.getByLongname('Foo#method1')[0]; + var barMethod2 = docSet3.getByLongname('Bar#method2')[0]; + var fooBarMethod1 = docSet3.getByLongname('FooBar#method1')[0]; + var fooBarMethod2 = docSet3.getByLongname('FooBar#method2')[0]; + + expect(fooBarMethod1).toBeDefined(); + expect(fooBarMethod2).toBeDefined(); + expect(fooBarMethod1.description).toBe(fooMethod1.description); + expect(fooBarMethod2.description).toBe(barMethod2.description); + }); + + it('When a symbol overrides an inherited method without documenting the method, it uses the parent\'s docs', function() { + var baseMethod1 = docSet4.getByLongname('Base#test1')[0]; + var derivedMethod1All = docSet4.getByLongname('Derived#test1'); + var derivedMethod1 = derivedMethod1All[1]; + + expect(derivedMethod1All.length).toBe(2); + expect(derivedMethod1.undocumented).not.toBe(true); + expect(derivedMethod1.description).toBe(baseMethod1.description); + }); + + it('When a symbol inherits two methods that would both have the same longname, the last one wins', function() { + var base1CommonMethod = docSet5.getByLongname('Base1#methodOfBaseCommon')[0]; + var classCommonMethod = docSet5.getByLongname('Class#methodOfBaseCommon'); + + expect(classCommonMethod.length).toBe(1); + expect(classCommonMethod[0].description).toBe(base1CommonMethod.description); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/authortag.js b/third_party/jsdoc/test/specs/tags/authortag.js new file mode 100644 index 0000000000..1a84b336e3 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/authortag.js @@ -0,0 +1,18 @@ +describe("@author tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/authortag.js'), + Thingy = docSet.getByLongname('Thingy')[0], + Thingy2 = docSet.getByLongname('Thingy2')[0]; + + it('When a symbol has a @author tag, the doclet has a author property with that value.', function() { + expect(Thingy.author).toBeDefined(); + expect(Array.isArray(Thingy.author)).toBe(true); + expect(Thingy.author[0]).toBe('Michael Mathews '); + }); + + it('When a symbol has multiple @author tags, the doclet has a author property, an array with those values.', function() { + expect(Thingy2.author).toBeDefined(); + expect(Array.isArray(Thingy2.author)).toBe(true); + expect(Thingy2.author).toContain('Jane Doe '); + expect(Thingy2.author).toContain('John Doe '); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/borrowstag.js b/third_party/jsdoc/test/specs/tags/borrowstag.js new file mode 100644 index 0000000000..4a6fdf570a --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/borrowstag.js @@ -0,0 +1,24 @@ +describe("@borrows tag", function() { + it('When a symbol has a @borrows-as tag, that is added to the symbol\'s "borrowed" property.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/borrowstag.js'), + util = docSet.getByLongname('util').filter(function($) { + return ! $.undocumented; + })[0]; + expect(util.borrowed.length).toBe(1); + expect(util.borrowed[0].from).toBe('trstr'); + expect(util.borrowed[0].as).toBe('trim'); + }); + + it('When a symbol has a @borrows tag, the borrowed symbol is added to the symbol.', function() { + var borrow = require('jsdoc/borrow'), + docSet = jasmine.getDocSetFromFile('test/fixtures/borrowstag2.js'); + + borrow.resolveBorrows(docSet.doclets); + + var str_rtrim = docSet.getByLongname('str.rtrim').filter(function($) { + return ! $.undocumented; + })[0]; + + expect(typeof str_rtrim).toBe('object'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/classdesctag.js b/third_party/jsdoc/test/specs/tags/classdesctag.js new file mode 100644 index 0000000000..2a369cf1c9 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/classdesctag.js @@ -0,0 +1,8 @@ +describe("@classdesc tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/classdesctag.js'), + doc = docSet.getByLongname('Foo')[0]; + + it('adds a classdesc property to the doclet with the description', function() { + expect(doc.classdesc).toBe('A description of the class.'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/classtag.js b/third_party/jsdoc/test/specs/tags/classtag.js new file mode 100644 index 0000000000..0a28a54d65 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/classtag.js @@ -0,0 +1,14 @@ +describe("@class tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/classtag.js'), + ticker = docSet.getByLongname('Ticker')[0], + news = docSet.getByLongname('NewsSource')[0]; + + it('When a symbol has a @class tag, the doclet has a kind property set to "class".', function() { + expect(ticker.kind).toBe('class'); + }); + + it('When a symbol has a @class tag with a value, the doclet has a name property set to that value.', function() { + expect(news.kind).toBe('class'); + expect(news.longname).toBe('NewsSource'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/constanttag.js b/third_party/jsdoc/test/specs/tags/constanttag.js new file mode 100644 index 0000000000..7c33c0b576 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/constanttag.js @@ -0,0 +1,66 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ +describe("@constant tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/constanttag.js'); + var FOO = docSet.getByLongname('FOO')[0]; + var BAR = docSet.getByLongname('BAR')[0]; + var BAZ = docSet.getByLongname('BAZ')[0]; + var QUX = docSet.getByLongname('QUX')[0]; + var SOCKET = docSet.getByLongname('SOCKET')[0]; + var ROCKET = docSet.getByLongname('ROCKET')[0]; + + it("sets the doclet's 'kind' property to 'constant'", function() { + expect(FOO).toBeDefined(); + expect(FOO.kind).toBe('constant'); + + expect(BAR).toBeDefined(); + expect(BAR.kind).toBe('constant'); + + expect(BAZ).toBeDefined(); + expect(BAZ.kind).toBe('constant'); + + expect(QUX).toBeDefined(); + expect(QUX.kind).toBe('constant'); + + expect(SOCKET).toBeDefined(); + expect(SOCKET.kind).toBe('constant'); + + expect(ROCKET).toBeDefined(); + expect(ROCKET.kind).toBe('constant'); + }); + + it("If used as a standalone, takes the name from the code", function() { + expect(FOO.name).toBe('FOO'); + }); + + it("If used with just a name, sets the doclet's name to that", function() { + expect(BAR.name).toBe('BAR'); + }); + + it("If used with a name and a type, sets the doclet's name and type appropriately", function() { + expect(BAZ.name).toBe('BAZ'); + expect(typeof BAZ.type).toBe('object'); + expect(BAZ.type.names).toBeDefined(); + expect(BAZ.type.names.length).toBe(1); + expect(BAZ.type.names[0]).toBe('string'); + }); + + it("If used with just a type, adds the type and takes the name from the code", function() { + expect(QUX.name).toBe('QUX'); + expect(typeof QUX.type).toBe('object'); + expect(QUX.type.names).toBeDefined(); + expect(QUX.type.names.length).toBe(1); + expect(QUX.type.names[0]).toBe('number'); + }); + + it("If used with a name and type, ignores the name in the code", function() { + expect(SOCKET.name).toBe('SOCKET'); + expect(typeof SOCKET.type).toBe('object'); + expect(SOCKET.type.names).toBeDefined(); + expect(SOCKET.type.names.length).toBe(1); + expect(SOCKET.type.names[0]).toBe('Object'); + }); + + it("If used with just a name, ignores the name in the code", function() { + expect(ROCKET.name).toBe('ROCKET'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/constructortag.js b/third_party/jsdoc/test/specs/tags/constructortag.js new file mode 100644 index 0000000000..dd2bedadaa --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/constructortag.js @@ -0,0 +1,13 @@ +describe("@constructor tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/constructortag.js'), + feed = docSet.getByLongname('Feed')[0]; + + it('When a symbol has an @constructor tag, it is documented as a class.', function() { + expect(feed.kind).toBe('class'); + }); + + it('When a symbol has an @constructor tag and a @class tag, the value of the @class tag becomes the classdesc property.', function() { + expect(feed.classdesc).toBe('Describe your class here.'); + expect(feed.description).toBe('Describe your constructor function here.'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/constructstag.js b/third_party/jsdoc/test/specs/tags/constructstag.js new file mode 100644 index 0000000000..a7198a4c3b --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/constructstag.js @@ -0,0 +1,54 @@ +describe("@constructs tag", function() { + + it('When a symbol has an @constructs tag, it is documented as a class with that name.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/constructstag.js'), + textblock = docSet.getByLongname('TextBlock')[0]; + + expect(textblock.kind).toBe('class'); + expect(textblock.longname).toBe('TextBlock'); + }); + + it('When a symbol has an @constructs tag, it is documented as a class.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/constructstag2.js'), + menu = docSet.getByLongname('Menu')[0]; + + expect(menu.name).toBe('Menu'); + expect(menu.kind).toBe('class'); + }); + + it('When a function symbol has an @constructs tag, any this-variables are ducumented as instance members of the class.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/constructstag3.js'), + personName = docSet.getByLongname('Person#name')[0]; + + expect(personName.memberof).toBe('Person'); + expect(personName.scope).toBe('instance'); + }); + + it('When a function symbol has an @constructs tag with no value, in a @lends block with a "Name#" value, the function is documented as a constructor of "Name".', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/constructstag4.js'), + person = docSet.getByLongname('Person').filter(function($) { + return ! $.undocumented; + })[0]; + + expect(person.kind).toBe('class'); + }); + + it('When a function symbol has an @constructs tag with no value, any this-variables are documented as instance members of the class.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/constructstag4.js'), + personName = docSet.getByLongname('Person#name')[0]; + + expect(personName.memberof).toBe('Person'); + expect(personName.scope).toBe('instance'); + }); + + it('When a object literal property has an @constructs tag with no value, and the object has a @lends, the property is documented as the lent class.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/constructstag5.js'), + duck = docSet.getByLongname('Duck').filter(function($) { + return ! $.undocumented; + })[0]; + + expect(duck.longname).toBe('Duck'); + expect(duck.kind).toBe('class'); + expect(duck.description).toBe('Constructs a duck.'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/copyrighttag.js b/third_party/jsdoc/test/specs/tags/copyrighttag.js new file mode 100644 index 0000000000..c9c7a5f125 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/copyrighttag.js @@ -0,0 +1,8 @@ +describe("@copyright tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/copyrighttag.js'), + Thingy = docSet.getByLongname('Thingy')[0]; + + it('When a symbol has a @copyright tag, the doclet has a copyright property with that value.', function() { + expect(Thingy.copyright).toBe('(c) 2011 Michael Mathews'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/defaulttag.js b/third_party/jsdoc/test/specs/tags/defaulttag.js new file mode 100644 index 0000000000..50f529ae56 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/defaulttag.js @@ -0,0 +1,75 @@ +/*global describe, expect, it, jasmine */ +'use strict'; + +describe('@default tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/defaulttag.js'); + + it('When symbol set to null has a @default tag with no text, the doclet\'s defaultValue property should be: null', function() { + var request = docSet.getByLongname('request')[0]; + expect(request.defaultvalue).toBe('null'); + }); + + it('When symbol set to a string has a @default tag with no text, the doclet\'s defaultValue property should be that string', function() { + var response = docSet.getByLongname('response')[0]; + expect(response.defaultvalue).toBe('ok'); + }); + + it('When symbol set to a number has a @default tag with no text, the doclet\'s defaultValue property should be that number.', function() { + var rcode = docSet.getByLongname('rcode')[0]; + expect(rcode.defaultvalue).toBe('200'); + }); + + it('When symbol has a @default tag with text, the doclet\'s defaultValue property should be that text.', function() { + var win = docSet.getByLongname('win')[0]; + expect(win.defaultvalue).toBe('the parent window'); + }); + + it('When symbol has a @default tag with true.', function() { + var rvalid = docSet.getByLongname('rvalid')[0]; + expect(rvalid.defaultvalue).toBe('true'); + }); + + it('When symbol has a @default tag with false.', function() { + var rerrored = docSet.getByLongname('rerrored')[0]; + expect(rerrored.defaultvalue, 'false'); + }); + + it('When symbol has a @default tag with a function call.', function() { + var header = docSet.getByLongname('header')[0]; + expect(header.defaultvalue).toBeUndefined(); + }); + + it('When symbol has a @default tag with an object, the doclet should contain the stringified object', function() { + var obj = docSet.getByLongname('obj')[0]; + var testObj = { valueA: 'a', valueB: false, valueC: 7}; + + expect(obj.defaultvalue).toBe( JSON.stringify(testObj) ); + expect(obj.defaultvaluetype).toBe('object'); + }); + + it('When symbol has a @default tag with a multiline object, the doclet should contain the stringified object', function() { + var multilineObject = docSet.getByLongname('multilineObject')[0]; + var testObj = { + valueA: 'a', + valueB: false, + valueC: 7 + }; + + expect(multilineObject.defaultvalue).toBe( JSON.stringify(testObj) ); + expect(multilineObject.defaultvaluetype).toBe('object'); + }); + + it('When symbol has a @default tag with an array, the doclet should contain the stringified array', function() { + var arr = docSet.getByLongname('arr')[0]; + var testArray = ['foo', true, 19]; + + expect(arr.defaultvalue).toBe( JSON.stringify(testArray) ); + expect(arr.defaultvaluetype).toBe('array'); + }); + + it('When symbol has a @default tag and a @type tag, the default value should be set correctly', function() { + var defaultWithType = docSet.getByLongname('defaultWithType')[0]; + + expect(defaultWithType.defaultvalue).toBe('a'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/deprecatedtag.js b/third_party/jsdoc/test/specs/tags/deprecatedtag.js new file mode 100644 index 0000000000..0e60ce29c6 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/deprecatedtag.js @@ -0,0 +1,14 @@ +describe("@deprecated tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/deprecatedtag.js'), + foo = docSet.getByLongname('foo')[0], + bar = docSet.getByLongname('bar')[0]; + + it('When a symbol has a @deprecated tag with no value, the doclet has a deprecated property set to true.', function() { + expect(foo.deprecated).toBe(true); + }); + + it('When a symbol has a @deprecated tag with a value, the doclet has a deprecated property set to that value.', function() { + expect(bar.deprecated).toBe('since version 2.0'); + }); + +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/descriptiontag.js b/third_party/jsdoc/test/specs/tags/descriptiontag.js new file mode 100644 index 0000000000..855280dc9e --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/descriptiontag.js @@ -0,0 +1,15 @@ +describe("@description tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/descriptiontag.js'), + doc = docSet.getByLongname('x')[0], + doc2 = docSet.getByLongname('y')[0]; + + it("sets the doclet's 'description' property to the description", function() { + expect(doc2.description).toBeDefined(); + expect(doc2.description).toBe('lkjasdf'); + }); + + it("overrides the default description", function() { + expect(doc.description).toBeDefined(); + expect(doc.description).toBe('halb halb halb'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/enumtag.js b/third_party/jsdoc/test/specs/tags/enumtag.js new file mode 100644 index 0000000000..d588e5a66d --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/enumtag.js @@ -0,0 +1,58 @@ +/*global describe, expect, it, jasmine */ +'use strict'; + +describe('@enum tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/enumtag.js'); + var tristate = docSet.getByLongname('TriState')[0]; + + it('When a symbol has an @enum tag, it has a properties array.', function() { + expect(typeof tristate.properties).toBe('object'); + }); + + it('If no @type is given for the property, it is inherited from the enum.', function() { + expect(tristate.properties[0].type.names.join(', ')).toBe('number'); + }); + + it('If no comment is given for the property, it is still included in the enum.', function() { + expect(tristate.properties[1].longname).toBe('TriState.FALSE'); + expect(tristate.properties[1].undocumented).toBeUndefined(); + }); + + it('A property of an enum gets its defaultvalue set.', function() { + expect(tristate.properties[1].defaultvalue).toBe('-1'); + }); + + it('If a @type is given for the property, it is reflected in the property value.', function() { + expect(tristate.properties[2].type.names.join(', ')).toBe('boolean'); + }); + + it('An enum does not contain any circular references.', function() { + var dump = require('jsdoc/util/dumper').dump; + + expect( dump(tristate) ).not.toMatch(''); + }); + + describe('chained assignments', function() { + var pentaState; + var PENTASTATE; + var quadState; + + docSet = jasmine.getDocSetFromFile('test/fixtures/enumtag2.js'); + pentaState = docSet.getByLongname('module:my/enums.PentaState')[0]; + PENTASTATE = docSet.getByLongname('module:my/enums.PENTASTATE')[0]; + quadState = docSet.getByLongname('module:my/enums.QuadState')[0]; + + it('When a symbol at the start of an assignment chain has an @enum tag, that symbol has a properties array.', function() { + expect( Array.isArray(quadState.properties) ).toBe(true); + expect(quadState.properties.length).toBe(4); + }); + + it('When multiple symbols in an assignment chain have @enum tags, each symbol has a properties array.', function() { + expect( Array.isArray(pentaState.properties) ).toBe(true); + expect(pentaState.properties.length).toBe(5); + + expect( Array.isArray(PENTASTATE.properties) ).toBe(true); + expect(pentaState.properties.length).toBe(5); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/eventfirestag.js b/third_party/jsdoc/test/specs/tags/eventfirestag.js new file mode 100644 index 0000000000..55d86f4af9 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/eventfirestag.js @@ -0,0 +1,30 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ +describe('@event and @fires/@emits tags', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/eventfirestag.js'), + snowballMethod = docSet.getByLongname('Hurl#snowball')[0], + snowballEvent = docSet.getByLongname('Hurl#event:snowball')[0], + footballMatchMethod = docSet.getByLongname('Hurl#footballMatch')[0]; + + // @event tag + it('When a symbol has an @event tag, the doclet is of kind "event".', function() { + expect(snowballEvent.kind).toBe('event'); + }); + + // @fires/@emits tag + it('When a symbol has a @fires tag, the doclet has an array named "fires".', function() { + expect(typeof snowballMethod.fires).toBe('object'); + }); + + it('When a symbol has an @emits tag, the doclet has an array named "fires".', function() { + expect(typeof footballMatchMethod.fires).toBe('object'); + }); + + it('When a symbol has a "fires" array, the members have the "event:" namespace.', function() { + expect(snowballMethod.fires[0]).toBe('Hurl#event:snowball'); + }); + + it('When a symbol has a "fires" array with a name that already has an "event:" namespace, ' + + 'it does not have a second namespace applied.', function() { + expect(snowballMethod.fires[1]).toBe('Hurl#event:brick'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/exampletag.js b/third_party/jsdoc/test/specs/tags/exampletag.js new file mode 100644 index 0000000000..6e817177c1 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/exampletag.js @@ -0,0 +1,22 @@ +describe("@example tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/exampletag.js'), + doc = docSet.getByLongname('x')[0], + doc2 = docSet.getByLongname('y')[0], + txtRegExp = new RegExp('console\\.log\\("foo"\\);[\\r\\n]{1,2}console\\.log\\("bar"\\);'), + txt2RegExp = new RegExp('Example 2[\\r\\n]{1,2}1 \\+ 2;'); + + it("creates an 'examples' property on the doclet with the example", function() { + expect(doc.examples).toBeDefined(); + expect(Array.isArray(doc.examples)).toBe(true); + expect(doc.examples.length).toBe(1); + expect(doc.examples).toMatch(txtRegExp); + }); + + it("can be specified multiple times on one doclet", function() { + expect(doc2.examples).toBeDefined(); + expect(Array.isArray(doc2.examples)).toBe(true); + expect(doc2.examples.length).toBe(2); + expect(doc2.examples).toMatch(txtRegExp); + expect(doc2.examples).toMatch(txt2RegExp); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/exceptiontag.js b/third_party/jsdoc/test/specs/tags/exceptiontag.js new file mode 100644 index 0000000000..e0119fc5e8 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/exceptiontag.js @@ -0,0 +1,32 @@ +/*global describe, expect, it, jasmine */ +'use strict'; + +describe('@exception tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/exceptiontag.js'); + var foo = docSet.getByLongname('foo')[0]; + var bar = docSet.getByLongname('bar')[0]; + var pez = docSet.getByLongname('pez')[0]; + var cos = docSet.getByLongname('cos')[0]; + + it('When a symbol has an @exception tag, the doclet has a exception property set to that value.', function() { + expect(typeof foo.exceptions).toBe('object'); + expect(foo.exceptions.length).toBe(1); + + expect(typeof bar.exceptions).toBe('object'); + expect(bar.exceptions.length).toBe(1); + + expect(typeof pez.exceptions).toBe('object'); + expect(pez.exceptions.length).toBe(1); + }); + + it('The description and type for the @exception tag are not added to the parent doclet.', function() { + expect(pez.description).not.toBeDefined(); + expect(pez.type).not.toBeDefined(); + }); + + it('When a symbol has a description, plus an @exception tag with a description, neither description overwrites the other.', function() { + expect(cos.description).toBe('A description of the function.'); + expect(cos.exceptions.length).toBe(1); + expect(cos.exceptions[0].description).toBe('A description of the exception.'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/exportstag.js b/third_party/jsdoc/test/specs/tags/exportstag.js new file mode 100644 index 0000000000..00feb54cfd --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/exportstag.js @@ -0,0 +1,215 @@ +/*global describe, expect, it, jasmine */ +'use strict'; + +describe('@exports tag', function() { + + describe('object literals', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/exportstag.js'); + var shirt = docSet.getByLongname('module:my/shirt')[0]; + var color = docSet.getByLongname('module:my/shirt.color')[0]; + var tneck = docSet.getByLongname('module:my/shirt.Turtleneck')[0]; + var size = docSet.getByLongname('module:my/shirt.Turtleneck#size')[0]; + + it('When an objlit symbol has an @exports tag, the doclet is aliased to "module:" + the tag value.', function() { + expect(typeof shirt).toEqual('object'); + expect(shirt.alias).toEqual('my/shirt'); + expect(shirt.undocumented).not.toBeDefined(); + }); + + it('When an objlit symbol has an @exports tag, the doclet\'s longname includes the "module:" namespace.', function() { + expect(shirt.longname).toEqual('module:my/shirt'); + }); + + it('When an objlit symbol has an @exports tag, the doclet kind is set to module.', function() { + expect(shirt.kind).toEqual('module'); + }); + + it('When an objlit symbol has an @exports tag, the module doclet does not have a scope.', function() { + expect(shirt.scope).not.toBeDefined(); + }); + + it('When an objlit symbol has an @exports tag, the objlit members are documented as members of the module.', function() { + expect(typeof color).toEqual('object'); + expect(color.memberof).toEqual('module:my/shirt'); + + expect(typeof tneck).toEqual('object'); + expect(tneck.memberof).toEqual('module:my/shirt'); + + expect(typeof size).toEqual('object'); + expect(size.memberof).toEqual('module:my/shirt.Turtleneck'); + }); + }); + + describe('functions', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/exportstag2.js'); + var coat = docSet.getByLongname('module:my/coat')[0]; + var wool = docSet.getByLongname('module:my/coat#wool')[0]; + + it('When a function symbol has an @exports tag, the doclet is aliased to "module:" + the tag value.', function() { + expect(typeof coat).toEqual('object'); + expect(coat.alias).toEqual('my/coat'); + }); + + it('When a function symbol has an @exports tag, the doclet\'s longname includes the "module:" namespace.', function() { + expect(coat.longname).toEqual('module:my/coat'); + }); + + it('When a function symbol has an @exports tag, the doclet kind is set to module.', function() { + expect(coat.kind).toEqual('module'); + }); + + it('When a function symbol has an @exports tag, the module doclet does not have a scope.', function() { + expect(coat.scope).not.toBeDefined(); + }); + + it('When a function symbol has an @exports tag, the this members are documented as instance members of the module.', function() { + expect(typeof wool).toEqual('object'); + expect(wool.memberof).toEqual('module:my/coat'); + }); + }); + + describe("functions and 'exports' object", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/exportstag3.js'); + var html = docSet.getByLongname('module:html/utils')[0]; + var getstyle = docSet.getByLongname('module:html/utils.getStyleProperty')[0]; + var inhead = docSet.getByLongname('module:html/utils.isInHead')[0]; + + it('When a function symbol has an @exports tag, the module doclet does not have a scope.', function() { + expect(html.scope).not.toBeDefined(); + }); + + it('When a function symbol has an @exports tag and there is an objlit named "exports" the members are documented as members of the module.', function() { + expect(typeof getstyle).toEqual('object'); + expect(getstyle.memberof).toEqual('module:html/utils'); + }); + + it('When a function symbol has an @exports tag and there are members assigned to an "exports" name, the members are documented as members of the module.', function() { + expect(typeof inhead).toEqual('object'); + expect(inhead.memberof).toEqual('module:html/utils'); + + }); + }); + + describe('inner classes', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/exportstag4.js'); + var module = docSet.getByLongname('module:some/module')[0]; + var innerClass = docSet.getByLongname('module:some/module~myClass')[0]; + var method = docSet.getByLongname('module:some/module~myClass#myMethod')[0]; + + it('When a function symbol has an @exports tag, the module doclet does not have a scope.', function() { + expect(module.scope).not.toBeDefined(); + }); + + it('An inner class declared as a function in a module should be documented.', function() { + expect(typeof innerClass).toEqual('object'); + }); + + it('A method of an inner class declared as a function in a module should be documented.', function() { + expect(typeof method).toEqual('object'); + }); + }); + + describe('variable shadowing', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/exportstag5.js'); + var foo = docSet.getByLongname('module:Foo')[0]; + var method = docSet.getByLongname('module:Foo#bar')[0]; + + it('When a var has an @exports tag, the module doclet does not have a scope.', function() { + expect(foo.scope).not.toBeDefined(); + }); + + it('A variable defined in an inner scope should correctly shadow a variable in an outer scope.', function() { + expect(method.description).toBe('This should be in the Foo module doc.'); + }); + }); + + describe("'exports' object as a parameter to 'define'", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/exportstag6.js'); + var shirt = docSet.getByLongname('module:my/shirt')[0]; + var color = docSet.getByLongname('module:my/shirt.color')[0]; + var tneck = docSet.getByLongname('module:my/shirt.Turtleneck')[0]; + var size = docSet.getByLongname('module:my/shirt.Turtleneck#size')[0]; + + it('When a param has an @exports tag, the doclet is aliased to "module:" + the tag value.', function() { + expect(typeof shirt).toBe('object'); + expect(shirt.alias).toBe('my/shirt'); + expect(shirt.undocumented).not.toBeDefined(); + }); + + it('When a param has an @exports tag, the doclet\'s longname includes the "module:" namespace.', function() { + expect(shirt.longname).toBe('module:my/shirt'); + }); + + it('When a param has an @exports tag, the doclet kind is set to module.', function() { + expect(shirt.kind).toEqual('module'); + }); + + it('When a param has an @exports tag, the module doclet does not have a scope.', function() { + expect(shirt.scope).not.toBeDefined(); + }); + + it('When a param has an @exports tag, the properties added to the param are documented as members of the module.', function() { + expect(typeof color).toBe('object'); + expect(color.memberof).toBe('module:my/shirt'); + + expect(typeof tneck).toBe('object'); + expect(tneck.memberof).toBe('module:my/shirt'); + + expect(typeof size).toBe('object'); + expect(size.memberof).toBe('module:my/shirt.Turtleneck'); + }); + }); + + describe("alias to the 'exports' object", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/exportstag7.js'); + var shirt = docSet.getByLongname('module:my/shirt')[0]; + var color = docSet.getByLongname('module:my/shirt.color')[0]; + var tneck = docSet.getByLongname('module:my/shirt.Turtleneck')[0]; + var size = docSet.getByLongname('module:my/shirt.Turtleneck#size')[0]; + var iron = docSet.getByLongname('module:my/shirt.Turtleneck#iron')[0]; + + it('When a symbol has an @exports tag, the doclet is aliased to "module:" + the tag value.', function() { + expect(typeof shirt).toBe('object'); + expect(shirt.alias).toBe('my/shirt'); + expect(shirt.undocumented).not.toBeDefined(); + }); + + it('When a symbol has an @exports tag, the doclet kind is set to module.', function() { + expect(shirt.kind).toEqual('module'); + }); + + it('When a symbol has an @exports tag, the module doclet does not have a scope.', function() { + expect(shirt.scope).not.toBeDefined(); + }); + + it('When a symbol tagged with @exports is an alias to "exports", the symbol properties are documented as members of the module.', function() { + expect(typeof color).toBe('object'); + expect(color.memberof).toBe('module:my/shirt'); + + expect(typeof tneck).toBe('object'); + expect(tneck.memberof).toBe('module:my/shirt'); + }); + + it('When a symbol tagged with @exports is an alias to "exports", and a symbol property contains a class, the instance members of the class are documented correctly.', function() { + expect(typeof size).toBe('object'); + expect(size.name).toBe('size'); + expect(size.memberof).toBe('module:my/shirt.Turtleneck'); + expect(size.scope).toBe('instance'); + + expect(typeof iron).toBe('object'); + expect(iron.name).toBe('iron'); + expect(iron.memberof).toBe('module:my/shirt.Turtleneck'); + expect(iron.scope).toBe('instance'); + }); + }); + + describe('"module:" namespace included in the name', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/exportstag8.js'); + var shirt = docSet.getByLongname('module:my/shirt')[0]; + + it('When the name for an @exports tag begins with the "module:" namespace, we remove the namespace', function() { + expect(typeof shirt).toBe('object'); + expect(shirt.name).toBe('my/shirt'); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/externaltag.js b/third_party/jsdoc/test/specs/tags/externaltag.js new file mode 100644 index 0000000000..8b02dd86d2 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/externaltag.js @@ -0,0 +1,37 @@ +/*global describe, expect, it, jasmine, xit */ +'use strict'; + +describe('@external tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/externaltag.js'); + var docSet2 = jasmine.getDocSetFromFile('test/fixtures/externaltag2.js'); + var docSet3 = jasmine.getDocSetFromFile('test/fixtures/externaltag3.js'); + + var fooBarBazExternal = docSet3.getByLongname('external:"foo.bar.baz"')[0]; + var jQueryExternal = docSet.getByLongname('external:"jQuery.fn"')[0]; + var stringExternal = docSet.getByLongname('external:String')[0]; + + + it('An @external should have its own doclet', function() { + expect(stringExternal).toBeDefined(); + expect(typeof stringExternal).toBe('object'); + }); + + it("An @external's name should be the same as its longname, minus 'external:'", function() { + expect(stringExternal.name).toBe('String'); + }); + + it('An @external should have its kind set to "external"', function() { + expect(stringExternal.kind).toBe('external'); + }); + + it('An @external with a quoted name should get the correct name', function() { + expect(jQueryExternal).toBeDefined(); + expect(jQueryExternal.name).toBe('"jQuery.fn"'); + }); + + // TODO: enable after jsdoc3/jsdoc#652 is fixed + xit('An @external should work correctly when the type is in curly braces', function() { + expect(fooBarBazExternal).toBeDefined(); + expect(fooBarBazExternal.name).toBe('"foo.bar.baz"'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/functiontag.js b/third_party/jsdoc/test/specs/tags/functiontag.js new file mode 100644 index 0000000000..df63f6b9e7 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/functiontag.js @@ -0,0 +1,18 @@ +describe("@function tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/functiontag.js'), + doc = docSet.getByLongname('Foo')[0], + doc2 = docSet.getByLongname('Bar')[0]; + + it("sets the doclet's kind to 'function'", function() { + expect(doc.kind).toBe('function'); + expect(doc2.kind).toBe('function'); + }); + + it("sets the doclet's name to the tag value, if provided", function() { + expect(doc.name).toBe('Foo'); + expect(doc2.name).toBe('Bar'); + }); + + // parameter etc tests take place elsewhere: on its own, all @func does is + // set doclet.kind to function and sets the doclet's name. +}); diff --git a/third_party/jsdoc/test/specs/tags/globaltag.js b/third_party/jsdoc/test/specs/tags/globaltag.js new file mode 100644 index 0000000000..3aeb34d807 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/globaltag.js @@ -0,0 +1,24 @@ +describe("@global tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/globaltag.js'); + + it('When an inner symbol has a @global tag it is documented as if it were global.', function() { + var found = docSet.getByLongname('foo').filter(function($) { + return ! $.undocumented; + }); + expect(found[0].name).toBe('foo'); + expect(found[0].longname).toBe('foo'); + expect(found[0].memberof).toBeUndefined(); + expect(found[0].scope).toBe('global'); + + }); + + it('When an nested symbol has a @global tag it is documented as if it were global.', function() { + var found = docSet.getByLongname('Bar').filter(function($) { + return ! $.undocumented; + }); + expect(found[0].name).toBe('Bar'); + expect(found[0].longname).toBe('Bar'); + expect(found[0].memberof).toBeUndefined(); + expect(found[0].scope).toBe('global'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/ignoretag.js b/third_party/jsdoc/test/specs/tags/ignoretag.js new file mode 100644 index 0000000000..8536143cdd --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/ignoretag.js @@ -0,0 +1,18 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ +describe("@ignore tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/ignoretag.js'), + foo = docSet.getByLongname('foo')[0]; + + it('When a symbol has an @ignore tag, the doclet has a ignore property set to true.', function() { + expect(foo.ignore).toBe(true); + }); + + it('When a symbol has an @ignore tag with a value an error is thrown', function() { + try { + docSet = jasmine.getDocSetFromFile('test/fixtures/ignoretag2.js'); + foo = docSet.getByLongname('foo')[0]; + } catch (e) { + expect(e instanceof Error).toBe(true); + } + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/implementstag.js b/third_party/jsdoc/test/specs/tags/implementstag.js new file mode 100644 index 0000000000..0a584aa98a --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/implementstag.js @@ -0,0 +1,31 @@ +/*global describe, expect, it, jasmine */ +'use strict'; + +describe('@implements tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/interface-implements.js'); + + var myTester = docSet.getByLongname('MyTester')[0]; + var myIncompleteWorker = docSet.getByLongname('MyWorker')[0]; + var beforeEachMethod = docSet.getByLongname('MyTester#beforeEach')[0]; + var processMethod = docSet.getByLongname('MyWorker#process')[0]; + + it('MyTester has an "implements" array', function() { + expect(Array.isArray(myTester.implements)).toBe(true); + expect(myTester.implements.length).toBe(1); + expect(myTester.implements[0]).toBe('ITester'); + }); + + it('beforeEach has an "implements" array', function() { + expect(Array.isArray(beforeEachMethod.implements)).toBe(true); + expect(beforeEachMethod.implements.length).toBe(1); + expect(beforeEachMethod.implements[0]).toBe('ITester#beforeEach'); + }); + + it('MyWorker\'s process() method does not implement an interface', function() { + expect(processMethod.implements).toBeUndefined(); + }); + + it('MyIncompleteWorker does not have any methods', function() { + expect(docSet.getByLongname('MyIncompleteWorker#work').length).toBe(0); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/interfacetag.js b/third_party/jsdoc/test/specs/tags/interfacetag.js new file mode 100644 index 0000000000..fb20b5d5bc --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/interfacetag.js @@ -0,0 +1,17 @@ +/*global describe, expect, it, jasmine */ +'use strict'; + +describe('@interface tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/interface-implements.js'); + + var testerInterface = docSet.getByLongname('ITester')[0]; + var testerImplementation = docSet.getByLongname('MyTester')[0]; + + it('ITester has its kind set to "interface"', function() { + expect(testerInterface.kind).toBe('interface'); + }); + + it('MyTester class has its kind set to "class" (not "interface")', function() { + expect(testerImplementation.kind).toBe('class'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/kindtag.js b/third_party/jsdoc/test/specs/tags/kindtag.js new file mode 100644 index 0000000000..49d9cd5ddc --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/kindtag.js @@ -0,0 +1,8 @@ +describe("@kind tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/kindtag.js'), + doc = docSet.getByLongname('x')[0]; + it("sets the doclet's 'kind' property to the tag value", function() { + expect(doc.kind).toBeDefined(); + expect(doc.kind).toBe('function'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/lendstag.js b/third_party/jsdoc/test/specs/tags/lendstag.js new file mode 100644 index 0000000000..0f4a685d68 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/lendstag.js @@ -0,0 +1,16 @@ +describe("@lends tag", function() { + // see also specs/documentation/lends.js for tests on @lends behaviour. + var doclet = require('jsdoc/doclet'), + doc = new doclet.Doclet('/** @lends */', {}), + doc2 = new doclet.Doclet('/** @lends MyClass# */', {}); + + it("sets the doclet's 'alias' property to the tag value or ", function() { + expect(doc.alias).toBe(''); + expect(doc2.alias).toBe('MyClass#'); + }); + + it("sets the doclet's 'undocumented' property to 'true'", function() { + expect(doc.undocumented).toBeTruthy(); + expect(doc2.undocumented).toBeTruthy(); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/licensetag.js b/third_party/jsdoc/test/specs/tags/licensetag.js new file mode 100644 index 0000000000..e37b1c6315 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/licensetag.js @@ -0,0 +1,8 @@ +describe("@license tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/licensetag.js'), + doc = docSet.getByLongname('x')[0]; + + it("sets the doclet's 'license' property to the tag value", function() { + expect(doc.license).toBe('GPL v2'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/listenstag.js b/third_party/jsdoc/test/specs/tags/listenstag.js new file mode 100644 index 0000000000..b200680450 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/listenstag.js @@ -0,0 +1,16 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ +describe("@listens tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/listenstag.js'), + doc = docSet.getByLongname('module:myModule~MyHandler')[0]; + + it("should create a 'listens' property on the doclet, an array, with the events that are listened to (with event namespace)", function() { + expect(Array.isArray(doc.listens)).toBeTruthy(); + expect(doc.listens).toContain('module:myModule.event:MyEvent'); + expect(doc.listens).toContain('module:myModule~Events.event:Event2'); + }); + + it("includes events even if non-existent", function() { + expect(doc.listens.length).toBe(3); + expect(doc.listens).toContain('event:fakeEvent'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/memberoftag.js b/third_party/jsdoc/test/specs/tags/memberoftag.js new file mode 100644 index 0000000000..0d43efa1dd --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/memberoftag.js @@ -0,0 +1,83 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ +describe("@memberof tag", function() { + + it('When a symbol has an @member tag, the doclet has a long name that includes the parent.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/memberoftag.js'), + Data = docSet.getByLongname('mathlib.Data')[0], + point = docSet.getByLongname('mathlib.Data#point')[0]; + + expect(typeof Data).toBe('object'); + expect(typeof point).toBe('object'); + + expect(Data.memberof).toBe('mathlib'); + expect(Data.name).toBe('Data'); + }); + + it('A symbol within a namespace for which no scope is specified.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/memberoftag4.js'), + doOtherStuff = docSet.getByLongname('doStuff.doOtherStuff')[0]; + + expect(doOtherStuff).toBeDefined(); + expect(doOtherStuff.scope).toBe('static'); + }); + + it('A symbol in which name === memberof.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/memberoftag4.js'), + doStuff = docSet.getByLongname('doStuff.doStuff')[0]; + + expect(doStuff).toBeDefined(); + expect(doStuff.scope).toBe('static'); + }); + + describe ("static", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/memberoftag2.js'), + publish = docSet.getByLongname('Observable#publish')[0], + cache = docSet.getByLongname('Observable.cache')[0]; + + it('A symbol is documented as a static @memberof a class.', function() { + //it should appear as a static member of that class + expect(typeof cache).toBe('object'); + expect(cache.memberof).toBe('Observable'); + expect(cache.scope).toBe('static'); + expect(cache.name).toBe('cache'); + expect(cache.longname).toBe('Observable.cache'); + }); + + it('A symbol is documented as a static @memberof a class prototype.', function() { + //it should appear as an instance member of that class + expect(typeof publish).toBe('object'); + expect(publish.memberof).toBe('Observable'); + expect(publish.scope).toBe('instance'); + expect(publish.name).toBe('publish'); + expect(publish.longname).toBe('Observable#publish'); + }); + }); + + describe ("forced", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/memberoftagforced.js'), + maproutes = docSet.getByLongname('map.routes')[0], + datapointy = docSet.getByLongname('Data#point.y')[0]; + + it('A nested symbol with a @memberof! tag set to .', function() { + expect(maproutes.name, 'map.routes', 'Has a shortname that includes the nested names.'); + }); + + it('A nested symbol with a @memberof! tag set to another symbol.', function() { + expect(datapointy.name, 'point.y', 'Has a shortname that includes the nested names.'); + }); + }); + + it('A symbol that is a nested class with a @memberof tag.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/memberoftag3.js'), + tree = docSet.getByLongname('module:terrain.Forest#Tree')[0]; + + expect(tree.longname, 'module:terrain.Forest#Tree'); + }); + + it('A symbol that is an instance member of a nested class with a @memberof tag.', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/memberoftag3.js'), + leaf = docSet.getByLongname('module:terrain.Forest#Tree#leaf')[0]; + + expect(leaf.longname, 'module:terrain.Forest#Tree#leaf'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/membertag.js b/third_party/jsdoc/test/specs/tags/membertag.js new file mode 100644 index 0000000000..2b311f148e --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/membertag.js @@ -0,0 +1,38 @@ +describe("@member tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/membertag.js'), + doc = docSet.getByLongname('x')[0], + doc2 = docSet.getByLongname('foobar')[0], + doc3 = docSet.getByLongname('baz')[0], + doc4 = docSet.getByLongname('y')[0]; + + it("sets the doclet's 'kind' property to 'member'", function() { + expect(doc.kind).toBe('member'); + expect(doc2.kind).toBe('member'); + expect(doc3.kind).toBe('member'); + expect(doc4.kind).toBe('member'); + }); + + it("If specified with a name, sets the doclet's name property", function() { + expect(doc.name).toBe('x'); + expect(doc2.name).toBe('foobar'); + expect(doc3.name).toBe('baz'); + }); + + it("If specified with a type and name, sets the doclet's type appropriately", function() { + expect(doc3.type).toBeDefined(); + expect(Array.isArray(doc3.type.names)).toBeTruthy(); + expect(doc3.type.names.length).toBe(1); + expect(doc3.type.names[0]).toBe('string'); + }); + + it("If specified with a type but no name, sets the doclet's name from the following JavaScript syntax", function() { + expect(doc4.name).toBe('y'); + }); + + it("If specified with a type but no name, sets the doclet's type appropriately", function() { + expect(doc4.type).toBeDefined(); + expect(Array.isArray(doc4.type.names)).toBeTruthy(); + expect(doc4.type.names.length).toBe(1); + expect(doc4.type.names[0]).toBe('Object'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/mixestag.js b/third_party/jsdoc/test/specs/tags/mixestag.js new file mode 100644 index 0000000000..6bb4f827b4 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/mixestag.js @@ -0,0 +1,19 @@ +describe("@mixes tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/mixintag.js'), + FormButton = docSet.getByLongname('FormButton')[0], + MyClass = docSet.getByLongname('MyClass')[0]; + + it("When a symbol has a @mixes tag, it gets an array property 'mixes' with the name of the mixin", function() { + expect(FormButton.mixes).toBeDefined(); + expect(Array.isArray(FormButton.mixes)).toBe(true); + expect(FormButton.mixes.length).toBe(1); + expect(FormButton.mixes[0]).toBe('Eventful'); + }); + + it("A symbol can @mixes multiple mixins and they are all added.", function() { + expect(MyClass.mixes).toBeDefined(); + expect(MyClass.mixes.length).toBe(2); + expect(MyClass.mixes).toContain('Eventful'); + expect(MyClass.mixes).toContain('AnotherMixin'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/mixintag.js b/third_party/jsdoc/test/specs/tags/mixintag.js new file mode 100644 index 0000000000..4c928fb26c --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/mixintag.js @@ -0,0 +1,13 @@ +describe("@mixin tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/mixintag.js'), + Eventful = docSet.getByLongname('Eventful')[0], + Mixin = docSet.getByLongname('AnotherMixin')[0]; + + it("When a symbol has a @mixin tag, the doclet's 'kind' property is set to 'mixin'", function() { + expect(Eventful.kind).toBe('mixin'); + }); + + it("When a symbol has a @mixin tag, its name is set to the tag's value (if present)", function() { + expect(Mixin).toBeDefined(); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/moduletag.js b/third_party/jsdoc/test/specs/tags/moduletag.js new file mode 100644 index 0000000000..08075f37d2 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/moduletag.js @@ -0,0 +1,90 @@ +/*global describe, expect, it, jasmine */ +'use strict'; + +describe('@module tag', function() { + describe("using 'this'", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/moduletag.js'); + var book = docSet.getByLongname('module:bookshelf.Book')[0]; + var title = docSet.getByLongname('module:bookshelf.Book#title')[0]; + + it('When a global symbol starts with "this" and is in a file with a @module tag, the symbol is documented as a member of that module.', function() { + expect(typeof book).toBe('object'); + expect(book.memberof).toBe('module:bookshelf'); + }); + + it('When an inner symbol starts with "this" and is in a file with a @module tag, the symbol is documented as a member of its enclosing constructor.', function() { + expect(typeof title).toBe('object'); + expect(title.memberof).toBe('module:bookshelf.Book'); + }); + }); + + describe('misc', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/moduletag2.js'); + var mixer = docSet.getByLongname('module:color/mixer').filter(function($) { + return ! $.undocumented; + })[0]; + var blend = docSet.getByLongname('module:color/mixer.blend')[0]; + var darken = docSet.getByLongname('module:color/mixer.darken')[0]; + + it('When a @module tag defines a module, a symbol of kind "module" is documented', function() { + expect(typeof mixer).toBe('object'); + expect(mixer.kind).toBe('module'); + }); + + it('When a @module tag defines a module, the module doclet does not have a "scope" property', function() { + expect(mixer.scope).not.toBeDefined(); + }); + + it('When an object literal is lent to a module with a @lends tag, a member of that object literal is documented as a member of the module', function() { + expect(typeof blend).toBe('object'); + expect(blend.kind).toBe('function'); + }); + + it('When a documented symbol is a member of a namespace "exports", it is documented as a member of the module', function() { + expect(typeof darken).toBe('object'); + expect(darken.kind).toBe('function'); + }); + }); + + describe('virtual comments', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/moduletag4.js'); + var m1 = docSet.getByLongname('module:M1').filter(function($) { + return ! $.undocumented; + })[0]; + var clickProperties = docSet.getByLongname('module:M1~ClickProperties')[0]; + var virtFunc = docSet.getByLongname('module:M1.VirtualComment')[0]; + var virtFunc2 = docSet.getByLongname('module:M1#VirtualComment2')[0]; + + it('When a virtual comment typedef is inside a module, the typedef is a memberof the module', function () { + expect(clickProperties.memberof).toBe('module:M1'); + }); + + it('When a virtual comment typedef is inside a module, the typedef longname contains the module name', function() { + expect(clickProperties.longname).toBe('module:M1~ClickProperties'); + }); + + it('When a virtual comment typedef is inside a module, the typedef scope is "inner"', function() { + expect(clickProperties.scope).toBe('inner'); + }); + + it('When a virtual comment function is inside a module with a static scope, the function has the correct memberof and longname', function () { + expect(virtFunc.longname).toBe('module:M1.VirtualComment'); + expect(virtFunc.memberof).toBe('module:M1'); + }); + + it('When a virtual comment function is inside a module with an instance scope, the function has the correct memberof and longname', function() { + expect(virtFunc2.longname).toBe('module:M1#VirtualComment2'); + expect(virtFunc2.memberof).toBe('module:M1'); + }); + }); + + describe('"module:" namespace included in the name', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/moduletag5.js'); + var bookshelf = docSet.getByLongname('module:bookshelf')[0]; + + it('When the name for a @module tag begins with the "module:" namespace, we remove the namespace', function() { + expect(typeof bookshelf).toBe('object'); + expect(bookshelf.name).toBe('bookshelf'); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/namespacetag.js b/third_party/jsdoc/test/specs/tags/namespacetag.js new file mode 100644 index 0000000000..751b4ce845 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/namespacetag.js @@ -0,0 +1,33 @@ +/*global describe, expect, it, jasmine */ +describe("@namespace tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/namespacetag.js'); + var x = docSet.getByLongname('x')[0]; + var Foo = docSet.getByLongname('Foo')[0]; + var Bar = docSet.getByLongname('Bar')[0]; + var Socket = docSet.getByLongname('S.Socket')[0]; + + it("sets the doclet's kind to 'namespace'", function () { + expect(x.kind).toBe('namespace'); + expect(Foo.kind).toBe('namespace'); + expect(Bar.kind).toBe('namespace'); + }); + + it("sets the doclet's name to the tag value (if provided)", function() { + expect(x.name).toBe('x'); + expect(Foo.name).toBe('Foo'); + expect(Bar.name).toBe('Bar'); + }); + + it("sets the doclet's type (if provided in @namespace)", function() { + expect(Bar.type).toBeDefined(); + expect(Array.isArray(Bar.type.names)).toBeTruthy(); + expect(Bar.type.names.length).toBe(1); + expect(Bar.type.names[0]).toBe('function'); + }); + + it("sets the doclet's longname correctly when the namespace is a substring of the name", + function() { + expect(Socket).toBeDefined(); + expect(Socket.name).toBe('Socket'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/nametag.js b/third_party/jsdoc/test/specs/tags/nametag.js new file mode 100644 index 0000000000..ab769af147 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/nametag.js @@ -0,0 +1,30 @@ +/*global describe, expect, it, jasmine */ +'use strict'; + +describe('@name tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/nametag.js'); + var view = docSet.getByLongname('View')[0]; + var controller = docSet.getByLongname('Controller')[0]; + var addToParent = docSet.getByLongname('MvcHelpers~addToParent')[0]; + + it('applies the specified name to the doclet', function() { + expect(view).toBeDefined(); + }); + + it('uses the name in the @name tag, ignoring the name in the code', function() { + expect(controller).toBeDefined(); + }); + + it('sets the doclet\'s scope to `global` by default', function() { + expect(view.scope).toBeDefined(); + expect(view.scope).toBe('global'); + + expect(controller.scope).toBeDefined(); + expect(controller.scope).toBe('global'); + }); + + it('uses the specified scope if one is provided', function() { + expect(addToParent).toBeDefined(); + expect(addToParent.scope).toBe('inner'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/overviewtag.js b/third_party/jsdoc/test/specs/tags/overviewtag.js new file mode 100644 index 0000000000..1d3568a8eb --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/overviewtag.js @@ -0,0 +1,81 @@ +/*global beforeEach, afterEach, describe, env, expect, it, jasmine */ + +describe("@overview tag", function() { + var path = require('jsdoc/path'); + var runtime = require('jsdoc/util/runtime'); + + var doclets; + + var pwd = env.pwd; + var srcParser = null; + var sourceFiles = env.sourceFiles.slice(0); + var sourcePaths = env.opts._.slice(0); + + beforeEach(function() { + env.opts._ = [path.normalize(env.pwd + '/test/fixtures/')]; + env.pwd = env.dirname; + env.sourceFiles = []; + srcParser = jasmine.createParser(); + require('jsdoc/src/handlers').attachTo(srcParser); + }); + + afterEach(function() { + env.opts._ = sourcePaths; + env.pwd = pwd; + env.sourceFiles = sourceFiles; + }); + + it('When a file overview tag appears in a doclet, the name of the doclet should contain the path to the file.', function() { + var filename = 'test/fixtures/file.js'; + + env.sourceFiles.push(filename); + doclets = srcParser.parse( + path.normalize( path.join(env.pwd, filename) ) + ); + expect(doclets[0].name).toMatch(/^file\.js$/); + }); + + it("The name and longname should be equal", function() { + var filename = 'test/fixtures/file.js'; + + env.sourceFiles.push(filename); + doclets = srcParser.parse( + path.normalize( path.join(env.pwd, filename) ) + ); + expect(doclets[0].name).toBe(doclets[0].longname); + }); + + it('The name should not include the entire filepath when the source file is outside the ' + + 'JSDoc directory', function() { + var Doclet = require('jsdoc/doclet').Doclet; + var os = require('os'); + + var doclet; + var docletMeta; + var docletSrc; + + var fakePath = '/Users/jdoe/foo/bar/someproject/junk/okayfile.js'; + + // set up the environment to reflect the fake filepath + env.pwd = '/Users/jdoe/someproject'; + env.sourceFiles = []; + env.opts._ = [fakePath]; + + // ensure that paths are resolved consistently on Windows + if (os.platform().indexOf('win') === 0) { + fakePath = 'c:' + fakePath; + env.pwd = 'c:' + env.pwd; + } + + // create a doclet with a fake filepath, then add a `@file` tag + docletSrc = '/** @class */'; + docletMeta = { + lineno: 1, + filename: fakePath + }; + doclet = new Doclet(docletSrc, docletMeta); + doclet.addTag('file', 'This file is pretty okay.'); + + expect(doclet.name).toBe('okayfile.js'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/paramtag.js b/third_party/jsdoc/test/specs/tags/paramtag.js new file mode 100644 index 0000000000..7953735dfb --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/paramtag.js @@ -0,0 +1,120 @@ +/*global describe, expect, it, jasmine */ +'use strict'; + +describe('@param tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/paramtag.js'); + var docSet2 = jasmine.getDocSetFromFile('test/fixtures/paramtag2.js'); + + it('When a symbol has an @param tag with a type before the name, the doclet has a params property that includes that param.', function() { + var find = docSet.getByLongname('find')[0]; + + expect(typeof find.params).toBe('object'); + expect(find.params.length).toBe(1); + expect(find.params[0].type.names.join(', ')).toBe('String, Array.'); + expect(find.params[0].name).toBe('targetName'); + expect(find.params[0].description).toBe('The name (or names) of what to find.'); + }); + + it('When a symbol has an @param tag with only a type and name, the doclet has a params property that includes that param.', function() { + var bind = docSet.getByLongname('bind')[0]; + + expect(typeof bind.params).toBe('object'); + expect(bind.params.length).toBe(1); + expect(bind.params[0].type.names.join(', ')).toBe('function'); + expect(bind.params[0].name).toBe('callback'); + expect(bind.params[0].description).toBeUndefined(); + }); + + it('When a symbol has an @param tag with only a type, the doclet has a params property that includes that param.', function() { + var unbind = docSet.getByLongname('unbind')[0]; + + expect(typeof unbind.params).toBe('object'); + expect(unbind.params.length).toBe(1); + expect(unbind.params[0].type.names.join(', ')).toBe('function'); + expect(unbind.params[0].description).toBeUndefined(); + }); + + it('When a symbol has an @param tag with no type, the doclet has a params property that includes that param.', function() { + var getElement = docSet.getByLongname('getElement')[0]; + + expect(typeof getElement.params).toBe('object'); + expect(getElement.params.length).toBe(1); + expect(getElement.params[0].type).toBeUndefined(); + expect(getElement.params[0].name).toBe('id'); + expect(getElement.params[0].description).toBe('The id of the element.'); + }); + + it('When a symbol has an @param tag with a non-alpha name like "...", the doclet has a params property that includes that param.', function() { + var combine = docSet.getByLongname('combine')[0]; + + expect(typeof combine.params).toBe('object'); + expect(combine.params.length).toBe(1); + expect(combine.params[0].type).toBeUndefined(); + expect(combine.params[0].name).toBe('...'); + expect(combine.params[0].description).toBe('Two or more elements.'); + }); + + it('When a symbol has an @param tag with name followed by a dash, the doclet has a params property that includes that param.', function() { + var split = docSet.getByLongname('split')[0]; + + expect(typeof split.params).toBe('object'); + expect(split.params.length).toBe(1); + expect(split.params[0].type).toBeUndefined(); + expect(split.params[0].name).toBe('delimiter'); + expect(split.params[0].description).toBe('What to split on.'); + }); + + it('When a symbol has an @param tag with no name or type, the doclet has a params property that includes that param.', function() { + var commit = docSet.getByLongname('commit')[0]; + + expect(typeof commit.params).toBe('object'); + expect(commit.params.length).toBe(1); + expect(commit.params[0].type).toBeUndefined(); + expect(commit.params[0].description).toBe('If true make the commit atomic.'); + }); + + it('When a symbol has a @param tag with no type but a name that indicates a default value or optional type, this is copied over to the params property.', function() { + var request = docSet.getByLongname('request')[0]; + + expect(typeof request.params).toBe('object'); + expect(request.params.length).toBe(1); + expect(request.params[0].type).toBeUndefined(); + expect(request.params[0].name).toBe('async'); + expect(request.params[0].defaultvalue).toBe('true'); + expect(request.params[0].optional).toBe(true); + expect(request.params[0].description).toBe('whether to be asynchronous'); + }); + + it('When a symbol has a @param tag with no name, the doclet includes the param name from the code', function() { + var commit = docSet.getByLongname('commit')[0]; + + expect(commit.params[0].name).toBe('atomic'); + }); + + it('When a symbol has a @param tag with no name, and the symbol is part of an assignment expression, the doclet includes the param name from the code', function() { + var classOpen = docSet.getByLongname('MySocket#open')[0]; + var moduleOpen = docSet2.getByLongname('module:mysocket.open')[0]; + + expect(classOpen.params[0].name).toBe('hostname'); + expect(classOpen.params[1].name).toBe('port'); + + expect(moduleOpen.params[0].name).toBe('hostname'); + expect(moduleOpen.params[1].name).toBe('port'); + }); + + it('When a symbol has a @param tag with an invalid type expression, the JSDoc comment is ignored.', function() { + var badDocSet = jasmine.getDocSetFromFile('test/fixtures/paramtaginvalidtype.js'); + var test = badDocSet.getByLongname('Test#test')[0]; + + expect(test).toBeDefined(); + expect(typeof test).toBe('object'); + + expect(test.meta).toBeDefined(); + expect(typeof test.meta).toBe('object'); + + expect(test.meta.filename).toBeDefined(); + expect(test.meta.filename).toBe('[[string0]]'); + + expect(test.description).not.toBeDefined(); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/privatetag.js b/third_party/jsdoc/test/specs/tags/privatetag.js new file mode 100644 index 0000000000..059f1af33e --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/privatetag.js @@ -0,0 +1,75 @@ +/*global afterEach, describe, expect, it, jasmine, spyOn */ +'use strict'; + +var definitions = require('jsdoc/tag/dictionary/definitions'); +var dictionary = require('jsdoc/tag/dictionary'); +var Dictionary = dictionary.Dictionary; +var doclet = require('jsdoc/doclet'); +var logger = require('jsdoc/util/logger'); + +var originalDictionary = dictionary; + +describe('@private tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/privatetag.js'); + var foo = docSet.getByLongname('Foo')[0]; + var bar = docSet.getByLongname('Foo#bar')[0]; + + it('When a symbol has a @private tag, the doclet has an `access` property set to `private`.', + function() { + expect(foo.access).toBe('private'); + }); + + it('When a symbol tagged with @private has members, the members do not inherit the @private ' + + 'tag.', function() { + expect(bar.access).not.toBeDefined(); + }); + + describe('JSDoc tags', function() { + afterEach(function() { + doclet._replaceDictionary(originalDictionary); + }); + + it('When JSDoc tags are enabled, the @private tag does not accept a value.', function() { + var dict = new Dictionary(); + var privateDocs; + + definitions.defineTags(dict, definitions.jsdocTags); + doclet._replaceDictionary(dict); + spyOn(logger, 'warn'); + + privateDocs = jasmine.getDocSetFromFile('test/fixtures/privatetag2.js'); + + expect(logger.warn).toHaveBeenCalled(); + }); + }); + + describe('Closure Compiler tags', function() { + afterEach(function() { + doclet._replaceDictionary(originalDictionary); + }); + + it('When Closure Compiler tags are enabled, the @private tag accepts a type expression.', + function() { + var connectionPorts; + var dict = new Dictionary(); + var privateDocs; + + definitions.defineTags(dict, definitions.closureTags); + doclet._replaceDictionary(dict); + spyOn(logger, 'warn'); + + privateDocs = jasmine.getDocSetFromFile('test/fixtures/privatetag2.js'); + connectionPorts = privateDocs.getByLongname('connectionPorts')[0]; + + expect(logger.warn).not.toHaveBeenCalled(); + + expect(connectionPorts).toBeDefined(); + expect(connectionPorts.access).toBe('private'); + + expect(connectionPorts.type).toBeDefined(); + expect(connectionPorts.type.names).toBeDefined(); + expect(connectionPorts.type.names.length).toBe(1); + expect(connectionPorts.type.names[0]).toBe('Object.'); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/propertytag.js b/third_party/jsdoc/test/specs/tags/propertytag.js new file mode 100644 index 0000000000..3fe20ac415 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/propertytag.js @@ -0,0 +1,21 @@ +'use strict'; + +describe('@property tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/propertytag.js'), + myobject = docSet.getByLongname('myobject')[0]; + + it('When a symbol has a @property tag, the property appears in the doclet.', function() { + expect(typeof myobject.properties).toBe('object'); + expect(myobject.properties.length).toBe(4); + + expect(myobject.properties[0].name).toBe('id'); + expect(myobject.properties[1].name).toBe('defaults'); + expect(myobject.properties[2].name).toBe('defaults.a'); + expect(myobject.properties[3].name).toBe('defaults.b'); + + expect(myobject.properties[0].defaultvalue).toBe('abc123'); + + expect(myobject.properties[1].description).toBe('The default values.'); + expect(myobject.properties[1].type.names[0]).toBe('Object'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/protectedtag.js b/third_party/jsdoc/test/specs/tags/protectedtag.js new file mode 100644 index 0000000000..b904a97f1c --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/protectedtag.js @@ -0,0 +1,75 @@ +/*global afterEach, describe, expect, it, jasmine, spyOn */ +'use strict'; + +var definitions = require('jsdoc/tag/dictionary/definitions'); +var dictionary = require('jsdoc/tag/dictionary'); +var Dictionary = dictionary.Dictionary; +var doclet = require('jsdoc/doclet'); +var logger = require('jsdoc/util/logger'); + +var originalDictionary = dictionary; + +describe('@protected tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/protectedtag.js'); + var uidCounter = docSet.getByLongname('module:uid~uidCounter')[0]; + var uidRoot = docSet.getByLongname('module:uid~uidObjects.root')[0]; + + it('When a symbol has a @protected tag, the doclet has an `access` property set to ' + + '`protected`.', function() { + expect(uidCounter.access).toBe('protected'); + }); + + it('When a symbol tagged with @protected has members, the members do not inherit the ' + + '@protected tag.', function() { + expect(uidRoot.access).not.toBeDefined(); + }); + + describe('JSDoc tags', function() { + afterEach(function() { + doclet._replaceDictionary(originalDictionary); + }); + + it('When JSDoc tags are enabled, the @protected tag does not accept a value.', function() { + var dict = new Dictionary(); + var protectedDocs; + + definitions.defineTags(dict, definitions.jsdocTags); + doclet._replaceDictionary(dict); + spyOn(logger, 'warn'); + + protectedDocs = jasmine.getDocSetFromFile('test/fixtures/protectedtag2.js'); + + expect(logger.warn).toHaveBeenCalled(); + }); + }); + + describe('Closure Compiler tags', function() { + afterEach(function() { + doclet._replaceDictionary(originalDictionary); + }); + + it('When Closure Compiler tags are enabled, the @private tag accepts a type expression.', + function() { + var dict = new Dictionary(); + var protectedDocs; + var uidCounter; + + definitions.defineTags(dict, definitions.closureTags); + doclet._replaceDictionary(dict); + spyOn(logger, 'warn'); + + protectedDocs = jasmine.getDocSetFromFile('test/fixtures/protectedtag2.js'); + uidCounter = protectedDocs.getByLongname('uidCounter')[0]; + + expect(logger.warn).not.toHaveBeenCalled(); + + expect(uidCounter).toBeDefined(); + expect(uidCounter.access).toBe('protected'); + + expect(uidCounter.type).toBeDefined(); + expect(uidCounter.type.names).toBeDefined(); + expect(uidCounter.type.names.length).toBe(1); + expect(uidCounter.type.names[0]).toBe('number'); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/readonlytag.js b/third_party/jsdoc/test/specs/tags/readonlytag.js new file mode 100644 index 0000000000..8465cb804d --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/readonlytag.js @@ -0,0 +1,9 @@ +describe("@readonly tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/readonlytag.js'), + Collection = docSet.getByLongname('Collection')[0], + length = docSet.getByLongname('Collection#length')[0]; + + it('When a symbol has an @readonly tag, the doclet has an readonly property that is true.', function() { + expect(length.readonly).toBe(true); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/requirestag.js b/third_party/jsdoc/test/specs/tags/requirestag.js new file mode 100644 index 0000000000..f6bcf7acb5 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/requirestag.js @@ -0,0 +1,24 @@ +/*global describe: true, expect: true, it: true, jasmine: true */ +describe("@requires tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/requirestag.js'); + var foo = docSet.getByLongname('foo')[0]; + var bar = docSet.getByLongname('bar')[0]; + var baz = docSet.getByLongname('baz')[0]; + + it('When a symbol has a @requires tag, the doclet has a requires property that includes that value, with the "module:" namespace added.', function() { + expect( Array.isArray(foo.requires) ).toBe(true); + expect(foo.requires[0]).toBe('module:foo/helper'); + + expect( Array.isArray(bar.requires) ).toBe(true); + expect(bar.requires[0]).toBe('module:foo'); + expect(bar.requires[1]).toBe('module:Pez#blat'); + }); + + it('When a symbol has a @requires tag whose value is an inline {@link} tag, the doclet has a requires property that includes that tag without modification.', function() { + expect( Array.isArray(baz.requires) ).toBe(true); + expect(baz.requires[0]).toBe('{@link module:zest}'); + expect(baz.requires[1]).toBe('{@linkplain module:zing}'); + // by design, we don't validate the tag name, as long as it starts with @link + expect(baz.requires[2]).toBe('{@linkstupid module:pizzazz}'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/returnstag.js b/third_party/jsdoc/test/specs/tags/returnstag.js new file mode 100644 index 0000000000..fdf6132189 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/returnstag.js @@ -0,0 +1,37 @@ +/*global describe, expect, it, jasmine */ +describe('@returns tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/returnstag.js'); + + it('When a symbol has a @returns tag with a type and description, the doclet has a "returns" property that includes that info.', function() { + var find = docSet.getByLongname('find')[0]; + + expect(typeof find.returns).toBe('object'); + expect(find.returns.length).toBe(1); + expect(find.returns[0].type.names.join(', ')).toBe('string, Array.'); + expect(find.returns[0].description).toBe('The names of the found item(s).'); + }); + + it('When a symbol has a @returns tag with a non-nullable type, the doclet indicates that the type is non-nullable', function() { + var getName = docSet.getByLongname('getName')[0]; + + expect(typeof getName.returns).toBe('object'); + expect(getName.returns.length).toBe(1); + expect(getName.returns[0].nullable).toBe(false); + }); + + it('When a symbol has a @returns tag with only a description, the doclet has a "returns" property that includes the description.', function() { + var bind = docSet.getByLongname('bind')[0]; + + expect(typeof bind.returns).toBe('object'); + expect(bind.returns.length).toBe(1); + expect(bind.returns[0].description).toBe('The binding id.'); + }); + + it('When a symbol has a @returns tag without a type but with an inline tag, the inline tag is not mistaken for a type.', function() { + var convert = docSet.getByLongname('convert')[0]; + + expect(typeof convert.returns).toBe('object'); + expect(convert.returns.length).toBe(1); + expect(convert.returns[0].description).toBe('An object to be passed to {@link find}.'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/scopetags.js b/third_party/jsdoc/test/specs/tags/scopetags.js new file mode 100644 index 0000000000..0b18a58c33 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/scopetags.js @@ -0,0 +1,31 @@ +describe('scope tags', function () { + var docSet = jasmine.getDocSetFromFile('test/fixtures/scopetags.js'); + + // @inner, @instance, @static (@global has its own file) + describe("@inner tag", function() { + var doc = docSet.getByLongname('module:scopetags~myInner')[0]; + + it("sets the doclet's 'scope' property to 'inner'", function() { + expect(doc.scope).toBeDefined(); + expect(doc.scope).toBe('inner'); + }); + }); + + describe("@instance tag", function() { + var doc = docSet.getByLongname('module:scopetags#myInstance')[0]; + + it("sets the doclet's 'scope' property to 'instance'", function() { + expect(doc.scope).toBeDefined(); + expect(doc.scope).toBe('instance'); + }); + }); + + describe("@static tag", function() { + var doc = docSet.getByLongname('module:scopetags.myStatic')[0]; + + it("sets the doclet's 'scope' property to 'static'", function() { + expect(doc.scope).toBeDefined(); + expect(doc.scope).toBe('static'); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/seetag.js b/third_party/jsdoc/test/specs/tags/seetag.js new file mode 100644 index 0000000000..d6228a5958 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/seetag.js @@ -0,0 +1,13 @@ +describe("@see tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/seetag.js'), + foo = docSet.getByLongname('foo')[0], + bar = docSet.getByLongname('bar')[0]; + + it('When a symbol has an @see tag, the doclet has a see property that includes that value.', function() { + expect(typeof foo.see).toBe('object'); + expect(foo.see[0]).toBe('{@link bar}'); + + expect(typeof bar.see).toBe('object'); + expect(bar.see[0]).toBe('http://example.com/someref'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/sincetag.js b/third_party/jsdoc/test/specs/tags/sincetag.js new file mode 100644 index 0000000000..91ab3d838b --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/sincetag.js @@ -0,0 +1,8 @@ +describe("@since tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/sincetag.js'), + foo = docSet.getByLongname('foo')[0]; + + it('When a symbol has an @since tag, the doclet has a since property set to true.', function() { + expect(foo.since).toBe('1.2.3'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/summarytag.js b/third_party/jsdoc/test/specs/tags/summarytag.js new file mode 100644 index 0000000000..f24874422f --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/summarytag.js @@ -0,0 +1,7 @@ +describe("@summary tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/summarytag.js'), + doc = docSet.getByLongname('Sam')[0]; + it("sets the doclet's 'summary' property to the tag value", function() { + expect(doc.summary).toBe('I do not like green eggs and ham!'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/thistag.js b/third_party/jsdoc/test/specs/tags/thistag.js new file mode 100644 index 0000000000..5fac4ae694 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/thistag.js @@ -0,0 +1,16 @@ +describe("@this tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/thistag.js'), + setName = docSet.getByLongname('setName')[0], + fooName = docSet.getByLongname('Foo#name')[0]; + + it('When a symbol has a @this tag, the doclet has a this property that is set to that value.', function() { + expect(setName['this']).toBe('Foo'); + }); + + it('When a this symbol is documented inside a function with a @this tag, the symbol is documented as a member of that tags value.', function() { + expect(typeof fooName).toBe('object'); + expect(fooName.name).toBe('name'); + expect(fooName.memberof).toBe('Foo'); + expect(fooName.scope).toBe('instance'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/todotag.js b/third_party/jsdoc/test/specs/tags/todotag.js new file mode 100644 index 0000000000..410bb55fe7 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/todotag.js @@ -0,0 +1,13 @@ +describe("@todo tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/todotag.js'), + doc = docSet.getByLongname('x')[0]; + + it("adds the entries into a 'todo' array on the doclet", function() { + expect(doc.todo).toBeDefined(); + expect(Array.isArray(doc.todo)).toBeTruthy(); + expect(doc.todo.length).toBe(2); + + expect(doc.todo).toContain('something'); + expect(doc.todo).toContain('something else'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/tutorialtag.js b/third_party/jsdoc/test/specs/tags/tutorialtag.js new file mode 100644 index 0000000000..ebb034b3f5 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/tutorialtag.js @@ -0,0 +1,12 @@ +describe("@tutorial tag", function() { + // these are tests for the block usage, not the inline usage. see util/templateHelper for that. + var docSet = jasmine.getDocSetFromFile('test/fixtures/tutorialtag.js'), + doc = docSet.getByLongname('x')[0]; + + it("adds the listed tutorials to a 'tutorials' array on the doclet", function () { + expect(Array.isArray(doc.tutorials)).toBeTruthy(); + expect(doc.tutorials.length).toBe(2); + expect(doc.tutorials).toContain('tute1'); + expect(doc.tutorials).toContain('tute2'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/typedeftag.js b/third_party/jsdoc/test/specs/tags/typedeftag.js new file mode 100644 index 0000000000..aa65fea502 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/typedeftag.js @@ -0,0 +1,40 @@ +/*global describe, expect, it, jasmine */ +describe('@typedef tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/typedeftag.js'); + var numberlike = docSet.getByLongname('calc.NumberLike')[0]; + var operator = docSet.getByLongname('calc.Operator')[0]; + var result = docSet.getByLongname('calc.Result')[0]; + var calculatorBattery = docSet.getByLongname('CalculatorBattery')[0]; + + it('When a comment has a @typedef tag, the doclet has a kind property set to "typedef".', function() { + expect(numberlike.kind).toBe('typedef'); + }); + + it('When a comment has a @typedef tag with a type, the doclet has a type property set to that type.', function() { + expect(typeof numberlike.type).toBe('object'); + expect(typeof numberlike.type.names).toBe('object'); + expect(numberlike.type.names.length).toBe(2); + expect(numberlike.type.names[0]).toBe('string'); + expect(numberlike.type.names[1]).toBe('number'); + }); + + it('When a comment has a @typedef tag with a name, the doclet has a name property set to that name.', function() { + expect(numberlike.name).toBe('NumberLike'); + expect(numberlike.longname).toBe('calc.NumberLike'); + }); + + it('When a symbol has a @typedef tag without a name, the doclet has a name property set to the symbol name.', function() { + expect(operator.name).toBe('Operator'); + expect(operator.longname).toBe('calc.Operator'); + }); + + it('When a symbol has a @typedef tag with a name, the name in the tag takes precedence over the symbol name.', function() { + expect(result.name).toBe('Result'); + expect(result.longname).toBe('calc.Result'); + }); + + it('When a symbol has a @typedef tag with a name and no scope, the scope defaults to `global`.', function() { + expect(calculatorBattery).toBeDefined(); + expect(calculatorBattery.scope).toBe('global'); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/typekind.js b/third_party/jsdoc/test/specs/tags/typekind.js new file mode 100644 index 0000000000..aea60db02a --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/typekind.js @@ -0,0 +1,15 @@ +describe("@kind tag with type", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/typekind.js'), + blog = docSet.getByLongname('module:blog/server')[0], + port = docSet.getByLongname('module:blog/server.port')[0]; + + it('When a module symbol has an kind tag, that includes a {type} clause, the doclet has a type property set to that {type} clause', function() { + expect(typeof blog.type).toBe('object'); + expect(blog.type.names.join(', ')).toBe('ConnectServer'); + }); + + it('When a property symbol has an kind tag, that includes a {type} clause, the doclet has a type property set to that {type} clause', function() { + expect(typeof port.type).toBe('object'); + expect(port.type.names.join(', ')).toBe('number'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/specs/tags/typetag.js b/third_party/jsdoc/test/specs/tags/typetag.js new file mode 100644 index 0000000000..2a863d8140 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/typetag.js @@ -0,0 +1,77 @@ +/*global afterEach, describe, expect, it, jasmine, spyOn */ +'use strict'; + +var definitions = require('jsdoc/tag/dictionary/definitions'); +var dictionary = require('jsdoc/tag/dictionary'); +var Dictionary = dictionary.Dictionary; +var doclet = require('jsdoc/doclet'); +var logger = require('jsdoc/util/logger'); + +var originalDictionary = dictionary; + +describe('@type tag', function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/typetag.js'); + + it('When a symbol has a @type tag, the doclet has a type property set to that value\'s type.', function() { + var foo = docSet.getByLongname('foo')[0]; + + expect(typeof foo.type).toBe('object'); + expect(typeof foo.type.names).toBe('object'); + expect(foo.type.names.join(', ')).toBe('string, Array.'); + }); + + it('When a symbol has a @type tag set to a plain string, the doclet has a type property set to that value\'s type.', function() { + var bar = docSet.getByLongname('bar')[0]; + + expect(bar.type.names.join(', ')).toBe('integer'); + }); + + it('When a symbol has a @type tag for a non-nullable type, the doclet indicates that the type is non-nullable', function() { + var baz = docSet.getByLongname('baz')[0]; + + expect(baz.nullable).toBe(false); + }); + + describe('JSDoc tags', function() { + afterEach(function() { + doclet._replaceDictionary(originalDictionary); + }); + + it('When JSDoc tags are enabled, the @type tag does not accept a description.', function() { + var dict = new Dictionary(); + var typeDocs; + + definitions.defineTags(dict, definitions.jsdocTags); + doclet._replaceDictionary(dict); + spyOn(logger, 'warn'); + + typeDocs = jasmine.getDocSetFromFile('test/fixtures/typetag2.js'); + + expect(logger.warn).toHaveBeenCalled(); + }); + }); + + describe('Closure tags', function() { + afterEach(function() { + doclet._replaceDictionary(originalDictionary); + }); + + it('When Closure tags are enabled, the @type tag accepts a description.', function() { + var dict = new Dictionary(); + var stringOrNumber; + var typeDocs; + + definitions.defineTags(dict, definitions.closureTags); + doclet._replaceDictionary(dict); + spyOn(logger, 'warn'); + + typeDocs = jasmine.getDocSetFromFile('test/fixtures/typetag2.js'); + stringOrNumber = typeDocs.getByLongname('stringOrNumber')[0]; + + expect(logger.warn).not.toHaveBeenCalled(); + + expect(stringOrNumber).toBeDefined(); + expect(stringOrNumber.description).toBe('A string or a number.'); + }); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/undocumentedtag.js b/third_party/jsdoc/test/specs/tags/undocumentedtag.js new file mode 100644 index 0000000000..daf14a5306 --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/undocumentedtag.js @@ -0,0 +1,12 @@ +describe("@undocumented tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/undocumentedtag.js'), + doc = docSet.getByLongname('x')[0]; + + it("sets the doclet's 'undocumented' property to true", function () { + expect(doc.undocumented).toBeTruthy(); + }); + + it("clears the doclet's 'comment' property", function () { + expect(doc.comment).toBe(''); + }); +}); diff --git a/third_party/jsdoc/test/specs/tags/versiontag.js b/third_party/jsdoc/test/specs/tags/versiontag.js new file mode 100644 index 0000000000..1b50ada28a --- /dev/null +++ b/third_party/jsdoc/test/specs/tags/versiontag.js @@ -0,0 +1,8 @@ +describe("@version tag", function() { + var docSet = jasmine.getDocSetFromFile('test/fixtures/versiontag.js'), + foo = docSet.getByLongname('foo')[0]; + + it('When a symbol has an @version tag, the doclet has a version property set to that value.', function() { + expect(foo.version).toBe('1.2.3'); + }); +}); \ No newline at end of file diff --git a/third_party/jsdoc/test/tutorials/build.sh b/third_party/jsdoc/test/tutorials/build.sh new file mode 100755 index 0000000000..8937f26da0 --- /dev/null +++ b/third_party/jsdoc/test/tutorials/build.sh @@ -0,0 +1,2 @@ +rm -rf out +../../jsdoc -u tutorials src -d out diff --git a/third_party/jsdoc/test/tutorials/duplicateDefined/asdf.html b/third_party/jsdoc/test/tutorials/duplicateDefined/asdf.html new file mode 100644 index 0000000000..1f8a662233 --- /dev/null +++ b/third_party/jsdoc/test/tutorials/duplicateDefined/asdf.html @@ -0,0 +1 @@ +

      tutorial ASDF

      diff --git a/third_party/jsdoc/test/tutorials/duplicateDefined/asdf.json b/third_party/jsdoc/test/tutorials/duplicateDefined/asdf.json new file mode 100644 index 0000000000..c769dc5489 --- /dev/null +++ b/third_party/jsdoc/test/tutorials/duplicateDefined/asdf.json @@ -0,0 +1 @@ +{"title": "Conflicting title"} diff --git a/third_party/jsdoc/test/tutorials/duplicateDefined/index.json b/third_party/jsdoc/test/tutorials/duplicateDefined/index.json new file mode 100644 index 0000000000..9ceb182fae --- /dev/null +++ b/third_party/jsdoc/test/tutorials/duplicateDefined/index.json @@ -0,0 +1,5 @@ +{ + "asdf": { + "title": "Tutorial Asdf" + } +} diff --git a/third_party/jsdoc/test/tutorials/incomplete/parent.html b/third_party/jsdoc/test/tutorials/incomplete/parent.html new file mode 100644 index 0000000000..aa4a4ff951 --- /dev/null +++ b/third_party/jsdoc/test/tutorials/incomplete/parent.html @@ -0,0 +1,3 @@ +

      Test.html

      + +

      {@link Test}

      diff --git a/third_party/jsdoc/test/tutorials/incomplete/parent.json b/third_party/jsdoc/test/tutorials/incomplete/parent.json new file mode 100644 index 0000000000..f2e4765a71 --- /dev/null +++ b/third_party/jsdoc/test/tutorials/incomplete/parent.json @@ -0,0 +1 @@ +{"title": "missing child tutorial", "children": ["child"]} diff --git a/third_party/jsdoc/test/tutorials/src/x.js b/third_party/jsdoc/test/tutorials/src/x.js new file mode 100644 index 0000000000..987f8f982a --- /dev/null +++ b/third_party/jsdoc/test/tutorials/src/x.js @@ -0,0 +1,8 @@ +/** + * Test {@tutorial test2} {@tutorial dupa} + * + * @class + * @tutorial test + * @tutorial jasia + */ +function Test() {} diff --git a/third_party/jsdoc/test/tutorials/tutorials/constructor.md b/third_party/jsdoc/test/tutorials/tutorials/constructor.md new file mode 100644 index 0000000000..2f181f0854 --- /dev/null +++ b/third_party/jsdoc/test/tutorials/tutorials/constructor.md @@ -0,0 +1 @@ +This tutorial has a tricksy name to make sure we are not loading Array.constructor or Object.constructor. diff --git a/third_party/jsdoc/test/tutorials/tutorials/multiple.json b/third_party/jsdoc/test/tutorials/tutorials/multiple.json new file mode 100644 index 0000000000..994d86559a --- /dev/null +++ b/third_party/jsdoc/test/tutorials/tutorials/multiple.json @@ -0,0 +1,12 @@ +{ + "test2": { + "title": "Test 2", + "children": ["test3", "test6"] + }, + "test3": { + "title": "Test 3", + "children": { + "test4": {"title": "Test 4"} + } + } +} diff --git a/third_party/jsdoc/test/tutorials/tutorials/recursive/test_recursive.md b/third_party/jsdoc/test/tutorials/tutorials/recursive/test_recursive.md new file mode 100644 index 0000000000..d87c0708b4 --- /dev/null +++ b/third_party/jsdoc/test/tutorials/tutorials/recursive/test_recursive.md @@ -0,0 +1 @@ +# test_recursive.md \ No newline at end of file diff --git a/third_party/jsdoc/test/tutorials/tutorials/test.html b/third_party/jsdoc/test/tutorials/tutorials/test.html new file mode 100644 index 0000000000..aa4a4ff951 --- /dev/null +++ b/third_party/jsdoc/test/tutorials/tutorials/test.html @@ -0,0 +1,3 @@ +

      Test.html

      + +

      {@link Test}

      diff --git a/third_party/jsdoc/test/tutorials/tutorials/test.json b/third_party/jsdoc/test/tutorials/tutorials/test.json new file mode 100644 index 0000000000..d894f21e77 --- /dev/null +++ b/third_party/jsdoc/test/tutorials/tutorials/test.json @@ -0,0 +1 @@ +{"title": "Test tutorial", "children": ["test2"]} diff --git a/third_party/jsdoc/test/tutorials/tutorials/test2.markdown b/third_party/jsdoc/test/tutorials/tutorials/test2.markdown new file mode 100644 index 0000000000..09510c03c4 --- /dev/null +++ b/third_party/jsdoc/test/tutorials/tutorials/test2.markdown @@ -0,0 +1 @@ +# test2.markdown diff --git a/third_party/jsdoc/test/tutorials/tutorials/test3.htm b/third_party/jsdoc/test/tutorials/tutorials/test3.htm new file mode 100644 index 0000000000..8eac05240a --- /dev/null +++ b/third_party/jsdoc/test/tutorials/tutorials/test3.htm @@ -0,0 +1,3 @@ +

      Test3.html

      + +

      {@link Test}

      diff --git a/third_party/jsdoc/test/tutorials/tutorials/test4.md b/third_party/jsdoc/test/tutorials/tutorials/test4.md new file mode 100644 index 0000000000..0be836ad34 --- /dev/null +++ b/third_party/jsdoc/test/tutorials/tutorials/test4.md @@ -0,0 +1 @@ +# test4.md diff --git a/third_party/jsdoc/test/tutorials/tutorials/test5.txt b/third_party/jsdoc/test/tutorials/tutorials/test5.txt new file mode 100644 index 0000000000..1769f0de89 --- /dev/null +++ b/third_party/jsdoc/test/tutorials/tutorials/test5.txt @@ -0,0 +1 @@ +Should not be included as a tutorial. diff --git a/third_party/jsdoc/test/tutorials/tutorials/test6.xml b/third_party/jsdoc/test/tutorials/tutorials/test6.xml new file mode 100644 index 0000000000..1c489be3e7 --- /dev/null +++ b/third_party/jsdoc/test/tutorials/tutorials/test6.xml @@ -0,0 +1 @@ +

      test 6 - has no metadata

      diff --git a/tutorials/caveats.html b/tutorials/caveats.html new file mode 100644 index 0000000000..af7a06e711 --- /dev/null +++ b/tutorials/caveats.html @@ -0,0 +1,46 @@ + + +

      +There are several content scenarios we do not support. We make no claims about +their validity within the DASH specification nor any related specification. We +have not found any of these things in any existing DASH content. Most of these +restrictions allow us to make simplifying assumptions in the code. + +

        +
      • MPD Representations containing more than one of SegmentBase, SegmentList, + or SegmentBase.
      • +
      • MPD SegmentTimelines which contain gaps between segments.
      • +
      • MPD SegmentTimeline combined with an explicit segment index URL.
      • +
      • Representations within an AdaptationSet where the MIME type varies.
      • +
      • Changing DRM schemes across MPD Periods.
      • +
      • Changing DRM schemes when switching MPD Representations.
      • +
      • WebM streams without initialization segments.
      • +
      • Parsing dynamically-sized EBML elements in WebM streams.
      • +
      • Parsing 8-byte EBML IDs in WebM streams. *
      • +
      • Parsing EBML integers with more than 53 significant bits in WebM streams. + *
      • +
      • Parsing nested SIDX boxes in MP4 streams.
      • +
      • MP4 video AdaptationSet with WebM audio AdaptationSet, or vice-versa. +
        • We support MPDs with both MP4 and WebM in the same manifest, but we + will choose either MP4 or WebM for both audio and video. They will + never be mixed.
        +
      • +
      + +* = JavaScript limitation +

      + diff --git a/tutorials/caveats.json b/tutorials/caveats.json new file mode 100644 index 0000000000..4de0e12d9a --- /dev/null +++ b/tutorials/caveats.json @@ -0,0 +1,3 @@ +{ + "title": "Content Restrictions" +} diff --git a/tutorials/dev.html b/tutorials/dev.html new file mode 100644 index 0000000000..772942e05c --- /dev/null +++ b/tutorials/dev.html @@ -0,0 +1,225 @@ + + +

      +Quick Setup (TL;DR) +

      + +

      +To get started quickly, just run ./build/all.sh and skip this document. +

      + + +

      +Closure, Annotations, and the Build Process +

      + +

      +The Shaka Player library was designed to be compiled with Google's open-sourced +JavaScript compiler, {@link http://goo.gl/HZfHi Closure}. The Closure compiler +produces JavaScript code which is minified, optimized, obfuscated, and stripped +of dead code. Deploying a Closure-compiled JavaScript library saves bandwidth, +and the browser can also load and parse the code faster. +

      + +

      +Closure also enforces structure and type-safety on the JavaScript language. It +allows developers to annotate code with type information. The compiler can use +this information to optimize the code, but it also helps catch bugs early, such +as missing arguments, arguments of the wrong type, typos in names, etc. +

      + +

      +The annotation syntax is derived from {@link http://usejsdoc.org/ JSDoc}. This +means that the annotated code is also self-documenting. We generate docs based +on these annotations using JSDoc. For information on annotation syntax, please +refer to {@link http://goo.gl/xxHG2W Annotating JavaScript} on Closure's site. +

      + +

      +The Closure compiler and JSDoc are both included in the source. To compile the +Shaka Player library, simply run ./build/all.sh from the source root. Compiled +JavaScript will be output to shaka-player.compiled.js in the source root. +

      + +

      +To generate documentation, run ./build/docs.sh from the source root. Docs will +be output to ./docs/api/ in HTML format. +

      + + +

      +The Test App +

      + +

      +The project includes a test application which we used for manual testing during +development. The test app is made up of index.html, index.css, and app.js, and +can be accessed by pointing a web server at your source code checkout. +

      + +

      +Many of the settings in the test app can be controlled with URL parameters such +that the page can be reloaded without the need to manually change settings. To +see a canonical list of URL parameters, see app.js. +

      + +

      +Some parameters require a value, but most are boolean. The behavior of boolean +parameters is activated by presence. Parameters are separated by semicolons. + +

        +
      • lang=LANG - Changes the language preference fed to the library. Language + settings use language tags from {@link http://goo.gl/J6yQvS BCP 47}, such + as "en", "en-US', "fr-CA", "el", "deu-AT", etc.
      • +
      • nocenc - Select the non-encrypted version of the default sample.
      • +
      • vp9 - Select a VP9 DASH sample.
      • +
      • dash - Auto-play the selected DASH stream.
      • +
      • compiled - Load the library in compiled mode. See "Loader" below.
      • +
      • debug - Set the log level to show debug messages.
      • +
      • v - Set the log level to show debug and verbose messages.
      • +
      +

      + +

      +Example URLs for the test app: +

        +
      • http://localhost/shaka/?nocenc +
        • Defaults the UI to a non-encrypted sample.
        +
      • +
      • http://localhost/shaka/?dash;vp9 +
        • Selects the VP9 sample and auto-plays it.
        +
      • +
      • http://localhost/shaka/?dash;lang=fr +
        • Sets the language to French and auto-plays.
        +
      • +
      • http://localhost/shaka/?compiled;dash +
        • Auto-plays the default sample in compiled mode.
        +
      • +
      +

      + + +

      +The Loader +

      + +

      +The Shaka Player library can be used without compiling it. This is useful when +making changes, since it shortens the testing cycle for the developer. But the +library will be deployed in a compiled form, so it is critical to test a change +using the compiled library once it has been vetted uncompiled. +

      + +

      +To make it easier to switch between compiled and uncompiled mode during testing +and development, load.js acts as a shim between index.html and the library. It +will select which version of the library to load based on the boolean parameter +"compiled". (See "Test App" above for info on parameters.) +

      + +

      +At any time during development or testing, you can switch modes by changing the +URL. Once the application has loaded, you cannot change modes without reloading +the page. A production application would directly include the compiled library +instead of using this loader. +

      + +

      +Remember, when running in compiled mode, you must recompile the library after a +change by running ./build/all.sh. +

      + + +

      +Tests +

      + +

      +Tests live in the "spec" folder and are run by spec_runner.html. To run tests, +just point your browser at this HTML file. Do not do this using a file:// URL, +but through a local web server. +

      + + +

      +Source Code Layout +

      + +

      +

        +
      • assets/ - assets for smoke-testing basic functionality
      • +
      • build/ - build scripts +
          +
        • build.sh - compiles the library and generates + shaka-player.compiled.js
        • +
        • gendeps.sh - computes dependencies for running non-compiled code
        • +
        • lint.sh - checks the code for style issues
        • +
        • check.sh - combination of build.sh and lint.sh
        • +
        • all.sh - combination of gendeps.sh, build.sh, and lint.sh
        • +
        • docs.sh - generate documentation
        • +
        +
      • +
      • docs/ - documentation +
          +
        • api/ - generated documentation
        • +
        • reference/ - reference documents
        • +
        +
      • +
      • externs/ - definitions of external APIs, used by the compiler
      • +
      • lib/ - the Shaka Player Library source, organized by namespace +
          +
        • dash/ - DASH-related classes (internal)
        • +
        • debug/ - debug-related classes (internal)
        • +
        • player/ - all classes that an integrator must interact with
        • +
        • polyfill/ - all {@tutorial polyfills}
        • +
        • util/ - utility classes (internal)
        • +
        +
      • +
      • spec/ - unit and integration tests
      • +
      • spec_runner.html - front-end to run unit and integration tests
      • +
      • support.html - browser API support test
      • +
      • third_party/ - third_party dependencies +
          +
        • SUMMARY.txt - summary of all libraries and their licenses
        • +
        • blanket_jasmine/ - Blanket JS coverage library
        • +
        • closure/ - Closure JS compiler and JS library
        • +
        • jasmine/ - Jasmine JS testing framework
        • +
        • jsdoc/ - JS documentation generator
        • +
        +
      • +
      • tutorials/ - source code for these tutorials
      • + +
        + +
      • app.js - manual testing/sample application (JS)
      • +
      • index.html - manual testing/sample application (HTML)
      • +
      • index.css - manual testing/sample application (CSS)
      • +
      • load.js - library loader (for testing/bootstrapping)
      • + +
        + +
      • jsdoc.conf.json - configuration for generating documentation
      • +
      • shaka-player.compiled.js - compiler output (suitable for deployment)
      • +
      • shaka-player.compiled.debug.js - compiler output (with debugging enabled + via source map)
      • +
      • shaka-player.compiled.debug.map - compiler output + ({@link http://goo.gl/5xQEy source map})
      • +
      • shaka-player.uncompiled.js - requires all exported classes in uncompiled + mode
      • +
      +

      + diff --git a/tutorials/dev.json b/tutorials/dev.json new file mode 100644 index 0000000000..f0e86da1f8 --- /dev/null +++ b/tutorials/dev.json @@ -0,0 +1,3 @@ +{ + "title": "Shaka Player Development" +} diff --git a/tutorials/intro.html b/tutorials/intro.html new file mode 100644 index 0000000000..f58054b56d --- /dev/null +++ b/tutorials/intro.html @@ -0,0 +1,107 @@ + + +

      +DASH Streams at a High Level +

      + +

      +Dynamic Adaptive Streaming over HTTP (DASH) is a technique for adaptive bitrate +streaming of content over the Internet delivered from conventional web servers. +This is a quick overview of DASH for those who are not familiar with it. For a +more detailed view of DASH, visit {@link http://goo.gl/Hle6dc dashif.org}. +

      + +

      +Every DASH stream is described by a manifest written in XML. Within a manifest +file, representation elements describe the DASH stream's audio, video, and text +sources. There may be more than one representation of the same content. These +typically differ in terms of bitrate and resolution, although there can be some +other variations as well. +

      + +

      +Representations are grouped into one or more adaptation sets, and a DASH client +can switch automatically between representations within a set. Adaptation sets +can differ in codec and language. DASH streams usually have audio and video in +separate adaptation sets. Although the DASH specification allows for audio and +video to be multiplexed, the Shaka Player does not support this type of stream. +

      + + +

      +Multiple Resolutions and Bitrates +

      + +

      +For the Shaka Player to adapt and choose the best representation for the user's +available bandwidth, you must encode your content into more than one resolution +and bitrate. It is up to you what bitrates and resolutions make sense for your +site. Note that you can encode at multiple bitrates for a single resolution. +

      + +

      +Example video encoding scheme: +

        +
      • 1920x1080, 5.0 Mbit/s
      • +
      • 1920x1080, 3.0 Mbit/s
      • +
      • 1280x720, 2.0 Mbit/s
      • +
      • 1280x720, 1.0 Mbit/s
      • +
      • 854x480, 750 kbit/s
      • +
      • 640x360, 350 kbit/s
      • +
      +

      + +

      +There are many open-source tools to transcode video, including: + +

      +

      + + +

      +Packaging Content +

      + +

      +A DASH packager is required to get your media ready to stream. There are a few +open-source packagers available for MP4 / ISO BMFF streams, including: + +

      +

      + +

      +Your DASH packager will convert your content to a valid, segmented ISOBMFF file +and generate a DASH manifest (MPD file). Most DASH packagers can not transcode +your media into multiple resolutions and bitrates. (DASHEncoder does both.) +

      + +

      +To package WebM streams for DASH, the WebM project provides tools for this: + +

      +

      + diff --git a/tutorials/intro.json b/tutorials/intro.json new file mode 100644 index 0000000000..3b63cdf3ea --- /dev/null +++ b/tutorials/intro.json @@ -0,0 +1,3 @@ +{ + "title": "Intro to DASH Streams" +} diff --git a/tutorials/language.html b/tutorials/language.html new file mode 100644 index 0000000000..1d74a2d2e7 --- /dev/null +++ b/tutorials/language.html @@ -0,0 +1,103 @@ + + +

      +Language Tags +

      + +

      +Languages are represented by {@link http://goo.gl/Lq37Vx ISO 639} language tags +such as "en", "fra-CA", "deu-AT", etc. Both 2-letter and 3-letter ISO tags are +supported, as are sublanguages. +

      + +

      +Some languages have both 2-letter and 3-letter tags defined, and some languages +even have more than one 3-letter tag defined. The DASH spec says that language +tags in an MPD must always be given in their shortest form. This follows {@link +http://goo.gl/9i0UUg BCP 47}. However, MP4 track metadata always uses 3-letter +tags, and some MPD creation tools use what is in the MP4 track directly. +

      + +

      +The Shaka Player will normalize all language tags (from MPDs or in the language +preference setting) to their shortest form when they are ingested. Any 3-letter +tag with an equivalent 2-letter tag will be mapped to 2 letters. Languages with +multiple 3-letter tags are also handled through this mapping. For example, you +could use either of Greek's 3-letter tags ("ell" and "gre"), and both would map +to the 2-letter tag "el". +

      + + +

      +Language APIs +

      + +

      +The Player library provides APIs to list and select audio and text tracks. The +audio and text track objects include a language property. This allows a user to +manually change audio or subtitle languages. There is also an API to enable or +disable the selected subtitles. See: + +

        +
      • {@link shaka.player.Player#getAudioTracks}
      • +
      • {@link shaka.player.Player#getTextTracks}
      • +
      • {@link shaka.player.Player#selectAudioTrack}
      • +
      • {@link shaka.player.Player#selectTextTrack}
      • +
      • {@link shaka.player.Player#enableTextTrack}
      • +
      + + +Please note that {@link shaka.player.Player#selectVideoTrack} is also provided, +but can interfere with automatic bitrate adaptation. If you allow your users to +choose video tracks manually, call {@link shaka.player.Player#enableAdaptation} +(false) to disable automatic video adaptation when a manual choice is made. + +

      + +

      +To avoid the necessity of choosing audio and text tracks manually, Shaka Player +provides {@link shaka.player.Player#setPreferredLanguage an API to set a user's +preferred language}. The language preference defaults to "en" (English) if not +set. +

      + + +

      +Automatic Stream Selection Using Language Preference +

      + +

      +When starting a video, the Player will choose streams which best fit the user's +language preference. This uses a fuzzy matching algorithm for languages. +

      + +

      +An exact language match is preferred ("en-US" ≡ "en-US"), followed by the +base language of the user's sublanguage ("en" ⊃ "en-US"), followed by other +sublanguages with the same base language ("en-GB" ≈ "en-US"). If none of +these fuzzy matches work, the first available language from the MPD is used. +

      + +

      +The matching algorithm can be found in {@link shaka.util.LanguageUtils.match}. +

      + +

      +The same matching algorithm is used to select both audio and text tracks. When +no audio track matches the user preference, text tracks are enabled by default. +

      + diff --git a/tutorials/language.json b/tutorials/language.json new file mode 100644 index 0000000000..a385605b23 --- /dev/null +++ b/tutorials/language.json @@ -0,0 +1,3 @@ +{ + "title": "Language Support" +} diff --git a/tutorials/player.html b/tutorials/player.html new file mode 100644 index 0000000000..434e9e7b1b --- /dev/null +++ b/tutorials/player.html @@ -0,0 +1,961 @@ + + +

      +Prerequisites +

      + +

      +If you have not familiarized yourself with the build process, please start with +{@tutorial dev}. +

      + +

      +If you are not already familiar with how DASH and DASH content work, please see +the {@tutorial intro} tutorial. We will not cover the encoding of DASH videos, +but {@tutorial intro} links to external tools which could be used to encode and +package content. +

      + + +

      +Streaming Content +

      + +

      +If you want to start streaming with HTML5 and DASH, the first thing you need is +source material. For this tutorial, we'll be using DASH-packaged turtle videos +to create the world's next big streaming site: TurtleTube. The DASH videos for +this tutorial have already been uploaded to appspot, so the URLs used are real. +

      + + +

      +The Shaka Player +

      + +

      +This library relies on polyfills for certain browser functionality. If you are +not familiar with the concept, please browse the {@tutorial polyfills} tutorial +before continuing this one. +

      + +

      +The Shaka Player is composed of two parts: the player and the video source. To +start, you instantiate a player object, then create and load a video source. A +video source can be anything that implements {@link shaka.player.IVideoSource}. +The library provides {@link shaka.player.DashVideoSource} for DASH content. We +will be using this in the tutorial. +

      + +

      +First, create a page with a {@link http://goo.gl/NwDFQ video tag}. Install the +polyfills before you do anything else with the library. Then, instantiate your +{@link shaka.player.Player} object to manage the video element. For simplicty, +we will use the video tag's built-in controls in this example. +

      + +

      +Next, construct a {@link shaka.player.DashVideoSource} object. This object will +manage DASH streaming and adaptation. To pass the video source into the player, +call {@link shaka.player.Player#load player.load}. +

      + +

      +Here is a simple page which demonstrates basic DASH playback: +

      + +
      <!DOCTYPE html>
      +<html>
      +  <head>
      +    <meta charset="utf-8">
      +    <title>TurtleTube - Basic Test</title>
      +    <!-- Load the Shaka Player library. -->
      +    <script src="shaka-player.compiled.js"></script>
      +  </head>
      +  <body>
      +    <video id="video"
      +           width="640" height="480"
      +           crossorigin="anonymous"
      +           controls>
      +      Your browser does not support HTML5 video.
      +    </video>
      +  </body>
      +  <script>
      +    function initPlayer() {
      +      // Install polyfills.
      +      shaka.polyfill.Fullscreen.install();
      +      shaka.polyfill.MediaKeys.install();
      +      shaka.polyfill.VideoPlaybackQuality.install();
      +
      +      // Find the video element.
      +      var video = document.getElementById('video');
      +
      +      // Construct a Player to wrap around it.
      +      var player = new shaka.player.Player(video);
      +
      +      // Attach the player to the window so that it can be easily debugged.
      +      window.player = player;
      +
      +      // Listen for errors from the Player.
      +      player.addEventListener('error', function(event) {
      +        console.error(event);
      +      });
      +
      +      // Construct a DashVideoSource to represent the DASH manifest.
      +      var mpdUrl = 'http://turtle-tube.appspot.com/t/t2/dash.mpd';
      +      var source = new shaka.player.DashVideoSource(mpdUrl, null);
      +
      +      // Load the source into the Player.
      +      player.load(source);
      +    }
      +    document.addEventListener('DOMContentLoaded', initPlayer);
      +  </script>
      +</html>
      +
      + + +

      +Autoplay vs Load +

      + +

      +There are two ways to start a video playing right away. The simplest is to use +the {@link http://goo.gl/McU4Rh autoplay attribute} on the video tag. +

      + +
      <!DOCTYPE html>
      +<html>
      +  <head>
      +    <meta charset="utf-8">
      +    <title>TurtleTube - Autoplay</title>
      +    <!-- Load the Shaka Player library. -->
      +    <script src="shaka-player.compiled.js"></script>
      +  </head>
      +  <body>
      +    <video id="video"
      +           width="640" height="480"
      +           crossorigin="anonymous"
      +           controls
      +           autoplay><!-- Start playing right away on load. -->
      +      Your browser does not support HTML5 video.
      +    </video>
      +  </body>
      +  <script>
      +    function initPlayer() {
      +      // Install polyfills.
      +      shaka.polyfill.Fullscreen.install();
      +      shaka.polyfill.MediaKeys.install();
      +      shaka.polyfill.VideoPlaybackQuality.install();
      +
      +      // Find the video element.
      +      var video = document.getElementById('video');
      +
      +      // Construct a Player to wrap around it.
      +      var player = new shaka.player.Player(video);
      +
      +      // Attach the player to the window so that it can be easily debugged.
      +      window.player = player;
      +
      +      // Listen for errors from the Player.
      +      player.addEventListener('error', function(event) {
      +        console.error(event);
      +      });
      +
      +      // Construct a DashVideoSource to represent the DASH manifest.
      +      var mpdUrl = 'http://turtle-tube.appspot.com/t/t2/dash.mpd';
      +      var source = new shaka.player.DashVideoSource(mpdUrl, null);
      +
      +      // Load the source into the Player.
      +      player.load(source);
      +    }
      +    document.addEventListener('DOMContentLoaded', initPlayer);
      +  </script>
      +</html>
      +
      + +

      +If you want to do more than start the video playing, you can execute a function +asynchronously after {@link shaka.player.Player#load player.load} completes. It +returns a {@link http://goo.gl/8NHkC1 Promise} which is resolved once the video +is loaded. You can execute any arbitrary code when load completes. +

      + +
      <!DOCTYPE html>
      +<html>
      +  <head>
      +    <meta charset="utf-8">
      +    <title>TurtleTube - Async Load</title>
      +    <!-- Load the Shaka Player library. -->
      +    <script src="shaka-player.compiled.js"></script>
      +  </head>
      +  <body>
      +    <ul id="videoTracks"></ul>
      +    <video id="video"
      +           width="640" height="480"
      +           crossorigin="anonymous"
      +           controls><!-- No autoplay attribute. -->
      +      Your browser does not support HTML5 video.
      +    </video>
      +  </body>
      +  <script>
      +    function initPlayer() {
      +      // Install polyfills.
      +      shaka.polyfill.Fullscreen.install();
      +      shaka.polyfill.MediaKeys.install();
      +      shaka.polyfill.VideoPlaybackQuality.install();
      +
      +      // Find the video element.
      +      var video = document.getElementById('video');
      +
      +      // Construct a Player to wrap around it.
      +      var player = new shaka.player.Player(video);
      +
      +      // Attach the player to the window so that it can be easily debugged.
      +      window.player = player;
      +
      +      // Listen for errors from the Player.
      +      player.addEventListener('error', function(event) {
      +        console.error(event);
      +      });
      +
      +      // Construct a DashVideoSource to represent the DASH manifest.
      +      var mpdUrl = 'http://turtle-tube.appspot.com/t/t2/dash.mpd';
      +      var source = new shaka.player.DashVideoSource(mpdUrl, null);
      +
      +      // Load the source into the Player.
      +      // Then query the video tracks to display in the videoTracks list element.
      +      // Resize the video element to match the aspect ratio of the active track.
      +      // Finally, begin playback.
      +      player.load(source).then(function() {
      +        var videoTracks = player.getVideoTracks();
      +        var activeTrack;
      +
      +        // Add track info to the DOM.
      +        var ul = document.getElementById('videoTracks');
      +        for (var i = 0; i < videoTracks.length; ++i) {
      +          var track = videoTracks[i];
      +          if (track.active) activeTrack = track;
      +
      +          var text = track.width + ' x ' + track.height;
      +          text += ' ' + (track.bandwidth / 1024).toFixed(0) + ' kbits/s';
      +
      +          var li = document.createElement('li');
      +          li.innerText = text;
      +          ul.appendChild(li);
      +        }
      +
      +        // Correct aspect ratio.
      +        if (activeTrack) {
      +          var aspectRatio = activeTrack.width / activeTrack.height;
      +          video.width = video.height * aspectRatio;
      +        } else {
      +          console.error('Unable to query aspect ratio!');
      +        }
      +
      +        // Begin playback, since autoplay is not enabled on the video tag.
      +        player.play();
      +      });
      +    }
      +    document.addEventListener('DOMContentLoaded', initPlayer);
      +  </script>
      +</html>
      +
      + +

      +Autoplay on the video tag does have one distinct advantage. If autoplay is set, +{@link shaka.player.Stats#playbackLatency playbackLatency stats} are collected. +If this information is important to you, please use autoplay. +

      + +

      +(The use of the autoplay attribute and the execution of asynchronous code after +load are not mutually exclusive.) +

      + + +

      +Protected Content +

      + +

      +Some of our TurtleTube content is worth a lot of money, so we are going to make +use of Widevine to protect it. +

      + +

      +The Shaka Player uses the {@link http://goo.gl/o9Guuu EME} APIs to get licenses +and decrypt protected content. However, many details needed to do this are not +part of the DASH spec. Although there is a ContentProtection element specified, +its contents and interpretation are application-specific. +

      + +

      +To bridge the gap between DASH and EME with application-specific details, Shaka +uses a callback to the application to interpret ContentProtection elements from +the DASH manifest. The application receives a parsed element from the manifest +in the form of a {@link shaka.dash.mpd.ContentProtection} object. The app then +returns a {@link shaka.player.DrmSchemeInfo} object which has important details +in a consistent structure. +

      + +

      +Here is how {@link shaka.dash.DashVideoSource#ContentProtectionCallback} can be +used to enable encrypted content: +

      + +
      <!DOCTYPE html>
      +<html>
      +  <head>
      +    <meta charset="utf-8">
      +    <title>TurtleTube - Encrypted Content</title>
      +    <!-- Load the Shaka Player library. -->
      +    <script src="shaka-player.compiled.js"></script>
      +  </head>
      +  <body>
      +    <video id="video"
      +           width="640" height="480"
      +           crossorigin="anonymous"
      +           controls autoplay>
      +      Your browser does not support HTML5 video.
      +    </video>
      +  </body>
      +  <script>
      +    function initPlayer() {
      +      // Install polyfills.
      +      shaka.polyfill.Fullscreen.install();
      +      shaka.polyfill.MediaKeys.install();
      +      shaka.polyfill.VideoPlaybackQuality.install();
      +
      +      // Find the video element.
      +      var video = document.getElementById('video');
      +
      +      // Construct a Player to wrap around it.
      +      var player = new shaka.player.Player(video);
      +
      +      // Attach the player to the window so that it can be easily debugged.
      +      window.player = player;
      +
      +      // Listen for errors from the Player.
      +      player.addEventListener('error', function(event) {
      +        console.error(event);
      +      });
      +
      +      // Construct a DashVideoSource to represent the DASH manifest and provide
      +      // a callback to interpret the ContentProtection elements.
      +      var mpdUrl = 'http://turtle-tube.appspot.com/t/e6/dash.mpd';
      +      var source = new shaka.player.DashVideoSource(mpdUrl,
      +                                                    interpretContentProtection);
      +
      +      // Load the source into the Player.
      +      player.load(source);
      +    }
      +
      +    /**
      +     * @param {!shaka.dash.mpd.ContentProtection} contentProtection The
      +     *     ContentProtection element from the MPD.
      +     * @return {shaka.player.DrmSchemeInfo} or null if the element is not
      +     *     understood by this application.
      +     */
      +    function interpretContentProtection(contentProtection) {
      +      // This is the UUID which is used by edash-packager to represent
      +      // Widevine.  This is the only scheme we are expecting for this
      +      // application.
      +      if (contentProtection.schemeIdUri ==
      +          'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed') {
      +        // We will use Widevine's testing license server.  In a real app,
      +        // you would run your own front-end service for this.
      +        var licenseServerUrl = 'http://widevine-proxy.appspot.com/proxy';
      +
      +        // The EME key system identifier for Widevine.
      +        var keySystem = 'com.widevine.alpha';
      +
      +        return new shaka.player.DrmSchemeInfo(keySystem,
      +                                              true /* suppressMultipleEvents */,
      +                                              licenseServerUrl,
      +                                              false /* withCredentials */,
      +                                              null /* initData */,
      +                                              null /* licensePostProcessor */);
      +      }
      +
      +      console.warn('Unrecognized scheme: ' + contentProtection.schemeIdUri);
      +      return null;
      +    }
      +
      +    document.addEventListener('DOMContentLoaded', initPlayer);
      +  </script>
      +</html>
      +
      + +

      +This example uses Widevine, but the Shaka Player library queries the browser to +find out what key systems are supported. So you should be able to protect your +content with multiple key systems for different browsers. The only requirement +is that the browser has a standards compliant implementation of EME. If that's +the case, you can put multiple ContentProtection tags in your MPD, and once the +tags are resolved as DrmSchemeInfo objects, the player will discard those which +cannot be used by the browser. This way, there's no need to detect the browser +vendor and serve different content based on that. +

      + + +

      +A Full Site +

      + +

      +This is a sample demonstrating a complete TurtleTube site. Just click on one of +the video thumbnails, and the appropriate video source will be constructed. The +video will play back in an overlay on top of the thumbnails. +

      + +
      <!DOCTYPE html>
      +<html>
      +  <head>
      +    <meta charset="utf-8">
      +    <title>TurtleTube - Beta!</title>
      +    <!-- Load the Shaka Player library. -->
      +    <script src="shaka-player.compiled.js"></script>
      +    <style>
      +      body {
      +        background-color: #4a8;
      +        color: #000;
      +      }
      +      h1, h2 {
      +        text-align: center;
      +      }
      +      #thumbContainer {
      +        display: table;
      +        margin: auto;
      +      }
      +      .thumbRow {
      +        display: table-row;
      +      }
      +      .thumbCell {
      +        display: table-cell;
      +        width: 270px;
      +        padding: 10px;
      +      }
      +      .thumbCell img {
      +        width: 270px;
      +        height: 180px;
      +        border: 5px ridge #07a;
      +        margin: 0;
      +      }
      +      #videoOverlay {
      +        background-color: rgba(0, 0, 0, 0.5);
      +        position: fixed;
      +        top: 2px;
      +        left: 2px;
      +        right: 2px;
      +        bottom: 2px;
      +        z-index: 1;
      +        overflow: hidden;
      +        text-align: center;
      +        /* Hidden until needed. */
      +        display: none;
      +      }
      +      #closeButton {
      +        position: relative;
      +        margin-top: 10px;
      +        z-index: 2;
      +      }
      +      #vcenterWrapper {
      +        position: absolute;
      +        width: 0;
      +        height: 0;
      +        /* Move the top-left corner of this div to the center. */
      +        top: 50%;
      +        left: 50%;
      +      }
      +      #video {
      +        width: 640px;
      +        height: 480px;
      +        position: relative;
      +        /* Center the video inside the overlay. */
      +        top: -240px;
      +        left: -320px;
      +      }
      +    </style>
      +  </head>
      +  <body>
      +    <h1>TurtleTube!</h1>
      +    <h2>Choose a video:</h2>
      +
      +    <div id="thumbContainer">
      +      <div class="thumbRow">
      +        <div class="thumbCell">
      +          <img id="t1"
      +               src="http://turtle-tube.appspot.com/t/t1/thumb.png"
      +               onclick="onImageClick(this)"><br>
      +          <i>cute green sea turtle in Ko'olina Hawai'i</i><br>
      +          (MP4, WebM)
      +        </div>
      +        <div class="thumbCell">
      +          <img id="t2"
      +               src="http://turtle-tube.appspot.com/t/t2/thumb.png"
      +               onclick="onImageClick(this)"><br>
      +          <i>Endangered Ocean: Sea Turtles</i><br>
      +          (MP4, WebM)
      +        </div>
      +      </div>
      +      <div class="thumbRow">
      +        <div class="thumbCell">
      +          <img id="t3"
      +               src="http://turtle-tube.appspot.com/t/t3/thumb.png"
      +               onclick="onImageClick(this)"><br>
      +          <i>sea turtles exercise: bent arms</i><br>
      +          (WebM only)
      +        </div>
      +        <div class="thumbCell">
      +          <img id="t4"
      +               src="http://turtle-tube.appspot.com/t/t4/thumb.png"
      +               onclick="onImageClick(this)"><br>
      +          <i>sea turtles exercise: straight arms</i><br>
      +          (WebM only)
      +        </div>
      +      </div>
      +      <div class="thumbRow">
      +        <div class="thumbCell">
      +          <img id="t5"
      +               src="http://turtle-tube.appspot.com/t/t5/thumb.png"
      +               onclick="onImageClick(this)"><br>
      +          <i>Using robots to reveal secrets of walking baby sea turtles</i><br>
      +          (MP4, WebM)
      +        </div>
      +        <div class="thumbCell">
      +          <img id="e6"
      +               src="http://turtle-tube.appspot.com/t/e6/thumb.png"
      +               onclick="onImageClick(this)"><br>
      +          <i>kitten vs sea turtle</i><br>
      +          (MP4 only, encrypted)
      +        </div>
      +      </div>
      +    </div>
      +    <div id="videoOverlay">
      +      <div id="vcenterWrapper">
      +        <video id="video"
      +               poster="http://turtle-tube.appspot.com/poster.jpg"
      +               crossorigin="anonymous"
      +               controls autoplay>
      +          Your browser does not support HTML5 video.
      +        </video>
      +      </div>
      +      <button id="closeButton" onclick="closeVideo()">Close Video</button>
      +    </div>
      +  </body>
      +  <script>
      +    var video;
      +    var player;
      +
      +    function initPlayer() {
      +      // Install polyfills.
      +      shaka.polyfill.Fullscreen.install();
      +      shaka.polyfill.MediaKeys.install();
      +      shaka.polyfill.VideoPlaybackQuality.install();
      +
      +      // Get the video element.
      +      video = document.getElementById('video');
      +
      +      // Construct the Player to wrap around it.
      +      player = new shaka.player.Player(video);
      +
      +      // Attach the player to the window so that it can be easily debugged.
      +      window.player = player;
      +
      +      // Listen for errors from the Player.
      +      player.addEventListener('error', function(event) {
      +        console.error(event);
      +      });
      +    }
      +
      +    /**
      +     * @param {!HTMLImageElement} image
      +     */
      +    function onImageClick(image) {
      +      // Construct a DashVideoSource to represent the DASH manifest and provide
      +      // a callback to interpret the ContentProtection elements (if any).
      +      var mpdUrl = 'http://turtle-tube.appspot.com/t/' + image.id + '/dash.mpd';
      +      var source = new shaka.player.DashVideoSource(mpdUrl,
      +                                                    interpretContentProtection);
      +
      +      // Show the video player overlay.
      +      var overlay = document.getElementById('videoOverlay');
      +      overlay.style.display = 'block';
      +
      +      // Load the source into the Player.
      +      player.load(source);
      +    }
      +
      +    /**
      +     * @param {!shaka.dash.mpd.ContentProtection} contentProtection The
      +     *     ContentProtection element from the MPD.
      +     * @return {shaka.player.DrmSchemeInfo} or null if the element is not
      +     *     understood by this application.
      +     */
      +    function interpretContentProtection(contentProtection) {
      +      // This is the UUID which is used by edash-packager to represent
      +      // Widevine.  This is the only scheme we are expecting for this
      +      // application.
      +      if (contentProtection.schemeIdUri ==
      +          'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed') {
      +        // We will use Widevine's testing license server.  In a real app,
      +        // you would run your own front-end service for this.
      +        var licenseServerUrl = 'http://widevine-proxy.appspot.com/proxy';
      +
      +        // The EME key system identifier for Widevine.
      +        var keySystem = 'com.widevine.alpha';
      +
      +        return new shaka.player.DrmSchemeInfo(keySystem,
      +                                              true /* suppressMultipleEvents */,
      +                                              licenseServerUrl,
      +                                              false /* withCredentials */,
      +                                              null /* initData */,
      +                                              null /* licensePostProcessor */);
      +      }
      +
      +      console.warn('Unrecognized scheme: ' + contentProtection.schemeIdUri);
      +      return null;
      +    }
      +
      +    function closeVideo() {
      +      // Unload the video source.
      +      player.unload();
      +
      +      // Hide the video player overlay.
      +      var overlay = document.getElementById('videoOverlay');
      +      overlay.style.display = 'none';
      +    }
      +
      +    document.addEventListener('DOMContentLoaded', initPlayer);
      +  </script>
      +</html>
      +
      + + +

      +Bonus: Event Handling +

      + +

      +There a few events which the Player will dispatch. This sample demonstrates how +you can listen for the adaptation event and use to it indicate when an HD video +is being played. It also shows an error dialog if/when error events occur. +

      + +
      <!DOCTYPE html>
      +<html>
      +  <head>
      +    <meta charset="utf-8">
      +    <title>TurtleTube - Beta (HD)!</title>
      +    <!-- Load the Shaka Player library. -->
      +    <script src="shaka-player.compiled.js"></script>
      +    <style>
      +      body {
      +        background-color: #4a8;
      +        color: #000;
      +      }
      +      h1, h2 {
      +        text-align: center;
      +      }
      +      #thumbContainer {
      +        display: table;
      +        margin: auto;
      +      }
      +      .thumbRow {
      +        display: table-row;
      +      }
      +      .thumbCell {
      +        display: table-cell;
      +        width: 270px;
      +        padding: 10px;
      +      }
      +      .thumbCell img {
      +        width: 270px;
      +        height: 180px;
      +        border: 5px ridge #07a;
      +        margin: 0;
      +      }
      +      #videoOverlay {
      +        background-color: rgba(0, 0, 0, 0.5);
      +        position: fixed;
      +        top: 2px;
      +        left: 2px;
      +        right: 2px;
      +        bottom: 2px;
      +        z-index: 1;
      +        overflow: hidden;
      +        text-align: center;
      +        /* Hidden until needed. */
      +        display: none;
      +      }
      +      #closeButton {
      +        position: relative;
      +        margin-top: 10px;
      +        z-index: 2;
      +      }
      +      #vcenterWrapper {
      +        position: absolute;
      +        width: 0;
      +        height: 0;
      +        /* Move the top-left corner of this div to the center. */
      +        top: 50%;
      +        left: 50%;
      +      }
      +      #video {
      +        width: 640px;
      +        height: 426px;
      +        position: relative;
      +        /* Center the video inside the overlay. */
      +        left: -320px;
      +        top: -213px;
      +      }
      +      #errorOverlay {
      +        border: 2px solid black;
      +        background-color: rgba(100, 0, 0, 0.5);
      +        font-size: 175%;
      +        white-space: pre-line;
      +        width: 350px;
      +        height: 200px;
      +        position: absolute;
      +        /* Center the error inside the video overlay. */
      +        left: -175px;
      +        top: -100px;
      +        /* Hidden until needed. */
      +        display: none;
      +      }
      +      #hd {
      +        position: absolute;
      +        opacity: 0.6;
      +        /* Hidden until needed. */
      +        display: none;
      +      }
      +    </style>
      +  </head>
      +  <body>
      +    <h1>TurtleTube!</h1>
      +    <h2>Choose a video:</h2>
      +
      +    <div id="thumbContainer">
      +      <div class="thumbRow">
      +        <div class="thumbCell">
      +          <img id="t1"
      +               src="http://turtle-tube.appspot.com/t/t1/thumb.png"
      +               onclick="onImageClick(this)"><br>
      +          <i>cute green sea turtle in Ko'olina Hawai'i</i><br>
      +          (MP4, WebM)
      +        </div>
      +        <div class="thumbCell">
      +          <img id="t2"
      +               src="http://turtle-tube.appspot.com/t/t2/thumb.png"
      +               onclick="onImageClick(this)"><br>
      +          <i>Endangered Ocean: Sea Turtles</i><br>
      +          (MP4, WebM)
      +        </div>
      +      </div>
      +      <div class="thumbRow">
      +        <div class="thumbCell">
      +          <img id="t3"
      +               src="http://turtle-tube.appspot.com/t/t3/thumb.png"
      +               onclick="onImageClick(this)"><br>
      +          <i>sea turtles exercise: bent arms</i><br>
      +          (WebM only)
      +        </div>
      +        <div class="thumbCell">
      +          <img id="t4"
      +               src="http://turtle-tube.appspot.com/t/t4/thumb.png"
      +               onclick="onImageClick(this)"><br>
      +          <i>sea turtles exercise: straight arms</i><br>
      +          (WebM only)
      +        </div>
      +      </div>
      +      <div class="thumbRow">
      +        <div class="thumbCell">
      +          <img id="t5"
      +               src="http://turtle-tube.appspot.com/t/t5/thumb.png"
      +               onclick="onImageClick(this)"><br>
      +          <i>Using robots to reveal secrets of walking baby sea turtles</i><br>
      +          (MP4, WebM)
      +        </div>
      +        <div class="thumbCell">
      +          <img id="e6"
      +               src="http://turtle-tube.appspot.com/t/e6/thumb.png"
      +               onclick="onImageClick(this)"><br>
      +          <i>kitten vs sea turtle</i><br>
      +          (MP4 only, encrypted)
      +        </div>
      +      </div>
      +    </div>
      +    <div id="videoOverlay">
      +      <div id="vcenterWrapper">
      +        <video id="video"
      +               poster="http://turtle-tube.appspot.com/poster.jpg"
      +               crossorigin="anonymous"
      +               controls autoplay>
      +          Your browser does not support HTML5 video.
      +        </video>
      +        <img id="hd" src="http://turtle-tube.appspot.com/hd.png">
      +        <div id="errorOverlay"></div>
      +      </div>
      +      <button id="closeButton" onclick="closeVideo()">Close Video</button>
      +    </div>
      +  </body>
      +  <script>
      +    var video;
      +    var hd;
      +    var player;
      +
      +    function initPlayer() {
      +      // Install polyfills.
      +      shaka.polyfill.Fullscreen.install();
      +      shaka.polyfill.MediaKeys.install();
      +      shaka.polyfill.VideoPlaybackQuality.install();
      +
      +      // Get important elements.
      +      video = document.getElementById('video');
      +      hd = document.getElementById('hd');
      +
      +      // Construct the Player to wrap around it.
      +      player = new shaka.player.Player(video);
      +
      +      // Attach the player to the window so that it can be easily debugged.
      +      window.player = player;
      +
      +      // Listen for adaptation events.
      +      player.addEventListener('adaptation', onAdaptation);
      +
      +      // Listen for errors from the Player.
      +      player.addEventListener('error', onError);
      +    }
      +
      +    /**
      +     * @param {!Event} event
      +     */
      +    function onAdaptation(event) {
      +      // Ignore non-video adaptation events.
      +      if (event.contentType != 'video') {
      +        return;
      +      }
      +
      +      // Resize the video element to match the content's aspect ratio.
      +      var aspect = event.size.width / event.size.height;
      +      var w = video.offsetWidth;
      +      var h = w / aspect;
      +      video.style.width = w + 'px';
      +      video.style.height = h + 'px';
      +      video.style.left = (-w / 2) + 'px';
      +      video.style.top = (-h / 2) + 'px';
      +
      +      // Position the HD icon in the top-right of the video element.
      +      // 0,0 for this icon is the center of the video element.
      +      hd.style.top = ((-h / 2) + 5) + 'px';
      +      hd.style.right = ((-w / 2) + 5) + 'px';
      +
      +      // If the video is 720p or above, show the HD icon.
      +      if (event.size.height >= 720) {
      +        hd.style.display = 'block';
      +      } else {
      +        hd.style.display = 'none';
      +      }
      +    }
      +
      +    /**
      +     * @param {!Event} event
      +     */
      +    function onError(event) {
      +      var overlay = document.getElementById('errorOverlay');
      +      // This contains details about the error.
      +      var error = event.detail;
      +
      +      // Format a message to show to the user in the overlay.
      +      var text = 'Error (' + error.type + '):\n';
      +      text += error.message;
      +
      +      // Display it.
      +      overlay.textContent = text;
      +      overlay.style.display = 'block';
      +
      +      // It would also be a good idea to log an anonymized version of the error
      +      // object to the server.
      +    }
      +
      +    /**
      +     * @param {!HTMLImageElement} image
      +     */
      +    function onImageClick(image) {
      +      // Construct a DashVideoSource to represent the DASH manifest and provide
      +      // a callback to interpret the ContentProtection elements (if any).
      +      var mpdUrl = 'http://turtle-tube.appspot.com/t/' + image.id + '/dash.mpd';
      +      var source = new shaka.player.DashVideoSource(mpdUrl,
      +                                                    interpretContentProtection);
      +
      +      // Show the video player overlay.
      +      var overlay = document.getElementById('videoOverlay');
      +      overlay.style.display = 'block';
      +
      +      // Load the source into the Player.
      +      player.load(source);
      +    }
      +
      +    /**
      +     * @param {!shaka.dash.mpd.ContentProtection} contentProtection The
      +     *     ContentProtection element from the MPD.
      +     * @return {shaka.player.DrmSchemeInfo} or null if the element is not
      +     *     understood by this application.
      +     */
      +    function interpretContentProtection(contentProtection) {
      +      // This is the UUID which is used by edash-packager to represent
      +      // Widevine.  This is the only scheme we are expecting for this
      +      // application.
      +      if (contentProtection.schemeIdUri ==
      +          'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed') {
      +        // We will use Widevine's testing license server.  In a real app,
      +        // you would run your own front-end service for this.
      +        var licenseServerUrl = 'http://widevine-proxy.appspot.com/proxy';
      +
      +        // The EME key system identifier for Widevine.
      +        var keySystem = 'com.widevine.alpha';
      +
      +        return new shaka.player.DrmSchemeInfo(keySystem,
      +                                              true /* suppressMultipleEvents */,
      +                                              licenseServerUrl,
      +                                              false /* withCredentials */,
      +                                              null /* initData */,
      +                                              null /* licensePostProcessor */);
      +      }
      +
      +      console.warn('Unrecognized scheme: ' + contentProtection.schemeIdUri);
      +      return null;
      +    }
      +
      +    function closeVideo() {
      +      // Unload the video source.
      +      player.unload();
      +
      +      // Hide the video player overlay.
      +      var overlay = document.getElementById('videoOverlay');
      +      overlay.style.display = 'none';
      +
      +      // Hide the error overlay.
      +      overlay = document.getElementById('errorOverlay');
      +      overlay.style.display = 'none';
      +    }
      +
      +    document.addEventListener('DOMContentLoaded', initPlayer);
      +  </script>
      +</html>
      +
      + diff --git a/tutorials/player.json b/tutorials/player.json new file mode 100644 index 0000000000..ae67ec386a --- /dev/null +++ b/tutorials/player.json @@ -0,0 +1,3 @@ +{ + "title": "DASH Playback with the Shaka Player" +} diff --git a/tutorials/polyfills.html b/tutorials/polyfills.html new file mode 100644 index 0000000000..34fcd8f3d7 --- /dev/null +++ b/tutorials/polyfills.html @@ -0,0 +1,52 @@ + + +

      +Polyfills +

      + +

      +A {@link https://remysharp.com/2010/10/08/what-is-a-polyfill polyfill} is a bit +of code that fills in missing APIs or normalizes variations in APIs. A polyfill +allows other pieces of code to expect a single API from the underlying browser. +

      + +

      +The Shaka Player uses polyfills to modify, rename, or replace various pieces of +browser functionality. Before instantiating the Player, these polyfills should +be installed into the JavaScript environment. +

      + +

      +Each polyfill has its own class and a static install() method. We suggest that +you install all polyfills, since the install method will intelligently detect a +browser's capabilities and only patch APIs which need patching. +

      + +

      +Once browsers commonly implement the necessary functionality consistently, some +polyfills can be removed to reduce the size of the JavaScript code. +

      + +

      +For detailed information on our polyfills, please see: +

        +
      • {@link shaka.polyfill.Fullscreen}
      • +
      • {@link shaka.polyfill.MediaKeys}
      • +
      • {@link shaka.polyfill.VideoPlaybackQuality}
      • +
      +

      + diff --git a/tutorials/polyfills.json b/tutorials/polyfills.json new file mode 100644 index 0000000000..38873e80f3 --- /dev/null +++ b/tutorials/polyfills.json @@ -0,0 +1,3 @@ +{ + "title": "Polyfills" +} diff --git a/tutorials/sample1.txt b/tutorials/sample1.txt new file mode 100644 index 0000000000..7619a7819a --- /dev/null +++ b/tutorials/sample1.txt @@ -0,0 +1,47 @@ + + + + + TurtleTube - Basic Test + + + + + + + + diff --git a/tutorials/sample2.txt b/tutorials/sample2.txt new file mode 100644 index 0000000000..d631839103 --- /dev/null +++ b/tutorials/sample2.txt @@ -0,0 +1,48 @@ + + + + + TurtleTube - Autoplay + + + + + + + + diff --git a/tutorials/sample3.txt b/tutorials/sample3.txt new file mode 100644 index 0000000000..09e00bd917 --- /dev/null +++ b/tutorials/sample3.txt @@ -0,0 +1,79 @@ + + + + + TurtleTube - Async Load + + + + +
        + + + + diff --git a/tutorials/sample4.txt b/tutorials/sample4.txt new file mode 100644 index 0000000000..fd147625fe --- /dev/null +++ b/tutorials/sample4.txt @@ -0,0 +1,81 @@ + + + + + TurtleTube - Encrypted Content + + + + + + + + diff --git a/tutorials/sample5.txt b/tutorials/sample5.txt new file mode 100644 index 0000000000..5ce252d431 --- /dev/null +++ b/tutorials/sample5.txt @@ -0,0 +1,221 @@ + + + + + TurtleTube - Beta! + + + + + +

        TurtleTube!

        +

        Choose a video:

        + +
        +
        +
        +
        + cute green sea turtle in Ko'olina Hawai'i
        + (MP4, WebM) +
        +
        +
        + Endangered Ocean: Sea Turtles
        + (MP4, WebM) +
        +
        +
        +
        +
        + sea turtles exercise: bent arms
        + (WebM only) +
        +
        +
        + sea turtles exercise: straight arms
        + (WebM only) +
        +
        +
        +
        +
        + Using robots to reveal secrets of walking baby sea turtles
        + (MP4, WebM) +
        +
        +
        + kitten vs sea turtle
        + (MP4 only, encrypted) +
        +
        +
        +
        +
        + +
        + +
        + + + diff --git a/tutorials/sample6.txt b/tutorials/sample6.txt new file mode 100644 index 0000000000..f315f581f7 --- /dev/null +++ b/tutorials/sample6.txt @@ -0,0 +1,301 @@ + + + + + TurtleTube - Beta (HD)! + + + + + +

        TurtleTube!

        +

        Choose a video:

        + +
        +
        +
        +
        + cute green sea turtle in Ko'olina Hawai'i
        + (MP4, WebM) +
        +
        +
        + Endangered Ocean: Sea Turtles
        + (MP4, WebM) +
        +
        +
        +
        +
        + sea turtles exercise: bent arms
        + (WebM only) +
        +
        +
        + sea turtles exercise: straight arms
        + (WebM only) +
        +
        +
        +
        +
        + Using robots to reveal secrets of walking baby sea turtles
        + (MP4, WebM) +
        +
        +
        + kitten vs sea turtle
        + (MP4 only, encrypted) +
        +
        +
        +
        +
        + + +
        +
        + +
        + + + diff --git a/tutorials/update_samples.py b/tutorials/update_samples.py new file mode 100755 index 0000000000..415dd2bd2d --- /dev/null +++ b/tutorials/update_samples.py @@ -0,0 +1,120 @@ +#!/usr/bin/python +# +# Copyright 2014 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Updates samples in HTML files. + +The samples are maintained in .txt files by name. These samples are escaped +and inserted into tags whose id attribute matches the sample name. + +Samples are also diffed against the previous sample. If a sample is similar +to the previous one, it is marked up to highlight the new information. + +Run this script any time you update sample text. +""" + +import difflib +import os +import re +import sys + + +def html_escape(contents): + """Escape HTML entities in the contents.""" + contents = contents.replace('&', '&') + contents = contents.replace('<', '<') + contents = contents.replace('>', '>') + return contents + + +def regex_escape(contents): + """Escape regex special characters in the contents.""" + contents = contents.replace('\\', '\\\\') + return contents + + +def mark_up_changes(old, new): + """Mark up new text with highlights to show changes since the old text.""" + old = old.split('\n') + new = new.split('\n') + d = difflib.unified_diff(old, new, n=1000000, lineterm='') + output = [] + num_highlights = 0 + headers_done = False + + for chunk in d: + # Skip headers. + if not headers_done: + if chunk[0:2] == '@@': + headers_done = True + continue + + prefix = chunk[0] + data = chunk[1:] + + # Skip things unique to the old contents. + if prefix == '-': + continue + + # Mark up things unique to the new contents. + if prefix == '+': + data = '' + data + '' + num_highlights += 1 + + output.append(data) + + change_ratio = float(num_highlights) / len(output) + if change_ratio < 0.8: + # The contents have not changed too much. Return highlighted text. + return '\n'.join(output) + + # Too much has changed. Return the new text without extra markup. + return '\n'.join(new) + + +def get_sample_names(contents): + """Extract sample names from the contents.""" + for m in re.finditer(r').*?()' % name, + r'\1%s\2' % regex_escape(marked_up_contents), contents, + flags=re.DOTALL) + + file(html_path, 'w').write(contents) + + +if __name__ == '__main__': + script_path = os.path.dirname(__file__) + print 'Searching for HTML files in %s' % script_path + # Find all html files in the same folder as this script and update them. + for path in os.listdir(script_path): + if os.path.splitext(path)[1] != '.html': + continue + update_samples(script_path, path) + sys.exit(0) +