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

Change subject: Autolink URLs when typing
......................................................................


Autolink URLs when typing

Create ve.ui.LinkAction with an `autolinkUrl` method which autolinks
a selected URL, and a RegExp-based ve.ui.Sequence which matches
autolink-able text.

Add new `unanchoredExternalLinkUrlProtocolsRegExp` which is like the
existing `externalLinkUrlProtocolsRegExp` except it is not anchored
to the start of the string.  This allows you to execute searches for
possible links and/or add your own boundary conditions at the start
of the RegExp.  Related change I26904c85cad5de45856f1e1eea45fa14cafcbfe3
adds support for this to ve.init.mw.Platform.

Change-Id: I21976ce0cc823af29bdd2a5c163476fea2408488
---
M build/modules.json
M demos/ve/desktop.html
M demos/ve/mobile.html
M src/init/sa/ve.init.sa.Platform.js
M src/init/ve.init.Platform.js
A src/ui/actions/ve.ui.LinkAction.js
M src/ui/ve.ui.CommandRegistry.js
M tests/index.html
A tests/ui/actions/ve.ui.LinkAction.test.js
9 files changed, 225 insertions(+), 1 deletion(-)

Approvals:
  Cscott: Looks good to me, approved
  Esanders: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/build/modules.json b/build/modules.json
index ff294a6..02718c4 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -405,6 +405,7 @@
                        "src/ui/actions/ve.ui.FormatAction.js",
                        "src/ui/actions/ve.ui.HistoryAction.js",
                        "src/ui/actions/ve.ui.IndentationAction.js",
+                       "src/ui/actions/ve.ui.LinkAction.js",
                        "src/ui/actions/ve.ui.ListAction.js",
                        "src/ui/actions/ve.ui.TableAction.js",
                        "src/ui/actions/ve.ui.WindowAction.js",
@@ -558,6 +559,7 @@
                        "tests/ui/actions/ve.ui.AnnotationAction.test.js",
                        "tests/ui/actions/ve.ui.FormatAction.test.js",
                        "tests/ui/actions/ve.ui.IndentationAction.test.js",
+                       "tests/ui/actions/ve.ui.LinkAction.test.js",
                        "tests/ui/actions/ve.ui.ListAction.test.js",
                        "tests/ui/actions/ve.ui.TableAction.test.js",
                        
"tests/ui/datatransferhandlers/ve.ui.DSVFileTransferHandler.test.js",
diff --git a/demos/ve/desktop.html b/demos/ve/desktop.html
index 4974370..0a6289b 100644
--- a/demos/ve/desktop.html
+++ b/demos/ve/desktop.html
@@ -366,6 +366,7 @@
                <script 
src="../../src/ui/actions/ve.ui.FormatAction.js"></script>
                <script 
src="../../src/ui/actions/ve.ui.HistoryAction.js"></script>
                <script 
src="../../src/ui/actions/ve.ui.IndentationAction.js"></script>
+               <script src="../../src/ui/actions/ve.ui.LinkAction.js"></script>
                <script src="../../src/ui/actions/ve.ui.ListAction.js"></script>
                <script 
src="../../src/ui/actions/ve.ui.TableAction.js"></script>
                <script 
src="../../src/ui/actions/ve.ui.WindowAction.js"></script>
diff --git a/demos/ve/mobile.html b/demos/ve/mobile.html
index de4f1b2..210d3b4 100644
--- a/demos/ve/mobile.html
+++ b/demos/ve/mobile.html
@@ -368,6 +368,7 @@
                <script 
src="../../src/ui/actions/ve.ui.FormatAction.js"></script>
                <script 
src="../../src/ui/actions/ve.ui.HistoryAction.js"></script>
                <script 
src="../../src/ui/actions/ve.ui.IndentationAction.js"></script>
+               <script src="../../src/ui/actions/ve.ui.LinkAction.js"></script>
                <script src="../../src/ui/actions/ve.ui.ListAction.js"></script>
                <script 
src="../../src/ui/actions/ve.ui.TableAction.js"></script>
                <script 
src="../../src/ui/actions/ve.ui.WindowAction.js"></script>
diff --git a/src/init/sa/ve.init.sa.Platform.js 
b/src/init/sa/ve.init.sa.Platform.js
index aa83c61..76452b9 100644
--- a/src/init/sa/ve.init.sa.Platform.js
+++ b/src/init/sa/ve.init.sa.Platform.js
@@ -27,6 +27,7 @@
 
        // Properties
        this.externalLinkUrlProtocolsRegExp = /^https?\:\/\//;
+       this.unanchoredExternalLinkUrlProtocolsRegExp = /https?\:\/\//;
        this.messagePaths = messagePaths || [];
        this.parsedMessages = {};
        this.userLanguages = [ 'en' ];
@@ -43,6 +44,11 @@
        return this.externalLinkUrlProtocolsRegExp;
 };
 
+/** @inheritdoc */
+ve.init.sa.Platform.prototype.getUnanchoredExternalLinkUrlProtocolsRegExp = 
function () {
+       return this.unanchoredExternalLinkUrlProtocolsRegExp;
+};
+
 /**
  * Get message folder paths
  *
diff --git a/src/init/ve.init.Platform.js b/src/init/ve.init.Platform.js
index 14cd008..674c8ff 100644
--- a/src/init/ve.init.Platform.js
+++ b/src/init/ve.init.Platform.js
@@ -99,7 +99,8 @@
 /* Methods */
 
 /**
- * Get a regular expression that matches allowed external link URLs.
+ * Get an anchored regular expression that matches allowed external link URLs
+ * starting at the beginning of an input string.
  *
  * @method
  * @abstract
@@ -108,6 +109,16 @@
 ve.init.Platform.prototype.getExternalLinkUrlProtocolsRegExp = null;
 
 /**
+ * Get an unanchored regular expression that matches allowed external link URLs
+ * anywhere in an input string.
+ *
+ * @method
+ * @abstract
+ * @returns {RegExp} Regular expression object
+ */
+ve.init.Platform.prototype.getUnanchoredExternalLinkUrlProtocolsRegExp = null;
+
+/**
  * Get a config value from the platform.
  *
  * @method
diff --git a/src/ui/actions/ve.ui.LinkAction.js 
b/src/ui/actions/ve.ui.LinkAction.js
new file mode 100644
index 0000000..4e118a2
--- /dev/null
+++ b/src/ui/actions/ve.ui.LinkAction.js
@@ -0,0 +1,129 @@
+/*!
+ * VisualEditor UserInterface LinkAction class.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * Link action.
+ * This action transforms or inspects links (or potential links).
+ *
+ * @class
+ * @extends ve.ui.Action
+ * @constructor
+ * @param {ve.ui.Surface} surface Surface to act on
+ */
+ve.ui.LinkAction = function VeUiLinkAction( surface ) {
+       // Parent constructor
+       ve.ui.Action.call( this, surface );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.LinkAction, ve.ui.Action );
+
+/* Static Properties */
+
+ve.ui.LinkAction.static.name = 'link';
+
+/**
+ * RegExp matching an autolink + trailing space.
+ * @property {RegExp}
+ * @private
+ */
+ve.ui.LinkAction.static.autolinkRegExp = null; // Initialized below.
+
+/**
+ * List of allowed methods for the action.
+ *
+ * @static
+ * @property
+ */
+ve.ui.LinkAction.static.methods = [ 'autolinkUrl' ];
+
+/* Methods */
+
+/**
+ * Autolink the selection (which may have trailing whitespace).
+ *
+ * @method
+ * @return {boolean} Action was executed
+ */
+ve.ui.LinkAction.prototype.autolinkUrl = function () {
+       var range, rangeEnd, linktext, i,
+               surfaceModel = this.surface.getModel(),
+               documentModel = surfaceModel.getDocument(),
+               selection = surfaceModel.getSelection();
+
+       function isLinkAnnotation( annotation ) {
+               return /^link/.test( annotation.name );
+       }
+
+       if ( !( selection instanceof ve.dm.LinearSelection ) ) {
+               return false;
+       }
+
+       range = selection.getRange();
+       rangeEnd = range.end;
+
+       // Shrink range to eliminate trailing whitespace.
+       linktext = documentModel.data.getText( true, range ).replace( /\s+$/, 
'' );
+       range = range.truncate( linktext.length );
+
+       // Check that none of the range has an existing link annotation.
+       // Otherwise we could autolink an internal link, which would be ungood.
+       for ( i = range.start; i < range.end; i++ ) {
+               if ( documentModel.data.getAnnotationsFromOffset( i 
).containsMatching( isLinkAnnotation ) ) {
+                       // Don't autolink this.
+                       return false;
+               }
+       }
+
+       // Make sure `undo` doesn't expose the selected linktext.
+       surfaceModel.setLinearSelection( new ve.Range( rangeEnd, rangeEnd ) );
+
+       // Annotate the (previous) range.
+       surfaceModel.change(
+               ve.dm.Transaction.newFromAnnotation(
+                       documentModel,
+                       range,
+                       'set',
+                       this.getLinkAnnotation( linktext )
+               ),
+               surfaceModel.getSelection()
+       );
+
+       return true;
+};
+
+/**
+ * Return an appropriate annotation for the given href.
+ *
+ * @method
+ * @return {ve.dm.LinkAnnotation} The annotation to use.
+ */
+ve.ui.LinkAction.prototype.getLinkAnnotation = function ( href ) {
+       return new ve.dm.LinkAnnotation( {
+               type: 'link',
+               attributes: {
+                       href: href
+               }
+       } );
+};
+
+/* Registration */
+
+ve.ui.actionFactory.register( ve.ui.LinkAction );
+
+// Delayed initialization (wait until ve.init.platform exists)
+ve.init.Platform.static.initializedPromise.then( function () {
+
+       ve.ui.LinkAction.static.autolinkRegExp =
+               new RegExp(
+                       '\\b' + 
ve.init.platform.getUnanchoredExternalLinkUrlProtocolsRegExp().source + 
'\\S+(\\s|\\n\\n)$'
+               );
+
+       ve.ui.sequenceRegistry.register(
+               new ve.ui.Sequence( 'autolinkUrl', 'autolinkUrl', 
ve.ui.LinkAction.static.autolinkRegExp, 0, true )
+       );
+} );
diff --git a/src/ui/ve.ui.CommandRegistry.js b/src/ui/ve.ui.CommandRegistry.js
index 38d5d37..bdffba3 100644
--- a/src/ui/ve.ui.CommandRegistry.js
+++ b/src/ui/ve.ui.CommandRegistry.js
@@ -216,6 +216,12 @@
 );
 ve.ui.commandRegistry.register(
        new ve.ui.Command(
+               'autolinkUrl', 'link', 'autolinkUrl',
+               { supportedSelections: [ 'linear' ] }
+       )
+);
+ve.ui.commandRegistry.register(
+       new ve.ui.Command(
                'pasteSpecial', 'content', 'pasteSpecial',
                { supportedSelections: [ 'linear', 'table' ] }
        )
diff --git a/tests/index.html b/tests/index.html
index c979d43..bef18fc 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -295,6 +295,7 @@
                <script src="../src/ui/actions/ve.ui.FormatAction.js"></script>
                <script src="../src/ui/actions/ve.ui.HistoryAction.js"></script>
                <script 
src="../src/ui/actions/ve.ui.IndentationAction.js"></script>
+               <script src="../src/ui/actions/ve.ui.LinkAction.js"></script>
                <script src="../src/ui/actions/ve.ui.ListAction.js"></script>
                <script src="../src/ui/actions/ve.ui.TableAction.js"></script>
                <script src="../src/ui/actions/ve.ui.WindowAction.js"></script>
@@ -409,6 +410,7 @@
                <script 
src="../tests/ui/actions/ve.ui.AnnotationAction.test.js"></script>
                <script 
src="../tests/ui/actions/ve.ui.FormatAction.test.js"></script>
                <script 
src="../tests/ui/actions/ve.ui.IndentationAction.test.js"></script>
+               <script 
src="../tests/ui/actions/ve.ui.LinkAction.test.js"></script>
                <script 
src="../tests/ui/actions/ve.ui.ListAction.test.js"></script>
                <script 
src="../tests/ui/actions/ve.ui.TableAction.test.js"></script>
                <script 
src="../tests/ui/datatransferhandlers/ve.ui.DSVFileTransferHandler.test.js"></script>
diff --git a/tests/ui/actions/ve.ui.LinkAction.test.js 
b/tests/ui/actions/ve.ui.LinkAction.test.js
new file mode 100644
index 0000000..5c92a34
--- /dev/null
+++ b/tests/ui/actions/ve.ui.LinkAction.test.js
@@ -0,0 +1,66 @@
+/*!
+ * VisualEditor UserInterface Actions LinkAction tests.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+QUnit.module( 've.ui.LinkAction' );
+
+/* Tests */
+
+function runAutolinkTest( assert, html, method, range, expectedRange, 
expectedData, expectedOriginalData, msg ) {
+       var surface = ve.test.utils.createModelOnlySurfaceFromHtml( html || 
ve.dm.example.html ),
+               linkAction = new ve.ui.LinkAction( surface ),
+               data = ve.copy( surface.getModel().getDocument().getFullData() 
),
+               originalData = ve.copy( data );
+
+       expectedData( data );
+       if ( expectedOriginalData ) {
+               expectedOriginalData( originalData );
+       }
+       surface.getModel().setLinearSelection( range );
+       linkAction[method]();
+
+       assert.equalLinearData( surface.getModel().getDocument().getFullData(), 
data, msg + ': data models match' );
+       assert.equalRange( surface.getModel().getSelection().getRange(), 
expectedRange, msg + ': ranges match' );
+
+       surface.getModel().undo();
+
+       assert.equalLinearData( surface.getModel().getDocument().getFullData(), 
originalData, msg + ' (undo): data models match' );
+       assert.equalRange( surface.getModel().getSelection().getRange(), 
expectedRange, msg + ' (undo): ranges match' );
+}
+
+QUnit.test( 'autolink', function ( assert ) {
+       var i,
+               cases = [
+                       {
+                               html: '<p>http://example.com xyz</p>',
+                               range: new ve.Range( 1, 20 ),
+                               method: 'autolinkUrl',
+                               expectedRange: new ve.Range( 20, 20 ),
+                               expectedData: function ( data ) {
+                                       for ( var i = 1; i < 19; i++ ) {
+                                               data[i] = [ data[i], [ 0 ] ];
+                                       }
+                               },
+                               msg: 'Autolink after space'
+                       },
+                       {
+                               html: '<p>http://example.com</p><p>xyz</p>',
+                               range: new ve.Range( 1, 21 ),
+                               method: 'autolinkUrl',
+                               expectedRange: new ve.Range( 21, 21 ),
+                               expectedData: function ( data ) {
+                                       for ( var i = 1; i < 19; i++ ) {
+                                               data[i] = [ data[i], [ 0 ] ];
+                                       }
+                               },
+                               msg: 'Autolink after newline'
+                       }
+               ];
+
+       QUnit.expect( cases.length * 4 );
+       for ( i = 0; i < cases.length; i++ ) {
+               runAutolinkTest( assert, cases[i].html, cases[i].method, 
cases[i].range, cases[i].expectedRange, cases[i].expectedData, 
cases[i].expectedOriginalData, cases[i].msg );
+       }
+} );

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I21976ce0cc823af29bdd2a5c163476fea2408488
Gerrit-PatchSet: 8
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Cscott <[email protected]>
Gerrit-Reviewer: Cscott <[email protected]>
Gerrit-Reviewer: Esanders <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

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

Reply via email to