jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/342024 )

Change subject: Use XPath to get text nodes for utterances.
......................................................................


Use XPath to get text nodes for utterances.

Replaced the index array paths with XPath expressions.

CleanedTag was also removed, since it's not needed anymore.

Bug: T158954
Change-Id: Ifec50d6d22a9e3e6adf51d96d315237852aa11ac
---
M extension.json
M includes/CleanedContent.php
M includes/Cleaner.php
M includes/HtmlGenerator.php
M includes/Segmenter.php
M modules/ext.wikispeech.css
M modules/ext.wikispeech.js
M tests/phpunit/CleanerTest.php
M tests/phpunit/HtmlGeneratorTest.php
M tests/phpunit/SegmenterTest.php
M tests/qunit/ext.wikispeech.test.js
11 files changed, 223 insertions(+), 731 deletions(-)

Approvals:
  Lokal Profil: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/extension.json b/extension.json
index 0470c86..850c2b9 100644
--- a/extension.json
+++ b/extension.json
@@ -18,8 +18,6 @@
        "AutoloadClasses": {
                "SpecialWikispeech": "specials/SpecialWikispeech.php",
                "WikispeechHooks": "Hooks.php",
-               "CleanedContent": "includes/CleanedContent.php",
-               "CleanedTag": "includes/CleanedContent.php",
                "CleanedText": "includes/CleanedContent.php",
                "Cleaner": "includes/Cleaner.php",
                "HtmlGenerator": "includes/HtmlGenerator.php",
diff --git a/includes/CleanedContent.php b/includes/CleanedContent.php
index 3065afd..5042574 100644
--- a/includes/CleanedContent.php
+++ b/includes/CleanedContent.php
@@ -6,11 +6,9 @@
  * @license GPL-2.0+
  */
 
-abstract class CleanedContent {
+class CleanedText {
        /**
-        * The string representation of the content, as it is written in
-        * the HTML. This includes the tag name, any attributes, and the
-        * brackets, if content is a tag.
+        * The text content from the text node this was created from.
         *
         * @var string $string
         */
@@ -18,25 +16,8 @@
        public $string;
 
        /**
-        * Create a CleanedContent, given a string representation.
-        *
-        * @since 0.0.1
-        * @param string $string The string representation of this content.
-        */
-
-       function __construct( $string ) {
-               $this->string = $string;
-       }
-}
-
-class CleanedTag extends CleanedContent {
-}
-
-class CleanedText extends CleanedContent {
-       /**
-        * The path in the HTML to the text node that this was created
-        * from. The path consists of indices of the elements leading to
-        * the text node, and the index of the text node itself.
+        * The XPath expression for the text node that this was created
+        * from.
         *
         * @var array $path
         */
@@ -46,15 +27,15 @@
        /**
         * Create a CleanedText, given a string representation.
         *
-        * If the path isn't set, it defaults to the empty array.
+        * If the path isn't set, it defaults to the empty string.
         *
         * @since 0.0.1
         * @param string $string The string representation of this text.
         * @param array $path The path to the text node this was created from.
         */
 
-       function __construct( $string, $path=[] ) {
-               parent::__construct( $string );
+       function __construct( $string, $path='' ) {
+               $this->string = $string;
                $this->path = $path;
        }
 
@@ -70,8 +51,7 @@
 
        function toElement( $dom ) {
                $element = $dom->createElement( 'text', $this->string );
-               $pathString = implode( $this->path, ',' );
-               $element->setAttribute( 'path', $pathString );
+               $element->setAttribute( 'path', $this->path );
                return $element;
        }
 }
diff --git a/includes/Cleaner.php b/includes/Cleaner.php
index 1109d48..a65e0d6 100644
--- a/includes/Cleaner.php
+++ b/includes/Cleaner.php
@@ -16,25 +16,19 @@
         * @since 0.0.1
         * @param string $markedUpText Input text that may contain HTML
         *  tags.
-        * @return array An array of nodes where tags are stored as
-        *  `CleanedTag`s and text nodes as `Text`.
+        * @return array An array of `CleanedText`s representing text nodes.
         */
 
        public static function cleanHtml( $markedUpText ) {
                $dom = self::createDomDocument( $markedUpText );
-               $tags = self::getTags( $markedUpText );
-               // Start adding the nodes that are children of the dummy
-               // element. To not add the actual dummy tags, index starts on
-               // -1.
-               $tagIndex = -1;
+               $xpath = new DOMXPath( $dom );
+               // Only add elements below the dummy element. These are the
+               // elements from the original HTML.
+               $top = $xpath->evaluate( '/meta/dummy' )->item( 0 );
                $cleanedContent = [];
                self::addContent(
                        $cleanedContent,
-                       $dom->documentElement->firstChild,
-                       $markedUpText,
-                       $tags,
-                       $tagIndex,
-                       []
+                       $top
                );
                return $cleanedContent;
        }
@@ -66,296 +60,38 @@
        }
 
        /**
-        * Extract a list of tags from a string.
+        * Recursively add content as `CleanedText`s.
         *
-        * Tags are extracted in the order they appear. This is done using
-        * regex since we need the exact string representation of tags to
-        * get their correct lengths.
-        *
-        * When a start tag is encountered, it's stored as a string. This
-        * string is then added to an array, which holds a start-end pair
-        * of tags.
-        *
-        * When an end tag is encountered, it's stored as a string. This
-        * string is then added to the array containing the corresponding
-        * start tag.
-        *
-        * Empty element tags are added as tag strings.
+        * Goes through all the child nodes of $node and adds their content 
text.
         *
         * @since 0.0.1
-        * @param string $markedUpText The string to extract tags from.
-        * @return array An array containing the found tags.
-        */
-
-       private static function getTags( $markedUpText ) {
-               $potentialTagBrackets = [];
-               preg_match_all(
-                       '/[<>]/',
-                       $markedUpText,
-                       $potentialTagBrackets,
-                       PREG_SET_ORDER | PREG_OFFSET_CAPTURE
-               );
-               $tags = [];
-               $startBracket = null;
-               foreach ( $potentialTagBrackets as $match ) {
-                       // $match[0] is an array containing the matched string 
and it's
-                       // position.
-                       $bracketString = $match[0][0];
-                       if ( $bracketString == '<' ) {
-                               if ( $startBracket == null ) {
-                                       $startBracket = $match[0];
-                               }
-                       } elseif ( $bracketString == '>' && $startBracket != 
null ) {
-                               $tagString = substr(
-                                       $markedUpText,
-                                       $startBracket[1],
-                                       $match[0][1] - $startBracket[1] + 1
-                               );
-                               $startBracket = null;
-                               if ( self::isStartTag( $tagString ) ) {
-                                       array_push( $tags, [ $tagString ] );
-                               } elseif ( self::isEndTag( $tagString ) ) {
-                                       $startTagIndex = 
self::getCorrespondingStartTagIndex(
-                                               $tags,
-                                               $tagString
-                                       );
-                                       // Add the end tag to the array already 
containing the
-                                       // start tag.
-                                       array_push(
-                                               $tags[$startTagIndex],
-                                               $tagString
-                                       );
-                               } elseif ( self::isEmptyElementTag( $tagString 
) ) {
-                                       array_push( $tags, $tagString );
-                               }
-                       }
-               }
-               return $tags;
-       }
-
-       /**
-        * Test if a string is a start tag.
-        *
-        * @since 0.0.1
-        * @param $tagString The string to test.
-        * @return true if $tagString is a start tag, else false.
-        */
-
-       private static function isStartTag( $tagString ) {
-               return substr( $tagString, 0, 2 ) != '</' &&
-                       substr( $tagString, -2 ) != '/>';
-       }
-
-       /**
-        * Test if a string is an end tag.
-        *
-        * @since 0.0.1
-        * @param $tagString The string to test.
-        * @return true if $tagString is an end tag, else false.
-        */
-
-       private static function isEndTag( $tagString ) {
-               return substr( $tagString, 0, 2 ) == '</';
-       }
-
-       /**
-        * Test if a string is an empty element tag.
-        *
-        * @since 0.0.1
-        * @param $tagString The string to test.
-        * @return true if $tagString is an empty element tag, else false.
-        */
-
-       private static function isEmptyElementTag( $tagString ) {
-               return substr( $tagString, -2 ) == '/>';
-       }
-
-       /**
-        * Get the index in $tags of the tag that starts the element which
-        * ends with $tagString.
-        *
-        * Traverses $tags backwards and tests if start tags are of the
-        * same type as the one in $tagString.
-        *
-        * @since 0.0.1
-        * @param array $tags Tag array, as returned from getTags().
-        * @param string $tagString the end tag to find start tag for, as
-        *  HTML string.
-        * @return int The index in $tags of the start tag found.
-        */
-
-       private static function getCorrespondingStartTagIndex(
-               $tags,
-               $tagString
-       ) {
-               for ( $i = count( $tags ) - 1; $i >= 0; $i -- ) {
-                       $tag = $tags[$i];
-                       if ( is_array( $tag ) && count( $tag ) == 1 ) {
-                               // Make sure the tag to test is an array, i.e. 
a start
-                               // tag, and that it doesn't have an end tag 
already.
-                               $startTagType = self::getTagName( $tag[0] );
-                               $endTagType = self::getTagName( $tagString );
-                               if ( $startTagType == $endTagType ) {
-                                       return $i;
-                               }
-                       }
-               }
-       }
-
-       /**
-        * Get the tag name from a tag string.
-        *
-        * @since 0.0.1
-        * @param string $tagString The tag as string.
-        * @return string The name of the tag in $tagString.
-        */
-
-       private static function getTagName( $tagString ) {
-               $nameMatch = null;
-               preg_match( '!</?([^ />]+)( />)?!', $tagString, $nameMatch );
-               $tagName = $nameMatch[1];
-               return $tagName;
-       }
-
-       /**
-        * Recursively add content as either CleanedTags or strings.
-        *
-        * Goes through all the child nodes of $node and adds the
-        * corresponding content. If a child is a tag, it's added as a
-        * CleanedTag of the appropriate type (Start, End or Empty). If a
-        * child is a text node, the text is added as a string.
-        *
-        * @since 0.0.1
-        * @param array $content The resulting array of CleanedTags and
-        *  strings.
+        * @param array $content The resulting array of `CleanedText`s.
         * @param DOMNode $node The top node to add from.
-        * @param string $source The HTML string that DOM is generated
-        *  from. Used for retrieveing element contents.
-        * @param array $tags Tag array, as generated by getTags().
-        * @param int $tagIndex The index of the next tag, from $tags.
-        * @param array $path The indices of each ancestor node to
-        *  $node. Used for connecting a Text element to a text node in
-        *  the HTML.
         */
 
        private static function addContent(
                &$content,
-               $node,
-               $source,
-               $tags,
-               &$tagIndex,
-               $path
+               $node
        ) {
-               $startTag = null;
-               $endTag = null;
-               if ( $tagIndex >= 0 ) {
-                       // Don't add the dummy tag.
-                       if ( is_array( $tags[$tagIndex] ) ) {
-                               // If an item in $tags is an array, it holds 
arrays
-                               // for start and end tags.
-                               $startTag = $tags[$tagIndex][0];
-                               $endTag = $tags[$tagIndex][1];
-                               array_push( $content, new CleanedTag( $startTag 
) );
-                       } else {
-                               // If the tag is empty, just add it and return, 
since
-                               // there can't be any child nodes.
-                               $emptyElementTagString = $tags[$tagIndex];
-                               array_push(
-                                       $content,
-                                       new CleanedTag( $emptyElementTagString )
-                               );
-                               return;
+               if ( !self::matchesRemove( $node ) ) {
+                       foreach ( $node->childNodes as $child ) {
+                               if ( $child->nodeType == XML_TEXT_NODE ) {
+                                       // Remove the path to the dummy node 
and instead add
+                                       // "." to match when used with context.
+                                       $path = preg_replace(
+                                               '!^/meta/dummy!',
+                                               '.',
+                                               $child->getNodePath()
+                                       );
+                                       $text = new CleanedText( 
$child->textContent, $path );
+                                       array_push( $content, $text );
+                               } else {
+                                       self::addContent(
+                                               $content,
+                                               $child
+                                       );
+                               }
                        }
-               }
-               if ( self::matchesRemove( $node ) ) {
-                       // When a tag is removed, skip forward a number of tags
-                       // equal to the number of nodes under that tag.
-                       $tagIndex += self::getNumberOfDescendants( $node );
-               } else {
-                       self::addChildren(
-                               $content,
-                               $node,
-                               $source,
-                               $tags,
-                               $tagIndex,
-                               $path
-                       );
-               }
-               if ( $endTag != null ) {
-                       array_push(
-                               $content,
-                               new CleanedTag( $endTag )
-                       );
-               }
-       }
-
-       /**
-        * Get the number of nodes that are descendants of a given node.
-        *
-        * @since 0.0.1
-        * @param DOMNode $node The node to get number of descendants of.
-        * @return int The number of decendants of $node.
-        */
-
-       private static function getNumberOfDescendants( $node ) {
-               if ( !$node->hasChildNodes() ) {
-                       return 0;
-               }
-               $numberOfDescendants = 0;
-               foreach ( $node->childNodes as $child ) {
-                       if ( $child->nodeType != XML_TEXT_NODE ) {
-                               $numberOfDescendants +=
-                                       1 + self::getNumberOfDescendants( 
$child );
-                       }
-               }
-               return $numberOfDescendants;
-       }
-
-       /**
-        * Add content for child nodes to an array.
-        *
-        * @since 0.0.1
-        * @param array $content The array that the children are added to.
-        * @param DOMNode $node Add content for the children of this node.
-        * @param string $source The HTML string that DOM is generated
-        *  from. Used for retrieveing element contents.
-        * @param array $tags Tag array, as generated by getTags().
-        * @param int $tagIndex The index of the next tag, from $tags.
-        * @param array $path The indices of each ancestor node to
-        *  $node. Used for connecting a Text element to a text node in
-        *  the HTML.
-        */
-
-       private static function addChildren(
-               &$content,
-               $node,
-               $source,
-               $tags,
-               &$tagIndex,
-               $path
-       ) {
-               $childIndex = 0;
-               foreach ( $node->childNodes as $child ) {
-                       $childPath = $path;
-                       array_push( $childPath, $childIndex );
-                       if ( $child->nodeType == XML_TEXT_NODE ) {
-                               $text = new CleanedText( $child->textContent, 
$childPath );
-                               array_push( $content, $text );
-                       } else {
-                               // Nodes are handled even if their parents are
-                               // removed, to not get the DOM nodes out of 
sync with
-                               // $tags.
-                               $tagIndex += 1;
-                               self::addContent(
-                                       $content,
-                                       $child,
-                                       $source,
-                                       $tags,
-                                       $tagIndex,
-                                       $childPath
-                               );
-                       }
-                       $childIndex ++;
                }
        }
 
diff --git a/includes/HtmlGenerator.php b/includes/HtmlGenerator.php
index 649010e..136631a 100644
--- a/includes/HtmlGenerator.php
+++ b/includes/HtmlGenerator.php
@@ -62,7 +62,9 @@
         * The element looks like this in HTML:
         * <utterance id="utterance-0>
         *   <content>
-        *     Utterance string with <cleaned-tag>not removed tag</cleaned-tag>.
+        *     <text>Utterance with</text>
+        *     <text>tag</text>
+        *     <text>.</text>
         *   </content>
         * </utterance>
         *
@@ -70,16 +72,13 @@
         * utterances, when next or previous utterance should be played.
         *
         * The content element contains a representation of the HTML that
-        * was used to generate this utterance. Text nodes are the same as
-        * in the original HTML. Elements are represented by cleaned-tag
-        * elements, whose contents are the tags from the original HTML,
-        * excluding < and >.
+        * was used to generate this utterance. Text nodes are represented
+        * by `<text>` elements.
         *
         * @since 0.0.1
         * @param DOMDocument $dom The DOMDocument to use for creating the
         *  element.
-        * @param array $segment An array with position and content as an
-        *  array of CleanedTags and strings.
+        * @param array $segment An array of `CleanedText`s.
         * @param int $index The index of the element, used for giving it
         *  an id. Later used for playing the utterances in the correct
         *  order.
@@ -108,26 +107,17 @@
        /**
         * Create a content element from a content array.
         *
-        * CleanedTags are represented as cleaned-tag elements, strings as
-        * text nodes.
+        * `CleanedText`s are represented by `<text>` nodes.
         *
         * @since 0.0.1
-        * @param array $content An array of CleanedTags and strings.
+        * @param array $content An array of `CleanedText`s.
         * @return DOMNode A content element.
         */
 
        private static function createContentElement( $dom, $content ) {
                $contentElement = $dom->createElement( 'content' );
                foreach ( $content as $part ) {
-                       if ( $part instanceof CleanedTag ) {
-                               // Remove the < and > from the tag string to 
not have to
-                               // decode them later.
-                               $text = substr( $part->string, 1, -1 );
-                               $cleanedTagElement = $dom->createElement( 
'cleaned-tag', $text );
-                               $contentElement->appendChild( 
$cleanedTagElement );
-                       } else {
                                $contentElement->appendChild( $part->toElement( 
$dom ) );
-                       }
                }
                return $contentElement;
        }
diff --git a/includes/Segmenter.php b/includes/Segmenter.php
index 8eba68c..249bbf5 100644
--- a/includes/Segmenter.php
+++ b/includes/Segmenter.php
@@ -12,7 +12,7 @@
         * Divide a cleaned content array into segments, one for each sentence.
         *
         * A segment is an array with the keys "content", "startOffset"
-        * and "endOffset". "content" is an array of `CleanedContent`s.
+        * and "endOffset". "content" is an array of `CleanedText`s.
 
         * "startOffset" is the position of the first character of the
         * segment, within the text node it appears. "endOffset" is the
@@ -24,10 +24,10 @@
         * dot (full stop). Headings are also considered sentences.
         *
         * @since 0.0.1
-        * @param array $cleanedContent An array of `CleanedContent`s, as
+        * @param array $cleanedContent An array of `CleanedText`s, as
         *  returned by `Cleaner::cleanHtml()`.
         * @return array An array of segments, each containing the
-        *  `CleanedContent's in that segment.
+        *  `CleanedText's in that segment.
         */
 
        public static function segmentSentences( $cleanedContent ) {
@@ -37,17 +37,11 @@
                        'startOffset' => 0
                ];
                foreach ( $cleanedContent as $content ) {
-                       if ( $content instanceof CleanedTag ) {
-                               // Non-text nodes are always added to the 
current segment, as
-                               // they can't contain segment breaks.
-                               array_push( $currentSegment['content'], 
$content );
-                       } else {
-                               self::addSegments(
-                                       $segments,
-                                       $currentSegment,
-                                       $content
-                               );
-                       }
+                       self::addSegments(
+                               $segments,
+                               $currentSegment,
+                               $content
+                       );
                }
                if ( $currentSegment['content'] ) {
                        // Add the last segment, unless it's empty.
diff --git a/modules/ext.wikispeech.css b/modules/ext.wikispeech.css
index 7d27ba7..98d0148 100644
--- a/modules/ext.wikispeech.css
+++ b/modules/ext.wikispeech.css
@@ -32,11 +32,6 @@
        content: '\f050';
 }
 
-.ext-wikispeech-highlight-sentence {
-       background-color: rgb( 0, 200, 0 );
-       background-color: rgba( 0, 200, 0, 0.5 );
-}
-
 .ext-wikispeech-control-panel {
        position: fixed;
        left: 0;
@@ -46,3 +41,8 @@
        border: solid 1px rgb( 150, 150, 150 );
        background-color: rgb( 200, 200, 200 );
 }
+
+.ext-wikispeech-highlight-sentence {
+       background-color: rgb( 0, 200, 0 );
+       background-color: rgba( 0, 200, 0, 0.5 );
+}
diff --git a/modules/ext.wikispeech.js b/modules/ext.wikispeech.js
index 4290182..8423a4a 100644
--- a/modules/ext.wikispeech.js
+++ b/modules/ext.wikispeech.js
@@ -189,59 +189,58 @@
 
                this.highlightUtterance = function ( $utterance ) {
                        var firstTextElement, firstNode, lastTextElement, 
lastNode,
-                               firstNodeRange, startOffset, endOffset, 
textNode;
+                               startOffset, endOffset, textNode, middleNodes;
 
                        firstTextElement = $utterance.find( 'text' ).get( 0 );
                        if ( firstTextElement ) {
                                firstNode = self.getNodeForTextElement( 
firstTextElement );
                                lastTextElement = $utterance.find( 'text' 
).get( -1 );
                                lastNode = self.getNodeForTextElement( 
lastTextElement );
-
-                               // Range for the first node, since it may be 
that only
-                               // part of this should be highlighted.
-                               firstNodeRange = document.createRange();
                                startOffset =
                                        parseInt( $utterance.attr( 
'start-offset' ), 10 );
-                               firstNodeRange.setStart( firstNode, startOffset 
);
                                endOffset =
                                        parseInt( $utterance.attr( 'end-offset' 
), 10 );
                                if ( firstNode === lastNode ) {
-                                       // All highlighted text is in the same 
text node, so
-                                       // only a range is needed.
-                                       firstNodeRange.setEnd( firstNode, 
endOffset + 1 );
-                               } else {
-                                       // Since the highlighting extends 
beyond the first
-                                       // text node, all text from the start 
position should
-                                       // be highlighted.
-                                       firstNodeRange.setEnd(
+                                       // All highlighted text is in the same 
text node,
+                                       // so only one <span> is needed.
+                                       self.highlightRange(
                                                firstNode,
-                                               firstNode.textContent.length
+                                               startOffset,
+                                               endOffset
                                        );
-                                       // Add highlighting by range for the 
last text
-                                       // node, since it may be that the 
highlighting
-                                       // doesn't cover the whole node.
-                                       self.highlightRange( lastNode, 0, 
endOffset + 1 );
-
+                               } else {
+                                       // The middle nodes need to be found 
before the
+                                       // other highlighting, since adding 
<span>s
+                                       // changes how the XPath expressions are
+                                       // interpreted.
+                                       middleNodes = [];
                                        $utterance.find( 'text:gt(0):lt(-1)' 
).each( function () {
-                                               // Add highlighting to all text 
nodes between
-                                               // first and last node.
                                                textNode = 
self.getNodeForTextElement( this );
-                                               $( textNode ).wrap(
+                                               middleNodes.push( textNode );
+                                       } );
+
+                                       // Add a <span> around the part of the 
last node
+                                       // that is part if the utterance.
+                                       self.highlightRange( lastNode, 0, 
endOffset );
+                                       // Wrap all nodes between the first and 
last
+                                       // completely in <span>s.
+                                       middleNodes.forEach( function ( node ) {
+                                               $( node ).wrap(
                                                        
self.createHighilightUtteranceSpan()
                                                );
                                        } );
+                                       // Add a <span> around the part of the 
first node
+                                       // that is part if the utterance.
+                                       self.highlightRange( firstNode, 
startOffset );
                                }
-                               firstNodeRange.surroundContents(
-                                       self.createHighilightUtteranceSpan()
-                               );
                        }
                };
 
                /**
                 * Find the text node from which a `<text>` element was created.
                 *
-                * The path attribute of textElement is used to traverse the
-                * DOM tree.
+                * The path attribute of textElement is an XPath expression
+                * and is used to traverse the DOM tree.
                 *
                 * @param {HTMLElement} textElement The `<text>` element to find
                 *  the text node for.
@@ -249,11 +248,19 @@
                 */
 
                this.getNodeForTextElement = function ( textElement ) {
-                       var path, node;
+                       var path, node, result;
 
-                       path = textElement.getAttribute( 'path' ).split( ',' );
-                       node =
-                               self.getNodeFromPath( path, $( 
'#mw-content-text' ).get( 0 ) );
+                       path = textElement.getAttribute( 'path' );
+                       // The path should be unambiguous, so just get the first
+                       // matching node.
+                       result = document.evaluate(
+                               path,
+                               $( '#mw-content-text' ).get( 0 ),
+                               null,
+                               XPathResult.FIRST_ORDERED_NODE_TYPE,
+                               null
+                       );
+                       node = result.singleNodeValue;
                        return node;
                };
 
@@ -286,13 +293,17 @@
                 * @param {number} start The index of the first character in the
                 *  highlighting.
                 * @param {number} end The index of the last character in the
-                *  highlighting.
+                *  highlighting. If not specified, the last character in the
+                *  text node is used.
                 */
 
                this.highlightRange = function ( node, start, end ) {
                        var range = document.createRange();
                        range.setStart( node, start );
-                       range.setEnd( node, end );
+                       if ( end === undefined ) {
+                               end = node.textContent.length - 1;
+                       }
+                       range.setEnd( node, end + 1 );
                        range.surroundContents( 
self.createHighilightUtteranceSpan() );
                };
 
@@ -730,7 +741,7 @@
                 * The request should specify the following parameters:
                 * - lang: the language used by the synthesizer.
                 * - input_type: "ssml" if you want SSML markup, otherwise
-                *  "text" for plain text.
+                *    "text" for plain text.
                 * - input: the text to be synthesized.
                 * For more on the parameters, see:
                 * https://github.com/stts-se/wikispeech_mockup/wiki/api.
diff --git a/tests/phpunit/CleanerTest.php b/tests/phpunit/CleanerTest.php
index 31615e5..0303ef4 100644
--- a/tests/phpunit/CleanerTest.php
+++ b/tests/phpunit/CleanerTest.php
@@ -25,9 +25,7 @@
        public function testCleanTags() {
                $markedUpText = '<i>Element content</i>';
                $expectedCleanedContent = [
-                       new CleanedTag( '<i>' ),
-                       new CleanedText( 'Element content' ),
-                       new CleanedTag( '</i>' )
+                       new CleanedText( 'Element content' )
                ];
                $this->assertTextCleaned( $expectedCleanedContent, 
$markedUpText );
        }
@@ -105,9 +103,8 @@
         * Assert correct output when input is preceded and followed by text.
         *
         * Pre- and suffix strings are concatenated to the first and last
-        * part respectively, of the expected content if they are
-        * `Text`s. If they are `CleanedTag`s, they are added as new
-        * parts.
+        * `CleanedText` respectively, unless there are tags in the marked
+        * up text. In that case, new `CleanedText`s are added.
         *
         * @since 0.0.1
         * @param array $expectedCleanedContents The content array that is
@@ -120,20 +117,30 @@
                $expectedCleanedContents,
                $markedUpText
        ) {
-               if ( is_a( $expectedCleanedContents[0], 'CleanedText' ) ) {
+               if ( $markedUpText[0] == '<' ) {
+                       array_unshift(
+                               $expectedCleanedContents,
+                               new CleanedText( 'prefix' )
+                       );
+               } else {
                        $expectedCleanedContents[0] =
-                               new CleanedText( 'prefix' . 
$expectedCleanedContents[0]->string );
-               } else {
-                       array_unshift( $expectedCleanedContents, new 
CleanedText( 'prefix' ) );
-               }
-               $lastIndex = count( $expectedCleanedContents ) - 1;
-               if ( is_a( $expectedCleanedContents[$lastIndex], 'CleanedText' 
) ) {
-                       $expectedCleanedContents[$lastIndex] =
                                new CleanedText(
-                                       
$expectedCleanedContents[$lastIndex]->string . 'suffix'
+                                       'prefix' . 
$expectedCleanedContents[0]->string
                                );
+               }
+               $lastCharIndex = mb_strlen( $markedUpText ) - 1;
+               if ( $markedUpText[$lastCharIndex] == '>' ) {
+                       array_push(
+                               $expectedCleanedContents,
+                               new CleanedText( 'suffix' )
+                       );
                } else {
-                       array_push( $expectedCleanedContents, new CleanedText( 
'suffix' ) );
+                       $lastContentIndex = count( $expectedCleanedContents ) - 
1;
+                       $expectedCleanedContents[$lastContentIndex] =
+                               new CleanedText(
+                                       
$expectedCleanedContents[$lastContentIndex]->string
+                                       . 'suffix'
+                               );
                }
                $this->assertContentsEqual(
                        $expectedCleanedContents,
@@ -144,12 +151,11 @@
        /**
         * Assert correct output when input is repeated and separated by string.
         *
-        * If the first instance of the expected content ends with a
-        * `Text`, the infix is added after that. If the second instance
-        * starts with a `Text`, the infix is added before that. If both
-        * cases occur at the same time, the `Text` between the instances
-        * will consist of the last `Text` of first instance, infix and
-        * first `Text` of second instance.
+        * Adds the infix as a `CleanedText` between two copies of
+        * $expectedCleanedContents. If the marked up text doesn't end
+        * with a tag, the infix is added to the end of the first
+        * copy. Similarily, it's added to the beginning if the marked up
+        * text doesn't start with a tag.
         *
         * @since 0.0.1
         * @param array $expectedCleanedContents The content array that
@@ -164,13 +170,13 @@
        ) {
                $infix = new CleanedText( 'infix' );
                $firstContents = $expectedCleanedContents;
-               $lastIndex = count( $firstContents ) - 1;
-               if ( is_a( $firstContents[$lastIndex], 'CleanedText' ) ) {
+               $lastCharIndex = mb_strlen( $markedUpText ) - 1;
+               if ( $markedUpText[$lastCharIndex] != '>' ) {
                        $adjacent = array_pop( $firstContents );
                        $infix->string = $adjacent->string . $infix->string;
                }
                $secondContents = $expectedCleanedContents;
-               if ( is_a( $secondContents[0], 'CleanedText' ) ) {
+               if ( $markedUpText[0] != '<' ) {
                        $adjacent = array_shift( $secondContents );
                        $infix->string .= $adjacent->string;
                }
@@ -194,69 +200,42 @@
        public function testCleanNestedTags() {
                $markedUpText = '<i><b>Nested content</b></i>';
                $expectedCleanedContent = [
-                       new CleanedTag( '<i>' ),
-                       new CleanedTag( '<b>' ),
-                       new CleanedText( 'Nested content' ),
-                       new CleanedTag( '</b>' ),
-                       new CleanedTag( '</i>' )
+                       new CleanedText( 'Nested content' )
                ];
                $this->assertTextCleaned( $expectedCleanedContent, 
$markedUpText );
        }
 
        public function testCleanEmptyElementTags() {
                $markedUpText = '<br />';
-               $expectedCleanedContent = [
-                       new CleanedTag( '<br />' )
-               ];
-               $this->assertTextCleaned( $expectedCleanedContent, 
$markedUpText );
+               $this->assertTextCleaned( [], $markedUpText );
        }
 
        public function testRemoveTags() {
                $markedUpText = '<del>removed tag </del>';
-               $expectedCleanedContent = [
-                       new CleanedTag( '<del>' ),
-                       new CleanedTag( '</del>' )
-               ];
-               $this->assertTextCleaned( $expectedCleanedContent, 
$markedUpText );
+               $this->assertTextCleaned( [], $markedUpText );
        }
 
-       public function testDontAddCleanedTagsForTagsUnderRemovedTags() {
+       public function testRemoveNestedTags() {
                $markedUpText = '<del><i>nested removed tag</i></del>';
-               $expectedCleanedContent = [
-                       new CleanedTag( '<del>' ),
-                       new CleanedTag( '</del>' )
-               ];
-               $this->assertTextCleaned( $expectedCleanedContent, 
$markedUpText );
+               $this->assertTextCleaned( [], $markedUpText );
        }
 
        public function testRemoveDoubleNestedTags() {
                $markedUpText = '<del><i><b>double nested removed 
tag</b></i></del>';
-               $expectedCleanedContent = [
-                       new CleanedTag( '<del>' ),
-                       new CleanedTag( '</del>' )
-               ];
-               $this->assertTextCleaned( $expectedCleanedContent, 
$markedUpText );
+               $this->assertTextCleaned( [], $markedUpText );
        }
 
        public function testRemoveTagsWithCertainClass() {
                $markedUpText = '<sup class="reference">Remove this.</sup>';
-               $expectedCleanedContent = [
-                       new CleanedTag( '<sup class="reference">' ),
-                       new CleanedTag( '</sup>' )
-               ];
-               $this->assertTextCleaned( $expectedCleanedContent, 
$markedUpText );
+               $this->assertTextCleaned( [], $markedUpText );
        }
 
        public function testDontRemoveTagsWithoutCertainClass() {
                $markedUpText =
                        '<sup>I am not a reference.</sup><sup 
class="not-a-reference">Neither am I.</sup>';
                $expectedCleanedContent = [
-                       new CleanedTag( '<sup>' ),
                        new CleanedText( 'I am not a reference.' ),
-                       new CleanedTag( '</sup>' ),
-                       new CleanedTag( '<sup class="not-a-reference">' ),
-                       new CleanedText( 'Neither am I.' ),
-                       new CleanedTag( '</sup>' )
+                       new CleanedText( 'Neither am I.' )
                ];
                $this->assertTextCleaned( $expectedCleanedContent, 
$markedUpText );
        }
@@ -264,9 +243,7 @@
        public function testDontRemoveTagsWhoseCriteriaAreFalse() {
                $markedUpText = '<h2>Contents</h2>';
                $expectedCleanedContent = [
-                       new CleanedTag( '<h2>' ),
-                       new CleanedText( 'Contents' ),
-                       new CleanedTag( '</h2>' )
+                       new CleanedText( 'Contents' )
                ];
                $this->assertTextCleaned( $expectedCleanedContent, 
$markedUpText );
        }
@@ -274,23 +251,13 @@
        public function testHandleMultipleClasses() {
                $markedUpText =
                        '<sup class="reference another-class">Remove 
this.</sup>';
-               $expectedCleanedContent = [
-                       new CleanedTag( '<sup class="reference another-class">' 
),
-                       new CleanedTag( '</sup>' )
-               ];
-               $this->assertTextCleaned( $expectedCleanedContent, 
$markedUpText );
+               $this->assertTextCleaned( [], $markedUpText );
        }
 
        public function testCleanNestedTagsWhereSomeAreRemovedAndSomeAreKept() {
                $markedUpText = '<i><b>not removed</b><del>removed</del></i>';
                $expectedCleanedContent = [
-                       new CleanedTag( '<i>' ),
-                       new CleanedTag( '<b>' ),
-                       new CleanedText( 'not removed' ),
-                       new CleanedTag( '</b>' ),
-                       new CleanedTag( '<del>' ),
-                       new CleanedTag( '</del>' ),
-                       new CleanedTag( '</i>' )
+                       new CleanedText( 'not removed' )
                ];
                $this->assertTextCleaned( $expectedCleanedContent, 
$markedUpText );
        }
@@ -310,9 +277,7 @@
        public function testHandleNewlines() {
                $markedUpText = "<i>Keep this newline\n</i>";
                $expectedCleanedContent = [
-                       new CleanedTag( '<i>' ),
-                       new CleanedText( "Keep this newline\n" ),
-                       new CleanedTag( '</i>' )
+                       new CleanedText( "Keep this newline\n" )
                ];
                $this->assertTextCleaned( $expectedCleanedContent, 
$markedUpText );
        }
@@ -320,10 +285,7 @@
        public function testHandleEndTagFollowedByEmptyElementTag() {
                $markedUpText = '<i>content</i><br />';
                $expectedCleanedContent = [
-                       new CleanedTag( '<i>' ),
-                       new CleanedText( 'content' ),
-                       new CleanedTag( '</i>' ),
-                       new CleanedTag( '<br />' )
+                       new CleanedText( 'content' )
                ];
                $this->assertTextCleaned( $expectedCleanedContent, 
$markedUpText );
        }
@@ -331,10 +293,7 @@
        public function testHandleEmptyElementTagInsideElement() {
                $markedUpText = '<i>content<br /></i>';
                $expectedCleanedContent = [
-                       new CleanedTag( '<i>' ),
-                       new CleanedText( 'content' ),
-                       new CleanedTag( '<br />' ),
-                       new CleanedTag( '</i>' )
+                       new CleanedText( 'content' )
                ];
                $this->assertTextCleaned( $expectedCleanedContent, 
$markedUpText );
        }
@@ -342,14 +301,9 @@
        public function testGeneratePaths() {
                $markedUpText = '<i>level one<br /><b>level two</b></i>level 
zero';
                $expectedCleanedContent = [
-                       new CleanedTag( '<i>' ),
-                       new CleanedText( 'level one', [ 0, 0 ] ),
-                       new CleanedTag( '<br />' ),
-                       new CleanedTag( '<b>' ),
-                       new CleanedText( 'level two', [ 0, 2, 0 ] ),
-                       new CleanedTag( '</b>' ),
-                       new CleanedTag( '</i>' ),
-                       new CleanedText( 'level zero', [ 1 ] )
+                       new CleanedText( 'level one', './i/text()' ),
+                       new CleanedText( 'level two', './i/b/text()' ),
+                       new CleanedText( 'level zero', './text()' )
                ];
                $this->assertEquals(
                        $expectedCleanedContent,
@@ -360,12 +314,8 @@
        public function testGeneratePathsNestedOfSameType() {
                $markedUpText = '<i id="1">one<i id="2">two</i></i>';
                $expectedCleanedContent = [
-                       new CleanedTag( '<i id="1">' ),
-                       new CleanedText( 'one', [ 0, 0 ] ),
-                       new CleanedTag( '<i id="2">' ),
-                       new CleanedText( 'two', [ 0, 1, 0 ] ),
-                       new CleanedTag( '</i>' ),
-                       new CleanedTag( '</i>' )
+                       new CleanedText( 'one', './i/text()' ),
+                       new CleanedText( 'two', './i/i/text()' )
                ];
                $this->assertEquals(
                        $expectedCleanedContent,
@@ -373,69 +323,15 @@
                );
        }
 
-       public function testGetTags() {
-               $textWithTags = '<i>content</i>';
-               $expectedTags = [ [
-                       '<i>',
-                       '</i>'
-               ] ];
-               $this->assertEquals(
-                       $expectedTags,
-                       Util::call( 'Cleaner', 'getTags', $textWithTags )
-               );
-       }
-
-       public function testGetTagsEmptyElementTag() {
-               $textWithTags = '<br />';
-               $expectedTags = [ '<br />' ];
-               $this->assertEquals(
-                       $expectedTags,
-                       Util::call( 'Cleaner', 'getTags', $textWithTags )
-               );
-       }
-
-       public function testGetTagsEmptyElementTagWithoutSpace() {
-               $textWithTags = '<br/>';
-               $expectedTags = [ '<br/>' ];
-               $this->assertEquals(
-                       $expectedTags,
-                       Util::call( 'Cleaner', 'getTags', $textWithTags )
-               );
-       }
-
-       public function testGetTagsNestedTags() {
-               $textWithTags = '<i>content<b>content</b></i>';
-               $expectedTags = [
-                       [
-                               '<i>',
-                               '</i>'
-                       ],
-                       [
-                               '<b>',
-                               '</b>'
-                       ]
+       public function testGeneratePathsNodesOnSameLevel() {
+               $markedUpText = 'level zero<br />also level zero';
+               $expectedCleanedContent = [
+                       new CleanedText( 'level zero', './text()[1]' ),
+                       new CleanedText( 'also level zero', './text()[2]' )
                ];
                $this->assertEquals(
-                       $expectedTags,
-                       Util::call( 'Cleaner', 'getTags', $textWithTags )
-               );
-       }
-
-       public function testGetTagsNestedTagsOfSameType() {
-               $textWithTags = '<i id="1">content<i id="2">content</i></i>';
-               $expectedTags = [
-                       [
-                               '<i id="1">',
-                               '</i>'
-                       ],
-                       [
-                               '<i id="2">',
-                               '</i>'
-                       ]
-               ];
-               $this->assertEquals(
-                       $expectedTags,
-                       Util::call( 'Cleaner', 'getTags', $textWithTags )
+                       $expectedCleanedContent,
+                       Cleaner::cleanHtml( $markedUpText )
                );
        }
 }
diff --git a/tests/phpunit/HtmlGeneratorTest.php 
b/tests/phpunit/HtmlGeneratorTest.php
index c2d6a97..1008454 100644
--- a/tests/phpunit/HtmlGeneratorTest.php
+++ b/tests/phpunit/HtmlGeneratorTest.php
@@ -14,7 +14,7 @@
                $segment = [
                        'startOffset' => 0,
                        'endOffset' => 12,
-                       'content' => [ new CleanedText( 'An utterance.', [ 0, 1 
] ) ],
+                       'content' => [ new CleanedText( 'An utterance.', 
'/p/text()' ) ]
                ];
                $utteranceElement = Util::call(
                        'HtmlGenerator',
@@ -42,7 +42,10 @@
                $textElement = $xpath->query( '/utterance/content/text' 
)->item( 0 );
                $this->assertEquals( 'text', $textElement->nodeName );
                $this->assertEquals( 'An utterance.', $textElement->nodeValue );
-               $this->assertEquals( '0,1', $textElement->getAttribute( 'path' 
) );
+               $this->assertEquals(
+                       '/p/text()',
+                       $textElement->getAttribute( 'path' )
+               );
        }
 
        /**
@@ -63,7 +66,7 @@
                $segment = [
                        'startOffset' => null,
                        'endOffset' => null,
-                       'content' => [ new CleanedText( 'This is #1.', [] ) ]
+                       'content' => [ new CleanedText( 'This is #1.' ) ]
                ];
                $utteranceElement = Util::call(
                        'HtmlGenerator',
@@ -81,7 +84,7 @@
                $segment = [
                        'startOffset' => null,
                        'endOffset' => null,
-                       'content' => [ new CleanedText( 'This is not really a 
<tag>.', [] ) ]
+                       'content' => [ new CleanedText( 'This is not really a 
<tag>.' ) ]
                ];
                $utteranceElement = Util::call(
                        'HtmlGenerator',
@@ -114,12 +117,12 @@
                        [
                                'startOffset' => null,
                                'endOffset' => null,
-                               'content' => [ new CleanedText( 'Sentence 1.', 
[] ) ]
+                               'content' => [ new CleanedText( 'Sentence 1.' ) 
]
                        ],
                        [
                                'startOffset' => null,
                                'endOffset' => null,
-                               'content' => [ new CleanedText( ' Sentence 2.', 
[] ) ]
+                               'content' => [ new CleanedText( ' Sentence 2.' 
) ]
                        ]
                ];
                $utterancesElement = Util::call(
@@ -132,53 +135,5 @@
                        $utterancesElement->getElementsByTagName( 'utterance' );
                $this->assertEquals( 2, $utteranceElements->length );
                $this->assertTrue( $utterancesElement->hasAttribute( 'hidden' ) 
);
-       }
-
-       public function testCreateUtterancesContainingCleanedTags() {
-               $segment = [
-                       'startOffset' => null,
-                       'endOffset' => null,
-                       'content' => [
-                               new CleanedText( 'Here is a ', [] ),
-                               new CleanedTag( '<i>' ),
-                               new CleanedText( 'tag', [] ),
-                               new CleanedTag( '</i>', [] )
-                       ]
-               ];
-               $utteranceElement = Util::call(
-                       'HtmlGenerator',
-                       'createUtteranceElement',
-                       new DOMDocument(),
-                       $segment,
-                       0
-               );
-               $xpath = self::getXpath( $utteranceElement );
-               $contentChildren = $xpath->evaluate( '/utterance/content/*' );
-               $this->assertEquals( 'text', $contentChildren->item( 0 
)->nodeName );
-               $this->assertEquals(
-                       'Here is a ',
-                       $contentChildren->item( 0 )->nodeValue
-               );
-               $this->assertEquals(
-                       'cleaned-tag',
-                       $contentChildren->item( 1 )->nodeName
-               );
-               $this->assertEquals(
-                       'i',
-                       $contentChildren->item( 1 )->nodeValue
-               );
-               $this->assertEquals( 'text', $contentChildren->item( 2 
)->nodeName );
-               $this->assertEquals(
-                       'tag',
-                       $contentChildren->item( 2 )->nodeValue
-               );
-               $this->assertEquals(
-                       'cleaned-tag',
-                       $contentChildren->item( 3 )->nodeName
-               );
-               $this->assertEquals(
-                       '/i',
-                       $contentChildren->item( 3 )->nodeValue
-               );
        }
 }
diff --git a/tests/phpunit/SegmenterTest.php b/tests/phpunit/SegmenterTest.php
index 3496794..09800d0 100644
--- a/tests/phpunit/SegmenterTest.php
+++ b/tests/phpunit/SegmenterTest.php
@@ -78,44 +78,6 @@
                $this->assertEquals( 27, $segments[1]['endOffset'] );
        }
 
-       public function testSegmentContainingTag() {
-               $cleanedContent = [
-                       new CleanedText( 'Sentence with a ' ),
-                       new CleanedTag( '<i>' ),
-                       new CleanedText( 'tag' ),
-                       new CleanedTag( '</i>' ),
-                       new CleanedText( '.' )
-               ];
-               $expectedContent = [
-                       new CleanedText( 'Sentence with a ' ),
-                       new CleanedTag( '<i>' ),
-                       new CleanedText( 'tag' ),
-                       new CleanedTag( '</i>' ),
-                       new CleanedText( '.' )
-               ];
-               $segments = Segmenter::segmentSentences( $cleanedContent );
-               $this->assertEquals(
-                       $expectedContent,
-                       $segments[0]['content']
-               );
-       }
-
-       public function testSegmentEndingWithTag() {
-               $cleanedContent = [
-                       new CleanedText( "There's a tag after this" ),
-                       new CleanedTag( '<br />' )
-               ];
-               $expectedContent = [
-                       new CleanedText( "There's a tag after this" ),
-                       new CleanedTag( '<br />' )
-               ];
-               $segments = Segmenter::segmentSentences( $cleanedContent );
-               $this->assertEquals(
-                       $expectedContent,
-                       $segments[0]['content']
-               );
-       }
-
        public function testTextOffset() {
                $cleanedContent = [ new CleanedText( 'Sentence.' ) ];
                $segments = Segmenter::segmentSentences( $cleanedContent );
@@ -130,53 +92,5 @@
                $this->assertEquals( 13, $segments[0]['endOffset'] );
                $this->assertEquals( 13, $segments[1]['startOffset'] );
                $this->assertEquals( 27, $segments[1]['endOffset'] );
-       }
-
-       public function testTextOffsetWithTags() {
-               $cleanedContent = [
-                       new CleanedText( 'Sentence ' ),
-                       new CleanedTag( '<i>' ),
-                       new CleanedText( 'with' ),
-                       new CleanedTag( '</i>' ),
-                       new CleanedText( ' a tag.' )
-               ];
-               $segments = Segmenter::segmentSentences( $cleanedContent );
-               $this->assertEquals( 0, $segments[0]['startOffset'] );
-               $this->assertEquals( 7, $segments[0]['endOffset'] );
-       }
-
-       public function testTextOffsetAfterTags() {
-               $cleanedContent = [
-                       new CleanedText( 'Sentence one.' ),
-                       new CleanedTag( '<br />' ),
-                       new CleanedText( ' Sentence two.' )
-               ];
-               $segments = Segmenter::segmentSentences( $cleanedContent );
-               $this->assertEquals(
-                       [
-                               new CleanedTag( '<br />' ),
-                               new CleanedText( ' Sentence two.' )
-                       ],
-                       $segments[1]['content'] );
-               $this->assertEquals( 0, $segments[1]['startOffset'] );
-               $this->assertEquals( 14, $segments[1]['endOffset'] );
-       }
-
-       public function testStartTextOffsetWhenTagInSegment() {
-               $cleanedContent = [
-                       new CleanedText( 'Sentence one. Sentence' ),
-                       new CleanedTag( '<br />' ),
-                       new CleanedText( 'two.' )
-               ];
-               $segments = Segmenter::segmentSentences( $cleanedContent );
-               $this->assertEquals(
-                       [
-                               new CleanedText( ' Sentence' ),
-                               new CleanedTag( '<br />' ),
-                               new CleanedText( 'two.' )
-                       ],
-                       $segments[1]['content'] );
-               $this->assertEquals( 13, $segments[1]['startOffset'] );
-               $this->assertEquals( 4, $segments[1]['endOffset'] );
        }
 }
diff --git a/tests/qunit/ext.wikispeech.test.js 
b/tests/qunit/ext.wikispeech.test.js
index 3a2c881..45d1458 100644
--- a/tests/qunit/ext.wikispeech.test.js
+++ b/tests/qunit/ext.wikispeech.test.js
@@ -538,7 +538,7 @@
 
                assert.expect( 1 );
                $( '<content></content>' ).html(
-                       '<text>Utterance with 
</text><cleaned-tag>b</cleaned-tag><text>tag</text><cleaned-tag>/b</cleaned-tag><text>.</text>'
+                       '<text>Utterance with 
</text><text>tag</text><text>.</text>'
                )
                        .appendTo( $( '#utterance-0' ) );
                tokens = [
@@ -579,7 +579,7 @@
 
                assert.expect( 1 );
                $( '<content></content>' ).html(
-                       '<text>Utterance with 
</text><cleaned-tag>del</cleaned-tag><text>removed 
tag</text><cleaned-tag>/del</cleaned-tag><text>.</text>'
+                       '<text>Utterance with </text><text>removed 
tag</text><text>.</text>'
                )
                        .appendTo( $( '#utterance-0' ) );
                tokens = [
@@ -835,7 +835,7 @@
        } );
 
        QUnit.test( 'highlightUtterance()', function ( assert ) {
-               assert.expect( 2 );
+               assert.expect( 1 );
                $( '#qunit-fixture' ).append(
                        $( '<div></div>' )
                                .attr( 'id', 'mw-content-text' )
@@ -845,7 +845,7 @@
                        $( '<content></content>' )
                                .append( $( '<text></text>' )
                                        .text( 'An utterance.' )
-                                       .attr( 'path', '0' )
+                                       .attr( 'path', './text()' )
                                )
                );
                $( '#utterance-0' ).attr( 'start-offset', 0 );
@@ -856,10 +856,6 @@
                assert.strictEqual(
                        $( '#mw-content-text' ).prop( 'innerHTML' ),
                        '<span class="ext-wikispeech-highlight-sentence">An 
utterance.</span>'
-               );
-               assert.strictEqual(
-                       $( '#mw-content-text' ).prop( 'utterance' ),
-                       $( 'utterance-0' ).get( 0 )
                );
        } );
 
@@ -874,7 +870,7 @@
                        $( '<content></content>' )
                                .append( $( '<text></text>' )
                                        .text( ' Utterance two.' )
-                                       .attr( 'path', '0' )
+                                       .attr( 'path', './text()' )
                                )
                );
                $( '#utterance-0' ).attr( 'start-offset', 14 );
@@ -897,23 +893,20 @@
                );
                $( '#utterance-0' ).append(
                        $( '<content></content>' )
-                               .append( $( '<text></text>' )
-                                       .text( 'Utterance with ' )
-                                       .attr( 'path', '0,0' )
+                               .append(
+                                       $( '<text></text>' )
+                                               .text( 'Utterance with ' )
+                                               .attr( 'path', './p/text()[1]' )
                                )
-                               .append( $( '<cleaned-tag></cleaned-tag>' )
-                                       .text( 'b' )
+                               .append(
+                                       $( '<text></text>' )
+                                               .text( 'a' )
+                                               .attr( 'path', './p/b/text()' )
                                )
-                               .append( $( '<text></text>' )
-                                       .text( 'a' )
-                                       .attr( 'path', '0,1,0' )
-                               )
-                               .append( $( '<cleaned-tag></cleaned-tag>' )
-                                       .text( 'b' )
-                               )
-                               .append( $( '<text></text>' )
-                                       .text( ' tag.' )
-                                       .attr( 'path', '0,2' )
+                               .append(
+                                       $( '<text></text>' )
+                                               .text( ' tag.' )
+                                               .attr( 'path', './p/text()[2]' )
                                )
                );
                $( '#utterance-0' ).attr( 'start-offset', 0 );
@@ -924,6 +917,42 @@
                assert.strictEqual(
                        $( '#mw-content-text' ).prop( 'innerHTML' ),
                        '<p><span 
class="ext-wikispeech-highlight-sentence">Utterance with </span><b><span 
class="ext-wikispeech-highlight-sentence">a</span></b><span 
class="ext-wikispeech-highlight-sentence"> tag.</span></p>'
+               );
+       } );
+
+       QUnit.test( 'highlightUtterance(): wrap middle text nodes properly', 
function ( assert ) {
+               assert.expect( 1 );
+               $( '#qunit-fixture' ).append(
+                       $( '<div></div>' )
+                               .attr( 'id', 'mw-content-text' )
+                               .html( 'First<br />middle<br />last. Next 
utterance.' )
+               );
+               $( '#utterance-0' ).append(
+                       $( '<content></content>' )
+                               .append(
+                                       $( '<text></text>' )
+                                               .text( 'First' )
+                                               .attr( 'path', './text()[1]' )
+                               )
+                               .append(
+                                       $( '<text></text>' )
+                                               .text( 'middle' )
+                                               .attr( 'path', './text()[2]' )
+                               )
+                               .append(
+                                       $( '<text></text>' )
+                                               .text( 'last.' )
+                                               .attr( 'path', './text()[3]' )
+                               )
+               );
+               $( '#utterance-0' ).attr( 'start-offset', 0 );
+               $( '#utterance-0' ).attr( 'end-offset', 4 );
+
+               wikispeech.highlightUtterance( $( '#utterance-0' ) );
+
+               assert.strictEqual(
+                       $( '#mw-content-text' ).prop( 'innerHTML' ),
+                       '<span 
class="ext-wikispeech-highlight-sentence">First</span><br><span 
class="ext-wikispeech-highlight-sentence">middle</span><br><span 
class="ext-wikispeech-highlight-sentence">last.</span> Next utterance.'
                );
        } );
 
@@ -954,17 +983,6 @@
                                .attr( 'id', 'mw-content-text' )
                                .html( 'prefix <span 
class="ext-wikispeech-highlight-sentence">An utterance.</span> suffix' )
                );
-               $( '.ext-wikispeech-highlight-sentence' )
-                       .prop( 'utterance', $( '#utterance-0' ).get( 0 ) );
-               $( '#utterance-0' ).append(
-                       $( '<content></content>' )
-                               .append( $( '<text></text>' )
-                                       .text( 'An utterance.' )
-                                       .attr( 'path', '0' )
-                               )
-               );
-               $( '#utterance-0' ).attr( 'start-offset', 7 );
-               $( '#utterance-0' ).attr( 'end-offset', 19 );
 
                wikispeech.unhighlightUtterances();
 
@@ -972,11 +990,11 @@
                        $( '#mw-content-text' ).prop( 'innerHTML' ),
                        'prefix An utterance. suffix'
                );
-               assert.strictEqual( $( '#mw-content-text' ).contents().length, 
1 );
                assert.strictEqual(
                        $( '.ext-wikispeech-highlight-sentence' 
).contents().length,
                        0
                );
+               assert.strictEqual( $( '#mw-content-text' ).contents().length, 
1 );
        } );
 
        QUnit.test( 'unhighlightUtterance(): multiple highlights', function ( 
assert ) {

-- 
To view, visit https://gerrit.wikimedia.org/r/342024
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ifec50d6d22a9e3e6adf51d96d315237852aa11ac
Gerrit-PatchSet: 5
Gerrit-Project: mediawiki/extensions/Wikispeech
Gerrit-Branch: master
Gerrit-Owner: Sebastian Berlin (WMSE) <[email protected]>
Gerrit-Reviewer: Lokal Profil <[email protected]>
Gerrit-Reviewer: Sebastian Berlin (WMSE) <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to