Content::convert() for conv. betw. content models.
authordaniel <daniel.kinzler@wikimedia.de>
Mon, 5 Nov 2012 15:53:48 +0000 (16:53 +0100)
committerdaniel <daniel.kinzler@wikimedia.de>
Tue, 6 Nov 2012 16:39:10 +0000 (17:39 +0100)
This is needed to fix bug 41706 and similar, watch for follow-ups.

Automatic, implicit conversion may be handy in several cases, especially
for converting between different text based content models. E.g. it should
be possible to create a diff between a JavaScript and a wikitext page. This
change lais the foundations for this ability.

Change-Id: Ie7d87b67b24ac9897cb5696220a7785b228d3c79

docs/hooks.txt
includes/content/AbstractContent.php
includes/content/Content.php
includes/content/TextContent.php
tests/phpunit/includes/content/TextContentTest.php

index 998523f..03ee6c5 100644 (file)
@@ -752,6 +752,16 @@ cointent model name, but no entry for that model exists in $wgContentHandlers.
 $modeName: the requested content model name
 &$handler: set this to a ContentHandler object, if desired.
 
+'ConvertContent': Called by AbstractContent::convert when a conversion to another
+content model is requested.
+$content: The Content object to be converted.
+$toModel: The ID of the content model to convert to.
+$lossy:   boolean indicating whether lossy conversion is allowed.
+&$result: Output parameter, in case the handler function wants to provide a
+converted Content object. Note that $result->getContentModel() must return $toModel.
+Handler functions that modify $result should generally return false to further
+attempts at conversion.
+
 'ContribsPager::getQueryInfo': Before the contributions query is about to run
 &$pager: Pager object for contributions
 &$queryInfo: The query for the contribs Pager
index 0a8bb9e..386f55a 100644 (file)
@@ -411,4 +411,30 @@ abstract class AbstractContent implements Content {
        public function matchMagicWord( MagicWord $word ) {
                return false;
        }
+
+       /**
+        * @see Content::convert()
+        *
+        * This base implementation calls the hook ConvertContent to enable custom conversions.
+        * Subclasses may override this to implement conversion for "their" content model.
+        *
+        * @param String  $toModel the desired content model, use the CONTENT_MODEL_XXX flags.
+        * @param String  $lossy flag, set to "lossy" to allow lossy conversion. If lossy conversion is
+        * not allowed, full round-trip conversion is expected to work without losing information.
+        *
+        * @return Content|bool A content object with the content model $toModel, or false if
+        * that conversion is not supported.
+        */
+       public function convert( $toModel, $lossy = '' ) {
+               if ( $this->getModel() === $toModel ) {
+                       //nothing to do, shorten out.
+                       return $this;
+               }
+
+               $lossy = ( $lossy === 'lossy' ); // string flag, convert to boolean for convenience
+               $result = false;
+
+               wfRunHooks( 'ConvertContent', array( $this, $toModel, $lossy, &$result ) );
+               return $result;
+       }
 }
index 3c77694..35b51c3 100644 (file)
@@ -42,7 +42,7 @@ interface Content {
        /**
         * @since 1.21
         *
-        * @return string The wikitext to include when another page includes this
+        * @return string|false The wikitext to include when another page includes this
         * content, or false if the content is not includable in a wikitext page.
         *
         * @todo allow native handling, bypassing wikitext representation, like
@@ -481,7 +481,20 @@ interface Content {
         */
        public function matchMagicWord( MagicWord $word );
 
-       // TODO: ImagePage and CategoryPage interfere with per-content action handlers
+       /**
+        * Converts this content object into another content object with the given content model,
+        * if that is possible.
+        *
+        * @param String  $toModel the desired content model, use the CONTENT_MODEL_XXX flags.
+        * @param String  $lossy flag, set to "lossy" to allow lossy conversion. If lossy conversion is
+        * not allowed, full round-trip conversion is expected to work without losing information.
+        *
+        * @return Content|bool A content object with the content model $toModel, or false if
+        * that conversion is not supported.
+        */
+       public function convert( $toModel, $lossy = '' );
+
+               // TODO: ImagePage and CategoryPage interfere with per-content action handlers
        // TODO: nice&sane integration of GeSHi syntax highlighting
        //   [11:59] <vvv> Hooks are ugly; make CodeHighlighter interface and a
        //   config to set the class which handles syntax highlighting
index 0e22fce..872738b 100644 (file)
@@ -115,12 +115,21 @@ class TextContent extends AbstractContent {
        }
 
        /**
-        * Returns the text represented by this Content object, as a string.
+        * Returns attempts to convert this content object to wikitext,
+        * and then returns the text string. The conversion may be lossy.
         *
-        * @return string: the raw text
+        * @note: this allows any text-based content to be transcluded as if it was wikitext.
+        *
+        * @return string|false: the raw text, or null if the conversion failed
         */
        public function getWikitextForTransclusion( ) {
-               return $this->getNativeData();
+               $wikitext = $this->convert( CONTENT_MODEL_WIKITEXT, 'lossy' );
+
+               if ( $wikitext ) {
+                       return $wikitext->getNativeData();
+               } else {
+                       return false;
+               }
        }
 
        /**
@@ -237,4 +246,35 @@ class TextContent extends AbstractContent {
                # TODO: make Highlighter interface, use highlighter here, if available
                return htmlspecialchars( $this->getNativeData() );
        }
+
+       /**
+        * @see Content::convert()
+        *
+        * This implementation provides lossless conversion between content models based
+        * on TextContent.
+        *
+        * @param String  $toModel the desired content model, use the CONTENT_MODEL_XXX flags.
+        * @param String  $lossy flag, set to "lossy" to allow lossy conversion. If lossy conversion is
+        * not allowed, full round-trip conversion is expected to work without losing information.
+        *
+        * @return Content|bool A content object with the content model $toModel, or false if
+        * that conversion is not supported.
+        */
+       public function convert( $toModel, $lossy = '' ) {
+               $converted = parent::convert( $toModel, $lossy );
+
+               if ( $converted !== false ) {
+                       return $converted;
+               }
+
+               $toHandler = ContentHandler::getForModelID( $toModel );
+
+               if ( $toHandler instanceof TextContentHandler ) {
+                       //NOTE: ignore content serialization format - it's just text anyway.
+                       $text = $this->getNativeData();
+                       $converted = $toHandler->unserializeContent( $text );
+               }
+
+               return $converted;
+       }
 }
index c867a83..337945d 100644 (file)
@@ -378,4 +378,49 @@ class TextContentTest extends MediaWikiTestCase {
                }
        }
 
+       public static function provideConvert() {
+               return array(
+                       array( // #0
+                               'Hallo Welt',
+                               CONTENT_MODEL_WIKITEXT,
+                               'lossless',
+                               'Hallo Welt'
+                       ),
+                       array( // #1
+                               'Hallo Welt',
+                               CONTENT_MODEL_WIKITEXT,
+                               'lossless',
+                               'Hallo Welt'
+                       ),
+                       array( // #1
+                               'Hallo Welt',
+                               CONTENT_MODEL_CSS,
+                               'lossless',
+                               'Hallo Welt'
+                       ),
+                       array( // #1
+                               'Hallo Welt',
+                               CONTENT_MODEL_JAVASCRIPT,
+                               'lossless',
+                               'Hallo Welt'
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider provideConvert
+        */
+       public function testConvert( $text, $model, $lossy, $expectedNative ) {
+               $content = $this->newContent( $text );
+
+               $converted = $content->convert( $model, $lossy );
+
+               if ( $expectedNative === false ) {
+                       $this->assertFalse( $converted, "conversion to $model was expected to fail!" );
+               } else {
+                       $this->assertInstanceOf( 'Content', $converted );
+                       $this->assertEquals( $expectedNative, $converted->getNativeData() );
+               }
+       }
+
 }