countdown(start,end,units,max,20) with the largest possible number of `digits`, you might want to set `ts.years = Math.floor(ts.years)` before calling `ts.toString()`. The vain might want you to set `ts.years = Math.min(ts.years, 39)`!
----
### Timespan result
The return value is a Timespan object which always contains the following fields:
- `Date start`: the starting date object used for the calculation
- `Date end`: the ending date object used for the calculation
- `Number units`: the units specified
- `Number value`: total milliseconds difference (i.e., `end` - `start`). If `end < start` then `value` will be negative.
Typically the `end` occurs after `start`, but if the arguments were reversed, the only difference is `Timespan.value` will be negative. The sign of `value` can be used to determine if the event occurs in the future or in the past.
The following time unit fields are only present if their corresponding units were requested:
- `Number millennia`
- `Number centuries`
- `Number decades`
- `Number years`
- `Number months`
- `Number days`
- `Number hours`
- `Number minutes`
- `Number seconds`
- `Number milliseconds`
Finally, Timespan has two formatting methods each with some optional parameters. If the difference between `start` and `end` is less than the requested granularity of units, then `toString(...)` and `toHTML(...)` will return the empty label (defaults to an empty string).
`String toString(emptyLabel)`: formats the Timespan object as an English sentence. e.g., using the same input:
ts.toString() => "5 years, 1 month, 19 days, 12 hours and 17 minutes"
`String toHTML(tagName, emptyLabel)`: formats the Timespan object as an English sentence, with the specified HTML tag wrapped around each unit. If no tag name is provided, "`span`" is used. e.g., using the same input:
ts.toHTML() => "5 years, 1 month, 19 days, 12 hours and 17 minutes"
ts.toHTML("em") => "5 years, 1 month, 19 days, 12 hours and 17 minutes"
----
### Localization
Very basic localization is supported via the static `setLabels` and `resetLabels` methods. These change the functionality for all timespans on the page.
countdown.resetLabels();
countdown.setLabels(singular, plural, last, delim, empty, formatter);
The arguments:
- `singular` is a pipe (`'|'`) delimited ascending list of singular unit name overrides
- `plural` is a pipe (`'|'`) delimited ascending list of plural unit name overrides
- `last` is a delimiter before the last unit (default: `' and '`)
- `delim` is a delimiter to use between all other units (default: `', '`),
- `empty` is a label to use when all units are zero (default: `''`)
- `formatter` is a function which takes a `number` and returns a `string` (default uses `Number.toString()`),
allowing customization of the way numbers are formatted, e.g., commas every 3 digits or some unique style that is specific to your locale.
Notice that the spacing is part of the labels.
The following examples would translate the output into Brazilian Portuguese and French, respectively:
countdown.setLabels(
' milissegundo| segundo| minuto| hora| dia| semana| mês| ano| década| século| milênio',
' milissegundos| segundos| minutos| horas| dias| semanas| meses| anos| décadas| séculos| milênios',
' e ',
' + ',
'agora');
countdown.setLabels(
' milliseconde| seconde| minute| heure| jour| semaine| mois| année| décennie| siècle| millénaire',
' millisecondes| secondes| minutes| heures| jours| semaines| mois| années| décennies| siècles| millénaires',
' et ',
', ',
'maintenant');
If you only wanted to override some of the labels just leave the other pipe-delimited places empty. Similarly, leave off any of the delimiter arguments which do not need overriding.
countdown.setLabels(
'||| hr| d',
'ms| sec|||| wks|| yrs',
', and finally ');
ts.toString() => "1 millennium, 2 centuries, 5 yrs, 1 month, 7 wks, 19 days, 1 hr, 2 minutes, 17 sec, and finally 1 millisecond"
If you only wanted to override the empty label:
countdown.setLabels(
null,
null,
null,
null,
'Now.');
ts.toString() => "Now."
The following would be effectively the same as calling `countdown.resetLabels()`:
countdown.setLabels(
' millisecond| second| minute| hour| day| week| month| year| decade| century| millennium',
' milliseconds| seconds| minutes| hours| days| weeks| months| years| decades| centuries| millennia',
' and ',
', ',
'',
function(n){ return n.toString(); });
----
## License
Distributed under the terms of [The MIT license][2].
----
Copyright (c) 2006-2014 [Stephen M. McKamey][3]
[1]: http://countdownjs.org
[2]: https://raw.githubusercontent.com/mckamey/countdownjs/master/LICENSE.txt
[3]: http://mck.me
================================================
FILE: bower.json
================================================
{
"name": "countdownjs",
"version": "2.6.1",
"description": "A simple JavaScript API for producing an accurate, intuitive description of the timespan between two Date instances.",
"main": "countdown.js",
"keywords": [
"countdown",
"timer",
"clock",
"date",
"time",
"timespan",
"year",
"month",
"week",
"day",
"hour",
"minute",
"second"
],
"authors": [
"Stephen McKamey (http://mck.me)"
],
"license": "MIT",
"homepage": "http://countdownjs.org",
"ignore": [
"./!(countdown.js)"
]
}
================================================
FILE: build.xml
================================================
A simple JavaScript API for producing an accurate, intuitive description of the timespan between two Date instances.
================================================ FILE: lib/closure/COPYING ================================================ 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. ================================================ FILE: lib/closure/README ================================================ /* * Copyright 2009 The Closure Compiler Authors. * * 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. */ // // Contents // The Closure Compiler performs checking, instrumentation, and optimizations on JavaScript code. The purpose of this README is to explain how to build and run the Closure Compiler. The Closure Compiler requires Java 6 or higher. http://www.java.com/ // // Building The Closure Compiler // There are three ways to get a Closure Compiler executable. 1) Use one we built for you. Pre-built Closure binaries can be found at http://code.google.com/p/closure-compiler/downloads/list 2) Check out the source and build it with Apache Ant. First, check out the full source tree of the Closure Compiler. There are instructions on how to do this at the project site. http://code.google.com/p/closure-compiler/source/checkout Apache Ant is a cross-platform build tool. http://ant.apache.org/ At the root of the source tree, there is an Ant file named build.xml. To use it, navigate to the same directory and type the command ant jar This will produce a jar file called "build/compiler.jar". 3) Check out the source and build it with Eclipse. Eclipse is a cross-platform IDE. http://www.eclipse.org/ Under Eclipse's File menu, click "New > Project ..." and create a "Java Project." You will see an options screen. Give the project a name, select "Create project from existing source," and choose the root of the checked-out source tree as the existing directory. Verify that you are using JRE version 6 or higher. Eclipse can use the build.xml file to discover rules. When you navigate to the build.xml file, you will see all the build rules in the "Outline" pane. Run the "jar" rule to build the compiler in build/compiler.jar. // // Running The Closure Compiler // Once you have the jar binary, running the Closure Compiler is straightforward. On the command line, type java -jar compiler.jar This starts the compiler in interactive mode. Type var x = 17 + 25; then hit "Enter", then hit "Ctrl-Z" (on Windows) or "Ctrl-D" (on Mac or Linux) and "Enter" again. The Compiler will respond: var x=42; The Closure Compiler has many options for reading input from a file, writing output to a file, checking your code, and running optimizations. To learn more, type java -jar compiler.jar --help You can read more detailed documentation about the many flags at http://code.google.com/closure/compiler/docs/gettingstarted_app.html // // Compiling Multiple Scripts // If you have multiple scripts, you should compile them all together with one compile command. java -jar compiler.jar --js=in1.js --js=in2.js ... --js_output_file=out.js The Closure Compiler will concatenate the files in the order they're passed at the command line. If you need to compile many, many scripts together, you may start to run into problems with managing dependencies between scripts. You should check out the Closure Library. It contains functions for enforcing dependencies between scripts, and a tool called calcdeps.py that knows how to give scripts to the Closure Compiler in the right order. http://code.google.com/p/closure-library/ // // Licensing // Unless otherwise stated, all source files are licensed under the Apache License, Version 2.0. ----- Code under: src/com/google/javascript/rhino test/com/google/javascript/rhino URL: http://www.mozilla.org/rhino Version: 1.5R3, with heavy modifications License: Netscape Public License and MPL / GPL dual license Description: A partial copy of Mozilla Rhino. Mozilla Rhino is an implementation of JavaScript for the JVM. The JavaScript parser and the parse tree data structures were extracted and modified significantly for use by Google's JavaScript compiler. Local Modifications: The packages have been renamespaced. All code not relevant to parsing has been removed. A JsDoc parser and static typing system have been added. ----- Code in: lib/rhino Rhino URL: http://www.mozilla.org/rhino Version: Trunk License: Netscape Public License and MPL / GPL dual license Description: Mozilla Rhino is an implementation of JavaScript for the JVM. Local Modifications: Minor changes to parsing JSDoc that usually get pushed up-stream to Rhino trunk. ----- Code in: lib/args4j.jar Args4j URL: https://args4j.dev.java.net/ Version: 2.0.16 License: MIT Description: args4j is a small Java class library that makes it easy to parse command line options/arguments in your CUI application. Local Modifications: None. ----- Code in: lib/guava.jar Guava Libraries URL: http://code.google.com/p/guava-libraries/ Version: 14.0 License: Apache License 2.0 Description: Google's core Java libraries. Local Modifications: None. ----- Code in: lib/jsr305.jar Annotations for software defect detection URL: http://code.google.com/p/jsr-305/ Version: svn revision 47 License: BSD License Description: Annotations for software defect detection. Local Modifications: None. ----- Code in: lib/jarjar.jar Jar Jar Links URL: http://jarjar.googlecode.com/ Version: 1.1 License: Apache License 2.0 Description: A utility for repackaging Java libraries. Local Modifications: None. ---- Code in: lib/junit.jar JUnit URL: http://sourceforge.net/projects/junit/ Version: 4.10 License: Common Public License 1.0 Description: A framework for writing and running automated tests in Java. Local Modifications: None. --- Code in: lib/protobuf-java.jar Protocol Buffers URL: http://code.google.com/p/protobuf/ Version: 2.4.1 License: New BSD License Description: Supporting libraries for protocol buffers, an encoding of structured data. Local Modifications: None --- Code in: lib/ant.jar lib/ant-launcher.jar URL: http://ant.apache.org/bindownload.cgi Version: 1.8.1 License: Apache License 2.0 Description: Ant is a Java based build tool. In theory it is kind of like "make" without make's wrinkles and with the full portability of pure java code. Local Modifications: None --- Code in: lib/json.jar URL: http://json.org/java/index.html Version: JSON version 20090211 License: MIT license Description: JSON is a set of java files for use in transmitting data in JSON format. Local Modifications: None --- Code in: tools/maven-ant-tasks-2.1.3.jar URL: http://maven.apache.org Version 2.1.3 License: Apache License 2.0 Description: Maven Ant tasks are used to manage dependencies and to install/deploy to maven repositories. Local Modifications: None ================================================ FILE: lib/closure/docs.url ================================================ [InternetShortcut] URL=http://code.google.com/closure/compiler/docs/js-for-compiler.html ================================================ FILE: lib/closure/download.url ================================================ [InternetShortcut] URL=http://code.google.com/closure/compiler/ ================================================ FILE: lib/jslint/download.url ================================================ [InternetShortcut] URL=http://www.jslint.com/lint.html ================================================ FILE: lib/jslint/jslint.js ================================================ // jslint.js // 2010-10-16 /* Copyright (c) 2002 Douglas Crockford (www.JSLint.com) 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 shall be used for Good, not Evil. 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. */ /* JSLINT is a global function. It takes two parameters. var myResult = JSLINT(source, option); The first parameter is either a string or an array of strings. If it is a string, it will be split on '\n' or '\r'. If it is an array of strings, it is assumed that each string represents one line. The source can be a JavaScript text, or HTML text, or a Konfabulator text. The second parameter is an optional object of options which control the operation of JSLINT. Most of the options are booleans: They are all are optional and have a default value of false. If it checks out, JSLINT returns true. Otherwise, it returns false. If false, you can inspect JSLINT.errors to find out the problems. JSLINT.errors is an array of objects containing these members: { line : The line (relative to 0) at which the lint was found character : The character (relative to 0) at which the lint was found reason : The problem evidence : The text line in which the problem occurred raw : The raw message before the details were inserted a : The first detail b : The second detail c : The third detail d : The fourth detail } If a fatal error was found, a null will be the last element of the JSLINT.errors array. You can request a Function Report, which shows all of the functions and the parameters and vars that they use. This can be used to find implied global variables and other problems. The report is in HTML and can be inserted in an HTML . var myReport = JSLINT.report(limited); If limited is true, then the report will be limited to only errors. You can request a data structure which contains JSLint's results. var myData = JSLINT.data(); It returns a structure with this form: { errors: [ { line: NUMBER, character: NUMBER, reason: STRING, evidence: STRING } ], functions: [ name: STRING, line: NUMBER, last: NUMBER, param: [ STRING ], closure: [ STRING ], var: [ STRING ], exception: [ STRING ], outer: [ STRING ], unused: [ STRING ], global: [ STRING ], label: [ STRING ] ], globals: [ STRING ], member: { STRING: NUMBER }, unuseds: [ { name: STRING, line: NUMBER } ], implieds: [ { name: STRING, line: NUMBER } ], urls: [ STRING ], json: BOOLEAN } Empty arrays will not be included. */ /*jslint evil: true, nomen: false, onevar: false, regexp: false, strict: true */ /*members "\b", "\t", "\n", "\f", "\r", "!=", "!==", "\"", "%", "(begin)", "(breakage)", "(context)", "(error)", "(global)", "(identifier)", "(last)", "(line)", "(loopage)", "(name)", "(onevar)", "(params)", "(scope)", "(statement)", "(verb)", "*", "+", "++", "-", "--", "\/", "<", "<=", "==", "===", ">", ">=", ADSAFE, ActiveXObject, Array, Boolean, COM, CScript, Canvas, CustomAnimation, Date, Debug, E, Enumerator, Error, EvalError, FadeAnimation, Flash, FormField, Frame, Function, HotKey, Image, JSON, LN10, LN2, LOG10E, LOG2E, MAX_VALUE, MIN_VALUE, Math, MenuItem, MoveAnimation, NEGATIVE_INFINITY, Number, Object, Option, PI, POSITIVE_INFINITY, Point, RangeError, Rectangle, ReferenceError, RegExp, ResizeAnimation, RotateAnimation, SQRT1_2, SQRT2, ScrollBar, String, Style, SyntaxError, System, Text, TextArea, Timer, TypeError, URIError, URL, VBArray, WScript, Web, Window, XMLDOM, XMLHttpRequest, "\\", a, abbr, acronym, addEventListener, address, adsafe, alert, aliceblue, animator, antiquewhite, appleScript, applet, apply, approved, aqua, aquamarine, area, arguments, arity, article, aside, audio, autocomplete, azure, b, background, "background-attachment", "background-color", "background-image", "background-position", "background-repeat", base, bdo, beep, beige, big, bisque, bitwise, black, blanchedalmond, block, blockquote, blue, blueviolet, blur, body, border, "border-bottom", "border-bottom-color", "border-bottom-style", "border-bottom-width", "border-collapse", "border-color", "border-left", "border-left-color", "border-left-style", "border-left-width", "border-right", "border-right-color", "border-right-style", "border-right-width", "border-spacing", "border-style", "border-top", "border-top-color", "border-top-style", "border-top-width", "border-width", bottom, br, brown, browser, burlywood, button, bytesToUIString, c, cadetblue, call, callee, caller, canvas, cap, caption, "caption-side", cases, center, charAt, charCodeAt, character, chartreuse, chocolate, chooseColor, chooseFile, chooseFolder, cite, clear, clearInterval, clearTimeout, clip, close, closeWidget, closed, closure, cm, code, col, colgroup, color, command, comment, condition, confirm, console, constructor, content, convertPathToHFS, convertPathToPlatform, coral, cornflowerblue, cornsilk, "counter-increment", "counter-reset", create, crimson, css, cursor, cyan, d, darkblue, darkcyan, darkgoldenrod, darkgray, darkgreen, darkkhaki, darkmagenta, darkolivegreen, darkorange, darkorchid, darkred, darksalmon, darkseagreen, darkslateblue, darkslategray, darkturquoise, darkviolet, data, datalist, dd, debug, decodeURI, decodeURIComponent, deeppink, deepskyblue, defaultStatus, defineClass, del, deserialize, details, devel, dfn, dialog, dimension, dimgray, dir, direction, display, div, dl, document, dodgerblue, dt, edition, else, em, embed, empty, "empty-cells", encodeURI, encodeURIComponent, entityify, eqeqeq, errors, es5, escape, eval, event, evidence, evil, ex, exception, exec, exps, fieldset, figure, filesystem, firebrick, first, float, floor, floralwhite, focus, focusWidget, font, "font-face", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", footer, forestgreen, forin, form, fragment, frame, frames, frameset, from, fromCharCode, fuchsia, fud, funct, function, functions, g, gainsboro, gc, getComputedStyle, ghostwhite, global, globals, gold, goldenrod, gray, green, greenyellow, h1, h2, h3, h4, h5, h6, hasOwnProperty, head, header, height, help, hgroup, history, honeydew, hotpink, hr, 'hta:application', html, i, iTunes, id, identifier, iframe, img, immed, implieds, in, include, indent, indexOf, indianred, indigo, init, input, ins, isAlpha, isApplicationRunning, isDigit, isFinite, isNaN, ivory, join, jslint, json, kbd, keygen, khaki, konfabulatorVersion, label, labelled, lang, last, lavender, lavenderblush, lawngreen, laxbreak, lbp, led, left, legend, lemonchiffon, length, "letter-spacing", li, lib, lightblue, lightcoral, lightcyan, lightgoldenrodyellow, lightgreen, lightpink, lightsalmon, lightseagreen, lightskyblue, lightslategray, lightsteelblue, lightyellow, lime, limegreen, line, "line-height", linen, link, "list-style", "list-style-image", "list-style-position", "list-style-type", load, loadClass, location, log, m, magenta, map, margin, "margin-bottom", "margin-left", "margin-right", "margin-top", mark, "marker-offset", maroon, match, "max-height", "max-width", maxerr, maxlen, md5, media, mediumaquamarine, mediumblue, mediumorchid, mediumpurple, mediumseagreen, mediumslateblue, mediumspringgreen, mediumturquoise, mediumvioletred, member, menu, message, meta, meter, midnightblue, "min-height", "min-width", mintcream, mistyrose, mm, moccasin, moveBy, moveTo, name, nav, navajowhite, navigator, navy, new, newcap, noframes, nomen, noscript, nud, object, ol, oldlace, olive, olivedrab, on, onbeforeunload, onblur, onerror, onevar, onfocus, onload, onresize, onunload, opacity, open, openURL, opener, opera, optgroup, option, orange, orangered, orchid, outer, outline, "outline-color", "outline-style", "outline-width", output, overflow, "overflow-x", "overflow-y", p, padding, "padding-bottom", "padding-left", "padding-right", "padding-top", page, "page-break-after", "page-break-before", palegoldenrod, palegreen, paleturquoise, palevioletred, papayawhip, param, parent, parseFloat, parseInt, passfail, pc, peachpuff, peru, pink, play, plum, plusplus, pop, popupMenu, position, powderblue, pre, predef, preferenceGroups, preferences, print, progress, prompt, prototype, pt, purple, push, px, q, quit, quotes, random, range, raw, reach, readFile, readUrl, reason, red, regexp, reloadWidget, removeEventListener, replace, report, reserved, resizeBy, resizeTo, resolvePath, resumeUpdates, rhino, right, rosybrown, royalblue, rp, rt, ruby, runCommand, runCommandInBg, saddlebrown, safe, salmon, samp, sandybrown, saveAs, savePreferences, screen, script, scroll, scrollBy, scrollTo, seagreen, seal, search, seashell, section, select, serialize, setInterval, setTimeout, shift, showWidgetPreferences, sienna, silver, skyblue, slateblue, slategray, sleep, slice, small, snow, sort, source, span, spawn, speak, split, springgreen, src, stack, statement, status, steelblue, strict, strong, style, styleproperty, sub, substr, sup, supplant, suppressUpdates, sync, system, table, "table-layout", tan, tbody, td, teal, tellWidget, test, "text-align", "text-decoration", "text-indent", "text-shadow", "text-transform", textarea, tfoot, th, thead, thistle, time, title, toLowerCase, toString, toUpperCase, toint32, token, tomato, top, tr, tt, turquoise, type, u, ul, undef, unescape, "unicode-bidi", unused, unwatch, updateNow, urls, value, valueOf, var, version, "vertical-align", video, violet, visibility, watch, wheat, white, "white-space", whitesmoke, widget, width, windows, "word-spacing", "word-wrap", yahooCheckLogin, yahooLogin, yahooLogout, yellow, yellowgreen, "z-index" */ // We build the application inside a function so that we produce only a single // global variable. The function will be invoked, its return value is the JSLINT // application itself. "use strict"; var JSLINT = (function () { var adsafe_id, // The widget's ADsafe id. adsafe_may, // The widget may load approved scripts. adsafe_went, // ADSAFE.go has been called. anonname, // The guessed name for anonymous functions. approved, // ADsafe approved urls. atrule = { media : true, 'font-face': true, page : true }, // These are operators that should not be used with the ! operator. bang = { '<': true, '<=': true, '==': true, '===': true, '!==': true, '!=': true, '>': true, '>=': true, '+': true, '-': true, '*': true, '/': true, '%': true }, // These are members that should not be permitted in the safe subset. banned = { // the member names that ADsafe prohibits. 'arguments' : true, callee : true, caller : true, constructor : true, 'eval' : true, prototype : true, stack : true, unwatch : true, valueOf : true, watch : true }, // These are the JSLint boolean options. boolOptions = { adsafe : true, // if ADsafe should be enforced bitwise : true, // if bitwise operators should not be allowed browser : true, // if the standard browser globals should be predefined cap : true, // if upper case HTML should be allowed css : true, // if CSS workarounds should be tolerated debug : true, // if debugger statements should be allowed devel : true, // if logging should be allowed (console, alert, etc.) eqeqeq : true, // if === should be required es5 : true, // if ES5 syntax should be allowed evil : true, // if eval should be allowed forin : true, // if for in statements must filter fragment : true, // if HTML fragments should be allowed immed : true, // if immediate invocations must be wrapped in parens laxbreak : true, // if line breaks should not be checked newcap : true, // if constructor names must be capitalized nomen : true, // if names should be checked on : true, // if HTML event handlers should be allowed onevar : true, // if only one var statement per function should be allowed passfail : true, // if the scan should stop on first error plusplus : true, // if increment/decrement should not be allowed regexp : true, // if the . should not be allowed in regexp literals rhino : true, // if the Rhino environment globals should be predefined undef : true, // if variables should be declared before used safe : true, // if use of some browser features should be restricted windows : true, // if MS Windows-specigic globals should be predefined strict : true, // require the "use strict"; pragma sub : true, // if all forms of subscript notation are tolerated white : true, // if strict whitespace rules apply widget : true // if the Yahoo Widgets globals should be predefined }, // browser contains a set of global names which are commonly provided by a // web browser environment. browser = { addEventListener: false, blur : false, clearInterval : false, clearTimeout : false, close : false, closed : false, defaultStatus : false, document : false, event : false, focus : false, frames : false, getComputedStyle: false, history : false, Image : false, length : false, location : false, moveBy : false, moveTo : false, name : false, navigator : false, onbeforeunload : true, onblur : true, onerror : true, onfocus : true, onload : true, onresize : true, onunload : true, open : false, opener : false, Option : false, parent : false, print : false, removeEventListener: false, resizeBy : false, resizeTo : false, screen : false, scroll : false, scrollBy : false, scrollTo : false, setInterval : false, setTimeout : false, status : false, top : false, XMLHttpRequest : false }, cssAttributeData, cssAny, cssColorData = { "aliceblue" : true, "antiquewhite" : true, "aqua" : true, "aquamarine" : true, "azure" : true, "beige" : true, "bisque" : true, "black" : true, "blanchedalmond" : true, "blue" : true, "blueviolet" : true, "brown" : true, "burlywood" : true, "cadetblue" : true, "chartreuse" : true, "chocolate" : true, "coral" : true, "cornflowerblue" : true, "cornsilk" : true, "crimson" : true, "cyan" : true, "darkblue" : true, "darkcyan" : true, "darkgoldenrod" : true, "darkgray" : true, "darkgreen" : true, "darkkhaki" : true, "darkmagenta" : true, "darkolivegreen" : true, "darkorange" : true, "darkorchid" : true, "darkred" : true, "darksalmon" : true, "darkseagreen" : true, "darkslateblue" : true, "darkslategray" : true, "darkturquoise" : true, "darkviolet" : true, "deeppink" : true, "deepskyblue" : true, "dimgray" : true, "dodgerblue" : true, "firebrick" : true, "floralwhite" : true, "forestgreen" : true, "fuchsia" : true, "gainsboro" : true, "ghostwhite" : true, "gold" : true, "goldenrod" : true, "gray" : true, "green" : true, "greenyellow" : true, "honeydew" : true, "hotpink" : true, "indianred" : true, "indigo" : true, "ivory" : true, "khaki" : true, "lavender" : true, "lavenderblush" : true, "lawngreen" : true, "lemonchiffon" : true, "lightblue" : true, "lightcoral" : true, "lightcyan" : true, "lightgoldenrodyellow" : true, "lightgreen" : true, "lightpink" : true, "lightsalmon" : true, "lightseagreen" : true, "lightskyblue" : true, "lightslategray" : true, "lightsteelblue" : true, "lightyellow" : true, "lime" : true, "limegreen" : true, "linen" : true, "magenta" : true, "maroon" : true, "mediumaquamarine" : true, "mediumblue" : true, "mediumorchid" : true, "mediumpurple" : true, "mediumseagreen" : true, "mediumslateblue" : true, "mediumspringgreen" : true, "mediumturquoise" : true, "mediumvioletred" : true, "midnightblue" : true, "mintcream" : true, "mistyrose" : true, "moccasin" : true, "navajowhite" : true, "navy" : true, "oldlace" : true, "olive" : true, "olivedrab" : true, "orange" : true, "orangered" : true, "orchid" : true, "palegoldenrod" : true, "palegreen" : true, "paleturquoise" : true, "palevioletred" : true, "papayawhip" : true, "peachpuff" : true, "peru" : true, "pink" : true, "plum" : true, "powderblue" : true, "purple" : true, "red" : true, "rosybrown" : true, "royalblue" : true, "saddlebrown" : true, "salmon" : true, "sandybrown" : true, "seagreen" : true, "seashell" : true, "sienna" : true, "silver" : true, "skyblue" : true, "slateblue" : true, "slategray" : true, "snow" : true, "springgreen" : true, "steelblue" : true, "tan" : true, "teal" : true, "thistle" : true, "tomato" : true, "turquoise" : true, "violet" : true, "wheat" : true, "white" : true, "whitesmoke" : true, "yellow" : true, "yellowgreen" : true }, cssBorderStyle, cssBreak, cssLengthData = { '%': true, 'cm': true, 'em': true, 'ex': true, 'in': true, 'mm': true, 'pc': true, 'pt': true, 'px': true }, cssOverflow, devel = { alert : false, confirm : false, console : false, Debug : false, opera : false, prompt : false }, escapes = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '/' : '\\/', '\\': '\\\\' }, funct, // The current function functionicity = [ 'closure', 'exception', 'global', 'label', 'outer', 'unused', 'var' ], functions, // All of the functions global, // The global scope htmltag = { a: {}, abbr: {}, acronym: {}, address: {}, applet: {}, area: {empty: true, parent: ' map '}, article: {}, aside: {}, audio: {}, b: {}, base: {empty: true, parent: ' head '}, bdo: {}, big: {}, blockquote: {}, body: {parent: ' html noframes '}, br: {empty: true}, button: {}, canvas: {parent: ' body p div th td '}, caption: {parent: ' table '}, center: {}, cite: {}, code: {}, col: {empty: true, parent: ' table colgroup '}, colgroup: {parent: ' table '}, command: {parent: ' menu '}, datalist: {}, dd: {parent: ' dl '}, del: {}, details: {}, dialog: {}, dfn: {}, dir: {}, div: {}, dl: {}, dt: {parent: ' dl '}, em: {}, embed: {}, fieldset: {}, figure: {}, font: {}, footer: {}, form: {}, frame: {empty: true, parent: ' frameset '}, frameset: {parent: ' html frameset '}, h1: {}, h2: {}, h3: {}, h4: {}, h5: {}, h6: {}, head: {parent: ' html '}, header: {}, hgroup: {}, hr: {empty: true}, 'hta:application': {empty: true, parent: ' head '}, html: {parent: '*'}, i: {}, iframe: {}, img: {empty: true}, input: {empty: true}, ins: {}, kbd: {}, keygen: {}, label: {}, legend: {parent: ' details fieldset figure '}, li: {parent: ' dir menu ol ul '}, link: {empty: true, parent: ' head '}, map: {}, mark: {}, menu: {}, meta: {empty: true, parent: ' head noframes noscript '}, meter: {}, nav: {}, noframes: {parent: ' html body '}, noscript: {parent: ' body head noframes '}, object: {}, ol: {}, optgroup: {parent: ' select '}, option: {parent: ' optgroup select '}, output: {}, p: {}, param: {empty: true, parent: ' applet object '}, pre: {}, progress: {}, q: {}, rp: {}, rt: {}, ruby: {}, samp: {}, script: {empty: true, parent: ' body div frame head iframe p pre span '}, section: {}, select: {}, small: {}, span: {}, source: {}, strong: {}, style: {parent: ' head ', empty: true}, sub: {}, sup: {}, table: {}, tbody: {parent: ' table '}, td: {parent: ' tr '}, textarea: {}, tfoot: {parent: ' table '}, th: {parent: ' tr '}, thead: {parent: ' table '}, time: {}, title: {parent: ' head '}, tr: {parent: ' table tbody thead tfoot '}, tt: {}, u: {}, ul: {}, 'var': {}, video: {} }, ids, // HTML ids implied, // Implied globals inblock, indent, jsonmode, lines, lookahead, member, membersOnly, nexttoken, noreach, option, predefined, // Global variables defined by option prereg, prevtoken, rhino = { defineClass : false, deserialize : false, gc : false, help : false, load : false, loadClass : false, print : false, quit : false, readFile : false, readUrl : false, runCommand : false, seal : false, serialize : false, spawn : false, sync : false, toint32 : false, version : false }, scope, // The current scope windows = { ActiveXObject: false, CScript : false, Debug : false, Enumerator : false, System : false, VBArray : false, WScript : false }, src, stack, // standard contains the global names that are provided by the // ECMAScript standard. standard = { Array : false, Boolean : false, Date : false, decodeURI : false, decodeURIComponent : false, encodeURI : false, encodeURIComponent : false, Error : false, 'eval' : false, EvalError : false, Function : false, hasOwnProperty : false, isFinite : false, isNaN : false, JSON : false, Math : false, Number : false, Object : false, parseInt : false, parseFloat : false, RangeError : false, ReferenceError : false, RegExp : false, String : false, SyntaxError : false, TypeError : false, URIError : false }, standard_member = { E : true, LN2 : true, LN10 : true, LOG2E : true, LOG10E : true, PI : true, SQRT1_2 : true, SQRT2 : true, MAX_VALUE : true, MIN_VALUE : true, NEGATIVE_INFINITY : true, POSITIVE_INFINITY : true }, strict_mode, syntax = {}, tab, token, urls, warnings, // widget contains the global names which are provided to a Yahoo // (fna Konfabulator) widget. widget = { alert : true, animator : true, appleScript : true, beep : true, bytesToUIString : true, Canvas : true, chooseColor : true, chooseFile : true, chooseFolder : true, closeWidget : true, COM : true, convertPathToHFS : true, convertPathToPlatform : true, CustomAnimation : true, escape : true, FadeAnimation : true, filesystem : true, Flash : true, focusWidget : true, form : true, FormField : true, Frame : true, HotKey : true, Image : true, include : true, isApplicationRunning : true, iTunes : true, konfabulatorVersion : true, log : true, md5 : true, MenuItem : true, MoveAnimation : true, openURL : true, play : true, Point : true, popupMenu : true, preferenceGroups : true, preferences : true, print : true, prompt : true, random : true, Rectangle : true, reloadWidget : true, ResizeAnimation : true, resolvePath : true, resumeUpdates : true, RotateAnimation : true, runCommand : true, runCommandInBg : true, saveAs : true, savePreferences : true, screen : true, ScrollBar : true, showWidgetPreferences : true, sleep : true, speak : true, Style : true, suppressUpdates : true, system : true, tellWidget : true, Text : true, TextArea : true, Timer : true, unescape : true, updateNow : true, URL : true, Web : true, widget : true, Window : true, XMLDOM : true, XMLHttpRequest : true, yahooCheckLogin : true, yahooLogin : true, yahooLogout : true }, // xmode is used to adapt to the exceptions in html parsing. // It can have these states: // false .js script file // html // outer // script // style // scriptstring // styleproperty xmode, xquote, // unsafe comment or string ax = /@cc|<\/?|script|\]*s\]|<\s*!|</i, // unsafe characters that are silently deleted by one or more browsers cx = /[\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/, // token tx = /^\s*([(){}\[.,:;'"~\?\]#@]|==?=?|\/(\*(jslint|members?|global)?|=|\/)?|\*[\/=]?|\+(?:=|\++)?|-(?:=|-+)?|%=?|&[&=]?|\|[|=]?|>>?>?=?|<([\/=!]|\!(\[|--)?|<=?)?|\^=?|\!=?=?|[a-zA-Z_$][a-zA-Z0-9_$]*|[0-9]+([xX][0-9a-fA-F]+|\.[0-9]*)?([eE][+\-]?[0-9]+)?)/, // html token hx = /^\s*(['"=>\/]|<(?:\/|\!(?:--)?)?|[a-zA-Z][a-zA-Z0-9_\-:]*|[0-9]+|--)/, // characters in strings that need escapement nx = /[\u0000-\u001f&<"\/\\\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/, nxg = /[\u0000-\u001f&<"\/\\\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, // outer html token ox = /[>&]|<[\/!]?|--/, // star slash lx = /\*\/|\/\*/, // identifier ix = /^([a-zA-Z_$][a-zA-Z0-9_$]*)$/, // javascript url jx = /^(?:javascript|jscript|ecmascript|vbscript|mocha|livescript)\s*:/i, // url badness ux = /&|\+|\u00AD|\.\.|\/\*|%[^;]|base64|url|expression|data|mailto/i, // style sx = /^\s*([{:#%.=,>+\[\]@()"';]|\*=?|\$=|\|=|\^=|~=|[a-zA-Z_][a-zA-Z0-9_\-]*|[0-9]+|<\/|\/\*)/, ssx = /^\s*([@#!"'};:\-%.=,+\[\]()*_]|[a-zA-Z][a-zA-Z0-9._\-]*|\/\*?|\d+(?:\.\d+)?|<\/)/, // attributes characters qx = /[^a-zA-Z0-9+\-_\/ ]/, // query characters for ids dx = /[\[\]\/\\"'*<>.&:(){}+=#]/, rx = { outer: hx, html: hx, style: sx, styleproperty: ssx }; function F() {} if (typeof Object.create !== 'function') { Object.create = function (o) { F.prototype = o; return new F(); }; } function is_own(object, name) { return Object.prototype.hasOwnProperty.call(object, name); } function combine(t, o) { var n; for (n in o) { if (is_own(o, n)) { t[n] = o[n]; } } } String.prototype.entityify = function () { return this .replace(/&/g, '&') .replace(//g, '>'); }; String.prototype.isAlpha = function () { return (this >= 'a' && this <= 'z\uffff') || (this >= 'A' && this <= 'Z\uffff'); }; String.prototype.isDigit = function () { return (this >= '0' && this <= '9'); }; String.prototype.supplant = function (o) { return this.replace(/\{([^{}]*)\}/g, function (a, b) { var r = o[b]; return typeof r === 'string' || typeof r === 'number' ? r : a; }); }; String.prototype.name = function () { // If the string looks like an identifier, then we can return it as is. // If the string contains no control characters, no quote characters, and no // backslash characters, then we can simply slap some quotes around it. // Otherwise we must also replace the offending characters with safe // sequences. if (ix.test(this)) { return this; } if (nx.test(this)) { return '"' + this.replace(nxg, function (a) { var c = escapes[a]; if (c) { return c; } return '\\u' + ('0000' + a.charCodeAt().toString(16)).slice(-4); }) + '"'; } return '"' + this + '"'; }; function assume() { if (!option.safe) { if (option.rhino) { combine(predefined, rhino); } if (option.devel) { combine(predefined, devel); } if (option.browser) { combine(predefined, browser); } if (option.windows) { combine(predefined, windows); } if (option.widget) { combine(predefined, widget); } } } // Produce an error warning. function quit(m, l, ch) { throw { name: 'JSLintError', line: l, character: ch, message: m + " (" + Math.floor((l / lines.length) * 100) + "% scanned)." }; } function warning(m, t, a, b, c, d) { var ch, l, w; t = t || nexttoken; if (t.id === '(end)') { // `~ t = token; } l = t.line || 0; ch = t.from || 0; w = { id: '(error)', raw: m, evidence: lines[l - 1] || '', line: l, character: ch, a: a, b: b, c: c, d: d }; w.reason = m.supplant(w); JSLINT.errors.push(w); if (option.passfail) { quit('Stopping. ', l, ch); } warnings += 1; if (warnings >= option.maxerr) { quit("Too many errors.", l, ch); } return w; } function warningAt(m, l, ch, a, b, c, d) { return warning(m, { line: l, from: ch }, a, b, c, d); } function error(m, t, a, b, c, d) { var w = warning(m, t, a, b, c, d); quit("Stopping, unable to continue.", w.line, w.character); } function errorAt(m, l, ch, a, b, c, d) { return error(m, { line: l, from: ch }, a, b, c, d); } // lexical analysis var lex = (function lex() { var character, from, line, s; // Private lex methods function nextLine() { var at; if (line >= lines.length) { return false; } character = 1; s = lines[line]; line += 1; at = s.search(/ \t/); if (at >= 0) { warningAt("Mixed spaces and tabs.", line, at + 1); } s = s.replace(/\t/g, tab); at = s.search(cx); if (at >= 0) { warningAt("Unsafe character.", line, at); } if (option.maxlen && option.maxlen < s.length) { warningAt("Line too long.", line, s.length); } return true; } // Produce a token object. The token inherits from a syntax symbol. function it(type, value) { var i, t; if (type === '(color)' || type === '(range)') { t = {type: type}; } else if (type === '(punctuator)' || (type === '(identifier)' && is_own(syntax, value))) { t = syntax[value] || syntax['(error)']; } else { t = syntax[type]; } t = Object.create(t); if (type === '(string)' || type === '(range)') { if (jx.test(value)) { warningAt("Script URL.", line, from); } } if (type === '(identifier)') { t.identifier = true; if (value === '__iterator__' || value === '__proto__') { errorAt("Reserved name '{a}'.", line, from, value); } else if (option.nomen && (value.charAt(0) === '_' || value.charAt(value.length - 1) === '_')) { warningAt("Unexpected {a} in '{b}'.", line, from, "dangling '_'", value); } } t.value = value; t.line = line; t.character = character; t.from = from; i = t.id; if (i !== '(endline)') { prereg = i && (('(,=:[!&|?{};'.indexOf(i.charAt(i.length - 1)) >= 0) || i === 'return'); } return t; } // Public lex methods return { init: function (source) { if (typeof source === 'string') { lines = source .replace(/\r\n/g, '\n') .replace(/\r/g, '\n') .split('\n'); } else { lines = source; } line = 0; nextLine(); from = 1; }, range: function (begin, end) { var c, value = ''; from = character; if (s.charAt(0) !== begin) { errorAt("Expected '{a}' and instead saw '{b}'.", line, character, begin, s.charAt(0)); } for (;;) { s = s.slice(1); character += 1; c = s.charAt(0); switch (c) { case '': errorAt("Missing '{a}'.", line, character, c); break; case end: s = s.slice(1); character += 1; return it('(range)', value); case xquote: case '\\': warningAt("Unexpected '{a}'.", line, character, c); } value += c; } }, // token -- this is called by advance to get the next token. token: function () { var b, c, captures, d, depth, high, i, l, low, q, t; function match(x) { var r = x.exec(s), r1; if (r) { l = r[0].length; r1 = r[1]; c = r1.charAt(0); s = s.substr(l); from = character + l - r1.length; character += l; return r1; } } function string(x) { var c, j, r = ''; if (jsonmode && x !== '"') { warningAt("Strings must use doublequote.", line, character); } if (xquote === x || (xmode === 'scriptstring' && !xquote)) { return it('(punctuator)', x); } function esc(n) { var i = parseInt(s.substr(j + 1, n), 16); j += n; if (i >= 32 && i <= 126 && i !== 34 && i !== 92 && i !== 39) { warningAt("Unnecessary escapement.", line, character); } character += n; c = String.fromCharCode(i); } j = 0; for (;;) { while (j >= s.length) { j = 0; if (xmode !== 'html' || !nextLine()) { errorAt("Unclosed string.", line, from); } } c = s.charAt(j); if (c === x) { character += 1; s = s.substr(j + 1); return it('(string)', r, x); } if (c < ' ') { if (c === '\n' || c === '\r') { break; } warningAt("Control character in string: {a}.", line, character + j, s.slice(0, j)); } else if (c === xquote) { warningAt("Bad HTML string", line, character + j); } else if (c === '<') { if (option.safe && xmode === 'html') { warningAt("ADsafe string violation.", line, character + j); } else if (s.charAt(j + 1) === '/' && (xmode || option.safe)) { warningAt("Expected '<\\/' and instead saw ''.", line, character); } else if (s.charAt(j + 1) === '!' && (xmode || option.safe)) { warningAt("Unexpected ' 0) { character += 1; s = s.slice(i); break; } else { if (!nextLine()) { return it('(end)', ''); } } } // t = match(rx[xmode] || tx); // if (!t) { // if (xmode === 'html') { // return it('(error)', s.charAt(0)); // } else { // t = ''; // c = ''; // while (s && s < '!') { // s = s.substr(1); // } // if (s) { // errorAt("Unexpected '{a}'.", // line, character, s.substr(0, 1)); // } // } t = match(rx[xmode] || tx); if (!t) { t = ''; c = ''; while (s && s < '!') { s = s.substr(1); } if (s) { if (xmode === 'html') { return it('(error)', s.charAt(0)); } else { errorAt("Unexpected '{a}'.", line, character, s.substr(0, 1)); } } } else { // identifier if (c.isAlpha() || c === '_' || c === '$') { return it('(identifier)', t); } // number if (c.isDigit()) { if (xmode !== 'style' && !isFinite(Number(t))) { warningAt("Bad number '{a}'.", line, character, t); } if (xmode !== 'style' && xmode !== 'styleproperty' && s.substr(0, 1).isAlpha()) { warningAt("Missing space after '{a}'.", line, character, t); } if (c === '0') { d = t.substr(1, 1); if (d.isDigit()) { if (token.id !== '.' && xmode !== 'styleproperty') { warningAt("Don't use extra leading zeros '{a}'.", line, character, t); } } else if (jsonmode && (d === 'x' || d === 'X')) { warningAt("Avoid 0x-. '{a}'.", line, character, t); } } if (t.substr(t.length - 1) === '.') { warningAt( "A trailing decimal point can be confused with a dot '{a}'.", line, character, t); } return it('(number)', t); } switch (t) { // string case '"': case "'": return string(t); // // comment case '//': if (src || (xmode && xmode !== 'script')) { warningAt("Unexpected comment.", line, character); } else if (xmode === 'script' && /<\s*\//i.test(s)) { warningAt("Unexpected <\/ in comment.", line, character); } else if ((option.safe || xmode === 'script') && ax.test(s)) { warningAt("Dangerous comment.", line, character); } s = ''; token.comment = true; break; // /* comment case '/*': if (src || (xmode && xmode !== 'script' && xmode !== 'style' && xmode !== 'styleproperty')) { warningAt("Unexpected comment.", line, character); } if (option.safe && ax.test(s)) { warningAt("ADsafe comment violation.", line, character); } for (;;) { i = s.search(lx); if (i >= 0) { break; } if (!nextLine()) { errorAt("Unclosed comment.", line, character); } else { if (option.safe && ax.test(s)) { warningAt("ADsafe comment violation.", line, character); } } } character += i + 2; if (s.substr(i, 1) === '/') { errorAt("Nested comment.", line, character); } s = s.substr(i + 2); token.comment = true; break; // /*members /*jslint /*global case '/*members': case '/*member': case '/*jslint': case '/*global': case '*/': return { value: t, type: 'special', line: line, character: character, from: from }; case '': break; // / case '/': if (token.id === '/=') { errorAt( "A regular expression literal can be confused with '/='.", line, from); } if (prereg) { depth = 0; captures = 0; l = 0; for (;;) { b = true; c = s.charAt(l); l += 1; switch (c) { case '': errorAt("Unclosed regular expression.", line, from); return; case '/': if (depth > 0) { warningAt("Unescaped '{a}'.", line, from + l, '/'); } c = s.substr(0, l - 1); q = { g: true, i: true, m: true }; while (q[s.charAt(l)] === true) { q[s.charAt(l)] = false; l += 1; } character += l; s = s.substr(l); q = s.charAt(0); if (q === '/' || q === '*') { errorAt("Confusing regular expression.", line, from); } return it('(regexp)', c); case '\\': c = s.charAt(l); if (c < ' ') { warningAt( "Unexpected control character in regular expression.", line, from + l); } else if (c === '<') { warningAt( "Unexpected escaped character '{a}' in regular expression.", line, from + l, c); } l += 1; break; case '(': depth += 1; b = false; if (s.charAt(l) === '?') { l += 1; switch (s.charAt(l)) { case ':': case '=': case '!': l += 1; break; default: warningAt( "Expected '{a}' and instead saw '{b}'.", line, from + l, ':', s.charAt(l)); } } else { captures += 1; } break; case '|': b = false; break; case ')': if (depth === 0) { warningAt("Unescaped '{a}'.", line, from + l, ')'); } else { depth -= 1; } break; case ' ': q = 1; while (s.charAt(l) === ' ') { l += 1; q += 1; } if (q > 1) { warningAt( "Spaces are hard to count. Use {{a}}.", line, from + l, q); } break; case '[': c = s.charAt(l); if (c === '^') { l += 1; if (option.regexp) { warningAt("Insecure '{a}'.", line, from + l, c); } else if (s.charAt(l) === ']') { errorAt("Unescaped '{a}'.", line, from, '^'); } } q = false; if (c === ']') { warningAt("Empty class.", line, from + l - 1); q = true; } klass: do { c = s.charAt(l); l += 1; switch (c) { case '[': case '^': warningAt("Unescaped '{a}'.", line, from + l, c); q = true; break; case '-': if (q) { q = false; } else { warningAt("Unescaped '{a}'.", line, from + l, '-'); q = true; } break; case ']': if (!q) { warningAt("Unescaped '{a}'.", line, from + l - 1, '-'); } break klass; case '\\': c = s.charAt(l); if (c < ' ') { warningAt( "Unexpected control character in regular expression.", line, from + l); } else if (c === '<') { warningAt( "Unexpected escaped character '{a}' in regular expression.", line, from + l, c); } l += 1; q = true; break; case '/': warningAt("Unescaped '{a}'.", line, from + l - 1, '/'); q = true; break; case '<': if (xmode === 'script') { c = s.charAt(l); if (c === '!' || c === '/') { warningAt( "HTML confusion in regular expression '<{a}'.", line, from + l, c); } } q = true; break; default: q = true; } } while (c); break; case '.': if (option.regexp) { warningAt("Insecure '{a}'.", line, from + l, c); } break; case ']': case '?': case '{': case '}': case '+': case '*': warningAt("Unescaped '{a}'.", line, from + l, c); break; case '<': if (xmode === 'script') { c = s.charAt(l); if (c === '!' || c === '/') { warningAt( "HTML confusion in regular expression '<{a}'.", line, from + l, c); } } } if (b) { switch (s.charAt(l)) { case '?': case '+': case '*': l += 1; if (s.charAt(l) === '?') { l += 1; } break; case '{': l += 1; c = s.charAt(l); if (c < '0' || c > '9') { warningAt( "Expected a number and instead saw '{a}'.", line, from + l, c); } l += 1; low = +c; for (;;) { c = s.charAt(l); if (c < '0' || c > '9') { break; } l += 1; low = +c + (low * 10); } high = low; if (c === ',') { l += 1; high = Infinity; c = s.charAt(l); if (c >= '0' && c <= '9') { l += 1; high = +c; for (;;) { c = s.charAt(l); if (c < '0' || c > '9') { break; } l += 1; high = +c + (high * 10); } } } if (s.charAt(l) !== '}') { warningAt( "Expected '{a}' and instead saw '{b}'.", line, from + l, '}', c); } else { l += 1; } if (s.charAt(l) === '?') { l += 1; } if (low > high) { warningAt( "'{a}' should not be greater than '{b}'.", line, from + l, low, high); } } } } c = s.substr(0, l - 1); character += l; s = s.substr(l); return it('(regexp)', c); } return it('(punctuator)', t); // punctuator case '.", line, character); } character += 3; s = s.slice(i + 3); break; case '#': if (xmode === 'html' || xmode === 'styleproperty') { for (;;) { c = s.charAt(0); if ((c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F')) { break; } character += 1; s = s.substr(1); t += c; } if (t.length !== 4 && t.length !== 7) { warningAt("Bad hex color '{a}'.", line, from + l, t); } return it('(color)', t); } return it('(punctuator)', t); default: if (xmode === 'outer' && c === '&') { character += 1; s = s.substr(1); for (;;) { c = s.charAt(0); character += 1; s = s.substr(1); if (c === ';') { break; } if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c === '#')) { errorAt("Bad entity", line, from + l, character); } } break; } return it('(punctuator)', t); } } } } }; }()); function addlabel(t, type) { if (option.safe && funct['(global)'] && typeof predefined[t] !== 'boolean') { warning('ADsafe global: ' + t + '.', token); } else if (t === 'hasOwnProperty') { warning("'hasOwnProperty' is a really bad name."); } // Define t in the current function in the current scope. if (is_own(funct, t) && !funct['(global)']) { warning(funct[t] === true ? "'{a}' was used before it was defined." : "'{a}' is already defined.", nexttoken, t); } funct[t] = type; if (funct['(global)']) { global[t] = funct; if (is_own(implied, t)) { warning("'{a}' was used before it was defined.", nexttoken, t); delete implied[t]; } } else { scope[t] = funct; } } function doOption() { var b, obj, filter, o = nexttoken.value, t, v; switch (o) { case '*/': error("Unbegun comment."); break; case '/*members': case '/*member': o = '/*members'; if (!membersOnly) { membersOnly = {}; } obj = membersOnly; break; case '/*jslint': if (option.safe) { warning("ADsafe restriction."); } obj = option; filter = boolOptions; break; case '/*global': if (option.safe) { warning("ADsafe restriction."); } obj = predefined; break; default: } t = lex.token(); loop: for (;;) { for (;;) { if (t.type === 'special' && t.value === '*/') { break loop; } if (t.id !== '(endline)' && t.id !== ',') { break; } t = lex.token(); } if (t.type !== '(string)' && t.type !== '(identifier)' && o !== '/*members') { error("Bad option.", t); } v = lex.token(); if (v.id === ':') { v = lex.token(); if (obj === membersOnly) { error("Expected '{a}' and instead saw '{b}'.", t, '*/', ':'); } if (t.value === 'indent' && o === '/*jslint') { b = +v.value; if (typeof b !== 'number' || !isFinite(b) || b <= 0 || Math.floor(b) !== b) { error("Expected a small integer and instead saw '{a}'.", v, v.value); } obj.white = true; obj.indent = b; } else if (t.value === 'maxerr' && o === '/*jslint') { b = +v.value; if (typeof b !== 'number' || !isFinite(b) || b <= 0 || Math.floor(b) !== b) { error("Expected a small integer and instead saw '{a}'.", v, v.value); } obj.maxerr = b; } else if (t.value === 'maxlen' && o === '/*jslint') { b = +v.value; if (typeof b !== 'number' || !isFinite(b) || b <= 0 || Math.floor(b) !== b) { error("Expected a small integer and instead saw '{a}'.", v, v.value); } obj.maxlen = b; } else if (v.value === 'true') { obj[t.value] = true; } else if (v.value === 'false') { obj[t.value] = false; } else { error("Bad option value.", v); } t = lex.token(); } else { if (o === '/*jslint') { error("Missing option value.", t); } obj[t.value] = false; t = v; } } if (filter) { assume(); } } // We need a peek function. If it has an argument, it peeks that much farther // ahead. It is used to distinguish // for ( var i in ... // from // for ( var i = ... function peek(p) { var i = p || 0, j = 0, t; while (j <= i) { t = lookahead[j]; if (!t) { t = lookahead[j] = lex.token(); } j += 1; } return t; } // Produce the next token. It looks for programming errors. function advance(id, t) { switch (token.id) { case '(number)': if (nexttoken.id === '.') { warning( "A dot following a number can be confused with a decimal point.", token); } break; case '-': if (nexttoken.id === '-' || nexttoken.id === '--') { warning("Confusing minusses."); } break; case '+': if (nexttoken.id === '+' || nexttoken.id === '++') { warning("Confusing plusses."); } break; } if (token.type === '(string)' || token.identifier) { anonname = token.value; } if (id && nexttoken.id !== id) { if (t) { if (nexttoken.id === '(end)') { warning("Unmatched '{a}'.", t, t.id); } else { warning( "Expected '{a}' to match '{b}' from line {c} and instead saw '{d}'.", nexttoken, id, t.id, t.line, nexttoken.value); } } else if (nexttoken.type !== '(identifier)' || nexttoken.value !== id) { warning("Expected '{a}' and instead saw '{b}'.", nexttoken, id, nexttoken.value); } } prevtoken = token; token = nexttoken; for (;;) { nexttoken = lookahead.shift() || lex.token(); if (nexttoken.id === '(end)' || nexttoken.id === '(error)') { return; } if (nexttoken.type === 'special') { doOption(); } else { if (nexttoken.id !== '(endline)') { break; } } } } // This is the heart of JSLINT, the Pratt parser. In addition to parsing, it // is looking for ad hoc lint patterns. We add to Pratt's model .fud, which is // like nud except that it is only used on the first token of a statement. // Having .fud makes it much easier to define JavaScript. I retained Pratt's // nomenclature. // .nud Null denotation // .fud First null denotation // .led Left denotation // lbp Left binding power // rbp Right binding power // They are key to the parsing method called Top Down Operator Precedence. function parse(rbp, initial) { var left; if (nexttoken.id === '(end)') { error("Unexpected early end of program.", token); } advance(); if (option.safe && typeof predefined[token.value] === 'boolean' && (nexttoken.id !== '(' && nexttoken.id !== '.')) { warning('ADsafe violation.', token); } if (initial) { anonname = 'anonymous'; funct['(verb)'] = token.value; } if (initial === true && token.fud) { left = token.fud(); } else { if (token.nud) { left = token.nud(); } else { if (nexttoken.type === '(number)' && token.id === '.') { warning( "A leading decimal point can be confused with a dot: '.{a}'.", token, nexttoken.value); advance(); return token; } else { error("Expected an identifier and instead saw '{a}'.", token, token.id); } } while (rbp < nexttoken.lbp) { advance(); if (token.led) { left = token.led(left); } else { error("Expected an operator and instead saw '{a}'.", token, token.id); } } } return left; } // Functions for conformance of style. function adjacent(left, right) { left = left || token; right = right || nexttoken; if (option.white || xmode === 'styleproperty' || xmode === 'style') { if (left.character !== right.from && left.line === right.line) { warning("Unexpected space after '{a}'.", right, left.value); } } } function nobreak(left, right) { left = left || token; right = right || nexttoken; if (left.character !== right.from || left.line !== right.line) { warning("Unexpected space before '{a}'.", right, right.value); } } function nospace(left, right) { left = left || token; right = right || nexttoken; if (option.white && !left.comment) { if (left.line === right.line) { adjacent(left, right); } } } function nonadjacent(left, right) { if (option.white) { left = left || token; right = right || nexttoken; if (left.line === right.line && left.character === right.from) { warning("Missing space after '{a}'.", nexttoken, left.value); } } } function nobreaknonadjacent(left, right) { left = left || token; right = right || nexttoken; if (!option.laxbreak && left.line !== right.line) { warning("Bad line breaking before '{a}'.", right, right.id); } else if (option.white) { left = left || token; right = right || nexttoken; if (left.character === right.from) { warning("Missing space after '{a}'.", nexttoken, left.value); } } } function indentation(bias) { var i; if (option.white && nexttoken.id !== '(end)') { i = indent + (bias || 0); if (nexttoken.from !== i) { warning( "Expected '{a}' to have an indentation at {b} instead at {c}.", nexttoken, nexttoken.value, i, nexttoken.from); } } } function nolinebreak(t) { t = t || token; if (t.line !== nexttoken.line) { warning("Line breaking error '{a}'.", t, t.value); } } function comma() { if (token.line !== nexttoken.line) { if (!option.laxbreak) { warning("Bad line breaking before '{a}'.", token, nexttoken.id); } } else if (token.character !== nexttoken.from && option.white) { warning("Unexpected space after '{a}'.", nexttoken, token.value); } advance(','); nonadjacent(token, nexttoken); } // Functional constructors for making the symbols that will be inherited by // tokens. function symbol(s, p) { var x = syntax[s]; if (!x || typeof x !== 'object') { syntax[s] = x = { id: s, lbp: p, value: s }; } return x; } function delim(s) { return symbol(s, 0); } function stmt(s, f) { var x = delim(s); x.identifier = x.reserved = true; x.fud = f; return x; } function blockstmt(s, f) { var x = stmt(s, f); x.block = true; return x; } function reserveName(x) { var c = x.id.charAt(0); if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { x.identifier = x.reserved = true; } return x; } function prefix(s, f) { var x = symbol(s, 150); reserveName(x); x.nud = (typeof f === 'function') ? f : function () { this.right = parse(150); this.arity = 'unary'; if (this.id === '++' || this.id === '--') { if (option.plusplus) { warning("Unexpected use of '{a}'.", this, this.id); } else if ((!this.right.identifier || this.right.reserved) && this.right.id !== '.' && this.right.id !== '[') { warning("Bad operand.", this); } } return this; }; return x; } function type(s, f) { var x = delim(s); x.type = s; x.nud = f; return x; } function reserve(s, f) { var x = type(s, f); x.identifier = x.reserved = true; return x; } function reservevar(s, v) { return reserve(s, function () { if (typeof v === 'function') { v(this); } return this; }); } function infix(s, f, p, w) { var x = symbol(s, p); reserveName(x); x.led = function (left) { if (!w) { nobreaknonadjacent(prevtoken, token); nonadjacent(token, nexttoken); } if (typeof f === 'function') { return f(left, this); } else { this.left = left; this.right = parse(p); return this; } }; return x; } function relation(s, f) { var x = symbol(s, 100); x.led = function (left) { nobreaknonadjacent(prevtoken, token); nonadjacent(token, nexttoken); var right = parse(100); if ((left && left.id === 'NaN') || (right && right.id === 'NaN')) { warning("Use the isNaN function to compare with NaN.", this); } else if (f) { f.apply(this, [left, right]); } if (left.id === '!') { warning("Confusing use of '{a}'.", left, '!'); } if (right.id === '!') { warning("Confusing use of '{a}'.", left, '!'); } this.left = left; this.right = right; return this; }; return x; } function isPoorRelation(node) { return node && ((node.type === '(number)' && +node.value === 0) || (node.type === '(string)' && node.value === '') || node.type === 'true' || node.type === 'false' || node.type === 'undefined' || node.type === 'null'); } function assignop(s, f) { symbol(s, 20).exps = true; return infix(s, function (left, that) { var l; that.left = left; if (predefined[left.value] === false && scope[left.value]['(global)'] === true) { warning("Read only.", left); } else if (left['function']) { warning("'{a}' is a function.", left, left.value); } if (option.safe) { l = left; do { if (typeof predefined[l.value] === 'boolean') { warning('ADsafe violation.', l); } l = l.left; } while (l); } if (left) { if (left.id === '.' || left.id === '[') { if (!left.left || left.left.value === 'arguments') { warning('Bad assignment.', that); } that.right = parse(19); return that; } else if (left.identifier && !left.reserved) { if (funct[left.value] === 'exception') { warning("Do not assign to the exception parameter.", left); } that.right = parse(19); return that; } if (left === syntax['function']) { warning( "Expected an identifier in an assignment and instead saw a function invocation.", token); } } error("Bad assignment.", that); }, 20); } function bitwise(s, f, p) { var x = symbol(s, p); reserveName(x); x.led = (typeof f === 'function') ? f : function (left) { if (option.bitwise) { warning("Unexpected use of '{a}'.", this, this.id); } this.left = left; this.right = parse(p); return this; }; return x; } function bitwiseassignop(s) { symbol(s, 20).exps = true; return infix(s, function (left, that) { if (option.bitwise) { warning("Unexpected use of '{a}'.", that, that.id); } nonadjacent(prevtoken, token); nonadjacent(token, nexttoken); if (left) { if (left.id === '.' || left.id === '[' || (left.identifier && !left.reserved)) { parse(19); return that; } if (left === syntax['function']) { warning( "Expected an identifier in an assignment, and instead saw a function invocation.", token); } return that; } error("Bad assignment.", that); }, 20); } function suffix(s, f) { var x = symbol(s, 150); x.led = function (left) { if (option.plusplus) { warning("Unexpected use of '{a}'.", this, this.id); } else if ((!left.identifier || left.reserved) && left.id !== '.' && left.id !== '[') { warning("Bad operand.", this); } this.left = left; return this; }; return x; } function optionalidentifier() { if (nexttoken.identifier) { advance(); if (option.safe && banned[token.value]) { warning("ADsafe violation: '{a}'.", token, token.value); } else if (token.reserved && !option.es5) { warning("Expected an identifier and instead saw '{a}' (a reserved word).", token, token.id); } return token.value; } } function identifier() { var i = optionalidentifier(); if (i) { return i; } if (token.id === 'function' && nexttoken.id === '(') { warning("Missing name in function statement."); } else { error("Expected an identifier and instead saw '{a}'.", nexttoken, nexttoken.value); } } function reachable(s) { var i = 0, t; if (nexttoken.id !== ';' || noreach) { return; } for (;;) { t = peek(i); if (t.reach) { return; } if (t.id !== '(endline)') { if (t.id === 'function') { warning( "Inner functions should be listed at the top of the outer function.", t); break; } warning("Unreachable '{a}' after '{b}'.", t, t.value, s); break; } i += 1; } } function statement(noindent) { var i = indent, r, s = scope, t = nexttoken; // We don't like the empty statement. if (t.id === ';') { warning("Unnecessary semicolon.", t); advance(';'); return; } // Is this a labelled statement? if (t.identifier && !t.reserved && peek().id === ':') { advance(); advance(':'); scope = Object.create(s); addlabel(t.value, 'label'); if (!nexttoken.labelled) { warning("Label '{a}' on {b} statement.", nexttoken, t.value, nexttoken.value); } if (jx.test(t.value + ':')) { warning("Label '{a}' looks like a javascript url.", t, t.value); } nexttoken.label = t.value; t = nexttoken; } // Parse the statement. if (!noindent) { indentation(); } r = parse(0, true); // Look for the final semicolon. if (!t.block) { if (!r || !r.exps) { warning( "Expected an assignment or function call and instead saw an expression.", token); } else if (r.id === '(' && r.left.id === 'new') { warning("Do not use 'new' for side effects."); } if (nexttoken.id !== ';') { warningAt("Missing semicolon.", token.line, token.from + token.value.length); } else { adjacent(token, nexttoken); advance(';'); nonadjacent(token, nexttoken); } } // Restore the indentation. indent = i; scope = s; return r; } function use_strict() { if (nexttoken.value === 'use strict') { advance(); advance(';'); strict_mode = true; option.newcap = true; option.undef = true; return true; } else { return false; } } function statements(begin) { var a = [], f, p; if (begin && !use_strict() && option.strict) { warning('Missing "use strict" statement.', nexttoken); } if (option.adsafe) { switch (begin) { case 'script': if (!adsafe_may) { if (nexttoken.value !== 'ADSAFE' || peek(0).id !== '.' || (peek(1).value !== 'id' && peek(1).value !== 'go')) { error('ADsafe violation: Missing ADSAFE.id or ADSAFE.go.', nexttoken); } } if (nexttoken.value === 'ADSAFE' && peek(0).id === '.' && peek(1).value === 'id') { if (adsafe_may) { error('ADsafe violation.', nexttoken); } advance('ADSAFE'); advance('.'); advance('id'); advance('('); if (nexttoken.value !== adsafe_id) { error('ADsafe violation: id does not match.', nexttoken); } advance('(string)'); advance(')'); advance(';'); adsafe_may = true; } break; case 'lib': if (nexttoken.value === 'ADSAFE') { advance('ADSAFE'); advance('.'); advance('lib'); advance('('); advance('(string)'); comma(); f = parse(0); if (f.id !== 'function') { error('The second argument to lib must be a function.', f); } p = f.funct['(params)']; p = p && p.join(', '); if (p && p !== 'lib') { error("Expected '{a}' and instead saw '{b}'.", f, '(lib)', '(' + p + ')'); } advance(')'); advance(';'); return a; } else { error("ADsafe lib violation."); } } } while (!nexttoken.reach && nexttoken.id !== '(end)') { if (nexttoken.id === ';') { warning("Unnecessary semicolon."); advance(';'); } else { a.push(statement()); } } return a; } function block(f) { var a, b = inblock, old_indent = indent, s = scope, t; inblock = f; scope = Object.create(scope); nonadjacent(token, nexttoken); t = nexttoken; if (nexttoken.id === '{') { advance('{'); if (nexttoken.id !== '}' || token.line !== nexttoken.line) { indent += option.indent; while (!f && nexttoken.from > indent) { indent += option.indent; } if (!f) { use_strict(); } a = statements(); indent -= option.indent; indentation(); } advance('}', t); indent = old_indent; } else { warning("Expected '{a}' and instead saw '{b}'.", nexttoken, '{', nexttoken.value); noreach = true; a = [statement()]; noreach = false; } funct['(verb)'] = null; scope = s; inblock = b; if (f && (!a || a.length === 0)) { warning("Empty block."); } return a; } function countMember(m) { if (membersOnly && typeof membersOnly[m] !== 'boolean') { warning("Unexpected /*member '{a}'.", token, m); } if (typeof member[m] === 'number') { member[m] += 1; } else { member[m] = 1; } } function note_implied(token) { var name = token.value, line = token.line, a = implied[name]; if (typeof a === 'function') { a = false; } if (!a) { a = [line]; implied[name] = a; } else if (a[a.length - 1] !== line) { a.push(line); } } // CSS parsing. function cssName() { if (nexttoken.identifier) { advance(); return true; } } function cssNumber() { if (nexttoken.id === '-') { advance('-'); adjacent(); nolinebreak(); } if (nexttoken.type === '(number)') { advance('(number)'); return true; } } function cssString() { if (nexttoken.type === '(string)') { advance(); return true; } } function cssColor() { var i, number, value; if (nexttoken.identifier) { value = nexttoken.value; if (value === 'rgb' || value === 'rgba') { advance(); advance('('); for (i = 0; i < 3; i += 1) { if (i) { advance(','); } number = nexttoken.value; if (nexttoken.type !== '(number)' || number < 0) { warning("Expected a positive number and instead saw '{a}'", nexttoken, number); advance(); } else { advance(); if (nexttoken.id === '%') { advance('%'); if (number > 100) { warning("Expected a percentage and instead saw '{a}'", token, number); } } else { if (number > 255) { warning("Expected a small number and instead saw '{a}'", token, number); } } } } if (value === 'rgba') { advance(','); number = +nexttoken.value; if (nexttoken.type !== '(number)' || number < 0 || number > 1) { warning("Expected a number between 0 and 1 and instead saw '{a}'", nexttoken, number); } advance(); if (nexttoken.id === '%') { warning("Unexpected '%'."); advance('%'); } } advance(')'); return true; } else if (cssColorData[nexttoken.value] === true) { advance(); return true; } } else if (nexttoken.type === '(color)') { advance(); return true; } return false; } function cssLength() { if (nexttoken.id === '-') { advance('-'); adjacent(); nolinebreak(); } if (nexttoken.type === '(number)') { advance(); if (nexttoken.type !== '(string)' && cssLengthData[nexttoken.value] === true) { adjacent(); advance(); } else if (+token.value !== 0) { warning("Expected a linear unit and instead saw '{a}'.", nexttoken, nexttoken.value); } return true; } return false; } function cssLineHeight() { if (nexttoken.id === '-') { advance('-'); adjacent(); } if (nexttoken.type === '(number)') { advance(); if (nexttoken.type !== '(string)' && cssLengthData[nexttoken.value] === true) { adjacent(); advance(); } return true; } return false; } function cssWidth() { if (nexttoken.identifier) { switch (nexttoken.value) { case 'thin': case 'medium': case 'thick': advance(); return true; } } else { return cssLength(); } } function cssMargin() { if (nexttoken.identifier) { if (nexttoken.value === 'auto') { advance(); return true; } } else { return cssLength(); } } function cssAttr() { if (nexttoken.identifier && nexttoken.value === 'attr') { advance(); advance('('); if (!nexttoken.identifier) { warning("Expected a name and instead saw '{a}'.", nexttoken, nexttoken.value); } advance(); advance(')'); return true; } return false; } function cssCommaList() { while (nexttoken.id !== ';') { if (!cssName() && !cssString()) { warning("Expected a name and instead saw '{a}'.", nexttoken, nexttoken.value); } if (nexttoken.id !== ',') { return true; } comma(); } } function cssCounter() { if (nexttoken.identifier && nexttoken.value === 'counter') { advance(); advance('('); advance(); if (nexttoken.id === ',') { comma(); if (nexttoken.type !== '(string)') { warning("Expected a string and instead saw '{a}'.", nexttoken, nexttoken.value); } advance(); } advance(')'); return true; } if (nexttoken.identifier && nexttoken.value === 'counters') { advance(); advance('('); if (!nexttoken.identifier) { warning("Expected a name and instead saw '{a}'.", nexttoken, nexttoken.value); } advance(); if (nexttoken.id === ',') { comma(); if (nexttoken.type !== '(string)') { warning("Expected a string and instead saw '{a}'.", nexttoken, nexttoken.value); } advance(); } if (nexttoken.id === ',') { comma(); if (nexttoken.type !== '(string)') { warning("Expected a string and instead saw '{a}'.", nexttoken, nexttoken.value); } advance(); } advance(')'); return true; } return false; } function cssShape() { var i; if (nexttoken.identifier && nexttoken.value === 'rect') { advance(); advance('('); for (i = 0; i < 4; i += 1) { if (!cssLength()) { warning("Expected a number and instead saw '{a}'.", nexttoken, nexttoken.value); break; } } advance(')'); return true; } return false; } function cssUrl() { var c, url; if (nexttoken.identifier && nexttoken.value === 'url') { nexttoken = lex.range('(', ')'); url = nexttoken.value; c = url.charAt(0); if (c === '"' || c === '\'') { if (url.slice(-1) !== c) { warning("Bad url string."); } else { url = url.slice(1, -1); if (url.indexOf(c) >= 0) { warning("Bad url string."); } } } if (!url) { warning("Missing url."); } advance(); if (option.safe && ux.test(url)) { error("ADsafe URL violation."); } urls.push(url); return true; } return false; } cssAny = [cssUrl, function () { for (;;) { if (nexttoken.identifier) { switch (nexttoken.value.toLowerCase()) { case 'url': cssUrl(); break; case 'expression': warning("Unexpected expression '{a}'.", nexttoken, nexttoken.value); advance(); break; default: advance(); } } else { if (nexttoken.id === ';' || nexttoken.id === '!' || nexttoken.id === '(end)' || nexttoken.id === '}') { return true; } advance(); } } }]; cssBorderStyle = [ 'none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'ridge', 'inset', 'outset' ]; cssBreak = [ 'auto', 'always', 'avoid', 'left', 'right' ]; cssOverflow = [ 'auto', 'hidden', 'scroll', 'visible' ]; cssAttributeData = { background: [ true, 'background-attachment', 'background-color', 'background-image', 'background-position', 'background-repeat' ], 'background-attachment': ['scroll', 'fixed'], 'background-color': ['transparent', cssColor], 'background-image': ['none', cssUrl], 'background-position': [ 2, [cssLength, 'top', 'bottom', 'left', 'right', 'center'] ], 'background-repeat': [ 'repeat', 'repeat-x', 'repeat-y', 'no-repeat' ], 'border': [true, 'border-color', 'border-style', 'border-width'], 'border-bottom': [ true, 'border-bottom-color', 'border-bottom-style', 'border-bottom-width' ], 'border-bottom-color': cssColor, 'border-bottom-style': cssBorderStyle, 'border-bottom-width': cssWidth, 'border-collapse': ['collapse', 'separate'], 'border-color': ['transparent', 4, cssColor], 'border-left': [ true, 'border-left-color', 'border-left-style', 'border-left-width' ], 'border-left-color': cssColor, 'border-left-style': cssBorderStyle, 'border-left-width': cssWidth, 'border-right': [ true, 'border-right-color', 'border-right-style', 'border-right-width' ], 'border-right-color': cssColor, 'border-right-style': cssBorderStyle, 'border-right-width': cssWidth, 'border-spacing': [2, cssLength], 'border-style': [4, cssBorderStyle], 'border-top': [ true, 'border-top-color', 'border-top-style', 'border-top-width' ], 'border-top-color': cssColor, 'border-top-style': cssBorderStyle, 'border-top-width': cssWidth, 'border-width': [4, cssWidth], bottom: [cssLength, 'auto'], 'caption-side' : ['bottom', 'left', 'right', 'top'], clear: ['both', 'left', 'none', 'right'], clip: [cssShape, 'auto'], color: cssColor, content: [ 'open-quote', 'close-quote', 'no-open-quote', 'no-close-quote', cssString, cssUrl, cssCounter, cssAttr ], 'counter-increment': [ cssName, 'none' ], 'counter-reset': [ cssName, 'none' ], cursor: [ cssUrl, 'auto', 'crosshair', 'default', 'e-resize', 'help', 'move', 'n-resize', 'ne-resize', 'nw-resize', 'pointer', 's-resize', 'se-resize', 'sw-resize', 'w-resize', 'text', 'wait' ], direction: ['ltr', 'rtl'], display: [ 'block', 'compact', 'inline', 'inline-block', 'inline-table', 'list-item', 'marker', 'none', 'run-in', 'table', 'table-caption', 'table-cell', 'table-column', 'table-column-group', 'table-footer-group', 'table-header-group', 'table-row', 'table-row-group' ], 'empty-cells': ['show', 'hide'], 'float': ['left', 'none', 'right'], font: [ 'caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar', true, 'font-size', 'font-style', 'font-weight', 'font-family' ], 'font-family': cssCommaList, 'font-size': [ 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'larger', 'smaller', cssLength ], 'font-size-adjust': ['none', cssNumber], 'font-stretch': [ 'normal', 'wider', 'narrower', 'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', 'semi-expanded', 'expanded', 'extra-expanded' ], 'font-style': [ 'normal', 'italic', 'oblique' ], 'font-variant': [ 'normal', 'small-caps' ], 'font-weight': [ 'normal', 'bold', 'bolder', 'lighter', cssNumber ], height: [cssLength, 'auto'], left: [cssLength, 'auto'], 'letter-spacing': ['normal', cssLength], 'line-height': ['normal', cssLineHeight], 'list-style': [ true, 'list-style-image', 'list-style-position', 'list-style-type' ], 'list-style-image': ['none', cssUrl], 'list-style-position': ['inside', 'outside'], 'list-style-type': [ 'circle', 'disc', 'square', 'decimal', 'decimal-leading-zero', 'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha', 'upper-latin', 'hebrew', 'katakana', 'hiragana-iroha', 'katakana-oroha', 'none' ], margin: [4, cssMargin], 'margin-bottom': cssMargin, 'margin-left': cssMargin, 'margin-right': cssMargin, 'margin-top': cssMargin, 'marker-offset': [cssLength, 'auto'], 'max-height': [cssLength, 'none'], 'max-width': [cssLength, 'none'], 'min-height': cssLength, 'min-width': cssLength, opacity: cssNumber, outline: [true, 'outline-color', 'outline-style', 'outline-width'], 'outline-color': ['invert', cssColor], 'outline-style': [ 'dashed', 'dotted', 'double', 'groove', 'inset', 'none', 'outset', 'ridge', 'solid' ], 'outline-width': cssWidth, overflow: cssOverflow, 'overflow-x': cssOverflow, 'overflow-y': cssOverflow, padding: [4, cssLength], 'padding-bottom': cssLength, 'padding-left': cssLength, 'padding-right': cssLength, 'padding-top': cssLength, 'page-break-after': cssBreak, 'page-break-before': cssBreak, position: ['absolute', 'fixed', 'relative', 'static'], quotes: [8, cssString], right: [cssLength, 'auto'], 'table-layout': ['auto', 'fixed'], 'text-align': ['center', 'justify', 'left', 'right'], 'text-decoration': [ 'none', 'underline', 'overline', 'line-through', 'blink' ], 'text-indent': cssLength, 'text-shadow': ['none', 4, [cssColor, cssLength]], 'text-transform': ['capitalize', 'uppercase', 'lowercase', 'none'], top: [cssLength, 'auto'], 'unicode-bidi': ['normal', 'embed', 'bidi-override'], 'vertical-align': [ 'baseline', 'bottom', 'sub', 'super', 'top', 'text-top', 'middle', 'text-bottom', cssLength ], visibility: ['visible', 'hidden', 'collapse'], 'white-space': [ 'normal', 'nowrap', 'pre', 'pre-line', 'pre-wrap', 'inherit' ], width: [cssLength, 'auto'], 'word-spacing': ['normal', cssLength], 'word-wrap': ['break-word', 'normal'], 'z-index': ['auto', cssNumber] }; function styleAttribute() { var v; while (nexttoken.id === '*' || nexttoken.id === '#' || nexttoken.value === '_') { if (!option.css) { warning("Unexpected '{a}'.", nexttoken, nexttoken.value); } advance(); } if (nexttoken.id === '-') { if (!option.css) { warning("Unexpected '{a}'.", nexttoken, nexttoken.value); } advance('-'); if (!nexttoken.identifier) { warning( "Expected a non-standard style attribute and instead saw '{a}'.", nexttoken, nexttoken.value); } advance(); return cssAny; } else { if (!nexttoken.identifier) { warning("Excepted a style attribute, and instead saw '{a}'.", nexttoken, nexttoken.value); } else { if (is_own(cssAttributeData, nexttoken.value)) { v = cssAttributeData[nexttoken.value]; } else { v = cssAny; if (!option.css) { warning("Unrecognized style attribute '{a}'.", nexttoken, nexttoken.value); } } } advance(); return v; } } function styleValue(v) { var i = 0, n, once, match, round, start = 0, vi; switch (typeof v) { case 'function': return v(); case 'string': if (nexttoken.identifier && nexttoken.value === v) { advance(); return true; } return false; } for (;;) { if (i >= v.length) { return false; } vi = v[i]; i += 1; if (vi === true) { break; } else if (typeof vi === 'number') { n = vi; vi = v[i]; i += 1; } else { n = 1; } match = false; while (n > 0) { if (styleValue(vi)) { match = true; n -= 1; } else { break; } } if (match) { return true; } } start = i; once = []; for (;;) { round = false; for (i = start; i < v.length; i += 1) { if (!once[i]) { if (styleValue(cssAttributeData[v[i]])) { match = true; round = true; once[i] = true; break; } } } if (!round) { return match; } } } function styleChild() { if (nexttoken.id === '(number)') { advance(); if (nexttoken.value === 'n' && nexttoken.identifier) { adjacent(); advance(); if (nexttoken.id === '+') { adjacent(); advance('+'); adjacent(); advance('(number)'); } } return; } else { switch (nexttoken.value) { case 'odd': case 'even': if (nexttoken.identifier) { advance(); return; } } } warning("Unexpected token '{a}'.", nexttoken, nexttoken.value); } function substyle() { var v; for (;;) { if (nexttoken.id === '}' || nexttoken.id === '(end)' || xquote && nexttoken.id === xquote) { return; } while (nexttoken.id === ';') { warning("Misplaced ';'."); advance(';'); } v = styleAttribute(); advance(':'); if (nexttoken.identifier && nexttoken.value === 'inherit') { advance(); } else { if (!styleValue(v)) { warning("Unexpected token '{a}'.", nexttoken, nexttoken.value); advance(); } } if (nexttoken.id === '!') { advance('!'); adjacent(); if (nexttoken.identifier && nexttoken.value === 'important') { advance(); } else { warning("Expected '{a}' and instead saw '{b}'.", nexttoken, 'important', nexttoken.value); } } if (nexttoken.id === '}' || nexttoken.id === xquote) { warning("Missing '{a}'.", nexttoken, ';'); } else { advance(';'); } } } function styleSelector() { if (nexttoken.identifier) { if (!is_own(htmltag, option.cap ? nexttoken.value.toLowerCase() : nexttoken.value)) { warning("Expected a tagName, and instead saw {a}.", nexttoken, nexttoken.value); } advance(); } else { switch (nexttoken.id) { case '>': case '+': advance(); styleSelector(); break; case ':': advance(':'); switch (nexttoken.value) { case 'active': case 'after': case 'before': case 'checked': case 'disabled': case 'empty': case 'enabled': case 'first-child': case 'first-letter': case 'first-line': case 'first-of-type': case 'focus': case 'hover': case 'last-child': case 'last-of-type': case 'link': case 'only-of-type': case 'root': case 'target': case 'visited': advance(); break; case 'lang': advance(); advance('('); if (!nexttoken.identifier) { warning("Expected a lang code, and instead saw :{a}.", nexttoken, nexttoken.value); } advance(')'); break; case 'nth-child': case 'nth-last-child': case 'nth-last-of-type': case 'nth-of-type': advance(); advance('('); styleChild(); advance(')'); break; case 'not': advance(); advance('('); if (nexttoken.id === ':' && peek(0).value === 'not') { warning("Nested not."); } styleSelector(); advance(')'); break; default: warning("Expected a pseudo, and instead saw :{a}.", nexttoken, nexttoken.value); } break; case '#': advance('#'); if (!nexttoken.identifier) { warning("Expected an id, and instead saw #{a}.", nexttoken, nexttoken.value); } advance(); break; case '*': advance('*'); break; case '.': advance('.'); if (!nexttoken.identifier) { warning("Expected a class, and instead saw #.{a}.", nexttoken, nexttoken.value); } advance(); break; case '[': advance('['); if (!nexttoken.identifier) { warning("Expected an attribute, and instead saw [{a}].", nexttoken, nexttoken.value); } advance(); if (nexttoken.id === '=' || nexttoken.value === '~=' || nexttoken.value === '$=' || nexttoken.value === '|=' || nexttoken.id === '*=' || nexttoken.id === '^=') { advance(); if (nexttoken.type !== '(string)') { warning("Expected a string, and instead saw {a}.", nexttoken, nexttoken.value); } advance(); } advance(']'); break; default: error("Expected a CSS selector, and instead saw {a}.", nexttoken, nexttoken.value); } } } function stylePattern() { var name; if (nexttoken.id === '{') { warning("Expected a style pattern, and instead saw '{a}'.", nexttoken, nexttoken.id); } else if (nexttoken.id === '@') { advance('@'); name = nexttoken.value; if (nexttoken.identifier && atrule[name] === true) { advance(); return name; } warning("Expected an at-rule, and instead saw @{a}.", nexttoken, name); } for (;;) { styleSelector(); if (nexttoken.id === '' || nexttoken.id === '{' || nexttoken.id === '(end)') { return ''; } if (nexttoken.id === ',') { comma(); } } } function styles() { var i; while (nexttoken.id === '@') { i = peek(); if (i.identifier && i.value === 'import') { advance('@'); advance(); if (!cssUrl()) { warning("Expected '{a}' and instead saw '{b}'.", nexttoken, 'url', nexttoken.value); advance(); } advance(';'); } else { break; } } while (nexttoken.id !== '' && nexttoken.id !== '(end)') { stylePattern(); xmode = 'styleproperty'; if (nexttoken.id === ';') { advance(';'); } else { advance('{'); substyle(); xmode = 'style'; advance('}'); } } } // HTML parsing. function doBegin(n) { if (n !== 'html' && !option.fragment) { if (n === 'div' && option.adsafe) { error("ADSAFE: Use the fragment option."); } else { error("Expected '{a}' and instead saw '{b}'.", token, 'html', n); } } if (option.adsafe) { if (n === 'html') { error( "Currently, ADsafe does not operate on whole HTML documents. It operates onProblem' + (isFinite(c.line) ? ' at line ' + c.line + ' character ' + c.character : '') + ': ' + c.reason.entityify() + '
' + (e && (e.length > 80 ? e.slice(0, 77) + '...' : e).entityify()) + '
'); } } } if (data.implieds) { s = []; for (i = 0; i < data.implieds.length; i += 1) { s[i] = '' + data.implieds[i].name + ' ' +
data.implieds[i].line + '';
}
o.push('Implied global: ' + s.join(', ') + '
'); } if (data.unused) { s = []; for (i = 0; i < data.unused.length; i += 1) { s[i] = '' + data.unused[i].name + ' ' +
data.unused[i].line + ' ' +
data.unused[i]['function'] + '';
}
o.push('Unused variable: ' + s.join(', ') + '
'); } if (data.json) { o.push('JSON: bad.
'); } o.push('CSS.
'); } else if (data.json && !err) { o.push('JSON: good.
'); } else if (data.globals) { o.push('/*members ';
l = 10;
for (i = 0; i < a.length; i += 1) {
k = a[i];
n = k.name();
if (l + n.length > 72) {
o.push(m + '
');
m = ' ';
l = 1;
}
l += n.length + 2;
if (data.member[k] === 1) {
n = '' + n + '';
}
if (i < a.length - 1) {
n += ', ';
}
m += n;
}
o.push(m + '
*/');
}
o.push('| Expected: | ' + expected + ' |
|---|---|
| Result: | ' + actual + ' |
| Diff: | ' + QUnit.diff(expected, actual) +' |
| Source: | ' + escapeInnerText(source) + ' |
A simple JavaScript API for producing an accurate, intuitive description of the timespan between two Date instances.
While seemingly a trivial problem, the human descriptions for a span of time tend to be fuzzier than a computer naturally computes. More specifically, months are an inherently messed up unit of time. For instance, when a human says "in 1 month" how long do they mean? Banks often interpret this as thirty days but that is only correct one third of the time. People casually talk about a month being four weeks long but there is only one month in a year which is four weeks long and it is only that long about three quarters of the time. Even intuitively defining these terms can be problematic. For instance, what is the date one month after January 31st, 2001? JavaScript will happily call this March 3rd, 2001. Humans will typically debate either February 28th, 2001 or March 1st, 2001. It seems there isn't a "right" answer, per se.
Countdown.js emphasizes producing intuitively correct description of timespans which are consistent as time goes on. To do this, Countdown.js uses the concept of "today's date next month" to mean "a month from now". As the days go by, Countdown.js produces consecutively increasing or decreasing counts without inconsistent jumps. The range of accuracy is only limited by the underlying system clock.
Countdown.js approaches finding the difference between two times like an elementary school subtraction problem. Each unit acts like a base-10 place where any overflow is carried to the next highest unit, and any underflow is borrowed from the next highest unit. In base-10 subtraction, every column is worth 10 times the previous column. With time, it is a little more complex since the conversions between the units of time are not the same and months are an inconsistent number of days. Internally, Countdown.js maintains the concept of a "reference month" which determines how many days a given month or year represents. In the final step of the algorithm, Countdown.js then prunes the set of time units down to only those requested, forcing larger units down to smaller.
As of v2.4, Countdown.js performs all calculations with respect to the viewer's local time zone. Earlier versions performed difference calculations in UTC, which is generally the correct way to do math on time. In this situation, however, an issue with using UTC happens when either of the two dates being worked with is within one time zone offset of a month boundary. If the UTC interpretation of that instant in time is in a different month than that of the local time zone, then the viewer's perception is that the calculated time span is incorrect. This is the heart of the problem that Countdown.js attempts to solve: talking about spans of time can be ambiguous. Nearly all bugs reported for Countdown.js have been because the viewer expects something different due to their particular time zone.
JavaScript (ECMA-262) only works with dates as UTC or the local time zone, not arbitrary time zones. By design, all JS Date objects represent an instant in time (milliseconds since midnight Jan 1, 1970 in UTC) interpreted as the user's local time. Since most humans think about local time not UTC, it the most makes sense to perform this time span algorithm in reference to local time.
Daylight Savings Time further complicates things, creating hours which get repeated and hours which cannot exist. Countdown.js effectively ignores these edge cases and talks about time preferring human intuition about time over surprise exactness. Example: A viewer asks for the description from noon the day before a daylight savings begins to noon the day after. A computer would answer "23 hours" whereas a human would confidently answer "1 day" even after being reminded to "Spring Forward". The computer is technically more accurate but this is not the value that humans actually expect or desire. Humans pretend that time is simple and makes sense. Unfortunately, humans made time far more complex than it needed to be with time zones and daylight savings. UTC simplifies time but at the cost of being inconsistent with human experience.
A simple but flexible API is the goal of Countdown.js. There is one global function with a set of static constants:
var timespan = countdown(start|callback, end|callback, units, max, digits);
The parameters are a starting Date, ending Date, an optional set of units, an optional maximum number of units, and an optional maximum number of decimal places on the smallest unit. units defaults to countdown.DEFAULTS, max defaults to NaN (all specified units), digits defaults to 0.
countdown.ALL =
countdown.MILLENNIA |
countdown.CENTURIES |
countdown.DECADES |
countdown.YEARS |
countdown.MONTHS |
countdown.WEEKS |
countdown.DAYS |
countdown.HOURS |
countdown.MINUTES |
countdown.SECONDS |
countdown.MILLISECONDS;
countdown.DEFAULTS =
countdown.YEARS |
countdown.MONTHS |
countdown.DAYS |
countdown.HOURS |
countdown.MINUTES |
countdown.SECONDS;
This allows a very minimal call to accept the defaults and get the time since/until a single date. For example:
countdown( new Date(2000, 0, 1) ).toString();
This will produce a human readable description like:
11 years, 8 months, 4 days, 10 hours, 12 minutes and 43 seconds
start / end argumentsThe parameters start and end can be one of several values:
null which indicates "now".Date object.number specifying the number of milliseconds since midnight Jan 1, 1970 UTC (i.e., the "UNIX epoch").function accepting one timespan argument.To reference a specific instant in time, either use a number offset from the epoch, or a JavaScript Date object instantiated with the specific offset from the epoch.
In JavaScript, if a Date object is instantiated using year/month/date/etc values, then those values are interpreted in reference to the browser's local time zone and daylight savings settings.
If start and end are both specified, then repeated calls to countdown(…) will always return the same result.
If one date argument is left null while the other is provided, then repeated calls will count up if the provided date is in the past, and it will count down if the provided date is in the future.
For example,
var daysSinceLastWorkplaceAccident = countdown(507314280000, null, countdown.DAYS);
If a callback function is supplied, then an interval timer will be started with a frequency based upon the smallest unit (e.g., if countdown.SECONDS is the smallest unit, the callback will be invoked once per second). Rather than returning a Timespan object, the timer's ID will be returned to allow canceling by passing into window.clearInterval(id). For example, to show a timer since the page first loaded:
var timerId =
countdown(
new Date(),
function(ts) {
document.getElementById('pageTimer').innerHTML = ts.toHTML("strong");
},
countdown.HOURS|countdown.MINUTES|countdown.SECONDS);
// later on this timer may be stopped
window.clearInterval(timerId);
units argumentThe static units constants can be combined using standard bitwise operators. For example, to explicitly include "months or days" use bitwise-OR:
countdown.MONTHS | countdown.DAYS
To explicitly exclude units like "not weeks and not milliseconds" combine bitwise-NOT and bitwise-AND:
~countdown.WEEKS & ~countdown.MILLISECONDS
Equivalently, to specify everything but "not weeks or milliseconds" wrap bitwise-NOT around bitwise-OR:
~(countdown.WEEKS | countdown.MILLISECONDS)
max argumentThe next optional argument max specifies a maximum number of unit labels to display. This allows specifying which units are interesting but only displaying the max most significant units.
countdown(start, end, units).toString() => "5 years, 1 month, 19 days, 12 hours and 17 minutes"
Specifying max as 2 ensures that only the two most significant units are displayed (note the rounding of the least significant unit):
countdown(start, end, units, 2).toString() => "5 years and 2 months"
Negative or zero values of max are ignored.
Previously, the max number of unit labels argument used to be specified when formatting in timespan.toString(…) and timespan.toHTML(…). v2.3.0 moves it to countdown(…), which improves efficiency as well as enabling fractional units (see below).
digits argumentThe final optional argument digits allows fractional values on the smallest unit.
countdown(start, end, units, max).toString() => "5 years and 2 months"
Specifying digits as 2 allows up to 2 digits beyond the decimal point to be displayed (note the rounding of the least significant unit):
countdown(start, end, units, max, 2).toString() => "5 years and 1.65 months"
digits must be between 0 and 20, inclusive.
With the calculations of fractional units in v2.3.0, the smallest displayed unit now properly rounds. Previously, the equivalent of "1.99 years" would be truncated to "1 year", as of v2.3.0 it will display as "2 years".
Typically, this is the intended interpretation but there are a few circumstances where people expect the truncated behavior. For example, people often talk about their age as the lowest possible interpretation. e.g., they claim "39-years-old" right up until the morning of their 40th birthday (some people do even for years after!). In these cases, after calling countdown(start,end,units,max,20) with the largest possible number of digits, you might want to set ts.years = Math.floor(ts.years) before calling ts.toString(). The vain might want you to set ts.years = Math.min(ts.years, 39)!
The return value is a Timespan object which always contains the following fields:
Date start: the starting date object used for the calculationDate end: the ending date object used for the calculationNumber units: the units specifiedNumber value: total milliseconds difference (i.e., end - start). If end < start then value will be negative.Typically the end occurs after start, but if the arguments were reversed, the only difference is Timespan.value will be negative. The sign of value can be used to determine if the event occurs in the future or in the past.
The following time unit fields are only present if their corresponding units were requested:
Number millenniaNumber centuriesNumber decadesNumber yearsNumber monthsNumber daysNumber hoursNumber minutesNumber secondsNumber millisecondsFinally, Timespan has two formatting methods each with some optional parameters. If the difference between start and end is less than the requested granularity of units, then toString(…) and toHTML(…) will return the empty label (defaults to an empty string).
String toString(emptyLabel): formats the Timespan object as an English sentence. e.g., using the same input:
ts.toString() => "5 years, 1 month, 19 days, 12 hours and 17 minutes"String toHTML(tagName, emptyLabel): formats the Timespan object as an English sentence, with the specified HTML tag wrapped around each unit. If no tag name is provided, "span" is used. e.g., using the same input:
ts.toHTML() => "<span>5 years</span>, <span>1 month</span>, <span>19 days</span>, <span>12 hours</span> and <span>17 minutes</span>"
ts.toHTML("em") => "<em>5 years</em>, <em>1 month</em>, <em>19 days</em>, <em>12 hours</em> and <em>17 minutes</em>"Very basic localization is supported via the static setLabels and resetLabels methods. These change the functionality for all timespans on the page.
countdown.resetLabels();
countdown.setLabels(singular, plural, last, delim, empty, formatter);
The arguments:
singular is a pipe ('|') delimited ascending list of singular unit name overrides
plural is a pipe ('|') delimited ascending list of plural unit name overrides
last is a delimiter before the last unit (default: ' and ')
delim is a delimiter to use between all other units (default: ', ')
empty is a label to use when all units are zero (default: '')
formatter is a function which takes a number and returns a string (default uses Number.toString()),The following examples would translate the output into Brazilian Portuguese and French, respectively:
countdown.setLabels(
' milissegundo| segundo| minuto| hora| dia| semana| mês| ano| década| século| milênio',
' milissegundos| segundos| minutos| horas| dias| semanas| meses| anos| décadas| séculos| milênios',
' e ',
' + ',
'agora');
countdown.setLabels(
' milliseconde| seconde| minute| heure| jour| semaine| mois| année| décennie| siècle| millénaire',
' millisecondes| secondes| minutes| heures| jours| semaines| mois| années| décennies| siècles| millénaires',
' et ',
', ',
'maintenant');
If you only wanted to override some of the labels just leave the other pipe-delimited places empty. Similarly, leave off any of the delimiter arguments which do not need overriding.
countdown.setLabels(
'||| hr| d',
'ms| sec|||| wks|| yrs',
', and finally ');
ts.toString() => "1 millennium, 2 centuries, 5 yrs, 1 month, 7 wks, 19 days, 1 hr, 2 minutes, 17 sec, and finally 1 millisecond"
If you only wanted to override the empty label:
countdown.setLabels(
null,
null,
null,
null,
'Now.');
ts.toString() => "Now."
The following would be effectively the same as calling countdown.resetLabels():
countdown.setLabels(
' millisecond| second| minute| hour| day| week| month| year| decade| century| millennium',
' milliseconds| seconds| minutes| hours| days| weeks| months| years| decades| centuries| millennia',
' and ',
', ',
'',
function(n){ return n.toString(); });
Distributed under the terms of The MIT license.
================================================ FILE: test/fonts/SIL Open Font License 1.1.txt ================================================ This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ----------------------------------------------------------- PREAMBLE The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. DEFINITIONS "Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. "Reserved Font Name" refers to any names specified as such after the copyright statement(s). "Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). "Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. "Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. PERMISSION & CONDITIONS Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: 1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. 2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. 3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. 5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. TERMINATION This license becomes null and void if any of the above conditions are not met. DISCLAIMER THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. ================================================ FILE: test/formatTests.js ================================================ try{ module('Timespan.toString()'); test('Default empty label override', function() { var input = countdown(0, 0, countdown.ALL); countdown.setFormat({ empty: 'Now.' }); var expected = 'Now.'; var actual = input.toString(); countdown.resetFormat(); same(actual, expected, ''); }); test('Empty label override', function() { var input = countdown(0, 0, countdown.ALL); var expected = 'Now!'; var actual = input.toString('Now!'); same(actual, expected, ''); }); test('Default empty label override, overridden', function() { var input = countdown(0, 0, countdown.ALL); countdown.setFormat({ empty: 'Now.' }); var expected = 'Right now!'; var actual = input.toString('Right now!'); countdown.resetFormat(); same(actual, expected, ''); }); test('Zero', function() { var input = countdown(0, 0, countdown.ALL); var expected = ''; var actual = input.toString(); same(actual, expected, ''); }); test('1 ms', function() { var input = countdown(0, 1, countdown.ALL); var expected = '1 millisecond'; var actual = input.toString(); same(actual, expected, ''); }); test('2 ms', function() { var input = countdown(0, 2, countdown.ALL); var expected = '2 milliseconds'; var actual = input.toString(); same(actual, expected, ''); }); test('1 sec, 2 ms', function() { var input = countdown(1000, 2002, countdown.ALL); var expected = '1 second and 2 milliseconds'; var actual = input.toString(); same(actual, expected, ''); }); test('2 sec, 1 ms', function() { var input = countdown(10000, 12001, countdown.ALL); var expected = '2 seconds and 1 millisecond'; var actual = input.toString(); same(actual, expected, ''); }); test('1 day, reversed', function() { var start = new Date(1970, 0, 1, 0, 0, 0, 0); var end = new Date(1970, 0, 1, 0, 0, 0, 0); end.setDate( end.getDate()+1 ); var input = countdown(end, start, countdown.ALL); var expected = '1 day'; var actual = input.toString(); same(actual, expected, ''); }); test('15 days', function() { var start = new Date(1970, 0, 1, 0, 0, 0, 0); var end = new Date(1970, 0, 1, 0, 0, 0, 0); end.setDate( end.getDate()+15 ); var input = countdown(end, start, countdown.ALL); var expected = '2 weeks and 1 day'; var actual = input.toString(); same(actual, expected, ''); }); test('32 days', function() { var start = new Date(1970, 0, 1, 0, 0, 0, 0); var end = new Date(1970, 0, 1, 0, 0, 0, 0); end.setDate( end.getDate()+32 ); var input = countdown(end, start, countdown.ALL); var expected = '1 month and 1 day'; var actual = input.toString(); same(actual, expected, ''); }); test('Millennium, week', function() { var start = new Date(0); var end = new Date(10 * 100 * 365.25 * 24 * 60 * 60 * 1000);// millennium, week // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL); var expected = '1 millennium and 1 week'; var actual = input.toString(); same(actual, expected, ''); }); test('One of each', function() { var start = new Date(0); var end = new Date( (11 * 100) * (365.25 * 24 * 60 * 60 * 1000) + // millennium, century, week, day (365 * 24 * 60 * 60 * 1000) + // year (31 * 24 * 60 * 60 * 1000) + // month (60 * 60 * 1000) + // hour (60 * 1000) + // min 1000 + // sec 1); // ms // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL); var expected = '1 millennium, 1 century, 1 year, 1 month, 1 week, 1 day, 1 hour, 1 minute, 1 second and 1 millisecond'; var actual = input.toString(); same(actual, expected, ''); }); test('Two of each', function() { var start = new Date(0); var end = new Date( (22 * 100) * (365.25 * 24 * 60 * 60 * 1000) + // millennium, century, week, day (2 * 365 * 24 * 60 * 60 * 1000) + // year (31 + 29) * (24 * 60 * 60 * 1000) + // month (2 * 60 * 60 * 1000) + // hour (2 * 60 * 1000) + // min (2 * 1000) + // sec 2); // ms // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL); var expected = '2 millennia, 2 centuries, 2 years, 2 months, 2 weeks, 2 days, 2 hours, 2 minutes, 2 seconds and 2 milliseconds'; var actual = input.toString(); same(actual, expected, ''); }); module('Timespan.toString(number)'); test('Millennium, week; 1 max', function() { var start = new Date(0); var end = new Date(10 * 100 * 365.25 * 24 * 60 * 60 * 1000); // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL, 1); var expected = '1 millennium'; var actual = input.toString(); same(actual, expected, ''); }); test('Millennium, week; 2 max', function() { var start = new Date(0); var end = new Date(10 * 100 * 365.25 * 24 * 60 * 60 * 1000); // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL, 2); var expected = '1 millennium and 1 week'; var actual = input.toString(); same(actual, expected, ''); }); test('One of each; 3 max', function() { var start = new Date(0); var end = new Date( (11 * 100) * (365.25 * 24 * 60 * 60 * 1000) + // millennium, century, week, day (365 * 24 * 60 * 60 * 1000) + // year (31 * 24 * 60 * 60 * 1000) + // month (60 * 60 * 1000) + // hour (60 * 1000) + // min 1000 + // sec 1); // ms // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL, 3); var expected = '1 millennium, 1 century and 1 year'; var actual = input.toString(); same(actual, expected, ''); }); test('One of each; zero max', function() { var start = new Date(0); var end = new Date( (11 * 100) * (365.25 * 24 * 60 * 60 * 1000) + // millennium, century, week, day (365 * 24 * 60 * 60 * 1000) + // year (31 * 24 * 60 * 60 * 1000) + // month (60 * 60 * 1000) + // hour (60 * 1000) + // min 1000 + // sec 1); // ms // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL, 0); var expected = '1 millennium, 1 century, 1 year, 1 month, 1 week, 1 day, 1 hour, 1 minute, 1 second and 1 millisecond'; var actual = input.toString(); same(actual, expected, ''); }); test('One of each; -2 max', function() { var start = new Date(0); var end = new Date( (11 * 100) * (365.25 * 24 * 60 * 60 * 1000) + // millennium, century, week, day (365 * 24 * 60 * 60 * 1000) + // year (31 * 24 * 60 * 60 * 1000) + // month (60 * 60 * 1000) + // hour (60 * 1000) + // min 1000 + // sec 1); // ms // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL, -2); var expected = '1 millennium, 1 century, 1 year, 1 month, 1 week, 1 day, 1 hour, 1 minute, 1 second and 1 millisecond'; var actual = input.toString(); same(actual, expected, ''); }); test('Almost 2 minutes, full 3 digits', function() { var input = countdown(new Date(915220800000), new Date(915220919999), countdown.DEFAULTS, 0, 3); var expected = "1 minute and 59.999 seconds"; var actual = input.toString(); same(actual, expected, ''); }); test('Almost 2 minutes, rounded 2 digits', function() { var input = countdown(new Date(915220800000), new Date(915220919999), countdown.DEFAULTS, 0, 2); var expected = "2 minutes"; var actual = input.toString(); same(actual, expected, ''); }); module('Timespan.toHTML(tag)'); test('Default empty label override', function() { var input = countdown(0, 0, countdown.ALL); countdown.setFormat({ empty: 'Now.' }); var expected = 'Now.'; var actual = input.toHTML(); countdown.resetFormat(); same(actual, expected, 'Now.'); }); test('Empty label override', function() { var input = countdown(0, 0, countdown.ALL); var expected = 'Now!'; var actual = input.toHTML(null, 'Now!'); same(actual, expected, ''); }); test('Default empty label override, overridden', function() { var input = countdown(0, 0, countdown.ALL); countdown.setFormat({ empty: 'Now.' }); var expected = 'Right now!'; var actual = input.toHTML(null, 'Right now!'); countdown.resetFormat(); same(actual, expected, ''); }); test('Zero', function() { var input = countdown(0, 0, countdown.ALL); var expected = ''; var actual = input.toHTML(); same(actual, expected, ''); }); test('Empty label and tag override', function() { var input = countdown(0, 0, countdown.ALL); var expected = 'Now!'; var actual = input.toHTML('em', 'Now!'); same(actual, expected, ''); }); test('1 ms', function() { var start = new Date(0); var end = new Date(1); // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL); var expected = '1 millisecond'; var actual = input.toHTML(); same(actual, expected, ''); }); test('2 days, reversed', function() { var start = new Date(2 * 24 * 60 * 60 * 1000); var end = new Date(0); // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL); var expected = '2 days'; var actual = input.toHTML(); same(actual, expected, ''); }); test('8 days', function() { var start = new Date(0); var end = new Date(8 * 24 * 60 * 60 * 1000); // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL); var expected = '1 week and 1 day'; var actual = input.toHTML(); same(actual, expected, ''); }); test('70 days', function() { var start = new Date(0); var end = new Date(70 * 24 * 60 * 60 * 1000); // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL); var expected = '2 months, 1 week and 4 days'; var actual = input.toHTML(); same(actual, expected, ''); }); test('366 days, non-leap year', function() { var start = new Date(0); var end = new Date(366 * 24 * 60 * 60 * 1000); // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL); var expected = '1 year and 1 day'; var actual = input.toHTML(); same(actual, expected, ''); }); test('366 days, leap year', function() { var start = new Date(2000, 0, 1); var end = new Date(start.getTime() + 366 * 24 * 60 * 60 * 1000); // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL); var expected = '1 year'; var actual = input.toHTML(); same(actual, expected, ''); }); test('One of each', function() { var start = new Date(0); var end = new Date( (11 * 100) * (365.25 * 24 * 60 * 60 * 1000) + // millennium, century, week, day (365 * 24 * 60 * 60 * 1000) + // year (31 * 24 * 60 * 60 * 1000) + // month (60 * 60 * 1000) + // hour (60 * 1000) + // min 1000 + // sec 1); // ms // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL); var expected = '1 millennium, ' + '1 century, ' + '1 year, ' + '1 month, ' + '1 week, ' + '1 day, ' + '1 hour, ' + '1 minute, ' + '1 second and ' + '1 millisecond'; var actual = input.toHTML('em'); same(actual, expected, ''); }); test('Singular overrides', function() { var start = new Date(0); var end = new Date( (11 * 100) * (365.25 * 24 * 60 * 60 * 1000) + // millennium, century, week, day (365 * 24 * 60 * 60 * 1000) + // year (31 * 24 * 60 * 60 * 1000) + // month (60 * 60 * 1000) + // hour (60 * 1000) + // min 1000 + // sec 1); // ms // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL); countdown.setFormat({ singular: ' a| b| c| d| e| f| g| h| i| j| k' }); var expected = '1 k, ' + '1 j, ' + '1 h, ' + '1 g, ' + '1 f, ' + '1 e, ' + '1 d, ' + '1 c, ' + '1 b and ' + '1 a'; var actual = input.toHTML('em'); countdown.resetFormat(); same(actual, expected, ''); expected = '1 millennium, ' + '1 century, ' + '1 year, ' + '1 month, ' + '1 week, ' + '1 day, ' + '1 hour, ' + '1 minute, ' + '1 second and ' + '1 millisecond'; actual = input.toHTML('em'); same(actual, expected, ''); }); test('Plural overrides', function() { var start = new Date(0); var end = new Date( (22 * 100) * (365.25 * 24 * 60 * 60 * 1000) + // millennium, century, week, day (2 * 365 * 24 * 60 * 60 * 1000) + // year (31 + 29) * (24 * 60 * 60 * 1000) + // month (2 * 60 * 60 * 1000) + // hour (2 * 60 * 1000) + // min (2 * 1000) + // sec 2); // ms // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL); countdown.setFormat({ plural: ' A| B| C| D| E| F| G| H| I| J| K', last: ' & ', delim: ' & ' }); var expected = '2 K & ' + '2 J & ' + '2 H & ' + '2 G & ' + '2 F & ' + '2 E & ' + '2 D & ' + '2 C & ' + '2 B & ' + '2 A'; var actual = input.toHTML('em'); countdown.resetFormat(); same(actual, expected, ''); expected = '2 millennia, ' + '2 centuries, ' + '2 years, ' + '2 months, ' + '2 weeks, ' + '2 days, ' + '2 hours, ' + '2 minutes, ' + '2 seconds and ' + '2 milliseconds'; actual = input.toHTML('em'); same(actual, expected, ''); }); test('Partial singular overrides', function() { var start = new Date(0); var end = new Date( (11 * 100) * (365.25 * 24 * 60 * 60 * 1000) + // millennium, century, week, day (365 * 24 * 60 * 60 * 1000) + // year (31 * 24 * 60 * 60 * 1000) + // month (60 * 60 * 1000) + // hour (60 * 1000) + // min 1000 + // sec 1); // ms // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL); countdown.setFormat({ singular: ' a|| c|| e|| g|| i|| k', plural: '| B|| D|| F|| H|| J|', last: ' + finally ', delim: ' + ' }); var expected = '1 k + ' + '1 century + ' + '1 year + ' + '1 g + ' + '1 week + ' + '1 e + ' + '1 hour + ' + '1 c + ' + '1 second + finally ' + '1 a'; var actual = input.toHTML('em'); countdown.resetFormat(); same(actual, expected, ''); expected = '1 millennium, ' + '1 century, ' + '1 year, ' + '1 month, ' + '1 week, ' + '1 day, ' + '1 hour, ' + '1 minute, ' + '1 second and ' + '1 millisecond'; actual = input.toHTML('em'); same(actual, expected, ''); }); test('Partial plural overrides', function() { var start = new Date(0); var end = new Date( (22 * 100) * (365.25 * 24 * 60 * 60 * 1000) + // millennium, century, week, day (2 * 365 * 24 * 60 * 60 * 1000) + // year (31 + 29) * (24 * 60 * 60 * 1000) + // month (2 * 60 * 60 * 1000) + // hour (2 * 60 * 1000) + // min (2 * 1000) + // sec 2); // ms // account for local timezone start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); var input = countdown(start, end, countdown.ALL); countdown.setFormat({ singular: ' a|| c|| e|| g|| i|| k', plural: '| B|| D|| F|| H|| J|', last: ', ' }); var expected = '2 millennia, ' + '2 J, ' + '2 H, ' + '2 months, ' + '2 F, ' + '2 days, ' + '2 D, ' + '2 minutes, ' + '2 B, ' + '2 milliseconds'; var actual = input.toHTML('em'); countdown.resetFormat(); same(actual, expected, ''); expected = '2 millennia, ' + '2 centuries, ' + '2 years, ' + '2 months, ' + '2 weeks, ' + '2 days, ' + '2 hours, ' + '2 minutes, ' + '2 seconds and ' + '2 milliseconds'; actual = input.toHTML('em'); same(actual, expected, ''); }); test('Custom number formatter', function() { var sillyFormatter = function(num) { num = ''+num; var str = ''; for (var i=0, len=num.length; i