├── 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 |
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 | '' +
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 += '| Expected: | ' + expected + ' |
';
671 | if (actual != expected) {
672 | output += '| Result: | ' + actual + ' |
';
673 | output += '| Diff: | ' + QUnit.diff(expected, actual) +' |
';
674 | }
675 | var source = sourceFromStacktrace();
676 | if (source) {
677 | details.source = source;
678 | output += '| Source: | ' + escapeInnerText(source) + ' |
';
679 | }
680 | output += "
";
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 |
--------------------------------------------------------------------------------