From 3148e28f690205301d0f2557911e12d5c61395e4 Mon Sep 17 00:00:00 2001 From: Ed Sanders Date: Mon, 27 Jun 2022 16:29:24 +0100 Subject: [PATCH] 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 --- .../init/targets/ve.init.mw.ArticleTarget.js | 36 ++++-- .../ve.init.mw.DesktopArticleTarget.init.less | 2 +- modules/ve-mw/preinit/ve.utils.parsoid.js | 89 +++++--------- .../ve-mw/ui/dialogs/ve.ui.MWSaveDialog.js | 114 ++++-------------- 4 files changed, 77 insertions(+), 164 deletions(-) diff --git a/modules/ve-mw/init/targets/ve.init.mw.ArticleTarget.js b/modules/ve-mw/init/targets/ve.init.mw.ArticleTarget.js index 9e853d4ddc..363e9df5ee 100644 --- a/modules/ve-mw/init/targets/ve.init.mw.ArticleTarget.js +++ b/modules/ve-mw/init/targets/ve.init.mw.ArticleTarget.js @@ -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 () { diff --git a/modules/ve-mw/preinit/styles/ve.init.mw.DesktopArticleTarget.init.less b/modules/ve-mw/preinit/styles/ve.init.mw.DesktopArticleTarget.init.less index 396cafcc37..4cca028f95 100644 --- a/modules/ve-mw/preinit/styles/ve.init.mw.DesktopArticleTarget.init.less +++ b/modules/ve-mw/preinit/styles/ve.init.mw.DesktopArticleTarget.init.less @@ -86,7 +86,7 @@ } } - #catlinks { + .ve-init-mw-desktopArticleTarget-originalContent #catlinks { cursor: pointer; &:hover { diff --git a/modules/ve-mw/preinit/ve.utils.parsoid.js b/modules/ve-mw/preinit/ve.utils.parsoid.js index 6ab357103d..22e37b7d13 100644 --- a/modules/ve-mw/preinit/ve.utils.parsoid.js +++ b/modules/ve-mw/preinit/ve.utils.parsoid.js @@ -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. * diff --git a/modules/ve-mw/ui/dialogs/ve.ui.MWSaveDialog.js b/modules/ve-mw/ui/dialogs/ve.ui.MWSaveDialog.js index b9acc4e278..91aee49306 100644 --- a/modules/ve-mw/ui/dialogs/ve.ui.MWSaveDialog.js +++ b/modules/ve-mw/ui/dialogs/ve.ui.MWSaveDialog.js @@ -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 + $( '' ).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 - $( '
' ).addClass( 'mw-content-' + mw.config.get( 'wgVisualEditor' ).pageLanguageDir ).append( - body.childNodes - ) + // eslint-disable-next-line no-jquery/no-html + $( '
' ).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 - $( '' ).append( docOrMsg ) - ); + // Run hooks so other things can alter the document + mw.hook( 'wikipage.content' ).fire( this.$previewViewer ); } this.popPending();