'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',
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',
);
define( 'CONTENT_MODEL_JAVASCRIPT', 'javascript' );
define( 'CONTENT_MODEL_CSS', 'css' );
define( 'CONTENT_MODEL_TEXT', 'text' );
+define( 'CONTENT_MODEL_JSON', 'json' );
/**@}*/
/**@{
--- /dev/null
+<?php
+/**
+ * JSON Content Model
+ *
+ * @file
+ *
+ * @author Ori Livneh <ori@wikimedia.org>
+ * @author Kunal Mehta <legoktm@gmail.com>
+ */
+
+/**
+ * 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 );
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * JSON Schema Content Handler
+ *
+ * @file
+ *
+ * @author Ori Livneh <ori@wikimedia.org>
+ * @author Kunal Mehta <legoktm@gmail.com>
+ */
+
+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' );
+ }
+}
'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',
--- /dev/null
+/**
+ * CSS for styling HTML-formatted JSON Schema objects
+ *
+ * @file
+ * @author Munaf Assaf <massaf@wikimedia.org>
+ */
+
+.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;
+}
--- /dev/null
+<?php
+
+/**
+ * @author Adam Shorland
+ * @covers JSONContent
+ */
+class JSONContentTest extends MediaWikiLangTestCase {
+
+ /**
+ * @dataProvider provideValidConstruction
+ */
+ public function testValidConstruct( $text, $modelId, $isValid, $expected ) {
+ $obj = new JSONContent( $text, $modelId );
+ $this->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(),
+ '<table class="mw-json"><tbody></tbody></table>'
+ ),
+ array(
+ array( 'foo' ),
+ '<table class="mw-json"><tbody><tr><th>0</th><td class="value">"foo"</td></tr></tbody></table>'
+ ),
+ array(
+ array( 'foo', 'bar' ),
+ '<table class="mw-json"><tbody><tr><th>0</th><td class="value">"foo"</td></tr>' .
+ "\n" .
+ '<tr><th>1</th><td class="value">"bar"</td></tr></tbody></table>'
+ ),
+ array(
+ array( 'baz' => 'foo', 'bar' ),
+ '<table class="mw-json"><tbody><tr><th>baz</th><td class="value">"foo"</td></tr>' .
+ "\n" .
+ '<tr><th>0</th><td class="value">"bar"</td></tr></tbody></table>'
+ ),
+ array(
+ array( 'baz' => 1000, 'bar' ),
+ '<table class="mw-json"><tbody><tr><th>baz</th><td class="value">1000</td></tr>' .
+ "\n" .
+ '<tr><th>0</th><td class="value">"bar"</td></tr></tbody></table>'
+ ),
+ );
+ }
+
+}
\ No newline at end of file