173 lines
5.4 KiB
JavaScript
173 lines
5.4 KiB
JavaScript
'use strict';
|
|
|
|
import XmlNode from './xmlNode.js';
|
|
import { Matcher } from 'path-expression-matcher';
|
|
|
|
const METADATA_SYMBOL = XmlNode.getMetaDataSymbol();
|
|
|
|
/**
|
|
* Helper function to strip attribute prefix from attribute map
|
|
* @param {object} attrs - Attributes with prefix (e.g., {"@_class": "code"})
|
|
* @param {string} prefix - Attribute prefix to remove (e.g., "@_")
|
|
* @returns {object} Attributes without prefix (e.g., {"class": "code"})
|
|
*/
|
|
function stripAttributePrefix(attrs, prefix) {
|
|
if (!attrs || typeof attrs !== 'object') return {};
|
|
if (!prefix) return attrs;
|
|
|
|
const rawAttrs = {};
|
|
for (const key in attrs) {
|
|
if (key.startsWith(prefix)) {
|
|
const rawName = key.substring(prefix.length);
|
|
rawAttrs[rawName] = attrs[key];
|
|
} else {
|
|
// Attribute without prefix (shouldn't normally happen, but be safe)
|
|
rawAttrs[key] = attrs[key];
|
|
}
|
|
}
|
|
return rawAttrs;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {array} node
|
|
* @param {any} options
|
|
* @param {Matcher} matcher - Path matcher instance
|
|
* @returns
|
|
*/
|
|
export default function prettify(node, options, matcher, readonlyMatcher) {
|
|
return compress(node, options, matcher, readonlyMatcher);
|
|
}
|
|
|
|
/**
|
|
* @param {array} arr
|
|
* @param {object} options
|
|
* @param {Matcher} matcher - Path matcher instance
|
|
* @returns object
|
|
*/
|
|
function compress(arr, options, matcher, readonlyMatcher) {
|
|
let text;
|
|
const compressedObj = {}; //This is intended to be a plain object
|
|
for (let i = 0; i < arr.length; i++) {
|
|
const tagObj = arr[i];
|
|
const property = propName(tagObj);
|
|
|
|
// Push current property to matcher WITH RAW ATTRIBUTES (no prefix)
|
|
if (property !== undefined && property !== options.textNodeName) {
|
|
const rawAttrs = stripAttributePrefix(
|
|
tagObj[":@"] || {},
|
|
options.attributeNamePrefix
|
|
);
|
|
matcher.push(property, rawAttrs);
|
|
}
|
|
|
|
if (property === options.textNodeName) {
|
|
if (text === undefined) text = tagObj[property];
|
|
else text += "" + tagObj[property];
|
|
} else if (property === undefined) {
|
|
continue;
|
|
} else if (tagObj[property]) {
|
|
|
|
let val = compress(tagObj[property], options, matcher, readonlyMatcher);
|
|
const isLeaf = isLeafTag(val, options);
|
|
|
|
if (tagObj[":@"]) {
|
|
assignAttributes(val, tagObj[":@"], readonlyMatcher, options);
|
|
} else if (Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode) {
|
|
val = val[options.textNodeName];
|
|
} else if (Object.keys(val).length === 0) {
|
|
if (options.alwaysCreateTextNode) val[options.textNodeName] = "";
|
|
else val = "";
|
|
}
|
|
|
|
if (tagObj[METADATA_SYMBOL] !== undefined && typeof val === "object" && val !== null) {
|
|
val[METADATA_SYMBOL] = tagObj[METADATA_SYMBOL]; // copy over metadata
|
|
}
|
|
|
|
|
|
if (compressedObj[property] !== undefined && Object.prototype.hasOwnProperty.call(compressedObj, property)) {
|
|
if (!Array.isArray(compressedObj[property])) {
|
|
compressedObj[property] = [compressedObj[property]];
|
|
}
|
|
compressedObj[property].push(val);
|
|
} else {
|
|
//TODO: if a node is not an array, then check if it should be an array
|
|
//also determine if it is a leaf node
|
|
|
|
// Pass jPath string or readonlyMatcher based on options.jPath setting
|
|
const jPathOrMatcher = options.jPath ? readonlyMatcher.toString() : readonlyMatcher;
|
|
if (options.isArray(property, jPathOrMatcher, isLeaf)) {
|
|
compressedObj[property] = [val];
|
|
} else {
|
|
compressedObj[property] = val;
|
|
}
|
|
}
|
|
|
|
// Pop property from matcher after processing
|
|
if (property !== undefined && property !== options.textNodeName) {
|
|
matcher.pop();
|
|
}
|
|
}
|
|
|
|
}
|
|
// if(text && text.length > 0) compressedObj[options.textNodeName] = text;
|
|
if (typeof text === "string") {
|
|
if (text.length > 0) compressedObj[options.textNodeName] = text;
|
|
} else if (text !== undefined) compressedObj[options.textNodeName] = text;
|
|
|
|
|
|
return compressedObj;
|
|
}
|
|
|
|
function propName(obj) {
|
|
const keys = Object.keys(obj);
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const key = keys[i];
|
|
if (key !== ":@") return key;
|
|
}
|
|
}
|
|
|
|
function assignAttributes(obj, attrMap, readonlyMatcher, options) {
|
|
if (attrMap) {
|
|
const keys = Object.keys(attrMap);
|
|
const len = keys.length; //don't make it inline
|
|
for (let i = 0; i < len; i++) {
|
|
const atrrName = keys[i]; // This is the PREFIXED name (e.g., "@_class")
|
|
|
|
// Strip prefix for matcher path (for isArray callback)
|
|
const rawAttrName = atrrName.startsWith(options.attributeNamePrefix)
|
|
? atrrName.substring(options.attributeNamePrefix.length)
|
|
: atrrName;
|
|
|
|
// For attributes, we need to create a temporary path
|
|
// Pass jPath string or matcher based on options.jPath setting
|
|
const jPathOrMatcher = options.jPath
|
|
? readonlyMatcher.toString() + "." + rawAttrName
|
|
: readonlyMatcher;
|
|
|
|
if (options.isArray(atrrName, jPathOrMatcher, true, true)) {
|
|
obj[atrrName] = [attrMap[atrrName]];
|
|
} else {
|
|
obj[atrrName] = attrMap[atrrName];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function isLeafTag(obj, options) {
|
|
const { textNodeName } = options;
|
|
const propCount = Object.keys(obj).length;
|
|
|
|
if (propCount === 0) {
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
propCount === 1 &&
|
|
(obj[textNodeName] || typeof obj[textNodeName] === "boolean" || obj[textNodeName] === 0)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
} |