From: Kunal Mehta Date: Fri, 8 Aug 2014 16:41:26 +0000 (+0100) Subject: Add JSONContent and handler from EventLogging X-Git-Tag: 1.31.0-rc.0~14514 X-Git-Url: http://git.cyclocoop.org///%22%40url%40//%22?a=commitdiff_plain;h=d2a82fcb604efe62b2c994d63a568177783ee0ef;p=lhc%2Fweb%2Fwiklou.git Add JSONContent and handler from EventLogging As was discussed at the architecture summit, a basic JSON content class which handles validation and basic display. Not intended to be used directly, but for extensions to subclass. Co-Authored-By: addshore Change-Id: Ifcde9bcd0efcf15a3ab692dd2a0a3038559e0254 --- diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index 0cdb424fed..5002617c31 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -387,6 +387,8 @@ $wgAutoloadLocalClasses = array( 'CssContent' => 'includes/content/CssContent.php', 'JavaScriptContentHandler' => 'includes/content/JavaScriptContentHandler.php', 'JavaScriptContent' => 'includes/content/JavaScriptContent.php', + 'JSONContentHandler' => 'includes/content/JSONContentHandler.php', + 'JSONContent' => 'includes/content/JSONContent.php', 'MessageContent' => 'includes/content/MessageContent.php', 'MWContentSerializationException' => 'includes/content/ContentHandler.php', 'TextContentHandler' => 'includes/content/TextContentHandler.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 94476c5b04..80b8e520a5 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -858,9 +858,11 @@ $wgContentHandlers = array( CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', // dumb version, no syntax highlighting CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', + // simple implementation, for use by extensions, etc. + CONTENT_MODEL_JSON => 'JSONContentHandler', // dumb version, no syntax highlighting CONTENT_MODEL_CSS => 'CssContentHandler', - // plain text, for use by extensions etc + // plain text, for use by extensions, etc. CONTENT_MODEL_TEXT => 'TextContentHandler', ); diff --git a/includes/Defines.php b/includes/Defines.php index e0579cbf06..017e9ea4da 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -281,6 +281,7 @@ define( 'CONTENT_MODEL_WIKITEXT', 'wikitext' ); define( 'CONTENT_MODEL_JAVASCRIPT', 'javascript' ); define( 'CONTENT_MODEL_CSS', 'css' ); define( 'CONTENT_MODEL_TEXT', 'text' ); +define( 'CONTENT_MODEL_JSON', 'json' ); /**@}*/ /**@{ diff --git a/includes/content/JSONContent.php b/includes/content/JSONContent.php new file mode 100644 index 0000000000..0fa9282241 --- /dev/null +++ b/includes/content/JSONContent.php @@ -0,0 +1,119 @@ + + * @author Kunal Mehta + */ + +/** + * Represents the content of a JSON content. + */ +class JSONContent extends TextContent { + + public function __construct( $text, $modelId = CONTENT_MODEL_JSON ) { + parent::__construct( $text, $modelId ); + } + + /** + * Decodes the JSON into a PHP associative array. + * @return array + */ + public function getJsonData() { + return FormatJson::decode( $this->getNativeData(), true ); + } + + /** + * @return bool Whether content is valid JSON. + */ + public function isValid() { + return $this->getJsonData() !== null; + } + + /** + * Pretty-print JSON + * + * @return bool|null|string + */ + public function beautifyJSON() { + $decoded = FormatJson::decode( $this->getNativeData(), true ); + if ( !is_array( $decoded ) ) { + return null; + } + return FormatJson::encode( $decoded, true ); + + } + + /** + * Beautifies JSON prior to save. + * @param Title $title Title + * @param User $user User + * @param ParserOptions $popts + * @return JSONContent + */ + public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) { + return new JSONContent( $this->beautifyJSON() ); + } + + /** + * Set the HTML and add the appropriate styles + * + * + * @param Title $title + * @param int $revId + * @param ParserOptions $options + * @param bool $generateHtml + * @param ParserOutput $output + */ + protected function fillParserOutput( Title $title, $revId, + ParserOptions $options, $generateHtml, ParserOutput &$output + ) { + if ( $generateHtml ) { + $output->setText( $this->objectTable( $this->getJsonData() ) ); + $output->addModuleStyles( 'mediawiki.content.json' ); + } else { + $output->setText( '' ); + } + } + /** + * Constructs an HTML representation of a JSON object. + * @param Array $mapping + * @return string HTML. + */ + protected function objectTable( $mapping ) { + $rows = array(); + + foreach ( $mapping as $key => $val ) { + $rows[] = $this->objectRow( $key, $val ); + } + return Xml::tags( 'table', array( 'class' => 'mw-json' ), + Xml::tags( 'tbody', array(), join( "\n", $rows ) ) + ); + } + + /** + * Constructs HTML representation of a single key-value pair. + * @param string $key + * @param mixed $val + * @return string HTML. + */ + protected function objectRow( $key, $val ) { + $th = Xml::elementClean( 'th', array(), $key ); + if ( is_array( $val ) ) { + $td = Xml::tags( 'td', array(), self::objectTable( $val ) ); + } else { + if ( is_string( $val ) ) { + $val = '"' . $val . '"'; + } else { + $val = FormatJson::encode( $val ); + } + + $td = Xml::elementClean( 'td', array( 'class' => 'value' ), $val ); + } + + return Xml::tags( 'tr', array(), $th . $td ); + } + +} diff --git a/includes/content/JSONContentHandler.php b/includes/content/JSONContentHandler.php new file mode 100644 index 0000000000..b290fb2aa1 --- /dev/null +++ b/includes/content/JSONContentHandler.php @@ -0,0 +1,48 @@ + + * @author Kunal Mehta + */ + +class JSONContentHandler extends TextContentHandler { + + public function __construct( $modelId = CONTENT_MODEL_JSON ) { + parent::__construct( $modelId, array( CONTENT_FORMAT_JSON ) ); + } + + /** + * Unserializes a JSONContent object. + * + * @param string $text Serialized form of the content + * @param null|string $format The format used for serialization + * + * @return JSONContent + */ + public function unserializeContent( $text, $format = null ) { + $this->checkFormat( $format ); + return new JSONContent( $text ); + } + + /** + * Creates an empty JSONContent object. + * + * @return JSONContent + */ + public function makeEmptyContent() { + return new JSONContent( '' ); + } + + /** JSON is English **/ + public function getPageLanguage( Title $title, Content $content = null ) { + return wfGetLangObj( 'en' ); + } + + /** JSON is English **/ + public function getPageViewLanguage( Title $title, Content $content = null ) { + return wfGetLangObj( 'en' ); + } +} diff --git a/resources/Resources.php b/resources/Resources.php index b588cf24a3..e5d80cad71 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -811,6 +811,9 @@ return array( 'user.tokens', ), ), + 'mediawiki.content.json' => array( + 'styles' => 'resources/src/mediawiki/mediawiki.content.json.css', + ), 'mediawiki.debug' => array( 'scripts' => array( 'resources/src/mediawiki/mediawiki.debug.js', diff --git a/resources/src/mediawiki/mediawiki.content.json.css b/resources/src/mediawiki/mediawiki.content.json.css new file mode 100644 index 0000000000..583dd39899 --- /dev/null +++ b/resources/src/mediawiki/mediawiki.content.json.css @@ -0,0 +1,54 @@ +/** + * CSS for styling HTML-formatted JSON Schema objects + * + * @file + * @author Munaf Assaf + */ + +.mw-json { + border-collapse: collapse; + border-spacing: 0; + font-family: 'Bitstream Vera Sans', 'DejaVu Sans', 'Lucida Sans', 'Lucida Grande', sans-serif; + font-style: normal; +} + +.mw-json th, +.mw-json td { + border: 1px solid gray; + font-size: 16px; + padding: 0.5em 1em; +} + +.mw-json td { + background-color: #eee; + font-style: italic; +} + +.mw-json .value { + background-color: #dcfae3; + font-family: 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', Monaco, Courier, monospace; + white-space: pre-wrap; +} + +.mw-json tr { + margin-bottom: 0.5em; +} + +.mw-json th { + background-color: #fff; + font-weight: normal; +} + +.mw-json caption { + /* For stylistic reasons, suppress the caption of the outermost table */ + display: none; +} + +.mw-json table caption { + color: gray; + display: inline-block; + font-size: 10px; + font-style: italic; + margin-bottom: 0.5em; + text-align: left; +} diff --git a/tests/phpunit/includes/content/JSONContentTest.php b/tests/phpunit/includes/content/JSONContentTest.php new file mode 100644 index 0000000000..df8d2e2651 --- /dev/null +++ b/tests/phpunit/includes/content/JSONContentTest.php @@ -0,0 +1,112 @@ +assertEquals( $isValid, $obj->isValid() ); + $this->assertEquals( $expected, $obj->getJsonData() ); + } + + public function provideValidConstruction() { + return array( + array( 'foo', CONTENT_MODEL_JSON, false, null ), + array( FormatJson::encode( array() ), CONTENT_MODEL_JSON, true, array() ), + array( FormatJson::encode( array( 'foo' ) ), CONTENT_MODEL_JSON, true, array( 'foo' ) ), + ); + } + + /** + * @dataProvider provideDataToEncode + */ + public function testBeautifyUsesFormatJson( $data ) { + $obj = new JSONContent( FormatJson::encode( $data) ); + $this->assertEquals( FormatJson::encode( $data, true ), $obj->beautifyJSON() ); + } + + public function provideDataToEncode() { + return array( + array( array() ), + array( array( 'foo' ) ), + array( array( 'foo', 'bar' ) ), + array( array( 'baz' => 'foo', 'bar' ) ), + array( array( 'baz' => 1000, 'bar' ) ), + ); + } + + /** + * @dataProvider provideDataToEncode + */ + public function testPreSaveTransform( $data ) { + $obj = new JSONContent( FormatJson::encode( $data ) ); + $newObj = $obj->preSaveTransform( $this->getMockTitle(), $this->getMockUser() , $this->getMockParserOptions() ); + $this->assertTrue( $newObj->equals( new JSONContent( FormatJson::encode( $data, true ) ) ) ); + } + + private function getMockTitle() { + return $this->getMockBuilder( 'Title' ) + ->disableOriginalConstructor() + ->getMock(); + } + + private function getMockUser() { + return $this->getMockBuilder( 'User' ) + ->disableOriginalConstructor() + ->getMock(); + } + private function getMockParserOptions() { + return $this->getMockBuilder( 'ParserOptions' ) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @dataProvider provideDataAndParserText + */ + public function testFillParserOutput( $data, $expected ) { + $obj = new JSONContent( FormatJson::encode( $data ) ); + $parserOutput = $obj->getParserOutput( $this->getMockTitle(), null, null, true ); + $this->assertInstanceOf( 'ParserOutput', $parserOutput ); +// var_dump( $parserOutput->getText(), "\n" ); + $this->assertEquals( $expected, $parserOutput->getText() ); + } + + public function provideDataAndParserText() { + return array( + array( + array(), + '
' + ), + array( + array( 'foo' ), + '
0"foo"
' + ), + array( + array( 'foo', 'bar' ), + '' . + "\n" . + '
0"foo"
1"bar"
' + ), + array( + array( 'baz' => 'foo', 'bar' ), + '' . + "\n" . + '
baz"foo"
0"bar"
' + ), + array( + array( 'baz' => 1000, 'bar' ), + '' . + "\n" . + '
baz1000
0"bar"
' + ), + ); + } + +} \ No newline at end of file