@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
189 lines (187 loc) • 6.37 kB
JavaScript
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as published by
* the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import prettierXmlPlugin from '@prettier/plugin-xml';
import { format } from 'prettier/standalone';
import { nnou } from './object';
import { XMLParser } from 'fast-xml-parser';
import { legacyUnescapeXml } from './string';
export function fromString(xml) {
return xml != null ? new DOMParser().parseFromString(xml, 'text/xml') : null;
}
export function serialize(doc) {
return new XMLSerializer().serializeToString(doc);
}
export function beautify(xml, options) {
return format(xml, {
tabWidth: 2,
printWidth: +Infinity,
xmlWhitespaceSensitivity: 'ignore',
xmlSelfClosingSpace: true,
...options,
// @ts-ignore
parser: 'xml',
plugins: [prettierXmlPlugin]
});
}
export function getInnerHtml(element, options) {
let opts = Object.assign({ trim: true, applyLegacyUnescaping: false }, options);
let content = element?.innerHTML;
if (content) {
// @ts-ignore downlevelIteration
const matches = [...content.matchAll(/<!\[CDATA\[([\s\S\n]*?)\]\]>/g)];
if (matches.length > 0) {
content = matches[0][1].trim();
}
if (opts.trim) {
content = content.trim();
}
if (opts.applyLegacyUnescaping) {
content = legacyUnescapeXml(content);
}
}
return nnou(content) ? content : null;
}
export function getInnerHtmlNumber(element, parser = parseInt) {
const content = getInnerHtml(element);
const num = parser(content);
if (content === null || content === '') {
return null;
} else if (isNaN(num)) {
console.error(`[utils/xml/getInnerHtmlNumber] Expected number but got NaN. Received value was "${content}".`);
return null;
} else {
return num;
}
}
export function extractLocalizedElements(nodes) {
const items = {};
if (!Array.isArray(nodes)) {
nodes = Array.from(nodes);
}
nodes.forEach((tag) => {
const tagName = tag.tagName;
const lang = tag.getAttribute('lang');
if (lang === null) {
items[tagName] = getInnerHtml(tag);
} else {
items[`tagName_${lang}`] = getInnerHtml(tag);
}
});
return items;
}
export function commentless(xml) {
return xml.replace(/<!--[\s\S\n]*?-->/g, '');
}
export function findDocumentElement(element) {
do {
if (element instanceof XMLDocument) {
return element;
}
element = element?.parentNode;
} while (element);
return null;
}
export function createElements(element, data) {
Object.entries(data).forEach(([tag, content]) => {
if (tag === '@attributes') {
Object.entries(content).forEach(([attr, value]) => {
element.setAttribute(attr, `${value}`);
});
} else {
const elem = createElement(tag);
if (typeof content === 'string' || typeof content === 'number' || typeof content === 'boolean') {
elem.innerHTML = `${content}`;
} else if (Array.isArray(content)) {
elem.setAttribute('item-list', 'true');
if (content.length) {
if (typeof content[0] === 'object') {
content.forEach((itemData) => {
const item = createElement('item');
createElements(item, itemData);
elem.appendChild(item);
});
} else {
console.error(
`[utils/xml/createElements] Incorrect data supplied. Received an array with items of type ${typeof content[0]}.`,
content
);
}
}
} else if (content instanceof Element) {
elem.appendChild(content);
} else if (content !== null && content !== void 0) {
createElements(elem, content);
}
element.appendChild(elem);
}
});
}
export function wrapElementInAuxDocument(element) {
return fromString(`<?xml version="1.0" encoding="UTF-8"?>${element.outerHTML}`);
}
export function newXMLDocument(rootTagName = 'root') {
// With the document.implementation.createDocument, new elements then inserted into the xml document
// end up with an undesirable namespace and serialization looses the case, so sticking with creating from string.
return fromString(`<?xml version="1.0" encoding="UTF-8"?><${rootTagName} />`);
}
export function createElement(tagName, options) {
return newXMLDocument().createElement(tagName, options);
}
export function deserialize(xml, options) {
const parser = new XMLParser({
attributeNamePrefix: '',
ignoreAttributes: false,
htmlEntities: true,
ignoreDeclaration: true,
...options
});
if (typeof xml !== 'string') {
xml = serialize(xml);
}
return parser.parse(xml);
}
export function cdataWrap(value) {
return `<![CDATA[${value}]]>`;
}
export function parseValidateDocument(content) {
const xml = fromString(content);
const parseError = xml.querySelector('parsererror');
if (parseError) {
return parseError.querySelector('div').innerText;
}
return xml;
}
export const entityEncodingTagValueProcessor = (tag, value) =>
value
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/&/g, '&');