jenkins-bot has submitted this change and it was merged.

Change subject: Detect paste of wikitext and convert it
......................................................................


Detect paste of wikitext and convert it

This invokes Parsoid to convert likely wikitext pastes.

This addresses about 20% of the <nowiki> insertion quantified at
https://fr.wikipedia.org/wiki/Wikip%C3%A9dia:%C3%89diteurVisuel/Avis/Nowiki
for 2015-04-08 which were due to inadvertent pastes of wikitext.

For a better user experience, suppress Parsoid's P-wrapping if the
output consists of a single top-level P element.  This ensures that
pasting '[[Foo]]' won't add an unexpected newline before the paste.

Bug: T54204
Bug: T109449
Change-Id: I26a4cd8dc5b7e7caf16ca081dbe7baf6a7db8e5c
---
M VisualEditor.hooks.php
M extension.json
M modules/ve-mw/ce/nodes/ve.ce.MWTransclusionNode.js
A 
modules/ve-mw/tests/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.test.js
A modules/ve-mw/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.js
5 files changed, 358 insertions(+), 1 deletion(-)

Approvals:
  Jforrester: Looks good to me, but someone else must approve
  Esanders: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/VisualEditor.hooks.php b/VisualEditor.hooks.php
index 41cb6bf..1a79507 100644
--- a/VisualEditor.hooks.php
+++ b/VisualEditor.hooks.php
@@ -611,6 +611,7 @@
                                // VisualEditor DataTransferHandler tests
                                
'lib/ve/tests/ui/datatransferhandlers/ve.ui.DSVFileTransferHandler.test.js',
                                
'lib/ve/tests/ui/datatransferhandlers/ve.ui.UrlStringTransferHandler.test.js',
+                               
'modules/ve-mw/tests/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.test.js',
                                // VisualEditor initialization Tests
                                'lib/ve/tests/init/ve.init.Platform.test.js',
                                
'modules/ve-mw/tests/init/targets/ve.init.mw.DesktopArticleTarget.test.js',
diff --git a/extension.json b/extension.json
index e999934..183e459 100644
--- a/extension.json
+++ b/extension.json
@@ -950,6 +950,7 @@
                                "modules/ve-mw/ui/ve.ui.MWCommandRegistry.js",
                                "modules/ve-mw/ui/ve.ui.MWSequenceRegistry.js",
                                
"modules/ve-mw/ui/commands/ve.ui.MWWikitextWarningCommand.js",
+                               
"modules/ve-mw/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.js",
                                
"modules/ve-mw/ui/widgets/ve.ui.MWTargetWidget.js",
                                
"modules/ve-mw/ui/widgets/ve.ui.MWTocItemWidget.js",
                                "modules/ve-mw/ui/widgets/ve.ui.MWTocWidget.js",
diff --git a/modules/ve-mw/ce/nodes/ve.ce.MWTransclusionNode.js 
b/modules/ve-mw/ce/nodes/ve.ce.MWTransclusionNode.js
index 966c624..fd2ac49 100644
--- a/modules/ve-mw/ce/nodes/ve.ce.MWTransclusionNode.js
+++ b/modules/ve-mw/ce/nodes/ve.ce.MWTransclusionNode.js
@@ -127,7 +127,7 @@
 ve.ce.MWTransclusionNode.prototype.onParseSuccess = function ( deferred, 
response ) {
        var contentNodes;
 
-       if ( !response || response.error || !response.visualeditor || 
response.visualeditor.result !== 'success' ) {
+       if ( ve.getProp( response, 'visualeditor', 'result' ) !== 'success' ) {
                return this.onParseError.call( this, deferred );
        }
 
diff --git 
a/modules/ve-mw/tests/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.test.js
 
b/modules/ve-mw/tests/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.test.js
new file mode 100644
index 0000000..51801b1
--- /dev/null
+++ 
b/modules/ve-mw/tests/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.test.js
@@ -0,0 +1,233 @@
+/*!
+ * VisualEditor UserInterface MWWikitextStringTransferHandler tests.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+var MWWIKITEXT_MOCK_API = true;
+
+QUnit.module( 've.ui.MWWikitextStringTransferHandler', QUnit.newMwEnvironment( 
{
+       setup: function () {
+               // Mock XHR for mw.Api()
+               this.server = MWWIKITEXT_MOCK_API ? 
this.sandbox.useFakeServer() : null;
+       }
+} ) );
+
+/* Tests */
+
+function runWikitextStringHandlerTest( assert, server, string, mimeType, 
expectedResponse, expectedData, annotations, msg ) {
+       var handler, i, j,
+               done = assert.async(),
+               item = ve.ui.DataTransferItem.static.newFromString( string, 
mimeType ),
+               doc = ve.dm.example.createExampleDocument(),
+               mockSurface = {
+                       getModel: function () {
+                               return {
+                                       getDocument: function () {
+                                               return doc;
+                                       }
+                               };
+                       }
+               };
+
+       // Preprocess the expectedData array
+       for ( i = 0; i < expectedData.length; i++ ) {
+               if ( Array.isArray( expectedData[ i ] ) ) {
+                       for ( j = 0; j < expectedData[ i ][ 1 ].length; j++ ) {
+                               if ( typeof expectedData[ i ][ 1 ][ j ] === 
'number' ) {
+                                       expectedData[ i ][ 1 ][ j ] = 
annotations[ expectedData[ i ][ 1 ][ j ] ];
+                               }
+                       }
+               }
+       }
+
+       // Invoke the handler
+       handler = ve.ui.dataTransferHandlerFactory.create( 'wikitextString', 
mockSurface, item );
+
+       handler.getInsertableData().done( function ( doc2 ) {
+               var actualData = doc2.getData();
+               ve.dm.example.postprocessAnnotations( actualData, 
doc2.getStore() );
+               assert.equalLinearData( actualData, expectedData, msg + ': data 
match' );
+               done();
+       } );
+
+       if ( server ) {
+               server.respond( [ 200, { 'Content-Type': 'application/json' }, 
JSON.stringify( {
+                       visualeditor: {
+                               result: 'success',
+                               content: expectedResponse
+                       }
+               } ) ] );
+       }
+}
+
+QUnit.test( 'convert', function ( assert ) {
+       var i,
+               cases = [
+                       {
+                               msg: 'Simple link',
+                               pasteString: '[[Foo]]',
+                               pasteType: 'text/plain',
+                               parsoidResponse: '<body 
data-parsoid=\'{"dsr":[0,7,0,0]}\' lang="en" class="mw-content-ltr sitedir-ltr 
ltr mw-body mw-body-content mediawiki" dir="ltr"><p 
data-parsoid=\'{"dsr":[0,7,0,0]}\'><a rel="mw:WikiLink" href="./Foo" 
title="Foo" 
data-parsoid=\'{"stx":"simple","a":{"href":"./Foo"},"sa":{"href":"Foo"},"dsr":[0,7,2,2]}\'>Foo</a></p></body>',
+                               annotations: [ {
+                                       type: 'link/mwInternal',
+                                       attributes: {
+                                               hrefPrefix: './',
+                                               lookupTitle: 'Foo',
+                                               normalizedTitle: 'Foo',
+                                               origTitle: 'Foo',
+                                               title: 'Foo'
+                                       }
+                               } ],
+                               expectedData: [
+                                       [ 'F', [ 0 ] ],
+                                       [ 'o', [ 0 ] ],
+                                       [ 'o', [ 0 ] ],
+                                       { type: 'internalList' },
+                                       { type: '/internalList' }
+                               ]
+                       },
+                       {
+                               msg: 'Simple link with no p-wrapping',
+                               pasteString: '*[[Foo]]',
+                               pasteType: 'text/plain',
+                               parsoidResponse: '<body 
data-parsoid=\'{"dsr":[0,8,0,0]}\' lang="en" class="mw-content-ltr sitedir-ltr 
ltr mw-body mw-body-content mediawiki" dir="ltr"><ul 
data-parsoid=\'{"dsr":[0,8,0,0]}\'><li data-parsoid=\'{"dsr":[0,8,1,0]}\'><a 
rel="mw:WikiLink" href="./Foo" title="Foo" 
data-parsoid=\'{"stx":"simple","a":{"href":"./Foo"},"sa":{"href":"Foo"},"dsr":[1,8,2,2]}\'>Foo</a></li></ul></body>',
+                               annotations: [ {
+                                       type: 'link/mwInternal',
+                                       attributes: {
+                                               hrefPrefix: './',
+                                               lookupTitle: 'Foo',
+                                               normalizedTitle: 'Foo',
+                                               origTitle: 'Foo',
+                                               title: 'Foo'
+                                       }
+                               } ],
+                               expectedData: [
+                                       {
+                                               type: 'list',
+                                               attributes: { style: 'bullet' }
+                                       },
+                                       { type: 'listItem' },
+                                       {
+                                               type: 'paragraph',
+                                               internal: { generated: 
'wrapper' }
+                                       },
+                                       [ 'F', [ 0 ] ],
+                                       [ 'o', [ 0 ] ],
+                                       [ 'o', [ 0 ] ],
+                                       { type: '/paragraph' },
+                                       { type: '/listItem' },
+                                       { type: '/list' },
+                                       { type: 'internalList' },
+                                       { type: '/internalList' }
+                               ]
+                       },
+                       {
+                               msg: 'Heading',
+                               pasteString: '==heading==',
+                               pasteType: 'text/plain',
+                               parsoidResponse: '<body 
data-parsoid=\'{"dsr":[0,11,0,0]}\' lang="en" class="mw-content-ltr sitedir-ltr 
ltr mw-body mw-body-content mediawiki" dir="ltr"><h2 
data-parsoid=\'{"dsr":[0,11,2,2]}\'>heading</h2></body>',
+                               annotations: [],
+                               expectedData: [
+                                       { type: 'heading', attributes: { level: 
2 } },
+                                       'h',
+                                       'e',
+                                       'a',
+                                       'd',
+                                       'i',
+                                       'n',
+                                       'g',
+                                       { type: '/heading' },
+                                       { type: 'internalList' },
+                                       { type: '/internalList' }
+                               ]
+                       },
+                       {
+                               msg: 'Magic link (RFC)',
+                               pasteString: 'RFC 1234',
+                               pasteType: 'text/plain',
+                               parsoidResponse: '<body 
data-parsoid=\'{"dsr":[0,8,0,0]}\' lang="en" class="mw-content-ltr sitedir-ltr 
ltr mw-body mw-body-content mediawiki" dir="ltr"><p 
data-parsoid=\'{"dsr":[0,8,0,0]}\'><a href="//tools.ietf.org/html/rfc1234" 
rel="mw:ExtLink" data-parsoid=\'{"stx":"magiclink","dsr":[0,8,0,0]}\'>RFC 
1234</a></p></body>',
+                               annotations: [ {
+                                       type: 'link/mwExternal',
+                                       attributes: {
+                                               href: 
'//tools.ietf.org/html/rfc1234',
+                                               rel: 'mw:ExtLink'
+                                       }
+                               } ],
+                               expectedData: [
+                                       [ 'R', [ 0 ] ],
+                                       [ 'F', [ 0 ] ],
+                                       [ 'C', [ 0 ] ],
+                                       [ ' ', [ 0 ] ],
+                                       [ '1', [ 0 ] ],
+                                       [ '2', [ 0 ] ],
+                                       [ '3', [ 0 ] ],
+                                       [ '4', [ 0 ] ],
+                                       { type: 'internalList' },
+                                       { type: '/internalList' }
+                               ]
+                       },
+                       {
+                               msg: 'Magic link (PMID)',
+                               pasteString: 'PMID 1234',
+                               pasteType: 'text/plain',
+                               parsoidResponse: '<body 
data-parsoid=\'{"dsr":[0,9,0,0]}\' lang="en" class="mw-content-ltr sitedir-ltr 
ltr mw-body mw-body-content mediawiki" dir="ltr"><p 
data-parsoid=\'{"dsr":[0,9,0,0]}\'><a 
href="//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract" rel="mw:ExtLink" 
data-parsoid=\'{"stx":"magiclink","dsr":[0,9,0,0]}\'>PMID 1234</a></p></body>',
+                               annotations: [ {
+                                       type: 'link/mwExternal',
+                                       attributes: {
+                                               href: 
'//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract',
+                                               rel: 'mw:ExtLink'
+                                       }
+                               } ],
+                               expectedData: [
+                                       [ 'P', [ 0 ] ],
+                                       [ 'M', [ 0 ] ],
+                                       [ 'I', [ 0 ] ],
+                                       [ 'D', [ 0 ] ],
+                                       [ ' ', [ 0 ] ],
+                                       [ '1', [ 0 ] ],
+                                       [ '2', [ 0 ] ],
+                                       [ '3', [ 0 ] ],
+                                       [ '4', [ 0 ] ],
+                                       { type: 'internalList' },
+                                       { type: '/internalList' }
+                               ]
+                       },
+                       {
+                               msg: 'Magic link (ISBN)',
+                               pasteString: 'ISBN 123456789X',
+                               pasteType: 'text/plain',
+                               parsoidResponse: '<body 
data-parsoid=\'{"dsr":[0,15,0,0]}\' lang="en" class="mw-content-ltr sitedir-ltr 
ltr mw-body mw-body-content mediawiki" dir="ltr"><p 
data-parsoid=\'{"dsr":[0,15,0,0]}\'><a href="./Special:BookSources/123456789X" 
rel="mw:ExtLink" data-parsoid=\'{"stx":"magiclink","dsr":[0,15,0,0]}\'>ISBN 
123456789X</a></p></body>',
+                               annotations: [ {
+                                       type: 'link/mwExternal',
+                                       attributes: {
+                                               href: 
'./Special:BookSources/123456789X',
+                                               rel: 'mw:ExtLink'
+                                       }
+                               } ],
+                               expectedData: [
+                                       [ 'I', [ 0 ] ],
+                                       [ 'S', [ 0 ] ],
+                                       [ 'B', [ 0 ] ],
+                                       [ 'N', [ 0 ] ],
+                                       [ ' ', [ 0 ] ],
+                                       [ '1', [ 0 ] ],
+                                       [ '2', [ 0 ] ],
+                                       [ '3', [ 0 ] ],
+                                       [ '4', [ 0 ] ],
+                                       [ '5', [ 0 ] ],
+                                       [ '6', [ 0 ] ],
+                                       [ '7', [ 0 ] ],
+                                       [ '8', [ 0 ] ],
+                                       [ '9', [ 0 ] ],
+                                       [ 'X', [ 0 ] ],
+                                       { type: 'internalList' },
+                                       { type: '/internalList' }
+                               ]
+                       }
+               ];
+
+       QUnit.expect( cases.length );
+       for ( i = 0; i < cases.length; i++ ) {
+               runWikitextStringHandlerTest( assert, this.server, 
cases[i].pasteString, cases[i].pasteType, cases[i].parsoidResponse, 
cases[i].expectedData, cases[i].annotations, cases[i].msg );
+       }
+} );
diff --git 
a/modules/ve-mw/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.js
 
b/modules/ve-mw/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.js
new file mode 100644
index 0000000..e520c2b
--- /dev/null
+++ 
b/modules/ve-mw/ui/datatransferhandlers/ve.ui.MWWikitextStringTransferHandler.js
@@ -0,0 +1,122 @@
+/*!
+ * VisualEditor UserInterface MWWikitextStringTransferHandler class.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * Detect an attempt to paste wikitext, and convert it to proper
+ * HTML.
+ *
+ * @class
+ * @extends ve.ui.PlainTextStringTransferHandler
+ *
+ * @constructor
+ * @param {ve.ui.Surface} surface
+ * @param {ve.ui.DataTransferItem} item
+ */
+ve.ui.MWWikitextStringTransferHandler = function 
VeUiMWWikitextStringTransferHandler() {
+       // Parent constructor
+       ve.ui.MWWikitextStringTransferHandler.super.apply( this, arguments );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.MWWikitextStringTransferHandler, 
ve.ui.PlainTextStringTransferHandler );
+
+/* Static properties */
+
+ve.ui.MWWikitextStringTransferHandler.static.name = 'wikitextString';
+
+ve.ui.MWWikitextStringTransferHandler.static.types =
+       ve.ui.MWWikitextStringTransferHandler.super.static.types.concat(
+               [ 'text/x-wiki' ]
+       );
+
+ve.ui.MWWikitextStringTransferHandler.static.handlesPaste = true;
+
+/**
+ * Heuristic pattern which attempts to discover wikitext, without
+ * incurring too many false positives.
+ *
+ * Currently the pattern looks for ==...==, [[...]], or {{...}}
+ * which occur on a single line of max 80 characters.
+ */
+ve.ui.MWWikitextStringTransferHandler.static.matchRegExp =
+       /(^\s*(={2,6})[^=\r\n]{1,80}\1\s*$)|\[\[.{1,80}\]\]|\{\{.{1,80}\}\}/m;
+
+ve.ui.MWWikitextStringTransferHandler.static.matchFunction = function ( item ) 
{
+       var text = item.getAsString();
+
+       // If the mime type is explicitly wikitext (ie, not plain text),
+       // always accept.
+       if ( item.type === 'text/x-wiki' ) {
+               return true;
+       }
+
+       // Detect autolink opportunities for magic words.
+       // (The link should be the only contents of paste to match this 
heuristic)
+       if ( /^\s*(RFC|ISBN|PMID)[-\s0-9]+[Xx]?\s*$/.test( text ) ) {
+               return true;
+       }
+
+       // Use a heuristic regexp to find text likely to be wikitext.
+       // This test could be made more sophisticated in the future.
+       if ( this.matchRegExp.test( text ) ) {
+               return true;
+       }
+       return false;
+};
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ui.MWWikitextStringTransferHandler.prototype.process = function () {
+       var xhr,
+               handler = this,
+               wikitext = this.item.getAsString();
+
+       function failure() {
+               // There's no DTH fallback handling for failures, so just paste
+               // the raw wikitext if things go wrong.
+               handler.resolve( wikitext );
+       }
+
+       // Convert wikitext to html using Parsoid.
+       xhr = new mw.Api().post( {
+               action: 'visualeditor',
+               paction: 'parsefragment',
+               page: mw.config.get( 'wgRelevantPageName' ),
+               wikitext: wikitext
+       } ).then( function ( response ) {
+               var doc, surface;
+               if ( ve.getProp( response, 'visualeditor', 'result' ) !== 
'success' ) {
+                       return failure();
+               }
+
+               doc = handler.surface.getModel().getDocument().newFromHtml(
+                       response.visualeditor.content,
+                       null // No sanitization, since HTML is from Parsoid
+               );
+
+               // Attempt to undo outermost p-wrapping if possible
+               surface = new ve.dm.Surface( doc );
+               try {
+                       surface.change(
+                               ve.dm.Transaction.newFromWrap( doc, new 
ve.Range( 0, doc.data.countNonInternalElements() ), [], [], [ { type: 
'paragraph' } ], [] )
+                       );
+               } catch ( e ) {
+                       // Sometimes there is no p-wrapping, for example: "* 
foo"
+                       // Sometimes there are multiple <p> tags in the output.
+                       // That's okay: ignore the error and paste what we've 
got.
+               }
+
+               handler.resolve( doc );
+       }, failure );
+};
+
+/* Registration */
+
+ve.ui.dataTransferHandlerFactory.register( 
ve.ui.MWWikitextStringTransferHandler );

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I26a4cd8dc5b7e7caf16ca081dbe7baf6a7db8e5c
Gerrit-PatchSet: 19
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Cscott <[email protected]>
Gerrit-Reviewer: Bartosz DziewoƄski <[email protected]>
Gerrit-Reviewer: Cscott <[email protected]>
Gerrit-Reviewer: Eranroz <[email protected]>
Gerrit-Reviewer: Esanders <[email protected]>
Gerrit-Reviewer: Jforrester <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

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

Reply via email to