/**
* These utilities are for processing content that's generated
* by parsing source input (ex: wikitext)
*
* @module
*/
'use strict';
require('../../core-upgrade.js');
const XMLSerializer = require('../wt2html/XMLSerializer.js');
const { DOMDataUtils } = require('./DOMDataUtils.js');
const { DOMPostOrder } = require('./DOMPostOrder.js');
const { DOMUtils } = require('./DOMUtils.js');
const { Util } = require('./Util.js');
const { WTUtils } = require('./WTUtils.js');
class ContentUtils {
/**
* XML Serializer.
*
* @param {Node} node
* @param {Object} [options] XMLSerializer options.
* @return {string}
*/
static toXML(node, options) {
return XMLSerializer.serialize(node, options).html;
}
/**
* .dataobject aware XML serializer, to be used in the DOM
* post-processing phase.
*
* @param {Node} node
* @param {Object} [options]
* @return {string}
*/
static ppToXML(node, options) {
// We really only want to pass along `options.keepTmp`
DOMDataUtils.visitAndStoreDataAttribs(node, options);
return this.toXML(node, options);
}
/**
* .dataobject aware HTML parser, to be used in the DOM
* post-processing phase.
*
* @param {MWParserEnvironment} env
* @param {string} html
* @param {Object} [options]
* @return {Node}
*/
static ppToDOM(env, html, options) {
options = options || {};
var node = options.node;
if (node === undefined) {
node = env.createDocument(html).body;
} else {
node.innerHTML = html;
}
if (options.reinsertFosterableContent) {
DOMUtils.visitDOM(node, (n, ...args) => {
// untunnel fostered content
const meta = WTUtils.reinsertFosterableContent(env, n, true);
n = (meta !== null) ? meta : n;
// load data attribs
DOMDataUtils.loadDataAttribs(n, ...args);
}, options);
} else {
// load data attribs
DOMDataUtils.visitAndLoadDataAttribs(node, options);
}
return node;
}
/**
* Pull the data-parsoid script element out of the doc before serializing.
*
* @param {Node} node
* @param {Object} [options] XMLSerializer options.
* @return {string}
*/
static extractDpAndSerialize(node, options) {
if (!options) { options = {}; }
var pb = DOMDataUtils.extractPageBundle(DOMUtils.isBody(node) ? node.ownerDocument : node);
var out = XMLSerializer.serialize(node, options);
out.pb = pb;
return out;
}
static stripSectionTagsAndFallbackIds(node) {
var n = node.firstChild;
while (n) {
var next = n.nextSibling;
if (DOMUtils.isElt(n)) {
// Recurse into subtree before stripping this
this.stripSectionTagsAndFallbackIds(n);
// Strip <section> tags
if (WTUtils.isParsoidSectionTag(n)) {
DOMUtils.migrateChildren(n, n.parentNode, n);
n.parentNode.removeChild(n);
}
// Strip <span typeof='mw:FallbackId' ...></span>
if (WTUtils.isFallbackIdSpan(n)) {
n.parentNode.removeChild(n);
}
}
n = next;
}
}
/**
* Shift the DSR of a DOM fragment.
*/
static shiftDSR(env, rootNode, dsrFunc) {
/* eslint-disable no-use-before-define */ // mutual recursion ftw
const dsrThunk = (dsr) => {
// Clone the dsr
const nDsr = dsrFunc(Array.from(dsr));
// Map 'null' to 'undefined' in return value.
return nDsr === null ? undefined : nDsr;
};
const convertNode = (node) => {
if (!DOMUtils.isElt(node)) { return; }
const dp = DOMDataUtils.getDataParsoid(node);
if (Array.isArray(dp.dsr)) {
dp.dsr = dsrThunk(dp.dsr);
}
if (Array.isArray(dp.tmp && dp.tmp.origDSR)) {
dp.tmp.origDSR = dsrThunk(dp.tmp.origDSR);
}
if (Array.isArray(dp.extTagOffsets)) {
dp.extTagOffsets = dsrThunk(dp.extTagOffsets);
}
// We don't need to setDataParsoid because dp is not a copy
// Handle embedded HTML in Language Variant markup
const dmwv =
DOMDataUtils.getJSONAttribute(node, 'data-mw-variant', null);
if (dmwv) {
if (dmwv.disabled) {
dmwv.disabled.t = convertString(dmwv.disabled.t);
}
if (dmwv.twoway) {
dmwv.twoway.forEach((l) => {
l.t = convertString(l.t);
});
}
if (dmwv.oneway) {
dmwv.oneway.forEach((l) => {
l.f = convertString(l.f);
l.t = convertString(l.t);
});
}
if (dmwv.filter) {
dmwv.filter.t = convertString(dmwv.filter.t);
}
DOMDataUtils.setJSONAttribute(node, 'data-mw-variant', dmwv);
}
if (DOMUtils.matchTypeOf(node, /^mw:(Image|ExpandedAttrs)$/)) {
const dmw = DOMDataUtils.getDataMw(node);
// Handle embedded HTML in template-affected attributes
if (dmw.attribs) {
dmw.attribs.forEach(a => a.forEach((kOrV) => {
if (typeof (kOrV) !== 'string' && kOrV.html) {
kOrV.html = convertString(kOrV.html);
}
}));
}
// Handle embedded HTML in figure-inline captions
if (dmw.caption) {
dmw.caption = convertString(dmw.caption);
}
DOMDataUtils.setDataMw(node, dmw);
}
if (DOMUtils.matchTypeOf(node, /^mw:DOMFragment(\/|$)/)) {
const dp = DOMDataUtils.getDataParsoid(node);
// Handle embedded HTML in tunneled DOM Fragments
if (dp.html) {
const nodes = env.fragmentMap.get(dp.html);
nodes.forEach((n) => {
DOMDataUtils.visitAndLoadDataAttribs(n);
DOMPostOrder(n, convertNode);
DOMDataUtils.visitAndStoreDataAttribs(n);
});
}
}
};
const convertString = (str) => {
const parentNode = rootNode.ownerDocument.createElement('body');
const node = ContentUtils.ppToDOM(env, str, { node: parentNode });
DOMPostOrder(node, convertNode);
return ContentUtils.ppToXML(node, { innerXML: true });
};
/* eslint-enable no-use-before-define */
DOMPostOrder(rootNode, convertNode);
return rootNode; // chainable
}
/**
* Dump the DOM with attributes.
*
* @param {Node} rootNode
* @param {string} title
* @param {Object} [options]
*/
static dumpDOM(rootNode, title, options) {
options = options || {};
if (options.storeDiffMark || options.dumpFragmentMap) { console.assert(options.env); }
function cloneData(node, clone) {
if (!DOMUtils.isElt(node)) { return; }
var d = DOMDataUtils.getNodeData(node);
DOMDataUtils.setNodeData(clone, Util.clone(d));
node = node.firstChild;
clone = clone.firstChild;
while (node) {
cloneData(node, clone);
node = node.nextSibling;
clone = clone.nextSibling;
}
}
function emit(buf, opts) {
if ('outBuffer' in opts) {
opts.outBuffer += buf.join('\n');
} else if (opts.outStream) {
opts.outStream.write(buf.join('\n') + '\n');
} else {
console.warn(buf.join('\n'));
}
}
// cloneNode doesn't clone data => walk DOM to clone it
var clonedRoot = rootNode.cloneNode(true);
cloneData(rootNode, clonedRoot);
var buf = [];
if (!options.quiet) {
buf.push('----- ' + title + ' -----');
}
buf.push(ContentUtils.ppToXML(clonedRoot, options));
emit(buf, options);
// Dump cached fragments
if (options.dumpFragmentMap) {
Array.from(options.env.fragmentMap.keys()).forEach(function(k) {
buf = [];
buf.push('='.repeat(15));
buf.push("FRAGMENT " + k);
buf.push("");
emit(buf, options);
const newOpts = Object.assign({}, options, { dumpFragmentMap: false, quiet: true });
const fragment = options.env.fragmentMap.get(k);
ContentUtils.dumpDOM(Array.isArray(fragment) ? fragment[0] : fragment, '', newOpts);
});
}
if (!options.quiet) {
emit(['-'.repeat(title.length + 12)], options);
}
}
}
if (typeof module === "object") {
module.exports.ContentUtils = ContentUtils;
}