Sebastian Berlin (WMSE) has uploaded a new change for review. (
https://gerrit.wikimedia.org/r/336816 )
Change subject: Highlight recited sentence
......................................................................
Highlight recited sentence
Added highlighting to the sentence that is currently being recited,
using span elements. The highlighting visuals can be changed in
modules/ext.wikispeech.css. All text nodes that were used to generate
the utterance is wrapped in spans, except for the first and
last. These only get the ranges used as part of the utterance wrapped.
A `path` attribute was introduced to make the connection from an
utterance to the text nodes that were used to generate it. In doing
this, it was discovered that the position attributes, that were
previously added to both utterances and tokens, aren't used in this
solution. They were therefore removed.
Tests were changed a bit to have less overlap in what's being tested
and to make them easier to work with.
Bug: T148622
Change-Id: I12c8873265b7236c0677b25ce089bbb887ffdd9c
---
A includes/CleanedContent.php
D includes/CleanedTag.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/phpunit/Util.php
M tests/qunit/ext.wikispeech.test.js
12 files changed, 962 insertions(+), 629 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Wikispeech
refs/changes/16/336816/1
diff --git a/includes/CleanedContent.php b/includes/CleanedContent.php
new file mode 100644
index 0000000..4d123b3
--- /dev/null
+++ b/includes/CleanedContent.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * @ingroup Extensions
+ * @license GPL-2.0+
+ */
+
+abstract class CleanedContent {
+ /**
+ * 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.
+ *
+ * @var string $string
+ */
+
+ public $string;
+
+ function __construct( $string ) {
+ $this->string = $string;
+ }
+}
+
+class CleanedTag extends CleanedContent {
+}
+
+class Text extends CleanedContent {
+ public $path;
+
+ function __construct( $string, $path=[] ) {
+ parent::__construct( $string );
+ $this->path = $path;
+ }
+
+ /**
+ * Create a Text element from the content.
+ *
+ * Path is converted to a string and added as an attribute.
+ *
+ * @since 0.0.1
+ * @param DOMDocument $dom The DOM Document used to create the element.
+ * @return DOMElement The length of the tag string.
+ */
+
+ function toElement( $dom ) {
+ $element = $dom->createElement( 'text', $this->string );
+ $pathString = implode( $this->path, ',' );
+ $element->setAttribute( 'path', $pathString );
+ return $element;
+ }
+}
diff --git a/includes/CleanedTag.php b/includes/CleanedTag.php
deleted file mode 100644
index 697032c..0000000
--- a/includes/CleanedTag.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-/**
- * @file
- * @ingroup Extensions
- * @license GPL-2.0+
- */
-
-abstract class CleanedTag {
-
- /**
- * The string representation of the tag, as it is written in the
- * HTML. This includes the tag name, any attributes, and the
- * brackets.
- *
- * @var string $tagString
- */
-
- public $tagString;
-
- function __construct( $tagString ) {
- $this->tagString = $tagString;
- }
-
- /**
- * Get the length of the tag string.
- *
- * @since 0.0.1
- * @return int The length of the tag string.
- */
-
- function getLength() {
- return strlen( $this->tagString );
- }
-}
-
-class CleanedStartTag extends CleanedTag {
-
- /**
- * The length of the element content, i.e. the string delimited by
- * this start tag and the corresponding end tag.
- *
- * @var int $contentLength
- */
-
- public $contentLength;
-
- function __construct( $tagString ) {
- parent::__construct( $tagString );
- $this->contentLength = 0;
- }
-
- /**
- * Get the length of the tag string.
- *
- * @since 0.0.1
- * @return int The length of the tag string, including element content.
- */
-
- function getLength() {
- $length = strlen( $this->tagString );
- if ( $this->contentLength ) {
- $length += $this->contentLength;
- }
- return $length;
- }
-}
-
-class CleanedEndTag extends CleanedTag {
-}
-
-class CleanedEmptyElementTag extends CleanedTag {
-}
diff --git a/includes/Cleaner.php b/includes/Cleaner.php
index e7b4a1f..dc547f6 100644
--- a/includes/Cleaner.php
+++ b/includes/Cleaner.php
@@ -6,7 +6,7 @@
* @license GPL-2.0+
*/
-require_once 'CleanedTag.php';
+require_once 'CleanedContent.php';
class Cleaner {
@@ -35,7 +35,8 @@
$dom->documentElement->firstChild,
$markedUpText,
$tags,
- $tagIndex
+ $tagIndex,
+ []
);
return $cleanedContent;
}
@@ -73,14 +74,12 @@
* 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 an array
- * containing the tag string and the start position of the
- * tag. This array is then added to another array, which holds a
- * start-end pair of tags.
+ * 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 an array
- * containing the tag string and the end position of the tag. This
- * array is then added to the array containing the corresponding
+ * 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.
@@ -117,10 +116,7 @@
$bracketPosition = $startBracket[1];
$startBracket = null;
if ( self::isStartTag( $tagString ) ) {
- array_push( $tags, [ [
- 'string' => $tagString,
- 'position' => $bracketPosition
- ] ] );
+ array_push( $tags, [ $tagString ] );
} elseif ( self::isEndTag( $tagString ) ) {
$startTagIndex =
self::getCorrespondingStartTagIndex(
$tags,
@@ -130,10 +126,7 @@
// start tag.
array_push(
$tags[$startTagIndex],
- [
- 'string' => $tagString,
- 'position' =>
$bracketPosition
- ]
+ $tagString
);
} elseif ( self::isEmptyElementTag( $tagString
) ) {
array_push( $tags, $tagString );
@@ -203,7 +196,7 @@
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]['string'] );
+ $startTagType = self::getTagName( $tag[0] );
$endTagType = self::getTagName( $tagString );
if ( $startTagType == $endTagType ) {
return $i;
@@ -243,6 +236,9 @@
* 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 Contains indices of each ancestor node to
+ * $node. Used for connecting a Text element to a text node in
+ * the HTML.
*/
private static function addContent(
@@ -250,7 +246,8 @@
$node,
$source,
$tags,
- &$tagIndex
+ &$tagIndex,
+ $path
) {
$startTagArray = null;
$endTagArray = null;
@@ -263,7 +260,7 @@
$endTagArray = $tags[$tagIndex][1];
$cleanedStartTag = self::addStartTag(
$content,
- $startTagArray['string']
+ $startTagArray
);
} else {
// If the tag is empty, just add it and return,
since
@@ -277,24 +274,20 @@
// When a tag is removed, skip forward a number of tags
// equal to the number of nodes under that tag.
$tagIndex += self::getNumberOfDescendants( $node );
- $cleanedStartTag->contentLength =
self::getContentLength(
- $startTagArray,
- $endTagArray,
- $source
- );
} else {
self::addChildren(
$content,
$node,
$source,
$tags,
- $tagIndex
+ $tagIndex,
+ $path
);
}
if ( $endTagArray != null ) {
array_push(
$content,
- new CleanedEndTag( $endTagArray['string'] )
+ new CleanedTag( $endTagArray )
);
}
}
@@ -310,7 +303,7 @@
*/
private static function addStartTag( &$content, $tagString ) {
- $cleanedStartTag = new CleanedStartTag( $tagString );
+ $cleanedStartTag = new CleanedTag( $tagString );
array_push( $content, $cleanedStartTag );
return $cleanedStartTag;
}
@@ -325,7 +318,7 @@
*/
private static function addEmptyElementTag( &$content, $tagString ) {
- $cleanedTag = new CleanedEmptyElementTag( $tagString );
+ $cleanedTag = new CleanedTag( $tagString );
array_push( $content, $cleanedTag );
}
@@ -352,34 +345,6 @@
}
/**
- * Get the length of the element content.
- *
- * The element content is the string between the start tag and the
- * end tag, excluding the tags themselves.
- *
- * @since 0.0.1
- * @param array $startTagArray Array containing string and
- * position for the start tag.
- * @param array $endTagArray Array containing string and
- * position for the end tag.
- * @param string $source The HTML string that DOM is generated
- * from. Used for retrieveing element contents.
- */
-
- private static function getContentLength(
- $startTagArray,
- $endTagArray,
- $source
- ) {
- $elementContentStartPosition =
- $startTagArray['position'] + strlen(
$startTagArray['string'] );
- $length = $endTagArray['position'] -
$elementContentStartPosition;
- $elementContentString =
- substr( $source, $elementContentStartPosition, $length
);
- return strlen( $elementContentString );
- }
-
- /**
* Add content for child nodes to an array.
*
* @since 0.0.1
@@ -389,6 +354,9 @@
* 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 Contains indices of each ancestor node to
+ * $node. Used for connecting a Text element to a text node in
+ * the HTML.
*/
private static function addChildren(
@@ -396,11 +364,16 @@
$node,
$source,
$tags,
- &$tagIndex
+ &$tagIndex,
+ $path
) {
+ $childIndex = 0;
foreach ( $node->childNodes as $child ) {
+ $childPath = $path;
+ array_push( $childPath, $childIndex );
if ( $child->nodeType == XML_TEXT_NODE ) {
- array_push( $content, $child->textContent );
+ $text = new Text( $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
@@ -411,9 +384,11 @@
$child,
$source,
$tags,
- $tagIndex
+ $tagIndex,
+ $childPath
);
}
+ $childIndex ++;
}
}
diff --git a/includes/HtmlGenerator.php b/includes/HtmlGenerator.php
index 8278fd0..d84e0b6 100644
--- a/includes/HtmlGenerator.php
+++ b/includes/HtmlGenerator.php
@@ -6,7 +6,7 @@
* @license GPL-2.0+
*/
-require_once 'CleanedTag.php';
+require_once 'CleanedContent.php';
class HtmlGenerator {
@@ -21,11 +21,29 @@
*/
public static function createUtterancesHtml( $segments ) {
+ $dom = new DOMDocument();
+ $utterancesElement = self::createUtterancesElement( $segments,
$dom );
+ $utterancesHtml = $dom->saveHTML( $utterancesElement );
+ return $utterancesHtml;
+ }
+
+ /**
+ * Create an utterances element.
+ *
+ * The element is populated with utterance elements.
+ *
+ * @since 0.0.1
+ * @param array $segments Array of segments to generate utterances
+ * from.
+ * @param DOMDocument $dom The DOM Document used to create the element.
+ * @return DOMElement The utterances element.
+ */
+
+ private static function createUtterancesElement( $segments, $dom ) {
if ( count( $segments ) ) {
- $dom = new DOMDocument();
- $utterancesNode = $dom->createElement( 'utterances' );
+ $utterancesElement = $dom->createElement( 'utterances'
);
// Hide the content of the utterance elements.
- $utterancesNode->setAttribute( 'hidden', '' );
+ $utterancesElement->setAttribute( 'hidden', '' );
$index = 0;
foreach ( $segments as $segment ) {
$utteranceElement =
self::createUtteranceElement(
@@ -33,11 +51,10 @@
$segment,
$index
);
- $utterancesNode->appendChild( $utteranceElement
);
+ $utterancesElement->appendChild(
$utteranceElement );
$index += 1;
}
- $utternacesHtml = $dom->saveHTML( $utterancesNode );
- return $utternacesHtml;
+ return $utterancesElement;
}
}
@@ -75,8 +92,12 @@
$utteranceElement = $dom->createElement( 'utterance' );
$utteranceElement->setAttribute( 'id', "utterance-$index" );
$utteranceElement->setAttribute(
- 'position',
- $segment['position']
+ 'start-offset',
+ $segment['startOffset']
+ );
+ $utteranceElement->setAttribute(
+ 'end-offset',
+ $segment['endOffset']
);
$contentElement = self::createContentElement(
$dom,
@@ -103,17 +124,11 @@
if ( $part instanceof CleanedTag ) {
// Remove the < and > from the tag string to
not have to
// decode them later.
- $text = substr( $part->tagString, 1, -1 );
+ $text = substr( $part->string, 1, -1 );
$cleanedTagElement = $dom->createElement(
'cleaned-tag', $text );
- if (
- $part instanceof CleanedStartTag &&
- $part->contentLength
- ) {
- $cleanedTagElement->setAttribute(
'removed', $part->contentLength );
- }
$contentElement->appendChild(
$cleanedTagElement );
} else {
- $contentElement->appendChild(
$dom->createTextNode( $part ) );
+ $contentElement->appendChild( $part->toElement(
$dom ) );
}
}
return $contentElement;
diff --git a/includes/Segmenter.php b/includes/Segmenter.php
index a1fea80..fe4eab6 100644
--- a/includes/Segmenter.php
+++ b/includes/Segmenter.php
@@ -6,33 +6,37 @@
* @license GPL-2.0+
*/
-require_once 'CleanedTag.php';
+require_once 'CleanedContent.php';
class Segmenter {
/**
* Divide a cleaned content array into segments, one for each sentence.
*
- * A segment is an array with the keys "content" and "position".
Content is
- * an array of CleanedTags and strings. Position is the start
- * position, in the HTML, for the first node in content, i.e. the start
- * position of the segment.
+ * A segment is an array with the keys "contents", "startOffsets"
+ * and "endOffset". "contents" is an array of `CleanedContent`s.
+
+ * "startOffset" is the position of the first character of the
+ * segment, within the text node it appears. "endOffset" is the
+ * position of the last character of the segment, within the text
+ * node it appears. These are used to determine start and end of a
+ * segment in the original HTML.
*
- * A sentence is here defined as a number of tokens ending with a dot
(full
- * stop). Headings are also considered sentences.
+ * A sentence is here defined as a number of tokens ending with a
+ * dot (full stop). Headings are also considered sentences.
*
* @since 0.0.1
- * @param array $cleanedContent An array of cleaned content, as
returned by
- * Cleaner::cleanHtml().
- * @return array An array of segments, each containing the nodes in that
- * segment and the start position in the HTML.
+ * @param array $cleanedContents An array of `CleanedContent`s, as
+ * returned by `Cleaner::cleanHtml()`.
+ * @return array An array of segments, each containing the
+ * `CleanedContent's in that segment.
*/
public static function segmentSentences( $cleanedContent ) {
$segments = [];
$currentSegment = [
- 'position' => 0,
- 'content' => []
+ 'content' => [],
+ 'startOffset' => 0
];
foreach ( $cleanedContent as $content ) {
if ( $content instanceof CleanedTag ) {
@@ -82,34 +86,59 @@
// Find the indices of all characters that may be sentence
final.
preg_match_all(
"/\./",
- $text,
+ $text->string,
$matches,
PREG_OFFSET_CAPTURE
);
$position = 0;
+ $textOffset = 0;
+ $containsText = false;
+ foreach ( $currentSegment['content'] as $content ) {
+ if ( is_a( $content, 'Text' ) ) {
+ $containsText = true;
+ break;
+ }
+ }
+ if ( !$containsText ) {
+ // If this is the first `Text` of the segment, reset the
+ // start offset.
+ $currentSegment['startOffset'] = 0;
+ }
foreach ( $matches[0] as $match ) {
$sentenceFinalPosition = $match[1];
- if ( self::isSentenceFinal( $text,
$sentenceFinalPosition ) ) {
+ if (
+ self::isSentenceFinal(
+ $text->string,
+ $sentenceFinalPosition
+ )
+ ) {
$length = $sentenceFinalPosition - $position +
1;
- $segmentText = substr( $text, $position,
$length );
+ $segmentText = substr( $text->string,
$position, $length );
if ( trim( $segmentText ) != '' ) {
// Don't add segments with only
whitespaces.
- array_push( $currentSegment['content'],
$segmentText );
+ $newText = new Text(
+ $segmentText,
+ $text->path
+ );
+ array_push( $currentSegment['content'],
$newText );
+ $textOffset += $sentenceFinalPosition -
$position;
+ $currentSegment['endOffset'] =
$textOffset;
+ $textOffset += 1;
$position = $sentenceFinalPosition + 1;
array_push( $segments, $currentSegment
);
- $nextSegmentPosition =
$currentSegment['position'] +
- self::getSegmentLength(
$currentSegment['content'] );
$currentSegment = [
- 'position' =>
$nextSegmentPosition,
- 'content' => []
+ 'content' => [],
+ 'startOffset' => $textOffset
];
}
}
}
- $remainder = substr( $text, $position );
+ $remainder = substr( $text->string, $position );
if ( $remainder ) {
// Add any remaining part of the string.
- array_push( $currentSegment['content'], $remainder );
+ $remainderText = new Text( $remainder, $text->path );
+ $currentSegment['endOffset'] = $textOffset + strlen(
$remainder );
+ array_push( $currentSegment['content'], $remainderText
);
}
}
@@ -160,25 +189,5 @@
private static function isUpper( $string ) {
return mb_strtoupper( $string, 'UTF-8' ) == $string;
- }
-
- /**
- * Calculate the length of a segment, as it is represented in HTML.
- *
- * @since 0.0.1
- * @param array $segment An array of nodes.
- * @return int The combinded length of the HTML of the nodes in
$segment.
- */
-
- private static function getSegmentLength( $segment ) {
- $length = 0;
- foreach ( $segment as $content ) {
- if ( $content instanceof CleanedTag ) {
- $length += $content->getLength();
- } else {
- $length += strlen( $content );
- }
- }
- return $length;
}
}
diff --git a/modules/ext.wikispeech.css b/modules/ext.wikispeech.css
index 18f6934..4b5bfb3 100644
--- a/modules/ext.wikispeech.css
+++ b/modules/ext.wikispeech.css
@@ -25,3 +25,8 @@
.ext-wikispeech-skip-back-word:after {
content: "Skip back word";
}
+
+.ext-wikispeech-highlight-sentence {
+ background-color: rgb(0, 200, 0);
+ background-color: rgba(0, 200, 0, 0.5);
+}
\ No newline at end of file
diff --git a/modules/ext.wikispeech.js b/modules/ext.wikispeech.js
index 759733d..fcdac36 100644
--- a/modules/ext.wikispeech.js
+++ b/modules/ext.wikispeech.js
@@ -102,6 +102,7 @@
self.stopUtterance( $currentUtterance );
$currentUtterance = $utterance;
$utterance.children( 'audio' ).trigger( 'play' );
+ self.highlightUtterance( $utterance );
};
/**
@@ -115,6 +116,151 @@
$utterance.children( 'audio' ).trigger( 'pause' );
// Rewind audio for next time it plays.
$utterance.children( 'audio' ).prop( 'currentTime', 0 );
+ self.unhighlightUtterances();
+ };
+
+ /**
+ * Remove any highlighting from utterances.
+ */
+
+ this.unhighlightUtterances = function () {
+ var parents, $span, i, parent;
+
+ parents = [];
+ $span = $( '.ext-wikispeech-highlight-sentence' );
+ $span.replaceWith( function () {
+ var textNode;
+
+ parents.push( this.parentNode );
+ textNode = this.firstChild;
+ return textNode.textContent;
+ } );
+ for ( i = 0; i < parents.length; i++ ) {
+ parent = parents[ i ];
+ // Merge text nodes, if the original was divided
+ // by adding the <span>.
+ parent.normalize();
+ }
+ };
+
+ /**
+ * Highlight text associated with an utterance.
+ *
+ * Adds highlight spans to the text nodes from which the
+ * tokens of $utterance were created. For first and last node,
+ * it's possible that only part of the text is highlighted,
+ * since they may contain start/end of next/previous
+ * utterance.
+ *
+ * @param {jQuery} $utterance The utterance to add
+ * highlighting to.
+ */
+
+ this.highlightUtterance = function ( $utterance ) {
+ var firstTextElement, firstNode, lastTextElement,
lastNode,
+ firstNodeRange, startOffset, endOffset,
lastNodeRange,
+ textNode;
+
+ 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 ) + 1;
+ if ( firstNode === lastNode ) {
+ // All highlighted text is in the same
text node, so
+ // only a range is needed.
+ firstNodeRange.setEnd( firstNode,
endOffset );
+ } else {
+ // Since the highlighting extends
beyond the first
+ // text node, all text from the start
position should
+ // be highlighted.
+ firstNodeRange.setEnd(
+ firstNode,
+ firstNode.textContent.length
+ );
+ // Range for last text node, since it
may be that the
+ // highlighting doesn't cover the whole
node.
+ lastNodeRange = document.createRange();
+ lastNodeRange.setStart( lastNode, 0 );
+ lastNodeRange.setEnd( lastNode,
endOffset );
+ lastNodeRange.surroundContents(
+
self.createHighilightUtteranceSpan()
+ );
+ $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(
+
self.createHighilightUtteranceSpan()
+ );
+ } );
+ }
+ 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.
+ *
+ * @param {HTMLElement} textElement The <text> element to find
+ * the text node for.
+ * @return {Node} The text node associated with textElement.
+ */
+
+ this.getNodeForTextElement = function ( textElement ) {
+ var path, node;
+
+ path = textElement.getAttribute( 'path' ).split( ',' );
+ node =
+ self.getNodeFromPath( path, $(
'#mw-content-text' ).get( 0 ) );
+ return node;
+ };
+
+ /**
+ * Get the node from a path.
+ *
+ * Starts at topNode and traverses the DOM tree along path.
+ *
+ * @param {int[]} path Indices of each step in the path.
+ * @param {Node} topNode The node to start from.
+ * @return {Node} The node found by following path.
+ */
+
+ this.getNodeFromPath = function ( path, topNode ) {
+ var node, i, step;
+
+ node = topNode;
+ for ( i = 0; i < path.length; i++ ) {
+ step = path[ i ];
+ node = $( node ).contents().get( parseInt(
step, 10 ) );
+ }
+ return node;
+ };
+
+ /**
+ * Create a span used for highlighting sentences.
+ *
+ * @return {HTMLElement} The highlighting <span>.
+ */
+
+ this.createHighilightUtteranceSpan = function () {
+ var span = $( '<span></span>' )
+ .addClass( 'ext-wikispeech-highlight-sentence' )
+ .get( 0 );
+ return span;
};
/**
@@ -488,7 +634,11 @@
function () {
// Filter text nodes. Not using
Node.TEXT_NODE to
// support IE7.
- return this.nodeType === 3;
+ if ( this.nodeType === 1 &&
this.tagName === 'TEXT' ) {
+ return true;
+ } else {
+ return false;
+ }
}
).text();
self.requestTts( text, function ( response ) {
@@ -558,39 +708,23 @@
*/
this.addTokenElements = function ( $utterance, tokens ) {
- var position, $tokensElement, $content, firstTokenIndex,
- removedLength;
+ var $tokensElement, $contentElement, firstTokenIndex;
- // The character position in the original HTML. Starting
- // at the position of the utterance, since that's the
- // earliest a child token can appear.
- position = parseInt( $utterance.attr( 'position' ), 10
);
$tokensElement = $( '<tokens></tokens>' ).appendTo(
$utterance );
- $content = $utterance.children( 'content' );
+ $contentElement = $utterance.children( 'content' );
firstTokenIndex = 0;
mw.log( 'Adding tokens to ' + $utterance.attr( 'id' ) +
'.' );
- $content.contents().each( function ( i, element ) {
- if ( element.tagName === 'CLEANED-TAG' ) {
- removedLength = element.getAttribute(
'removed' );
- if ( removedLength !== null ) {
- position += parseInt(
removedLength, 10 );
- }
- // Advance position two steps extra for
the < and
- // >, that were stripped from the tag
at an
- // earlier stage.
- position += 2;
- } else {
+ $contentElement.contents().each( function ( i, element
) {
+ if ( element.tagName === 'TEXT' ) {
// firstTokenIndex is the index, in
tokens, of the
// first token we haven't created an
element for.
firstTokenIndex =
self.addTokensForTextElement(
tokens,
element,
- position,
$tokensElement,
firstTokenIndex
);
}
- position += element.textContent.length;
} );
};
@@ -599,11 +733,10 @@
* of the given text element.
*
* Goes through textElement, finds substrings matching tokens
- * and creates token elements for these. The position for the
- * token elements is the substring position plus the position
- * of textElement. When a token can no longer be found, the
- * index of that token is returned to remember what to start
- * looking for in the next text element.
+ * and creates token elements for these. When a token can no
+ * longer be found, the index of that token is returned to
+ * remember what to start looking for in the next text
+ * element.
*
* @param {Object[]} tokens Tokens from a server response,
* where each token is an object. For these objects, the
@@ -611,9 +744,7 @@
* audio for the token.
* @param {HTMLElement} textElement The text element to match
* tokens against.
- * @param {number} startPosition The position of the original
- * text element.
- * @param {HTMLElement} $tokensElement Element which token
+ * @param {JQuery} $tokensElement Element which token
* elements are added to.
* @param {number} firstTokenIndex The index of the first
* token in tokens to search for.
@@ -624,29 +755,20 @@
this.addTokensForTextElement = function (
tokens,
textElement,
- startPosition,
$tokensElement,
firstTokenIndex
) {
- var positionInElement, matchingPosition,
tokenPositionInHtml,
- orthographicToken, i, token, startTime;
+ var orthographicToken, i, token, startTime;
- positionInElement = 0;
for ( i = firstTokenIndex; i < tokens.length; i++ ) {
token = tokens[ i ];
orthographicToken = token.orth;
- // Look for the token in the remaining string.
- matchingPosition =
- textElement.nodeValue.slice(
positionInElement )
- .indexOf( orthographicToken );
- if ( matchingPosition === -1 ) {
+ if ( !textElement.textContent.includes(
orthographicToken ) ) {
// The token wasn't found in this
element. Stop
// looking for more and return the
index of the
// token.
return i;
}
- tokenPositionInHtml = startPosition +
positionInElement +
- matchingPosition;
if ( i === 0 ) {
startTime = 0.0;
} else {
@@ -654,11 +776,9 @@
}
$( '<token></token>' )
.text( orthographicToken )
- .attr( 'position', tokenPositionInHtml )
.attr( 'start-time', startTime )
.attr( 'end-time', tokens[ i ].endtime )
.appendTo( $tokensElement );
- positionInElement += orthographicToken.length;
}
};
}
diff --git a/tests/phpunit/CleanerTest.php b/tests/phpunit/CleanerTest.php
index 94c67fc..82409d2 100644
--- a/tests/phpunit/CleanerTest.php
+++ b/tests/phpunit/CleanerTest.php
@@ -25,126 +25,164 @@
public function testCleanTags() {
$markedUpText = '<i>Element content</i>';
$expectedCleanedContent = [
- Util::createStartTag( '<i>' ),
- 'Element content',
- new CleanedEndTag( '</i>' )
+ new CleanedTag( '<i>' ),
+ new Text( 'Element content' ),
+ new CleanedTag( '</i>' )
];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
/**
- * Run several tests to ensure that the cleaning functions neither do
- * more nor less than they should. This includes: the tested string;
- * the tested string preceded and followed by strings, that should not
- * be altered; the tested string twice in a row, joined by a string
- * that should not be altered; a string that contains no markup, that
- * should not be altered.
+ * Assert cleaning doesn't do more or less than it should.
+ *
+ * Runs several tests to ensure that the cleaning functions
+ * neither do more nor less than they should. This includes: the
+ * tested string; the tested string preceded and followed by
+ * strings, that should not be altered; the tested string twice in
+ * a row, joined by a string that should not be altered.
*
* @since 0.0.1
- * @param array $expectedCleanedContent The content that is the expected
- * output.
+ * @param array $expectedCleanedContents The contents that is the
+ * expected output.
* @param string $markedUpText The string that contains the markup
* that should be cleaned
*/
private function assertTextCleaned(
- $expectedCleanedContent,
+ $expectedCleanedContents,
$markedUpText
) {
- $this->assertEquals(
- $expectedCleanedContent,
+ $this->assertContentsEqual(
+ $expectedCleanedContents,
Cleaner::cleanHtml( $markedUpText )
);
$this->assertWithPrefixAndSuffix(
- $expectedCleanedContent,
+ $expectedCleanedContents,
$markedUpText
);
$this->assertWithInfix(
- $expectedCleanedContent,
+ $expectedCleanedContents,
$markedUpText
);
}
/**
- * Make sure that the correct content is given when preceded and
- * followed by text.
+ * Assert two arrays of `CleanedContent`s have matching strings.
+ *
+ * Checking only the strings makes it more convenient to write
+ * tests where other variables aren't relevant.
+ *
+ * @since 0.0.1
+ * @param array $expectedCleanedContents The content array that is
+ * the expected output.
+ * @param array $cleanedContents The content array to test.
+ */
+
+ private function assertContentsEqual(
+ $expectedCleanedContents,
+ $cleanedContents
+ ) {
+ // This is needed to not test path too. Looping over the
+ // contents and asserting only the string variable is not
+ // possible, as it gives warning:
+ // Generic.CodeAnalysis.ForLoopWithTestFunctionCall.NotAllowed.
+ foreach ( $cleanedContents as $cleanedContent ) {
+ $cleanedContent->path = null;
+ }
+ foreach ( $expectedCleanedContents as $expectedCleanedContent )
{
+ $expectedCleanedContent->path = null;
+ }
+ $this->assertEquals(
+ $expectedCleanedContents,
+ $cleanedContents
+ );
+ }
+
+ /**
+ * 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
- * strings. If they are CleanedTags, they are added as new parts.
+ * `Text`s. If they are `CleanedTag`s, they are added as new
+ * parts.
*
* @since 0.0.1
- * @param array $expectedCleanedContent The content that is the expected
- * output, excluding pre- and suffix.
+ * @param array $expectedCleanedContent The contents that is the
+ * expected output, excluding pre- and suffix.
* @param string $markedUpText The string that contains the markup
* that should be cleaned
*/
private function assertWithPrefixAndSuffix(
- $expectedCleanedContent,
+ $expectedCleanedContents,
$markedUpText
) {
- if ( is_string( $expectedCleanedContent[0] ) ) {
- $expectedCleanedContent[0] = 'prefix' .
$expectedCleanedContent[0];
+ if ( is_a( $expectedCleanedContents[0], 'Text' ) ) {
+ $expectedCleanedContents[0] =
+ new Text( 'prefix' .
$expectedCleanedContents[0]->string );
} else {
- array_unshift( $expectedCleanedContent, 'prefix' );
+ array_unshift( $expectedCleanedContents, new Text(
'prefix' ) );
}
- $lastIndex = count( $expectedCleanedContent ) - 1;
- if ( is_string( $expectedCleanedContent[$lastIndex] ) ) {
- $expectedCleanedContent[$lastIndex] .= 'suffix';
+ $lastIndex = count( $expectedCleanedContents ) - 1;
+ if ( is_a( $expectedCleanedContents[$lastIndex], 'Text' ) ) {
+ $expectedCleanedContents[$lastIndex] =
+ new Text(
+
$expectedCleanedContents[$lastIndex]->string . 'suffix'
+ );
} else {
- array_push( $expectedCleanedContent, 'suffix' );
+ array_push( $expectedCleanedContents, new Text(
'suffix' ) );
}
- $this->assertEquals(
- $expectedCleanedContent,
+ $this->assertContentsEqual(
+ $expectedCleanedContents,
Cleaner::cleanHtml( 'prefix' . $markedUpText . 'suffix'
)
);
}
/**
- * Make sure that the correct content is given when the marked up
- * text is repeated, with text in between.
+ * Assert correct output when input is repeated and separated by string.
*
* If the first instance of the expected content end with a
- * string, the infix is added after that. If the second instance
- * starts with a string, the infix is added before that. If both
- * cases occur at the same time, the string between the instances
- * will consist of the last string of first instance, infix and
- * first string of second instance.
+ * `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.
*
* @since 0.0.1
- * @param array $expectedCleanedContent The content that will be
+ * @param array $expectedCleanedContents The contents that will be
* repeated to create the expected output.
* @param string $markedUpText The string that contains the markup
* that should be cleaned
*/
private function assertWithInfix(
- $expectedCleanedContent,
+ $expectedCleanedContents,
$markedUpText
) {
- $infix = 'infix';
- $firstContent = $expectedCleanedContent;
- if ( is_string( $firstContent[0] ) ) {
- $adjacent = array_pop( $firstContent );
- $infix = $adjacent . $infix;
+ $infix = new Text( 'infix' );
+ $firstContents = $expectedCleanedContents;
+ $lastIndex = count( $firstContents ) - 1;
+ if ( is_a( $firstContents[$lastIndex], 'Text' ) ) {
+ $adjacent = array_pop( $firstContents );
+ $infix->string = $adjacent->string . $infix->string;
}
- $secondContent = $expectedCleanedContent;
- $lastIndex = count( $secondContent ) - 1;
- if ( is_string( $expectedCleanedContent[$lastIndex] ) ) {
- $adjacent = array_shift( $secondContent );
- $infix .= $adjacent;
+ $secondContents = $expectedCleanedContents;
+ if ( is_a( $secondContents[0], 'Text' ) ) {
+ $adjacent = array_shift( $secondContents );
+ $infix->string .= $adjacent->string;
}
- $this->assertEquals(
- array_merge( $firstContent, [ $infix ], $secondContent
),
+ $this->assertContentsEqual(
+ array_merge( $firstContents, [ $infix ],
$secondContents ),
Cleaner::cleanHtml( $markedUpText . 'infix' .
$markedUpText )
);
}
- public function testDontAlterStringWithoutMarkup() {
+ public function testDontAlterStringsWithoutMarkup() {
$markedUpText = 'A string without any fancy markup.';
- $expectedCleanedContent = [ 'A string without any fancy
markup.' ];
- $this->assertEquals(
+ $expectedCleanedContent = [
+ new Text( 'A string without any fancy markup.' )
+ ];
+ $this->assertContentsEqual(
$expectedCleanedContent,
Cleaner::cleanHtml( $markedUpText )
);
@@ -153,11 +191,11 @@
public function testCleanNestedTags() {
$markedUpText = '<i><b>Nested content</b></i>';
$expectedCleanedContent = [
- Util::createStartTag( '<i>' ),
- Util::createStartTag( '<b>' ),
- 'Nested content',
- new CleanedEndTag( '</b>' ),
- new CleanedEndTag( '</i>' )
+ new CleanedTag( '<i>' ),
+ new CleanedTag( '<b>' ),
+ new Text( 'Nested content' ),
+ new CleanedTag( '</b>' ),
+ new CleanedTag( '</i>' )
];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
@@ -165,7 +203,7 @@
public function testCleanEmptyElementTags() {
$markedUpText = '<br />';
$expectedCleanedContent = [
- new CleanedEmptyElementTag( '<br />' )
+ new CleanedTag( '<br />' )
];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
@@ -173,8 +211,8 @@
public function testRemoveTags() {
$markedUpText = '<del>removed tag </del>';
$expectedCleanedContent = [
- Util::createStartTag( '<del>', 'removed tag ' ),
- new CleanedEndTag( '</del>' )
+ new CleanedTag( '<del>' ),
+ new CleanedTag( '</del>' )
];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
@@ -182,8 +220,8 @@
public function testDontAddCleanedTagsForTagsUnderRemovedTags() {
$markedUpText = '<del><i>nested removed tag</i></del>';
$expectedCleanedContent = [
- Util::createStartTag( '<del>', '<i>nested removed
tag</i>' ),
- new CleanedEndTag( '</del>' )
+ new CleanedTag( '<del>' ),
+ new CleanedTag( '</del>' )
];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
@@ -191,11 +229,8 @@
public function testRemoveDoubleNestedTags() {
$markedUpText = '<del><i><b>double nested removed
tag</b></i></del>';
$expectedCleanedContent = [
- Util::createStartTag(
- '<del>',
- '<i><b>double nested removed tag</i></u>'
- ),
- new CleanedEndTag( '</del>' )
+ new CleanedTag( '<del>' ),
+ new CleanedTag( '</del>' )
];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
@@ -203,11 +238,8 @@
public function testRemoveTagsWithCertainClass() {
$markedUpText = '<sup class="reference">Remove this.</sup>';
$expectedCleanedContent = [
- Util::createStartTag(
- '<sup class="reference">',
- 'Remove this.'
- ),
- new CleanedEndTag( '</sup>' )
+ new CleanedTag( '<sup class="reference">' ),
+ new CleanedTag( '</sup>' )
];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
@@ -216,22 +248,22 @@
$markedUpText =
'<sup>I am not a reference.</sup><sup
class="not-a-reference">Neither am I.</sup>';
$expectedCleanedContent = [
- Util::createStartTag( '<sup>' ),
- 'I am not a reference.',
- new CleanedEndTag( '</sup>' ),
- Util::createStartTag( '<sup class="not-a-reference">' ),
- 'Neither am I.',
- new CleanedEndTag( '</sup>' )
+ new CleanedTag( '<sup>' ),
+ new Text( 'I am not a reference.' ),
+ new CleanedTag( '</sup>' ),
+ new CleanedTag( '<sup class="not-a-reference">' ),
+ new Text( 'Neither am I.' ),
+ new CleanedTag( '</sup>' )
];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
- public function testDontRemoveTagsWhichCriteriaAreFalse() {
+ public function testDontRemoveTagsWhoseCriteriaAreFalse() {
$markedUpText = '<h2>Contents</h2>';
$expectedCleanedContent = [
- Util::createStartTag( '<h2>' ),
- 'Contents',
- new CleanedEndTag( '</h2>' )
+ new CleanedTag( '<h2>' ),
+ new Text( 'Contents' ),
+ new CleanedTag( '</h2>' )
];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
@@ -240,11 +272,8 @@
$markedUpText =
'<sup class="reference another-class">Remove
this.</sup>';
$expectedCleanedContent = [
- Util::createStartTag(
- '<sup class="reference another-class">',
- 'Remove this.'
- ),
- new CleanedEndTag( '</sup>' )
+ new CleanedTag( '<sup class="reference another-class">'
),
+ new CleanedTag( '</sup>' )
];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
@@ -252,35 +281,35 @@
public function testCleanNestedTagsWhereSomeAreRemovedAndSomeAreKept() {
$markedUpText = '<i><b>not removed</b><del>removed</del></i>';
$expectedCleanedContent = [
- Util::createStartTag( '<i>' ),
- Util::createStartTag( '<b>' ),
- 'not removed',
- new CleanedEndTag( '</b>' ),
- Util::createStartTag( '<del>', 'removed' ),
- new CleanedEndTag( '</del>' ),
- new CleanedEndTag( '</i>' )
+ new CleanedTag( '<i>' ),
+ new CleanedTag( '<b>' ),
+ new Text( 'not removed' ),
+ new CleanedTag( '</b>' ),
+ new CleanedTag( '<del>' ),
+ new CleanedTag( '</del>' ),
+ new CleanedTag( '</i>' )
];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
public function testHandleUtf8Characters() {
$markedUpText = '—';
- $expectedCleanedContent = [ '—' ];
+ $expectedCleanedContent = [ new Text( '—' ) ];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
public function testHandleHtmlEntities() {
$markedUpText = '6 p.m';
- $expectedCleanedContent = [ '6 p.m' ];
+ $expectedCleanedContent = [ new Text( '6 p.m' ) ];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
public function testHandleNewlines() {
$markedUpText = "<i>Keep this newline\n</i>";
$expectedCleanedContent = [
- Util::createStartTag( '<i>' ),
- "Keep this newline\n",
- new CleanedEndTag( '</i>' )
+ new CleanedTag( '<i>' ),
+ new Text( "Keep this newline\n" ),
+ new CleanedTag( '</i>' )
];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
@@ -288,10 +317,10 @@
public function testHandleEndTagFollowedByEmptyElementTag() {
$markedUpText = '<i>content</i><br />';
$expectedCleanedContent = [
- Util::createStartTag( '<i>' ),
- 'content',
- new CleanedEndTag( '</i>' ),
- new CleanedEmptyElementTag( '<br />' )
+ new CleanedTag( '<i>' ),
+ new Text( 'content' ),
+ new CleanedTag( '</i>' ),
+ new CleanedTag( '<br />' )
];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
}
@@ -299,19 +328,37 @@
public function testHandleEmptyElementTagInsideElement() {
$markedUpText = '<i>content<br /></i>';
$expectedCleanedContent = [
- Util::createStartTag( '<i>' ),
- 'content',
- new CleanedEmptyElementTag( '<br />' ),
- new CleanedEndTag( '</i>' )
+ new CleanedTag( '<i>' ),
+ new Text( 'content' ),
+ new CleanedTag( '<br />' ),
+ new CleanedTag( '</i>' )
];
$this->assertTextCleaned( $expectedCleanedContent,
$markedUpText );
+ }
+
+ public function testGeneratePaths() {
+ $markedUpText = '<i>level one<br /><b>level two</b></i>level
zero';
+ $expectedCleanedContent = [
+ new CleanedTag( '<i>' ),
+ new Text( 'level one', [ 0, 0 ] ),
+ new CleanedTag( '<br />' ),
+ new CleanedTag( '<b>' ),
+ new Text( 'level two', [ 0, 2, 0 ] ),
+ new CleanedTag( '</b>' ),
+ new CleanedTag( '</i>' ),
+ new Text( 'level zero', [ 1 ] )
+ ];
+ $this->assertEquals(
+ $expectedCleanedContent,
+ Cleaner::cleanHtml( $markedUpText )
+ );
}
public function testGetTags() {
$textWithTags = '<i>content</i>';
$expectedTags = [ [
- [ 'string' => '<i>', 'position' => 0 ],
- [ 'string' => '</i>', 'position' => 10 ]
+ '<i>',
+ '</i>'
] ];
$this->assertEquals(
$expectedTags,
@@ -341,12 +388,12 @@
$textWithTags = '<i>content<b>content</b></i>';
$expectedTags = [
[
- [ 'string' => '<i>', 'position' => 0 ],
- [ 'string' => '</i>', 'position' => 24 ]
+ '<i>',
+ '</i>'
],
[
- [ 'string' => '<b>', 'position' => 10 ],
- [ 'string' => '</b>', 'position' => 20 ]
+ '<b>',
+ '</b>'
]
];
$this->assertEquals(
@@ -359,12 +406,12 @@
$textWithTags = '<i id="1">content<i id="2">content</i></i>';
$expectedTags = [
[
- [ 'string' => '<i id="1">', 'position' => 0 ],
- [ 'string' => '</i>', 'position' => 38 ]
+ '<i id="1">',
+ '</i>'
],
[
- [ 'string' => '<i id="2">', 'position' => 17 ],
- [ 'string' => '</i>', 'position' => 34 ]
+ '<i id="2">',
+ '</i>'
]
];
$this->assertEquals(
diff --git a/tests/phpunit/HtmlGeneratorTest.php
b/tests/phpunit/HtmlGeneratorTest.php
index 929f0c4..5df2166 100644
--- a/tests/phpunit/HtmlGeneratorTest.php
+++ b/tests/phpunit/HtmlGeneratorTest.php
@@ -12,103 +12,163 @@
class HtmlGeneratorTest extends MediaWikiTestCase {
public function testCreateUtteranceElement() {
$segment = [
- 'position' => 0,
- 'content' => [ 'An utterance.' ],
+ 'startOffset' => 0,
+ 'endOffset' => 12,
+ 'content' => [ new Text( 'An utterance.', [ 0, 1 ] ) ],
];
- $element = Util::call(
+ $utteranceElement = Util::call(
'HtmlGenerator',
'createUtteranceElement',
new DOMDocument(),
$segment,
0
);
- $this->assertEquals( 'utterance', $element->nodeName );
- $this->assertEquals( 'utterance-0', $element->getAttribute(
'id' ) );
- $this->assertEquals( 0, $element->getAttribute( 'position' ) );
- $this->assertEquals( 'content', $element->firstChild->nodeName
);
+ // Appending the node is needed for Xpath functions to work.
+ $utteranceElement->ownerDocument->appendChild(
$utteranceElement );
+ $this->assertEquals( 'utterance', $utteranceElement->nodeName );
$this->assertEquals(
- 'An utterance.',
- $element->firstChild->nodeValue
+ 'utterance-0',
+ $utteranceElement->getAttribute( 'id' )
);
+ $this->assertEquals(
+ 0,
+ $utteranceElement->getAttribute( 'start-offset' )
+ );
+ $this->assertEquals(
+ 12,
+ $utteranceElement->getAttribute( 'end-offset' )
+ );
+ $xpath = new DOMXPath( $utteranceElement->ownerDocument );
+ $contentElement = $xpath->evaluate( '/utterance/content'
)->item( 0 );
+ $this->assertEquals( 'content', $contentElement->nodeName );
+ $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'
) );
}
public function testCreateUtteranceContainingNumberSign() {
$segment = [
- 'position' => 0,
- 'content' => [ 'This is #1.' ]
+ 'startOffset' => null,
+ 'endOffset' => null,
+ 'content' => [ new Text( 'This is #1.', [] ) ]
];
- $element = Util::call(
+ $utteranceElement = Util::call(
'HtmlGenerator',
'createUtteranceElement',
- new DOMDocument,
+ new DOMDocument(),
$segment,
0
);
- $this->assertEquals( 'This is #1.',
$element->firstChild->nodeValue );
+ $utteranceElement->ownerDocument->appendChild(
$utteranceElement );
+ $xpath = new DOMXPath( $utteranceElement->ownerDocument );
+ $textElement = $xpath->query( '/utterance/content/text'
)->item( 0 );
+ $this->assertEquals( 'This is #1.', $textElement->nodeValue );
}
public function testCreateUtteranceContainingTagBrackets() {
$segment = [
- 'position' => 0,
- 'content' => [ 'This is not really a <tag>.' ]
+ 'startOffset' => null,
+ 'endOffset' => null,
+ 'content' => [ new Text( 'This is not really a <tag>.',
[] ) ]
];
- $element = Util::call(
+ $utteranceElement = Util::call(
'HtmlGenerator',
'createUtteranceElement',
- new DOMDocument,
+ new DOMDocument(),
$segment,
0
);
+ $utteranceElement->ownerDocument->appendChild(
$utteranceElement );
+ $xpath = new DOMXPath( $utteranceElement->ownerDocument );
+ $textElement = $xpath->query( '/utterance/content/text'
)->item( 0 );
$this->assertEquals(
'This is not really a <tag>.',
- $element->firstChild->nodeValue
+ $textElement->nodeValue
);
}
public function testDontCreateUtterancesHtmlForNoUtterances() {
$segments = [];
- $html = HtmlGenerator::createUtterancesHtml( $segments );
- $expectedHtml = '';
- $this->assertEquals( $expectedHtml, $html );
+ $utterancesElement = Util::call(
+ 'HtmlGenerator',
+ 'createUtterancesElement',
+ $segments,
+ new DOMDocument()
+ );
+ $this->assertNull( $utterancesElement );
}
public function testCreateUtterancesMultipleUtterances() {
$segments = [
[
- 'position' => 0,
- 'content' => [ 'Sentence 1.' ]
+ 'startOffset' => null,
+ 'endOffset' => null,
+ 'content' => [ new Text( 'Sentence 1.', [] ) ]
],
[
- 'position' => 11,
- 'content' => [ ' Sentence 2.' ]
+ 'startOffset' => null,
+ 'endOffset' => null,
+ 'content' => [ new Text( ' Sentence 2.', [] ) ]
]
-
];
- $actualHtml = HtmlGenerator::createUtterancesHtml( $segments );
- // @codingStandardsIgnoreStart
- $expectedHtml =
- '<utterances hidden=""><utterance id="utterance-0"
position="0"><content>Sentence 1.</content></utterance><utterance
id="utterance-1" position="11"><content> Sentence
2.</content></utterance></utterances>';
- // @codingStandardsIgnoreEnd
- $this->assertEquals( $expectedHtml, $actualHtml );
+ $utterancesElement = Util::call(
+ 'HtmlGenerator',
+ 'createUtterancesElement',
+ $segments,
+ new DOMDocument()
+ );
+ $utteranceElements =
+ $utterancesElement->getElementsByTagName( 'utterance' );
+ $this->assertEquals( 2, $utteranceElements->length );
}
- public function testCreateUtterancesContainingRemovedTags() {
- $segments = [
- [
- 'position' => 0,
- 'content' => [
- 'Here is a ',
- Util::createStartTag( '<i>' ),
- 'tag',
- new CleanedEndTag( '</i>' )
- ]
+ public function testCreateUtterancesContainingCleanedTags() {
+ $segment = [
+ 'startOffset' => null,
+ 'endOffset' => null,
+ 'content' => [
+ new Text( 'Here is a ', [] ),
+ new CleanedTag( '<i>' ),
+ new Text( 'tag', [] ),
+ new CleanedTag( '</i>', [] )
]
];
- $html = HtmlGenerator::createUtterancesHtml( $segments );
- // @codingStandardsIgnoreStart
- $expectedHtml =
- '<utterances hidden=""><utterance id="utterance-0"
position="0"><content>Here is a
<cleaned-tag>i</cleaned-tag>tag<cleaned-tag>/i</cleaned-tag></content></utterance></utterances>';
- // @codingStandardsIgnoreEnd
- $this->assertEquals( $expectedHtml, $html );
+ $utteranceElement = Util::call(
+ 'HtmlGenerator',
+ 'createUtteranceElement',
+ new DOMDocument(),
+ $segment,
+ 0
+ );
+ $utteranceElement->ownerDocument->appendChild(
$utteranceElement );
+ $xpath = new DOMXPath( $utteranceElement->ownerDocument );
+ $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 0b4f6cf..da614d4 100644
--- a/tests/phpunit/SegmenterTest.php
+++ b/tests/phpunit/SegmenterTest.php
@@ -12,16 +12,18 @@
class SegmenterTest extends MediaWikiTestCase {
public function testSegmentSentences() {
$cleanedContent = [
- 'Sentence 1. Sentence 2.'
+ new Text( 'Sentence 1. Sentence 2.' )
];
$expectedSegments = [
[
- 'position' => 0,
- 'content' => [ 'Sentence 1.' ]
+ 'startOffset' => 0,
+ 'endOffset' => 10,
+ 'content' => [ new Text( 'Sentence 1.' ) ]
],
[
- 'position' => 11,
- 'content' => [ ' Sentence 2.' ]
+ 'startOffset' => 11,
+ 'endOffset' => 22,
+ 'content' => [ new Text( ' Sentence 2.' ) ]
]
];
$segments = Segmenter::segmentSentences( $cleanedContent );
@@ -30,151 +32,150 @@
public function testDontSegmentByEllipses() {
$cleanedContent = [
- 'This is... one sentence.'
+ new Text( 'This is... one sentence.' )
];
- $expectedSegments = [
- [
- 'position' => 0,
- 'content' => [ 'This is... one sentence.' ]
- ]
- ];
$segments = Segmenter::segmentSentences( $cleanedContent );
- $this->assertEquals( $expectedSegments, $segments );
+ $this->assertEquals(
+ [ new Text( 'This is... one sentence.' ) ],
+ $segments[0]['content']
+ );
}
public function testDontSegmentByAbbreviations() {
- $cleanedContent = [ 'One sentence i.e. one segment.' ];
- $expectedSegments = [
- [
- 'position' => 0,
- 'content' => [ 'One sentence i.e. one segment.'
]
- ]
- ];
+ $cleanedContent = [ new Text( 'One sentence i.e. one segment.'
) ];
$segments = Segmenter::segmentSentences( $cleanedContent );
- $this->assertEquals( $expectedSegments, $segments );
+ $this->assertEquals(
+ [ new Text( 'One sentence i.e. one segment.' ) ],
+ $segments[0]['content']
+ );
}
public function testDontSegmentByDotDirectlyFollowedByComma() {
- $cleanedContent = [ 'As with etc., jr. and friends.' ];
- $expectedSegments = [
- [
- 'position' => 0,
- 'content' => [ 'As with etc., jr. and friends.'
]
- ]
- ];
+ $cleanedContent = [ new Text( 'As with etc., jr. and friends.'
) ];
$segments = Segmenter::segmentSentences( $cleanedContent );
- $this->assertEquals( $expectedSegments, $segments );
+ $this->assertEquals(
+ [ new Text( 'As with etc., jr. and friends.' ) ],
+ $segments[0]['content']
+ );
}
public function testDontSegmentByDecimalDot() {
- $cleanedContent = [ 'In numbers like 2.9.' ];
- $expectedSegments = [
- [
- 'position' => 0,
- 'content' => [ 'In numbers like 2.9.' ]
- ]
- ];
+ $cleanedContent = [ new Text( 'In numbers like 2.9.' ) ];
$segments = Segmenter::segmentSentences( $cleanedContent );
- $this->assertEquals( $expectedSegments, $segments );
+ $this->assertEquals(
+ [ new Text( 'In numbers like 2.9.' ) ],
+ $segments[0]['content']
+ );
}
public function
testKeepLastSegmentEvenIfNotEndingWithSentenceFinalCharacter() {
- $cleanedContent = [ 'Recording sessions' ];
- $expectedSegments = [
- [
- 'position' => 0,
- 'content' => [ 'Recording sessions' ]
- ]
- ];
+ $cleanedContent = [ new Text( 'Recording sessions' ) ];
$segments = Segmenter::segmentSentences( $cleanedContent );
- $this->assertEquals( $expectedSegments, $segments );
+ $this->assertEquals(
+ [ new Text( 'Recording sessions' ) ],
+ $segments[0]['content']
+ );
}
public function testSegmentContainingTag() {
$cleanedContent = [
- 'Sentence with a ',
- Util::createStartTag( '<i>' ),
- 'tag',
- new CleanedEndTag( '</i>' ),
- '.'
+ new Text( 'Sentence with a ' ),
+ new CleanedTag( '<i>' ),
+ new Text( 'tag' ),
+ new CleanedTag( '</i>' ),
+ new Text( '.' )
];
- $expectedSegments = [
- [
- 'position' => 0,
- 'content' => [
- 'Sentence with a ',
- Util::createStartTag( '<i>' ),
- 'tag',
- new CleanedEndTag( '</i>' ),
- '.'
- ]
- ]
+ $expectedContent = [
+ new Text( 'Sentence with a ' ),
+ new CleanedTag( '<i>' ),
+ new Text( 'tag' ),
+ new CleanedTag( '</i>' ),
+ new Text( '.' )
];
$segments = Segmenter::segmentSentences( $cleanedContent );
- $this->assertEquals( $expectedSegments, $segments );
+ $this->assertEquals(
+ $expectedContent,
+ $segments[0]['content']
+ );
}
public function testSegmentEndingWithTag() {
$cleanedContent = [
- "There's a tag after this",
- new CleanedEmptyElementTag( '<br />' )
+ new Text( "There's a tag after this" ),
+ new CleanedTag( '<br />' )
];
- $expectedSegments = [
- [
- 'position' => 0,
- 'content' => [
- "There's a tag after this",
- new CleanedEmptyElementTag( '<br />' )
- ]
- ]
+ $expectedContent = [
+ new Text( "There's a tag after this" ),
+ new CleanedTag( '<br />' )
];
$segments = Segmenter::segmentSentences( $cleanedContent );
- $this->assertEquals( $expectedSegments, $segments );
+ $this->assertEquals(
+ $expectedContent,
+ $segments[0]['content']
+ );
}
- public function testCalculatePosition() {
- $cleanedContent = [ 'Segment 1.', 'Segment 2.', 'Segment 3.' ];
- $expectedSegments = [
- [
- 'position' => 0,
- 'content' => [ 'Segment 1.' ]
- ],
- [
- 'position' => 10,
- 'content' => [ 'Segment 2.' ]
- ],
- [
- 'position' => 20,
- 'content' => [ 'Segment 3.' ]
- ],
- ];
+ public function testTextOffset() {
+ $cleanedContent = [ new Text( 'Sentence.' ) ];
$segments = Segmenter::segmentSentences( $cleanedContent );
- $this->assertEquals( $expectedSegments, $segments );
+ $this->assertEquals( 0, $segments[0]['startOffset'] );
+ $this->assertEquals( 8, $segments[0]['endOffset'] );
}
- public function testCalculatePositionWhenTagIsRemoved() {
+ public function testTextOffsetMultipleUtterances() {
+ $cleanedContent = [ new Text( 'Sentence one. Sentence two.' ) ];
+ $segments = Segmenter::segmentSentences( $cleanedContent );
+ $this->assertEquals( 0, $segments[0]['startOffset'] );
+ $this->assertEquals( 12, $segments[0]['endOffset'] );
+ $this->assertEquals( 13, $segments[1]['startOffset'] );
+ $this->assertEquals( 26, $segments[1]['endOffset'] );
+ }
+
+ public function testTextOffsetWithTags() {
$cleanedContent = [
- 'Sentence with a ',
- Util::createStartTag( '<del>', 'removed ' ),
- new CleanedEndTag( '</del>' ),
- 'tag. Another sentence.'
- ];
- $expectedSegments = [
- [
- 'position' => 0,
- 'content' => [
- 'Sentence with a ',
- Util::createStartTag( '<del>', 'removed
' ),
- new CleanedEndTag( '</del>' ),
- 'tag.',
- ]
- ],
- [
- 'position' => 39,
- 'content' => [ ' Another sentence.' ]
- ]
+ new Text( 'Sentence ' ),
+ new CleanedTag( '<i>' ),
+ new Text( 'with' ),
+ new CleanedTag( '</i>' ),
+ new Text( ' a tag.' )
];
$segments = Segmenter::segmentSentences( $cleanedContent );
- $this->assertEquals( $expectedSegments, $segments );
+ $this->assertEquals( 0, $segments[0]['startOffset'] );
+ $this->assertEquals( 6, $segments[0]['endOffset'] );
+ }
+
+ public function testTextOffsetAfterTags() {
+ $cleanedContent = [
+ new Text( 'Sentence one.' ),
+ new CleanedTag( '<br />' ),
+ new Text( ' Sentence two.' )
+ ];
+ $segments = Segmenter::segmentSentences( $cleanedContent );
+ $this->assertEquals(
+ [
+ new CleanedTag( '<br />' ),
+ new Text( ' Sentence two.' )
+ ],
+ $segments[1]['content'] );
+ $this->assertEquals( 0, $segments[1]['startOffset'] );
+ $this->assertEquals( 13, $segments[1]['endOffset'] );
+ }
+
+ public function testStartTextOffsetWhenTagInSegment() {
+ $cleanedContent = [
+ new Text( 'Sentence one. Sentence' ),
+ new CleanedTag( '<br />' ),
+ new Text( 'two.' )
+ ];
+ $segments = Segmenter::segmentSentences( $cleanedContent );
+ $this->assertEquals(
+ [
+ new Text( ' Sentence' ),
+ new CleanedTag( '<br />' ),
+ new Text( 'two.' )
+ ],
+ $segments[1]['content'] );
+ $this->assertEquals( 13, $segments[1]['startOffset'] );
+ $this->assertEquals( 3, $segments[1]['endOffset'] );
}
}
diff --git a/tests/phpunit/Util.php b/tests/phpunit/Util.php
index a526fb7..d60bd0a 100644
--- a/tests/phpunit/Util.php
+++ b/tests/phpunit/Util.php
@@ -7,29 +7,6 @@
*/
class Util {
-
- /**
- * Create a CleanedStartTag and set it's $contentLength.
- *
- * @since 0.0.1
- * @param string $tagString The tag string for the CleanedStartTag.
- * @param string $contentString The content string, used for
- * calculating $contentLength for the CleanedStartTag, if not
- * null. null by default.
- * @return CleanedStartTag
- */
-
- public static function createStartTag(
- $tagString,
- $contentString=null
- ) {
- $cleanedTag = new CleanedStartTag( $tagString );
- if ( $contentString != null ) {
- $cleanedTag->contentLength = strlen( $contentString );
- }
- return $cleanedTag;
- }
-
/**
* Call a private function.
*
@@ -49,5 +26,4 @@
$arguments = array_slice( func_get_args(), 2 );
return $reflection->invokeArgs( null, $arguments );
}
-
}
diff --git a/tests/qunit/ext.wikispeech.test.js
b/tests/qunit/ext.wikispeech.test.js
index 5bfefe6..c603f40 100644
--- a/tests/qunit/ext.wikispeech.test.js
+++ b/tests/qunit/ext.wikispeech.test.js
@@ -17,11 +17,9 @@
);
$( '<utterance></utterance>' )
.attr( 'id', 'utterance-0' )
- .attr( 'position', 0 )
.appendTo( $utterances );
$( '<utterance></utterance>' )
.attr( 'id', 'utterance-1' )
- .attr( 'position', 1 )
.appendTo( $utterances );
mw.config.set(
'wgWikispeechKeyboardShortcuts', {
@@ -137,10 +135,10 @@
} );
QUnit.test( 'loadAudio()', function ( assert ) {
+ var $content = $( '<content></content>' );
assert.expect( 4 );
- $( '<content></content>' )
- .append( 'An utterance.' )
- .appendTo( $( '#utterance-0' ) );
+ $content.append( $( '<text></text>' ).text( 'An utterance.' ) );
+ $( '#utterance-0' ).append( $content );
server.respondWith(
'{"audio": "http://server.url/audio", "tokens":
[{"orth": "An"}, {"orth": "utterance"}, {"orth": "."}]}'
);
@@ -480,10 +478,11 @@
} );
QUnit.test( 'addTokenElements()', function ( assert ) {
- var tokens, $tokensElement, $expectedTokensElement;
+ var tokens, $tokensElement, $expectedTokensElement, $content;
- $( '<content></content>' ).html( 'An utterance.' )
- .appendTo( $( '#utterance-0' ) );
+ $content = $( '<content></content>' );
+ $content.append( $( '<text></text>' ).text( 'An utterance.' ) );
+ $( '#utterance-0' ).append( $content );
tokens = [
{
orth: 'An',
@@ -503,9 +502,9 @@
$tokensElement = $( '#utterance-0' ).children( 'tokens' );
$expectedTokensElement = $( '<tokens></tokens>' );
- addToken( $expectedTokensElement, 'An', 0, 0.0, 1.0 );
- addToken( $expectedTokensElement, 'utterance', 3, 1.0, 2.0 );
- addToken( $expectedTokensElement, '.', 12, 2.0, 3.0 );
+ addToken( $expectedTokensElement, 'An', 0.0, 1.0 );
+ addToken( $expectedTokensElement, 'utterance', 1.0, 2.0 );
+ addToken( $expectedTokensElement, '.', 2.0, 3.0 );
assert.strictEqual(
$tokensElement.prop( 'outerHTML' ),
$expectedTokensElement.prop( 'outerHTML' )
@@ -517,17 +516,15 @@
*
* @param {jQuery} $parent The jQuery to add the element to.
* @param {string} string The token string.
- * @param {number} position The position of the token.
* @param {number} startTime The start time of the token.
* @param {number} endTime The end time of the token.
* @return {HTMLElement} The added token element.
*/
- function addToken( $parent, string, position, startTime, endTime ) {
+ function addToken( $parent, string, startTime, endTime ) {
var $token = $( '<token></token>' )
.text( string )
.attr( {
- position: position,
'start-time': startTime,
'end-time': endTime
} );
@@ -539,7 +536,7 @@
var tokens, $tokensElement, $expectedTokensElement;
$( '<content></content>' ).html(
- 'Utterance with
<cleaned-tag>b</cleaned-tag>tag<cleaned-tag>/b</cleaned-tag>.'
+ '<text>Utterance with
</text><cleaned-tag>b</cleaned-tag><text>tag</text><cleaned-tag>/b</cleaned-tag><text>.</text>'
)
.appendTo( $( '#utterance-0' ) );
tokens = [
@@ -565,44 +562,10 @@
$tokensElement = $( '#utterance-0' ).children( 'tokens' );
$expectedTokensElement = $( '<tokens></tokens>' );
- addToken( $expectedTokensElement, 'Utterance', 0, 0.0, 1.0 );
- addToken( $expectedTokensElement, 'with', 10, 1.0, 2.0 );
- addToken( $expectedTokensElement, 'tag', 18, 2.0, 3.0 );
- addToken( $expectedTokensElement, '.', 25, 3.0, 4.0 );
- assert.strictEqual(
- $tokensElement.prop( 'outerHTML' ),
- $expectedTokensElement.prop( 'outerHTML' )
- );
- } );
-
- QUnit.test( 'addTokenElements(): utterance position offset', function (
assert ) {
- var tokens, $tokensElement, $expectedTokensElement;
-
- $( '<content></content>' ).html( 'An utterance.' )
- .appendTo( $( '#utterance-0' ) );
- $( '#utterance-0' ).attr( 'position', 3 );
- tokens = [
- {
- orth: 'An',
- endtime: 1.0
- },
- {
- orth: 'utterance',
- endtime: 2.0
- },
- {
- orth: '.',
- endtime: 3.0
- }
- ];
-
- wikispeech.addTokenElements( $( '#utterance-0' ), tokens );
-
- $tokensElement = $( '#utterance-0' ).children( 'tokens' );
- $expectedTokensElement = $( '<tokens></tokens>' );
- addToken( $expectedTokensElement, 'An', 3, 0.0, 1.0 );
- addToken( $expectedTokensElement, 'utterance', 6, 1.0, 2.0 );
- addToken( $expectedTokensElement, '.', 15, 2.0, 3.0 );
+ addToken( $expectedTokensElement, 'Utterance', 0.0, 1.0 );
+ addToken( $expectedTokensElement, 'with', 1.0, 2.0 );
+ addToken( $expectedTokensElement, 'tag', 2.0, 3.0 );
+ addToken( $expectedTokensElement, '.', 3.0, 4.0 );
assert.strictEqual(
$tokensElement.prop( 'outerHTML' ),
$expectedTokensElement.prop( 'outerHTML' )
@@ -613,7 +576,7 @@
var tokens, $tokensElement, $expectedTokensElement;
$( '<content></content>' ).html(
- 'Utterance with <cleaned-tag>del</cleaned-tag>removed
tag<cleaned-tag>/del</cleaned-tag>.'
+ '<text>Utterance with
</text><cleaned-tag>del</cleaned-tag><text>removed
tag</text><cleaned-tag>/del</cleaned-tag><text>.</text>'
)
.appendTo( $( '#utterance-0' ) );
tokens = [
@@ -635,9 +598,9 @@
$tokensElement = $( '#utterance-0' ).children( 'tokens' );
$expectedTokensElement = $( '<tokens></tokens>' );
- addToken( $expectedTokensElement, 'Utterance', 0, 0.0, 1.0 );
- addToken( $expectedTokensElement, 'with', 10, 1.0, 2.0 );
- addToken( $expectedTokensElement, '.', 37, 2.0, 3.0 );
+ addToken( $expectedTokensElement, 'Utterance', 0.0, 1.0 );
+ addToken( $expectedTokensElement, 'with', 1.0, 2.0 );
+ addToken( $expectedTokensElement, '.', 2.0, 3.0 );
assert.strictEqual(
$tokensElement.prop( 'outerHTML' ),
$expectedTokensElement.prop( 'outerHTML' )
@@ -650,9 +613,9 @@
assert.expect( 1 );
wikispeech.prepareUtterance( $( '#utterance-0' ) );
$tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0'
) );
- addToken( $tokens, '', 0, 0.0, 1.0 );
- expectedToken = addToken( $tokens, '', 0, 1.0, 2.0 );
- addToken( $tokens, '', 0, 2.0, 3.0 );
+ addToken( $tokens, '', 0.0, 1.0 );
+ expectedToken = addToken( $tokens, '', 1.0, 2.0 );
+ addToken( $tokens, '', 2.0, 3.0 );
$( '#utterance-0 audio' ).prop( 'currentTime', 1.1 );
wikispeech.play();
@@ -667,8 +630,8 @@
assert.expect( 1 );
wikispeech.prepareUtterance( $( '#utterance-0' ) );
$tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0'
) );
- expectedToken = addToken( $tokens, '', 0, 0.0, 1.0 );
- addToken( $tokens, '', 0, 1.0, 2.0 );
+ expectedToken = addToken( $tokens, '', 0.0, 1.0 );
+ addToken( $tokens, '', 1.0, 2.0 );
$( '#utterance-0 audio' ).prop( 'currentTime', 0.1 );
wikispeech.play();
@@ -683,9 +646,9 @@
assert.expect( 1 );
wikispeech.prepareUtterance( $( '#utterance-0' ) );
$tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0'
) );
- addToken( $tokens, '', 0, 0.0, 1.0 );
- addToken( $tokens, '', 0, 1.0, 2.0 );
- expectedToken = addToken( $tokens, '', 0, 2.0, 3.0 );
+ addToken( $tokens, '', 0.0, 1.0 );
+ addToken( $tokens, '', 1.0, 2.0 );
+ expectedToken = addToken( $tokens, '', 2.0, 3.0 );
$( '#utterance-0 audio' ).prop( 'currentTime', 2.1 );
wikispeech.play();
@@ -700,8 +663,8 @@
assert.expect( 1 );
wikispeech.prepareUtterance( $( '#utterance-0' ) );
$tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0'
) );
- addToken( $tokens, '', 0, 0.0, 1.0 );
- expectedToken = addToken( $tokens, '', 0, 1.0, 2.0 );
+ addToken( $tokens, '', 0.0, 1.0 );
+ expectedToken = addToken( $tokens, '', 1.0, 2.0 );
$( '#utterance-0 audio' ).prop( 'currentTime', 2.0 );
wikispeech.play();
@@ -716,9 +679,9 @@
assert.expect( 1 );
wikispeech.prepareUtterance( $( '#utterance-0' ) );
$tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0'
) );
- addToken( $tokens, '', 0, 0.0, 1.0 );
- addToken( $tokens, '', 0, 1.0, 1.0 );
- expectedToken = addToken( $tokens, '', 0, 1.0, 2.0 );
+ addToken( $tokens, '', 0.0, 1.0 );
+ addToken( $tokens, '', 1.0, 1.0 );
+ expectedToken = addToken( $tokens, '', 1.0, 2.0 );
$( '#utterance-0 audio' ).prop( 'currentTime', 1.1 );
wikispeech.play();
@@ -733,9 +696,9 @@
assert.expect( 1 );
wikispeech.prepareUtterance( $( '#utterance-0' ) );
$tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0'
) );
- addToken( $tokens, '', 0, 0.0, 1.0 );
- addToken( $tokens, '', 0, 1.0, 1.0 );
- expectedToken = addToken( $tokens, '', 0, 1.0, 2.0 );
+ addToken( $tokens, '', 0.0, 1.0 );
+ addToken( $tokens, '', 1.0, 1.0 );
+ expectedToken = addToken( $tokens, '', 1.0, 2.0 );
$( '#utterance-0 audio' ).prop( 'currentTime', 1.1 );
wikispeech.play();
@@ -750,8 +713,8 @@
assert.expect( 1 );
wikispeech.prepareUtterance( $( '#utterance-0' ) );
$tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0'
) );
- addToken( $tokens, 'one', 0, 0.0, 1.0 );
- addToken( $tokens, 'two', 0, 1.0, 2.0 );
+ addToken( $tokens, 'one', 0.0, 1.0 );
+ addToken( $tokens, 'two', 1.0, 2.0 );
wikispeech.play();
wikispeech.skipAheadToken();
@@ -768,8 +731,8 @@
assert.expect( 1 );
wikispeech.prepareUtterance( $( '#utterance-0' ) );
$tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0'
) );
- addToken( $tokens, 'first', 0, 0.0, 1.0 );
- addToken( $tokens, 'last', 0, 1.0, 2.0 );
+ addToken( $tokens, 'first', 0.0, 1.0 );
+ addToken( $tokens, 'last', 1.0, 2.0 );
wikispeech.play();
$( '#utterance-0 audio' ).prop( 'currentTime', 1.1 );
sinon.spy( wikispeech, 'skipAheadUtterance' );
@@ -785,10 +748,10 @@
assert.expect( 1 );
wikispeech.prepareUtterance( $( '#utterance-0' ) );
$tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0'
) );
- addToken( $tokens, 'starting word', 0, 0.0, 1.0 );
- addToken( $tokens, 'no duration', 0, 1.0, 1.0 );
- addToken( $tokens, '', 0, 1.0, 2.0 );
- addToken( $tokens, 'goal', 0, 2.0, 3.0 );
+ addToken( $tokens, 'starting word', 0.0, 1.0 );
+ addToken( $tokens, 'no duration', 1.0, 1.0 );
+ addToken( $tokens, '', 1.0, 2.0 );
+ addToken( $tokens, 'goal', 2.0, 3.0 );
wikispeech.play();
wikispeech.skipAheadToken();
@@ -805,8 +768,8 @@
assert.expect( 1 );
wikispeech.prepareUtterance( $( '#utterance-0' ) );
$tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0'
) );
- addToken( $tokens, 'one', 0, 0.0, 1.0 );
- addToken( $tokens, 'two', 0, 1.0, 2.0 );
+ addToken( $tokens, 'one', 0.0, 1.0 );
+ addToken( $tokens, 'two', 1.0, 2.0 );
wikispeech.play();
$( '#utterance-0 audio' ).prop( 'currentTime', 1.1 );
@@ -825,10 +788,10 @@
wikispeech.prepareUtterance( $( '#utterance-0' ) );
wikispeech.prepareUtterance( $( '#utterance-1' ) );
$tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0'
) );
- addToken( $tokens, 'one', 0, 0.0, 1.0 );
- addToken( $tokens, 'two', 0, 1.0, 2.0 );
+ addToken( $tokens, 'one', 0.0, 1.0 );
+ addToken( $tokens, 'two', 1.0, 2.0 );
$tokens2 = $( '<tokens></tokens>' ).appendTo( $( '#utterance-1'
) );
- addToken( $tokens2, 'three', 0, 0.0, 1.0 );
+ addToken( $tokens2, 'three', 0.0, 1.0 );
wikispeech.playUtterance( $( '#utterance-1' ) );
wikispeech.skipBackToken();
@@ -853,10 +816,10 @@
assert.expect( 1 );
wikispeech.prepareUtterance( $( '#utterance-0' ) );
$tokens = $( '<tokens></tokens>' ).appendTo( $( '#utterance-0'
) );
- addToken( $tokens, 'starting word', 0, 0.0, 1.0 );
- addToken( $tokens, 'no duration', 0, 1.0, 1.0 );
- addToken( $tokens, '', 0, 1.0, 2.0 );
- addToken( $tokens, 'goal', 0, 2.0, 3.0 );
+ addToken( $tokens, 'starting word', 0.0, 1.0 );
+ addToken( $tokens, 'no duration', 1.0, 1.0 );
+ addToken( $tokens, '', 1.0, 2.0 );
+ addToken( $tokens, 'goal', 2.0, 3.0 );
wikispeech.playUtterance( $( '#utterance-0' ) );
$( '#utterance-0 audio' ).prop( 'currentTime', 2.1 );
@@ -867,4 +830,187 @@
0.0
);
} );
+
+ QUnit.test( 'highlightUtterance()', function ( assert ) {
+ assert.expect( 2 );
+
+ $( '#qunit-fixture' ).append(
+ $( '<div></div>' )
+ .attr( 'id', 'mw-content-text' )
+ .text( 'An utterance.' )
+ );
+ $( '#utterance-0' ).append(
+ $( '<content></content>' )
+ .append( $( '<text></text>' )
+ .text( 'An utterance.' )
+ .attr( 'path', '0' )
+ )
+ );
+ $( '#utterance-0' ).attr( 'start-offset', 0 );
+ $( '#utterance-0' ).attr( 'end-offset', 12 );
+
+ wikispeech.highlightUtterance( $( '#utterance-0' ) );
+
+ 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 )
+ );
+ } );
+
+ QUnit.test( 'highlightUtterance(): multiple utterances', function (
assert ) {
+ assert.expect( 1 );
+
+ $( '#qunit-fixture' ).append(
+ $( '<div></div>' )
+ .attr( 'id', 'mw-content-text' )
+ .text( 'Utterance one. Utterance two. Utterance
three.' )
+ );
+
+ $( '#utterance-0' ).append(
+ $( '<content></content>' )
+ .append( $( '<text></text>' )
+ .text( ' Utterance two.' )
+ .attr( 'path', '0' )
+ )
+ );
+ $( '#utterance-0' ).attr( 'start-offset', 14 );
+ $( '#utterance-0' ).attr( 'end-offset', 28 );
+
+ wikispeech.highlightUtterance( $( '#utterance-0' ) );
+
+ assert.strictEqual(
+ $( '#mw-content-text' ).prop( 'innerHTML' ),
+ 'Utterance one.<span
class="ext-wikispeech-highlight-sentence"> Utterance two.</span> Utterance
three.'
+ );
+ } );
+
+ QUnit.test( 'highlightUtterance(): with tags', function ( assert ) {
+ assert.expect( 1 );
+
+ $( '#qunit-fixture' ).append(
+ $( '<div></div>' )
+ .attr( 'id', 'mw-content-text' )
+ .html( '<p>Utterance with <b>a</b> tag.</p>' )
+ );
+
+ $( '#utterance-0' ).append(
+ $( '<content></content>' )
+ .append( $( '<text></text>' )
+ .text( 'Utterance with ' )
+ .attr( 'path', '0,0' )
+ )
+ .append( $( '<cleaned-tag></cleaned-tag>' )
+ .text( 'b' )
+ )
+ .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' )
+ )
+ );
+ $( '#utterance-0' ).attr( 'start-offset', 0 );
+ $( '#utterance-0' ).attr( 'end-offset', 4 );
+
+ wikispeech.highlightUtterance( $( '#utterance-0' ) );
+
+ 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( 'unhighlightUtterance()', function ( assert ) {
+ assert.expect( 1 );
+
+ $( '#qunit-fixture' ).append(
+ $( '<div></div>' )
+ .attr( 'id', 'mw-content-text' )
+ .html( '<span
class="ext-wikispeech-highlight-sentence">An utterance.</span>' )
+ );
+ $( '.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', 0 );
+ $( '#utterance-0' ).attr( 'end-offset', 12 );
+
+ wikispeech.unhighlightUtterances();
+
+ assert.strictEqual(
+ $( '#mw-content-text' ).prop( 'innerHTML' ),
+ 'An utterance.'
+ );
+ } );
+
+ QUnit.test( 'unhighlightUtterance(): restore text nodes as one',
function ( assert ) {
+ assert.expect( 3 );
+
+ $( '#qunit-fixture' ).append(
+ $( '<div></div>' )
+ .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();
+
+ assert.strictEqual(
+ $( '#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
+ );
+ } );
+
+ QUnit.test( 'unhighlightUtterance(): multiple highlights', function (
assert ) {
+ assert.expect( 3 );
+
+ $( '#qunit-fixture' ).append(
+ $( '<div></div>' )
+ .attr( 'id', 'mw-content-text' )
+ .html( '<span
class="ext-wikispeech-highlight-sentence">An </span><span
class="ext-wikispeech-highlight-sentence">utterance.</span>' )
+ );
+ $( '.ext-wikispeech-highlight-sentence' )
+ .prop( 'utterance', $( '#utterance-0' ).get( 0 ) );
+
+ wikispeech.unhighlightUtterances();
+
+ assert.strictEqual(
+ $( '#mw-content-text' ).prop( 'innerHTML' ),
+ 'An utterance.'
+ );
+ assert.strictEqual( $( '#mw-content-text' ).contents().length,
1 );
+ assert.strictEqual(
+ $( '.ext-wikispeech-highlight-sentence'
).contents().length,
+ 0
+ );
+ } );
} )( mediaWiki, jQuery );
--
To view, visit https://gerrit.wikimedia.org/r/336816
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I12c8873265b7236c0677b25ce089bbb887ffdd9c
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Wikispeech
Gerrit-Branch: master
Gerrit-Owner: Sebastian Berlin (WMSE) <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits