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 <addshorewiki@gmail.com>
Change-Id: Ifcde9bcd0efcf15a3ab692dd2a0a3038559e0254
This commit is contained in:
Kunal Mehta 2014-08-08 17:41:26 +01:00 committed by Reedy
parent c8409c6756
commit d2a82fcb60
8 changed files with 342 additions and 1 deletions

View file

@ -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',

View file

@ -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',
);

View file

@ -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' );
/**@}*/
/**@{

View file

@ -0,0 +1,119 @@
<?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 );
}
}

View file

@ -0,0 +1,48 @@
<?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' );
}
}

View file

@ -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',

View file

@ -0,0 +1,54 @@
/**
* 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;
}

View file

@ -0,0 +1,112 @@
<?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">&quot;foo&quot;</td></tr></tbody></table>'
),
array(
array( 'foo', 'bar' ),
'<table class="mw-json"><tbody><tr><th>0</th><td class="value">&quot;foo&quot;</td></tr>' .
"\n" .
'<tr><th>1</th><td class="value">&quot;bar&quot;</td></tr></tbody></table>'
),
array(
array( 'baz' => 'foo', 'bar' ),
'<table class="mw-json"><tbody><tr><th>baz</th><td class="value">&quot;foo&quot;</td></tr>' .
"\n" .
'<tr><th>0</th><td class="value">&quot;bar&quot;</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">&quot;bar&quot;</td></tr></tbody></table>'
),
);
}
}