Skip to content
This repository was archived by the owner on May 5, 2021. It is now read-only.

Commit 8bd598b

Browse files
Zyntondmo-odoo
authored andcommitted
[FIX] formattingSpace: properly ignore empty text and hidden inputs.
1 parent 3a05a17 commit 8bd598b

File tree

3 files changed

+67
-24
lines changed

3 files changed

+67
-24
lines changed

packages/utils/src/formattingSpace.ts

+38-24
Original file line numberDiff line numberDiff line change
@@ -102,42 +102,56 @@ function _followsInlineSpace(node: Node): boolean {
102102
* @returns {boolean}
103103
*/
104104
function _isAtSegmentBreak(node: Node, side: 'start' | 'end'): boolean {
105-
const siblingSide = side === 'start' ? 'previousSibling' : 'nextSibling';
106-
const sibling = node && node[siblingSide];
107-
const isAgainstAnotherSegment = _isAgainstAnotherSegment(node, side);
105+
const direction = side === 'start' ? 'previous' : 'next';
106+
const sibling = _significantSibling(node, direction);
107+
const isAgainstAnotherSegment = sibling && _isSegment(sibling);
108108
const isAtEdgeOfOwnSegment = _isBlockEdge(node, side);
109109
// In the DOM, a space before a BR is rendered but a space after a BR isn't.
110110
const isBeforeBR = side === 'end' && sibling && nodeName(sibling) === 'BR';
111111
return (isAgainstAnotherSegment && !isBeforeBR) || isAtEdgeOfOwnSegment;
112112
}
113113
/**
114-
* Return true if the given node is just before or just after another segment.
115-
* Eg: <div>abc<div>def</div></div> -> abc is before another segment (div).
116-
* Eg: <div><a>abc</a> <div>def</div></div> -> abc is before another segment
117-
* (div).
114+
* Return the next|previous "significant" node. Nodes to skip are empty text
115+
* nodes (text nodes with no text content or only whitespace), and hidden
116+
* inputs.
117+
* If `searchUp` is true (which is the default), the next|previous "significant"
118+
* node may be a cousin.
118119
*
119120
* @param {Node} node
120-
* @param {'start'|'end'} side
121-
* @returns {boolean}
121+
* @param {'previous'|'next'} direction
122+
* @param {boolean} [searchUp] (default: true)
123+
* @returns {Node}
122124
*/
123-
function _isAgainstAnotherSegment(node: Node, side: 'start' | 'end'): boolean {
124-
const siblingSide = side === 'start' ? 'previousSibling' : 'nextSibling';
125-
const sibling = node && node[siblingSide];
126-
if (sibling) {
127-
return sibling && _isSegment(sibling);
128-
} else {
125+
function _significantSibling(node: Node, direction: 'previous' | 'next', searchUp = true): Node {
126+
const siblingSide = direction === 'previous' ? 'previousSibling' : 'nextSibling';
127+
let sibling: Node = node && node[siblingSide];
128+
let lastExisting = sibling || node;
129+
if (
130+
sibling &&
131+
// Sibling is an empty text node.
132+
((sibling.nodeType === Node.TEXT_NODE &&
133+
onlyTabsSpacesAndNewLines.test(sibling.textContent)) ||
134+
// Sibling is a hidden input.
135+
(nodeName(sibling) === 'INPUT' &&
136+
(sibling as Element).getAttribute('type') === 'hidden'))
137+
) {
138+
lastExisting = sibling;
139+
sibling = sibling[siblingSide];
140+
}
141+
if (!sibling && searchUp) {
129142
// Look further (eg.: `<div><a>abc</a> <div>def</div></div>`: the
130143
// space should be removed).
131-
let ancestor = node;
132-
while (ancestor && !ancestor[siblingSide]) {
144+
let ancestor = lastExisting;
145+
while (ancestor && !_significantSibling(ancestor, direction, false)) {
133146
ancestor = ancestor.parentNode;
134147
}
135-
let cousin = ancestor && !_isSegment(ancestor) && ancestor.nextSibling;
136-
while (cousin && isInstanceOf(cousin, Text)) {
137-
cousin = cousin.nextSibling;
148+
sibling =
149+
ancestor && !_isSegment(ancestor) && _significantSibling(ancestor, direction, false);
150+
while (sibling && isInstanceOf(sibling, Text)) {
151+
sibling = _significantSibling(sibling, direction, false);
138152
}
139-
return cousin && _isSegment(cousin);
140153
}
154+
return sibling;
141155
}
142156
/**
143157
* Return true if the node is a segment according to W3 formatting model.
@@ -175,15 +189,15 @@ function _isBlockEdge(node: Node, side: 'start' | 'end'): boolean {
175189

176190
// Return true if no ancestor up to the first block ancestor has a
177191
// sibling on the specified side
178-
const siblingSide = side === 'start' ? 'previousSibling' : 'nextSibling';
192+
const direction = side === 'start' ? 'previous' : 'next';
179193
return ancestorsUpToBlock.every(ancestor => {
180-
let sibling = ancestor[siblingSide];
194+
let sibling = _significantSibling(ancestor, direction);
181195
while (
182196
sibling &&
183197
isInstanceOf(sibling, Text) &&
184198
sibling.textContent.match(onlyTabsSpacesAndNewLines)
185199
) {
186-
sibling = sibling[siblingSide];
200+
sibling = _significantSibling(sibling, direction);
187201
}
188202
return !sibling;
189203
});

packages/utils/src/isBlock.ts

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const blockTagNames = [
4242
'TABLE',
4343
'UL',
4444
// The following elements are not in the W3C list, for some reason.
45+
'SELECT',
4546
'TR',
4647
'TD',
4748
'TBODY',

packages/utils/test/formattingSpace.test.ts

+28
Original file line numberDiff line numberDiff line change
@@ -326,5 +326,33 @@ describe('removeFormattingSpace() util function', () => {
326326
const nodes = htmlToElements('<div><a>abc</a> <div>def</div></div>');
327327
expect(removeFormattingSpace(nodes[0].childNodes[1])).to.equal('');
328328
});
329+
it('should remove space between a <a> and a <select>', async () => {
330+
const nodes = htmlToElements(
331+
'<div><a>abc</a> <select><option>def</option></select></div>',
332+
);
333+
expect(removeFormattingSpace(nodes[0].childNodes[1])).to.equal('');
334+
});
335+
it('should remove space between hidden inputs', async () => {
336+
const nodes = htmlToElements(
337+
'<div><input type="hidden"/> <input type="hidden"/></div>',
338+
);
339+
expect(removeFormattingSpace(nodes[0].childNodes[1])).to.equal('');
340+
});
341+
it('should preserve space between text nodes separated by hidden input (1)', async () => {
342+
const nodes = htmlToElements('<div>abc <input type="hidden"/>def</div>');
343+
expect(removeFormattingSpace(nodes[0].childNodes[0])).to.equal('abc ');
344+
});
345+
it('should preserve space between text nodes separated by hidden input (2)', async () => {
346+
const nodes = htmlToElements('<div>abc<input type="hidden"/> def</div>');
347+
expect(removeFormattingSpace(nodes[0].childNodes[2])).to.equal(' def');
348+
});
349+
it('should remove space between text node and block separated by hidden input (1)', async () => {
350+
const nodes = htmlToElements('<div>abc <input type="hidden"/><p>def</p></div>');
351+
expect(removeFormattingSpace(nodes[0].childNodes[0])).to.equal('abc');
352+
});
353+
it('should remove space between text node and block separated by hidden input (1)', async () => {
354+
const nodes = htmlToElements('<div>abc<input type="hidden"/> <p>def</p></div>');
355+
expect(removeFormattingSpace(nodes[0].childNodes[2])).to.equal('');
356+
});
329357
});
330358
});

0 commit comments

Comments
 (0)