import _ from 'lodash'; import NODE_MAP from './node-map'; import {ATTR_MAP, REMOVE_ATTRS} from './attr-map'; import TRANSFORMS from './transformers'; export const ATTR_PREFIX = '@_'; export const IDX_PATH_PREFIX = `${ATTR_PREFIX}indexPath`; export const IDX_PREFIX = `${ATTR_PREFIX}index`; const isAttr = (/** @type {string} */ k) => k.startsWith(ATTR_PREFIX); const isNode = (/** @type {string} */ k) => !isAttr(k); /** * * @param {string} xmlStr * @param {string} platform * @param {{metadata?: Object, addIndexPath?: boolean}} opts * @returns {Promise<{xml: string, unknowns: NodesAndAttributes}>} */ export async function transformSourceXml(xmlStr, platform, {metadata = {}, addIndexPath = false} = {}) { // first thing we want to do is modify the ios source root node, because it doesn't include the // necessary index attribute, so we add it if it's not there xmlStr = xmlStr.replace('', ''); const xmlObj = (await singletonXmlParser()).parse(xmlStr); const unknowns = transformNode(xmlObj, platform, { metadata, addIndexPath, parentPath: '', }); let transformedXml = (await singletonXmlBuilder()).build(xmlObj).trim(); transformedXml = `\n${transformedXml}`; return {xml: transformedXml, unknowns}; } /** * * @param {Object} nameMap * @param {string} name * @param {string} platform * @returns {string | null} */ function getUniversalName(nameMap, name, platform) { for (const translatedName of Object.keys(nameMap)) { const sourceNodes = nameMap[translatedName]?.[platform]; if (_.isArray(sourceNodes) && sourceNodes.includes(name)) { return translatedName; } if (sourceNodes === name) { return translatedName; } } return null; } /** * * @param {any} nodeName * @param {string} platform * @returns {string?} */ export function getUniversalNodeName(nodeName, platform) { return getUniversalName(NODE_MAP, nodeName, platform); } /** * * @param {string} attrName * @param {string} platform * @returns {string?} */ export function getUniversalAttrName(attrName, platform) { return getUniversalName(ATTR_MAP, attrName, platform); } /** * * @param {any} nodeObj * @param {string} platform * @param {{metadata?: Object, addIndexPath?: boolean, parentPath?: string}} opts * @returns {NodesAndAttributes} */ export function transformNode(nodeObj, platform, {metadata, addIndexPath, parentPath}) { const unknownNodes = []; const unknownAttrs = []; if (_.isPlainObject(nodeObj)) { const keys = Object.keys(nodeObj); const childNodeNames = keys.filter(isNode); const attrs = keys.filter(isAttr); let thisIndexPath = parentPath; if (attrs.length && addIndexPath) { if (!attrs.includes(IDX_PREFIX)) { throw new Error(`Index path is required but node found with no 'index' attribute`); } thisIndexPath = `${parentPath}/${nodeObj[IDX_PREFIX]}`; nodeObj[IDX_PATH_PREFIX] = thisIndexPath; } TRANSFORMS[platform]?.(nodeObj, metadata); unknownAttrs.push(...transformAttrs(nodeObj, attrs, platform)); const unknowns = transformChildNodes(nodeObj, childNodeNames, platform, { metadata, addIndexPath, parentPath: thisIndexPath, }); unknownAttrs.push(...unknowns.attrs); unknownNodes.push(...unknowns.nodes); } else if (_.isArray(nodeObj)) { for (const childObj of nodeObj) { const {nodes, attrs} = transformNode(childObj, platform, { metadata, addIndexPath, parentPath, }); unknownNodes.push(...nodes); unknownAttrs.push(...attrs); } } return { nodes: _.uniq(unknownNodes), attrs: _.uniq(unknownAttrs), }; } /** * * @param {any} nodeObj * @param {string[]} childNodeNames * @param {string} platform * @param {{metadata?: Object, addIndexPath?: boolean, parentPath?: string}} opts * @returns {NodesAndAttributes} */ export function transformChildNodes( nodeObj, childNodeNames, platform, {metadata, addIndexPath, parentPath} ) { const unknownNodes = []; const unknownAttrs = []; for (const nodeName of childNodeNames) { // before modifying the name of this child node, recurse down and modify the subtree const {nodes, attrs} = transformNode(nodeObj[nodeName], platform, { metadata, addIndexPath, parentPath, }); unknownNodes.push(...nodes); unknownAttrs.push(...attrs); // now translate the node name and replace the subtree with this node const universalName = getUniversalNodeName(nodeName, platform); if (universalName === null) { unknownNodes.push(nodeName); continue; } // since multiple child node names could map to the same new transformed node name, we can't // simply assign nodeObj[universalName] = nodeObj[nodeName]; we need to be sensitive to the // situation where the end result is an array of children having the same node name if (nodeObj[universalName]) { // if we already have a node with the universal name, that means we are mapping a second // original node name to the same universal node name, so we just push all its children into // the list nodeObj[universalName].push(...nodeObj[nodeName]); } else { nodeObj[universalName] = nodeObj[nodeName]; } delete nodeObj[nodeName]; } return {nodes: unknownNodes, attrs: unknownAttrs}; } /** * * @param {any} nodeObj * @param {string[]} attrs * @param {string} platform * @returns {string[]} */ export function transformAttrs(nodeObj, attrs, platform) { const unknownAttrs = []; for (const attr of attrs) { const cleanAttr = attr.substring(2); if (REMOVE_ATTRS.includes(cleanAttr)) { delete nodeObj[attr]; continue; } const universalAttr = getUniversalAttrName(cleanAttr, platform); if (universalAttr === null) { unknownAttrs.push(cleanAttr); continue; } const newAttr = `${ATTR_PREFIX}${universalAttr}`; if (newAttr !== attr) { nodeObj[newAttr] = nodeObj[attr]; delete nodeObj[attr]; } } return unknownAttrs; } const singletonXmlBuilder = _.memoize(async function makeXmlBuilder() { const { XMLBuilder } = await import('fast-xml-parser'); return new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: ATTR_PREFIX, suppressBooleanAttributes: false, format: true, }); }); const singletonXmlParser = _.memoize(async function makeXmlParser() { const { XMLParser } = await import('fast-xml-parser'); return new XMLParser({ ignoreAttributes: false, ignoreDeclaration: true, attributeNamePrefix: ATTR_PREFIX, isArray: (name, jPath, isLeafNode, isAttribute) => !isAttribute, }); }); /** * @typedef {{nodes: string[], attrs: string[]}} NodesAndAttributes */