mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/DiscussionTools
synced 2024-11-24 00:13:36 +00:00
Merge "Handle reply/topic preview entirely server-side"
This commit is contained in:
commit
094b77b4bb
|
@ -50,8 +50,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"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -299,8 +298,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",
|
||||
|
@ -430,6 +427,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