mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-12-01 01:16:30 +00:00
Support wikitext link editing
Depends-On: I3eb2d5ee0da52942db1de75ef9f8b0ae2632657e Change-Id: I7ad61899a221a198a06e206614d907a331861dbb
This commit is contained in:
parent
66ba383d3e
commit
a007781e81
|
@ -2661,6 +2661,7 @@
|
|||
"modules/ve-mw/tests/ui/datatransferhandlers/ve.ui.UrlStringTransferHandler.test.js",
|
||||
"modules/ve-mw/tests/init/targets/ve.init.mw.DesktopArticleTarget.test.js",
|
||||
"lib/ve/tests/ui/inspectors/ve.ui.FragmentInspector.test.js",
|
||||
"modules/ve-mw/tests/ui/inspectors/ve.ui.FragmentInspector.test.js",
|
||||
"lib/ve/tests/ce/ve.ce.TestRunner.js",
|
||||
"lib/ve/tests/ce/ve.ce.imetests.test.js",
|
||||
"lib/ve/tests/ce/imetests/backspace-chromium-ubuntu-none.js",
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
/*!
|
||||
* VisualEditor UserInterface FragmentInspector tests.
|
||||
*
|
||||
* @copyright 2011-2019 VisualEditor Team and others; see http://ve.mit-license.org
|
||||
*/
|
||||
|
||||
QUnit.module( 've.ui.FragmentInspector (MW)', ve.test.utils.mwEnvironment );
|
||||
|
||||
/* Tests */
|
||||
|
||||
QUnit.test( 'Wikitext link inspector', function ( assert ) {
|
||||
var done = assert.async(),
|
||||
surface = ve.init.target.createSurface(
|
||||
ve.dm.converter.getModelFromDom(
|
||||
ve.createDocumentFromHtml(
|
||||
'<p>Foo [[bar]] [[Quux|baz]] x</p>' +
|
||||
'<p>wh]]ee</p>'
|
||||
)
|
||||
),
|
||||
{ mode: 'source' }
|
||||
),
|
||||
cases = [
|
||||
{
|
||||
msg: 'Collapsed selection expands to word',
|
||||
name: 'wikitextLink',
|
||||
range: new ve.Range( 2 ),
|
||||
expectedRange: new ve.Range( 1, 8 ),
|
||||
expectedData: function ( data ) {
|
||||
data.splice(
|
||||
1, 3,
|
||||
'[', '[', 'F', 'o', 'o', ']', ']'
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
msg: 'Collapsed selection in word (noExpand)',
|
||||
name: 'wikitextLink',
|
||||
range: new ve.Range( 2 ),
|
||||
setupData: { noExpand: true },
|
||||
expectedRange: new ve.Range( 2 ),
|
||||
expectedData: function () {}
|
||||
},
|
||||
{
|
||||
msg: 'Cancel restores original data & selection',
|
||||
name: 'wikitextLink',
|
||||
range: new ve.Range( 2 ),
|
||||
expectedRange: new ve.Range( 2 ),
|
||||
expectedData: function () {},
|
||||
actionData: {}
|
||||
},
|
||||
{
|
||||
msg: 'Collapsed selection inside existing link',
|
||||
name: 'wikitextLink',
|
||||
range: new ve.Range( 5 ),
|
||||
expectedRange: new ve.Range( 5, 12 ),
|
||||
expectedData: function () {}
|
||||
},
|
||||
{
|
||||
msg: 'Selection inside existing link',
|
||||
name: 'wikitextLink',
|
||||
range: new ve.Range( 19, 20 ),
|
||||
expectedRange: new ve.Range( 13, 25 ),
|
||||
expectedData: function () {}
|
||||
},
|
||||
{
|
||||
msg: 'Selection spanning existing link',
|
||||
name: 'wikitextLink',
|
||||
range: new ve.Range( 3, 8 ),
|
||||
expectedRange: new ve.Range( 3, 8 ),
|
||||
expectedData: function () {}
|
||||
},
|
||||
{
|
||||
msg: 'Selection with whitespace is trimmed',
|
||||
name: 'wikitextLink',
|
||||
range: new ve.Range( 1, 5 ),
|
||||
expectedRange: new ve.Range( 1, 8 )
|
||||
},
|
||||
{
|
||||
msg: 'Link insertion',
|
||||
name: 'wikitextLink',
|
||||
range: new ve.Range( 26 ),
|
||||
input: function () {
|
||||
this.annotationInput.getTextInputWidget().setValue( 'quux' );
|
||||
},
|
||||
expectedRange: new ve.Range( 34 ),
|
||||
expectedData: function ( data ) {
|
||||
data.splice.apply( data, [ 26, 0 ].concat( '[[quux]]'.split( '' ) ) );
|
||||
}
|
||||
},
|
||||
{
|
||||
msg: 'Link insertion with no input is no-op',
|
||||
name: 'wikitextLink',
|
||||
range: new ve.Range( 26 ),
|
||||
expectedRange: new ve.Range( 26 ),
|
||||
expectedData: function () {}
|
||||
},
|
||||
{
|
||||
msg: 'Link modified',
|
||||
name: 'wikitextLink',
|
||||
range: new ve.Range( 5, 12 ),
|
||||
input: function () {
|
||||
this.annotationInput.getTextInputWidget().setValue( 'quux' );
|
||||
},
|
||||
expectedRange: new ve.Range( 5, 17 ),
|
||||
expectedData: function ( data ) {
|
||||
data.splice.apply( data, [ 7, 3 ].concat( 'Quux|bar'.split( '' ) ) );
|
||||
}
|
||||
},
|
||||
{
|
||||
msg: 'Link modified with initial selection including whitespace',
|
||||
name: 'wikitextLink',
|
||||
range: new ve.Range( 4, 13 ),
|
||||
input: function () {
|
||||
this.annotationInput.getTextInputWidget().setValue( 'quux' );
|
||||
},
|
||||
expectedRange: new ve.Range( 5, 17 ),
|
||||
expectedData: function ( data ) {
|
||||
data.splice.apply( data, [ 7, 3 ].concat( 'Quux|bar'.split( '' ) ) );
|
||||
}
|
||||
},
|
||||
{
|
||||
msg: 'Piped link modified',
|
||||
name: 'wikitextLink',
|
||||
range: new ve.Range( 16 ),
|
||||
input: function () {
|
||||
this.annotationInput.getTextInputWidget().setValue( 'whee' );
|
||||
},
|
||||
expectedRange: new ve.Range( 13, 25 ),
|
||||
expectedData: function ( data ) {
|
||||
data.splice.apply( data, [ 15, 4 ].concat( 'Whee'.split( '' ) ) );
|
||||
}
|
||||
},
|
||||
{
|
||||
msg: 'Link modified',
|
||||
name: 'wikitextLink',
|
||||
range: new ve.Range( 30, 36 ),
|
||||
input: function () {
|
||||
this.annotationInput.getTextInputWidget().setValue( 'foo' );
|
||||
},
|
||||
expectedRange: new ve.Range( 30, 61 ),
|
||||
expectedData: function ( data ) {
|
||||
data.splice.apply( data, [ 30, 6 ].concat( '[[Foo|wh<nowiki>]]</nowiki>ee]]'.split( '' ) ) );
|
||||
}
|
||||
}
|
||||
// Skips clear annotation test, not implement yet
|
||||
];
|
||||
|
||||
ve.test.utils.runFragmentInspectorTests( surface, assert, cases ).finally( function () {
|
||||
done();
|
||||
} );
|
||||
} );
|
|
@ -20,6 +20,9 @@
|
|||
// Ensure a mock server is used (e.g. as in ve.ui.MWWikitextStringTransferHandler)
|
||||
return new mw.Api().post();
|
||||
};
|
||||
MWDummyTarget.prototype.getContentApi = function () {
|
||||
return new mw.Api();
|
||||
};
|
||||
MWDummyTarget.prototype.createSurface = ve.init.mw.Target.prototype.createSurface;
|
||||
MWDummyTarget.prototype.getSurfaceConfig = ve.init.mw.Target.prototype.getSurfaceConfig;
|
||||
// Copy import rules from mw target, for paste tests.
|
||||
|
|
|
@ -31,27 +31,76 @@ ve.ui.MWWikitextLinkAnnotationInspector.static.modelClasses = [];
|
|||
|
||||
ve.ui.MWWikitextLinkAnnotationInspector.static.handlesSource = true;
|
||||
|
||||
// TODO: Support [[linktrail]]s & [[pipe trick|]]
|
||||
ve.ui.MWWikitextLinkAnnotationInspector.static.internalLinkParser = ( function () {
|
||||
var openLink = '\\[\\[',
|
||||
closeLink = '\\]\\]',
|
||||
noCloseLink = '(?:(?!' + closeLink + ').)*',
|
||||
noCloseLinkOrPipe = '(?:(?!' + closeLink + ')[^|])*';
|
||||
|
||||
return new RegExp(
|
||||
openLink +
|
||||
'(' + noCloseLinkOrPipe + ')' +
|
||||
'(?:\\|(' + noCloseLink + '))?' +
|
||||
closeLink,
|
||||
'g'
|
||||
);
|
||||
}() );
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
ve.ui.MWWikitextLinkAnnotationInspector.prototype.getSetupProcess = function ( data ) {
|
||||
// Annotation inspector stages the annotation, so call its parent
|
||||
// Call grand-parent
|
||||
return ve.ui.FragmentInspector.prototype.getSetupProcess.call( this, data )
|
||||
return ve.ui.AnnotationInspector.super.prototype.getSetupProcess.call( this, data )
|
||||
.next( function () {
|
||||
var fragment = this.getFragment();
|
||||
var text, matches, matchTitle, range, contextFragment, contextRange, linkMatches, linkRange, title,
|
||||
inspectorTitle,
|
||||
internalLinkParser = this.constructor.static.internalLinkParser,
|
||||
fragment = this.getFragment();
|
||||
|
||||
// Only supports linear selections
|
||||
if ( !( this.previousSelection instanceof ve.dm.LinearSelection ) ) {
|
||||
return ve.createDeferred().reject().promise();
|
||||
}
|
||||
|
||||
// Initialize range
|
||||
if ( this.previousSelection instanceof ve.dm.LinearSelection ) {
|
||||
if (
|
||||
fragment.getSelection().isCollapsed() &&
|
||||
fragment.getDocument().data.isContentOffset( fragment.getSelection().getRange().start )
|
||||
) {
|
||||
// Expand to nearest word
|
||||
if ( !data.noExpand ) {
|
||||
fragment = fragment.expandLinearSelection( 'word' );
|
||||
if ( !fragment.getSelection().isCollapsed() ) {
|
||||
// Trim whitespace
|
||||
fragment = fragment.trimLinearSelection();
|
||||
}
|
||||
// Expand to existing link, if present
|
||||
// Find all links in the paragraph and see which one contains
|
||||
// the current selection.
|
||||
contextFragment = fragment.expandLinearSelection( 'siblings' );
|
||||
contextRange = contextFragment.getSelection().getCoveringRange();
|
||||
range = fragment.getSelection().getCoveringRange();
|
||||
text = contextFragment.getText();
|
||||
internalLinkParser.lastIndex = 0;
|
||||
while ( ( matches = internalLinkParser.exec( text ) ) !== null ) {
|
||||
matchTitle = mw.Title.newFromText( matches[ 1 ] );
|
||||
if ( !matchTitle ) {
|
||||
continue;
|
||||
}
|
||||
linkRange = new ve.Range(
|
||||
contextRange.start + matches.index,
|
||||
contextRange.start + matches.index + matches[ 0 ].length
|
||||
);
|
||||
if ( linkRange.containsRange( range ) ) {
|
||||
linkMatches = matches;
|
||||
fragment = fragment.getSurface().getLinearFragment( linkRange );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( !linkMatches ) {
|
||||
if ( !data.noExpand && fragment.getSelection().isCollapsed() ) {
|
||||
// expand to nearest word
|
||||
fragment = fragment.expandLinearSelection( 'word' );
|
||||
} else {
|
||||
// Trim whitespace
|
||||
fragment = fragment.trimLinearSelection();
|
||||
|
@ -60,17 +109,45 @@ ve.ui.MWWikitextLinkAnnotationInspector.prototype.getSetupProcess = function ( d
|
|||
|
||||
// Update selection
|
||||
fragment.select();
|
||||
this.initialSelection = fragment.getSelection();
|
||||
|
||||
this.initialSelection = fragment.getSelection();
|
||||
this.fragment = fragment;
|
||||
this.initialLabelText = this.fragment.getText();
|
||||
|
||||
if ( linkMatches ) {
|
||||
// Group 1 is the link target, group 2 is the label after | if present
|
||||
title = mw.Title.newFromText( linkMatches[ 1 ] );
|
||||
this.initialLabelText = linkMatches[ 2 ] || linkMatches[ 1 ];
|
||||
// HACK: Remove escaping probably added by this tool.
|
||||
// We should really do a full parse from wikitext to HTML if
|
||||
// we see any syntax
|
||||
this.initialLabelText = this.initialLabelText.replace( /<nowiki>(\]{2,})<\/nowiki>/g, '$1' );
|
||||
} else {
|
||||
title = mw.Title.newFromText( this.initialLabelText );
|
||||
}
|
||||
if ( title ) {
|
||||
this.initialAnnotation = this.newInternalLinkAnnotationFromTitle( title );
|
||||
}
|
||||
|
||||
// eslint-disable-next-line mediawiki/msg-doc
|
||||
inspectorTitle = ve.msg(
|
||||
this.isReadOnly() ?
|
||||
'visualeditor-linkinspector-title' : (
|
||||
!linkMatches ?
|
||||
'visualeditor-linkinspector-title-add' :
|
||||
'visualeditor-linkinspector-title-edit'
|
||||
)
|
||||
);
|
||||
|
||||
this.title.setLabel( inspectorTitle ).setTitle( inspectorTitle );
|
||||
this.annotationInput.setReadOnly( this.isReadOnly() );
|
||||
|
||||
this.actions.setMode( this.getMode() );
|
||||
|
||||
this.initialAnnotation = this.getAnnotationFromFragment( fragment );
|
||||
this.linkTypeIndex.setTabPanel(
|
||||
this.initialAnnotation instanceof ve.dm.MWExternalLinkAnnotation ? 'external' : 'internal'
|
||||
);
|
||||
this.annotationInput.setAnnotation( this.initialAnnotation );
|
||||
|
||||
this.updateActions();
|
||||
}, this );
|
||||
};
|
||||
|
@ -93,8 +170,10 @@ ve.ui.MWWikitextLinkAnnotationInspector.prototype.getTeardownProcess = function
|
|||
insert = this.initialSelection.isCollapsed() && insertion.length;
|
||||
if ( insert ) {
|
||||
fragment.insertContent( insertion );
|
||||
labelText = insertion;
|
||||
} else {
|
||||
labelText = this.initialLabelText;
|
||||
}
|
||||
labelText = fragment.getText();
|
||||
|
||||
// Build internal links locally
|
||||
if ( annotation instanceof ve.dm.MWInternalLinkAnnotation ) {
|
||||
|
|
Loading…
Reference in a new issue