Wikitext mode: Use action=parse for preview

Using Parsoid HTML in the 2017WTE has enabled us to iron
out lots of rendering bugs over the past few years.

In that time Parsoid has been moved into PHP, and at some point
we also become the default parser.

Also more extensions have started to use content transform hooks,
which are only supported by the action API.

As a result it now seems like a good time to migrate back to the
content API instead of building the preview from Parsoid HTML.

Bug: T154844
Change-Id: I90d775dd71d5f5a61d651b63d946ab60a27e2ca3
This commit is contained in:
Ed Sanders 2022-06-27 16:29:24 +01:00
parent b1a12aeaab
commit 3148e28f69
4 changed files with 77 additions and 164 deletions

View file

@ -953,21 +953,31 @@ ve.init.mw.ArticleTarget.prototype.onSaveDialogPreview = function () {
this.emit( 'savePreview' );
this.saveDialog.pushPending();
var wikitext = this.getDocToSave();
if ( this.sectionTitle && this.sectionTitle.getValue() ) {
wikitext = '== ' + this.sectionTitle.getValue() + ' ==\n\n' + wikitext;
var params = {};
var sectionTitle = this.sectionTitle && this.sectionTitle.getValue();
if ( sectionTitle ) {
params.section = 'new';
params.sectiontitle = sectionTitle;
}
if ( mw.config.get( 'wgUserVariant' ) ) {
params.variant = mw.config.get( 'wgUserVariant' );
}
api.post( {
action: 'visualeditor',
paction: 'parsedoc',
page: this.getPageName(),
wikitext: wikitext,
pst: true
} ).then( function ( response ) {
var baseDoc = target.getSurface().getModel().getDocument().getHtmlDocument();
var doc = target.constructor.static.parseDocument( response.visualeditor.content, 'visual' );
target.saveDialog.showPreview( doc, baseDoc );
api.post( ve.extendObject( params, {
action: 'parse',
title: this.getPageName(),
text: this.getDocToSave(),
pst: true,
preview: true,
sectionpreview: this.section !== null,
disableeditsection: true,
uselang: mw.config.get( 'wgUserLanguage' ),
useskin: mw.config.get( 'skin' ),
mobileformat: OO.ui.isMobile(),
prop: [ 'text', 'categorieshtml', 'displaytitle', 'subtitle', 'modules', 'jsconfigvars' ]
} ) ).then( function ( response ) {
target.saveDialog.showPreview( response );
}, function ( errorCode, details ) {
target.saveDialog.showPreview( target.extractErrorMessages( details ) );
} ).always( function () {

View file

@ -86,7 +86,7 @@
}
}
#catlinks {
.ve-init-mw-desktopArticleTarget-originalContent #catlinks {
cursor: pointer;
&:hover {

View file

@ -214,31 +214,40 @@ mw.libs.ve.fixFragmentLinks = function ( container, docTitle, prefix ) {
var docTitleText = docTitle.getPrefixedText();
prefix = prefix || '';
Array.prototype.forEach.call( container.querySelectorAll( 'a[href*="#"]' ), function ( el ) {
var fragment = new mw.Uri( el.href ).fragment,
targetData = mw.libs.ve.getTargetDataFromHref( el.href, el.ownerDocument );
var fragment = null;
if ( el.getAttribute( 'href' )[ 0 ] === '#' ) {
// Leagcy parser
fragment = el.getAttribute( 'href' ).slice( 1 );
} else {
// Parsoid HTML
var targetData = mw.libs.ve.getTargetDataFromHref( el.href, el.ownerDocument );
if ( targetData.isInternal ) {
var title = mw.Title.newFromText( targetData.title );
if ( title && title.getPrefixedText() === docTitleText ) {
if ( !fragment ) {
// Special case for empty fragment, even if prefix set
el.setAttribute( 'href', '#' );
} else {
if ( prefix ) {
var target = container.querySelector( '#' + $.escapeSelector( fragment ) );
// There may be multiple links to a specific target, so check the target
// hasn't already been fixed (in which case it would be null)
if ( target ) {
target.setAttribute( 'id', prefix + fragment );
target.setAttribute( 'data-mw-id-fixed', '' );
}
}
el.setAttribute( 'href', '#' + prefix + fragment );
if ( targetData.isInternal ) {
var title = mw.Title.newFromText( targetData.title );
if ( title && title.getPrefixedText() === docTitleText ) {
fragment = new mw.Uri( el.href ).fragment;
}
el.removeAttribute( 'target' );
}
}
if ( fragment !== null ) {
if ( !fragment ) {
// Special case for empty fragment, even if prefix set
el.setAttribute( 'href', '#' );
} else {
if ( prefix ) {
var target = container.querySelector( '#' + $.escapeSelector( fragment ) );
// There may be multiple links to a specific target, so check the target
// hasn't already been fixed (in which case it would be null)
if ( target ) {
target.setAttribute( 'id', prefix + fragment );
target.setAttribute( 'data-mw-id-fixed', '' );
}
}
el.setAttribute( 'href', '#' + prefix + fragment );
}
el.removeAttribute( 'target' );
}
} );
// Remove any section heading anchors which weren't fixed above (T218492)
Array.prototype.forEach.call( container.querySelectorAll( 'h1, h2, h3, h4, h5, h6' ), function ( el ) {
@ -314,44 +323,6 @@ mw.libs.ve.getTargetDataFromHref = function ( href, doc ) {
return data;
};
/**
* Expand a string of the form jquery.foo,bar|jquery.ui.baz,quux to
* an array of module names like [ 'jquery.foo', 'jquery.bar',
* 'jquery.ui.baz', 'jquery.ui.quux' ]
*
* Implementation of ResourceLoaderContext::expandModuleNames
* TODO: Consider upstreaming this to MW core.
*
* @param {string} moduleNames Packed module name list
* @return {string[]} Array of module names
*/
mw.libs.ve.expandModuleNames = function ( moduleNames ) {
var modules = [];
moduleNames.split( '|' ).forEach( function ( group ) {
if ( group.indexOf( ',' ) === -1 ) {
// This is not a set of modules in foo.bar,baz notation
// but a single module
modules.push( group );
} else {
// This is a set of modules in foo.bar,baz notation
var matches = group.match( /(.*)\.([^.]*)/ );
if ( !matches ) {
// Prefixless modules, i.e. without dots
modules = modules.concat( group.split( ',' ) );
} else {
// We have a prefix and a bunch of suffixes
var prefix = matches[ 1 ];
var suffixes = matches[ 2 ].split( ',' ); // [ 'bar', 'baz' ]
suffixes.forEach( function ( suffix ) {
modules.push( prefix + '.' + suffix );
} );
}
}
} );
return modules;
};
/**
* Split Parsoid resource name into the href prefix and the page title.
*

View file

@ -204,107 +204,39 @@ ve.ui.MWSaveDialog.prototype.setDiffAndReview = function ( wikitextDiffPromise,
/**
* Set preview content and show preview panel.
*
* @param {HTMLDocument|jQuery} docOrMsg Document to preview, or error message
* @param {HTMLDocument} [baseDoc] Base document against which to normalise links, if document provided
* @param {Object|jQuery} response action=parse API response, or error message
*/
ve.ui.MWSaveDialog.prototype.showPreview = function ( docOrMsg, baseDoc ) {
var dialog = this;
if ( docOrMsg instanceof HTMLDocument ) {
var modules = [];
// Extract required modules for stylesheet tags (avoids re-loading styles)
Array.prototype.forEach.call( docOrMsg.head.querySelectorAll( 'link[rel~=stylesheet]' ), function ( link ) {
var uri = new mw.Uri( link.href );
if ( uri.query.modules ) {
modules = modules.concat( mw.libs.ve.expandModuleNames( uri.query.modules ) );
}
} );
// Remove skin-specific modules (T187075 / T185284)
modules = modules.filter( function ( module ) {
return !/^(skins|mediawiki\.skinning)\./.test( module );
} );
mw.loader.using( modules );
var body = docOrMsg.body;
var categories = [];
// Take a snapshot of all categories
Array.prototype.forEach.call( body.querySelectorAll( 'link[rel~="mw:PageProp/Category"]' ), function ( element ) {
categories.push( ve.dm.nodeFactory.createFromElement( ve.dm.MWCategoryMetaItem.static.toDataElement( [ element ] ) ) );
} );
// Import body to current document, then resolve attributes against original document (parseDocument called #fixBase)
document.adoptNode( body );
// TODO: This code is very similar to ve.ui.PreviewElement+ve.ui.MWPreviewElement
ve.resolveAttributes( body, docOrMsg, ve.dm.Converter.static.computedAttributes );
// Document title will only be set if wikitext contains {{DISPLAYTITLE}}
if ( docOrMsg.title ) {
// HACK: Parse title as it can contain basic wikitext (T122976)
ve.init.target.getContentApi().post( {
action: 'parse',
title: ve.init.target.getPageName(),
prop: 'displaytitle',
text: '{{DISPLAYTITLE:' + docOrMsg.title + '}}\n'
} ).then( function ( response ) {
if ( ve.getProp( response, 'parse', 'displaytitle' ) ) {
// eslint-disable-next-line no-jquery/no-html
dialog.$previewHeading.html( response.parse.displaytitle );
}
} );
}
// Redirect
var $redirect = $( [] );
var redirectMeta = body.querySelector( 'link[rel="mw:PageProp/redirect"]' );
if ( redirectMeta ) {
$redirect = ve.init.mw.ArticleTarget.static.buildRedirectMsg(
mw.libs.ve.getTargetDataFromHref(
redirectMeta.getAttribute( 'href' ),
document
).title
);
}
// TODO: This won't work with formatted titles (T122976)
this.$previewHeading.text( docOrMsg.title || mw.Title.newFromText( ve.init.target.getPageName() ).getPrefixedText() );
ve.ui.MWSaveDialog.prototype.showPreview = function ( response ) {
if ( response instanceof $ ) {
this.$previewViewer.empty().append(
// eslint-disable-next-line no-jquery/no-append-html
$( '<em>' ).append( response )
);
} else {
var data = response.parse;
mw.config.set( data.jsconfigvars );
mw.loader.using( ( data.modules || [] ).concat( data.modulestyles || [] ) );
// eslint-disable-next-line no-jquery/no-html
this.$previewHeading.html( data.displaytitle );
// eslint-disable-next-line no-jquery/no-append-html
this.$previewViewer.empty().append(
$redirect,
// The following classes are used here:
// * mw-content-ltr
// * mw-content-rtl
// eslint-disable-next-line no-jquery/no-append-html
$( '<div>' ).addClass( 'mw-content-' + mw.config.get( 'wgVisualEditor' ).pageLanguageDir ).append(
body.childNodes
)
// eslint-disable-next-line no-jquery/no-html
$( '<div>' ).addClass( 'mw-content-' + mw.config.get( 'wgVisualEditor' ).pageLanguageDir ).html(
data.text
),
data.categorieshtml
);
ve.targetLinksToNewWindow( this.$previewViewer[ 0 ] );
// Add styles so links render with their appropriate classes
ve.init.platform.linkCache.styleParsoidElements( this.$previewViewer, baseDoc );
mw.libs.ve.fixFragmentLinks( this.$previewViewer[ 0 ], mw.Title.newFromText( ve.init.target.getPageName() ), 'mw-save-preview-' );
var deferred;
if ( categories.length ) {
// If there are categories, we need to render them. This involves
// a delay, since they might be hidden categories.
deferred = ve.init.target.renderCategories( categories ).done( function ( $categories ) {
dialog.$previewViewer.append( $categories );
ve.targetLinksToNewWindow( $categories[ 0 ] );
// Add styles so links render with their appropriate classes
ve.init.platform.linkCache.styleParsoidElements( $categories, baseDoc );
} );
} else {
deferred = ve.createDeferred().resolve();
}
deferred.done( function () {
// Run hooks so other things can alter the document
mw.hook( 'wikipage.content' ).fire( dialog.$previewViewer );
} );
} else if ( docOrMsg instanceof $ ) {
this.$previewViewer.empty().append(
// eslint-disable-next-line no-jquery/no-append-html
$( '<em>' ).append( docOrMsg )
);
// Run hooks so other things can alter the document
mw.hook( 'wikipage.content' ).fire( this.$previewViewer );
}
this.popPending();