/** @module */
"use strict";
const { DOMDataUtils } = require('../utils/DOMDataUtils.js');
const { DOMUtils } = require('../utils/DOMUtils.js');
const { DiffUtils } = require('./DiffUtils.js');
const { WTUtils } = require('../utils/WTUtils.js');
const { KV, TagTk, EndTagTk } = require('../tokens/TokenTypes.js');
/** @namespace */
class WTSUtils {
static isValidSep(sep) {
return sep.match(/^(\s|<!--([^\-]|-(?!->))*-->)*$/);
}
static hasValidTagWidths(dsr) {
return dsr &&
typeof (dsr[2]) === 'number' && dsr[2] >= 0 &&
typeof (dsr[3]) === 'number' && dsr[3] >= 0;
}
/**
* Get the attributes on a node in an array of KV objects.
*
* @param {Node} node
* @return {KV[]}
*/
static getAttributeKVArray(node) {
var attribs = node.attributes;
var kvs = [];
for (var i = 0, l = attribs.length; i < l; i++) {
var attrib = attribs.item(i);
kvs.push(new KV(attrib.name, attrib.value));
}
return kvs;
}
/**
* Create a `TagTk` corresponding to a DOM node.
*/
static mkTagTk(node) {
var attribKVs = this.getAttributeKVArray(node);
return new TagTk(node.nodeName.toLowerCase(), attribKVs, DOMDataUtils.getDataParsoid(node));
}
/**
* Create a `EndTagTk` corresponding to a DOM node.
*/
static mkEndTagTk(node) {
var attribKVs = this.getAttributeKVArray(node);
return new EndTagTk(node.nodeName.toLowerCase(), attribKVs, DOMDataUtils.getDataParsoid(node));
}
/**
* For new elements, attrs are always considered modified. However, For
* old elements, we only consider an attribute modified if we have shadow
* info for it and it doesn't match the current value.
* @return {Object}
* @return {any} return.value
* @return {boolean} return.modified If the value of the attribute changed since we parsed the wikitext.
* @return {boolean} return.fromsrc Whether we got the value from source-based roundtripping.
*/
static getShadowInfo(node, name, curVal) {
var dp = DOMDataUtils.getDataParsoid(node);
// Not the case, continue regular round-trip information.
if (dp.a === undefined || dp.a[name] === undefined) {
return {
value: curVal,
// Mark as modified if a new element
modified: WTUtils.isNewElt(node),
fromsrc: false,
};
} else if (dp.a[name] !== curVal) {
return {
value: curVal,
modified: true,
fromsrc: false,
};
} else if (dp.sa === undefined || dp.sa[name] === undefined) {
return {
value: curVal,
modified: false,
fromsrc: false,
};
} else {
return {
value: dp.sa[name],
modified: false,
fromsrc: true,
};
}
}
/**
* Get shadowed information about an attribute on a node.
*
* @param {Node} node
* @param {string} name
* @return {Object}
* @return {any} return.value
* @return {boolean} return.modified If the value of the attribute changed since we parsed the wikitext.
* @return {boolean} return.fromsrc Whether we got the value from source-based roundtripping.
*/
static getAttributeShadowInfo(node, name) {
return this.getShadowInfo(node, name, node.hasAttribute(name) ? node.getAttribute(name) : null);
}
static commentWT(comment) {
return '<!--' + WTUtils.decodeComment(comment) + '-->';
}
/**
* Emit the start tag source when not round-trip testing, or when the node is
* not marked with autoInsertedStart.
*/
static emitStartTag(src, node, state, dontEmit) {
if (!state.rtTestMode || !DOMDataUtils.getDataParsoid(node).autoInsertedStart) {
if (!dontEmit) {
state.emitChunk(src, node);
}
return true;
} else {
// drop content
return false;
}
}
/**
* Emit the start tag source when not round-trip testing, or when the node is
* not marked with autoInsertedStart.
*/
static emitEndTag(src, node, state, dontEmit) {
if (!state.rtTestMode || !DOMDataUtils.getDataParsoid(node).autoInsertedEnd) {
if (!dontEmit) {
state.emitChunk(src, node);
}
return true;
} else {
// drop content
return false;
}
}
/**
* In wikitext, did origNode occur next to a block node which has been
* deleted? While looking for next, we look past DOM nodes that are
* transparent in rendering. (See emitsSolTransparentSingleLineWT for
* which nodes.)
*/
static nextToDeletedBlockNodeInWT(origNode, before) {
if (!origNode || DOMUtils.isBody(origNode)) {
return false;
}
while (true) { // eslint-disable-line
// Find the nearest node that shows up in HTML (ignore nodes that show up
// in wikitext but don't affect sol-state or HTML rendering -- note that
// whitespace is being ignored, but that whitespace occurs between block nodes).
var node = origNode;
do {
node = before ? node.previousSibling : node.nextSibling;
if (DiffUtils.maybeDeletedNode(node)) {
return DiffUtils.isDeletedBlockNode(node);
}
} while (node && WTUtils.emitsSolTransparentSingleLineWT(node));
if (node) {
return false;
} else {
// Walk up past zero-width wikitext parents
node = origNode.parentNode;
if (!WTUtils.isZeroWidthWikitextElt(node)) {
// If the parent occupies space in wikitext,
// clearly, we are not next to a deleted block node!
// We'll eventually hit BODY here and return.
return false;
}
origNode = node;
}
}
}
/**
* Check if whitespace preceding this node would NOT trigger an indent-pre.
*/
static precedingSpaceSuppressesIndentPre(node, sepNode) {
if (node !== sepNode && DOMUtils.isText(node)) {
// if node is the same as sepNode, then the separator text
// at the beginning of it has been stripped out already, and
// we cannot use it to test it for indent-pre safety
return node.nodeValue.match(/^[ \t]*\n/);
} else if (node.nodeName === 'BR') {
return true;
} else if (WTUtils.isFirstEncapsulationWrapperNode(node)) {
// Dont try any harder than this
return (!node.hasChildNodes()) || node.innerHTML.match(/^\n/);
} else {
return WTUtils.isBlockNodeWithVisibleWT(node);
}
}
static traceNodeName(node) {
switch (node.nodeType) {
case node.ELEMENT_NODE:
return DOMUtils.isDiffMarker(node) ?
"DIFF_MARK" : "NODE: " + node.nodeName;
case node.TEXT_NODE:
return "TEXT: " + JSON.stringify(node.nodeValue);
case node.COMMENT_NODE:
return "CMT : " + JSON.stringify(WTSUtils.commentWT(node.nodeValue));
default:
return node.nodeName;
}
}
/**
* In selser mode, check if an unedited node's wikitext from source wikitext
* is reusable as is.
* @param {MWParserEnvironment} env
* @param {Node} node
* @return {boolean}
*/
static origSrcValidInEditedContext(env, node) {
var prev;
if (WTUtils.isRedirectLink(node)) {
return DOMUtils.isBody(node.parentNode) && !node.previousSibling;
} else if (node.nodeName === 'TH' || node.nodeName === 'TD') {
// The wikitext representation for them is dependent
// on cell position (first cell is always single char).
// If there is no previous sibling, nothing to worry about.
prev = node.previousSibling;
if (!prev) {
return true;
}
// If previous sibling is unmodified, nothing to worry about.
if (!DOMUtils.isDiffMarker(prev) &&
!DiffUtils.hasInsertedDiffMark(prev, env) &&
!DiffUtils.directChildrenChanged(prev, env)) {
return true;
}
// If it didn't have a stx marker that indicated that the cell
// showed up on the same line via the "||" or "!!" syntax, nothing
// to worry about.
return DOMDataUtils.getDataParsoid(node).stx !== 'row';
} else if (node.nodeName === 'TR' && !DOMDataUtils.getDataParsoid(node).startTagSrc) {
// If this <tr> didn't have a startTagSrc, it would have been
// the first row of a table in original wikitext. So, it is safe
// to reuse the original source for the row (without a "|-") as long as
// it continues to be the first row of the table. If not, since we need to
// insert a "|-" to separate it from the newly added row (in an edit),
// we cannot simply reuse orig. wikitext for this <tr>.
return !DOMUtils.previousNonSepSibling(node);
} else if (DOMUtils.isNestedListOrListItem(node)) {
// If there are no previous siblings, bullets were assigned to
// containing elements in the ext.core.ListHandler. For example,
//
// *** a
//
// Will assign bullets as,
//
// <ul><li-*>
// <ul><li-*>
// <ul><li-*> a</li></ul>
// </li></ul>
// </li></ul>
//
// If we reuse the src for the inner li with the a, we'd be missing
// two bullets because the tag handler for lists in the serializer only
// emits start tag src when it hits a first child that isn't a list
// element. We need to walk up and get them.
prev = node.previousSibling;
if (!prev) {
return false;
}
// If a previous sibling was modified, we can't reuse the start dsr.
while (prev) {
if (DOMUtils.isDiffMarker(prev) ||
DiffUtils.hasInsertedDiffMark(prev, env)
) {
return false;
}
prev = prev.previousSibling;
}
return true;
} else {
return true;
}
}
/**
* Extracts the media type from attribute string
*
* @param {Node} node
* @return {Object}
*/
static getMediaType(node) {
const typeOf = node.getAttribute('typeof') || '';
const match = typeOf.match(/(?:^|\s)(mw:(?:Image|Video|Audio))(?:\/(\w*))?(?:\s|$)/);
return {
rdfaType: match && match[1] || '',
format: match && match[2] || '',
};
}
/**
* @param {Object} dataMw
* @param {string} key
* @param {boolean} keep
* @return {Array|null}
*/
static getAttrFromDataMw(dataMw, key, keep) {
const arr = dataMw.attribs || [];
const i = arr.findIndex(a => (a[0] === key || a[0].txt === key));
if (i < 0) { return null; }
const ret = arr[i];
if (!keep && ret[1].html === undefined) {
arr.splice(i, 1);
}
return ret;
}
}
if (typeof module === "object") {
module.exports.WTSUtils = WTSUtils;
}