jenkins-bot has submitted this change and it was merged.
Change subject: translateAnnotations with partial mapping
......................................................................
translateAnnotations with partial mapping
LinearDoc:
* Allow partial rangeMappings for translateAnnotations
* Fallback to plaintext where range info is incomplete
* Fallback to segment end placement for references
* Inline span['data-mw'] (like img) if it only contains whitespace
LinearDoc.test.js, translate.test.json
* Test translateAnnotations with plaintext translation and range mapping
Change-Id: I960dbe6155a5ea859b716e4a8748002a513618a6
---
M lineardoc/LinearDoc.js
M tests/lineardoc/LinearDoc.test.js
A tests/lineardoc/translate.test.json
3 files changed, 235 insertions(+), 13 deletions(-)
Approvals:
Santhosh: Looks good to me, approved
jenkins-bot: Verified
diff --git a/lineardoc/LinearDoc.js b/lineardoc/LinearDoc.js
index 92e8549..58a0cdd 100644
--- a/lineardoc/LinearDoc.js
+++ b/lineardoc/LinearDoc.js
@@ -316,20 +316,137 @@
* @returns {TextBlock} Translated textblock with annotations applied
*/
TextBlock.prototype.translateAnnotations = function ( targetText,
rangeMappings ) {
- var i, len, rangeMapping, oldTextChunk, newText,
- newTextChunks = [];
+ var i, iLen, j, rangeMapping, sourceTextChunk, text, pos, textChunk,
offset,
+ sourceRangeEnd, targetRangeEnd, tail, tailSpace,
+ // map of { offset: x, textChunks: [...] }
+ emptyTextChunks = {},
+ emptyTextChunkOffsets = [],
+ // list of { start: x, length: x, textChunk: x }
+ textChunks = [];
- for ( i = 0, len = rangeMappings.length; i < len; i++ ) {
- rangeMapping = rangeMappings[ i ];
- oldTextChunk = this.getTextChunkAt( rangeMapping.source.start );
- newText = targetText.substr( rangeMapping.target.start,
rangeMapping.target.length );
- newTextChunks.push( new TextChunk(
- newText,
- oldTextChunk.tags,
- oldTextChunk.inlineContent
- ) );
+ function pushEmptyTextChunks( offset, chunks ) {
+ var c, cLen;
+ for ( c = 0, cLen = chunks.length; c < cLen; c++ ) {
+ textChunks.push( {
+ start: offset,
+ length: 0,
+ textChunk: chunks[ c ]
+ } );
+ }
}
- return new TextBlock( newTextChunks );
+
+ // Create map of empty text chunks, by offset
+ for ( i = 0, iLen = this.textChunks.length; i < iLen; i++ ) {
+ textChunk = this.textChunks[ i ];
+ offset = this.startOffsets[ i ];
+ if ( textChunk.text.length > 0 ) {
+ continue;
+ }
+ if ( !emptyTextChunks[ offset ] ) {
+ emptyTextChunks[ offset ] = [];
+ }
+ emptyTextChunks[ offset ].push( textChunk );
+ }
+ for ( offset in emptyTextChunks ) {
+ emptyTextChunkOffsets.push( offset );
+ }
+ emptyTextChunkOffsets.sort( function ( a, b ) { return a - b; } );
+
+ for ( i = 0, iLen = rangeMappings.length; i < iLen; i++ ) {
+ // Copy annotations from source text start offset
+ rangeMapping = rangeMappings[ i ];
+ sourceRangeEnd = rangeMapping.source.start +
rangeMapping.source.length;
+ targetRangeEnd = rangeMapping.target.start +
rangeMapping.target.length;
+ sourceTextChunk = this.getTextChunkAt(
rangeMapping.source.start );
+ text = targetText.substr( rangeMapping.target.start,
rangeMapping.target.length );
+ textChunks.push( {
+ start: rangeMapping.target.start,
+ length: rangeMapping.target.length,
+ textChunk: new TextChunk(
+ text,
+ sourceTextChunk.tags,
+ sourceTextChunk.inlineContent
+ )
+ } );
+
+ // Empty source text chunks will not be represented in the
target plaintext
+ // (because they have no plaintext representation). Therefore
we must clone each
+ // one manually into the target rich text.
+
+ // Iterate through all remaining emptyTextChunks
+ for ( j = 0; j < emptyTextChunkOffsets.length; j++ ) {
+ offset = emptyTextChunkOffsets[ j ];
+ // Check whether chunk is in range
+ if ( offset < rangeMapping.source.start || offset >
sourceRangeEnd ) {
+ continue;
+ }
+ // Push chunk into target text at the current point
+ pushEmptyTextChunks( targetRangeEnd, emptyTextChunks[
offset ] );
+ // Remove chunk from remaining list
+ delete emptyTextChunks[ offset ];
+ emptyTextChunkOffsets.splice( j, 1 );
+ // Decrement pointer to match removal
+ j--;
+ }
+ }
+ // Sort by start position
+ textChunks.sort( function ( textChunk1, textChunk2 ) {
+ return textChunk1.start - textChunk2.start;
+ } );
+ // Fill in any textChunk gaps with unannotated text
+ pos = 0;
+ for ( i = 0, iLen = textChunks.length; i < iLen; i++ ) {
+ textChunk = textChunks[ i ];
+ if ( textChunk.start < pos ) {
+ throw new Error( 'Overlappping chunks at pos=' + pos +
', i=' + i );
+ } else if ( textChunk.start > pos ) {
+ // Unmapped chunk: insert plaintext and adjust offset
+ textChunks.splice( i, 0, {
+ start: pos,
+ length: textChunk.start - pos,
+ textChunk: new TextChunk(
+ targetText.substr( pos, textChunk.start
- pos ),
+ []
+ )
+ } );
+ i++;
+ iLen++;
+ }
+ pos = textChunk.start + textChunk.length;
+ }
+
+ // Get trailing text and trailing whitespace
+ tail = targetText.substr( pos );
+ tailSpace = tail.match( /\s*$/ )[ 0 ];
+ if ( tailSpace ) {
+ tail = tail.substr( 0, tail.length - tailSpace.length );
+ }
+
+ if ( tail ) {
+ // Append tail as unannotated text
+ textChunks.push( {
+ start: pos,
+ length: tail.length,
+ textChunk: new TextChunk( tail, [] )
+ } );
+ pos += tail.length;
+ }
+
+ // Copy any remaining textChunks that have no text
+ for ( i = 0, iLen = emptyTextChunkOffsets.length; i < iLen; i++ ) {
+ offset = emptyTextChunkOffsets[ i ];
+ pushEmptyTextChunks( pos, emptyTextChunks[ offset ] );
+ }
+ if ( tailSpace ) {
+ // Append tailSpace as unannotated text
+ textChunks.push( {
+ start: pos,
+ length: tailSpace.length,
+ textChunk: new TextChunk( tailSpace, [] )
+ } );
+ pos += tail.length;
+ }
+ return new TextBlock( textChunks.map( function ( x ) { return
x.textChunk; } ) );
};
/**
diff --git a/tests/lineardoc/LinearDoc.test.js
b/tests/lineardoc/LinearDoc.test.js
index af13187..7b09d22 100644
--- a/tests/lineardoc/LinearDoc.test.js
+++ b/tests/lineardoc/LinearDoc.test.js
@@ -1,6 +1,7 @@
QUnit.module( 'LinearDoc' );
-var fs = require( 'fs' );
+var fs = require( 'fs' ),
+ transTests = require( __dirname + '/translate.test.json' );
QUnit.test( 'LinearDoc tests', function ( assert ) {
var parser, testXhtmlFile, resultXmlFile, resultXhtmlFile, testXhtml,
resultXml,
@@ -30,3 +31,32 @@
);
}
} );
+
+QUnit.test( 'LinearDoc translateAnnotations', function ( assert ) {
+ var parser, textBlock1, textBlock2, i, len, test, doc;
+
+ QUnit.expect( 2 * transTests.length );
+
+ for ( i = 0, len = transTests.length; i < len; i++ ) {
+ test = transTests[ i ];
+ parser = new CX.LinearDoc.Parser();
+ parser.init();
+ parser.write( '<div>' + test.source + '</div>' );
+ doc = parser.builder.doc;
+ textBlock1 = parser.builder.doc.items[ 1 ].item;
+ assert.strictEqual(
+ textBlock1.getHtml(),
+ test.source,
+ 'Reconstructed source HTML'
+ );
+ textBlock2 = textBlock1.translateAnnotations(
+ test.targetText,
+ test.rangeMappings
+ );
+ assert.strictEqual(
+ textBlock2.getHtml(),
+ test.expect,
+ 'Translated HTML'
+ );
+ }
+} );
diff --git a/tests/lineardoc/translate.test.json
b/tests/lineardoc/translate.test.json
new file mode 100644
index 0000000..da48d1c
--- /dev/null
+++ b/tests/lineardoc/translate.test.json
@@ -0,0 +1,75 @@
+[
+{
+ "source": "A <b>big<span typeof=\"mw:Extension/ref\">[1]</span>
<i>brown</i><span typeof=\"mw:Extension/ref\">[2]</span> </b><u>dog</u> from
<img /> the <a href=\"GB\">United Kingdom</a><span
typeof=\"mw:Extension/ref\">[3]</span>. ",
+ "targetText": "The hound (enormous and brown) from Great Britain. ",
+ "rangeMappings": [
+ {
+ "source": { "start": 12, "length": 3 },
+ "target": { "start": 4, "length": 5 }
+ },
+ {
+ "source": { "start": 2, "length": 9 },
+ "target": { "start": 11, "length": 18 }
+ },
+ {
+ "source": { "start": 27, "length": 14 },
+ "target": { "start": 36, "length": 13 }
+ }
+ ],
+ "expect": "The <u>hound</u> (<b>enormous and brown<span
typeof=\"mw:Extension/ref\">[1]</span><span
typeof=\"mw:Extension/ref\">[2]</span></b>) from <a href=\"GB\">Great
Britain</a><span typeof=\"mw:Extension/ref\">[3]</span>.<img /> "
+},
+{
+ "source": "x <span data-mw=\"foo\" />y <span data-mw=\"bar\" />z. ",
+ "targetText": "Z X Y. ",
+ "rangeMappings": [
+ {
+ "source": { "start": 0, "length": 3 },
+ "target": { "start": 2, "length": 3 }
+ },
+ {
+ "source": { "start": 5, "length": 1 },
+ "target": { "start": 0, "length": 1 }
+ }
+ ],
+ "expect": "Z X Y<span data-mw=\"foo\" />.<span data-mw=\"bar\" /> "
+},
+{
+ "source": "<span data-mw=\"x\">\n</span>",
+ "targetText": "",
+ "rangeMappings": [],
+ "expect": "<span data-mw=\"x\">\n</span>"
+},
+{
+ "source": "s <span>\n</span>",
+ "targetText": "S \n",
+ "rangeMappings": [],
+ "expect": "S \n"
+},
+{
+ "source": "<b>a1 a2</b> <u>b1 b2</u> <i>c1 c2</i>",
+ "targetText": "C1 A1 B1 B2 A2 C2",
+ "rangeMappings": [
+ {
+ "source": { "start": 0, "length": 5 },
+ "target": { "start": 3, "length": 2 }
+ },
+ {
+ "source": { "start": 0, "length": 5 },
+ "target": { "start": 12, "length": 2 }
+ },
+ {
+ "source": { "start": 6, "length": 5 },
+ "target": { "start": 6, "length": 5 }
+ },
+ {
+ "source": { "start": 12, "length": 5 },
+ "target": { "start": 0, "length": 2 }
+ },
+ {
+ "source": { "start": 12, "length": 5 },
+ "target": { "start": 15, "length": 2 }
+ }
+ ],
+ "expect": "<i>C1</i> <b>A1</b> <u>B1 B2</u> <b>A2</b> <i>C2</i>"
+}
+]
--
To view, visit https://gerrit.wikimedia.org/r/156518
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I960dbe6155a5ea859b716e4a8748002a513618a6
Gerrit-PatchSet: 8
Gerrit-Project: mediawiki/services/cxserver
Gerrit-Branch: master
Gerrit-Owner: Divec <[email protected]>
Gerrit-Reviewer: Amire80 <[email protected]>
Gerrit-Reviewer: Divec <[email protected]>
Gerrit-Reviewer: Santhosh <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits