@@ -23,6 +23,7 @@ import indexOf from '@ckeditor/ckeditor5-utils/src/dom/indexof';
2323import getAncestors from '@ckeditor/ckeditor5-utils/src/dom/getancestors' ;
2424import getCommonAncestor from '@ckeditor/ckeditor5-utils/src/dom/getcommonancestor' ;
2525import isText from '@ckeditor/ckeditor5-utils/src/dom/istext' ;
26+ import isElement from '@ckeditor/ckeditor5-utils/src/lib/lodash/isElement' ;
2627
2728/**
2829 * DomConverter is a set of tools to do transformations between DOM nodes and view nodes. It also handles
@@ -948,10 +949,11 @@ export default class DomConverter {
948949 * Takes text data from native `Text` node and processes it to a correct {@link module:engine/view/text~Text view text node} data.
949950 *
950951 * Following changes are done:
952+ *
951953 * * multiple whitespaces are replaced to a single space,
952- * * space at the beginning of the text node is removed, if it is a first text node in it's container
953- * element or if previous text node ends by space character,
954- * * space at the end of the text node is removed, if it is a last text node in it's container.
954+ * * space at the beginning of a text node is removed if it is the first text node in its container
955+ * element or if the previous text node ends with a space character,
956+ * * space at the end of the text node is removed, if it is the last text node in its container.
955957 *
956958 * @param {Node } node DOM text node to process.
957959 * @returns {String } Processed data.
@@ -970,17 +972,20 @@ export default class DomConverter {
970972 // We're replacing 1+ (and not 2+) to also normalize singular \n\t\r characters (#822).
971973 data = data . replace ( / [ \n \t \r ] { 1 , } / g, ' ' ) ;
972974
973- const prevNode = this . _getTouchingDomTextNode ( node , false ) ;
974- const nextNode = this . _getTouchingDomTextNode ( node , true ) ;
975+ const prevNode = this . _getTouchingInlineDomNode ( node , false ) ;
976+ const nextNode = this . _getTouchingInlineDomNode ( node , true ) ;
977+
978+ const shouldLeftTrim = this . _checkShouldLeftTrimDomText ( prevNode ) ;
979+ const shouldRightTrim = this . _checkShouldRightTrimDomText ( node , nextNode ) ;
975980
976- // If previous dom text node does not exist or it ends by whitespace character, remove space character from the beginning
981+ // If the previous dom text node does not exist or it ends by whitespace character, remove space character from the beginning
977982 // of this text node. Such space character is treated as a whitespace.
978- if ( ! prevNode || / [ ^ \S \u00A0 ] / . test ( prevNode . data . charAt ( prevNode . data . length - 1 ) ) ) {
983+ if ( shouldLeftTrim ) {
979984 data = data . replace ( / ^ / , '' ) ;
980985 }
981986
982- // If next text node does not exist remove space character from the end of this text node.
983- if ( ! nextNode && ! startsWithFiller ( node ) ) {
987+ // If the next text node does not exist remove space character from the end of this text node.
988+ if ( shouldRightTrim ) {
984989 data = data . replace ( / $ / , '' ) ;
985990 }
986991
@@ -1001,15 +1006,15 @@ export default class DomConverter {
10011006 // Then, change character that is at the beginning of the text node to space character.
10021007 // As above, that was created for rendering reasons but it's real meaning is just a space character.
10031008 // We do that replacement only if this is the first node or the previous node ends on whitespace character.
1004- if ( ! prevNode || / [ ^ \S \u00A0 ] / . test ( prevNode . data . charAt ( prevNode . data . length - 1 ) ) ) {
1009+ if ( shouldLeftTrim ) {
10051010 data = data . replace ( / ^ \u00A0 / , ' ' ) ;
10061011 }
10071012
10081013 // Since input text data could be: `x_ _`, we would not replace the first after `x` character.
10091014 // We have to fix it. Since we already change all ` `, we will have something like this at the end of text data:
10101015 // `x_ _ _` -> `x_ `. Find at the end of string (can be followed only by spaces).
1011- // We do that replacement only if this is the last node or the next node starts by .
1012- if ( ! nextNode || nextNode . data . charAt ( 0 ) == '\u00A0' ) {
1016+ // We do that replacement only if this is the last node or the next node starts with or is a <br> .
1017+ if ( isText ( nextNode ) ? nextNode . data . charAt ( 0 ) == '\u00A0' : true ) {
10131018 data = data . replace ( / \u00A0 ( * ) $ / , ' $1' ) ;
10141019 }
10151020
@@ -1018,6 +1023,39 @@ export default class DomConverter {
10181023 return data ;
10191024 }
10201025
1026+ /**
1027+ * Helper function which checks if a DOM text node, preceded by the given `prevNode` should
1028+ * be trimmed from the left side.
1029+ *
1030+ * @param {Node } prevNode
1031+ */
1032+ _checkShouldLeftTrimDomText ( prevNode ) {
1033+ if ( ! prevNode ) {
1034+ return true ;
1035+ }
1036+
1037+ if ( isElement ( prevNode ) ) {
1038+ return true ;
1039+ }
1040+
1041+ return / [ ^ \S \u00A0 ] / . test ( prevNode . data . charAt ( prevNode . data . length - 1 ) ) ;
1042+ }
1043+
1044+ /**
1045+ * Helper function which checks if a DOM text node, succeeded by the given `nextNode` should
1046+ * be trimmed from the right side.
1047+ *
1048+ * @param {Node } node
1049+ * @param {Node } prevNode
1050+ */
1051+ _checkShouldRightTrimDomText ( node , nextNode ) {
1052+ if ( nextNode ) {
1053+ return false ;
1054+ }
1055+
1056+ return ! startsWithFiller ( node ) ;
1057+ }
1058+
10211059 /**
10221060 * Helper function. For given {@link module:engine/view/text~Text view text node}, it finds previous or next sibling
10231061 * that is contained in the same container element. If there is no such sibling, `null` is returned.
@@ -1033,12 +1071,17 @@ export default class DomConverter {
10331071 } ) ;
10341072
10351073 for ( const value of treeWalker ) {
1074+ // ViewContainerElement is found on a way to next ViewText node, so given `node` was first/last
1075+ // text node in its container element.
10361076 if ( value . item . is ( 'containerElement' ) ) {
1037- // ViewContainerElement is found on a way to next ViewText node, so given `node` was first/last
1038- // text node in its container element.
10391077 return null ;
1040- } else if ( value . item . is ( 'textProxy' ) ) {
1041- // Found a text node in the same container element.
1078+ }
1079+ // <br> found – it works like a block boundary, so do not scan further.
1080+ else if ( value . item . is ( 'br' ) ) {
1081+ return null ;
1082+ }
1083+ // Found a text node in the same container element.
1084+ else if ( value . item . is ( 'textProxy' ) ) {
10421085 return value . item ;
10431086 }
10441087 }
@@ -1047,15 +1090,27 @@ export default class DomConverter {
10471090 }
10481091
10491092 /**
1050- * Helper function. For given `Text` node, it finds previous or next sibling that is contained in the same block element.
1051- * If there is no such sibling, `null` is returned.
1093+ * Helper function. For the given text node, it finds the closest touching node which is either
1094+ * a text node or a `<br>`. The search is terminated at block element boundaries and if a matching node
1095+ * wasn't found so far, `null` is returned.
1096+ *
1097+ * In the following DOM structure:
1098+ *
1099+ * <p>foo<b>bar</b><br>bom</p>
1100+ *
1101+ * * `foo` doesn't have its previous touching inline node (`null` is returned),
1102+ * * `foo`'s next touching inline node is `bar`
1103+ * * `bar`'s next touching inline node is `<br>`
1104+ *
1105+ * This method returns text nodes and `<br>` elements because these types of nodes affect how
1106+ * spaces in the given text node need to be converted.
10521107 *
10531108 * @private
10541109 * @param {Text } node
10551110 * @param {Boolean } getNext
1056- * @returns {Text|null }
1111+ * @returns {Text|Element| null }
10571112 */
1058- _getTouchingDomTextNode ( node , getNext ) {
1113+ _getTouchingInlineDomNode ( node , getNext ) {
10591114 if ( ! node . parentNode ) {
10601115 return null ;
10611116 }
@@ -1064,7 +1119,19 @@ export default class DomConverter {
10641119 const document = node . ownerDocument ;
10651120 const topmostParent = getAncestors ( node ) [ 0 ] ;
10661121
1067- const treeWalker = document . createTreeWalker ( topmostParent , NodeFilter . SHOW_TEXT ) ;
1122+ const treeWalker = document . createTreeWalker ( topmostParent , NodeFilter . SHOW_TEXT | NodeFilter . SHOW_ELEMENT , {
1123+ acceptNode ( node ) {
1124+ if ( isText ( node ) ) {
1125+ return NodeFilter . FILTER_ACCEPT ;
1126+ }
1127+
1128+ if ( node . tagName == 'BR' ) {
1129+ return NodeFilter . FILTER_ACCEPT ;
1130+ }
1131+
1132+ return NodeFilter . FILTER_SKIP ;
1133+ }
1134+ } ) ;
10681135
10691136 treeWalker . currentNode = node ;
10701137
0 commit comments