├── test ├── test.html └── tests.js ├── pkg ├── stocktwits-text-0.9.0.js ├── stocktwits-text-0.9.1.js └── stocktwits-text-0.9.2.js ├── README.md ├── stocktwits-text.js ├── lib ├── qunit.css └── qunit.js └── LICENSE /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

stocktwits-text-js

13 |

14 |

15 |
    16 | 17 | -------------------------------------------------------------------------------- /pkg/stocktwits-text-0.9.0.js: -------------------------------------------------------------------------------- 1 | stwt = window.stwt || {}; 2 | 3 | (function() { 4 | stwt.txt = {}; 5 | stwt.txt.regexen = {}; 6 | stwt.txt.regexen.cashtag = /(^|[\s\,\.\-\+\(]\$?|^\$)(\$([a-z1-9]{1}[a-z]{1,3}_F|(?!\d+[bmkts]{1}?(il(lion)?)?\b|[\d]+\b)[a-z0-9]{1,9}(?:[-\.]{1}[a-z]{1,2})?(?:[-\.]{1}[a-z]{1,2})?))\b(?!\$)/ig; 7 | 8 | stwt.txt.extractCashtags = function(text) { 9 | var matches = []; 10 | 11 | text.replace(stwt.txt.regexen.cashtag, function(match, prefix, cashtag) { 12 | matches.push(cashtag.slice(1)); 13 | }); 14 | 15 | return matches; 16 | } 17 | 18 | stwt.txt.autoLinkCashtags = function(text, options) { 19 | if (typeof(options) === "function") { 20 | return text.replace(stwt.txt.regexen.cashtag, function(match, before, cashtag) { 21 | return before + options.call(this, cashtag.toUpperCase(), cashtag.toUpperCase().slice(1)); 22 | }); 23 | } 24 | 25 | var html = []; 26 | var opts = options || {}; 27 | 28 | opts.urlClass = (opts.urlClass === undefined) ? "stwt-url cashtag" : opts.urlClass; 29 | opts.urlTarget = opts.urlTarget || null; 30 | opts.urlNofollow = opts.urlNofollow ? true : false; 31 | opts.url = opts.url || "http://stocktwits.com/symbol/%s"; 32 | 33 | if (opts.urlClass) { html.push("class=\"" + opts.urlClass + "\""); } 34 | if (opts.urlTarget) { html.push("target=\"" + opts.urlTarget + "\""); } 35 | if (opts.urlNofollow) { html.push("rel=\"nofollow\""); } 36 | 37 | html = (html.length > 0) ? (" " + html.join(" ") + " ") : " "; 38 | 39 | return text.replace(stwt.txt.regexen.cashtag, function(match, before, cashtag) { 40 | cashtag = cashtag.toUpperCase(); 41 | return before + "" + cashtag + ""; 42 | }); 43 | } 44 | })(); -------------------------------------------------------------------------------- /pkg/stocktwits-text-0.9.1.js: -------------------------------------------------------------------------------- 1 | stwt = window.stwt || {}; 2 | 3 | (function() { 4 | stwt.txt = {}; 5 | stwt.txt.regexen = {}; 6 | stwt.txt.regexen.cashtag = /(^|[\s\,\.\-\+\(\/\"]\$?|^\$)(\$([a-z1-9]{1}[a-z]{1,3}_F|(?!\d+[bmkts]{1}?(il(lion)?|ln|m|n)?\b|[\d]+\b)(?!\d+usd)[a-z0-9]{1,9}(?:[-\.]{1}[a-z]{1,2})?(?:[-\.]{1}[a-z]{1,2})?))\b(?!\$)/ig; 7 | 8 | stwt.txt.extractCashtags = function(text) { 9 | var matches = []; 10 | 11 | text.replace(stwt.txt.regexen.cashtag, function(match, prefix, cashtag) { 12 | matches.push(cashtag.slice(1)); 13 | }); 14 | 15 | return matches; 16 | } 17 | 18 | stwt.txt.autoLinkCashtags = function(text, options) { 19 | if (typeof(options) === "function") { 20 | return text.replace(stwt.txt.regexen.cashtag, function(match, before, cashtag) { 21 | return before + options.call(this, cashtag.toUpperCase(), cashtag.toUpperCase().slice(1)); 22 | }); 23 | } 24 | 25 | var html = []; 26 | var opts = options || {}; 27 | 28 | opts.urlClass = (opts.urlClass === undefined) ? "stwt-url cashtag" : opts.urlClass; 29 | opts.urlTarget = opts.urlTarget || null; 30 | opts.urlNofollow = opts.urlNofollow ? true : false; 31 | opts.url = opts.url || "http://stocktwits.com/symbol/%s"; 32 | 33 | if (opts.urlClass) { html.push("class=\"" + opts.urlClass + "\""); } 34 | if (opts.urlTarget) { html.push("target=\"" + opts.urlTarget + "\""); } 35 | if (opts.urlNofollow) { html.push("rel=\"nofollow\""); } 36 | 37 | html = (html.length > 0) ? (" " + html.join(" ") + " ") : " "; 38 | 39 | return text.replace(stwt.txt.regexen.cashtag, function(match, before, cashtag) { 40 | cashtag = cashtag.toUpperCase(); 41 | return before + "" + cashtag + ""; 42 | }); 43 | } 44 | })(); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stocktwits-text-js 2 | 3 | A Javascript library that provides text processing routines for StockTwits Messages. This library provides autolinking and extraction for cashtags (e.g. $GOOG). 4 | 5 | ## Cashtag Examples 6 | 7 | ### Extraction 8 | 9 | stwt.txt.extractCashtags("$FOO $BAR") 10 | → ['FOO', 'BAR'] 11 | 12 | ### Autolinking 13 | 14 | // Default 15 | stwt.txt.autoLinkCashtags("$FOO") 16 | → '$FOO' 17 | 18 | // Options 19 | stwt.txt.autoLinkCashtags("$FOO", { urlClass: 'foo bar', urlTarget: '_blank', urlNofollow: true }); 20 | → '$FOO' 21 | 22 | // URL interpolation 23 | stwt.txt.autoLinkCashtags("$FOO", { url: "http://example.com/?q=%s&c=1" }) 24 | → '$FOO' 25 | 26 | // Callback 27 | stwt.txt.autoLinkCashtags("$FOO", function(cashtag, symbol) { 28 | return "" + cashtag + "" 29 | }); 30 | → '$FOO' 31 | 32 | ### Using with jQuery 33 | 34 | var contentHtml = $('#content').html(); 35 | $('#content').html(stwt.txt.autoLinkCashtags(contentHtml)); 36 | 37 | ## Credits 38 | 39 | This library is modeled after Twitter's excellent text processing libraries. 40 | 41 | ## Reporting Bugs 42 | 43 | Please direct bug reports to the [stocktwits-text-js issue tracker on GitHub](http://github.com/stocktwits/stocktwits-text-js/issues) 44 | 45 | ## Copyright and License 46 | 47 | Copyright 2012 StockTwits, Inc. 48 | 49 | Licensed under the Apache License, Version 2.0 (the "License"); 50 | you may not use this work except in compliance with the License. 51 | You may obtain a copy of the License in the LICENSE file, or at: 52 | 53 | http://www.apache.org/licenses/LICENSE-2.0 54 | 55 | Unless required by applicable law or agreed to in writing, software 56 | distributed under the License is distributed on an "AS IS" BASIS, 57 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 58 | See the License for the specific language governing permissions and 59 | limitations under the License. 60 | -------------------------------------------------------------------------------- /stocktwits-text.js: -------------------------------------------------------------------------------- 1 | stwt = window.stwt || {}; 2 | 3 | (function() { 4 | stwt.txt = {}; 5 | stwt.txt.regexen = {}; 6 | stwt.txt.regexen.cashtag = /(^|[\s\,\.\-\+\(\/\"\']\$?|^\$)(\$([a-z1-9]{1}[a-z]{1,3}_F|(?!\d+[bmkts]{1}?(il(lion)?|ln|m|n)?\b|[\d]+\b)(?!\d+usd)[a-z0-9]{1,9}(?:[-\.]{1}[a-z]{1,2})?(?:[-\.]{1}[a-z]{1,2})?))\b(?!\$)/ig; 7 | 8 | stwt.txt.extractCashtags = function(text) { 9 | var matches = []; 10 | 11 | text.replace(stwt.txt.regexen.cashtag, function(match, prefix, cashtag) { 12 | matches.push(cashtag.slice(1)); 13 | }); 14 | 15 | return matches; 16 | } 17 | 18 | stwt.txt.autoLinkCashtags = function(text, options) { 19 | if (typeof(options) === "function") { 20 | return text.replace(stwt.txt.regexen.cashtag, function(match, before, cashtag) { 21 | return before + options.call(this, cashtag.toUpperCase(), cashtag.toUpperCase().slice(1)); 22 | }); 23 | } 24 | 25 | var html = []; 26 | var opts = options || {}; 27 | var htmlAttributes = {} 28 | for (var k in options) { 29 | if (k !== "urlClass" && k !== "urlTarget" && k !== "urlNofollow" && k !== "url") { 30 | htmlAttributes[k] = options[k]; 31 | } 32 | } 33 | 34 | var classes = []; 35 | if (htmlAttributes['class']) { 36 | classes.push(htmlAttributes['class']); 37 | } 38 | if (opts.urlClass === undefined) { 39 | classes.push("stwt-url cashtag"); 40 | } else if (opts.urlClass) { 41 | classes.push(opts.urlClass); 42 | } 43 | htmlAttributes['class'] = classes.join(" "); 44 | 45 | if (opts.urlTarget) { 46 | htmlAttributes.target = opts.urlTarget; 47 | } 48 | if (opts.urlNofollow) { 49 | htmlAttributes.rel = "nofollow"; 50 | } 51 | 52 | opts.url = opts.url || "http://stocktwits.com/symbol/%s"; 53 | htmlAttributes.href = opts.url 54 | 55 | return text.replace(stwt.txt.regexen.cashtag, function(match, before, cashtag) { 56 | cashtag = cashtag.toUpperCase(); 57 | var html = ""; 58 | var v; 59 | for (k in htmlAttributes) { 60 | if (v = htmlAttributes[k]) { 61 | html += " " + k + "=\"" + v.replace('%s', cashtag.slice(1)) + "\""; 62 | } 63 | } 64 | return before + "" + cashtag + ""; 65 | }); 66 | } 67 | })(); 68 | -------------------------------------------------------------------------------- /pkg/stocktwits-text-0.9.2.js: -------------------------------------------------------------------------------- 1 | stwt = window.stwt || {}; 2 | 3 | (function() { 4 | stwt.txt = {}; 5 | stwt.txt.regexen = {}; 6 | stwt.txt.regexen.cashtag = /(^|[\s\,\.\-\+\(\/\"\']\$?|^\$)(\$([a-z1-9]{1}[a-z]{1,3}_F|(?!\d+[bmkts]{1}?(il(lion)?|ln|m|n)?\b|[\d]+\b)(?!\d+usd)[a-z0-9]{1,9}(?:[-\.]{1}[a-z]{1,2})?(?:[-\.]{1}[a-z]{1,2})?))\b(?!\$)/ig; 7 | 8 | stwt.txt.extractCashtags = function(text) { 9 | var matches = []; 10 | 11 | text.replace(stwt.txt.regexen.cashtag, function(match, prefix, cashtag) { 12 | matches.push(cashtag.slice(1)); 13 | }); 14 | 15 | return matches; 16 | } 17 | 18 | stwt.txt.autoLinkCashtags = function(text, options) { 19 | if (typeof(options) === "function") { 20 | return text.replace(stwt.txt.regexen.cashtag, function(match, before, cashtag) { 21 | return before + options.call(this, cashtag.toUpperCase(), cashtag.toUpperCase().slice(1)); 22 | }); 23 | } 24 | 25 | var html = []; 26 | var opts = options || {}; 27 | var htmlAttributes = {} 28 | for (var k in options) { 29 | if (k !== "urlClass" && k !== "urlTarget" && k !== "urlNofollow" && k !== "url") { 30 | htmlAttributes[k] = options[k]; 31 | } 32 | } 33 | 34 | var classes = []; 35 | if (htmlAttributes['class']) { 36 | classes.push(htmlAttributes['class']); 37 | } 38 | if (opts.urlClass === undefined) { 39 | classes.push("stwt-url cashtag"); 40 | } else if (opts.urlClass) { 41 | classes.push(opts.urlClass); 42 | } 43 | htmlAttributes['class'] = classes.join(" "); 44 | 45 | if (opts.urlTarget) { 46 | htmlAttributes.target = opts.urlTarget; 47 | } 48 | if (opts.urlNofollow) { 49 | htmlAttributes.rel = "nofollow"; 50 | } 51 | 52 | opts.url = opts.url || "http://stocktwits.com/symbol/%s"; 53 | htmlAttributes.href = opts.url 54 | 55 | return text.replace(stwt.txt.regexen.cashtag, function(match, before, cashtag) { 56 | cashtag = cashtag.toUpperCase(); 57 | var html = ""; 58 | var v; 59 | for (k in htmlAttributes) { 60 | if (v = htmlAttributes[k]) { 61 | html += " " + k + "=\"" + v.replace('%s', cashtag.slice(1)) + "\""; 62 | } 63 | } 64 | return before + "" + cashtag + ""; 65 | }); 66 | } 67 | })(); 68 | -------------------------------------------------------------------------------- /lib/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.3.0pre - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-header label { 58 | display: inline-block; 59 | } 60 | 61 | #qunit-banner { 62 | height: 5px; 63 | } 64 | 65 | #qunit-testrunner-toolbar { 66 | padding: 0.5em 0 0.5em 2em; 67 | color: #5E740B; 68 | background-color: #eee; 69 | } 70 | 71 | #qunit-userAgent { 72 | padding: 0.5em 0 0.5em 2.5em; 73 | background-color: #2b81af; 74 | color: #fff; 75 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 76 | } 77 | 78 | 79 | /** Tests: Pass/Fail */ 80 | 81 | #qunit-tests { 82 | list-style-position: inside; 83 | } 84 | 85 | #qunit-tests li { 86 | padding: 0.4em 0.5em 0.4em 2.5em; 87 | border-bottom: 1px solid #fff; 88 | list-style-position: inside; 89 | } 90 | 91 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 92 | display: none; 93 | } 94 | 95 | #qunit-tests li strong { 96 | cursor: pointer; 97 | } 98 | 99 | #qunit-tests li a { 100 | padding: 0.5em; 101 | color: #c2ccd1; 102 | text-decoration: none; 103 | } 104 | #qunit-tests li a:hover, 105 | #qunit-tests li a:focus { 106 | color: #000; 107 | } 108 | 109 | #qunit-tests ol { 110 | margin-top: 0.5em; 111 | padding: 0.5em; 112 | 113 | background-color: #fff; 114 | 115 | border-radius: 15px; 116 | -moz-border-radius: 15px; 117 | -webkit-border-radius: 15px; 118 | 119 | box-shadow: inset 0px 2px 13px #999; 120 | -moz-box-shadow: inset 0px 2px 13px #999; 121 | -webkit-box-shadow: inset 0px 2px 13px #999; 122 | } 123 | 124 | #qunit-tests table { 125 | border-collapse: collapse; 126 | margin-top: .2em; 127 | } 128 | 129 | #qunit-tests th { 130 | text-align: right; 131 | vertical-align: top; 132 | padding: 0 .5em 0 0; 133 | } 134 | 135 | #qunit-tests td { 136 | vertical-align: top; 137 | } 138 | 139 | #qunit-tests pre { 140 | margin: 0; 141 | white-space: pre-wrap; 142 | word-wrap: break-word; 143 | } 144 | 145 | #qunit-tests del { 146 | background-color: #e0f2be; 147 | color: #374e0c; 148 | text-decoration: none; 149 | } 150 | 151 | #qunit-tests ins { 152 | background-color: #ffcaca; 153 | color: #500; 154 | text-decoration: none; 155 | } 156 | 157 | /*** Test Counts */ 158 | 159 | #qunit-tests b.counts { color: black; } 160 | #qunit-tests b.passed { color: #5E740B; } 161 | #qunit-tests b.failed { color: #710909; } 162 | 163 | #qunit-tests li li { 164 | margin: 0.5em; 165 | padding: 0.4em 0.5em 0.4em 0.5em; 166 | background-color: #fff; 167 | border-bottom: none; 168 | list-style-position: inside; 169 | } 170 | 171 | /*** Passing Styles */ 172 | 173 | #qunit-tests li li.pass { 174 | color: #5E740B; 175 | background-color: #fff; 176 | border-left: 26px solid #C6E746; 177 | } 178 | 179 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 180 | #qunit-tests .pass .test-name { color: #366097; } 181 | 182 | #qunit-tests .pass .test-actual, 183 | #qunit-tests .pass .test-expected { color: #999999; } 184 | 185 | #qunit-banner.qunit-pass { background-color: #C6E746; } 186 | 187 | /*** Failing Styles */ 188 | 189 | #qunit-tests li li.fail { 190 | color: #710909; 191 | background-color: #fff; 192 | border-left: 26px solid #EE5757; 193 | white-space: pre; 194 | } 195 | 196 | #qunit-tests > li:last-child { 197 | border-radius: 0 0 15px 15px; 198 | -moz-border-radius: 0 0 15px 15px; 199 | -webkit-border-bottom-right-radius: 15px; 200 | -webkit-border-bottom-left-radius: 15px; 201 | } 202 | 203 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 204 | #qunit-tests .fail .test-name, 205 | #qunit-tests .fail .module-name { color: #000000; } 206 | 207 | #qunit-tests .fail .test-actual { color: #EE5757; } 208 | #qunit-tests .fail .test-expected { color: green; } 209 | 210 | #qunit-banner.qunit-fail { background-color: #EE5757; } 211 | 212 | 213 | /** Result */ 214 | 215 | #qunit-testresult { 216 | padding: 0.5em 0.5em 0.5em 2.5em; 217 | 218 | color: #2b81af; 219 | background-color: #D2E0E6; 220 | 221 | border-bottom: 1px solid white; 222 | } 223 | 224 | /** Fixture */ 225 | 226 | #qunit-fixture { 227 | position: absolute; 228 | top: -10000px; 229 | left: -10000px; 230 | width: 1000px; 231 | height: 1000px; 232 | } 233 | -------------------------------------------------------------------------------- /test/tests.js: -------------------------------------------------------------------------------- 1 | 2 | module("stwt.txt"); 3 | 4 | test("extractCashtags", function() { 5 | var tests = [ 6 | ["$FOO", "FOO"], 7 | ["$FOO$BAR", ""], 8 | ["$FOO $BAR", "FOO,BAR"], 9 | ["$$FOO", "FOO"], 10 | ["$FOO.A $FOO-B", "FOO.A,FOO-B"], 11 | ["$FOO,$BAR,,$BAZ", "FOO,BAR,BAZ"], 12 | ["$6B $6M $25M $5k", ""], 13 | ["$2.55 $33 $95.00", ""], 14 | ["$3million $5billion $12trillion", ""], 15 | ["$ES_F $6C_F", "ES_F,6C_F"], 16 | ["$A.B.C", "A.B.C"], 17 | ["$A.B.C.D", "A.B.C"], 18 | ["$A.B.C..$D", "A.B.C,D"], 19 | [" $FOO ", "FOO"], 20 | ["@#^$!@#$FOO", ""], 21 | ["FU$$Y", ""], 22 | [" $FOO..$BAR,$FOO", "FOO,BAR,FOO"], 23 | ["$$FOO$$BAR $$BAZ", "BAZ"], 24 | ["$ES_F, -$FOO+$BAR", "ES_F,FOO,BAR"], 25 | ["$LONGLONG", "LONGLONG"], 26 | ["($FOO)($BAR)", "FOO,BAR"], 27 | ["+$FOO-$BAR", "FOO,BAR"], 28 | ["$FOO/$BAR, $BAZ", "FOO,BAR,BAZ"], 29 | ["$83BLN $5MM $3BLN $10BN", ""], 30 | ["$50USD $3.10USD", ""], 31 | ['"$AAPL" or "$GOOG"', "AAPL,GOOG"], 32 | ["'$AAPL' or '$GOOG'", "AAPL,GOOG"] 33 | ]; 34 | 35 | for(var i=0; i " + tests[i][1]); 37 | } 38 | }); 39 | 40 | 41 | test("autoLinkCashtags (no options)", function() { 42 | var tests = [ 43 | ["hello $FOO", "hello $FOO"], 44 | ["hello $FOO,$BAR", "hello $FOO,$BAR"], 45 | ["$$FOO$$BAR..$BAZ", "$$FOO$$BAR..$BAZ"], 46 | ["-$FOO+$BAR", "-$FOO+$BAR"] 47 | ] 48 | 49 | for(var i=0; i " + tests[i][1]); 51 | } 52 | }); 53 | 54 | test("autoLinkCashtags (options)", function() { 55 | var tests = [ 56 | [["test $FOO", { urlClass: null }], 57 | "test $FOO", 58 | "test $FOO { urlClass: null }"], 59 | [["test $FOO", { urlClass: "" }], 60 | "test $FOO", 61 | "test $FOO { urlClass: \"\" }"], 62 | [["test $FOO", { urlClass: undefined }], 63 | "test $FOO", 64 | "test $FOO { urlClass: undefined }"], 65 | [["test $FOO", { urlClass: false }], 66 | "test $FOO", 67 | "test $FOO { urlClass: false }"], 68 | [["test $FOO", { urlClass: "testa testb" }], 69 | "test $FOO", 70 | "test $FOO { urlClass: \"testa testb\" }"], 71 | [["test $FOO", { urlTarget: "_new" }], 72 | "test $FOO", 73 | "test $FOO { urlTarget: \"_new\" }"], 74 | [["test $FOO", { urlNofollow: true }], 75 | "test $FOO", 76 | "test $FOO { urlNofollow: true }"], 77 | [["test $FOO", { urlClass: "foo", urlNofollow: true, urlTarget: "_new" }], 78 | "test $FOO", 79 | "test $FOO { urlClass: \"foo\", urlNofollow: true, urlTarget: \"_new\" }"], 80 | [["test $FOO", { url: "http://example.com?q=%s&foo=1" }], 81 | "test $FOO", 82 | "test $FOO { url: \"http://example.com?q=%s&foo=1\" }"], 83 | [["test $FOO", { "data-special": "foobar" }], 84 | "test $FOO", 85 | "test $FOO { data-special: \"foobar\" }"], 86 | [["test $FOO", { "data-special": "foobar", urlClass: "foo" }], 87 | "test $FOO", 88 | "test $FOO { data-special: \"foobar\", urlClass: \"foo\" }"], 89 | [["test $FOO", { "class": "foobar", urlClass: "foo" }], 90 | "test $FOO", 91 | "test $FOO { class: \"foobar\", urlClass: \"foo\" }"], 92 | [["test $FOO", { "data-special": "%s" }], 93 | "test $FOO", 94 | "test $FOO { data-special: \"%s\" }"], 95 | ]; 96 | 97 | for(var i=0; i " + tests[i][1]); 99 | } 100 | }); 101 | 102 | test("autoLinkCashtags (callback)", function() { 103 | var tests = [ 104 | [["test $FOO", function(cashtag, symbol) { return "http://example.com/symbol/" + symbol + " " + cashtag; }], 105 | "test http://example.com/symbol/FOO $FOO"], 106 | [["test $FOO,$BAR", function(cashtag, symbol) { return "" + cashtag + ""; }], 107 | "test $FOO,$BAR"], 108 | ]; 109 | 110 | for(var i=0; i(" + bad + ", " + good + ", " + this.assertions.length + ")"; 178 | 179 | var a = document.createElement("a"); 180 | a.innerHTML = "Rerun"; 181 | a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 182 | 183 | addEvent(b, "click", function() { 184 | var next = b.nextSibling.nextSibling, 185 | display = next.style.display; 186 | next.style.display = display === "none" ? "block" : "none"; 187 | }); 188 | 189 | addEvent(b, "dblclick", function(e) { 190 | var target = e && e.target ? e.target : window.event.srcElement; 191 | if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { 192 | target = target.parentNode; 193 | } 194 | if ( window.location && target.nodeName.toLowerCase() === "strong" ) { 195 | window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); 196 | } 197 | }); 198 | 199 | var li = id(this.id); 200 | li.className = bad ? "fail" : "pass"; 201 | li.removeChild( li.firstChild ); 202 | li.appendChild( b ); 203 | li.appendChild( a ); 204 | li.appendChild( ol ); 205 | 206 | } else { 207 | for ( var i = 0; i < this.assertions.length; i++ ) { 208 | if ( !this.assertions[i].result ) { 209 | bad++; 210 | config.stats.bad++; 211 | config.moduleStats.bad++; 212 | } 213 | } 214 | } 215 | 216 | try { 217 | QUnit.reset(); 218 | } catch(e) { 219 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); 220 | } 221 | 222 | runLoggingCallbacks( 'testDone', QUnit, { 223 | name: this.testName, 224 | module: this.module, 225 | failed: bad, 226 | passed: this.assertions.length - bad, 227 | total: this.assertions.length 228 | } ); 229 | }, 230 | 231 | queue: function() { 232 | var test = this; 233 | synchronize(function() { 234 | test.init(); 235 | }); 236 | function run() { 237 | // each of these can by async 238 | synchronize(function() { 239 | test.setup(); 240 | }); 241 | synchronize(function() { 242 | test.run(); 243 | }); 244 | synchronize(function() { 245 | test.teardown(); 246 | }); 247 | synchronize(function() { 248 | test.finish(); 249 | }); 250 | } 251 | // defer when previous test run passed, if storage is available 252 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); 253 | if (bad) { 254 | run(); 255 | } else { 256 | synchronize(run, true); 257 | }; 258 | } 259 | 260 | }; 261 | 262 | var QUnit = { 263 | 264 | // call on start of module test to prepend name to all tests 265 | module: function(name, testEnvironment) { 266 | config.currentModule = name; 267 | config.currentModuleTestEnviroment = testEnvironment; 268 | }, 269 | 270 | asyncTest: function(testName, expected, callback) { 271 | if ( arguments.length === 2 ) { 272 | callback = expected; 273 | expected = null; 274 | } 275 | 276 | QUnit.test(testName, expected, callback, true); 277 | }, 278 | 279 | test: function(testName, expected, callback, async) { 280 | var name = '' + escapeInnerText(testName) + ''; 281 | 282 | if ( arguments.length === 2 ) { 283 | callback = expected; 284 | expected = null; 285 | } 286 | 287 | if ( config.currentModule ) { 288 | name = '' + config.currentModule + ": " + name; 289 | } 290 | 291 | if ( !validTest(config.currentModule + ": " + testName) ) { 292 | return; 293 | } 294 | 295 | var test = new Test(name, testName, expected, async, callback); 296 | test.module = config.currentModule; 297 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 298 | test.queue(); 299 | }, 300 | 301 | /** 302 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 303 | */ 304 | expect: function(asserts) { 305 | config.current.expected = asserts; 306 | }, 307 | 308 | /** 309 | * Asserts true. 310 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 311 | */ 312 | ok: function(a, msg) { 313 | if (!config.current) { 314 | throw new Error("ok() assertion outside test context, was " + sourceFromStacktrace(2)); 315 | } 316 | a = !!a; 317 | var details = { 318 | result: a, 319 | message: msg 320 | }; 321 | msg = escapeInnerText(msg); 322 | runLoggingCallbacks( 'log', QUnit, details ); 323 | config.current.assertions.push({ 324 | result: a, 325 | message: msg 326 | }); 327 | }, 328 | 329 | /** 330 | * Checks that the first two arguments are equal, with an optional message. 331 | * Prints out both actual and expected values. 332 | * 333 | * Prefered to ok( actual == expected, message ) 334 | * 335 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 336 | * 337 | * @param Object actual 338 | * @param Object expected 339 | * @param String message (optional) 340 | */ 341 | equal: function(actual, expected, message) { 342 | QUnit.push(expected == actual, actual, expected, message); 343 | }, 344 | 345 | notEqual: function(actual, expected, message) { 346 | QUnit.push(expected != actual, actual, expected, message); 347 | }, 348 | 349 | deepEqual: function(actual, expected, message) { 350 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 351 | }, 352 | 353 | notDeepEqual: function(actual, expected, message) { 354 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 355 | }, 356 | 357 | strictEqual: function(actual, expected, message) { 358 | QUnit.push(expected === actual, actual, expected, message); 359 | }, 360 | 361 | notStrictEqual: function(actual, expected, message) { 362 | QUnit.push(expected !== actual, actual, expected, message); 363 | }, 364 | 365 | raises: function(block, expected, message) { 366 | var actual, ok = false; 367 | 368 | if (typeof expected === 'string') { 369 | message = expected; 370 | expected = null; 371 | } 372 | 373 | try { 374 | block(); 375 | } catch (e) { 376 | actual = e; 377 | } 378 | 379 | if (actual) { 380 | // we don't want to validate thrown error 381 | if (!expected) { 382 | ok = true; 383 | // expected is a regexp 384 | } else if (QUnit.objectType(expected) === "regexp") { 385 | ok = expected.test(actual); 386 | // expected is a constructor 387 | } else if (actual instanceof expected) { 388 | ok = true; 389 | // expected is a validation function which returns true is validation passed 390 | } else if (expected.call({}, actual) === true) { 391 | ok = true; 392 | } 393 | } 394 | 395 | QUnit.ok(ok, message); 396 | }, 397 | 398 | start: function(count) { 399 | config.semaphore -= count || 1; 400 | if (config.semaphore > 0) { 401 | // don't start until equal number of stop-calls 402 | return; 403 | } 404 | if (config.semaphore < 0) { 405 | // ignore if start is called more often then stop 406 | config.semaphore = 0; 407 | } 408 | // A slight delay, to avoid any current callbacks 409 | if ( defined.setTimeout ) { 410 | window.setTimeout(function() { 411 | if (config.semaphore > 0) { 412 | return; 413 | } 414 | if ( config.timeout ) { 415 | clearTimeout(config.timeout); 416 | } 417 | 418 | config.blocking = false; 419 | process(true); 420 | }, 13); 421 | } else { 422 | config.blocking = false; 423 | process(true); 424 | } 425 | }, 426 | 427 | stop: function(count) { 428 | config.semaphore += count || 1; 429 | config.blocking = true; 430 | 431 | if ( config.testTimeout && defined.setTimeout ) { 432 | clearTimeout(config.timeout); 433 | config.timeout = window.setTimeout(function() { 434 | QUnit.ok( false, "Test timed out" ); 435 | config.semaphore = 1; 436 | QUnit.start(); 437 | }, config.testTimeout); 438 | } 439 | } 440 | }; 441 | 442 | //We want access to the constructor's prototype 443 | (function() { 444 | function F(){}; 445 | F.prototype = QUnit; 446 | QUnit = new F(); 447 | //Make F QUnit's constructor so that we can add to the prototype later 448 | QUnit.constructor = F; 449 | })(); 450 | 451 | // deprecated; still export them to window to provide clear error messages 452 | // next step: remove entirely 453 | QUnit.equals = function() { 454 | throw new Error("QUnit.equals has been deprecated since 2009 (e88049a0), use QUnit.equal instead"); 455 | }; 456 | QUnit.same = function() { 457 | throw new Error("QUnit.same has been deprecated since 2009 (e88049a0), use QUnit.deepEqual instead"); 458 | }; 459 | 460 | // Maintain internal state 461 | var config = { 462 | // The queue of tests to run 463 | queue: [], 464 | 465 | // block until document ready 466 | blocking: true, 467 | 468 | // when enabled, show only failing tests 469 | // gets persisted through sessionStorage and can be changed in UI via checkbox 470 | hidepassed: false, 471 | 472 | // by default, run previously failed tests first 473 | // very useful in combination with "Hide passed tests" checked 474 | reorder: true, 475 | 476 | // by default, modify document.title when suite is done 477 | altertitle: true, 478 | 479 | urlConfig: ['noglobals', 'notrycatch'], 480 | 481 | //logging callback queues 482 | begin: [], 483 | done: [], 484 | log: [], 485 | testStart: [], 486 | testDone: [], 487 | moduleStart: [], 488 | moduleDone: [] 489 | }; 490 | 491 | // Load paramaters 492 | (function() { 493 | var location = window.location || { search: "", protocol: "file:" }, 494 | params = location.search.slice( 1 ).split( "&" ), 495 | length = params.length, 496 | urlParams = {}, 497 | current; 498 | 499 | if ( params[ 0 ] ) { 500 | for ( var i = 0; i < length; i++ ) { 501 | current = params[ i ].split( "=" ); 502 | current[ 0 ] = decodeURIComponent( current[ 0 ] ); 503 | // allow just a key to turn on a flag, e.g., test.html?noglobals 504 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; 505 | urlParams[ current[ 0 ] ] = current[ 1 ]; 506 | } 507 | } 508 | 509 | QUnit.urlParams = urlParams; 510 | config.filter = urlParams.filter; 511 | 512 | // Figure out if we're running the tests from a server or not 513 | QUnit.isLocal = !!(location.protocol === 'file:'); 514 | })(); 515 | 516 | // Expose the API as global variables, unless an 'exports' 517 | // object exists, in that case we assume we're in CommonJS 518 | if ( typeof exports === "undefined" || typeof require === "undefined" ) { 519 | extend(window, QUnit); 520 | window.QUnit = QUnit; 521 | } else { 522 | module.exports = QUnit; 523 | } 524 | 525 | // define these after exposing globals to keep them in these QUnit namespace only 526 | extend(QUnit, { 527 | config: config, 528 | 529 | // Initialize the configuration options 530 | init: function() { 531 | extend(config, { 532 | stats: { all: 0, bad: 0 }, 533 | moduleStats: { all: 0, bad: 0 }, 534 | started: +new Date, 535 | updateRate: 1000, 536 | blocking: false, 537 | autostart: true, 538 | autorun: false, 539 | filter: "", 540 | queue: [], 541 | semaphore: 0 542 | }); 543 | 544 | var qunit = id( "qunit" ); 545 | if ( qunit ) { 546 | qunit.innerHTML = 547 | '

    ' + document.title + '

    ' + 548 | '

    ' + 549 | '
    ' + 550 | '

    ' + 551 | '
      '; 552 | } 553 | 554 | var tests = id( "qunit-tests" ), 555 | banner = id( "qunit-banner" ), 556 | result = id( "qunit-testresult" ); 557 | 558 | if ( tests ) { 559 | tests.innerHTML = ""; 560 | } 561 | 562 | if ( banner ) { 563 | banner.className = ""; 564 | } 565 | 566 | if ( result ) { 567 | result.parentNode.removeChild( result ); 568 | } 569 | 570 | if ( tests ) { 571 | result = document.createElement( "p" ); 572 | result.id = "qunit-testresult"; 573 | result.className = "result"; 574 | tests.parentNode.insertBefore( result, tests ); 575 | result.innerHTML = 'Running...
       '; 576 | } 577 | }, 578 | 579 | /** 580 | * Resets the test setup. Useful for tests that modify the DOM. 581 | * 582 | * If jQuery is available, uses jQuery's replaceWith(), otherwise use replaceChild 583 | */ 584 | reset: function() { 585 | if ( window.jQuery ) { 586 | jQuery( "#qunit-fixture" ).replaceWith( config.fixture.cloneNode(true) ); 587 | } else { 588 | var main = id( 'qunit-fixture' ); 589 | if ( main ) { 590 | main.parentNode.replaceChild(config.fixture.cloneNode(true), main); 591 | } 592 | } 593 | }, 594 | 595 | /** 596 | * Trigger an event on an element. 597 | * 598 | * @example triggerEvent( document.body, "click" ); 599 | * 600 | * @param DOMElement elem 601 | * @param String type 602 | */ 603 | triggerEvent: function( elem, type, event ) { 604 | if ( document.createEvent ) { 605 | event = document.createEvent("MouseEvents"); 606 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 607 | 0, 0, 0, 0, 0, false, false, false, false, 0, null); 608 | elem.dispatchEvent( event ); 609 | 610 | } else if ( elem.fireEvent ) { 611 | elem.fireEvent("on"+type); 612 | } 613 | }, 614 | 615 | // Safe object type checking 616 | is: function( type, obj ) { 617 | return QUnit.objectType( obj ) == type; 618 | }, 619 | 620 | objectType: function( obj ) { 621 | if (typeof obj === "undefined") { 622 | return "undefined"; 623 | 624 | // consider: typeof null === object 625 | } 626 | if (obj === null) { 627 | return "null"; 628 | } 629 | 630 | var type = toString.call( obj ).match(/^\[object\s(.*)\]$/)[1] || ''; 631 | 632 | switch (type) { 633 | case 'Number': 634 | if (isNaN(obj)) { 635 | return "nan"; 636 | } else { 637 | return "number"; 638 | } 639 | case 'String': 640 | case 'Boolean': 641 | case 'Array': 642 | case 'Date': 643 | case 'RegExp': 644 | case 'Function': 645 | return type.toLowerCase(); 646 | } 647 | if (typeof obj === "object") { 648 | return "object"; 649 | } 650 | return undefined; 651 | }, 652 | 653 | push: function(result, actual, expected, message) { 654 | if (!config.current) { 655 | throw new Error("assertion outside test context, was " + sourceFromStacktrace()); 656 | } 657 | var details = { 658 | result: result, 659 | message: message, 660 | actual: actual, 661 | expected: expected 662 | }; 663 | 664 | message = escapeInnerText(message) || (result ? "okay" : "failed"); 665 | message = '' + message + ""; 666 | var output = message; 667 | if (!result) { 668 | expected = escapeInnerText(QUnit.jsDump.parse(expected)); 669 | actual = escapeInnerText(QUnit.jsDump.parse(actual)); 670 | output += ''; 671 | if (actual != expected) { 672 | output += ''; 673 | output += ''; 674 | } 675 | var source = sourceFromStacktrace(); 676 | if (source) { 677 | details.source = source; 678 | output += ''; 679 | } 680 | output += "
      Expected:
      ' + expected + '
      Result:
      ' + actual + '
      Diff:
      ' + QUnit.diff(expected, actual) +'
      Source:
      ' + escapeInnerText(source) + '
      "; 681 | } 682 | 683 | runLoggingCallbacks( 'log', QUnit, details ); 684 | 685 | config.current.assertions.push({ 686 | result: !!result, 687 | message: output 688 | }); 689 | }, 690 | 691 | url: function( params ) { 692 | params = extend( extend( {}, QUnit.urlParams ), params ); 693 | var querystring = "?", 694 | key; 695 | for ( key in params ) { 696 | if ( !hasOwn.call( params, key ) ) { 697 | continue; 698 | } 699 | querystring += encodeURIComponent( key ) + "=" + 700 | encodeURIComponent( params[ key ] ) + "&"; 701 | } 702 | return window.location.pathname + querystring.slice( 0, -1 ); 703 | }, 704 | 705 | extend: extend, 706 | id: id, 707 | addEvent: addEvent 708 | }); 709 | 710 | //QUnit.constructor is set to the empty F() above so that we can add to it's prototype later 711 | //Doing this allows us to tell if the following methods have been overwritten on the actual 712 | //QUnit object, which is a deprecated way of using the callbacks. 713 | extend(QUnit.constructor.prototype, { 714 | // Logging callbacks; all receive a single argument with the listed properties 715 | // run test/logs.html for any related changes 716 | begin: registerLoggingCallback('begin'), 717 | // done: { failed, passed, total, runtime } 718 | done: registerLoggingCallback('done'), 719 | // log: { result, actual, expected, message } 720 | log: registerLoggingCallback('log'), 721 | // testStart: { name } 722 | testStart: registerLoggingCallback('testStart'), 723 | // testDone: { name, failed, passed, total } 724 | testDone: registerLoggingCallback('testDone'), 725 | // moduleStart: { name } 726 | moduleStart: registerLoggingCallback('moduleStart'), 727 | // moduleDone: { name, failed, passed, total } 728 | moduleDone: registerLoggingCallback('moduleDone') 729 | }); 730 | 731 | if ( typeof document === "undefined" || document.readyState === "complete" ) { 732 | config.autorun = true; 733 | } 734 | 735 | QUnit.load = function() { 736 | runLoggingCallbacks( 'begin', QUnit, {} ); 737 | 738 | // Initialize the config, saving the execution queue 739 | var oldconfig = extend({}, config); 740 | QUnit.init(); 741 | extend(config, oldconfig); 742 | 743 | config.blocking = false; 744 | 745 | var urlConfigHtml = '', len = config.urlConfig.length; 746 | for ( var i = 0, val; i < len, val = config.urlConfig[i]; i++ ) { 747 | config[val] = QUnit.urlParams[val]; 748 | urlConfigHtml += ''; 749 | } 750 | 751 | var userAgent = id("qunit-userAgent"); 752 | if ( userAgent ) { 753 | userAgent.innerHTML = navigator.userAgent; 754 | } 755 | var banner = id("qunit-header"); 756 | if ( banner ) { 757 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + urlConfigHtml; 758 | addEvent( banner, "change", function( event ) { 759 | var params = {}; 760 | params[ event.target.name ] = event.target.checked ? true : undefined; 761 | window.location = QUnit.url( params ); 762 | }); 763 | } 764 | 765 | var toolbar = id("qunit-testrunner-toolbar"); 766 | if ( toolbar ) { 767 | var filter = document.createElement("input"); 768 | filter.type = "checkbox"; 769 | filter.id = "qunit-filter-pass"; 770 | addEvent( filter, "click", function() { 771 | var ol = document.getElementById("qunit-tests"); 772 | if ( filter.checked ) { 773 | ol.className = ol.className + " hidepass"; 774 | } else { 775 | var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; 776 | ol.className = tmp.replace(/ hidepass /, " "); 777 | } 778 | if ( defined.sessionStorage ) { 779 | if (filter.checked) { 780 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 781 | } else { 782 | sessionStorage.removeItem("qunit-filter-passed-tests"); 783 | } 784 | } 785 | }); 786 | if ( config.hidepassed || defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { 787 | filter.checked = true; 788 | var ol = document.getElementById("qunit-tests"); 789 | ol.className = ol.className + " hidepass"; 790 | } 791 | toolbar.appendChild( filter ); 792 | 793 | var label = document.createElement("label"); 794 | label.setAttribute("for", "qunit-filter-pass"); 795 | label.innerHTML = "Hide passed tests"; 796 | toolbar.appendChild( label ); 797 | } 798 | 799 | var main = id('qunit-fixture'); 800 | if ( main ) { 801 | config.fixture = main.cloneNode(true); 802 | } 803 | 804 | if (config.autostart) { 805 | QUnit.start(); 806 | } 807 | }; 808 | 809 | addEvent(window, "load", QUnit.load); 810 | 811 | // addEvent(window, "error") gives us a useless event object 812 | window.onerror = function( message, file, line ) { 813 | if ( QUnit.config.current ) { 814 | ok( false, message + ", " + file + ":" + line ); 815 | } else { 816 | test( "global failure", function() { 817 | ok( false, message + ", " + file + ":" + line ); 818 | }); 819 | } 820 | }; 821 | 822 | function done() { 823 | config.autorun = true; 824 | 825 | // Log the last module results 826 | if ( config.currentModule ) { 827 | runLoggingCallbacks( 'moduleDone', QUnit, { 828 | name: config.currentModule, 829 | failed: config.moduleStats.bad, 830 | passed: config.moduleStats.all - config.moduleStats.bad, 831 | total: config.moduleStats.all 832 | } ); 833 | } 834 | 835 | var banner = id("qunit-banner"), 836 | tests = id("qunit-tests"), 837 | runtime = +new Date - config.started, 838 | passed = config.stats.all - config.stats.bad, 839 | html = [ 840 | 'Tests completed in ', 841 | runtime, 842 | ' milliseconds.
      ', 843 | '', 844 | passed, 845 | ' tests of ', 846 | config.stats.all, 847 | ' passed, ', 848 | config.stats.bad, 849 | ' failed.' 850 | ].join(''); 851 | 852 | if ( banner ) { 853 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 854 | } 855 | 856 | if ( tests ) { 857 | id( "qunit-testresult" ).innerHTML = html; 858 | } 859 | 860 | if ( config.altertitle && typeof document !== "undefined" && document.title ) { 861 | // show ✖ for good, ✔ for bad suite result in title 862 | // use escape sequences in case file gets loaded with non-utf-8-charset 863 | document.title = [ 864 | (config.stats.bad ? "\u2716" : "\u2714"), 865 | document.title.replace(/^[\u2714\u2716] /i, "") 866 | ].join(" "); 867 | } 868 | 869 | // clear own sessionStorage items if all tests passed 870 | if ( config.reorder && defined.sessionStorage && config.stats.bad === 0 ) { 871 | for (var key in sessionStorage) { 872 | if (sessionStorage.hasOwnProperty(key) && key.indexOf("qunit-") === 0 ) { 873 | sessionStorage.removeItem(key); 874 | } 875 | } 876 | } 877 | 878 | runLoggingCallbacks( 'done', QUnit, { 879 | failed: config.stats.bad, 880 | passed: passed, 881 | total: config.stats.all, 882 | runtime: runtime 883 | } ); 884 | } 885 | 886 | function validTest( name ) { 887 | var filter = config.filter, 888 | run = false; 889 | 890 | if ( !filter ) { 891 | return true; 892 | } 893 | 894 | var not = filter.charAt( 0 ) === "!"; 895 | if ( not ) { 896 | filter = filter.slice( 1 ); 897 | } 898 | 899 | if ( name.indexOf( filter ) !== -1 ) { 900 | return !not; 901 | } 902 | 903 | if ( not ) { 904 | run = true; 905 | } 906 | 907 | return run; 908 | } 909 | 910 | // so far supports only Firefox, Chrome and Opera (buggy) 911 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 912 | function sourceFromStacktrace(offset) { 913 | offset = offset || 3; 914 | try { 915 | throw new Error(); 916 | } catch ( e ) { 917 | if (e.stacktrace) { 918 | // Opera 919 | return e.stacktrace.split("\n")[offset + 3]; 920 | } else if (e.stack) { 921 | // Firefox, Chrome 922 | var stack = e.stack.split("\n"); 923 | if (/^error$/i.test(stack[0])) { 924 | stack.shift(); 925 | } 926 | return stack[offset]; 927 | } else if (e.sourceURL) { 928 | // Safari, PhantomJS 929 | // TODO sourceURL points at the 'throw new Error' line above, useless 930 | //return e.sourceURL + ":" + e.line; 931 | } 932 | } 933 | } 934 | 935 | function escapeInnerText(s) { 936 | if (!s) { 937 | return ""; 938 | } 939 | s = s + ""; 940 | return s.replace(/[\&<>]/g, function(s) { 941 | switch(s) { 942 | case "&": return "&"; 943 | case "<": return "<"; 944 | case ">": return ">"; 945 | default: return s; 946 | } 947 | }); 948 | } 949 | 950 | function synchronize( callback, last ) { 951 | config.queue.push( callback ); 952 | 953 | if ( config.autorun && !config.blocking ) { 954 | process(last); 955 | } 956 | } 957 | 958 | function process( last ) { 959 | var start = new Date().getTime(); 960 | config.depth = config.depth ? config.depth + 1 : 1; 961 | 962 | while ( config.queue.length && !config.blocking ) { 963 | if ( !defined.setTimeout || config.updateRate <= 0 || ( ( new Date().getTime() - start ) < config.updateRate ) ) { 964 | config.queue.shift()(); 965 | } else { 966 | window.setTimeout( function(){ 967 | process( last ); 968 | }, 13 ); 969 | break; 970 | } 971 | } 972 | config.depth--; 973 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 974 | done(); 975 | } 976 | } 977 | 978 | function saveGlobal() { 979 | config.pollution = []; 980 | 981 | if ( config.noglobals ) { 982 | for ( var key in window ) { 983 | if ( !hasOwn.call( window, key ) ) { 984 | continue; 985 | } 986 | config.pollution.push( key ); 987 | } 988 | } 989 | } 990 | 991 | function checkPollution( name ) { 992 | var old = config.pollution; 993 | saveGlobal(); 994 | 995 | var newGlobals = diff( config.pollution, old ); 996 | if ( newGlobals.length > 0 ) { 997 | ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); 998 | } 999 | 1000 | var deletedGlobals = diff( old, config.pollution ); 1001 | if ( deletedGlobals.length > 0 ) { 1002 | ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); 1003 | } 1004 | } 1005 | 1006 | // returns a new Array with the elements that are in a but not in b 1007 | function diff( a, b ) { 1008 | var result = a.slice(); 1009 | for ( var i = 0; i < result.length; i++ ) { 1010 | for ( var j = 0; j < b.length; j++ ) { 1011 | if ( result[i] === b[j] ) { 1012 | result.splice(i, 1); 1013 | i--; 1014 | break; 1015 | } 1016 | } 1017 | } 1018 | return result; 1019 | } 1020 | 1021 | function fail(message, exception, callback) { 1022 | if ( typeof console !== "undefined" && console.error && console.warn ) { 1023 | console.error(message); 1024 | console.error(exception); 1025 | console.error(exception.stack); 1026 | console.warn(callback.toString()); 1027 | 1028 | } else if ( window.opera && opera.postError ) { 1029 | opera.postError(message, exception, callback.toString); 1030 | } 1031 | } 1032 | 1033 | function extend(a, b) { 1034 | for ( var prop in b ) { 1035 | if ( b[prop] === undefined ) { 1036 | delete a[prop]; 1037 | 1038 | // Avoid "Member not found" error in IE8 caused by setting window.constructor 1039 | } else if ( prop !== "constructor" || a !== window ) { 1040 | a[prop] = b[prop]; 1041 | } 1042 | } 1043 | 1044 | return a; 1045 | } 1046 | 1047 | function addEvent(elem, type, fn) { 1048 | if ( elem.addEventListener ) { 1049 | elem.addEventListener( type, fn, false ); 1050 | } else if ( elem.attachEvent ) { 1051 | elem.attachEvent( "on" + type, fn ); 1052 | } else { 1053 | fn(); 1054 | } 1055 | } 1056 | 1057 | function id(name) { 1058 | return !!(typeof document !== "undefined" && document && document.getElementById) && 1059 | document.getElementById( name ); 1060 | } 1061 | 1062 | function registerLoggingCallback(key){ 1063 | return function(callback){ 1064 | config[key].push( callback ); 1065 | }; 1066 | } 1067 | 1068 | // Supports deprecated method of completely overwriting logging callbacks 1069 | function runLoggingCallbacks(key, scope, args) { 1070 | //debugger; 1071 | var callbacks; 1072 | if ( QUnit.hasOwnProperty(key) ) { 1073 | QUnit[key].call(scope, args); 1074 | } else { 1075 | callbacks = config[key]; 1076 | for( var i = 0; i < callbacks.length; i++ ) { 1077 | callbacks[i].call( scope, args ); 1078 | } 1079 | } 1080 | } 1081 | 1082 | // Test for equality any JavaScript type. 1083 | // Author: Philippe Rathé 1084 | QUnit.equiv = function () { 1085 | 1086 | var innerEquiv; // the real equiv function 1087 | var callers = []; // stack to decide between skip/abort functions 1088 | var parents = []; // stack to avoiding loops from circular referencing 1089 | 1090 | // Call the o related callback with the given arguments. 1091 | function bindCallbacks(o, callbacks, args) { 1092 | var prop = QUnit.objectType(o); 1093 | if (prop) { 1094 | if (QUnit.objectType(callbacks[prop]) === "function") { 1095 | return callbacks[prop].apply(callbacks, args); 1096 | } else { 1097 | return callbacks[prop]; // or undefined 1098 | } 1099 | } 1100 | } 1101 | 1102 | var getProto = Object.getPrototypeOf || function (obj) { 1103 | return obj.__proto__; 1104 | }; 1105 | 1106 | var callbacks = function () { 1107 | 1108 | // for string, boolean, number and null 1109 | function useStrictEquality(b, a) { 1110 | if (b instanceof a.constructor || a instanceof b.constructor) { 1111 | // to catch short annotaion VS 'new' annotation of a 1112 | // declaration 1113 | // e.g. var i = 1; 1114 | // var j = new Number(1); 1115 | return a == b; 1116 | } else { 1117 | return a === b; 1118 | } 1119 | } 1120 | 1121 | return { 1122 | "string" : useStrictEquality, 1123 | "boolean" : useStrictEquality, 1124 | "number" : useStrictEquality, 1125 | "null" : useStrictEquality, 1126 | "undefined" : useStrictEquality, 1127 | 1128 | "nan" : function(b) { 1129 | return isNaN(b); 1130 | }, 1131 | 1132 | "date" : function(b, a) { 1133 | return QUnit.objectType(b) === "date" 1134 | && a.valueOf() === b.valueOf(); 1135 | }, 1136 | 1137 | "regexp" : function(b, a) { 1138 | return QUnit.objectType(b) === "regexp" 1139 | && a.source === b.source && // the regex itself 1140 | a.global === b.global && // and its modifers 1141 | // (gmi) ... 1142 | a.ignoreCase === b.ignoreCase 1143 | && a.multiline === b.multiline; 1144 | }, 1145 | 1146 | // - skip when the property is a method of an instance (OOP) 1147 | // - abort otherwise, 1148 | // initial === would have catch identical references anyway 1149 | "function" : function() { 1150 | var caller = callers[callers.length - 1]; 1151 | return caller !== Object && typeof caller !== "undefined"; 1152 | }, 1153 | 1154 | "array" : function(b, a) { 1155 | var i, j, loop; 1156 | var len; 1157 | 1158 | // b could be an object literal here 1159 | if (!(QUnit.objectType(b) === "array")) { 1160 | return false; 1161 | } 1162 | 1163 | len = a.length; 1164 | if (len !== b.length) { // safe and faster 1165 | return false; 1166 | } 1167 | 1168 | // track reference to avoid circular references 1169 | parents.push(a); 1170 | for (i = 0; i < len; i++) { 1171 | loop = false; 1172 | for (j = 0; j < parents.length; j++) { 1173 | if (parents[j] === a[i]) { 1174 | loop = true;// dont rewalk array 1175 | } 1176 | } 1177 | if (!loop && !innerEquiv(a[i], b[i])) { 1178 | parents.pop(); 1179 | return false; 1180 | } 1181 | } 1182 | parents.pop(); 1183 | return true; 1184 | }, 1185 | 1186 | "object" : function(b, a) { 1187 | var i, j, loop; 1188 | var eq = true; // unless we can proove it 1189 | var aProperties = [], bProperties = []; // collection of 1190 | // strings 1191 | 1192 | // comparing constructors is more strict than using 1193 | // instanceof 1194 | if (a.constructor !== b.constructor) { 1195 | // Allow objects with no prototype to be equivalent to 1196 | // objects with Object as their constructor. 1197 | if (!((getProto(a) === null && getProto(b) === Object.prototype) || 1198 | (getProto(b) === null && getProto(a) === Object.prototype))) 1199 | { 1200 | return false; 1201 | } 1202 | } 1203 | 1204 | // stack constructor before traversing properties 1205 | callers.push(a.constructor); 1206 | // track reference to avoid circular references 1207 | parents.push(a); 1208 | 1209 | for (i in a) { // be strict: don't ensures hasOwnProperty 1210 | // and go deep 1211 | loop = false; 1212 | for (j = 0; j < parents.length; j++) { 1213 | if (parents[j] === a[i]) 1214 | loop = true; // don't go down the same path 1215 | // twice 1216 | } 1217 | aProperties.push(i); // collect a's properties 1218 | 1219 | if (!loop && !innerEquiv(a[i], b[i])) { 1220 | eq = false; 1221 | break; 1222 | } 1223 | } 1224 | 1225 | callers.pop(); // unstack, we are done 1226 | parents.pop(); 1227 | 1228 | for (i in b) { 1229 | bProperties.push(i); // collect b's properties 1230 | } 1231 | 1232 | // Ensures identical properties name 1233 | return eq 1234 | && innerEquiv(aProperties.sort(), bProperties 1235 | .sort()); 1236 | } 1237 | }; 1238 | }(); 1239 | 1240 | innerEquiv = function() { // can take multiple arguments 1241 | var args = Array.prototype.slice.apply(arguments); 1242 | if (args.length < 2) { 1243 | return true; // end transition 1244 | } 1245 | 1246 | return (function(a, b) { 1247 | if (a === b) { 1248 | return true; // catch the most you can 1249 | } else if (a === null || b === null || typeof a === "undefined" 1250 | || typeof b === "undefined" 1251 | || QUnit.objectType(a) !== QUnit.objectType(b)) { 1252 | return false; // don't lose time with error prone cases 1253 | } else { 1254 | return bindCallbacks(a, callbacks, [ b, a ]); 1255 | } 1256 | 1257 | // apply transition with (1..n) arguments 1258 | })(args[0], args[1]) 1259 | && arguments.callee.apply(this, args.splice(1, 1260 | args.length - 1)); 1261 | }; 1262 | 1263 | return innerEquiv; 1264 | 1265 | }(); 1266 | 1267 | /** 1268 | * jsDump Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | 1269 | * http://flesler.blogspot.com Licensed under BSD 1270 | * (http://www.opensource.org/licenses/bsd-license.php) Date: 5/15/2008 1271 | * 1272 | * @projectDescription Advanced and extensible data dumping for Javascript. 1273 | * @version 1.0.0 1274 | * @author Ariel Flesler 1275 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1276 | */ 1277 | QUnit.jsDump = (function() { 1278 | function quote( str ) { 1279 | return '"' + str.toString().replace(/"/g, '\\"') + '"'; 1280 | }; 1281 | function literal( o ) { 1282 | return o + ''; 1283 | }; 1284 | function join( pre, arr, post ) { 1285 | var s = jsDump.separator(), 1286 | base = jsDump.indent(), 1287 | inner = jsDump.indent(1); 1288 | if ( arr.join ) 1289 | arr = arr.join( ',' + s + inner ); 1290 | if ( !arr ) 1291 | return pre + post; 1292 | return [ pre, inner + arr, base + post ].join(s); 1293 | }; 1294 | function array( arr, stack ) { 1295 | var i = arr.length, ret = Array(i); 1296 | this.up(); 1297 | while ( i-- ) 1298 | ret[i] = this.parse( arr[i] , undefined , stack); 1299 | this.down(); 1300 | return join( '[', ret, ']' ); 1301 | }; 1302 | 1303 | var reName = /^function (\w+)/; 1304 | 1305 | var jsDump = { 1306 | parse:function( obj, type, stack ) { //type is used mostly internally, you can fix a (custom)type in advance 1307 | stack = stack || [ ]; 1308 | var parser = this.parsers[ type || this.typeOf(obj) ]; 1309 | type = typeof parser; 1310 | var inStack = inArray(obj, stack); 1311 | if (inStack != -1) { 1312 | return 'recursion('+(inStack - stack.length)+')'; 1313 | } 1314 | //else 1315 | if (type == 'function') { 1316 | stack.push(obj); 1317 | var res = parser.call( this, obj, stack ); 1318 | stack.pop(); 1319 | return res; 1320 | } 1321 | // else 1322 | return (type == 'string') ? parser : this.parsers.error; 1323 | }, 1324 | typeOf:function( obj ) { 1325 | var type; 1326 | if ( obj === null ) { 1327 | type = "null"; 1328 | } else if (typeof obj === "undefined") { 1329 | type = "undefined"; 1330 | } else if (QUnit.is("RegExp", obj)) { 1331 | type = "regexp"; 1332 | } else if (QUnit.is("Date", obj)) { 1333 | type = "date"; 1334 | } else if (QUnit.is("Function", obj)) { 1335 | type = "function"; 1336 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { 1337 | type = "window"; 1338 | } else if (obj.nodeType === 9) { 1339 | type = "document"; 1340 | } else if (obj.nodeType) { 1341 | type = "node"; 1342 | } else if ( 1343 | // native arrays 1344 | toString.call( obj ) === "[object Array]" || 1345 | // NodeList objects 1346 | ( typeof obj.length === "number" && typeof obj.item !== "undefined" && ( obj.length ? obj.item(0) === obj[0] : ( obj.item( 0 ) === null && typeof obj[0] === "undefined" ) ) ) 1347 | ) { 1348 | type = "array"; 1349 | } else { 1350 | type = typeof obj; 1351 | } 1352 | return type; 1353 | }, 1354 | separator:function() { 1355 | return this.multiline ? this.HTML ? '
      ' : '\n' : this.HTML ? ' ' : ' '; 1356 | }, 1357 | indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing 1358 | if ( !this.multiline ) 1359 | return ''; 1360 | var chr = this.indentChar; 1361 | if ( this.HTML ) 1362 | chr = chr.replace(/\t/g,' ').replace(/ /g,' '); 1363 | return Array( this._depth_ + (extra||0) ).join(chr); 1364 | }, 1365 | up:function( a ) { 1366 | this._depth_ += a || 1; 1367 | }, 1368 | down:function( a ) { 1369 | this._depth_ -= a || 1; 1370 | }, 1371 | setParser:function( name, parser ) { 1372 | this.parsers[name] = parser; 1373 | }, 1374 | // The next 3 are exposed so you can use them 1375 | quote:quote, 1376 | literal:literal, 1377 | join:join, 1378 | // 1379 | _depth_: 1, 1380 | // This is the list of parsers, to modify them, use jsDump.setParser 1381 | parsers:{ 1382 | window: '[Window]', 1383 | document: '[Document]', 1384 | error:'[ERROR]', //when no parser is found, shouldn't happen 1385 | unknown: '[Unknown]', 1386 | 'null':'null', 1387 | 'undefined':'undefined', 1388 | 'function':function( fn ) { 1389 | var ret = 'function', 1390 | name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE 1391 | if ( name ) 1392 | ret += ' ' + name; 1393 | ret += '('; 1394 | 1395 | ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); 1396 | return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); 1397 | }, 1398 | array: array, 1399 | nodelist: array, 1400 | arguments: array, 1401 | object:function( map, stack ) { 1402 | var ret = [ ]; 1403 | QUnit.jsDump.up(); 1404 | for ( var key in map ) { 1405 | var val = map[key]; 1406 | ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(val, undefined, stack)); 1407 | } 1408 | QUnit.jsDump.down(); 1409 | return join( '{', ret, '}' ); 1410 | }, 1411 | node:function( node ) { 1412 | var open = QUnit.jsDump.HTML ? '<' : '<', 1413 | close = QUnit.jsDump.HTML ? '>' : '>'; 1414 | 1415 | var tag = node.nodeName.toLowerCase(), 1416 | ret = open + tag; 1417 | 1418 | for ( var a in QUnit.jsDump.DOMAttrs ) { 1419 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1420 | if ( val ) 1421 | ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); 1422 | } 1423 | return ret + close + open + '/' + tag + close; 1424 | }, 1425 | functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function 1426 | var l = fn.length; 1427 | if ( !l ) return ''; 1428 | 1429 | var args = Array(l); 1430 | while ( l-- ) 1431 | args[l] = String.fromCharCode(97+l);//97 is 'a' 1432 | return ' ' + args.join(', ') + ' '; 1433 | }, 1434 | key:quote, //object calls it internally, the key part of an item in a map 1435 | functionCode:'[code]', //function calls it internally, it's the content of the function 1436 | attribute:quote, //node calls it internally, it's an html attribute value 1437 | string:quote, 1438 | date:quote, 1439 | regexp:literal, //regex 1440 | number:literal, 1441 | 'boolean':literal 1442 | }, 1443 | DOMAttrs:{//attributes to dump from nodes, name=>realName 1444 | id:'id', 1445 | name:'name', 1446 | 'class':'className' 1447 | }, 1448 | HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) 1449 | indentChar:' ',//indentation unit 1450 | multiline:true //if true, items in a collection, are separated by a \n, else just a space. 1451 | }; 1452 | 1453 | return jsDump; 1454 | })(); 1455 | 1456 | // from Sizzle.js 1457 | function getText( elems ) { 1458 | var ret = "", elem; 1459 | 1460 | for ( var i = 0; elems[i]; i++ ) { 1461 | elem = elems[i]; 1462 | 1463 | // Get the text from text nodes and CDATA nodes 1464 | if ( elem.nodeType === 3 || elem.nodeType === 4 ) { 1465 | ret += elem.nodeValue; 1466 | 1467 | // Traverse everything else, except comment nodes 1468 | } else if ( elem.nodeType !== 8 ) { 1469 | ret += getText( elem.childNodes ); 1470 | } 1471 | } 1472 | 1473 | return ret; 1474 | }; 1475 | 1476 | //from jquery.js 1477 | function inArray( elem, array ) { 1478 | if ( array.indexOf ) { 1479 | return array.indexOf( elem ); 1480 | } 1481 | 1482 | for ( var i = 0, length = array.length; i < length; i++ ) { 1483 | if ( array[ i ] === elem ) { 1484 | return i; 1485 | } 1486 | } 1487 | 1488 | return -1; 1489 | } 1490 | 1491 | /* 1492 | * Javascript Diff Algorithm 1493 | * By John Resig (http://ejohn.org/) 1494 | * Modified by Chu Alan "sprite" 1495 | * 1496 | * Released under the MIT license. 1497 | * 1498 | * More Info: 1499 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1500 | * 1501 | * Usage: QUnit.diff(expected, actual) 1502 | * 1503 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1504 | */ 1505 | QUnit.diff = (function() { 1506 | function diff(o, n) { 1507 | var ns = {}; 1508 | var os = {}; 1509 | 1510 | for (var i = 0; i < n.length; i++) { 1511 | if (ns[n[i]] == null) 1512 | ns[n[i]] = { 1513 | rows: [], 1514 | o: null 1515 | }; 1516 | ns[n[i]].rows.push(i); 1517 | } 1518 | 1519 | for (var i = 0; i < o.length; i++) { 1520 | if (os[o[i]] == null) 1521 | os[o[i]] = { 1522 | rows: [], 1523 | n: null 1524 | }; 1525 | os[o[i]].rows.push(i); 1526 | } 1527 | 1528 | for (var i in ns) { 1529 | if ( !hasOwn.call( ns, i ) ) { 1530 | continue; 1531 | } 1532 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1533 | n[ns[i].rows[0]] = { 1534 | text: n[ns[i].rows[0]], 1535 | row: os[i].rows[0] 1536 | }; 1537 | o[os[i].rows[0]] = { 1538 | text: o[os[i].rows[0]], 1539 | row: ns[i].rows[0] 1540 | }; 1541 | } 1542 | } 1543 | 1544 | for (var i = 0; i < n.length - 1; i++) { 1545 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && 1546 | n[i + 1] == o[n[i].row + 1]) { 1547 | n[i + 1] = { 1548 | text: n[i + 1], 1549 | row: n[i].row + 1 1550 | }; 1551 | o[n[i].row + 1] = { 1552 | text: o[n[i].row + 1], 1553 | row: i + 1 1554 | }; 1555 | } 1556 | } 1557 | 1558 | for (var i = n.length - 1; i > 0; i--) { 1559 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && 1560 | n[i - 1] == o[n[i].row - 1]) { 1561 | n[i - 1] = { 1562 | text: n[i - 1], 1563 | row: n[i].row - 1 1564 | }; 1565 | o[n[i].row - 1] = { 1566 | text: o[n[i].row - 1], 1567 | row: i - 1 1568 | }; 1569 | } 1570 | } 1571 | 1572 | return { 1573 | o: o, 1574 | n: n 1575 | }; 1576 | } 1577 | 1578 | return function(o, n) { 1579 | o = o.replace(/\s+$/, ''); 1580 | n = n.replace(/\s+$/, ''); 1581 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1582 | 1583 | var str = ""; 1584 | 1585 | var oSpace = o.match(/\s+/g); 1586 | if (oSpace == null) { 1587 | oSpace = [" "]; 1588 | } 1589 | else { 1590 | oSpace.push(" "); 1591 | } 1592 | var nSpace = n.match(/\s+/g); 1593 | if (nSpace == null) { 1594 | nSpace = [" "]; 1595 | } 1596 | else { 1597 | nSpace.push(" "); 1598 | } 1599 | 1600 | if (out.n.length == 0) { 1601 | for (var i = 0; i < out.o.length; i++) { 1602 | str += '' + out.o[i] + oSpace[i] + ""; 1603 | } 1604 | } 1605 | else { 1606 | if (out.n[0].text == null) { 1607 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1608 | str += '' + out.o[n] + oSpace[n] + ""; 1609 | } 1610 | } 1611 | 1612 | for (var i = 0; i < out.n.length; i++) { 1613 | if (out.n[i].text == null) { 1614 | str += '' + out.n[i] + nSpace[i] + ""; 1615 | } 1616 | else { 1617 | var pre = ""; 1618 | 1619 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1620 | pre += '' + out.o[n] + oSpace[n] + ""; 1621 | } 1622 | str += " " + out.n[i].text + nSpace[i] + pre; 1623 | } 1624 | } 1625 | } 1626 | 1627 | return str; 1628 | }; 1629 | })(); 1630 | 1631 | // get at whatever the global object is, like window in browsers 1632 | })( (function() {return this}).call() ); 1633 | --------------------------------------------------------------------------------