Welcome to mirror list, hosted at ThFree Co, Russian Federation.

clean_html_element_serializer.js « __helpers__ « frontend « spec - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: d787f5126ec54b62b528ebc1b7de063f1d70555c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// slot-scope attribute is a result of Vue.js 3 stubs being serialized in slot context, drop it
// modelModifiers are result of Vue.js 3 model modifiers handling and should not be in snapshot
const ATTRIBUTES_TO_REMOVE = ['slot-scope', 'modelmodifiers'];
// Taken from https://github.com/vuejs/vue/blob/72aed6a149b94b5b929fb47370a7a6d4cb7491c5/src/platforms/web/util/attrs.ts#L37-L44
const BOOLEAN_ATTRIBUTES = new Set(
  (
    'allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,' +
    'default,defaultchecked,defaultmuted,defaultselected,defer,disabled,' +
    'enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,' +
    'muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,' +
    'required,reversed,scoped,seamless,selected,sortable,' +
    'truespeed,typemustmatch,visible'
  ).split(','),
);

function sortClassesAlphabetically(node) {
  // Make classes render in alphabetical order for both Vue2 and Vue3
  if (node.hasAttribute('class')) {
    const classes = node.getAttribute('class');
    if (classes === '') {
      node.removeAttribute('class');
    } else {
      node.setAttribute('class', Array.from(node.classList).sort().join(' '));
    }
  }
}

const TRANSITION_VALUES_TO_REMOVE = [
  { attributeName: 'css', defaultValue: 'true' },
  { attributeName: 'persisted', defaultValue: 'true' },
];
function removeInternalPropsLeakingToTransitionStub(node) {
  TRANSITION_VALUES_TO_REMOVE.forEach((hash) => {
    if (node.getAttribute(hash.attributeName) === hash.defaultValue) {
      node.removeAttribute(hash.attributeName);
    }
  });
}

function normalizeText(node) {
  const newText = node.textContent.trim();
  const textWithoutNewLines = newText.replace(/\n/g, '');
  const textWithoutDeepSpace = textWithoutNewLines.replace(/(?<=\S)\s+/g, ' ');
  // eslint-disable-next-line no-param-reassign
  node.textContent = textWithoutDeepSpace;
}

const visited = new WeakSet();

// Lovingly borrowed from https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace#whitespace_helper_functions
function isAllWhitespace(node) {
  return !/[^\t\n\r ]/.test(node.textContent);
}

function isIgnorable(node) {
  return (
    node.nodeType === Node.COMMENT_NODE || // A comment node
    (node.nodeType === Node.TEXT_NODE && isAllWhitespace(node))
  ); // a text node, all ws
}

const REFERENCE_ATTRIBUTES = ['aria-controls', 'aria-labelledby', 'for'];
function updateIdTags(root) {
  const elementsWithIds = [...(root.id ? [root] : []), ...root.querySelectorAll('[id]')];

  const referenceSelector = REFERENCE_ATTRIBUTES.map((attr) => `[${attr}]`).join(',');
  const elementsWithReference = [
    ...(root.matches(referenceSelector) ? [root] : []),
    ...root.querySelectorAll(REFERENCE_ATTRIBUTES.map((attr) => `[${attr}]`).join(',')),
  ];

  elementsWithReference.forEach((el) => {
    REFERENCE_ATTRIBUTES.filter((attr) => el.getAttribute(attr)).forEach((target) => {
      const index = elementsWithIds.findIndex((t) => t.id === el.getAttribute(target));
      if (index !== -1) {
        el.setAttribute(target, `reference-${index}`);
      }
    });
  });

  elementsWithIds.forEach((el, index) => {
    el.setAttribute('id', `reference-${index}`);
  });
}

export function test(received) {
  return received instanceof Element && !visited.has(received);
}

export function serialize(received, config, indentation, depth, refs, printer) {
  // Explicitly set empty string values of img.src to `null` as Vue3 does
  // We need to do this before `clone`, otherwise src prop diff will be lost
  received.querySelectorAll('img').forEach((img) => img.setAttribute('src', img.src || null));

  const clone = received.cloneNode(true);

  updateIdTags(clone);
  visited.add(clone);

  const iterator = document.createNodeIterator(
    clone,
    // eslint-disable-next-line no-bitwise
    NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
  );
  const ignorableNodes = [];

  for (let currentNode = iterator.nextNode(); currentNode; currentNode = iterator.nextNode()) {
    if (isIgnorable(currentNode)) {
      ignorableNodes.push(currentNode);
    } else {
      if (currentNode instanceof Element) {
        ATTRIBUTES_TO_REMOVE.forEach((attr) => currentNode.removeAttribute(attr));

        if (!currentNode.tagName.includes('-')) {
          // We want to normalize boolean attributes rendering only on native tags
          BOOLEAN_ATTRIBUTES.forEach((attr) => {
            if (currentNode.hasAttribute(attr) && currentNode.getAttribute(attr) === attr) {
              currentNode.setAttribute(attr, '');
            }
          });
        }

        sortClassesAlphabetically(currentNode);

        if (currentNode.tagName === 'TRANSITION-STUB') {
          removeInternalPropsLeakingToTransitionStub(currentNode);
        }
      }

      if (currentNode.nodeType === Node.TEXT_NODE) {
        normalizeText(currentNode);
      }

      currentNode.normalize();
      visited.add(currentNode);
    }
  }

  ignorableNodes.forEach((x) => x.remove());

  return printer(clone, config, indentation, depth, refs);
}