better Ox.isEqual(), more tests, more documentation
This commit is contained in:
parent
a1ed6a44c5
commit
37219bfbe9
5 changed files with 466 additions and 252 deletions
|
@ -63,8 +63,8 @@ Ox.load('UI', {
|
|||
.html(
|
||||
Ox.repeat(' ', 4) +
|
||||
'<b>> ' + Ox.encodeHTML(test.statement) + ' </b> ==> ' +
|
||||
Ox.encodeHTML(test.actual) +
|
||||
(test.success ? '' : ' !=> ' + Ox.encodeHTML(test.expected))
|
||||
(test.success ? '' : Ox.encodeHTML(test.actual) + ' !=> ') +
|
||||
Ox.encodeHTML(test.expected)
|
||||
)
|
||||
.hide()
|
||||
.appendTo($foo);
|
||||
|
@ -78,7 +78,10 @@ Ox.load('UI', {
|
|||
height: '15px',
|
||||
padding: '4px 8px 4px 8px',
|
||||
fontFamily: 'Menlo, Monaco, Courier',
|
||||
fontSize: '12px'
|
||||
fontSize: '12px',
|
||||
whiteSpace: 'nowrap',
|
||||
MozUserSelect: 'text',
|
||||
WebkitUserSelect: 'text'
|
||||
});
|
||||
gradients.forEach(function(gradient) {
|
||||
$div.css({
|
||||
|
|
|
@ -254,6 +254,18 @@ Dialog
|
|||
cursor: se-resize;
|
||||
}
|
||||
|
||||
/*
|
||||
================================================================================
|
||||
Documentation
|
||||
================================================================================
|
||||
*/
|
||||
|
||||
.OxDocPage code {
|
||||
//border: 1px solid rgb(232, 232, 232);
|
||||
//background: rgb(248, 248, 248);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/*
|
||||
================================================================================
|
||||
Drag & Drop
|
||||
|
|
|
@ -6,7 +6,7 @@ Ox.DocPage = function(options, self) {
|
|||
item: {}
|
||||
})
|
||||
.options(options || {})
|
||||
.addClass('OxText')
|
||||
.addClass('OxDocPage OxText')
|
||||
.css({
|
||||
overflow: 'auto'
|
||||
});
|
||||
|
@ -42,8 +42,13 @@ Ox.DocPage = function(options, self) {
|
|||
if (item[section]) {
|
||||
if (section == 'description') {
|
||||
$elements.push($('<div>')
|
||||
.css({paddingLeft: (level * 32) + 'px'})
|
||||
.html(item.description)
|
||||
.css({
|
||||
paddingTop: (level ? 0 : 8) + 'px',
|
||||
borderTop: level ? '': '1px solid rgb(192, 192, 192)',
|
||||
marginTop: (level ? 0 : 8) + 'px',
|
||||
marginLeft: (level * 32) + 'px',
|
||||
})
|
||||
.html(Ox.parseHTML(item.description))
|
||||
);
|
||||
} else {
|
||||
$elements.push($('<div>')
|
||||
|
@ -108,7 +113,7 @@ Ox.DocPage = function(options, self) {
|
|||
.addClass(className)
|
||||
.css({marginLeft: (level * 32 + 16) + 'px'})
|
||||
.html(
|
||||
'<code>' + Ox.parseHTML(example.result) + '</code>'
|
||||
'<code>' + Ox.encodeHTML(example.result) + '</code>'
|
||||
)
|
||||
)
|
||||
});
|
||||
|
|
|
@ -39,7 +39,7 @@ Ox.SyntaxHighlighter = function(options, self) {
|
|||
!(self.options.stripComments && token.type == 'comment')
|
||||
) {
|
||||
classNames = 'Ox' + Ox.toTitleCase(token.type);
|
||||
if (token.type == 'whitespace') {
|
||||
if (self.options.showWhitespace && token.type == 'whitespace') {
|
||||
if (isAfterLinebreak() && hasIrregularSpaces()) {
|
||||
classNames += ' OxLeading'
|
||||
} else if (isBeforeLinebreak()) {
|
||||
|
@ -65,27 +65,15 @@ Ox.SyntaxHighlighter = function(options, self) {
|
|||
}
|
||||
});
|
||||
self.lines = self.source.split('<br/>');
|
||||
self.lineNumbersWidth = (self.lines.length + self.options.offset - 1).toString().length * 7;
|
||||
self.sourceCodeWidth = 80 * 7 + (
|
||||
self.lines.length > 40 ? Ox.UI.SCROLLBAR_SIZE : 0
|
||||
);
|
||||
self.height = 40 * 14 + (
|
||||
Math.max.apply(null, self.lines.map(function(line) {
|
||||
return line.length;
|
||||
})) > 80 ? Ox.UI.SCROLLBAR_SIZE : 0
|
||||
);
|
||||
|
||||
that.css({
|
||||
//width: self.lineNumbersWidth + self.sourceCodeWidth,
|
||||
//height: self.height
|
||||
});
|
||||
self.lineNumbersWidth = (
|
||||
self.lines.length + self.options.offset - 1
|
||||
).toString().length * 7;
|
||||
|
||||
self.$lineNumbers = new Ox.Element()
|
||||
.addClass('OxLineNumbers')
|
||||
.css({
|
||||
display: 'table-cell',
|
||||
width: self.lineNumbersWidth + 'px',
|
||||
//height: (self.lines.length * 14) + 8 + 'px',
|
||||
padding: '4px',
|
||||
})
|
||||
.html(
|
||||
|
@ -98,8 +86,6 @@ Ox.SyntaxHighlighter = function(options, self) {
|
|||
.addClass('OxSourceCode')
|
||||
.css({
|
||||
display: 'table-cell',
|
||||
//width: self.sourceCodeWidth + 'px',
|
||||
//height: (self.lines.length * 14) + 'px',
|
||||
padding: '4px'
|
||||
})
|
||||
.html(self.source)
|
||||
|
|
662
source/Ox.js
662
source/Ox.js
|
@ -2,6 +2,34 @@
|
|||
|
||||
// OxJS (c) 2011 Ox2620, dual-licensed GPL/MIT, see http://oxjs.org for details
|
||||
|
||||
/*
|
||||
Some conventions:
|
||||
Functions
|
||||
- only one var statement, in the first line of the function
|
||||
- return only once, from the last line of the function body
|
||||
Variable names
|
||||
arg argument
|
||||
args arguments
|
||||
arr array
|
||||
callback callback function
|
||||
col collection (array, string or object)
|
||||
date date
|
||||
fn function
|
||||
hasFoo boolean
|
||||
i index (integer key)
|
||||
isFoo boolean
|
||||
k key (of a key/value pair)
|
||||
key key (of a key/value pair)
|
||||
max maximum value
|
||||
min minumum value
|
||||
num number
|
||||
obj object
|
||||
re regexp
|
||||
ret return value
|
||||
v value (of a key/value pair)
|
||||
val value (of a key/value pair)
|
||||
*/
|
||||
|
||||
// todo: check http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/
|
||||
// also see https://github.com/tlrobinson/narwhal/blob/master/lib/util.js
|
||||
|
||||
|
@ -164,6 +192,41 @@ Ox.merge = function(arr) {
|
|||
return arr;
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.sort <f> Sorts an array, handling leading digits and ignoring capitalization
|
||||
> Ox.sort(['10', '9', 'B', 'a'])
|
||||
['9', '10', 'a', 'B']
|
||||
@*/
|
||||
Ox.sort = function(arr) {
|
||||
var len, matches = {}, sort = {};
|
||||
// find leading numbers
|
||||
arr.forEach(function(val, i) {
|
||||
var match = /^\d+/(val);
|
||||
matches[val] = match ? match[0] : '';
|
||||
});
|
||||
// get length of longest leading number
|
||||
len = Ox.max(Ox.map(matches, function(val) {
|
||||
return val.length;
|
||||
}));
|
||||
// pad leading numbers, and make lower case
|
||||
arr.forEach(function(val) {
|
||||
sort[val] = (
|
||||
matches[val] ?
|
||||
Ox.pad(matches[val], len) + val.toString().substr(matches[val].length) :
|
||||
val
|
||||
).toLowerCase();
|
||||
});
|
||||
return arr.sort(function(a, b) {
|
||||
var ret = 0;
|
||||
if (sort[a] < sort[b]) {
|
||||
ret = -1;
|
||||
} else if (sort[a] > sort[b]) {
|
||||
ret = 1;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.unique <f> Returns an array without duplicate values
|
||||
> Ox.unique([1, 2, 3, 2, 1])
|
||||
|
@ -473,7 +536,7 @@ Ox.getset = function(obj, args, callback, context) {
|
|||
}
|
||||
|
||||
/*@
|
||||
Ox.isEmpty <f> Returns true if an array, object or string is empty
|
||||
Ox.isEmpty <f> Returns true if a collection is empty
|
||||
> Ox.isEmpty([])
|
||||
true
|
||||
> Ox.isEmpty({})
|
||||
|
@ -486,7 +549,7 @@ Ox.isEmpty = function(val) {
|
|||
};
|
||||
|
||||
/*@
|
||||
Ox.keys <f> Returns the keys of an array, object or string
|
||||
Ox.keys <f> Returns the keys of a collection
|
||||
Unlike <code>Object.keys()</code>, <code>Ox.keys()</code> works for arrays,
|
||||
objects and strings.
|
||||
> Ox.keys([1, 2, 3])
|
||||
|
@ -502,7 +565,6 @@ Ox.keys <f> Returns the keys of an array, object or string
|
|||
@*/
|
||||
|
||||
// fixme: is this really needed? arrays... ok... but strings??
|
||||
|
||||
Ox.keys = function(obj) {
|
||||
var keys = [];
|
||||
Ox.forEach(obj, function(v, k) {
|
||||
|
@ -727,27 +789,20 @@ Ox.range = function() {
|
|||
return arr;
|
||||
};
|
||||
|
||||
Ox.serialize = function(obj) {
|
||||
/*
|
||||
>>> Ox.serialize({a: 1, b: 2, c: 3})
|
||||
'a=1&b=2&c=3'
|
||||
*/
|
||||
var arr = [];
|
||||
Ox.forEach(obj, function(val, key) {
|
||||
val !== '' && arr.push(key + '=' + val);
|
||||
});
|
||||
return arr.join('&');
|
||||
};
|
||||
|
||||
Ox.setPropertyOnce = function(arr, str) {
|
||||
/*
|
||||
>>> Ox.setPropertyOnce([{selected: false}, {selected: false}], 'selected')
|
||||
/*@
|
||||
Ox.setPropertyOnce <f> Sets a property once
|
||||
Given a array of objects, each of which has a property with a boolean
|
||||
value, this sets exactly one of these to true, and returns the index
|
||||
of the object whose property is true.
|
||||
> Ox.setPropertyOnce([{selected: false}, {selected: false}], 'selected')
|
||||
0
|
||||
>>> Ox.setPropertyOnce([{selected: false}, {selected: true}], 'selected')
|
||||
> Ox.setPropertyOnce([{selected: false}, {selected: true}], 'selected')
|
||||
1
|
||||
>>> Ox.setPropertyOnce([{selected: true}, {selected: true}], 'selected')
|
||||
0
|
||||
*/
|
||||
> Ox.setPropertyOnce([{selected: true}, {selected: true}], 'selected')
|
||||
0
|
||||
@*/
|
||||
// fixme: strange name, and shouldn't it return the full array?
|
||||
Ox.setPropertyOnce = function(arr, str) {
|
||||
var pos = -1;
|
||||
Ox.forEach(arr, function(v, i) {
|
||||
if (pos == -1 && arr[i][str]) {
|
||||
|
@ -763,74 +818,72 @@ Ox.setPropertyOnce = function(arr, str) {
|
|||
return pos;
|
||||
};
|
||||
|
||||
Ox.shuffle = function(arr) {
|
||||
/*
|
||||
>>> Ox.shuffle([1, 2, 3]).length
|
||||
/*@
|
||||
Ox.shuffle <f> Randomizes the order of values within a collection
|
||||
> Ox.shuffle([1, 2, 3]).length
|
||||
3
|
||||
*/
|
||||
var shuffle = arr;
|
||||
return shuffle.sort(function() {
|
||||
> Ox.len(Ox.shuffle({a: 1, b: 2, c: 3}))
|
||||
3
|
||||
> Ox.shuffle('123').length
|
||||
3
|
||||
@*/
|
||||
|
||||
Ox.shuffle = function(col) {
|
||||
var keys, ret, type = Ox.typeOf(col), values;
|
||||
function sort() {
|
||||
return Math.random() - 0.5;
|
||||
});
|
||||
}
|
||||
if (type == 'array') {
|
||||
ret = col.sort(sort);
|
||||
} else if (type == 'object') {
|
||||
keys = Object.keys(col);
|
||||
values = Ox.values(col).sort(sort);
|
||||
ret = {};
|
||||
keys.forEach(function(key, i) {
|
||||
ret[key] = values[i]
|
||||
});
|
||||
} else if (type == 'string') {
|
||||
ret = col.split('').sort(sort).join('');
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.some <f> Tests if one or more elements of a collection satisfy a given condition
|
||||
Unlike <code>[].some()</code>, <code>Ox.some()</code> works for arrays,
|
||||
objects and strings.
|
||||
> Ox.some([2, 1, 0], function(i, v) { return i == v; })
|
||||
true
|
||||
> Ox.some({a: 1, b: 2, c: 3}, function(v) { return v == 1; })
|
||||
true
|
||||
> Ox.some("foo", function(v) { return v == 'f'; })
|
||||
true
|
||||
@*/
|
||||
Ox.some = function(obj, fn) {
|
||||
/*
|
||||
Ox.some() works for arrays, objects and strings, unlike [].some()
|
||||
>>> Ox.some([2, 1, 0], function(i, v) { return i == v; })
|
||||
true
|
||||
>>> Ox.some({a: 1, b: 2, c: 3}, function(v) { return v == 1; })
|
||||
true
|
||||
>>> Ox.some("foo", function(v) { return v == 'f'; })
|
||||
true
|
||||
*/
|
||||
return Ox.filter(Ox.values(obj), fn).length > 0;
|
||||
};
|
||||
|
||||
Ox.sort = function(arr) {
|
||||
/*
|
||||
>>> Ox.sort(['10', '9', 'B', 'a'])
|
||||
['9', '10', 'a', 'B']
|
||||
*/
|
||||
var len, matches = {}, sort = {};
|
||||
// find leading numbers
|
||||
arr.forEach(function(val, i) {
|
||||
var match = /^\d+/(val);
|
||||
matches[val] = match ? match[0] : '';
|
||||
});
|
||||
// get length of longest leading number
|
||||
len = Ox.max(Ox.map(matches, function(val) {
|
||||
return val.length;
|
||||
}));
|
||||
// pad leading numbers, and make lower case
|
||||
arr.forEach(function(val) {
|
||||
sort[val] = (
|
||||
matches[val] ?
|
||||
Ox.pad(matches[val], len) + val.toString().substr(matches[val].length) :
|
||||
val
|
||||
).toLowerCase();
|
||||
});
|
||||
return arr.sort(function(a, b) {
|
||||
var ret = 0;
|
||||
if (sort[a] < sort[b]) {
|
||||
ret = -1;
|
||||
} else if (sort[a] > sort[b]) {
|
||||
ret = 1;
|
||||
}
|
||||
return ret;
|
||||
});
|
||||
};
|
||||
|
||||
Ox.sum = function(obj) {
|
||||
/*
|
||||
>>> Ox.sum([-1, 0, 1])
|
||||
0
|
||||
>>> Ox.sum({a: 1, b: 2, c: 3})
|
||||
/*@
|
||||
Ox.sum <f> Returns the sum of the values of a collection
|
||||
> Ox.sum(1, 2, 3)
|
||||
6
|
||||
*/
|
||||
> Ox.sum([1, 2, 3])
|
||||
6
|
||||
> Ox.sum({a: 1, b: 2, c: 3})
|
||||
6
|
||||
> Ox.sum('123')
|
||||
6
|
||||
> Ox.sum('123foo')
|
||||
6
|
||||
> Ox.sum('08', -2, 'foo')
|
||||
6
|
||||
@*/
|
||||
Ox.sum = function(col) {
|
||||
var sum = 0;
|
||||
Ox.forEach(obj, function(val) {
|
||||
sum += val;
|
||||
col = arguments.length > 1 ? Ox.makeArray(arguments) : col;
|
||||
Ox.forEach(col, function(val) {
|
||||
val = +val;
|
||||
sum += Ox.isNumber(val) ? val : 0;
|
||||
});
|
||||
return sum;
|
||||
};
|
||||
|
@ -854,43 +907,39 @@ Ox.toArray = function(obj) {
|
|||
return arr;
|
||||
};
|
||||
|
||||
Ox.unserialize = function(str) {
|
||||
/*
|
||||
>>> Ox.unserialize('a=1&b=2&c=3').c
|
||||
'3'
|
||||
*/
|
||||
var arr, obj = {};
|
||||
Ox.forEach(str.split('&'), function(val) {
|
||||
arr = val.split('=');
|
||||
obj[arr[0]] = arr[1];
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
|
||||
Ox.values = function(obj) {
|
||||
/*
|
||||
>>> Ox.values([1, 2, 3])
|
||||
/*@
|
||||
Ox.values <f> Returns the values of a collection
|
||||
> Ox.values([1, 2, 3])
|
||||
[1, 2, 3]
|
||||
>>> Ox.values({a: 1, b: 2, c: 3})
|
||||
> Ox.values({a: 1, b: 2, c: 3})
|
||||
[1, 2, 3]
|
||||
>>> Ox.values('abc')
|
||||
> Ox.values('abc')
|
||||
['a', 'b', 'c']
|
||||
>>> Ox.values([1,])
|
||||
[1]
|
||||
*/
|
||||
// fixme: why doesn't this use map?
|
||||
> Ox.values([1,,3])
|
||||
[1, 3]
|
||||
@*/
|
||||
Ox.values = function(col) {
|
||||
// this happens to works for arrays and strings, but still:
|
||||
// Ox.values(arr) -> arr, Ox.values(str) -> str.split('')
|
||||
var values = [];
|
||||
Ox.forEach(obj, function(val) {
|
||||
Ox.forEach(col, function(val) {
|
||||
values.push(val);
|
||||
});
|
||||
return values;
|
||||
};
|
||||
|
||||
Ox.walk = function(obj, fn) {
|
||||
/*
|
||||
>>> a = 0; Ox.walk({a: 1, b: {c: 2, d: 3}}, function(v, k) { a += Ox.isNumber(v) ? v : 0}); a
|
||||
/*@
|
||||
Ox.walk <f> Recursively walk a tree-like key/value store
|
||||
<script>
|
||||
Ox.test.number = 0;
|
||||
Ox.walk({a: 1, b: {c: 2, d: 3}}, function (v) {
|
||||
Ox.test.number += Ox.isNumber(v) ? v : 0;
|
||||
});
|
||||
</script>
|
||||
> Ox.test.number
|
||||
6
|
||||
*/
|
||||
@*/
|
||||
Ox.walk = function(obj, fn) {
|
||||
Ox.forEach(obj, function(val, key) {
|
||||
fn(val, key, obj);
|
||||
Ox.walk(obj[key], fn);
|
||||
|
@ -2788,6 +2837,7 @@ Ox.doc <f> Generates documentation for annotated JavaScript
|
|||
@*/
|
||||
|
||||
Ox.doc = (function() {
|
||||
// fixme: dont require the trailing '@'
|
||||
var re = {
|
||||
item: /^(.+?) <(.+?)> (.+)$/,
|
||||
multiline: /^\/\*\@.*?\n([\w\W]+)\n.*?\@\*\/$/,
|
||||
|
@ -2886,6 +2936,7 @@ Ox.doc = (function() {
|
|||
};
|
||||
}
|
||||
function parseTokens(tokens, includeLeading) {
|
||||
// fixme: do not strip whitespace from the beginning of the first line of the items' source
|
||||
var leading = [],
|
||||
tokens_ = [];
|
||||
tokens.forEach(function(token) {
|
||||
|
@ -3079,7 +3130,7 @@ Ox.test = function(file, callback) {
|
|||
var actual = eval(example.statement);
|
||||
if (example.result) {
|
||||
tests.push({
|
||||
actual: actual,
|
||||
actual: JSON.stringify(actual),
|
||||
expected: example.result,
|
||||
name: item.name,
|
||||
section: item.section,
|
||||
|
@ -3174,18 +3225,19 @@ Ox.tokenize = (function() {
|
|||
'shift', 'slice', 'some', 'sort', 'splice',
|
||||
'unshift',
|
||||
// Date
|
||||
'getDate', 'getDay', 'getFullYear', 'getHours', 'getMilliseconds',
|
||||
'getMinutes', 'getMonth', 'getSeconds', 'getTime', 'getTimezoneOffset',
|
||||
'getUTCDate', 'getUTCDay', 'getUTCFullYear', 'getUTCHours', 'getUTCMilliseconds',
|
||||
'getUTCMinutes', 'getUTCMonth', 'getUTCSeconds',
|
||||
'getDate', 'getDay', 'getFullYear', 'getHours',
|
||||
'getMilliseconds', 'getMinutes', 'getMonth', 'getSeconds',
|
||||
'getTime', 'getTimezoneOffset',
|
||||
'getUTCDate', 'getUTCDay', 'getUTCFullYear', 'getUTCHours',
|
||||
'getUTCMilliseconds', 'getUTCMinutes', 'getUTCMonth', 'getUTCSeconds',
|
||||
'now',
|
||||
'parse',
|
||||
'setDate', 'setFullYear', 'setHours', 'setMilliseconds', 'setMinutes',
|
||||
'setMonth', 'setSeconds', 'setTime',
|
||||
'setUTCDate', 'setUTCFullYear', 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes',
|
||||
'setUTCMonth', 'setUTCSeconds',
|
||||
'toDateString', 'toJSON', 'toLocaleDateString', 'toLocaleString', 'toLocaleTimeString',
|
||||
'toTimeString', 'toUTCString',
|
||||
'setDate', 'setFullYear', 'setHours', 'setMilliseconds',
|
||||
'setMinutes', 'setMonth', 'setSeconds', 'setTime',
|
||||
'setUTCDate', 'setUTCFullYear', 'setUTCHours', 'setUTCMilliseconds',
|
||||
'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds',
|
||||
'toDateString', 'toJSON', 'toLocaleDateString', 'toLocaleString',
|
||||
'toLocaleTimeString', 'toTimeString', 'toUTCString',
|
||||
'UTC',
|
||||
// Function
|
||||
'apply', 'bind', 'call', 'isGenerator',
|
||||
|
@ -3226,7 +3278,22 @@ Ox.tokenize = (function() {
|
|||
'match',
|
||||
'replace',
|
||||
'search', 'slice', 'split', 'substr', 'substring',
|
||||
'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toUpperCase', 'trim'
|
||||
'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toUpperCase', 'trim',
|
||||
// Window
|
||||
'addEventListener', 'alert', 'atob',
|
||||
'blur', 'btoa',
|
||||
'clearInterval', 'clearTimeout', 'close', 'confirm',
|
||||
'dispatchEvent',
|
||||
'escape',
|
||||
'find', 'focus',
|
||||
'getComputedStyle', 'getSelection',
|
||||
'moveBy', 'moveTo',
|
||||
'open',
|
||||
'postMessage', 'print', 'prompt',
|
||||
'removeEventListener', 'resizeBy', 'resizeTo',
|
||||
'scroll', 'scrollBy', 'scrollTo',
|
||||
'setCursor', 'setInterval', 'setTimeout', 'stop',
|
||||
'unescape'
|
||||
],
|
||||
object: [
|
||||
'Array',
|
||||
|
@ -3250,8 +3317,24 @@ Ox.tokenize = (function() {
|
|||
// Function
|
||||
'constructor', 'length', 'prototype',
|
||||
// RegExp
|
||||
'global', 'ignoreCase', 'lastIndex', 'multiline', 'source'
|
||||
'global', 'ignoreCase', 'lastIndex', 'multiline', 'source',
|
||||
// Window
|
||||
'applicationCache',
|
||||
'closed', 'content', 'crypto',
|
||||
'defaultStatus', 'document',
|
||||
'frameElement', 'frames',
|
||||
'history',
|
||||
'innerHeight', 'innerWidth',
|
||||
'length', 'location', 'locationbar', 'localStorage',
|
||||
'menubar',
|
||||
'name', 'navigator',
|
||||
'opener', 'outerHeight', 'outerWidth',
|
||||
'pageXOffset', 'pageYOffset', 'parent', 'personalbar',
|
||||
'screen', 'screenX', 'screenY', 'scrollbars', 'scrollX', 'scrollY',
|
||||
'self', 'sessionStorage', 'status', 'statusbar',
|
||||
'toolbar', 'top'
|
||||
]
|
||||
// Window stuff? 'atob', 'btoa', 'console', 'document' ...
|
||||
};
|
||||
|
||||
return function(source) {
|
||||
|
@ -3272,7 +3355,7 @@ Ox.tokenize = (function() {
|
|||
},
|
||||
identifier: function() {
|
||||
var str;
|
||||
while (identifier.indexOf(source[++cursor]) > -1) {}
|
||||
while ((identifier + number).indexOf(source[++cursor]) > -1) {}
|
||||
str = source.substring(start, cursor);
|
||||
Ox.forEach(word, function(value, key) {
|
||||
if (value.indexOf(str) > -1) {
|
||||
|
@ -3419,6 +3502,7 @@ Ox.divideInt <f> Divides a number by another and returns an array of integers
|
|||
[16, 16, 17, 17, 17, 17]
|
||||
@*/
|
||||
Ox.divideInt = function(num, by) {
|
||||
// fixme: for loops are so C ;)
|
||||
var arr = [],
|
||||
div = parseInt(num / by),
|
||||
mod = num % by,
|
||||
|
@ -3550,6 +3634,34 @@ Ox.extend = function() {
|
|||
return obj;
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.serialize <f> Parses an object into query parameters
|
||||
> Ox.serialize({a: 1, b: 2, c: 3})
|
||||
'a=1&b=2&c=3'
|
||||
@*/
|
||||
Ox.serialize = function(obj) {
|
||||
var arr = [];
|
||||
Ox.forEach(obj, function(val, key) {
|
||||
arr.push(key + '=' + val);
|
||||
});
|
||||
return arr.join('&');
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.unserialize <f> Parses query parameters into an object
|
||||
> Ox.unserialize('a=1&b=2&c=3')
|
||||
{a: 1, b: 2, c: 3}
|
||||
@*/
|
||||
Ox.unserialize = function(str) {
|
||||
var obj = {};
|
||||
Ox.forEach(str.split('&'), function(val) {
|
||||
var arr = val.split('='),
|
||||
num = +arr[1];
|
||||
obj[arr[0]] = Ox.isNumber(num) ? num : arr[1];
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
|
||||
/*
|
||||
================================================================================
|
||||
RegExp functions
|
||||
|
@ -3684,7 +3796,7 @@ Ox.loadFile = (function() {
|
|||
|
||||
Ox.basename = function(str) {
|
||||
/*
|
||||
fixme: this should go into Path functions
|
||||
fixme: deprecate
|
||||
>>> Ox.basename("foo/bar/foo.bar")
|
||||
"foo.bar"
|
||||
>>> Ox.basename("foo.bar")
|
||||
|
@ -3693,6 +3805,10 @@ Ox.basename = function(str) {
|
|||
return str.replace(/^.*[\/\\]/g, '');
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.char <f> Alias for String.fromCharCode
|
||||
@*/
|
||||
// fixme: add some mapping? like Ox.char(9, 13) or Ox.char([9, 13])?
|
||||
Ox.char = String.fromCharCode;
|
||||
|
||||
Ox.clean = function(str) {
|
||||
|
@ -3723,11 +3839,15 @@ Ox.contains = function(str, chr) {
|
|||
return str.indexOf(chr) > -1;
|
||||
};
|
||||
|
||||
Ox.endsWith = function(str, sub) {
|
||||
/*
|
||||
>>> Ox.endsWith("foobar", "bar")
|
||||
/*@
|
||||
Ox.endsWith <f> Checks if a string ends with a given substring
|
||||
While <code>Ox.endsWith('foobar', 'bar')</code> is longer than
|
||||
<code>/bar$/.test('foobar')</code>, <code>Ox.endsWith('foobar', bar)</code>
|
||||
is shorter than <code>new RegExp(bar + '$').test('foobar')</code>.
|
||||
> Ox.endsWith('foobar', 'bar')
|
||||
true
|
||||
*/
|
||||
@*/
|
||||
Ox.endsWith = function(str, sub) {
|
||||
return new RegExp(sub + '$').test(str);
|
||||
};
|
||||
|
||||
|
@ -3739,34 +3859,40 @@ Ox.highlight = function(txt, str) {
|
|||
) : txt;
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.isValidEmail <f> Tests if a string is a valid e-mail address
|
||||
(str) -> <b> True if the string is a valid e-mail address
|
||||
str <s> Any string
|
||||
> Ox.isValidEmail("foo@bar.com")
|
||||
true
|
||||
> Ox.isValidEmail("foo.bar@foobar.co.uk")
|
||||
true
|
||||
> Ox.isValidEmail("foo@bar")
|
||||
false
|
||||
> Ox.isValidEmail("foo@bar..com")
|
||||
false
|
||||
@*/
|
||||
Ox.isValidEmail = function(str) {
|
||||
/*
|
||||
>>> Ox.isValidEmail("foo@bar.com")
|
||||
true
|
||||
>>> Ox.isValidEmail("foo.bar@foobar.co.uk")
|
||||
true
|
||||
>>> Ox.isValidEmail("foo@bar")
|
||||
false
|
||||
>>> Ox.isValidEmail("foo@bar..com")
|
||||
false
|
||||
*/
|
||||
return !!/^[0-9A-Z\.\+\-_]+@(?:[0-9A-Z\-]+\.)+[A-Z]{2,6}$/i(str);
|
||||
}
|
||||
|
||||
/*@
|
||||
Ox.pad <f> Pad a string to a given length
|
||||
> Ox.pad(1, 2)
|
||||
"01"
|
||||
> Ox.pad("abc", 6, ".", "right")
|
||||
"abc..."
|
||||
> Ox.pad("foobar", 3, ".", "right")
|
||||
"foo"
|
||||
> Ox.pad("abc", 6, "123456", "right")
|
||||
"abc123"
|
||||
> Ox.pad("abc", 6, "123456", "left")
|
||||
"456abc"
|
||||
@*/
|
||||
Ox.pad = function(str, len, pad, pos) {
|
||||
// fixme: slighly obscure signature
|
||||
// fixme: weird for negative numbers
|
||||
/*
|
||||
>>> Ox.pad(1, 2)
|
||||
"01"
|
||||
>>> Ox.pad("abc", 6, ".", "right")
|
||||
"abc..."
|
||||
>>> Ox.pad("foobar", 3, ".", "right")
|
||||
"foo"
|
||||
>>> Ox.pad("abc", 6, "123456", "right")
|
||||
"abc123"
|
||||
>>> Ox.pad("abc", 6, "123456", "left")
|
||||
"456abc"
|
||||
*/
|
||||
str = str.toString().substr(0, len);
|
||||
pad = Ox.repeat(pad || '0', len - str.length);
|
||||
|
@ -3778,6 +3904,30 @@ Ox.pad = function(str, len, pad, pos) {
|
|||
return str;
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.parsePath <f> Returns the components of a path
|
||||
(str) -> <o> Path
|
||||
extension <s> File extension
|
||||
filename <s> Filename
|
||||
pathname <s> Pathname
|
||||
> Ox.parsePath('/foo/bar/foo.bar')
|
||||
{extension: 'bar', filename: 'foo.bar', pathname: '/foo/bar/'}
|
||||
> Ox.parsePath('foo/')
|
||||
{extension: '', filename: '', pathname: 'foo/'}
|
||||
> Ox.parsePath('foo')
|
||||
{extension: '', filename: 'foo', pathname: ''}
|
||||
> Ox.parsePath('.foo')
|
||||
{extension: '', filename: '.foo', pathname: ''}
|
||||
@*/
|
||||
Ox.parsePath = function(str) {
|
||||
var matches = /^(.+\/)?(.+?(\..+)?)?$/(str);
|
||||
return {
|
||||
pathname: matches[1] || '',
|
||||
filename: matches[2] || '',
|
||||
extension: matches[3] ? matches[3].substr(1) : ''
|
||||
};
|
||||
}
|
||||
|
||||
Ox.repeat = function(obj, num) {
|
||||
/*
|
||||
works for arrays, numbers and strings
|
||||
|
@ -3807,6 +3957,14 @@ Ox.reverse = function(str) {
|
|||
return str.toString().split('').reverse().join('');
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.startsWith <f> Checks if a string starts with a given substring
|
||||
While <code>Ox.startsWith('foobar', 'foo')</code> is longer than
|
||||
<code>/^foo/.test('foobar')</code>, <code>Ox.startsWith('foobar', foo)</code>
|
||||
is shorter than <code>new RegExp('^' + foo).test('foobar')</code>.
|
||||
> Ox.endsWith('foobar', 'bar')
|
||||
true
|
||||
@*/
|
||||
Ox.startsWith = function(str, sub) {
|
||||
/*
|
||||
>>> Ox.startsWith("foobar", "foo")
|
||||
|
@ -3819,12 +3977,13 @@ Ox.startsWith = function(str, sub) {
|
|||
return new RegExp('^' + sub).test(str);
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.stripTags <f> Strips HTML tags from a string
|
||||
> Ox.stripTags('f<span>o</span>o')
|
||||
'foo'
|
||||
@*/
|
||||
Ox.stripTags = function(str) {
|
||||
/*
|
||||
>>> Ox.stripTags("f<span>o</span>o")
|
||||
"foo"
|
||||
*/
|
||||
return str.replace(/(<.*?>)/gi, '');
|
||||
return str.replace(/(<.*?>)/g, '');
|
||||
};
|
||||
|
||||
Ox.substr = function(str, start, stop) {
|
||||
|
@ -3852,47 +4011,54 @@ Ox.substr = function(str, start, stop) {
|
|||
);
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.toCamelCase <f> Takes a string with '-', '/' or '_', returns a camelCase string
|
||||
> Ox.toCamelCase('foo-bar-baz')
|
||||
'fooBarBaz'
|
||||
> Ox.toCamelCase('foo/bar/baz')
|
||||
'fooBarBaz'
|
||||
> Ox.toCamelCase('foo_bar_baz')
|
||||
'fooBarBaz'
|
||||
@*/
|
||||
|
||||
Ox.toCamelCase = function(str) {
|
||||
/*
|
||||
>>> Ox.toCamelCase("foo-bar-baz")
|
||||
"fooBarBaz"
|
||||
>>> Ox.toCamelCase("foo/bar/baz")
|
||||
"fooBarBaz"
|
||||
>>> Ox.toCamelCase("foo_bar_baz")
|
||||
"fooBarBaz"
|
||||
*/
|
||||
return str.replace(/[\-_\/][a-z]/g, function(str) {
|
||||
return str[1].toUpperCase();
|
||||
});
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.toDashes <f> Takes a camelCase string, returns a string with dashes
|
||||
> Ox.toDashes('fooBarBaz')
|
||||
'foo-bar-baz'
|
||||
@*/
|
||||
Ox.toDashes = function(str) {
|
||||
/*
|
||||
>>> Ox.toDashes("fooBarBaz")
|
||||
"foo-bar-baz"
|
||||
*/
|
||||
return str.replace(/[A-Z]/g, function(str) {
|
||||
return '-' + str.toLowerCase();
|
||||
});
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.toSlashes <f> Takes a camelCase string, returns a string with slashes
|
||||
> Ox.toSlashes('fooBarBaz')
|
||||
'foo/bar/baz'
|
||||
@*/
|
||||
Ox.toSlashes = function(str) {
|
||||
/*
|
||||
>>> Ox.toSlashes("fooBarBaz")
|
||||
"foo/bar/baz"
|
||||
*/
|
||||
return str.replace(/[A-Z]/g, function(str) {
|
||||
return '/' + str.toLowerCase();
|
||||
});
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.toTitleCase <f> Returns a string with capitalized words
|
||||
> Ox.toTitleCase('foo')
|
||||
'Foo'
|
||||
> Ox.toTitleCase('Apple releases iPhone, IBM stock plummets')
|
||||
'Apple Releases iPhone, IBM Stock Plummets'
|
||||
@*/
|
||||
Ox.toTitleCase = function(str) {
|
||||
/*
|
||||
>>> Ox.toTitleCase("foo")
|
||||
"Foo"
|
||||
>>> Ox.toTitleCase("Apple releases iPhone, IBM stock plummets")
|
||||
"Apple Releases iPhone, IBM Stock Plummets"
|
||||
*/
|
||||
return Ox.map(str.split(' '), function(v) {
|
||||
var sub = v.substr(1),
|
||||
low = sub.toLowerCase();
|
||||
|
@ -3903,11 +4069,12 @@ Ox.toTitleCase = function(str) {
|
|||
}).join(" ");
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.toUnderscores <f> Takes a camelCase string, returns string with underscores
|
||||
> Ox.toUnderscores('fooBarBaz')
|
||||
'foo_bar_baz'
|
||||
@*/
|
||||
Ox.toUnderscores = function(str) {
|
||||
/*
|
||||
>>> Ox.toUnderscores("fooBarBaz")
|
||||
"foo_bar_baz"
|
||||
*/
|
||||
return str.replace(/[A-Z]/g, function(str) {
|
||||
return '_' + str.toLowerCase();
|
||||
});
|
||||
|
@ -3921,10 +4088,23 @@ Ox.trim = function(str) { // is in jQuery, and in JavaScript itself
|
|||
return str.replace(/^\s+|\s+$/g, "");
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.truncate <f> Truncate a string to a given length
|
||||
(string, length) <s> Truncated string
|
||||
(string, length, position) -> <s> Truncated string
|
||||
(string, length, placeholder) -> <s> Truncated string
|
||||
(string, length, position, placeholder) -> <s> Truncated string
|
||||
> Ox.truncate('anticonstitutionellement', 16)
|
||||
'anticonstitut...'
|
||||
> Ox.truncate('anticonstitutionellement', 16, -1)
|
||||
'...utionellement'
|
||||
> Ox.truncate('anticonstitutionellement', 16, '>')
|
||||
'anticonstitutio>'
|
||||
> Ox.truncate("anticonstitutionellement", 16, "...", "center")
|
||||
'anticon...lement'
|
||||
@*/
|
||||
Ox.truncate = function(str, len, pad, pos) {
|
||||
/*
|
||||
>>> Ox.truncate("anticonstitutionellement", 16, "...", "center")
|
||||
"anticon...lement"
|
||||
*/
|
||||
var pad = pad || {},
|
||||
pos = pos || "right",
|
||||
|
@ -3945,11 +4125,14 @@ Ox.truncate = function(str, len, pad, pos) {
|
|||
return str;
|
||||
};
|
||||
|
||||
/*@
|
||||
Ox.words <f> Splits a string into words, removing punctuation
|
||||
(string) -> <[s]> Array of words
|
||||
string <s> Any string
|
||||
> Ox.words('Let\'s "walk" a tree-like key/value store--okay?')
|
||||
["let's", "walk", "a", "tree-like", "key", "value", "store", "okay"]
|
||||
@*/
|
||||
Ox.words = function(str) {
|
||||
/*
|
||||
> Ox.words("The key/value pairs are read-only--aren't they?")
|
||||
["the", "key", "value", "pairs", "are", "read-only", "aren't", "they"]
|
||||
*/
|
||||
var arr = str.toLowerCase().split(/\b/),
|
||||
chr = "-'",
|
||||
len = arr.length,
|
||||
|
@ -3978,23 +4161,21 @@ Ox.words = function(str) {
|
|||
|
||||
/*@
|
||||
Ox.wordwrap <f> Wrap a string at word boundaries
|
||||
> Ox.wordwrap("Anticonstitutionellement, Paris s'eveille", 25, '<br/>')
|
||||
"Anticonstitutionellement, <br/>Paris s'eveille"
|
||||
> Ox.wordwrap("Anticonstitutionellement, Paris s'eveille", 16, '<br/>')
|
||||
"Anticonstitution<br/>ellement, Paris <br/>s'eveille"
|
||||
> Ox.wordwrap('These are short words', 16, '<br/>', true)
|
||||
'These are <br/>short words'
|
||||
@*/
|
||||
Ox.wordwrap = function(str, len, sep, bal, spa) {
|
||||
// fixme: bad API, sep/bal/spa should be in options object
|
||||
/*
|
||||
>>> Ox.wordwrap("Anticonstitutionellement, Paris s'eveille", 25, "<br/>")
|
||||
"Anticonstitutionellement, <br/>Paris s'eveille"
|
||||
>>> Ox.wordwrap("Anticonstitutionellement, Paris s'eveille", 16, "<br/>")
|
||||
"Anticonstitution<br/>ellement, Paris <br/>s'eveille"
|
||||
>>> Ox.wordwrap("These are short words", 16, "<br/>", true)
|
||||
"These are <br/>short words"
|
||||
*/
|
||||
var str = str === null ? '' : str.toString(),
|
||||
len = len || 80,
|
||||
sep = sep || "<br/>",
|
||||
sep = sep || '<br/>',
|
||||
bal = bal || false,
|
||||
spa = Ox.isUndefined(spa) ? true : spa,
|
||||
words = str.split(" "),
|
||||
words = str.split(' '),
|
||||
lines;
|
||||
if (bal) {
|
||||
// balance lines: test if same number of lines
|
||||
|
@ -4015,15 +4196,15 @@ Ox.wordwrap = function(str, len, sep, bal, spa) {
|
|||
}
|
||||
}
|
||||
}
|
||||
lines = [""];
|
||||
lines = [''];
|
||||
Ox.forEach(words, function(word) {
|
||||
if ((lines[lines.length - 1] + word + " ").length <= len + 1) {
|
||||
if ((lines[lines.length - 1] + word + ' ').length <= len + 1) {
|
||||
// word fits in current line
|
||||
lines[lines.length - 1] += word + " ";
|
||||
lines[lines.length - 1] += word + ' ';
|
||||
} else {
|
||||
if (word.length <= len) {
|
||||
// word fits in next line
|
||||
lines.push(word + " ");
|
||||
lines.push(word + ' ');
|
||||
} else {
|
||||
// word is longer than line
|
||||
var chr = len - lines[lines.length - 1].length;
|
||||
|
@ -4031,7 +4212,7 @@ Ox.wordwrap = function(str, len, sep, bal, spa) {
|
|||
for (var pos = chr; pos < word.length; pos += len) {
|
||||
lines.push(word.substr(pos, len));
|
||||
}
|
||||
lines[lines.length - 1] += " ";
|
||||
lines[lines.length - 1] += ' ';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -4106,48 +4287,75 @@ Ox.isElement = function(val) {
|
|||
|
||||
/*@
|
||||
Ox.isEqual <function> Returns true if two values are equal
|
||||
> Ox.isEqual(false, false)
|
||||
true
|
||||
> Ox.isEqual(0, 0)
|
||||
true
|
||||
> Ox.isEqual(NaN, NaN)
|
||||
false
|
||||
> Ox.isEqual('', '')
|
||||
> Ox.isEqual((function() { return arguments; }()), (function() { return arguments; }()))
|
||||
true
|
||||
> Ox.isEqual([1, 2, 3], [1, 2, 3])
|
||||
true
|
||||
> Ox.isEqual({a: 1, b: [2, 3], c: {d: '4'}}, {a: 1, b: [2, 3], c: {d: '4'}})
|
||||
> Ox.isEqual([1, 2, 3], [3, 2, 1])
|
||||
false
|
||||
> Ox.isEqual(false, false)
|
||||
true
|
||||
> Ox.isEqual(new Date(0), new Date(0))
|
||||
true
|
||||
> Ox.isEqual(new Date(0), new Date(1))
|
||||
false
|
||||
> Ox.isEqual(document.createElement('a'), document.createElement('a'))
|
||||
true
|
||||
> Ox.isEqual(document.createElement('a'), document.createElement('b'))
|
||||
false
|
||||
> Ox.isEqual(function(a) { return a; }, function(a) { return a; })
|
||||
true
|
||||
> Ox.isEqual(function(a) { return a; }, function(b) { return b; })
|
||||
false
|
||||
> Ox.isEqual(Infinity, Infinity)
|
||||
true
|
||||
> Ox.isEqual(-Infinity, Infinity)
|
||||
false
|
||||
> Ox.isEqual(NaN, NaN)
|
||||
false
|
||||
> Ox.isEqual(0, 0)
|
||||
true
|
||||
> Ox.isEqual({a: 1, b: 2, c: 3}, {c: 3, b: 2, a: 1})
|
||||
true // FIXME: is false
|
||||
> Ox.isEqual(function(arg) { return arg; }, function(arg) { return arg; })
|
||||
true
|
||||
> Ox.isEqual(function(foo) { return foo; }, function(bar) { return bar; })
|
||||
> Ox.isEqual({a: 1, b: [2, 3], c: {d: '4'}}, {a: 1, b: [2, 3], c: {d: '4'}})
|
||||
true
|
||||
> Ox.isEqual(/ /, / /)
|
||||
true
|
||||
> Ox.isEqual(/ /g, / /i)
|
||||
false
|
||||
> Ox.isEqual('', '')
|
||||
true
|
||||
> Ox.isEqual(void 0, void 0)
|
||||
true
|
||||
@*/
|
||||
Ox.isEqual = function(obj0, obj1) {
|
||||
var ret = false;
|
||||
if (obj0 === obj1) {
|
||||
ret = true;
|
||||
} else if (typeof(obj0) == typeof(obj1)) {
|
||||
if (obj0 == obj1) {
|
||||
ret = true;
|
||||
} else if (Ox.isArray(obj0) && obj0.length == obj1.length) {
|
||||
ret = true;
|
||||
Ox.forEach(obj0, function(v, i) {
|
||||
ret = Ox.isEqual(v, obj1[i]);
|
||||
return ret;
|
||||
Ox.isEqual = function(a, b) {
|
||||
var isEqual = false, type = Ox.typeOf(a);
|
||||
if (a === b) {
|
||||
isEqual = true;
|
||||
} else if (type == Ox.typeOf(b)) {
|
||||
if (a == b) {
|
||||
isEqual = true;
|
||||
} else if (type == 'date') {
|
||||
isEqual = a.getTime() == b.getTime();
|
||||
} else if (['element', 'function'].indexOf(type) > -1) {
|
||||
isEqual = a.toString() == b.toString();
|
||||
} else if (type == 'regexp') {
|
||||
isEqual = a.global == b.global &&
|
||||
a.ignore == b.ignore &&
|
||||
a.multiline == b.multiline &&
|
||||
a.source == b.source;
|
||||
} else if (
|
||||
['arguments', 'array', 'object'].indexOf(type) > -1 &&
|
||||
Ox.len(a) == Ox.len(b)
|
||||
) {
|
||||
isEqual = true;
|
||||
Ox.forEach(a, function(v, k) {
|
||||
isEqual = Ox.isEqual(v, b[k]);
|
||||
return isEqual;
|
||||
});
|
||||
} else if (Ox.isDate(obj0)) {
|
||||
ret = obj0.getTime() == obj1.getTime();
|
||||
} else if (Ox.isObject(obj0)) {
|
||||
ret = Ox.isEqual(Ox.keys(obj0), Ox.keys(obj1)) &&
|
||||
Ox.isEqual(Ox.values(obj0), Ox.values(obj1));
|
||||
} else if (Ox.isFunction(obj0)) {
|
||||
ret = obj0.toString() == obj1.toString();
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
return isEqual;
|
||||
};
|
||||
|
||||
/*@
|
||||
|
|
Loading…
Reference in a new issue