mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/DiscussionTools
synced 2024-11-24 00:13:36 +00:00
Handle reply/topic preview entirely server-side
We were rendering the preview in a completely different way from how we would add the real reply, and the results would be different sometimes, particularly for multi-line comments with messed-up markup. Render it server-side instead, in a very similar way to real replies (generating a DOM list node and transforming it through Parsoid), although without the whole context of the page to improve performance. We can remove a lot of client-side code that was used solely for this. This will allow the preview to accurately display the signatures when we change how they are added (T278442), without us having to implement those changes again from scratch for the preview. Change-Id: I53341f4d4075c25b67ec3b3032bff9b8a880dcd3
This commit is contained in:
parent
aeabff63c9
commit
1d43a024f9
|
@ -48,8 +48,7 @@
|
|||
"discussiontools-replywidget-feedback-link",
|
||||
"discussiontools-replywidget-feedback-link-newtopic",
|
||||
"discussiontools-replywidget-mention-prefix",
|
||||
"discussiontools-replywidget-mention-suffix",
|
||||
"discussiontools-signature-prefix"
|
||||
"discussiontools-replywidget-mention-suffix"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -295,8 +294,6 @@
|
|||
"cases/modified.json",
|
||||
"cases/reply.json",
|
||||
"cases/unwrap.json",
|
||||
"cases/isWikitextSigned.json",
|
||||
"cases/isHtmlSigned.json",
|
||||
"cases/linearWalk.json",
|
||||
"cases/sanitize-wikitext-linebreaks.json",
|
||||
"cases/timestamp-regex.json",
|
||||
|
@ -422,6 +419,9 @@
|
|||
"discussiontoolspageinfo": {
|
||||
"class": "MediaWiki\\Extension\\DiscussionTools\\ApiDiscussionToolsPageInfo"
|
||||
},
|
||||
"discussiontoolspreview": {
|
||||
"class": "MediaWiki\\Extension\\DiscussionTools\\ApiDiscussionToolsPreview"
|
||||
},
|
||||
"discussiontoolssubscribe": {
|
||||
"class": "MediaWiki\\Extension\\DiscussionTools\\ApiDiscussionToolsSubscribe",
|
||||
"services": [
|
||||
|
|
|
@ -22,6 +22,9 @@
|
|||
"apihelp-discussiontoolsgetsubscriptions-summary": "Get the subscription statuses of given topics.",
|
||||
"apihelp-discussiontoolspageinfo-param-oldid": "The revision number to use (defaults to latest revision).",
|
||||
"apihelp-discussiontoolspageinfo-summary": "Returns metadata required to initialize the discussion tools.",
|
||||
"apihelp-discussiontoolspreview-param-type": "Type of message to preview",
|
||||
"apihelp-discussiontoolspreview-param-wikitext": "Content to preview, as wikitext.",
|
||||
"apihelp-discussiontoolspreview-summary": "Preview a message on a discussion page.",
|
||||
"apihelp-discussiontoolssubscribe-param-commentname": "Name of the topic to subscribe to (or unsubscribe from)",
|
||||
"apihelp-discussiontoolssubscribe-param-page": "A page on which the topic appears",
|
||||
"apihelp-discussiontoolssubscribe-param-subscribe": "True to subscribe, false to unsubscribe",
|
||||
|
|
|
@ -24,6 +24,9 @@
|
|||
"apihelp-discussiontoolsgetsubscriptions-summary": "{{doc-apihelp-summary|discussiontoolsgetsubscriptions}}",
|
||||
"apihelp-discussiontoolspageinfo-param-oldid": "{{doc-apihelp-param|discussiontoolspageinfo|oldid}}",
|
||||
"apihelp-discussiontoolspageinfo-summary": "{{doc-apihelp-summary|discussiontoolspageinfo}}",
|
||||
"apihelp-discussiontoolspreview-param-type": "{{doc-apihelp-param|discussiontoolspreview|type}}",
|
||||
"apihelp-discussiontoolspreview-param-wikitext": "{{doc-apihelp-param|discussiontoolspreview|wikitext}}",
|
||||
"apihelp-discussiontoolspreview-summary": "{{doc-apihelp-summary|discussiontoolspreview}}",
|
||||
"apihelp-discussiontoolssubscribe-param-commentname": "{{doc-apihelp-param|discussiontoolssubscribe|commentname}}",
|
||||
"apihelp-discussiontoolssubscribe-param-page": "{{doc-apihelp-param|discussiontoolssubscribe|page}}",
|
||||
"apihelp-discussiontoolssubscribe-param-subscribe": "{{doc-apihelp-param|discussiontoolssubscribe|subscribe}}",
|
||||
|
|
94
includes/ApiDiscussionToolsPreview.php
Normal file
94
includes/ApiDiscussionToolsPreview.php
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
namespace MediaWiki\Extension\DiscussionTools;
|
||||
|
||||
use ApiBase;
|
||||
use ApiMain;
|
||||
use ApiParsoidTrait;
|
||||
use Title;
|
||||
use Wikimedia\ParamValidator\ParamValidator;
|
||||
|
||||
class ApiDiscussionToolsPreview extends ApiBase {
|
||||
|
||||
use ApiDiscussionToolsTrait;
|
||||
use ApiParsoidTrait;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __construct( ApiMain $main, string $name ) {
|
||||
parent::__construct( $main, $name );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function execute() {
|
||||
$params = $this->extractRequestParams();
|
||||
$title = Title::newFromText( $params['page'] );
|
||||
|
||||
if ( !$title ) {
|
||||
$this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['page'] ) ] );
|
||||
}
|
||||
if ( $params['type'] === 'topic' ) {
|
||||
$this->requireAtLeastOneParameter( $params, 'sectiontitle' );
|
||||
}
|
||||
|
||||
// Add signature if missing
|
||||
$signature = null;
|
||||
if ( !CommentModifier::isWikitextSigned( $params['wikitext'] ) ) {
|
||||
$signature = $this->msg( 'discussiontools-signature-prefix' )->inContentLanguage()->text() . '~~~~';
|
||||
// Drop opacity of signature in preview to make message body preview clearer.
|
||||
// Extract any leading spaces outside the <span> markup to ensure accurate previews.
|
||||
$signature = preg_replace_callback( '/^( *)(.+)$/', static function ( $matches ) {
|
||||
list( , $leadingSpaces, $sig ) = $matches;
|
||||
return $leadingSpaces . '<span style="opacity: 0.6;">' . $sig . '</span>';
|
||||
}, $signature );
|
||||
}
|
||||
|
||||
$result = $this->previewMessage( [
|
||||
'type' => $params['type'],
|
||||
'title' => $title,
|
||||
'wikitext' => $params['wikitext'],
|
||||
'sectiontitle' => $params['sectiontitle'],
|
||||
'signature' => $signature,
|
||||
] );
|
||||
|
||||
$this->getResult()->addValue( null, $this->getModuleName(), $result->serializeForApiResult() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getAllowedParams() {
|
||||
return [
|
||||
'type' => [
|
||||
ParamValidator::PARAM_REQUIRED => true,
|
||||
ParamValidator::PARAM_TYPE => [
|
||||
'reply',
|
||||
'topic',
|
||||
],
|
||||
ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
|
||||
],
|
||||
'page' => [
|
||||
ParamValidator::PARAM_REQUIRED => true,
|
||||
ApiBase::PARAM_HELP_MSG => 'apihelp-visualeditoredit-param-page',
|
||||
],
|
||||
'wikitext' => [
|
||||
ParamValidator::PARAM_REQUIRED => true,
|
||||
ParamValidator::PARAM_TYPE => 'text',
|
||||
],
|
||||
'sectiontitle' => [
|
||||
ParamValidator::PARAM_TYPE => 'string',
|
||||
ApiBase::PARAM_HELP_MSG => 'apihelp-edit-param-sectiontitle',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isInternal() {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -2,12 +2,20 @@
|
|||
|
||||
namespace MediaWiki\Extension\DiscussionTools;
|
||||
|
||||
use ApiMain;
|
||||
use ApiResult;
|
||||
use DerivativeContext;
|
||||
use DerivativeRequest;
|
||||
use IContextSource;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use Title;
|
||||
use Wikimedia\Parsoid\Utils\DOMCompat;
|
||||
use Wikimedia\Parsoid\Utils\DOMUtils;
|
||||
|
||||
/**
|
||||
* Random methods we want to share between API modules.
|
||||
*/
|
||||
trait ApiDiscussionToolsTrait {
|
||||
/**
|
||||
* @param RevisionRecord $revision
|
||||
|
@ -29,9 +37,99 @@ trait ApiDiscussionToolsTrait {
|
|||
return $parser->parse( $container, $title );
|
||||
}
|
||||
|
||||
/**
|
||||
* Given parameters describing a reply or new topic, transform them into wikitext using Parsoid,
|
||||
* then preview the wikitext using the legacy parser.
|
||||
*
|
||||
* @param array $params Associative array with the following keys:
|
||||
* - `type` (string) 'topic' or 'reply'
|
||||
* - `title` (Title) Context title for wikitext transformations
|
||||
* - `wikitext` (string) Content of the message
|
||||
* - `sectiontitle` (string) Content of the title, when `type` is 'topic'
|
||||
* - `signature` (string|null) Wikitext signature to add to the message
|
||||
* @return ApiResult action=parse API result
|
||||
*/
|
||||
protected function previewMessage( array $params ): ApiResult {
|
||||
$wikitext = $params['wikitext'];
|
||||
$title = $params['title'];
|
||||
$signature = $params['signature'] ?? null;
|
||||
|
||||
switch ( $params['type'] ) {
|
||||
case 'topic':
|
||||
$wikitext = CommentUtils::htmlTrim( $wikitext );
|
||||
if ( $signature !== null ) {
|
||||
$wikitext .= $signature;
|
||||
}
|
||||
|
||||
if ( $params['sectiontitle'] ) {
|
||||
$wikitext = "== " . $params['sectiontitle'] . " ==\n" . $wikitext;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'reply':
|
||||
$doc = DOMUtils::parseHTML( '' );
|
||||
|
||||
$container = CommentModifier::prepareWikitextReply( $doc, $wikitext );
|
||||
|
||||
if ( $signature !== null ) {
|
||||
CommentModifier::appendSignature( $container, $signature );
|
||||
}
|
||||
$list = CommentModifier::transferReply( $container );
|
||||
$html = DOMCompat::getOuterHTML( $list );
|
||||
|
||||
$wikitext = $this->transformHTML( $title, $html )[ 'body' ];
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$apiParams = [
|
||||
'action' => 'parse',
|
||||
'title' => $title->getPrefixedText(),
|
||||
'text' => $wikitext,
|
||||
'pst' => '1',
|
||||
'preview' => '1',
|
||||
'disableeditsection' => '1',
|
||||
'prop' => 'text|modules|jsconfigvars',
|
||||
];
|
||||
|
||||
$context = new DerivativeContext( $this->getContext() );
|
||||
$context->setRequest(
|
||||
new DerivativeRequest(
|
||||
$context->getRequest(),
|
||||
$apiParams,
|
||||
/* was posted? */ true
|
||||
)
|
||||
);
|
||||
$api = new ApiMain(
|
||||
$context,
|
||||
/* enable write? */ false
|
||||
);
|
||||
|
||||
$api->execute();
|
||||
return $api->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RevisionRecord $revision
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function requestRestbasePageHtml( RevisionRecord $revision ): array;
|
||||
|
||||
/**
|
||||
* @param Title $title
|
||||
* @param string $html
|
||||
* @param int|null $oldid
|
||||
* @param string|null $etag
|
||||
* @return array
|
||||
*/
|
||||
abstract protected function transformHTML(
|
||||
Title $title, string $html, int $oldid = null, string $etag = null
|
||||
): array;
|
||||
|
||||
/**
|
||||
* @return IContextSource
|
||||
*/
|
||||
abstract public function getContext();
|
||||
|
||||
}
|
||||
|
|
|
@ -541,6 +541,31 @@ class CommentModifier {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer comment DOM nodes into a list node, as if adding a reply, but without requiring a
|
||||
* ThreadItem.
|
||||
*
|
||||
* @param DocumentFragment $container Container of comment DOM nodes
|
||||
* @return Element $node List node
|
||||
*/
|
||||
public static function transferReply( DocumentFragment $container ): Element {
|
||||
$services = MediaWikiServices::getInstance();
|
||||
$dtConfig = $services->getConfigFactory()->makeConfig( 'discussiontools' );
|
||||
$replyIndentation = $dtConfig->get( 'DiscussionToolsReplyIndentation' );
|
||||
|
||||
$doc = $container->ownerDocument;
|
||||
|
||||
// Like addReply(), but we make our own list
|
||||
$list = $doc->createElement( $replyIndentation === 'invisible' ? 'dl' : 'ul' );
|
||||
while ( $container->childNodes->length ) {
|
||||
$item = $doc->createElement( $replyIndentation === 'invisible' ? 'dd' : 'li' );
|
||||
self::whitespaceParsoidHack( $item );
|
||||
$item->appendChild( $container->firstChild );
|
||||
$list->appendChild( $item );
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a container of comment DOM nodes from wikitext
|
||||
*
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
var controller = require( './controller.js' ),
|
||||
config = require( './config.json' ),
|
||||
uri = new mw.Uri();
|
||||
|
||||
/**
|
||||
|
@ -74,6 +73,5 @@ module.exports = {
|
|||
HeadingItem: require( './HeadingItem.js' ),
|
||||
CommentItem: require( './CommentItem.js' ),
|
||||
utils: require( './utils.js' ),
|
||||
logger: require( './logger.js' ),
|
||||
config: config
|
||||
logger: require( './logger.js' )
|
||||
};
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
var controller = require( 'ext.discussionTools.init' ).controller,
|
||||
modifier = require( 'ext.discussionTools.init' ).modifier,
|
||||
utils = require( 'ext.discussionTools.init' ).utils,
|
||||
logger = require( 'ext.discussionTools.init' ).logger,
|
||||
dtConf = require( 'ext.discussionTools.init' ).config,
|
||||
ModeTabSelectWidget = require( './ModeTabSelectWidget.js' ),
|
||||
ModeTabOptionWidget = require( './ModeTabOptionWidget.js' ),
|
||||
licenseMessages = require( './licenseMessages.json' ),
|
||||
|
@ -708,7 +706,6 @@ ReplyWidget.prototype.preparePreview = function ( wikitext ) {
|
|||
return $.Deferred().resolve().promise();
|
||||
}
|
||||
|
||||
var indent = dtConf.replyIndentation === 'invisible' ? ':' : '*';
|
||||
wikitext = wikitext !== undefined ? wikitext : this.getValue();
|
||||
wikitext = utils.htmlTrim( wikitext );
|
||||
var title = this.isNewTopic && this.commentController.sectionTitle.getValue();
|
||||
|
@ -728,40 +725,22 @@ ReplyWidget.prototype.preparePreview = function ( wikitext ) {
|
|||
if ( !wikitext ) {
|
||||
parsePromise = $.Deferred().resolve( null ).promise();
|
||||
} else {
|
||||
wikitext = this.commentController.doIndentReplacements( wikitext, indent );
|
||||
|
||||
if ( !modifier.isWikitextSigned( wikitext ) ) {
|
||||
// Add signature.
|
||||
var signature = mw.msg( 'discussiontools-signature-prefix' ) + '~~~~';
|
||||
// Drop opacity of signature in preview to make message body preview clearer.
|
||||
// Extract any leading spaces outside the <span> markup to ensure accurate previews.
|
||||
signature = signature.replace( /^( *)(.+)$/, function ( _, leadingSpaces, sig ) {
|
||||
return leadingSpaces + '<span style="opacity: 0.6;">' + sig + '</span>';
|
||||
} );
|
||||
wikitext += signature;
|
||||
}
|
||||
if ( title ) {
|
||||
wikitext = '== ' + title + ' ==\n' + wikitext;
|
||||
}
|
||||
this.previewRequest = parsePromise = controller.getApi().post( {
|
||||
action: 'parse',
|
||||
text: wikitext,
|
||||
pst: true,
|
||||
preview: true,
|
||||
disableeditsection: true,
|
||||
prop: [ 'text', 'modules', 'jsconfigvars' ],
|
||||
title: this.pageName
|
||||
action: 'discussiontoolspreview',
|
||||
type: this.isNewTopic ? 'topic' : 'reply',
|
||||
page: this.pageName,
|
||||
wikitext: wikitext,
|
||||
sectiontitle: title
|
||||
} );
|
||||
}
|
||||
// TODO: Add list context
|
||||
|
||||
return parsePromise.then( function ( response ) {
|
||||
widget.$preview.html( response ? response.parse.text : '' );
|
||||
widget.$preview.html( response ? response.discussiontoolspreview.parse.text : '' );
|
||||
|
||||
if ( response ) {
|
||||
mw.config.set( response.parse.jsconfigvars );
|
||||
mw.loader.load( response.parse.modulestyles );
|
||||
mw.loader.load( response.parse.modules );
|
||||
mw.config.set( response.discussiontoolspreview.parse.jsconfigvars );
|
||||
mw.loader.load( response.discussiontoolspreview.parse.modulestyles );
|
||||
mw.loader.load( response.discussiontoolspreview.parse.modules );
|
||||
}
|
||||
|
||||
mw.hook( 'wikipage.content' ).fire( widget.$preview );
|
||||
|
|
|
@ -363,57 +363,11 @@ function addSiblingListItem( previousItem ) {
|
|||
return listItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether wikitext contains a user signature.
|
||||
*
|
||||
* @param {string} wikitext
|
||||
* @return {boolean}
|
||||
*/
|
||||
function isWikitextSigned( wikitext ) {
|
||||
wikitext = utils.htmlTrim( wikitext );
|
||||
// Contains ~~~~ (four tildes), but not ~~~~~ (five tildes), at the end.
|
||||
return /([^~]|^)~~~~$/.test( wikitext );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether HTML node contains a user signature.
|
||||
*
|
||||
* @param {HTMLElement} container
|
||||
* @return {boolean}
|
||||
*/
|
||||
function isHtmlSigned( container ) {
|
||||
// Good enough?…
|
||||
var matches = container.querySelectorAll( 'span[typeof="mw:Transclusion"][data-mw*="~~~~"]' );
|
||||
if ( matches.length === 0 ) {
|
||||
return false;
|
||||
}
|
||||
var lastSig = matches[ matches.length - 1 ];
|
||||
// Signature must be at the end of the comment - there must be no sibling following this node, or its parents
|
||||
var node = lastSig;
|
||||
while ( node ) {
|
||||
// Skip over whitespace nodes
|
||||
while (
|
||||
node.nextSibling &&
|
||||
node.nextSibling.nodeType === Node.TEXT_NODE &&
|
||||
utils.htmlTrim( node.nextSibling.textContent ) === ''
|
||||
) {
|
||||
node = node.nextSibling;
|
||||
}
|
||||
if ( node.nextSibling ) {
|
||||
return false;
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
addReplyLink: addReplyLink,
|
||||
addListItem: addListItem,
|
||||
removeAddedListItem: removeAddedListItem,
|
||||
addSiblingListItem: addSiblingListItem,
|
||||
unwrapList: unwrapList,
|
||||
isWikitextSigned: isWikitextSigned,
|
||||
isHtmlSigned: isHtmlSigned,
|
||||
sanitizeWikitextLinebreaks: sanitizeWikitextLinebreaks
|
||||
};
|
||||
|
|
|
@ -122,33 +122,6 @@ QUnit.test( '#unwrapList', function ( assert ) {
|
|||
} );
|
||||
} );
|
||||
|
||||
QUnit.test( 'isWikitextSigned', function ( assert ) {
|
||||
var cases = require( '../cases/isWikitextSigned.json' );
|
||||
|
||||
cases.forEach( function ( caseItem ) {
|
||||
assert.strictEqual(
|
||||
modifier.isWikitextSigned( caseItem.wikitext ),
|
||||
caseItem.expected,
|
||||
caseItem.msg
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
QUnit.test( 'isHtmlSigned', function ( assert ) {
|
||||
var cases = require( '../cases/isHtmlSigned.json' );
|
||||
|
||||
cases.forEach( function ( caseItem ) {
|
||||
var container = document.createElement( 'div' );
|
||||
container.innerHTML = caseItem.html;
|
||||
|
||||
assert.strictEqual(
|
||||
modifier.isHtmlSigned( container ),
|
||||
caseItem.expected,
|
||||
caseItem.msg
|
||||
);
|
||||
} );
|
||||
} );
|
||||
|
||||
QUnit.test( 'sanitizeWikitextLinebreaks', function ( assert ) {
|
||||
var cases = require( '../cases/sanitize-wikitext-linebreaks.json' );
|
||||
|
||||
|
|
Loading…
Reference in a new issue