OLD | NEW |
| (Empty) |
1 /* | |
2 * Copyright (C) 2007 Apple Inc. All rights reserved. | |
3 * | |
4 * Redistribution and use in source and binary forms, with or without | |
5 * modification, are permitted provided that the following conditions | |
6 * are met: | |
7 * | |
8 * 1. Redistributions of source code must retain the above copyright | |
9 * notice, this list of conditions and the following disclaimer. | |
10 * 2. Redistributions in binary form must reproduce the above copyright | |
11 * notice, this list of conditions and the following disclaimer in the | |
12 * documentation and/or other materials provided with the distribution. | |
13 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of | |
14 * its contributors may be used to endorse or promote products derived | |
15 * from this software without specific prior written permission. | |
16 * | |
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY | |
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY | |
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | |
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF | |
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
27 */ | |
28 | |
29 Object.proxyType = function(objectProxy) | |
30 { | |
31 if (objectProxy === null) | |
32 return "null"; | |
33 | |
34 var type = typeof objectProxy; | |
35 if (type !== "object" && type !== "function") | |
36 return type; | |
37 | |
38 return objectProxy.type; | |
39 } | |
40 | |
41 Object.properties = function(obj) | |
42 { | |
43 var properties = []; | |
44 for (var prop in obj) | |
45 properties.push(prop); | |
46 return properties; | |
47 } | |
48 | |
49 Object.sortedProperties = function(obj, sortFunc) | |
50 { | |
51 return Object.properties(obj).sort(sortFunc); | |
52 } | |
53 | |
54 Function.prototype.bind = function(thisObject) | |
55 { | |
56 var func = this; | |
57 var args = Array.prototype.slice.call(arguments, 1); | |
58 return function() { return func.apply(thisObject, args.concat(Array.prototyp
e.slice.call(arguments, 0))) }; | |
59 } | |
60 | |
61 Node.prototype.rangeOfWord = function(offset, stopCharacters, stayWithinNode, di
rection) | |
62 { | |
63 var startNode; | |
64 var startOffset = 0; | |
65 var endNode; | |
66 var endOffset = 0; | |
67 | |
68 if (!stayWithinNode) | |
69 stayWithinNode = this; | |
70 | |
71 if (!direction || direction === "backward" || direction === "both") { | |
72 var node = this; | |
73 while (node) { | |
74 if (node === stayWithinNode) { | |
75 if (!startNode) | |
76 startNode = stayWithinNode; | |
77 break; | |
78 } | |
79 | |
80 if (node.nodeType === Node.TEXT_NODE) { | |
81 var start = (node === this ? (offset - 1) : (node.nodeValue.leng
th - 1)); | |
82 for (var i = start; i >= 0; --i) { | |
83 if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) { | |
84 startNode = node; | |
85 startOffset = i + 1; | |
86 break; | |
87 } | |
88 } | |
89 } | |
90 | |
91 if (startNode) | |
92 break; | |
93 | |
94 node = node.traversePreviousNode(stayWithinNode); | |
95 } | |
96 | |
97 if (!startNode) { | |
98 startNode = stayWithinNode; | |
99 startOffset = 0; | |
100 } | |
101 } else { | |
102 startNode = this; | |
103 startOffset = offset; | |
104 } | |
105 | |
106 if (!direction || direction === "forward" || direction === "both") { | |
107 node = this; | |
108 while (node) { | |
109 if (node === stayWithinNode) { | |
110 if (!endNode) | |
111 endNode = stayWithinNode; | |
112 break; | |
113 } | |
114 | |
115 if (node.nodeType === Node.TEXT_NODE) { | |
116 var start = (node === this ? offset : 0); | |
117 for (var i = start; i < node.nodeValue.length; ++i) { | |
118 if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) { | |
119 endNode = node; | |
120 endOffset = i; | |
121 break; | |
122 } | |
123 } | |
124 } | |
125 | |
126 if (endNode) | |
127 break; | |
128 | |
129 node = node.traverseNextNode(stayWithinNode); | |
130 } | |
131 | |
132 if (!endNode) { | |
133 endNode = stayWithinNode; | |
134 endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinN
ode.nodeValue.length : stayWithinNode.childNodes.length; | |
135 } | |
136 } else { | |
137 endNode = this; | |
138 endOffset = offset; | |
139 } | |
140 | |
141 var result = this.ownerDocument.createRange(); | |
142 result.setStart(startNode, startOffset); | |
143 result.setEnd(endNode, endOffset); | |
144 | |
145 return result; | |
146 } | |
147 | |
148 Element.prototype.removeStyleClass = function(className) | |
149 { | |
150 // Test for the simple case before using a RegExp. | |
151 if (this.className === className) { | |
152 this.className = ""; | |
153 return; | |
154 } | |
155 | |
156 this.removeMatchingStyleClasses(className.escapeForRegExp()); | |
157 } | |
158 | |
159 Element.prototype.removeMatchingStyleClasses = function(classNameRegex) | |
160 { | |
161 var regex = new RegExp("(^|\\s+)" + classNameRegex + "($|\\s+)"); | |
162 if (regex.test(this.className)) | |
163 this.className = this.className.replace(regex, " "); | |
164 } | |
165 | |
166 Element.prototype.addStyleClass = function(className) | |
167 { | |
168 if (className && !this.hasStyleClass(className)) | |
169 this.className += (this.className.length ? " " + className : className); | |
170 } | |
171 | |
172 Element.prototype.hasStyleClass = function(className) | |
173 { | |
174 if (!className) | |
175 return false; | |
176 // Test for the simple case before using a RegExp. | |
177 if (this.className === className) | |
178 return true; | |
179 var regex = new RegExp("(^|\\s)" + className.escapeForRegExp() + "($|\\s)"); | |
180 return regex.test(this.className); | |
181 } | |
182 | |
183 Element.prototype.positionAt = function(x, y) | |
184 { | |
185 this.style.left = x + "px"; | |
186 this.style.top = y + "px"; | |
187 } | |
188 | |
189 Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray) | |
190 { | |
191 for (var node = this; node && node !== this.ownerDocument; node = node.paren
tNode) | |
192 for (var i = 0; i < nameArray.length; ++i) | |
193 if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase()) | |
194 return node; | |
195 return null; | |
196 } | |
197 | |
198 Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName) | |
199 { | |
200 return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]); | |
201 } | |
202 | |
203 Node.prototype.enclosingNodeOrSelfWithClass = function(className) | |
204 { | |
205 for (var node = this; node && node !== this.ownerDocument; node = node.paren
tNode) | |
206 if (node.nodeType === Node.ELEMENT_NODE && node.hasStyleClass(className)
) | |
207 return node; | |
208 return null; | |
209 } | |
210 | |
211 Node.prototype.enclosingNodeWithClass = function(className) | |
212 { | |
213 if (!this.parentNode) | |
214 return null; | |
215 return this.parentNode.enclosingNodeOrSelfWithClass(className); | |
216 } | |
217 | |
218 Element.prototype.query = function(query) | |
219 { | |
220 return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDE
RED_NODE_TYPE, null).singleNodeValue; | |
221 } | |
222 | |
223 Element.prototype.removeChildren = function() | |
224 { | |
225 while (this.firstChild) | |
226 this.removeChild(this.firstChild); | |
227 } | |
228 | |
229 Element.prototype.isInsertionCaretInside = function() | |
230 { | |
231 var selection = window.getSelection(); | |
232 if (!selection.rangeCount || !selection.isCollapsed) | |
233 return false; | |
234 var selectionRange = selection.getRangeAt(0); | |
235 return selectionRange.startContainer === this || selectionRange.startContain
er.isDescendant(this); | |
236 } | |
237 | |
238 Element.prototype.__defineGetter__("totalOffsetLeft", function() | |
239 { | |
240 var total = 0; | |
241 for (var element = this; element; element = element.offsetParent) | |
242 total += element.offsetLeft; | |
243 return total; | |
244 }); | |
245 | |
246 Element.prototype.__defineGetter__("totalOffsetTop", function() | |
247 { | |
248 var total = 0; | |
249 for (var element = this; element; element = element.offsetParent) | |
250 total += element.offsetTop; | |
251 return total; | |
252 }); | |
253 | |
254 Element.prototype.offsetRelativeToWindow = function(targetWindow) | |
255 { | |
256 var elementOffset = {x: 0, y: 0}; | |
257 var curElement = this; | |
258 var curWindow = this.ownerDocument.defaultView; | |
259 while (curWindow && curElement) { | |
260 elementOffset.x += curElement.totalOffsetLeft; | |
261 elementOffset.y += curElement.totalOffsetTop; | |
262 if (curWindow === targetWindow) | |
263 break; | |
264 | |
265 curElement = curWindow.frameElement; | |
266 curWindow = curWindow.parent; | |
267 } | |
268 | |
269 return elementOffset; | |
270 } | |
271 | |
272 Node.prototype.isWhitespace = isNodeWhitespace; | |
273 Node.prototype.displayName = nodeDisplayName; | |
274 Node.prototype.isAncestor = function(node) | |
275 { | |
276 return isAncestorNode(this, node); | |
277 }; | |
278 Node.prototype.isDescendant = isDescendantNode; | |
279 Node.prototype.traverseNextNode = traverseNextNode; | |
280 Node.prototype.traversePreviousNode = traversePreviousNode; | |
281 Node.prototype.onlyTextChild = onlyTextChild; | |
282 | |
283 String.prototype.hasSubstring = function(string, caseInsensitive) | |
284 { | |
285 if (!caseInsensitive) | |
286 return this.indexOf(string) !== -1; | |
287 return this.match(new RegExp(string.escapeForRegExp(), "i")); | |
288 } | |
289 | |
290 String.prototype.escapeCharacters = function(chars) | |
291 { | |
292 var foundChar = false; | |
293 for (var i = 0; i < chars.length; ++i) { | |
294 if (this.indexOf(chars.charAt(i)) !== -1) { | |
295 foundChar = true; | |
296 break; | |
297 } | |
298 } | |
299 | |
300 if (!foundChar) | |
301 return this; | |
302 | |
303 var result = ""; | |
304 for (var i = 0; i < this.length; ++i) { | |
305 if (chars.indexOf(this.charAt(i)) !== -1) | |
306 result += "\\"; | |
307 result += this.charAt(i); | |
308 } | |
309 | |
310 return result; | |
311 } | |
312 | |
313 String.prototype.escapeForRegExp = function() | |
314 { | |
315 return this.escapeCharacters("^[]{}()\\.$*+?|"); | |
316 } | |
317 | |
318 String.prototype.escapeHTML = function() | |
319 { | |
320 return this.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">
"); | |
321 } | |
322 | |
323 String.prototype.collapseWhitespace = function() | |
324 { | |
325 return this.replace(/[\s\xA0]+/g, " "); | |
326 } | |
327 | |
328 String.prototype.trimLeadingWhitespace = function() | |
329 { | |
330 return this.replace(/^[\s\xA0]+/g, ""); | |
331 } | |
332 | |
333 String.prototype.trimTrailingWhitespace = function() | |
334 { | |
335 return this.replace(/[\s\xA0]+$/g, ""); | |
336 } | |
337 | |
338 String.prototype.trimWhitespace = function() | |
339 { | |
340 return this.replace(/^[\s\xA0]+|[\s\xA0]+$/g, ""); | |
341 } | |
342 | |
343 String.prototype.trimURL = function(baseURLDomain) | |
344 { | |
345 var result = this.replace(new RegExp("^http[s]?:\/\/", "i"), ""); | |
346 if (baseURLDomain) | |
347 result = result.replace(new RegExp("^" + baseURLDomain.escapeForRegExp()
, "i"), ""); | |
348 return result; | |
349 } | |
350 | |
351 function isNodeWhitespace() | |
352 { | |
353 if (!this || this.nodeType !== Node.TEXT_NODE) | |
354 return false; | |
355 if (!this.nodeValue.length) | |
356 return true; | |
357 return this.nodeValue.match(/^[\s\xA0]+$/); | |
358 } | |
359 | |
360 function nodeDisplayName() | |
361 { | |
362 if (!this) | |
363 return ""; | |
364 | |
365 switch (this.nodeType) { | |
366 case Node.DOCUMENT_NODE: | |
367 return "Document"; | |
368 | |
369 case Node.ELEMENT_NODE: | |
370 var name = "<" + this.nodeName.toLowerCase(); | |
371 | |
372 if (this.hasAttributes()) { | |
373 var value = this.getAttribute("id"); | |
374 if (value) | |
375 name += " id=\"" + value + "\""; | |
376 value = this.getAttribute("class"); | |
377 if (value) | |
378 name += " class=\"" + value + "\""; | |
379 if (this.nodeName.toLowerCase() === "a") { | |
380 value = this.getAttribute("name"); | |
381 if (value) | |
382 name += " name=\"" + value + "\""; | |
383 value = this.getAttribute("href"); | |
384 if (value) | |
385 name += " href=\"" + value + "\""; | |
386 } else if (this.nodeName.toLowerCase() === "img") { | |
387 value = this.getAttribute("src"); | |
388 if (value) | |
389 name += " src=\"" + value + "\""; | |
390 } else if (this.nodeName.toLowerCase() === "iframe") { | |
391 value = this.getAttribute("src"); | |
392 if (value) | |
393 name += " src=\"" + value + "\""; | |
394 } else if (this.nodeName.toLowerCase() === "input") { | |
395 value = this.getAttribute("name"); | |
396 if (value) | |
397 name += " name=\"" + value + "\""; | |
398 value = this.getAttribute("type"); | |
399 if (value) | |
400 name += " type=\"" + value + "\""; | |
401 } else if (this.nodeName.toLowerCase() === "form") { | |
402 value = this.getAttribute("action"); | |
403 if (value) | |
404 name += " action=\"" + value + "\""; | |
405 } | |
406 } | |
407 | |
408 return name + ">"; | |
409 | |
410 case Node.TEXT_NODE: | |
411 if (isNodeWhitespace.call(this)) | |
412 return "(whitespace)"; | |
413 return "\"" + this.nodeValue + "\""; | |
414 | |
415 case Node.COMMENT_NODE: | |
416 return "<!--" + this.nodeValue + "-->"; | |
417 | |
418 case Node.DOCUMENT_TYPE_NODE: | |
419 var docType = "<!DOCTYPE " + this.nodeName; | |
420 if (this.publicId) { | |
421 docType += " PUBLIC \"" + this.publicId + "\""; | |
422 if (this.systemId) | |
423 docType += " \"" + this.systemId + "\""; | |
424 } else if (this.systemId) | |
425 docType += " SYSTEM \"" + this.systemId + "\""; | |
426 if (this.internalSubset) | |
427 docType += " [" + this.internalSubset + "]"; | |
428 return docType + ">"; | |
429 } | |
430 | |
431 return this.nodeName.toLowerCase().collapseWhitespace(); | |
432 } | |
433 | |
434 function isAncestorNode(ancestor, node) | |
435 { | |
436 if (!node || !ancestor) | |
437 return false; | |
438 | |
439 var currentNode = node.parentNode; | |
440 while (currentNode) { | |
441 if (ancestor === currentNode) | |
442 return true; | |
443 currentNode = currentNode.parentNode; | |
444 } | |
445 return false; | |
446 } | |
447 | |
448 function isDescendantNode(descendant) | |
449 { | |
450 return isAncestorNode(descendant, this); | |
451 } | |
452 | |
453 function traverseNextNode(stayWithin) | |
454 { | |
455 if (!this) | |
456 return; | |
457 | |
458 var node = this.firstChild; | |
459 if (node) | |
460 return node; | |
461 | |
462 if (stayWithin && this === stayWithin) | |
463 return null; | |
464 | |
465 node = this.nextSibling; | |
466 if (node) | |
467 return node; | |
468 | |
469 node = this; | |
470 while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node
.parentNode !== stayWithin)) | |
471 node = node.parentNode; | |
472 if (!node) | |
473 return null; | |
474 | |
475 return node.nextSibling; | |
476 } | |
477 | |
478 function traversePreviousNode(stayWithin) | |
479 { | |
480 if (!this) | |
481 return; | |
482 if (stayWithin && this === stayWithin) | |
483 return null; | |
484 var node = this.previousSibling; | |
485 while (node && node.lastChild) | |
486 node = node.lastChild; | |
487 if (node) | |
488 return node; | |
489 return this.parentNode; | |
490 } | |
491 | |
492 function onlyTextChild() | |
493 { | |
494 if (!this) | |
495 return null; | |
496 | |
497 var firstChild = this.firstChild; | |
498 if (!firstChild || firstChild.nodeType !== Node.TEXT_NODE) | |
499 return null; | |
500 | |
501 var sibling = firstChild.nextSibling; | |
502 return sibling ? null : firstChild; | |
503 } | |
504 | |
505 function appropriateSelectorForNode(node, justSelector) | |
506 { | |
507 if (!node) | |
508 return ""; | |
509 | |
510 var lowerCaseName = node.localName || node.nodeName.toLowerCase(); | |
511 | |
512 var id = node.getAttribute("id"); | |
513 if (id) { | |
514 var selector = "#" + id; | |
515 return (justSelector ? selector : lowerCaseName + selector); | |
516 } | |
517 | |
518 var className = node.getAttribute("class"); | |
519 if (className) { | |
520 var selector = "." + className.replace(/\s+/, "."); | |
521 return (justSelector ? selector : lowerCaseName + selector); | |
522 } | |
523 | |
524 if (lowerCaseName === "input" && node.getAttribute("type")) | |
525 return lowerCaseName + "[type=\"" + node.getAttribute("type") + "\"]"; | |
526 | |
527 return lowerCaseName; | |
528 } | |
529 | |
530 function getDocumentForNode(node) | |
531 { | |
532 return node.nodeType == Node.DOCUMENT_NODE ? node : node.ownerDocument; | |
533 } | |
534 | |
535 function parentNode(node) | |
536 { | |
537 return node.parentNode; | |
538 } | |
539 | |
540 Number.secondsToString = function(seconds, formatterFunction, higherResolution) | |
541 { | |
542 if (!formatterFunction) | |
543 formatterFunction = String.sprintf; | |
544 | |
545 var ms = seconds * 1000; | |
546 if (higherResolution && ms < 1000) | |
547 return formatterFunction("%.3fms", ms); | |
548 else if (ms < 1000) | |
549 return formatterFunction("%.0fms", ms); | |
550 | |
551 if (seconds < 60) | |
552 return formatterFunction("%.2fs", seconds); | |
553 | |
554 var minutes = seconds / 60; | |
555 if (minutes < 60) | |
556 return formatterFunction("%.1fmin", minutes); | |
557 | |
558 var hours = minutes / 60; | |
559 if (hours < 24) | |
560 return formatterFunction("%.1fhrs", hours); | |
561 | |
562 var days = hours / 24; | |
563 return formatterFunction("%.1f days", days); | |
564 } | |
565 | |
566 Number.bytesToString = function(bytes, formatterFunction, higherResolution) | |
567 { | |
568 if (!formatterFunction) | |
569 formatterFunction = String.sprintf; | |
570 if (typeof higherResolution === "undefined") | |
571 higherResolution = true; | |
572 | |
573 if (bytes < 1024) | |
574 return formatterFunction("%.0fB", bytes); | |
575 | |
576 var kilobytes = bytes / 1024; | |
577 if (higherResolution && kilobytes < 1024) | |
578 return formatterFunction("%.2fKB", kilobytes); | |
579 else if (kilobytes < 1024) | |
580 return formatterFunction("%.0fKB", kilobytes); | |
581 | |
582 var megabytes = kilobytes / 1024; | |
583 if (higherResolution) | |
584 return formatterFunction("%.3fMB", megabytes); | |
585 else | |
586 return formatterFunction("%.0fMB", megabytes); | |
587 } | |
588 | |
589 Number.constrain = function(num, min, max) | |
590 { | |
591 if (num < min) | |
592 num = min; | |
593 else if (num > max) | |
594 num = max; | |
595 return num; | |
596 } | |
597 | |
598 HTMLTextAreaElement.prototype.moveCursorToEnd = function() | |
599 { | |
600 var length = this.value.length; | |
601 this.setSelectionRange(length, length); | |
602 } | |
603 | |
604 Array.prototype.remove = function(value, onlyFirst) | |
605 { | |
606 if (onlyFirst) { | |
607 var index = this.indexOf(value); | |
608 if (index !== -1) | |
609 this.splice(index, 1); | |
610 return; | |
611 } | |
612 | |
613 var length = this.length; | |
614 for (var i = 0; i < length; ++i) { | |
615 if (this[i] === value) | |
616 this.splice(i, 1); | |
617 } | |
618 } | |
619 | |
620 function insertionIndexForObjectInListSortedByFunction(anObject, aList, aFunctio
n) | |
621 { | |
622 // indexOf returns (-lowerBound - 1). Taking (-result - 1) works out to lowe
rBound. | |
623 return (-indexOfObjectInListSortedByFunction(anObject, aList, aFunction) - 1
); | |
624 } | |
625 | |
626 function indexOfObjectInListSortedByFunction(anObject, aList, aFunction) | |
627 { | |
628 var first = 0; | |
629 var last = aList.length - 1; | |
630 var floor = Math.floor; | |
631 var mid, c; | |
632 | |
633 while (first <= last) { | |
634 mid = floor((first + last) / 2); | |
635 c = aFunction(anObject, aList[mid]); | |
636 | |
637 if (c > 0) | |
638 first = mid + 1; | |
639 else if (c < 0) | |
640 last = mid - 1; | |
641 else { | |
642 // Return the first occurance of an item in the list. | |
643 while (mid > 0 && aFunction(anObject, aList[mid - 1]) === 0) | |
644 mid--; | |
645 first = mid; | |
646 break; | |
647 } | |
648 } | |
649 | |
650 // By returning 1 less than the negative lower search bound, we can reuse th
is function | |
651 // for both indexOf and insertionIndexFor, with some simple arithmetic. | |
652 return (-first - 1); | |
653 } | |
654 | |
655 String.sprintf = function(format) | |
656 { | |
657 return String.vsprintf(format, Array.prototype.slice.call(arguments, 1)); | |
658 } | |
659 | |
660 String.tokenizeFormatString = function(format) | |
661 { | |
662 var tokens = []; | |
663 var substitutionIndex = 0; | |
664 | |
665 function addStringToken(str) | |
666 { | |
667 tokens.push({ type: "string", value: str }); | |
668 } | |
669 | |
670 function addSpecifierToken(specifier, precision, substitutionIndex) | |
671 { | |
672 tokens.push({ type: "specifier", specifier: specifier, precision: precis
ion, substitutionIndex: substitutionIndex }); | |
673 } | |
674 | |
675 var index = 0; | |
676 for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; pre
centIndex = format.indexOf("%", index)) { | |
677 addStringToken(format.substring(index, precentIndex)); | |
678 index = precentIndex + 1; | |
679 | |
680 if (format[index] === "%") { | |
681 addStringToken("%"); | |
682 ++index; | |
683 continue; | |
684 } | |
685 | |
686 if (!isNaN(format[index])) { | |
687 // The first character is a number, it might be a substitution index
. | |
688 var number = parseInt(format.substring(index)); | |
689 while (!isNaN(format[index])) | |
690 ++index; | |
691 // If the number is greater than zero and ends with a "$", | |
692 // then this is a substitution index. | |
693 if (number > 0 && format[index] === "$") { | |
694 substitutionIndex = (number - 1); | |
695 ++index; | |
696 } | |
697 } | |
698 | |
699 var precision = -1; | |
700 if (format[index] === ".") { | |
701 // This is a precision specifier. If no digit follows the ".", | |
702 // then the precision should be zero. | |
703 ++index; | |
704 precision = parseInt(format.substring(index)); | |
705 if (isNaN(precision)) | |
706 precision = 0; | |
707 while (!isNaN(format[index])) | |
708 ++index; | |
709 } | |
710 | |
711 addSpecifierToken(format[index], precision, substitutionIndex); | |
712 | |
713 ++substitutionIndex; | |
714 ++index; | |
715 } | |
716 | |
717 addStringToken(format.substring(index)); | |
718 | |
719 return tokens; | |
720 } | |
721 | |
722 String.standardFormatters = { | |
723 d: function(substitution) | |
724 { | |
725 if (typeof substitution == "object" && Object.proxyType(substitution) ==
= "number") | |
726 substitution = substitution.description; | |
727 substitution = parseInt(substitution); | |
728 return !isNaN(substitution) ? substitution : 0; | |
729 }, | |
730 | |
731 f: function(substitution, token) | |
732 { | |
733 if (typeof substitution == "object" && Object.proxyType(substitution) ==
= "number") | |
734 substitution = substitution.description; | |
735 substitution = parseFloat(substitution); | |
736 if (substitution && token.precision > -1) | |
737 substitution = substitution.toFixed(token.precision); | |
738 return !isNaN(substitution) ? substitution : (token.precision > -1 ? Num
ber(0).toFixed(token.precision) : 0); | |
739 }, | |
740 | |
741 s: function(substitution) | |
742 { | |
743 if (typeof substitution == "object" && Object.proxyType(substitution) !=
= "null") | |
744 substitution = substitution.description; | |
745 return substitution; | |
746 }, | |
747 }; | |
748 | |
749 String.vsprintf = function(format, substitutions) | |
750 { | |
751 return String.format(format, substitutions, String.standardFormatters, "", f
unction(a, b) { return a + b; }).formattedResult; | |
752 } | |
753 | |
754 String.format = function(format, substitutions, formatters, initialValue, append
) | |
755 { | |
756 if (!format || !substitutions || !substitutions.length) | |
757 return { formattedResult: append(initialValue, format), unusedSubstituti
ons: substitutions }; | |
758 | |
759 function prettyFunctionName() | |
760 { | |
761 return "String.format(\"" + format + "\", \"" + substitutions.join("\",
\"") + "\")"; | |
762 } | |
763 | |
764 function warn(msg) | |
765 { | |
766 console.warn(prettyFunctionName() + ": " + msg); | |
767 } | |
768 | |
769 function error(msg) | |
770 { | |
771 console.error(prettyFunctionName() + ": " + msg); | |
772 } | |
773 | |
774 var result = initialValue; | |
775 var tokens = String.tokenizeFormatString(format); | |
776 var usedSubstitutionIndexes = {}; | |
777 | |
778 for (var i = 0; i < tokens.length; ++i) { | |
779 var token = tokens[i]; | |
780 | |
781 if (token.type === "string") { | |
782 result = append(result, token.value); | |
783 continue; | |
784 } | |
785 | |
786 if (token.type !== "specifier") { | |
787 error("Unknown token type \"" + token.type + "\" found."); | |
788 continue; | |
789 } | |
790 | |
791 if (token.substitutionIndex >= substitutions.length) { | |
792 // If there are not enough substitutions for the current substitutio
nIndex | |
793 // just output the format specifier literally and move on. | |
794 error("not enough substitution arguments. Had " + substitutions.leng
th + " but needed " + (token.substitutionIndex + 1) + ", so substitution was ski
pped."); | |
795 result = append(result, "%" + (token.precision > -1 ? token.precisio
n : "") + token.specifier); | |
796 continue; | |
797 } | |
798 | |
799 usedSubstitutionIndexes[token.substitutionIndex] = true; | |
800 | |
801 if (!(token.specifier in formatters)) { | |
802 // Encountered an unsupported format character, treat as a string. | |
803 warn("unsupported format character \u201C" + token.specifier + "\u20
1D. Treating as a string."); | |
804 result = append(result, substitutions[token.substitutionIndex]); | |
805 continue; | |
806 } | |
807 | |
808 result = append(result, formatters[token.specifier](substitutions[token.
substitutionIndex], token)); | |
809 } | |
810 | |
811 var unusedSubstitutions = []; | |
812 for (var i = 0; i < substitutions.length; ++i) { | |
813 if (i in usedSubstitutionIndexes) | |
814 continue; | |
815 unusedSubstitutions.push(substitutions[i]); | |
816 } | |
817 | |
818 return { formattedResult: result, unusedSubstitutions: unusedSubstitutions }
; | |
819 } | |
OLD | NEW |