xml-stream.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. const _ = require('./under-dash');
  2. const utils = require('./utils');
  3. // constants
  4. const OPEN_ANGLE = '<';
  5. const CLOSE_ANGLE = '>';
  6. const OPEN_ANGLE_SLASH = '</';
  7. const CLOSE_SLASH_ANGLE = '/>';
  8. const EQUALS_QUOTE = '="';
  9. const QUOTE = '"';
  10. const SPACE = ' ';
  11. function pushAttribute(xml, name, value) {
  12. xml.push(SPACE);
  13. xml.push(name);
  14. xml.push(EQUALS_QUOTE);
  15. xml.push(utils.xmlEncode(value.toString()));
  16. xml.push(QUOTE);
  17. }
  18. function pushAttributes(xml, attributes) {
  19. if (attributes) {
  20. _.each(attributes, (value, name) => {
  21. if (value !== undefined) {
  22. pushAttribute(xml, name, value);
  23. }
  24. });
  25. }
  26. }
  27. class XmlStream {
  28. constructor() {
  29. this._xml = [];
  30. this._stack = [];
  31. this._rollbacks = [];
  32. }
  33. get tos() {
  34. return this._stack.length ? this._stack[this._stack.length - 1] : undefined;
  35. }
  36. get cursor() {
  37. // handy way to track whether anything has been added
  38. return this._xml.length;
  39. }
  40. openXml(docAttributes) {
  41. const xml = this._xml;
  42. // <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
  43. xml.push('<?xml');
  44. pushAttributes(xml, docAttributes);
  45. xml.push('?>\n');
  46. }
  47. openNode(name, attributes) {
  48. const parent = this.tos;
  49. const xml = this._xml;
  50. if (parent && this.open) {
  51. xml.push(CLOSE_ANGLE);
  52. }
  53. this._stack.push(name);
  54. // start streaming node
  55. xml.push(OPEN_ANGLE);
  56. xml.push(name);
  57. pushAttributes(xml, attributes);
  58. this.leaf = true;
  59. this.open = true;
  60. }
  61. addAttribute(name, value) {
  62. if (!this.open) {
  63. throw new Error('Cannot write attributes to node if it is not open');
  64. }
  65. if (value !== undefined) {
  66. pushAttribute(this._xml, name, value);
  67. }
  68. }
  69. addAttributes(attrs) {
  70. if (!this.open) {
  71. throw new Error('Cannot write attributes to node if it is not open');
  72. }
  73. pushAttributes(this._xml, attrs);
  74. }
  75. writeText(text) {
  76. const xml = this._xml;
  77. if (this.open) {
  78. xml.push(CLOSE_ANGLE);
  79. this.open = false;
  80. }
  81. this.leaf = false;
  82. xml.push(utils.xmlEncode(text.toString()));
  83. }
  84. writeXml(xml) {
  85. if (this.open) {
  86. this._xml.push(CLOSE_ANGLE);
  87. this.open = false;
  88. }
  89. this.leaf = false;
  90. this._xml.push(xml);
  91. }
  92. closeNode() {
  93. const node = this._stack.pop();
  94. const xml = this._xml;
  95. if (this.leaf) {
  96. xml.push(CLOSE_SLASH_ANGLE);
  97. } else {
  98. xml.push(OPEN_ANGLE_SLASH);
  99. xml.push(node);
  100. xml.push(CLOSE_ANGLE);
  101. }
  102. this.open = false;
  103. this.leaf = false;
  104. }
  105. leafNode(name, attributes, text) {
  106. this.openNode(name, attributes);
  107. if (text !== undefined) {
  108. // zeros need to be written
  109. this.writeText(text);
  110. }
  111. this.closeNode();
  112. }
  113. closeAll() {
  114. while (this._stack.length) {
  115. this.closeNode();
  116. }
  117. }
  118. addRollback() {
  119. this._rollbacks.push({
  120. xml: this._xml.length,
  121. stack: this._stack.length,
  122. leaf: this.leaf,
  123. open: this.open,
  124. });
  125. return this.cursor;
  126. }
  127. commit() {
  128. this._rollbacks.pop();
  129. }
  130. rollback() {
  131. const r = this._rollbacks.pop();
  132. if (this._xml.length > r.xml) {
  133. this._xml.splice(r.xml, this._xml.length - r.xml);
  134. }
  135. if (this._stack.length > r.stack) {
  136. this._stack.splice(r.stack, this._stack.length - r.stack);
  137. }
  138. this.leaf = r.leaf;
  139. this.open = r.open;
  140. }
  141. get xml() {
  142. this.closeAll();
  143. return this._xml.join('');
  144. }
  145. }
  146. XmlStream.StdDocAttributes = {
  147. version: '1.0',
  148. encoding: 'UTF-8',
  149. standalone: 'yes',
  150. };
  151. module.exports = XmlStream;