Source: util/format_xml.es.js

  1. /**
  2. * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
  3. *
  4. * This library is free software; you can redistribute it and/or modify it under
  5. * the terms of the GNU Lesser General Public License as published by the Free
  6. * Software Foundation; either version 2.1 of the License, or (at your option)
  7. * any later version.
  8. *
  9. * This library is distributed in the hope that it will be useful, but WITHOUT
  10. * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  11. * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  12. * details.
  13. */
  14. const NEW_LINE = '\r\n';
  15. const REGEX_CDATA = /<!\[CDATA\[.*?\]\]>/gs;
  16. const REGEX_DECLARATIVE_CLOSE = /-->|\]>/;
  17. const REGEX_DECLARATIVE_OPEN = /<!/;
  18. const REGEX_DIRECTIVE = /<\?/;
  19. const REGEX_DOCTYPE = /!DOCTYPE/;
  20. const REGEX_ELEMENT = /^<\w/;
  21. const REGEX_ELEMENT_CLOSE = /^<\/\w/;
  22. const REGEX_ELEMENT_NAMESPACED = /^<[\w:\-.,]+/;
  23. const REGEX_ELEMENT_NAMESPACED_CLOSE = /^<\/[\w:\-.,]+/;
  24. const REGEX_ELEMENT_OPEN = /<\w/;
  25. const REGEX_NAMESPACE_XML = /xmlns(?::|=)/g;
  26. const REGEX_NAMESPACE_XML_ATTR = /\s*(xmlns)(:|=)/g;
  27. const REGEX_TAG_CLOSE = /<\//;
  28. const REGEX_TAG_OPEN = /</g;
  29. const REGEX_TAG_SINGLE_CLOSE = /\/>/;
  30. const REGEX_WHITESPACE_BETWEEN_TAGS = />\s+</g;
  31. const STR_BLANK = '';
  32. const STR_TOKEN = '~::~';
  33. const STR_TOKEN_CDATA = '<' + STR_TOKEN + 'CDATA' + STR_TOKEN + '>';
  34. const REGEX_TOKEN_CDATA = new RegExp(STR_TOKEN_CDATA, 'g');
  35. const TAG_INDENT = '\t';
  36. const DEFAULT_OPTIONS = {
  37. newLine: NEW_LINE,
  38. tagIndent: TAG_INDENT,
  39. };
  40. /**
  41. * Returns a formatted XML
  42. * @param {!String} content String to format
  43. * @param {Object} options Optional parameter that can accept provided options
  44. * @return {!String} Formatted content
  45. */
  46. export default function formatXML(content, options = {}) {
  47. const {newLine, tagIndent} = {
  48. ...DEFAULT_OPTIONS,
  49. ...options,
  50. };
  51. if (typeof content !== 'string') {
  52. throw new TypeError('Parameter content must be a string');
  53. }
  54. const cdata = [];
  55. content = content.trim();
  56. content = content.replace(REGEX_CDATA, (match) => {
  57. cdata.push(match);
  58. return STR_TOKEN_CDATA;
  59. });
  60. content = content.replace(REGEX_WHITESPACE_BETWEEN_TAGS, '><');
  61. content = content.replace(REGEX_TAG_OPEN, STR_TOKEN + '<');
  62. content = content.replace(REGEX_NAMESPACE_XML_ATTR, STR_TOKEN + '$1$2');
  63. content = content.replace(REGEX_TOKEN_CDATA, () => cdata.shift());
  64. let commentCounter = 0;
  65. let inComment = false;
  66. const items = content.split(STR_TOKEN);
  67. let level = 0;
  68. let result = '';
  69. items.forEach((item, index) => {
  70. if (REGEX_CDATA.test(item)) {
  71. result += indent(level, newLine, tagIndent) + item;
  72. }
  73. else if (REGEX_DECLARATIVE_OPEN.test(item)) {
  74. result += indent(level, newLine, tagIndent) + item;
  75. commentCounter++;
  76. inComment = true;
  77. if (
  78. REGEX_DECLARATIVE_CLOSE.test(item) ||
  79. REGEX_DOCTYPE.test(item)
  80. ) {
  81. commentCounter--;
  82. inComment = commentCounter !== 0;
  83. }
  84. }
  85. else if (REGEX_DECLARATIVE_CLOSE.test(item)) {
  86. result += item;
  87. commentCounter--;
  88. inComment = commentCounter !== 0;
  89. }
  90. else if (
  91. REGEX_ELEMENT.exec(items[index - 1]) &&
  92. REGEX_ELEMENT_CLOSE.exec(item) &&
  93. REGEX_ELEMENT_NAMESPACED.exec(items[index - 1]) ==
  94. REGEX_ELEMENT_NAMESPACED_CLOSE.exec(item)[0].replace(
  95. '/',
  96. STR_BLANK
  97. )
  98. ) {
  99. result += item;
  100. if (!inComment) {
  101. --level;
  102. }
  103. }
  104. else if (
  105. REGEX_ELEMENT_OPEN.test(item) &&
  106. !REGEX_TAG_CLOSE.test(item) &&
  107. !REGEX_TAG_SINGLE_CLOSE.test(item)
  108. ) {
  109. if (inComment) {
  110. result += item;
  111. }
  112. else {
  113. result += indent(level++, newLine, tagIndent) + item;
  114. }
  115. }
  116. else if (
  117. REGEX_ELEMENT_OPEN.test(item) &&
  118. REGEX_TAG_CLOSE.test(item)
  119. ) {
  120. if (inComment) {
  121. result += item;
  122. }
  123. else {
  124. result += indent(level, newLine, tagIndent) + item;
  125. }
  126. }
  127. else if (REGEX_TAG_CLOSE.test(item)) {
  128. if (inComment) {
  129. result += item;
  130. }
  131. else {
  132. result += indent(--level, newLine, tagIndent) + item;
  133. }
  134. }
  135. else if (REGEX_TAG_SINGLE_CLOSE.test(item)) {
  136. if (inComment) {
  137. result += item;
  138. }
  139. else {
  140. result += indent(level, newLine, tagIndent) + item;
  141. }
  142. }
  143. else if (REGEX_DIRECTIVE.test(item)) {
  144. result += indent(level, newLine, tagIndent) + item;
  145. }
  146. else if (REGEX_NAMESPACE_XML) {
  147. result += indent(level, newLine, tagIndent) + item;
  148. }
  149. else {
  150. result += item;
  151. }
  152. if (new RegExp('^' + newLine).test(result)) {
  153. result = result.slice(newLine.length);
  154. }
  155. });
  156. return result;
  157. }
  158. /**
  159. * Returns a string for starting a new line at the specified indent level
  160. * @param {number} level The level of indentation
  161. * @return {String} Return a string for starting a new line at the specified indent level
  162. */
  163. function indent(level, newLine, tagIndent) {
  164. return newLine + new Array(level + 1).join(tagIndent);
  165. }