dom-utils.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import { XMLElement } from '../models/evt-models';
  2. /**
  3. * Counter that takes into account the number of parsed elements with [xpath]{@link DOMUtilsService.html#xpath},
  4. * in order to allow the generation of unique ids when node path is not available.
  5. */
  6. let totIdsGenerated = 0;
  7. /**
  8. * Function to check if an element is nested into another particular element.
  9. * @param element The element to be checked
  10. * @param parentTagName TagName of the element that does not be a parent of the given element
  11. * @param attributes attributes
  12. *
  13. * @returns Whether the given element is nested in a node with given TagName or not
  14. */
  15. export function isNestedInElem(element, parentTagName: string, attributes?: Array<{ key: string, value }>): boolean {
  16. return !!element && isNodeNestedInElem(element, parentTagName, false, attributes);
  17. }
  18. /**
  19. * Function to check if an element is directly nested into another particular element.
  20. * @param element The element to be checked
  21. * @param parentTagName TagName of the element that does not be a parent of the given element
  22. * @param attributes attributes
  23. *
  24. * @returns Whether the given element is nested in a node with given TagName or not
  25. */
  26. export function isDirectlyNestedInElem(element, parentTagName: string, attributes?: Array<{ key: string, value }>): boolean {
  27. return isNodeNestedInElem(element, parentTagName, true, attributes);
  28. }
  29. /**
  30. * Function to check if an element is nested into another particular element.
  31. * @param element The element to be checked
  32. * @param parentTagName TagName of the element that does not be a parent of the given element
  33. * @param directCheck Whether to check only parentNode or analyize all ancestors
  34. * @param attributes attributes
  35. *
  36. * @returns Whether the given element is nested in a node with given TagName or not
  37. */
  38. export function isNodeNestedInElem(
  39. element,
  40. parentTagName: string,
  41. directCheck: boolean,
  42. attributes?: Array<{ key: string, value }>,
  43. ): boolean {
  44. if (element.parentNode !== null) {
  45. if (element.parentNode.tagName === 'text') {
  46. return false;
  47. }
  48. if (parentTagName === '' || element.parentNode.tagName === parentTagName || element.parentNode.nodeName === parentTagName) {
  49. if (!attributes || attributes.length === 0) {
  50. return true;
  51. }
  52. if (!element.parentNode.attributes || element.parentNode.attributes.length === 0) {
  53. return false;
  54. }
  55. let matchingAttr = 0;
  56. attributes.forEach(attr => {
  57. if (element.parentNode.attributes[attr.key] &&
  58. element.parentNode.attributes[attr.key].value === attr.value) {
  59. matchingAttr++;
  60. }
  61. });
  62. if (matchingAttr === attributes.length) {
  63. return true;
  64. }
  65. return directCheck ? false : isNestedInElem(element.parentNode, parentTagName, attributes);
  66. }
  67. return directCheck ? false : isNestedInElem(element.parentNode, parentTagName, attributes);
  68. }
  69. return false;
  70. }
  71. /**
  72. * This method will generate a string representing the xpath of the given element.
  73. * This string can be use as a unique identifier, since every element as a different xpath.
  74. * @param el XML element to analyze
  75. *
  76. * @returns calculated xpath of the given element
  77. */
  78. // tslint:disable-next-line: no-any
  79. export function xpath(el: any): string {
  80. try {
  81. if (typeof el === 'string') {
  82. // document.evaluate(xpathExpression, contextNode, namespaceResolver, resultType, result );
  83. return document.evaluate(el, document, undefined, 0, undefined).stringValue;
  84. }
  85. if (!el || el.nodeType !== 1) { return ''; }
  86. let sames = [];
  87. if (el.parentNode) {
  88. sames = [].filter.call(el.parentNode.children, (x) => {
  89. return x.tagName === el.tagName;
  90. });
  91. }
  92. let countIndex = sames.length > 1 ? ([].indexOf.call(sames, el) + 1) : 1;
  93. countIndex = `[${countIndex}]`;
  94. const tagName = el.tagName !== 'tei' ? '-' + el.tagName : '';
  95. return `${xpath(el.parentNode)}${tagName}${countIndex}`;
  96. } catch (e) {
  97. totIdsGenerated++; // TODO: remove side effects
  98. return `-id${totIdsGenerated}`;
  99. }
  100. }
  101. /**
  102. * This method will an excerpted or truncated XHTML string and returns a well-balanced XHTML string
  103. * - It checks for broken tags, e.g. <code>&lt;stro</code> [a <code>&lt;</code> after the last <code>&gt;</code> indicates a broken tag]
  104. * - It eventually truncates broken tags
  105. * - It checks for broken elements, e.g. <code>&lt;strong&gt;Hello, w</code>
  106. * - It gets an array of all tags (start, end, and self-closing)
  107. * - It prepares an empty array where to store broken tags (<code>stack</code>)
  108. * - It loops over all tags
  109. * - when it founds an end tag, it pops it off of the stack
  110. * - when it founds a start tag, it push it onto the stack
  111. * - then it founds a self-closing tag, it do nothing
  112. * - At the end of the loop, <code>stack</code> should contain only the start tags of the broken elements, most deeply-nested at the top
  113. * - It loops over stack array
  114. * - pops the unmatched tag off the stack
  115. * - gets just the tag name
  116. * - and appends the end tag
  117. *
  118. * @param XHTMLstring string to balanced
  119. *
  120. * @returns well-balanced XHTML string
  121. */
  122. export function balanceXHTML(XHTMLstring: string): string {
  123. // Check for broken tags, e.g. <stro
  124. // Check for a < after the last >, indicating a broken tag
  125. if (XHTMLstring) {
  126. if (XHTMLstring.lastIndexOf('<') > XHTMLstring.lastIndexOf('>')) {
  127. // Truncate broken tag
  128. XHTMLstring = XHTMLstring.substring(0, XHTMLstring.lastIndexOf('<'));
  129. }
  130. // Check for broken elements, e.g. <strong>Hello, w
  131. // Get an array of all tags (start, end, and self-closing)
  132. const tags = XHTMLstring.match(/<(?!\!)[^>]+>/g);
  133. const stack = [];
  134. const tagToOpen = [];
  135. for (const tag in tags) {
  136. if (tag.search('/') === 1) { // </tagName>
  137. // end tag -- pop off of the stack
  138. // If the last element of the stack is the corresponding of opening tag
  139. const tagName = tag.replace(/[<\/>]/ig, '');
  140. const openTag = stack[stack.length - 1];
  141. if (openTag && (openTag.search('<' + tagName + ' ') >= 0 || openTag.search('<' + tagName + '>') >= 0)) {
  142. stack.pop();
  143. } else { // Tag non aperto
  144. tagToOpen.push(tagName);
  145. }
  146. } else if (tag.search('/>') <= 0) { // <tagName>
  147. // start tag -- push onto the stack
  148. stack.push(tag);
  149. } else { // <tagName />
  150. // self-closing tag -- do nothing
  151. }
  152. }
  153. // stack should now contain only the start tags of the broken elements, most deeply-nested at the top
  154. while (stack.length > 0) {
  155. // pop the unmatched tag off the stack
  156. let endTag = stack.pop();
  157. // get just the tag name
  158. endTag = endTag.substring(1, endTag.search(/[ >]/));
  159. // append the end tag
  160. XHTMLstring += '</' + endTag + '>';
  161. }
  162. while (tagToOpen.length > 0) {
  163. const startTag = tagToOpen.shift();
  164. XHTMLstring = '<' + startTag + '>' + XHTMLstring;
  165. }
  166. }
  167. // Return the well-balanced XHTML string
  168. return (XHTMLstring ? XHTMLstring : '');
  169. }
  170. /**
  171. * Get all DOM elements contained between the node elements
  172. *
  173. * @param start starting node
  174. * @param end ending node
  175. *
  176. * @returns list of nodes contained between start node and end node
  177. */
  178. // tslint:disable-next-line: no-any
  179. export function getElementsBetweenTreeNode(start: any, end: any): XMLElement[] {
  180. const range = document.createRange();
  181. range.setStart(start, 0);
  182. range.setEnd(end, end.length || end.childNodes.length);
  183. const commonAncestorChild = Array.from((range.commonAncestorContainer as XMLElement).children);
  184. const startIdx = commonAncestorChild.indexOf(start);
  185. const endIdx = commonAncestorChild.indexOf(end);
  186. const rangeNodes = commonAncestorChild.slice(startIdx, endIdx).filter((c) => c !== start);
  187. rangeNodes.forEach((c: XMLElement) => c.setAttribute('xpath', xpath(c).replace(/-/g, '/')));
  188. const fragment = range.cloneContents();
  189. const nodes = Array.from(fragment.childNodes);
  190. return nodes as XMLElement[];
  191. }
  192. export function getOuterHTML(element): string {
  193. let outerHTML: string = element.outerHTML;
  194. outerHTML = outerHTML ? outerHTML.replace(/ xmlns="http:\/\/www\.tei-c\.org\/ns\/1\.0"/g, '') : outerHTML;
  195. return outerHTML;
  196. }
  197. export function getCommonAncestor(node1, node2) {
  198. const method = 'contains' in node1 ? 'contains' : 'compareDocumentPosition';
  199. const test = method === 'contains' ? 1 : 0x10;
  200. node1 = node1.parentNode;
  201. while (node1) {
  202. // tslint:disable-next-line:no-bitwise
  203. if ((node1[method](node2) & test) === test) {
  204. return node1;
  205. }
  206. node1 = node1.parentNode;
  207. }
  208. return undefined;
  209. }
  210. export function createNsResolver(doc: Document) {
  211. return (prefix: string) => prefix === 'ns' ? doc.documentElement.namespaceURI : undefined;
  212. }