| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- var helper = require('./options-helper');
- var isArray = require('./array-helper').isArray;
-
- var currentElement, currentElementName;
-
- function validateOptions(userOptions) {
- var options = helper.copyOptions(userOptions);
- helper.ensureFlagExists('ignoreDeclaration', options);
- helper.ensureFlagExists('ignoreInstruction', options);
- helper.ensureFlagExists('ignoreAttributes', options);
- helper.ensureFlagExists('ignoreText', options);
- helper.ensureFlagExists('ignoreComment', options);
- helper.ensureFlagExists('ignoreCdata', options);
- helper.ensureFlagExists('ignoreDoctype', options);
- helper.ensureFlagExists('compact', options);
- helper.ensureFlagExists('indentText', options);
- helper.ensureFlagExists('indentCdata', options);
- helper.ensureFlagExists('indentAttributes', options);
- helper.ensureFlagExists('indentInstruction', options);
- helper.ensureFlagExists('fullTagEmptyElement', options);
- helper.ensureFlagExists('noQuotesForNativeAttributes', options);
- helper.ensureSpacesExists(options);
- if (typeof options.spaces === 'number') {
- options.spaces = Array(options.spaces + 1).join(' ');
- }
- helper.ensureKeyExists('declaration', options);
- helper.ensureKeyExists('instruction', options);
- helper.ensureKeyExists('attributes', options);
- helper.ensureKeyExists('text', options);
- helper.ensureKeyExists('comment', options);
- helper.ensureKeyExists('cdata', options);
- helper.ensureKeyExists('doctype', options);
- helper.ensureKeyExists('type', options);
- helper.ensureKeyExists('name', options);
- helper.ensureKeyExists('elements', options);
- helper.checkFnExists('doctype', options);
- helper.checkFnExists('instruction', options);
- helper.checkFnExists('cdata', options);
- helper.checkFnExists('comment', options);
- helper.checkFnExists('text', options);
- helper.checkFnExists('instructionName', options);
- helper.checkFnExists('elementName', options);
- helper.checkFnExists('attributeName', options);
- helper.checkFnExists('attributeValue', options);
- helper.checkFnExists('attributes', options);
- helper.checkFnExists('fullTagEmptyElement', options);
- return options;
- }
-
- function writeIndentation(options, depth, firstLine) {
- return (!firstLine && options.spaces ? '\n' : '') + Array(depth + 1).join(options.spaces);
- }
-
- function writeAttributes(attributes, options, depth) {
- if (options.ignoreAttributes) {
- return '';
- }
- if ('attributesFn' in options) {
- attributes = options.attributesFn(attributes, currentElementName, currentElement);
- }
- var key, attr, attrName, quote, result = [];
- for (key in attributes) {
- if (attributes.hasOwnProperty(key) && attributes[key] !== null && attributes[key] !== undefined) {
- quote = options.noQuotesForNativeAttributes && typeof attributes[key] !== 'string' ? '' : '"';
- attr = '' + attributes[key]; // ensure number and boolean are converted to String
- attr = attr.replace(/"/g, '"');
- attrName = 'attributeNameFn' in options ? options.attributeNameFn(key, attr, currentElementName, currentElement) : key;
- result.push((options.spaces && options.indentAttributes? writeIndentation(options, depth+1, false) : ' '));
- result.push(attrName + '=' + quote + ('attributeValueFn' in options ? options.attributeValueFn(attr, key, currentElementName, currentElement) : attr) + quote);
- }
- }
- if (attributes && Object.keys(attributes).length && options.spaces && options.indentAttributes) {
- result.push(writeIndentation(options, depth, false));
- }
- return result.join('');
- }
-
- function writeDeclaration(declaration, options, depth) {
- currentElement = declaration;
- currentElementName = 'xml';
- return options.ignoreDeclaration ? '' : '<?' + 'xml' + writeAttributes(declaration[options.attributesKey], options, depth) + '?>';
- }
-
- function writeInstruction(instruction, options, depth) {
- if (options.ignoreInstruction) {
- return '';
- }
- var key;
- for (key in instruction) {
- if (instruction.hasOwnProperty(key)) {
- break;
- }
- }
- var instructionName = 'instructionNameFn' in options ? options.instructionNameFn(key, instruction[key], currentElementName, currentElement) : key;
- if (typeof instruction[key] === 'object') {
- currentElement = instruction;
- currentElementName = instructionName;
- return '<?' + instructionName + writeAttributes(instruction[key][options.attributesKey], options, depth) + '?>';
- } else {
- var instructionValue = instruction[key] ? instruction[key] : '';
- if ('instructionFn' in options) instructionValue = options.instructionFn(instructionValue, key, currentElementName, currentElement);
- return '<?' + instructionName + (instructionValue ? ' ' + instructionValue : '') + '?>';
- }
- }
-
- function writeComment(comment, options) {
- return options.ignoreComment ? '' : '<!--' + ('commentFn' in options ? options.commentFn(comment, currentElementName, currentElement) : comment) + '-->';
- }
-
- function writeCdata(cdata, options) {
- return options.ignoreCdata ? '' : '<![CDATA[' + ('cdataFn' in options ? options.cdataFn(cdata, currentElementName, currentElement) : cdata.replace(']]>', ']]]]><![CDATA[>')) + ']]>';
- }
-
- function writeDoctype(doctype, options) {
- return options.ignoreDoctype ? '' : '<!DOCTYPE ' + ('doctypeFn' in options ? options.doctypeFn(doctype, currentElementName, currentElement) : doctype) + '>';
- }
-
- function writeText(text, options) {
- if (options.ignoreText) return '';
- text = '' + text; // ensure Number and Boolean are converted to String
- text = text.replace(/&/g, '&'); // desanitize to avoid double sanitization
- text = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
- return 'textFn' in options ? options.textFn(text, currentElementName, currentElement) : text;
- }
-
- function hasContent(element, options) {
- var i;
- if (element.elements && element.elements.length) {
- for (i = 0; i < element.elements.length; ++i) {
- switch (element.elements[i][options.typeKey]) {
- case 'text':
- if (options.indentText) {
- return true;
- }
- break; // skip to next key
- case 'cdata':
- if (options.indentCdata) {
- return true;
- }
- break; // skip to next key
- case 'instruction':
- if (options.indentInstruction) {
- return true;
- }
- break; // skip to next key
- case 'doctype':
- case 'comment':
- case 'element':
- return true;
- default:
- return true;
- }
- }
- }
- return false;
- }
-
- function writeElement(element, options, depth) {
- currentElement = element;
- currentElementName = element.name;
- var xml = [], elementName = 'elementNameFn' in options ? options.elementNameFn(element.name, element) : element.name;
- xml.push('<' + elementName);
- if (element[options.attributesKey]) {
- xml.push(writeAttributes(element[options.attributesKey], options, depth));
- }
- var withClosingTag = element[options.elementsKey] && element[options.elementsKey].length || element[options.attributesKey] && element[options.attributesKey]['xml:space'] === 'preserve';
- if (!withClosingTag) {
- if ('fullTagEmptyElementFn' in options) {
- withClosingTag = options.fullTagEmptyElementFn(element.name, element);
- } else {
- withClosingTag = options.fullTagEmptyElement;
- }
- }
- if (withClosingTag) {
- xml.push('>');
- if (element[options.elementsKey] && element[options.elementsKey].length) {
- xml.push(writeElements(element[options.elementsKey], options, depth + 1));
- currentElement = element;
- currentElementName = element.name;
- }
- xml.push(options.spaces && hasContent(element, options) ? '\n' + Array(depth + 1).join(options.spaces) : '');
- xml.push('</' + elementName + '>');
- } else {
- xml.push('/>');
- }
- return xml.join('');
- }
-
- function writeElements(elements, options, depth, firstLine) {
- return elements.reduce(function (xml, element) {
- var indent = writeIndentation(options, depth, firstLine && !xml);
- switch (element.type) {
- case 'element': return xml + indent + writeElement(element, options, depth);
- case 'comment': return xml + indent + writeComment(element[options.commentKey], options);
- case 'doctype': return xml + indent + writeDoctype(element[options.doctypeKey], options);
- case 'cdata': return xml + (options.indentCdata ? indent : '') + writeCdata(element[options.cdataKey], options);
- case 'text': return xml + (options.indentText ? indent : '') + writeText(element[options.textKey], options);
- case 'instruction':
- var instruction = {};
- instruction[element[options.nameKey]] = element[options.attributesKey] ? element : element[options.instructionKey];
- return xml + (options.indentInstruction ? indent : '') + writeInstruction(instruction, options, depth);
- }
- }, '');
- }
-
- function hasContentCompact(element, options, anyContent) {
- var key;
- for (key in element) {
- if (element.hasOwnProperty(key)) {
- switch (key) {
- case options.parentKey:
- case options.attributesKey:
- break; // skip to next key
- case options.textKey:
- if (options.indentText || anyContent) {
- return true;
- }
- break; // skip to next key
- case options.cdataKey:
- if (options.indentCdata || anyContent) {
- return true;
- }
- break; // skip to next key
- case options.instructionKey:
- if (options.indentInstruction || anyContent) {
- return true;
- }
- break; // skip to next key
- case options.doctypeKey:
- case options.commentKey:
- return true;
- default:
- return true;
- }
- }
- }
- return false;
- }
-
- function writeElementCompact(element, name, options, depth, indent) {
- currentElement = element;
- currentElementName = name;
- var elementName = 'elementNameFn' in options ? options.elementNameFn(name, element) : name;
- if (typeof element === 'undefined' || element === null || element === '') {
- return 'fullTagEmptyElementFn' in options && options.fullTagEmptyElementFn(name, element) || options.fullTagEmptyElement ? '<' + elementName + '></' + elementName + '>' : '<' + elementName + '/>';
- }
- var xml = [];
- if (name) {
- xml.push('<' + elementName);
- if (typeof element !== 'object') {
- xml.push('>' + writeText(element,options) + '</' + elementName + '>');
- return xml.join('');
- }
- if (element[options.attributesKey]) {
- xml.push(writeAttributes(element[options.attributesKey], options, depth));
- }
- var withClosingTag = hasContentCompact(element, options, true) || element[options.attributesKey] && element[options.attributesKey]['xml:space'] === 'preserve';
- if (!withClosingTag) {
- if ('fullTagEmptyElementFn' in options) {
- withClosingTag = options.fullTagEmptyElementFn(name, element);
- } else {
- withClosingTag = options.fullTagEmptyElement;
- }
- }
- if (withClosingTag) {
- xml.push('>');
- } else {
- xml.push('/>');
- return xml.join('');
- }
- }
- xml.push(writeElementsCompact(element, options, depth + 1, false));
- currentElement = element;
- currentElementName = name;
- if (name) {
- xml.push((indent ? writeIndentation(options, depth, false) : '') + '</' + elementName + '>');
- }
- return xml.join('');
- }
-
- function writeElementsCompact(element, options, depth, firstLine) {
- var i, key, nodes, xml = [];
- for (key in element) {
- if (element.hasOwnProperty(key)) {
- nodes = isArray(element[key]) ? element[key] : [element[key]];
- for (i = 0; i < nodes.length; ++i) {
- switch (key) {
- case options.declarationKey: xml.push(writeDeclaration(nodes[i], options, depth)); break;
- case options.instructionKey: xml.push((options.indentInstruction ? writeIndentation(options, depth, firstLine) : '') + writeInstruction(nodes[i], options, depth)); break;
- case options.attributesKey: case options.parentKey: break; // skip
- case options.textKey: xml.push((options.indentText ? writeIndentation(options, depth, firstLine) : '') + writeText(nodes[i], options)); break;
- case options.cdataKey: xml.push((options.indentCdata ? writeIndentation(options, depth, firstLine) : '') + writeCdata(nodes[i], options)); break;
- case options.doctypeKey: xml.push(writeIndentation(options, depth, firstLine) + writeDoctype(nodes[i], options)); break;
- case options.commentKey: xml.push(writeIndentation(options, depth, firstLine) + writeComment(nodes[i], options)); break;
- default: xml.push(writeIndentation(options, depth, firstLine) + writeElementCompact(nodes[i], key, options, depth, hasContentCompact(nodes[i], options)));
- }
- firstLine = firstLine && !xml.length;
- }
- }
- }
- return xml.join('');
- }
-
- module.exports = function (js, options) {
- options = validateOptions(options);
- var xml = [];
- currentElement = js;
- currentElementName = '_root_';
- if (options.compact) {
- xml.push(writeElementsCompact(js, options, 0, true));
- } else {
- if (js[options.declarationKey]) {
- xml.push(writeDeclaration(js[options.declarationKey], options, 0));
- }
- if (js[options.elementsKey] && js[options.elementsKey].length) {
- xml.push(writeElements(js[options.elementsKey], options, 0, !xml.length));
- }
- }
- return xml.join('');
- };
|