Page Menu
Home
WickedGov Phorge
Search
Configure Global Search
Log In
Files
F4123379
ve.utils.js
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
40 KB
Referenced Files
None
Subscribers
None
ve.utils.js
View Options
/*!
* VisualEditor utilities.
*
* @copyright See AUTHORS.txt
*/
/**
* Checks if an object is an instance of one or more classes.
*
* @param {Object} subject Object to check
* @param {Function[]} classes Classes to compare with
* @return {boolean} Object inherits from one or more of the classes
*/
ve
.
isInstanceOfAny
=
function
(
subject
,
classes
)
{
let
i
=
classes
.
length
;
while
(
classes
[
--
i
]
)
{
if
(
subject
instanceof
classes
[
i
]
)
{
return
true
;
}
}
return
false
;
};
/**
* Get a deeply nested property of an object using variadic arguments, protecting against
* undefined property errors.
*
* `quux = OO.getProp( obj, 'foo', 'bar', 'baz' );` is equivalent to `quux = obj.foo.bar.baz;`
* except that the former protects against JS errors if one of the intermediate properties
* is undefined. Instead of throwing an error, this function will return undefined in
* that case.
*
* See https://doc.wikimedia.org/oojs/master/OO.html
*
* @method
* @param {Object} obj
* @param {...any} [keys]
* @return {Object|undefined} obj[arguments[1]][arguments[2]].... or undefined
*/
ve
.
getProp
=
OO
.
getProp
;
/**
* Set a deeply nested property of an object using variadic arguments, protecting against
* undefined property errors.
*
* `OO.setProp( obj, 'foo', 'bar', 'baz' );` is equivalent to `obj.foo.bar = baz;` except that
* the former protects against JS errors if one of the intermediate properties is
* undefined. Instead of throwing an error, undefined intermediate properties will be
* initialized to an empty object. If an intermediate property is not an object, or if obj itself
* is not an object, this function will silently abort.
*
* See https://doc.wikimedia.org/oojs/master/OO.html
*
* @method
* @param {Object} obj
* @param {...any} [keys]
* @param {any} [value]
*/
ve
.
setProp
=
OO
.
setProp
;
/**
* Delete a deeply nested property of an object using variadic arguments, protecting against
* undefined property errors, and deleting resulting empty objects.
*
* See https://doc.wikimedia.org/oojs/master/OO.html
*
* @method
* @param {Object} obj
* @param {...any} [keys]
*/
ve
.
deleteProp
=
OO
.
deleteProp
;
/**
* Create a new object that is an instance of the same
* constructor as the input, inherits from the same object
* and contains the same own properties.
*
* This makes a shallow non-recursive copy of own properties.
* To create a recursive copy of plain objects, use #copy.
*
* var foo = new Person( mom, dad );
* foo.setAge( 21 );
* var foo2 = OO.cloneObject( foo );
* foo.setAge( 22 );
*
* // Then
* foo2 !== foo; // true
* foo2 instanceof Person; // true
* foo2.getAge(); // 21
* foo.getAge(); // 22
*
* See https://doc.wikimedia.org/oojs/master/OO.html
*
* @method
* @param {Object} origin
* @return {Object} Clone of origin
*/
ve
.
cloneObject
=
OO
.
cloneObject
;
/**
* Get an array of all property values in an object.
*
* See https://doc.wikimedia.org/oojs/master/OO.html
*
* @method
* @param {Object} obj Object to get values from
* @return {Array} List of object values
*/
ve
.
getObjectValues
=
OO
.
getObjectValues
;
/**
* Use binary search to locate an element in a sorted array.
*
* searchFunc is given an element from the array. `searchFunc(elem)` must return a number
* above 0 if the element we're searching for is to the right of (has a higher index than) elem,
* below 0 if it is to the left of elem, or zero if it's equal to elem.
*
* To search for a specific value with a comparator function (a `function cmp(a,b)` that returns
* above 0 if `a > b`, below 0 if `a < b`, and 0 if `a == b`), you can use
* `searchFunc = cmp.bind( null, value )`.
*
* See https://doc.wikimedia.org/oojs/master/OO.html
*
* @method
* @param {Array} arr Array to search in
* @param {Function} searchFunc Search function
* @param {boolean} [forInsertion] If not found, return index where val could be inserted
* @return {number|null} Index where val was found, or null if not found
*/
ve
.
binarySearch
=
OO
.
binarySearch
;
/**
* Recursively compare properties between two objects.
*
* A false result may be caused by property inequality or by properties in one object missing from
* the other. An asymmetrical test may also be performed, which checks only that properties in the
* first object are present in the second object, but not the inverse.
*
* If either a or b is null or undefined it will be treated as an empty object.
*
* See https://doc.wikimedia.org/oojs/master/OO.html
*
* @method
* @param {Object|undefined|null} a First object to compare
* @param {Object|undefined|null} b Second object to compare
* @param {boolean} [asymmetrical] Whether to check only that a's values are equal to b's
* (i.e. a is a subset of b)
* @return {boolean} If the objects contain the same values as each other
*/
ve
.
compare
=
OO
.
compare
;
/**
* Create a plain deep copy of any kind of object.
*
* Copies are deep, and will either be an object or an array depending on `source`.
*
* See https://doc.wikimedia.org/oojs/master/OO.html
*
* @method
* @param {Object} source Object to copy
* @param {Function} [leafCallback] Applied to leaf values after they are cloned but before they are
* added to the clone
* @param {Function} [nodeCallback] Applied to all values before they are cloned. If the
* nodeCallback returns a value other than undefined, the returned value is used instead of
* attempting to clone.
* @return {Object} Copy of source object
*/
ve
.
copy
=
OO
.
copy
;
/**
* @method
* @see OO.ui.debounce
*/
ve
.
debounce
=
OO
.
ui
.
debounce
;
/**
* @method
* @see OO.ui.throttle
*/
ve
.
throttle
=
OO
.
ui
.
throttle
;
/**
* Create a jQuery.Deferred-compatible object
*
* See http://api.jquery.com/jQuery.Deferred/
*
* @method
* @return {jQuery.Deferred}
*/
ve
.
createDeferred
=
$
.
Deferred
;
/**
* Create a promise which resolves when the list of promises has resolved
*
* @param {jQuery.Promise[]} promises List of promises
* @return {jQuery.Promise} Promise which resolves when the list of promises has resolved
*/
ve
.
promiseAll
=
function
(
promises
)
{
return
$
.
when
.
apply
(
$
,
promises
);
};
/**
* Copy an array of DOM elements, optionally into a different document.
*
* @param {HTMLElement[]} domElements DOM elements to copy
* @param {HTMLDocument} [doc] Document to create the copies in; if unset, simply clone each element
* @return {HTMLElement[]} Copy of domElements with copies of each element
*/
ve
.
copyDomElements
=
function
(
domElements
,
doc
)
{
return
domElements
.
map
(
(
domElement
)
=>
doc
?
doc
.
importNode
(
domElement
,
true
)
:
domElement
.
cloneNode
(
true
)
);
};
/**
* Check if two arrays of DOM elements are equal (according to .isEqualNode())
*
* @param {HTMLElement[]} domElements1 First array of DOM elements
* @param {HTMLElement[]} domElements2 Second array of DOM elements
* @return {boolean} All elements are pairwise equal
*/
ve
.
isEqualDomElements
=
function
(
domElements1
,
domElements2
)
{
let
i
=
0
;
const
len
=
domElements1
.
length
;
if
(
len
!==
domElements2
.
length
)
{
return
false
;
}
for
(
;
i
<
len
;
i
++
)
{
if
(
!
domElements1
[
i
].
isEqualNode
(
domElements2
[
i
]
)
)
{
return
false
;
}
}
return
true
;
};
/**
* Compare two class lists, either whitespace separated strings or arrays
*
* Class lists are equivalent if they contain the same members,
* excluding duplicates and ignoring order.
*
* @param {string[]|string} classList1 First class list
* @param {string[]|string} classList2 Second class list
* @return {boolean} Class lists are equivalent
*/
ve
.
compareClassLists
=
function
(
classList1
,
classList2
)
{
const
removeEmpty
=
function
(
c
)
{
return
c
!==
''
;
};
classList1
=
Array
.
isArray
(
classList1
)
?
classList1
:
classList1
.
trim
().
split
(
/\s+/
);
classList2
=
Array
.
isArray
(
classList2
)
?
classList2
:
classList2
.
trim
().
split
(
/\s+/
);
classList1
=
classList1
.
filter
(
removeEmpty
);
classList2
=
classList2
.
filter
(
removeEmpty
);
return
ve
.
compare
(
OO
.
unique
(
classList1
).
sort
(),
OO
.
unique
(
classList2
).
sort
()
);
};
/**
* Check to see if an object is a plain object (created using "{}" or "new Object").
*
* See http://api.jquery.com/jQuery.isPlainObject/
*
* @method
* @param {Object} obj The object that will be checked to see if it's a plain object
* @return {boolean}
*/
ve
.
isPlainObject
=
$
.
isPlainObject
;
/**
* Check to see if an object is empty (contains no properties).
*
* See http://api.jquery.com/jQuery.isEmptyObject/
*
* @method
* @param {Object} obj The object that will be checked to see if it's empty
* @return {boolean}
*/
ve
.
isEmptyObject
=
$
.
isEmptyObject
;
/**
* Merge properties of one or more objects into another.
* Preserves original object's inheritance (e.g. Array, Object, whatever).
* In case of array or array-like objects only the indexed properties
* are copied over.
* Beware: If called with only one argument, it will consider
* 'target' as 'source' and 'this' as 'target'. Which means
* ve.extendObject( { a: 1 } ); sets ve.a = 1;
*
* See http://api.jquery.com/jQuery.extend/
*
* @method
* @param {boolean} [recursive=false]
* @param {any} [target] Object that will receive the new properties
* @param {...any} [sources] Variadic list of objects containing properties
* to be merged into the target.
* @return {any} Modified version of first or second argument
*/
ve
.
extendObject
=
$
.
extend
;
/**
* Splice one array into another.
*
* This is the equivalent of arr.splice( offset, remove, d1, d2, d3, … ) except that arguments are
* specified as an array rather than separate parameters.
*
* This method has been proven to be faster than using slice and concat to create a new array, but
* performance tests should be conducted on each use of this method to verify this is true for the
* particular use. Also, browsers change fast, never assume anything, always test everything.
*
* Includes a replacement for broken implementations of Array.prototype.splice().
*
* @param {Array|ve.dm.BranchNode} arr Target object (must have `splice` method, object will be modified)
* @param {number} offset Offset in arr to splice at. This MUST NOT be negative, unlike the
* 'index' parameter in Array#splice.
* @param {number} remove Number of elements to remove at the offset. May be zero
* @param {Array} data Array of items to insert at the offset. Must be non-empty if remove=0
* @return {Array} Array of items removed
*/
ve
.
batchSplice
=
function
(
arr
,
offset
,
remove
,
data
)
{
// We need to splice insertion in in batches, because of parameter list length limits which vary
// cross-browser - 1024 seems to be a safe batch size on all browsers
let
index
=
0
,
toRemove
=
remove
,
removed
=
[];
const
batchSize
=
1024
;
if
(
data
.
length
===
0
)
{
// Special case: data is empty, so we're just doing a removal
// The code below won't handle that properly, so we do it here
return
arr
.
splice
(
offset
,
remove
);
}
while
(
index
<
data
.
length
)
{
// Call arr.splice( offset, remove, i0, i1, i2, …, i1023 );
// Only set remove on the first call, and set it to zero on subsequent calls
const
spliced
=
arr
.
splice
(
index
+
offset
,
toRemove
,
...
data
.
slice
(
index
,
index
+
batchSize
)
);
if
(
toRemove
>
0
)
{
removed
=
spliced
;
}
index
+=
batchSize
;
toRemove
=
0
;
}
return
removed
;
};
/**
* Splice one array into another, replicating any holes
*
* Similar to arr.splice( offset, remove, ...data ), except holes in
* data remain holes in arr. Optimized for length changes that are negative, zero, or
* fairly small positive.
*
* @param {Array} arr Array to modify
* @param {number} offset Offset in arr to splice at. This MUST NOT be negative, unlike the
* 'index' parameter in Array#splice.
* @param {number} remove Number of elements to remove at the offset. May be zero
* @param {Array} data Array of items to insert at the offset
* @return {Array} Array of items removed, with holes preserved
*/
ve
.
sparseSplice
=
function
(
arr
,
offset
,
remove
,
data
)
{
const
removed
=
[],
endOffset
=
offset
+
remove
,
diff
=
data
.
length
-
remove
;
if
(
data
===
arr
)
{
// Pathological case: arr and data are reference-identical
data
=
data
.
slice
();
}
// Remove content without adjusting length
arr
.
slice
(
offset
,
endOffset
).
forEach
(
(
item
,
j
)
=>
{
removed
[
j
]
=
item
;
delete
arr
[
offset
+
j
];
}
);
// Adjust length
if
(
diff
>
0
)
{
// Grow with undefined values, then delete. (This is optimised for diff
// comparatively small: otherwise, it would sometimes be quicker to relocate
// each element of arr that lies above offset).
ve
.
batchSplice
(
arr
,
endOffset
,
0
,
new
Array
(
diff
)
);
for
(
let
i
=
endOffset
+
diff
-
1
;
i
>=
endOffset
;
i
--
)
{
delete
arr
[
i
];
}
}
else
if
(
diff
<
0
)
{
// Shrink
arr
.
splice
(
offset
,
-
diff
);
}
// Insert new content
data
.
forEach
(
(
item
,
j
)
=>
{
arr
[
offset
+
j
]
=
item
;
}
);
// Set removed.length in case there are holes at the end
removed
.
length
=
remove
;
return
removed
;
};
/**
* Insert one array into another.
*
* Shortcut for `ve.batchSplice( arr, offset, 0, src )`.
*
* @see ve.batchSplice
* @param {Array|ve.dm.BranchNode} arr Target object (must have `splice` method)
* @param {number} offset Offset in arr where items will be inserted
* @param {Array} src Items to insert at offset
*/
ve
.
insertIntoArray
=
function
(
arr
,
offset
,
src
)
{
ve
.
batchSplice
(
arr
,
offset
,
0
,
src
);
};
/**
* Push one array into another.
*
* This is the equivalent of arr.push( d1, d2, d3, … ) except that arguments are
* specified as an array rather than separate parameters.
*
* @param {Array|ve.dm.BranchNode} arr Object supporting .push() to insert at the end of the array. Will be modified
* @param {Array} data Array of items to insert.
* @return {number} length of the new array
*/
ve
.
batchPush
=
function
(
arr
,
data
)
{
// We need to push insertion in batches, because of parameter list length limits which vary
// cross-browser - 1024 seems to be a safe batch size on all browsers
let
index
=
0
;
const
batchSize
=
1024
;
if
(
batchSize
>=
data
.
length
)
{
// Avoid slicing for small lists
return
arr
.
push
(
...
data
);
}
let
length
;
while
(
index
<
data
.
length
)
{
// Call arr.push( i0, i1, i2, …, i1023 );
length
=
arr
.
push
(
...
data
.
slice
(
index
,
index
+
batchSize
)
);
index
+=
batchSize
;
}
return
length
;
};
/**
* Log data to the console.
*
* This implementation does nothing, to add a real implementation ve.debug needs to be loaded.
*
* @param {...any} [args] Data to log
*/
ve
.
log
=
ve
.
log
||
function
()
{
// Don't do anything, this is just a stub
};
/**
* Log error to the console.
*
* This implementation does nothing, to add a real implementation ve.debug needs to be loaded.
*
* @param {...any} [args] Data to log
*/
ve
.
error
=
ve
.
error
||
function
()
{
// Don't do anything, this is just a stub
};
/**
* Log an object to the console.
*
* This implementation does nothing, to add a real implementation ve.debug needs to be loaded.
*
* @param {Object} obj
*/
ve
.
dir
=
ve
.
dir
||
function
()
{
// Don't do anything, this is just a stub
};
/**
* Deep freeze an object, making it immutable
*
* This implementation does nothing, to add a real implementation ve.freeze needs to be loaded.
*
* @param {Object} obj
* @param {boolean} onlyProperties
* @return {Object}
*/
ve
.
deepFreeze
=
ve
.
deepFreeze
||
function
(
obj
)
{
// Don't do anything, this is just a stub
return
obj
;
};
/**
* Get a localized message.
*
* @param {string} key Message key
* @param {...any} [params] Message parameters
* @return {string} Localized message
*/
ve
.
msg
=
function
()
{
// Avoid using bind because ve.init.platform doesn't exist yet.
// TODO: Fix dependency issues between ve.js and ve.init.platform
return
ve
.
init
.
platform
.
getMessage
.
apply
(
ve
.
init
.
platform
,
arguments
);
};
/**
* Get an HTML localized message with HTML or DOM arguments.
*
* @param {string} key Message key
* @param {...any} [params] Message parameters
* @return {Node[]} Localized message
*/
ve
.
htmlMsg
=
function
()
{
// Avoid using bind because ve.init.platform doesn't exist yet.
// TODO: Fix dependency issues between ve.js and ve.init.platform
return
ve
.
init
.
platform
.
getHtmlMessage
.
apply
(
ve
.
init
.
platform
,
arguments
);
};
/**
* Get platform config value(s)
*
* @param {string|string[]} key Config key, or list of keys
* @return {any|Object} Config value, or keyed object of config values if list of keys provided
*/
ve
.
config
=
function
()
{
return
ve
.
init
.
platform
.
getConfig
.
apply
(
ve
.
init
.
platform
,
arguments
);
};
/**
* Get or set a user config value.
*
* @param {string|string[]|Object} key Config key, list of keys or object mapping keys to values
* @param {any} [value] Value to set, if setting and key is a string
* @return {any|Object|boolean} Config value, keyed object of config values if list of keys provided,
* or success boolean if setting.
*/
ve
.
userConfig
=
function
(
key
)
{
if
(
arguments
.
length
<=
1
&&
(
typeof
key
===
'string'
||
Array
.
isArray
(
key
)
)
)
{
// get( string key )
// get( Array keys )
return
ve
.
init
.
platform
.
getUserConfig
.
apply
(
ve
.
init
.
platform
,
arguments
);
}
else
{
// set( Object values )
// set( key, value )
return
ve
.
init
.
platform
.
setUserConfig
.
apply
(
ve
.
init
.
platform
,
arguments
);
}
};
/**
* Convert a grapheme cluster offset to a byte offset.
*
* @param {string} text Text in which to calculate offset
* @param {number} clusterOffset Grapheme cluster offset
* @return {number} Byte offset
*/
ve
.
getByteOffset
=
function
(
text
,
clusterOffset
)
{
return
unicodeJS
.
graphemebreak
.
splitClusters
(
text
)
.
slice
(
0
,
clusterOffset
)
.
join
(
''
)
.
length
;
};
/**
* Convert a byte offset to a grapheme cluster offset.
*
* @param {string} text Text in which to calculate offset
* @param {number} byteOffset Byte offset
* @return {number} Grapheme cluster offset
*/
ve
.
getClusterOffset
=
function
(
text
,
byteOffset
)
{
return
unicodeJS
.
graphemebreak
.
splitClusters
(
text
.
slice
(
0
,
byteOffset
)
).
length
;
};
/**
* Get a text substring, taking care not to split grapheme clusters.
*
* @param {string} text Text to take the substring from
* @param {number} start Start offset
* @param {number} end End offset
* @param {boolean} [outer=false] Include graphemes if the offset splits them
* @return {string} Substring of text
*/
ve
.
graphemeSafeSubstring
=
function
(
text
,
start
,
end
,
outer
)
{
// TODO: improve performance by incrementally inspecting characters around the offsets
let
unicodeStart
=
ve
.
getByteOffset
(
text
,
ve
.
getClusterOffset
(
text
,
start
)
),
unicodeEnd
=
ve
.
getByteOffset
(
text
,
ve
.
getClusterOffset
(
text
,
end
)
);
// If the selection collapses and we want an inner, then just return empty
// otherwise we'll end up crossing over start and end
if
(
unicodeStart
===
unicodeEnd
&&
!
outer
)
{
return
''
;
}
// The above calculations always move to the right of a multibyte grapheme.
// Depending on the outer flag, we may want to move to the left:
if
(
unicodeStart
>
start
&&
outer
)
{
unicodeStart
=
ve
.
getByteOffset
(
text
,
ve
.
getClusterOffset
(
text
,
start
)
-
1
);
}
if
(
unicodeEnd
>
end
&&
!
outer
)
{
unicodeEnd
=
ve
.
getByteOffset
(
text
,
ve
.
getClusterOffset
(
text
,
end
)
-
1
);
}
return
text
.
slice
(
unicodeStart
,
unicodeEnd
);
};
/**
* Escape non-word characters so they can be safely used as HTML attribute values.
*
* @param {string} value Attribute value to escape
* @return {string} Escaped attribute value
*/
ve
.
escapeHtml
=
(
function
()
{
function
escape
(
value
)
{
switch
(
value
)
{
case
'\''
:
return
'''
;
case
'"'
:
return
'"'
;
case
'<'
:
return
'<'
;
case
'>'
:
return
'>'
;
case
'&'
:
return
'&'
;
}
}
return
function
(
value
)
{
return
value
.
replace
(
/['"<>&]/g
,
escape
);
};
}()
);
/**
* Get the attributes of a DOM element as an object with key/value pairs.
*
* @param {HTMLElement} element
* @return {Object}
*/
ve
.
getDomAttributes
=
function
(
element
)
{
const
result
=
{};
for
(
let
i
=
0
;
i
<
element
.
attributes
.
length
;
i
++
)
{
result
[
element
.
attributes
[
i
].
name
]
=
element
.
attributes
[
i
].
value
;
}
return
result
;
};
/**
* Set the attributes of a DOM element as an object with key/value pairs.
*
* Use the `null` or `undefined` value to ensure an attribute's absence.
*
* @param {HTMLElement} element DOM element to apply attributes to
* @param {Object} attributes Attributes to apply
* @param {string[]} [allowedAttributes] List of attributes to exclusively allow (all lowercase names)
*/
ve
.
setDomAttributes
=
function
(
element
,
attributes
,
allowedAttributes
)
{
// Duck-typing for attribute setting
if
(
!
element
.
setAttribute
||
!
element
.
removeAttribute
)
{
return
;
}
for
(
const
key
in
attributes
)
{
if
(
allowedAttributes
&&
allowedAttributes
.
indexOf
(
key
.
toLowerCase
()
)
===
-
1
)
{
continue
;
}
if
(
attributes
[
key
]
===
undefined
||
attributes
[
key
]
===
null
)
{
element
.
removeAttribute
(
key
);
}
else
{
element
.
setAttribute
(
key
,
attributes
[
key
]
);
}
}
};
/**
* Get an HTML representation of a DOM element node, text node or comment node
*
* @param {Node} node The DOM node
* @return {string} HTML representation of the node
*/
ve
.
getNodeHtml
=
function
(
node
)
{
if
(
node
.
nodeType
===
Node
.
ELEMENT_NODE
)
{
return
node
.
outerHTML
;
}
else
{
const
div
=
document
.
createElement
(
'div'
);
div
.
appendChild
(
node
.
cloneNode
(
true
)
);
return
div
.
innerHTML
;
}
};
/**
* Build a summary of an HTML element.
*
* Summaries include node name, text, attributes and recursive summaries of children.
* Used for serializing or comparing HTML elements.
*
* @private
* @param {HTMLElement} element Element to summarize
* @param {boolean} [includeHtml=false] Include an HTML summary for element nodes
* @param {Function} [getAttributeSummary] Callback to modify the summary of an attribute
* @param {string} [getAttributeSummary.name] Name of the attribute to modify.
* @param {string} [getAttributeSummary.value] Value to return for the given attribute.
* @return {Object} Summary of element.
*/
ve
.
getDomElementSummary
=
function
(
element
,
includeHtml
,
getAttributeSummary
)
{
const
summary
=
{
type
:
element
.
nodeName
.
toLowerCase
(),
text
:
element
.
textContent
,
attributes
:
{},
children
:
[]
};
if
(
includeHtml
&&
element
.
nodeType
===
Node
.
ELEMENT_NODE
)
{
summary
.
html
=
element
.
outerHTML
;
}
// Gather attributes
if
(
element
.
attributes
)
{
for
(
let
i
=
0
;
i
<
element
.
attributes
.
length
;
i
++
)
{
const
name
=
element
.
attributes
[
i
].
name
;
if
(
name
===
'about'
)
{
// The about attribute is non-deterministic as we generate a new random
// one whenever a node is cloned (see ve.dm.Node.static.cloneElement).
// Exclude it from node comparisons.
continue
;
}
const
value
=
element
.
attributes
[
i
].
value
;
summary
.
attributes
[
name
]
=
getAttributeSummary
?
getAttributeSummary
(
name
,
value
)
:
value
;
}
}
// Summarize children
if
(
element
.
childNodes
)
{
for
(
let
i
=
0
;
i
<
element
.
childNodes
.
length
;
i
++
)
{
summary
.
children
.
push
(
ve
.
getDomElementSummary
(
element
.
childNodes
[
i
],
includeHtml
)
);
}
}
return
summary
;
};
/**
* Callback for #copy to convert nodes to a comparable summary.
*
* @private
* @param {Object} value Value in the object/array
* @return {Object} DOM element summary if value is a node, otherwise just the value
*/
ve
.
convertDomElements
=
function
(
value
)
{
// Use duck typing rather than instanceof Node; the latter doesn't always work correctly
if
(
value
&&
value
.
nodeType
)
{
return
ve
.
getDomElementSummary
(
value
);
}
return
value
;
};
ve
.
visibleWhitespaceCharacters
=
{
'\n'
:
'\u21b5'
,
// ↵
'\t'
:
'\u279e'
// ➞
};
/**
* Check whether a given node is contentEditable
*
* Handles 'inherit', via checking isContentEditable. Knows to check for text
* nodes, and will return whether the text node's parent is contentEditable.
*
* @param {HTMLElement|Text} node Node to check contenteditable status of
* @return {boolean} Node is contenteditable
*/
ve
.
isContentEditable
=
function
(
node
)
{
return
(
node
.
nodeType
===
Node
.
TEXT_NODE
?
node
.
parentNode
:
node
).
isContentEditable
;
};
/**
* Filter out metadata elements
*
* @param {Node[]} contents DOM nodes
* @return {Node[]} Filtered DOM nodes
*/
ve
.
filterMetaElements
=
function
(
contents
)
{
// Filter out link and style tags for T52043
// Previously filtered out meta tags, but restore these as they
// can be made visible with CSS.
// As of jQuery 3 we can't use $.not( 'tagName' ) as that doesn't
// match text nodes. Also we can't $.remove these elements as they
// aren't attached to anything.
contents
=
contents
.
filter
(
(
node
)
=>
node
.
tagName
!==
'LINK'
&&
node
.
tagName
!==
'STYLE'
);
// Also remove link and style tags nested inside other tags
$
(
contents
).
find
(
'link, style'
).
remove
();
return
contents
;
};
/**
* Modify a set of DOM elements to resolve attributes in the context of another document.
*
* This performs node.setAttribute( 'attr', nodeInDoc[attr] ); for every node.
*
* Doesn't use jQuery to avoid document switching performance bug
*
* @param {HTMLElement|HTMLElement[]|NodeList|jQuery} elementsOrJQuery Set of DOM elements to modify. Passing a jQuery selector is deprecated.
* @param {HTMLDocument} doc Document to resolve against (different from $elements' .ownerDocument)
* @param {string[]} attrs Attributes to resolve
*/
ve
.
resolveAttributes
=
function
(
elementsOrJQuery
,
doc
,
attrs
)
{
// Convert jQuery selections to plain arrays
let
elements
=
elementsOrJQuery
.
toArray
?
elementsOrJQuery
.
toArray
()
:
elementsOrJQuery
;
// Duck typing for array or NodeList :(
if
(
elements
.
length
===
undefined
)
{
elements
=
[
elements
];
}
let
attr
;
/**
* Resolves the value of attr to the computed property value.
*
* @private
* @param {HTMLElement} el Element
*/
function
resolveAttribute
(
el
)
{
const
nodeInDoc
=
doc
.
createElement
(
el
.
nodeName
);
nodeInDoc
.
setAttribute
(
attr
,
el
.
getAttribute
(
attr
)
);
if
(
nodeInDoc
[
attr
]
)
{
el
.
setAttribute
(
attr
,
nodeInDoc
[
attr
]
);
}
}
for
(
let
i
=
0
,
iLen
=
elements
.
length
;
i
<
iLen
;
i
++
)
{
const
element
=
elements
[
i
];
for
(
let
j
=
0
,
jLen
=
attrs
.
length
;
j
<
jLen
;
j
++
)
{
attr
=
attrs
[
j
];
if
(
element
.
hasAttribute
(
attr
)
)
{
resolveAttribute
(
element
);
}
Array
.
prototype
.
forEach
.
call
(
element
.
querySelectorAll
(
'['
+
attr
+
']'
),
resolveAttribute
);
}
}
};
/**
* Make all links within a DOM element open in a new window
*
* @param {HTMLElement} container DOM element to search for links
*/
ve
.
targetLinksToNewWindow
=
function
(
container
)
{
// Make all links open in a new window
Array
.
prototype
.
forEach
.
call
(
container
.
querySelectorAll
(
'a[href]'
),
(
el
)
=>
{
ve
.
appendToRel
(
el
,
'noopener'
);
el
.
setAttribute
(
'target'
,
'_blank'
);
}
);
};
/**
* Add a value to an element's rel attribute if it's not already present
*
* Rel is like class: it's actually a set, represented as a string. We don't
* want to add the same value to the attribute if this function is called
* repeatedly. This is mostly a placeholder for the relList property someday
* becoming widely supported.
*
* @param {HTMLElement} element DOM element whose attribute should be checked
* @param {string} value New rel value to be added
*/
ve
.
appendToRel
=
function
(
element
,
value
)
{
const
rel
=
element
.
getAttribute
(
'rel'
);
if
(
!
rel
)
{
// Avoid all that string-creation if it's not needed
element
.
setAttribute
(
'rel'
,
value
);
}
else
if
(
(
' '
+
rel
+
' '
).
indexOf
(
' '
+
value
+
' '
)
===
-
1
)
{
element
.
setAttribute
(
'rel'
,
rel
+
' '
+
value
);
}
};
/**
* Check if a string is a valid URI component.
*
* A URI component is considered invalid if decodeURIComponent() throws an exception.
*
* @param {string} s String to test
* @return {boolean} decodeURIComponent( s ) did not throw an exception
* @see ve.safeDecodeURIComponent
*/
ve
.
isUriComponentValid
=
function
(
s
)
{
try
{
decodeURIComponent
(
s
);
}
catch
(
e
)
{
return
false
;
}
return
true
;
};
/**
* Safe version of decodeURIComponent() that doesn't throw exceptions.
*
* If the native decodeURIComponent() call threw an exception, the original string
* will be returned.
*
* @param {string} s String to decode
* @return {string} Decoded string, or same string if decoding failed
* @see ve.isUriComponentValid
*/
ve
.
safeDecodeURIComponent
=
function
(
s
)
{
try
{
s
=
decodeURIComponent
(
s
);
}
catch
(
e
)
{}
return
s
;
};
/**
* Find the length of the common start sequence of one or more sequences
*
* Items are tested for sameness using === .
*
* @param {Array} sequences Array of sequences (arrays, strings etc)
* @return {number} Common start sequence length (0 if sequences is empty)
*/
ve
.
getCommonStartSequenceLength
=
function
(
sequences
)
{
if
(
sequences
.
length
===
0
)
{
return
0
;
}
let
commonLength
=
0
;
commonLengthLoop
:
while
(
true
)
{
if
(
commonLength
>=
sequences
[
0
].
length
)
{
break
;
}
const
val
=
sequences
[
0
][
commonLength
];
for
(
let
i
=
1
,
len
=
sequences
.
length
;
i
<
len
;
i
++
)
{
if
(
sequences
[
i
].
length
<=
commonLength
||
sequences
[
i
][
commonLength
]
!==
val
)
{
break
commonLengthLoop
;
}
}
commonLength
++
;
}
return
commonLength
;
};
/**
* Find the nearest common ancestor of DOM nodes
*
* @param {...Node|null} nodes DOM nodes
* @return {Node|null} Nearest common ancestor; or null if there is none / an argument is null
*/
ve
.
getCommonAncestor
=
function
(
...
nodes
)
{
const
nodeCount
=
nodes
.
length
;
if
(
nodeCount
===
0
)
{
return
null
;
}
let
minHeight
=
null
;
const
chains
=
[];
// Build every chain
for
(
let
i
=
0
;
i
<
nodeCount
;
i
++
)
{
const
chain
=
[];
let
node
=
nodes
[
i
];
while
(
node
!==
null
)
{
chain
.
unshift
(
node
);
node
=
node
.
parentNode
;
}
if
(
chain
.
length
===
0
)
{
// nodes[ i ] was null (so no common ancestor)
return
null
;
}
if
(
i
>
0
&&
chain
[
0
]
!==
chains
[
chains
.
length
-
1
][
0
]
)
{
// No common ancestor (different documents or unattached branches)
return
null
;
}
if
(
minHeight
===
null
||
minHeight
>
chain
.
length
)
{
minHeight
=
chain
.
length
;
}
chains
.
push
(
chain
);
}
// Step through chains in parallel, until they differ.
// All chains are guaranteed to start with the common document element (or the common root
// of an unattached branch)
for
(
let
i
=
1
;
i
<
minHeight
;
i
++
)
{
const
node
=
chains
[
0
][
i
];
for
(
let
j
=
1
;
j
<
nodeCount
;
j
++
)
{
if
(
node
!==
chains
[
j
][
i
]
)
{
return
chains
[
0
][
i
-
1
];
}
}
}
return
chains
[
0
][
minHeight
-
1
];
};
/**
* Get the index of a node in its parentNode's childNode list
*
* @param {Node} node The node
* @return {number} Index in parentNode's childNode list
*/
ve
.
parentIndex
=
function
(
node
)
{
return
Array
.
prototype
.
indexOf
.
call
(
node
.
parentNode
.
childNodes
,
node
);
};
/**
* Get the offset path from ancestor to offset in descendant
*
* @param {Node} ancestor The ancestor node
* @param {Node} node The descendant node
* @param {number} nodeOffset The offset in the descendant node
* @return {number[]} The offset path
*/
ve
.
getOffsetPath
=
function
(
ancestor
,
node
,
nodeOffset
)
{
const
path
=
[
nodeOffset
];
while
(
node
!==
ancestor
)
{
if
(
node
.
parentNode
===
null
)
{
ve
.
log
(
node
,
'is not a descendant of'
,
ancestor
);
throw
new
Error
(
'Not a descendant'
);
}
path
.
unshift
(
ve
.
parentIndex
(
node
)
);
node
=
node
.
parentNode
;
}
return
path
;
};
/**
* Compare two tuples in lexicographical order.
*
* This function first compares `a[0]` with `b[0]`, then `a[1]` with `b[1]`, etc.
* until it encounters a pair where `a[k] != b[k]`; then returns `a[k] - b[k]`.
*
* If `a[k] == b[k]` for every `k`, this function returns 0.
*
* If a and b are of unequal length, but `a[k] == b[k]` for all `k` that exist in both a and b, then
* this function returns `Infinity` (if a is longer) or `-Infinity` (if b is longer).
*
* @param {number[]} a First tuple
* @param {number[]} b Second tuple
* @return {number} `a[k] - b[k]` where k is the lowest k such that `a[k] != b[k]`
*/
ve
.
compareTuples
=
function
(
a
,
b
)
{
for
(
let
i
=
0
,
len
=
Math
.
min
(
a
.
length
,
b
.
length
);
i
<
len
;
i
++
)
{
if
(
a
[
i
]
!==
b
[
i
]
)
{
return
a
[
i
]
-
b
[
i
];
}
}
if
(
a
.
length
>
b
.
length
)
{
return
Infinity
;
}
if
(
a
.
length
<
b
.
length
)
{
return
-
Infinity
;
}
return
0
;
};
/**
* Compare two nodes for position in document
*
* Return null if either position is either null or incomparable (e.g. where one of the nodes
* is detached or the nodes are from different documents).
*
* @param {Node|null} node1 First node
* @param {number|null} offset1 First offset
* @param {Node|null} node2 Second node
* @param {number|null} offset2 Second offset
* @return {number|null} negative, zero or positive number, or null if nodes null or incomparable
*/
ve
.
compareDocumentOrder
=
function
(
node1
,
offset1
,
node2
,
offset2
)
{
const
commonAncestor
=
ve
.
getCommonAncestor
(
node1
,
node2
);
if
(
commonAncestor
===
null
)
{
// Signal no common ancestor. In theory we could disallow this case, and check
// the nodes for detachedness and same-documentness before each call, but such
// guard checks would duplicate (either explicitly or implicitly) much of the
// branch traversal performed in this method.
return
null
;
}
return
ve
.
compareTuples
(
ve
.
getOffsetPath
(
commonAncestor
,
node1
,
offset1
),
ve
.
getOffsetPath
(
commonAncestor
,
node2
,
offset2
)
);
};
/**
* @typedef {Object} DomPosition
* @memberof ve
* @property {Node|null} node The node, or null if we stepped past the root node
* @property {number|null} offset The offset, or null if we stepped past the root node
* @property {ve.PositionStep[]} steps Steps taken
*/
/**
* Get the closest matching DOM position in document order (forward or reverse)
*
* A DOM position is represented as an object with "node" and "offset" properties.
*
* The noDescend option can be used to exclude the positions inside certain element nodes; it is
* a jQuery selector/function ( used as a test by $node.is() - see http://api.jquery.com/is/ );
* it defaults to ve.rejectsCursor . Void elements (those matching ve.isVoidElement) are always
* excluded.
*
* As well as the end position, an array of ve.PositionSteps (node traversals) is returned.
* The "stop" option is a boolean-valued function used to test each ve.PositionStep in turn. If
* If it returns true, the position arrived at is returned; else the stepping continues to the
* next matching DOM position. It defaults to ve.isHardCursorStep .
*
* Limitation: some DOM positions cannot actually hold the cursor; e.g. the start of the interior
* of a table node. Browser cursoring jumps over text node/annotation node boundaries as if they
* were invisible, and skips over most grapheme clusters e.g. 'x\u0301' (though not all e.g.
* '\u062D\u0627'). Also, Chromium normalizes cursor focus/offset, when they are set, to the
* start-most such cursor position (Firefox does not).
*
* @param {Object} position Start position
* @param {Node} position.node Start node
* @param {number} position.offset Start offset
* @param {number} direction +1 for forward, -1 for reverse
* @param {Object} options
* @param {Function|string} [options.noDescend] Selector or function: nodes to skip over
* @param {Function} [options.stop] Boolean-valued ve.PositionStep test function
* @return {ve.DomPosition} The adjacent DOM position encountered
* @see ve.isHardCursorStep
*/
ve
.
adjacentDomPosition
=
function
(
position
,
direction
,
options
)
{
let
node
=
position
.
node
,
offset
=
position
.
offset
;
const
steps
=
[];
const
noDescend
=
options
.
noDescend
||
ve
.
rejectsCursor
;
const
stop
=
options
.
stop
||
ve
.
isHardCursorStep
;
direction
=
direction
<
0
?
-
1
:
1
;
const
forward
=
(
direction
===
1
);
while
(
true
)
{
// If we're at the node's leading edge, move to the adjacent position in the parent node
if
(
offset
===
(
forward
?
node
.
length
||
node
.
childNodes
.
length
:
0
)
)
{
const
step
=
new
ve
.
PositionStep
(
node
,
'leave'
);
steps
.
push
(
step
);
if
(
node
.
parentNode
===
null
)
{
return
{
node
:
null
,
offset
:
null
,
steps
:
steps
};
}
offset
=
ve
.
parentIndex
(
node
)
+
(
forward
?
1
:
0
);
node
=
node
.
parentNode
;
if
(
stop
(
step
)
)
{
return
{
node
:
node
,
offset
:
offset
,
steps
:
steps
};
}
// Else take another step
continue
;
}
// Else we're in the interior of a node
// If we're in a text node, move to the position in this node at the next offset
if
(
node
.
nodeType
===
Node
.
TEXT_NODE
)
{
const
step
=
new
ve
.
PositionStep
(
node
,
'internal'
,
offset
-
(
forward
?
0
:
1
)
);
steps
.
push
(
step
);
offset
+=
direction
;
if
(
stop
(
step
)
)
{
return
{
node
:
node
,
offset
:
offset
,
steps
:
steps
};
}
continue
;
}
// Else we're in the interior of an element node
const
childNode
=
node
.
childNodes
[
forward
?
offset
:
offset
-
1
];
// Support: Firefox
// If the child is uncursorable, or is an element matching noDescend, do not
// descend into it: instead, return the position just beyond it in the current node
if
(
childNode
.
nodeType
===
Node
.
ELEMENT_NODE
&&
(
ve
.
isVoidElement
(
childNode
)
||
$
(
childNode
).
is
(
noDescend
)
)
)
{
const
step
=
new
ve
.
PositionStep
(
childNode
,
'cross'
);
steps
.
push
(
step
);
offset
+=
forward
?
1
:
-
1
;
if
(
stop
(
step
)
)
{
return
{
node
:
node
,
offset
:
offset
,
steps
:
steps
};
}
// Else take another step
continue
;
}
// Go to the closest offset inside the child node
node
=
childNode
;
offset
=
forward
?
0
:
node
.
length
||
node
.
childNodes
.
length
;
const
posStep
=
new
ve
.
PositionStep
(
node
,
'enter'
);
steps
.
push
(
posStep
);
if
(
stop
(
posStep
)
)
{
return
{
node
:
node
,
offset
:
offset
,
steps
:
steps
};
}
}
};
/**
* Test whether a cursor movement step uses up a cursor press
*
* Essentially, this is true unless entering/exiting a contentEditable text/annotation node.
* For instance in <#text>X</#text><b><#text>y</#text></b>
* a single cursor press will jump from just after X to just after Y.
*
* @param {ve.PositionStep} step The cursor movement step to test
* @return {boolean} Whether the cursor movement step uses up a cursor press
* @see ve.adjacentDomPosition
*/
ve
.
isHardCursorStep
=
function
(
step
)
{
if
(
step
.
node
.
nodeType
===
Node
.
TEXT_NODE
)
{
return
step
.
type
===
'internal'
;
}
return
ve
.
isBlockElement
(
step
.
node
)
||
ve
.
rejectsCursor
(
step
.
node
);
};
/**
* Tests whether an adjacent cursor would be prevented from entering the node
*
* @param {Node} [node] Element node or text node; defaults to "this" if a Node
* @return {boolean} Whether an adjacent cursor would be prevented from entering
*/
ve
.
rejectsCursor
=
function
(
node
)
{
if
(
!
node
&&
this
instanceof
Node
)
{
node
=
this
;
}
if
(
node
.
nodeType
===
node
.
TEXT_NODE
)
{
return
false
;
}
if
(
ve
.
isVoidElement
(
node
)
)
{
return
true
;
}
// We don't need to check whether the ancestor-nearest contenteditable tag is
// false, because if so then there can be no adjacent cursor.
return
node
.
contentEditable
===
'false'
;
};
/**
* @typedef {Object} ChangeOffsets
* @memberof ve
* @return {number} start Offset from start of first changed element
* @return {number} end Offset from end of last changed element (nonoverlapping with start)
*/
/**
* Count the common elements at the start and end of two sequences
*
* @param {Array|string} before The original sequence
* @param {Array|string} after The modified sequence
* @param {Function} [equals] Two-argument comparison returning boolean (defaults to ===)
* @return {ve.ChangeOffsets|null} Change offsets (valid in both sequences), or null if unchanged
*/
ve
.
countEdgeMatches
=
function
(
before
,
after
,
equals
)
{
if
(
!
equals
)
{
equals
=
function
(
x
,
y
)
{
return
x
===
y
;
};
}
let
start
,
end
;
const
len
=
Math
.
min
(
before
.
length
,
after
.
length
);
// Find maximal matching left slice
for
(
start
=
0
;
start
<
len
;
start
++
)
{
if
(
!
equals
(
before
[
start
],
after
[
start
]
)
)
{
break
;
}
}
if
(
start
===
len
&&
before
.
length
===
after
.
length
)
{
return
null
;
}
// Find maximal matching right slice that doesn't overlap the left slice
for
(
end
=
0
;
end
<
len
-
start
;
end
++
)
{
if
(
!
equals
(
before
[
before
.
length
-
1
-
end
],
after
[
after
.
length
-
1
-
end
]
)
)
{
break
;
}
}
return
{
start
:
start
,
end
:
end
};
};
/**
* Same as Object.entries, because we don't yet presume ES2017
*
* @param {Object} ob The object
* @return {Array[]} Entries, in the form [string, any]
*/
// eslint-disable-next-line es-x/no-object-entries
ve
.
entries
=
Object
.
entries
||
(
(
ob
)
=>
Object
.
keys
(
ob
).
map
(
(
k
)
=>
[
k
,
ob
[
k
]
]
)
);
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Wed, Aug 19, 06:06 (2 d, 11 h ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
40/90/238750e9748d974a74b8f672a37f
Default Alt Text
ve.utils.js (40 KB)
Attached To
Mode
rMWPROD MediaWiki Production
Attached
Detach File
Event Timeline
Log In to Comment