电速宝智配引擎
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. var helper = require('./options-helper');
  2. var isArray = require('./array-helper').isArray;
  3. var currentElement, currentElementName;
  4. function validateOptions(userOptions) {
  5. var options = helper.copyOptions(userOptions);
  6. helper.ensureFlagExists('ignoreDeclaration', options);
  7. helper.ensureFlagExists('ignoreInstruction', options);
  8. helper.ensureFlagExists('ignoreAttributes', options);
  9. helper.ensureFlagExists('ignoreText', options);
  10. helper.ensureFlagExists('ignoreComment', options);
  11. helper.ensureFlagExists('ignoreCdata', options);
  12. helper.ensureFlagExists('ignoreDoctype', options);
  13. helper.ensureFlagExists('compact', options);
  14. helper.ensureFlagExists('indentText', options);
  15. helper.ensureFlagExists('indentCdata', options);
  16. helper.ensureFlagExists('indentAttributes', options);
  17. helper.ensureFlagExists('indentInstruction', options);
  18. helper.ensureFlagExists('fullTagEmptyElement', options);
  19. helper.ensureFlagExists('noQuotesForNativeAttributes', options);
  20. helper.ensureSpacesExists(options);
  21. if (typeof options.spaces === 'number') {
  22. options.spaces = Array(options.spaces + 1).join(' ');
  23. }
  24. helper.ensureKeyExists('declaration', options);
  25. helper.ensureKeyExists('instruction', options);
  26. helper.ensureKeyExists('attributes', options);
  27. helper.ensureKeyExists('text', options);
  28. helper.ensureKeyExists('comment', options);
  29. helper.ensureKeyExists('cdata', options);
  30. helper.ensureKeyExists('doctype', options);
  31. helper.ensureKeyExists('type', options);
  32. helper.ensureKeyExists('name', options);
  33. helper.ensureKeyExists('elements', options);
  34. helper.checkFnExists('doctype', options);
  35. helper.checkFnExists('instruction', options);
  36. helper.checkFnExists('cdata', options);
  37. helper.checkFnExists('comment', options);
  38. helper.checkFnExists('text', options);
  39. helper.checkFnExists('instructionName', options);
  40. helper.checkFnExists('elementName', options);
  41. helper.checkFnExists('attributeName', options);
  42. helper.checkFnExists('attributeValue', options);
  43. helper.checkFnExists('attributes', options);
  44. helper.checkFnExists('fullTagEmptyElement', options);
  45. return options;
  46. }
  47. function writeIndentation(options, depth, firstLine) {
  48. return (!firstLine && options.spaces ? '\n' : '') + Array(depth + 1).join(options.spaces);
  49. }
  50. function writeAttributes(attributes, options, depth) {
  51. if (options.ignoreAttributes) {
  52. return '';
  53. }
  54. if ('attributesFn' in options) {
  55. attributes = options.attributesFn(attributes, currentElementName, currentElement);
  56. }
  57. var key, attr, attrName, quote, result = [];
  58. for (key in attributes) {
  59. if (attributes.hasOwnProperty(key) && attributes[key] !== null && attributes[key] !== undefined) {
  60. quote = options.noQuotesForNativeAttributes && typeof attributes[key] !== 'string' ? '' : '"';
  61. attr = '' + attributes[key]; // ensure number and boolean are converted to String
  62. attr = attr.replace(/"/g, '"');
  63. attrName = 'attributeNameFn' in options ? options.attributeNameFn(key, attr, currentElementName, currentElement) : key;
  64. result.push((options.spaces && options.indentAttributes? writeIndentation(options, depth+1, false) : ' '));
  65. result.push(attrName + '=' + quote + ('attributeValueFn' in options ? options.attributeValueFn(attr, key, currentElementName, currentElement) : attr) + quote);
  66. }
  67. }
  68. if (attributes && Object.keys(attributes).length && options.spaces && options.indentAttributes) {
  69. result.push(writeIndentation(options, depth, false));
  70. }
  71. return result.join('');
  72. }
  73. function writeDeclaration(declaration, options, depth) {
  74. currentElement = declaration;
  75. currentElementName = 'xml';
  76. return options.ignoreDeclaration ? '' : '<?' + 'xml' + writeAttributes(declaration[options.attributesKey], options, depth) + '?>';
  77. }
  78. function writeInstruction(instruction, options, depth) {
  79. if (options.ignoreInstruction) {
  80. return '';
  81. }
  82. var key;
  83. for (key in instruction) {
  84. if (instruction.hasOwnProperty(key)) {
  85. break;
  86. }
  87. }
  88. var instructionName = 'instructionNameFn' in options ? options.instructionNameFn(key, instruction[key], currentElementName, currentElement) : key;
  89. if (typeof instruction[key] === 'object') {
  90. currentElement = instruction;
  91. currentElementName = instructionName;
  92. return '<?' + instructionName + writeAttributes(instruction[key][options.attributesKey], options, depth) + '?>';
  93. } else {
  94. var instructionValue = instruction[key] ? instruction[key] : '';
  95. if ('instructionFn' in options) instructionValue = options.instructionFn(instructionValue, key, currentElementName, currentElement);
  96. return '<?' + instructionName + (instructionValue ? ' ' + instructionValue : '') + '?>';
  97. }
  98. }
  99. function writeComment(comment, options) {
  100. return options.ignoreComment ? '' : '<!--' + ('commentFn' in options ? options.commentFn(comment, currentElementName, currentElement) : comment) + '-->';
  101. }
  102. function writeCdata(cdata, options) {
  103. return options.ignoreCdata ? '' : '<![CDATA[' + ('cdataFn' in options ? options.cdataFn(cdata, currentElementName, currentElement) : cdata.replace(']]>', ']]]]><![CDATA[>')) + ']]>';
  104. }
  105. function writeDoctype(doctype, options) {
  106. return options.ignoreDoctype ? '' : '<!DOCTYPE ' + ('doctypeFn' in options ? options.doctypeFn(doctype, currentElementName, currentElement) : doctype) + '>';
  107. }
  108. function writeText(text, options) {
  109. if (options.ignoreText) return '';
  110. text = '' + text; // ensure Number and Boolean are converted to String
  111. text = text.replace(/&amp;/g, '&'); // desanitize to avoid double sanitization
  112. text = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
  113. return 'textFn' in options ? options.textFn(text, currentElementName, currentElement) : text;
  114. }
  115. function hasContent(element, options) {
  116. var i;
  117. if (element.elements && element.elements.length) {
  118. for (i = 0; i < element.elements.length; ++i) {
  119. switch (element.elements[i][options.typeKey]) {
  120. case 'text':
  121. if (options.indentText) {
  122. return true;
  123. }
  124. break; // skip to next key
  125. case 'cdata':
  126. if (options.indentCdata) {
  127. return true;
  128. }
  129. break; // skip to next key
  130. case 'instruction':
  131. if (options.indentInstruction) {
  132. return true;
  133. }
  134. break; // skip to next key
  135. case 'doctype':
  136. case 'comment':
  137. case 'element':
  138. return true;
  139. default:
  140. return true;
  141. }
  142. }
  143. }
  144. return false;
  145. }
  146. function writeElement(element, options, depth) {
  147. currentElement = element;
  148. currentElementName = element.name;
  149. var xml = [], elementName = 'elementNameFn' in options ? options.elementNameFn(element.name, element) : element.name;
  150. xml.push('<' + elementName);
  151. if (element[options.attributesKey]) {
  152. xml.push(writeAttributes(element[options.attributesKey], options, depth));
  153. }
  154. var withClosingTag = element[options.elementsKey] && element[options.elementsKey].length || element[options.attributesKey] && element[options.attributesKey]['xml:space'] === 'preserve';
  155. if (!withClosingTag) {
  156. if ('fullTagEmptyElementFn' in options) {
  157. withClosingTag = options.fullTagEmptyElementFn(element.name, element);
  158. } else {
  159. withClosingTag = options.fullTagEmptyElement;
  160. }
  161. }
  162. if (withClosingTag) {
  163. xml.push('>');
  164. if (element[options.elementsKey] && element[options.elementsKey].length) {
  165. xml.push(writeElements(element[options.elementsKey], options, depth + 1));
  166. currentElement = element;
  167. currentElementName = element.name;
  168. }
  169. xml.push(options.spaces && hasContent(element, options) ? '\n' + Array(depth + 1).join(options.spaces) : '');
  170. xml.push('</' + elementName + '>');
  171. } else {
  172. xml.push('/>');
  173. }
  174. return xml.join('');
  175. }
  176. function writeElements(elements, options, depth, firstLine) {
  177. return elements.reduce(function (xml, element) {
  178. var indent = writeIndentation(options, depth, firstLine && !xml);
  179. switch (element.type) {
  180. case 'element': return xml + indent + writeElement(element, options, depth);
  181. case 'comment': return xml + indent + writeComment(element[options.commentKey], options);
  182. case 'doctype': return xml + indent + writeDoctype(element[options.doctypeKey], options);
  183. case 'cdata': return xml + (options.indentCdata ? indent : '') + writeCdata(element[options.cdataKey], options);
  184. case 'text': return xml + (options.indentText ? indent : '') + writeText(element[options.textKey], options);
  185. case 'instruction':
  186. var instruction = {};
  187. instruction[element[options.nameKey]] = element[options.attributesKey] ? element : element[options.instructionKey];
  188. return xml + (options.indentInstruction ? indent : '') + writeInstruction(instruction, options, depth);
  189. }
  190. }, '');
  191. }
  192. function hasContentCompact(element, options, anyContent) {
  193. var key;
  194. for (key in element) {
  195. if (element.hasOwnProperty(key)) {
  196. switch (key) {
  197. case options.parentKey:
  198. case options.attributesKey:
  199. break; // skip to next key
  200. case options.textKey:
  201. if (options.indentText || anyContent) {
  202. return true;
  203. }
  204. break; // skip to next key
  205. case options.cdataKey:
  206. if (options.indentCdata || anyContent) {
  207. return true;
  208. }
  209. break; // skip to next key
  210. case options.instructionKey:
  211. if (options.indentInstruction || anyContent) {
  212. return true;
  213. }
  214. break; // skip to next key
  215. case options.doctypeKey:
  216. case options.commentKey:
  217. return true;
  218. default:
  219. return true;
  220. }
  221. }
  222. }
  223. return false;
  224. }
  225. function writeElementCompact(element, name, options, depth, indent) {
  226. currentElement = element;
  227. currentElementName = name;
  228. var elementName = 'elementNameFn' in options ? options.elementNameFn(name, element) : name;
  229. if (typeof element === 'undefined' || element === null || element === '') {
  230. return 'fullTagEmptyElementFn' in options && options.fullTagEmptyElementFn(name, element) || options.fullTagEmptyElement ? '<' + elementName + '></' + elementName + '>' : '<' + elementName + '/>';
  231. }
  232. var xml = [];
  233. if (name) {
  234. xml.push('<' + elementName);
  235. if (typeof element !== 'object') {
  236. xml.push('>' + writeText(element,options) + '</' + elementName + '>');
  237. return xml.join('');
  238. }
  239. if (element[options.attributesKey]) {
  240. xml.push(writeAttributes(element[options.attributesKey], options, depth));
  241. }
  242. var withClosingTag = hasContentCompact(element, options, true) || element[options.attributesKey] && element[options.attributesKey]['xml:space'] === 'preserve';
  243. if (!withClosingTag) {
  244. if ('fullTagEmptyElementFn' in options) {
  245. withClosingTag = options.fullTagEmptyElementFn(name, element);
  246. } else {
  247. withClosingTag = options.fullTagEmptyElement;
  248. }
  249. }
  250. if (withClosingTag) {
  251. xml.push('>');
  252. } else {
  253. xml.push('/>');
  254. return xml.join('');
  255. }
  256. }
  257. xml.push(writeElementsCompact(element, options, depth + 1, false));
  258. currentElement = element;
  259. currentElementName = name;
  260. if (name) {
  261. xml.push((indent ? writeIndentation(options, depth, false) : '') + '</' + elementName + '>');
  262. }
  263. return xml.join('');
  264. }
  265. function writeElementsCompact(element, options, depth, firstLine) {
  266. var i, key, nodes, xml = [];
  267. for (key in element) {
  268. if (element.hasOwnProperty(key)) {
  269. nodes = isArray(element[key]) ? element[key] : [element[key]];
  270. for (i = 0; i < nodes.length; ++i) {
  271. switch (key) {
  272. case options.declarationKey: xml.push(writeDeclaration(nodes[i], options, depth)); break;
  273. case options.instructionKey: xml.push((options.indentInstruction ? writeIndentation(options, depth, firstLine) : '') + writeInstruction(nodes[i], options, depth)); break;
  274. case options.attributesKey: case options.parentKey: break; // skip
  275. case options.textKey: xml.push((options.indentText ? writeIndentation(options, depth, firstLine) : '') + writeText(nodes[i], options)); break;
  276. case options.cdataKey: xml.push((options.indentCdata ? writeIndentation(options, depth, firstLine) : '') + writeCdata(nodes[i], options)); break;
  277. case options.doctypeKey: xml.push(writeIndentation(options, depth, firstLine) + writeDoctype(nodes[i], options)); break;
  278. case options.commentKey: xml.push(writeIndentation(options, depth, firstLine) + writeComment(nodes[i], options)); break;
  279. default: xml.push(writeIndentation(options, depth, firstLine) + writeElementCompact(nodes[i], key, options, depth, hasContentCompact(nodes[i], options)));
  280. }
  281. firstLine = firstLine && !xml.length;
  282. }
  283. }
  284. }
  285. return xml.join('');
  286. }
  287. module.exports = function (js, options) {
  288. options = validateOptions(options);
  289. var xml = [];
  290. currentElement = js;
  291. currentElementName = '_root_';
  292. if (options.compact) {
  293. xml.push(writeElementsCompact(js, options, 0, true));
  294. } else {
  295. if (js[options.declarationKey]) {
  296. xml.push(writeDeclaration(js[options.declarationKey], options, 0));
  297. }
  298. if (js[options.elementsKey] && js[options.elementsKey].length) {
  299. xml.push(writeElements(js[options.elementsKey], options, 0, !xml.length));
  300. }
  301. }
  302. return xml.join('');
  303. };