четверг, 26 января 2017 г.

JavaScript XML конвертация в объект, DOM и обратно в XML

Файлы:
validate-xml.js
format-xml.js
escape-xml.js
convert-xml-to-dom.js
convert-object-to-dom-xml.js
convert-dom-to-xml.js
convert-dom-xml-to-object.js

Файл validate-xml.js

function validateXML (xml, options) {
    var xmlFragment = false; // false - проверяемый код это целый xml, true - проверяемый код не целый xml, а только его часть
    if (options && options.hasOwnProperty('xmlFragment')) {
        xmlFragment = options.xmlFragment;
    }
    var regTab = /[\n\t\r]+/g
        , regCommentAndCdata = /<!(?:--(?:[^-]|-[^-])*--|\[CDATA\[(?:[^\]]|\][^\]]|\]+[^\>\]])*]{2,})>/g
        , regInstruction = /<\?.*?\?>/
        , regDocType = /<\!DocType.*?>/i
        , regOutTagTextBegin = /^\s*[^<\s]+/
        , regEntityFull = /&(?:#(?:x[a-f\d]{1,4}|\d{2,5})|[a-z][\w\-]*);/gi
        , regAttribute = /(<[a-z_][\w:-]*)((?:\s+[a-z_][\w:-]*\s*=\s*(?:'[^<>']*'|"[^<>"]*"))*)\s*(\/?>)/gi
        , regAttributeUnique = /([a-z_][\w:-]*)\s*=\s*(?:'[^<>']*'|"[^<>"]*")/gi
        , regAttributeMatch = /[a-z_][\w:-]*/gi
        , regSingleTag = /<[a-z_][\w:-]*\/>/gi
        , regDoubleTag = /<([a-zA-Z_][\w:-]*)>[^<]*<\/\1\s*>/g;
    if (xml) {
        // Вырезаем табуляцию и переносы строк
        xml = xml.replace(regTab, ' ');
        // Вырезаем комментарии и CDATA
        xml = xml.replace(regCommentAndCdata, '');
        if (xml.indexOf('<!--') !== -1) {return false;}
        if (xml.indexOf(']]>') !== -1) {return false;}
        // Вырезаем инструкции
        if (!xmlFragment) {
            xml = xml.replace(regInstruction, '');
            if (xml.search(regInstruction) !== -1) {return false;}
        }
        // Вырезаем DocType
        if (!xmlFragment) {xml = xml.replace(regDocType, '');}
        if (xml.search(regDocType) !== -1) {return false;}
        // Ищем текст в начале и в конце строки, выходящий за пределы тегов
        if (!xmlFragment) {
            if (xml.search(regOutTagTextBegin) !== -1) {return false;}
            // Конец строки
            var valueLength = xml.length
                , isSpace = true;
            do {
                valueLength--;
                if (xml.charAt(valueLength) !== ' ') {isSpace = false;}
            } while (isSpace && valueLength > 0);
            if (!isSpace && xml.charAt(valueLength) !== '>') {
                return false;
            } else if (valueLength === 0) {
                return false;
            }
        }
        // Вырезаем Entities
        xml = xml.replace(regEntityFull, '');
        if (xml.indexOf('&') !== -1) {return false;}
        // Вырезаем аттрибуты и проверяем на дублирование
        var attributeUnique = true;
        xml = xml.replace(regAttribute, function ($0, $1, $2 ,$3) {
            $2 = $2.replace(regAttributeUnique, '$1');
            var attribute = $2.match(regAttributeMatch);
            if (attribute) {
                var matchCount = attribute.length;
                if (matchCount > 1) {
                    var i = 0
                        , j;
                    while (attributeUnique && i < matchCount - 1) {
                        j = i + 1;
                        while (attributeUnique && j < matchCount) {
                            if (attribute[i] !== attribute[j]) {
                                j++;
                            } else {
                                attributeUnique = false;
                            }
                        }
                        i++;
                    }
                }
            }
            return $1 + $3;
        });
        if (!attributeUnique) {return false;}
        // Параметр для вырезания тэгов
        var tagReplaceTo = '';
        if (!xmlFragment) {tagReplaceTo = '&';}
        // Вырезаем одинарные тэги
        xml = xml.replace(regSingleTag, tagReplaceTo);
        // Вырезаем двойные тэги
        var previousLen
            , len = 0;
        do {
            previousLen = len;
            xml = xml.replace(regDoubleTag, tagReplaceTo);
            len = xml.length;
        } while (len !== previousLen);
        if (!xmlFragment) {
            if (xml.indexOf(tagReplaceTo) !== xml.lastIndexOf(tagReplaceTo)) {return false;}
        }
        if (xml.indexOf('<') !== -1) {return false;}
        return true;
    } else { // Пустая строка вместо XML
        if (!xmlFragment) {
            return false;
        } else {
            return true;
        }
    }
}

// Test

var xmlString = '<a id="a"><b id="b">hey!</b></a>';
console.log(validateXML(xmlString, {xmlFragment: true})); // true | false

Файл format-xml.js

// Функция делает простое форматирование XML и
// удаляет перенос на другую строку внутри текста, расположенного между тэгами
function formatXML (xml) {
    xml = String(xml); // привести к строке на случай, если xml - это объект
    xml = xml.replace(/\r|\n/g, ''); // удалить уже существующие символы перехода на следующую строку \r и \n в том числе внутри текста, расположенного между тэгами
    xml = xml.replace(/(>)\s*(<)(\/*)/g, '$1\r\n$2$3'); // удалить пробелы между тэгами (<tag>      </tag>), заменив их на символы \r\n, итоговый результат: >\r\n</
    var formattedXML = ''
        , xmlParts = xml.split('\r\n') // разбить содержимое XML на части
        , xmlPartsLength = xmlParts.length
        , i
        , j
        , pad = 0
        , indent
        , padding;
    for (i = 0; i < xmlPartsLength; i++) {
        indent = 0;
        if (xmlParts[i].match(/.+<\/\w[^>]*>$/)) {
            // Пример: some text</tag>
            // любой символ встречается 1 или более раз,
            // за ним идут символы </ и далее одна буква,
            // после этого любой набор символов, кроме символа >
            // и в конце идет закрывающая скобка >
            indent = 0;
        } else if (xmlParts[i].match(/^<\/\w/)) {
            // Пример: </a
            // начинается с </ и одной буквы
            if (pad !== 0) {
                pad -= 1;
            }
        } else if (xmlParts[i].match( /^<\w[^>]*[^\/]>.*$/ )) {
            // Пример: <tag>some text
            // начинается с < и одной буквы,
            // далее идет любой набор символов, кроме символа >,
            // после чего идет любой набор символов, кроме символа /,
            // затем идет символ >
            // после этого идут любые сиволы до конца строки
            indent = 1;
        } else {
            indent = 0;
        }
        padding = '';
        for (j = 0; j < pad; j++) {
            padding += '    '; // 4 пробела можно заменить на Tab: padding += '\t';
        }
        formattedXML += padding + xmlParts[i] + '\r\n';
        pad += indent;
    }
    return formattedXML;
}

// Test

var xmlString = '<a id="a"><b id="b">hey!</b></a>';
console.log(formatXML(xmlString));
// Результ:
// <a id="a">
//   <b id="b">hey!</b>
// </a>

// Функция  делает более сложную проверку и форматирование XML и
// не удаляет перенос на другую строку внутри текста, расположенного между тэгами, оставляя его на разных строках
function alternativeFormatXML (xml) {
    xml = String(xml); // привести к строке на случай, если xml - это объект
    xml = xml.replace(/(>)\s*(<)(\/*)/g, '$1\n$2$3'); // удалить пробелы между тэгами (<tag>      </tag>), заменив их на символ \n, итоговый результат: >\n</
    xml = xml.replace(/ *(.*) +\n/g, '$1\n'); // вставить символ \n после последовательности |      some text    \n|, итоговый результат: |      some text    \n\n|
    xml = xml.replace(/(<.+>)(.+\n)/g, '$1\n$2'); // вставить символ \n между тэгом и текстом, итоговый результат: <tag>\nsome text\n
    var formattedXML = ''
        , transitions = {// 4 типа тэгов: single, closing, opening, other (text, doctype, comment) -  всего 4*4 = 16 вариантов transitions
              'single->single': 0
            , 'single->closing': -1
            , 'single->opening': 0
            , 'single->other': 0
            , 'closing->single': 0
            , 'closing->closing': -1
            , 'closing->opening': 0
            , 'closing->other': 0
            , 'opening->single': 1
            , 'opening->closing': 0
            , 'opening->opening': 1
            , 'opening->other': 1
            , 'other->single': 0
            , 'other->closing': -1
            , 'other->opening': 0
            , 'other->other': 0
          }
        , i
        , j
        , lines = xml.split('\n')
        , linesLength = lines.length
        , line
        , type
        , fromTo
        , lastType = 'other'
        , indent = 0
        , padding;
    for (i = 0; i < linesLength; i++) {
        line = lines[i];
                  if (/<.+\/>/.test(line)) {type = 'single'; // эта линия содержит одиночный (single) тэг, например: <br />
        } else if (/<\/.+>/.test(line)) {type = 'closing'; // эта линия содержит закрывающий (closing) тэг, например: </a>
        } else if (/<[^!].*>/.test(line)) {type = 'opening'; // эта линия содержит открывающий (opening) тэг, но это не что-то вроде <!something>, например: <a>
        } else {type = 'other'; // эта линия содержит text или тэг doctype, comment, например: <!--
        }
        fromTo = lastType + '->' + type;
        lastType = type;
        indent += transitions[fromTo];
        padding = '';
        for (j = 0; j < indent; j++) {
            padding += '    '; // 4 пробела можно заменить на Tab: padding += '\t';
        }
        if (fromTo === 'opening->closing') {
            formattedXML = formattedXML.substr(0, formattedXML.length - 1) + line + '\n'; // substr() удаляет разрыв строки (\n) оставшийся от предыдущего цикла
        } else {
            formattedXML += padding + line + '\n';
        }
    }
    return formattedXML;
}

// Test

var xmlString = '<a id="a"><b id="b">h\ne\ny\n!</b></a>';
console.log(alternativeFormatXML(xmlString));
// Результ:
// <a id="a">
//     <b id="b">
//         h
//         e
//         y
 //    !</b>
// </a>

Файл escape-xml.js

function escapeXML (xml) {
    // Замена символов: &, <, >, \n и пробелы на escape-последовательности
    return xml.replace(/&/g,'&amp;')
                    .replace(/</g,'&lt;')
                    .replace(/>/g,'&gt;')
                    .replace(/ /g, '&nbsp;')
                    .replace(/\n/g,'<br />');
}

// Test

var xmlString = '<a id="a"><b id="b">hey!</b></a>';
console.log(escapeXML(xmlString));

Файл convert-xml-to-dom.js

function convertXMLtoDOM (data) {
    if (typeof data !== 'string') {throw new Error('XML must be a string.');}
    if (data === '') {throw new Error('XML can\'t be an empty string.');}
    var xmlDOM;
    try {
        if (window.DOMParser) { // Standard
            xmlDOM = new DOMParser().parseFromString(data, 'text/xml');
        } else { // IE
            xmlDOM = new ActiveXObject('Microsoft.XMLDOM');
            xmlDOM.async = 'false';
            xmlDOM.loadXML(data);
        }
    } catch (e) {
        xmlDOM = undefined;
    }
    if (
            !xmlDOM
        || !xmlDOM.documentElement
        || xmlDOM.getElementsByTagName('parsererror').length
    ) {
        throw new Error('Invalid XML: ' + data);
    }
    return xmlDOM;
}

// Test

var xmlString = '<a id="a"><b id="b">hey!</b></a>'
    , xmlDOM = convertXMLtoDOM(xmlString);
console.log(xmlDOM.documentElement.nodeName); // Root element

Файл convert-object-to-dom-xml.js

// Create XML from Object

function createXML (oObjTree) {
  function loadObjTree (oParentEl, oParentObj) {
    var vValue, oChild;
    if (oParentObj.constructor === String || oParentObj.constructor === Number || oParentObj.constructor === Boolean) {
      oParentEl.appendChild(oNewDoc.createTextNode(oParentObj.toString())); /* verbosity level is 0 or 1 */
      if (oParentObj === oParentObj.valueOf()) { return; }
    } else if (oParentObj.constructor === Date) {
      oParentEl.appendChild(oNewDoc.createTextNode(oParentObj.toGMTString()));
    }
    for (var sName in oParentObj) {
      if (isFinite(sName)) { continue; } /* verbosity level is 0 */
      vValue = oParentObj[sName];
      if (sName === "keyValue") {
        if (vValue !== null && vValue !== true) { oParentEl.appendChild(oNewDoc.createTextNode(vValue.constructor === Date ? vValue.toGMTString() : String(vValue))); }
      } else if (sName === "keyAttributes") { /* verbosity level is 3 */
        for (var sAttrib in vValue) { oParentEl.setAttribute(sAttrib, vValue[sAttrib]); }
      } else if (sName.charAt(0) === "@") {
        oParentEl.setAttribute(sName.slice(1), vValue);
      } else if (vValue.constructor === Array) {
        for (var nItem = 0; nItem < vValue.length; nItem++) {
          oChild = oNewDoc.createElement(sName);
          loadObjTree(oChild, vValue[nItem]);
          oParentEl.appendChild(oChild);
        }
      } else {
        oChild = oNewDoc.createElement(sName);
        if (vValue instanceof Object) {
          loadObjTree(oChild, vValue);
        } else if (vValue !== null && vValue !== true) {
          oChild.appendChild(oNewDoc.createTextNode(vValue.toString()));
        }
        oParentEl.appendChild(oChild);
      }
    }
  }
  const oNewDoc = document.implementation.createDocument("", "", null);
  loadObjTree(oNewDoc, oObjTree);
  return oNewDoc;
}

var newDoc = createXML(myObject);
console.log((new XMLSerializer()).serializeToString(newDoc));

Файл convert-dom-to-xml.js

function convertDOMtoXML (domElement) {
    var serializer;
    if (window.XMLSerializer) {
        serializer = new XMLSerializer();
    } else {
        throw new Error('No XML serializer found.');
    }
    return serializer.serializeToString(domElement);
}

// Test

console.log(convertDOMtoXML(document));

var inputElement = document.createElement('input');
console.log(convertDOMtoXML(inputElement));

Файл convert-dom-xml-to-object.js

function convertDomXMLtoObject (domXMLelement) {
    var object = {};
    if (domXMLelement.nodeType === 1) { // element
        // Attributes
        if (domXMLelement.attributes.length > 0) {
            object['@attributes'] = {};
            var attribute;
            for (var i = 0, len = domXMLelement.attributes.length; i < len; i++) {
                attribute = domXMLelement.attributes.item(i);
                object['@attributes'][attribute.nodeName] = attribute.nodeValue; // пример: @attributes = {class: 'menu'}
            }
        }
    } else if (domXMLelement.nodeType === 3) { // text
        // Text
        object = domXMLelement.nodeValue;
    }
    // Children
    if (domXMLelement.hasChildNodes()) {
        var item
            , nodeName;
        for(var j = 0, len = domXMLelement.childNodes.length; j < len; j++) {
            item = domXMLelement.childNodes.item(j);
            nodeName = item.nodeName;
            if (typeof object[nodeName] === 'undefined') {
                object[nodeName] = convertDomXMLtoObject(item);
            } else {
                if (typeof object[nodeName].push === 'undefined') {
                    object[nodeName] = [];
                    var old = object[nodeName];
                    object[nodeName].push(old);
                }
                object[nodeName].push(convertDomXMLtoObject(item));
            }
        }
    }
    return object;
}

// Test

console.log(convertDomXMLtoObject(document));

Комментариев нет:

Отправить комментарий