getSlotDiffRenderer( RequestContext::getMain() ) * * @ingroup DifferenceEngine */ class TextSlotDiffRenderer extends SlotDiffRenderer { /** Use the PHP diff implementation (DiffEngine). */ public const ENGINE_PHP = 'php'; /** Use the wikidiff2 PHP module. */ public const ENGINE_WIKIDIFF2 = 'wikidiff2'; /** Use the wikidiff2 PHP module. */ public const ENGINE_WIKIDIFF2_INLINE = 'wikidiff2inline'; /** Use an external executable. */ public const ENGINE_EXTERNAL = 'external'; public const INLINE_LEGEND_KEY = '10_mw-diff-inline-legend'; public const INLINE_SWITCHER_KEY = '60_mw-diff-inline-switch'; /** @var IBufferingStatsdDataFactory|null */ private $statsdDataFactory; /** @var HookRunner|null */ private $hookRunner; /** @var string|null */ private $format; /** @var string */ private $contentModel; /** @var TextDiffer|null */ private $textDiffer; /** @var bool */ private $inlineToggleEnabled = false; /** @inheritDoc */ public function getExtraCacheKeys() { return $this->textDiffer->getCacheKeys( [ $this->format ] ); } /** * Convenience helper to use getTextDiff without an instance. * @param string $oldText * @param string $newText * @param array $options * @return string */ public static function diff( $oldText, $newText, $options = [] ) { /** @var TextSlotDiffRenderer $slotDiffRenderer */ $slotDiffRenderer = MediaWikiServices::getInstance() ->getContentHandlerFactory() ->getContentHandler( CONTENT_MODEL_TEXT ) ->getSlotDiffRenderer( RequestContext::getMain(), $options ); '@phan-var TextSlotDiffRenderer $slotDiffRenderer'; return $slotDiffRenderer->getTextDiff( $oldText, $newText ); } /** * @param IBufferingStatsdDataFactory $statsdDataFactory */ public function setStatsdDataFactory( IBufferingStatsdDataFactory $statsdDataFactory ) { $this->statsdDataFactory = $statsdDataFactory; } /** * This has no effect since MW 1.41. The language is now injected via setTextDiffer(). * * @param Language $language * @deprecated since 1.41 */ public function setLanguage( Language $language ) { wfDeprecated( __METHOD__, '1.41' ); } /** * @since 1.41 * @param HookContainer $hookContainer */ public function setHookContainer( HookContainer $hookContainer ): void { $this->hookRunner = new HookRunner( $hookContainer ); } /** * @param string $contentModel * @since 1.41 */ public function setContentModel( string $contentModel ) { $this->contentModel = $contentModel; } /** * Set which diff engine to use. * * @param string $type One of the ENGINE_* constants. * @param null $executable Must be null since 1.41. Previously a path to execute. */ public function setEngine( $type, $executable = null ) { if ( $executable !== null ) { throw new \InvalidArgumentException( 'The $executable parameter is no longer supported and must be null' ); } switch ( $type ) { case self::ENGINE_PHP: $engine = 'php'; $format = 'table'; break; case self::ENGINE_WIKIDIFF2: $engine = 'wikidiff2'; $format = 'table'; break; case self::ENGINE_EXTERNAL: $engine = 'external'; $format = 'external'; break; case self::ENGINE_WIKIDIFF2_INLINE: $engine = 'wikidiff2'; $format = 'inline'; break; default: throw new \InvalidArgumentException( '$type ' . 'must be one of the TextSlotDiffRenderer::ENGINE_* constants' ); } if ( $this->textDiffer instanceof ManifoldTextDiffer ) { $this->textDiffer->setEngine( $engine ); } $this->setFormat( $format ); } /** * Set the TextDiffer format * * @since 1.41 * @param string $format */ public function setFormat( $format ) { $this->format = $format; } /** * @param TextDiffer $textDiffer */ public function setTextDiffer( TextDiffer $textDiffer ) { $this->textDiffer = $textDiffer; } /** * Get the current TextDiffer, or throw an exception if setTextDiffer() has * not been called. * * @return TextDiffer */ private function getTextDiffer(): TextDiffer { return $this->textDiffer; } /** * Set a flag indicating whether the inline toggle switch is shown. * * @since 1.41 * @param bool $enabled */ public function setInlineToggleEnabled( $enabled = true ) { $this->inlineToggleEnabled = $enabled; } /** * Get the content model ID that this renderer acts on * * @since 1.41 * @return string */ public function getContentModel(): string { return $this->contentModel; } /** @inheritDoc */ public function getDiff( Content $oldContent = null, Content $newContent = null ) { $this->normalizeContents( $oldContent, $newContent, TextContent::class ); $oldText = $oldContent->serialize(); $newText = $newContent->serialize(); return $this->getTextDiff( $oldText, $newText ); } public function localizeDiff( $diff, $options = [] ) { return $this->textDiffer->localize( $this->format, $diff, $options ); } /** * @inheritDoc */ public function getTablePrefix( IContextSource $context, Title $newTitle ): array { $parts = $this->getTextDiffer()->getTablePrefixes( $this->format ); $showDiffToggleSwitch = $this->inlineToggleEnabled && $this->getTextDiffer()->hasFormat( 'inline' ); // If we support the inline type, add a toggle switch if ( $showDiffToggleSwitch ) { $values = $context->getRequest()->getValues(); $isInlineDiffType = $this->format === 'inline'; $values[ 'diff-type' ] = $isInlineDiffType ? 'table' : 'inline'; unset( $values[ 'title' ] ); $parts[self::INLINE_SWITCHER_KEY] = Html::rawElement( 'div', [ 'class' => 'mw-diffPage-inlineToggle-container' ], new OOUI\FieldLayout( new ToggleSwitchWidget( [ 'id' => 'mw-diffPage-inline-toggle-switch', 'href' => $newTitle->getLocalURL( $values ), 'value' => $isInlineDiffType, 'title' => $context->msg( 'diff-inline-switch-desc' )->plain() ] ), [ 'id' => 'mw-diffPage-inline-toggle-switch-layout', 'label' => $context->msg( 'diff-inline-format-label' )->plain(), 'infusable' => true, 'title' => $context->msg( 'diff-inline-switch-desc' )->plain() ] ), ); } // Add an empty placeholder for the legend is added when it's not in // use and other items have been added. $parts += [ self::INLINE_LEGEND_KEY => null, self::INLINE_SWITCHER_KEY => null ]; // Allow extensions to add other parts to this area (or modify the legend). $this->hookRunner->onTextSlotDiffRendererTablePrefix( $this, $context, $parts ); if ( count( $parts ) > 1 && $parts[self::INLINE_LEGEND_KEY] === null ) { $parts[self::INLINE_LEGEND_KEY] = Html::element( 'div' ); } return $parts; } /** * Diff the text representations of two content objects (or just two pieces of text in general). * @param string $oldText * @param string $newText * @return string HTML. One or more tags, or an empty string if the inputs are identical. */ public function getTextDiff( string $oldText, string $newText ) { $diff = function () use ( $oldText, $newText ) { $time = microtime( true ); $result = $this->getTextDiffInternal( $oldText, $newText ); $time = intval( ( microtime( true ) - $time ) * 1000 ); if ( $this->statsdDataFactory ) { $this->statsdDataFactory->timing( 'diff_time', $time ); } return $result; }; /** * @param Status $status * @throws FatalError * @return never */ $error = static function ( $status ) { throw new FatalError( $status->getWikiText() ); }; // Use PoolCounter if the diff looks like it can be expensive if ( strlen( $oldText ) + strlen( $newText ) > 20000 ) { $work = new PoolCounterWorkViaCallback( 'diff', md5( $oldText ) . md5( $newText ), [ 'doWork' => $diff, 'error' => $error ] ); return $work->execute(); } return $diff(); } /** * Diff the text representations of two content objects (or just two pieces of text in general). * This does the actual diffing, getTextDiff() wraps it with logging and resource limiting. * @param string $oldText * @param string $newText * @return string * @throws Exception */ protected function getTextDiffInternal( $oldText, $newText ) { $oldText = str_replace( "\r\n", "\n", $oldText ); $newText = str_replace( "\r\n", "\n", $newText ); if ( $oldText === $newText ) { return ''; } $textDiffer = $this->getTextDiffer(); $diffText = $textDiffer->render( $oldText, $newText, $this->format ); return $textDiffer->addRowWrapper( $this->format, $diffText ); } }