Autolink typed ISBN/RFC/PMIDs

Depends on Ibdad2fa98fca08eeaa96bf33a08dd7723c1edb8c in Parsoid.

Depends on I3dcd289ed7b565b9162ee671038eeb45449e1215 in ve-core.

Bug: T109498
Change-Id: I5650410d7fca30c90baddd4f0c3f6d80e6b39042
This commit is contained in:
C. Scott Ananian 2015-08-18 16:24:45 -04:00 committed by James D. Forrester
parent 3adfa82ace
commit ceb0e1f867
2 changed files with 139 additions and 14 deletions

View file

@ -10,14 +10,17 @@ QUnit.module( 've.ui.MWLinkAction' );
/* Tests */ /* Tests */
function runMWAutolinkTest( assert, html, method, range, expectedRange, expectedData, expectedOriginalData, msg ) { function runMWAutolinkTest( assert, html, method, range, expectedRange, expectedData, expectedOriginalData, msg ) {
var status, var status, actualData,
expectFail = /^Don't/.test( msg ), expectFail = /^Don't/.test( msg ),
surface = ve.test.utils.createModelOnlySurfaceFromHtml( html || ve.dm.example.html ), surface = ve.test.utils.createModelOnlySurfaceFromHtml( html || ve.dm.example.html ),
linkAction = new ve.ui.MWLinkAction( surface ), linkAction = new ve.ui.MWLinkAction( surface ),
data = ve.copy( surface.getModel().getDocument().getFullData() ), data = ve.copy( surface.getModel().getDocument().getFullData() ),
originalData = ve.copy( data ); originalData = ve.copy( data ),
makeLinkAnnotation = function ( linktext ) {
return linkAction.getLinkAnnotation( linktext ).element;
};
expectedData( data ); expectedData( data, makeLinkAnnotation );
if ( expectedOriginalData ) { if ( expectedOriginalData ) {
expectedOriginalData( originalData ); expectedOriginalData( originalData );
} }
@ -25,7 +28,9 @@ function runMWAutolinkTest( assert, html, method, range, expectedRange, expected
status = linkAction[ method ](); status = linkAction[ method ]();
assert.equal( status, !expectFail, msg + ': action return value' ); assert.equal( status, !expectFail, msg + ': action return value' );
assert.equalLinearData( surface.getModel().getDocument().getFullData(), data, msg + ': data models match' ); actualData = surface.getModel().getDocument().getFullData();
ve.dm.example.postprocessAnnotations( actualData, surface.getModel().getDocument().getStore() );
assert.equalLinearData( actualData, data, msg + ': data models match' );
assert.equalRange( surface.getModel().getSelection().getRange(), expectedRange, msg + ': ranges match' ); assert.equalRange( surface.getModel().getSelection().getRange(), expectedRange, msg + ': ranges match' );
if ( status ) { if ( status ) {
@ -40,17 +45,90 @@ QUnit.test( 'MW autolink', function ( assert ) {
var i, var i,
cases = [ cases = [
{ {
msg: 'Strip trailing punctuation (but not matched parens)',
html: '<p>https://en.wikipedia.org/wiki/Red_(disambiguation) xyz</p>', html: '<p>https://en.wikipedia.org/wiki/Red_(disambiguation) xyz</p>',
range: new ve.Range( 1, 52 ), range: new ve.Range( 1, 52 ),
method: 'autolinkUrl', method: 'autolinkUrl',
expectedRange: new ve.Range( 52, 52 ), expectedRange: new ve.Range( 52, 52 ),
expectedData: function ( data ) { expectedData: function ( data, makeAnnotation ) {
var i; var i,
a = makeAnnotation( 'https://en.wikipedia.org/wiki/Red_(disambiguation)' );
for ( i = 1; i < 51; i++ ) { for ( i = 1; i < 51; i++ ) {
data[ i ] = [ data[ i ], [ 0 ] ]; data[ i ] = [ data[ i ], [ a ] ];
}
} }
}, },
msg: 'Strip trailing punctuation (but not matched parens)' {
msg: 'Autolink valid RFC',
html: '<p>RFC 1234 xyz</p>',
range: new ve.Range( 1, 10 ),
method: 'autolinkMagicLink',
expectedRange: new ve.Range( 10, 10 ),
expectedData: function ( data, makeAnnotation ) {
var i,
a = makeAnnotation( '//tools.ietf.org/html/rfc1234' );
for ( i = 1; i < 9; i++ ) {
data[ i ] = [ data[ i ], [ a ] ];
}
}
},
{
msg: 'Don\'t autolink invalid RFC',
html: '<p>RFC 123x xyz</p>',
range: new ve.Range( 1, 10 ),
method: 'autolinkMagicLink',
expectedRange: new ve.Range( 1, 10 ),
expectedData: function ( /*data, makeAnnotation*/ ) {
/* no change, no link */
}
},
{
msg: 'Autolink valid PMID',
html: '<p>PMID 1234 xyz</p>',
range: new ve.Range( 1, 11 ),
method: 'autolinkMagicLink',
expectedRange: new ve.Range( 11, 11 ),
expectedData: function ( data, makeAnnotation ) {
var i,
a = makeAnnotation( '//www.ncbi.nlm.nih.gov/pubmed/1234?dopt=Abstract' );
for ( i = 1; i < 10; i++ ) {
data[ i ] = [ data[ i ], [ a ] ];
}
}
},
{
msg: 'Don\'t autolink invalid PMID',
html: '<p>PMID 123x xyz</p>',
range: new ve.Range( 1, 11 ),
method: 'autolinkMagicLink',
expectedRange: new ve.Range( 1, 11 ),
expectedData: function ( /*data, makeAnnotation*/ ) {
/* no change, no link */
}
},
{
msg: 'Autolink valid ISBN',
html: '<p>ISBN 978-0596517748 xyz</p>',
range: new ve.Range( 1, 21 ),
method: 'autolinkMagicLink',
expectedRange: new ve.Range( 21, 21 ),
expectedData: function ( data, makeAnnotation ) {
var i,
a = makeAnnotation( './Special:BookSources/9780596517748' );
for ( i = 1; i < 20; i++ ) {
data[ i ] = [ data[ i ], [ a ] ];
}
}
},
{
msg: 'Don\'t autolink invalid ISBN',
html: '<p>ISBN 978-059651774 xyz</p>',
range: new ve.Range( 1, 20 ),
method: 'autolinkMagicLink',
expectedRange: new ve.Range( 1, 20 ),
expectedData: function ( /*data, makeAnnotation*/ ) {
/* no change, no link */
}
} }
]; ];

View file

@ -31,7 +31,7 @@ OO.inheritClass( ve.ui.MWLinkAction, ve.ui.LinkAction );
* @static * @static
* @property * @property
*/ */
ve.ui.MWLinkAction.static.methods = ve.ui.MWLinkAction.super.static.methods.concat( [ 'open' ] ); ve.ui.MWLinkAction.static.methods = ve.ui.MWLinkAction.super.static.methods.concat( [ 'open', 'autolinkMagicLink' ] );
/* Methods */ /* Methods */
@ -53,8 +53,20 @@ ve.ui.MWLinkAction.prototype.getTrailingPunctuation = function ( candidate ) {
* @inheritdoc * @inheritdoc
* @return {ve.dm.MWExternalLinkAnnotation} The annotation to use. * @return {ve.dm.MWExternalLinkAnnotation} The annotation to use.
*/ */
ve.ui.MWLinkAction.prototype.getLinkAnnotation = function ( href ) { ve.ui.MWLinkAction.prototype.getLinkAnnotation = function ( linktext ) {
var title, var title, targetData, m,
href = linktext;
// The link has been validated in #autolinkMagicLink and/or
// #autolinkUrl, so we can use a quick and dirty regexp here to pull
// apart the magic link.
m = /^(RFC|PMID|ISBN)\s+(\S.*)$/.exec( linktext );
if ( m && m[ 1 ] === 'RFC' ) {
href = '//tools.ietf.org/html/rfc' + m[ 2 ];
} else if ( m && m[ 1 ] === 'PMID' ) {
href = '//www.ncbi.nlm.nih.gov/pubmed/' + m[ 2 ] + '?dopt=Abstract';
} else if ( m && m[ 1 ] === 'ISBN' ) {
title = mw.Title.newFromText( 'Special:BookSources/' + m[ 2 ].replace( /[^0-9Xx]/g, '' ) );
} else {
targetData = ve.dm.MWInternalLinkAnnotation.static.getTargetDataFromHref( targetData = ve.dm.MWInternalLinkAnnotation.static.getTargetDataFromHref(
href, href,
this.surface.getModel().getDocument().getHtmlDocument() this.surface.getModel().getDocument().getHtmlDocument()
@ -62,6 +74,7 @@ ve.ui.MWLinkAction.prototype.getLinkAnnotation = function ( href ) {
if ( targetData.isInternal ) { if ( targetData.isInternal ) {
title = mw.Title.newFromText( targetData.title ); title = mw.Title.newFromText( targetData.title );
} }
}
return title ? return title ?
ve.dm.MWInternalLinkAnnotation.static.newFromTitle( title ) : ve.dm.MWInternalLinkAnnotation.static.newFromTitle( title ) :
new ve.dm.MWExternalLinkAnnotation( { new ve.dm.MWExternalLinkAnnotation( {
@ -70,6 +83,27 @@ ve.ui.MWLinkAction.prototype.getLinkAnnotation = function ( href ) {
} ); } );
}; };
/**
* Autolink the selected RFC/PMID/ISBN, which may have trailing whitespace.
*
* @see ve.ui.LinkAction#autolinkUrl
* @method
* @return {boolean}
* True if the selection is a valid RFC/PMID/ISBN and the autolink action
* was executed; otherwise false.
*/
ve.ui.MWLinkAction.prototype.autolinkMagicLink = function () {
return this.autolink( function ( linktext ) {
if ( /^(RFC|PMID) [0-9]+$/.test( linktext ) ) {
return true; // Valid RFC/PMID
}
if ( /^ISBN (97[89][- ]?)?([0-9][- ]?){9}[0-9Xx]$/.test( linktext ) ) {
return true; // Valid ISBN
}
return false;
} );
};
/** /**
* Open either the 'link' or 'linkNode' window, depending on what is selected. * Open either the 'link' or 'linkNode' window, depending on what is selected.
* *
@ -90,3 +124,16 @@ ve.ui.MWLinkAction.prototype.open = function () {
/* Registration */ /* Registration */
ve.ui.actionFactory.register( ve.ui.MWLinkAction ); ve.ui.actionFactory.register( ve.ui.MWLinkAction );
ve.ui.commandRegistry.register(
new ve.ui.Command(
'autolinkMagicLink', ve.ui.MWLinkAction.static.name, 'autolinkMagicLink',
{ supportedSelections: [ 'linear' ] }
)
);
ve.ui.sequenceRegistry.register(
// This regexp doesn't have to be precise; we'll validate the magic
// link in #autolinkMagicLink above.
new ve.ui.Sequence( 'autolinkMagicLink', 'autolinkMagicLink', /\b(RFC|PMID|ISBN)\s+[0-9]([- 0-9]*[0-9Xx])?(\s|\n\n)$/, 0, true )
);