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:
parent
c8409c6756
commit
d2a82fcb60
8 changed files with 342 additions and 1 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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' );
|
||||
/**@}*/
|
||||
|
||||
/**@{
|
||||
|
|
|
|||
119
includes/content/JSONContent.php
Normal file
119
includes/content/JSONContent.php
Normal 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 );
|
||||
}
|
||||
|
||||
}
|
||||
48
includes/content/JSONContentHandler.php
Normal file
48
includes/content/JSONContentHandler.php
Normal 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' );
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
54
resources/src/mediawiki/mediawiki.content.json.css
Normal file
54
resources/src/mediawiki/mediawiki.content.json.css
Normal 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;
|
||||
}
|
||||
112
tests/phpunit/includes/content/JSONContentTest.php
Normal file
112
tests/phpunit/includes/content/JSONContentTest.php
Normal 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">"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>'
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue