Catrope has submitted this change and it was merged.
Change subject: Initial TemplateData commit
......................................................................
Initial TemplateData commit
Registers a parser tag <templatedata> that should have a JSON
blob as content. The blob is then validated and normalised when
MediaWiki parses the page (e.g. during save and preview).
If there are validation errors, the save is aborted from the
extension hook and an error is displayed.
If all goes well, the normalised blob is stored in the database
(which can be retrieved through the API). And an HTML
representation of the template parameters is returned to the
wikitext parser to show where the <templatedata> was in the page.
The blob format is specified in spec.templatedata.json and
is validated in TemplateDataBlob::parse.
Bug: 44444
Change-Id: Icf305892a9512545a63f5a5280cc0d340c61585f
---
A TemplateData.hooks.php
A TemplateData.i18n.php
A TemplateData.php
A TemplateDataBlob.php
A api/ApiQueryTemplateData.php
A resources/ext.templateData.css
A spec.templatedata.json
7 files changed, 644 insertions(+), 0 deletions(-)
Approvals:
Catrope: Verified; Looks good to me, approved
diff --git a/TemplateData.hooks.php b/TemplateData.hooks.php
new file mode 100644
index 0000000..2f478a9
--- /dev/null
+++ b/TemplateData.hooks.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * Hooks for TemplateInfo extension
+ *
+ * @file
+ * @ingroup Extensions
+ */
+
+class TemplateDataHooks {
+
+ /**
+ * Register parser hooks
+ */
+ public static function onParserFirstCallInit( &$parser ) {
+ $parser->setHook( 'templatedata', array( 'TemplateDataHooks',
'render' ) );
+ return true;
+ }
+
+ /**
+ * @param Page &$page
+ * @param User &$user
+ * @param Content &$content
+ * @param string &$summary
+ * @param $minor
+ * @param bool|null $watchthis
+ * @param $sectionanchor
+ * @param &$flags
+ * @param Status &$status
+ */
+ public static function onPageContentSave( &$page, &$user, &$content,
&$summary, $minor,
+ $watchthis, $sectionanchor, &$flags, &$status
+ ) {
+
+ // The PageContentSave hook provides raw $text, but not $parser
because at this stage
+ // the page is not actually parsed yet. Which means we can't
know whether self::render()
+ // got a valid tag or not. Looking at $text directly is not a
solution either as
+ // it may not be in the current page (it can be transcluded).
+ // Since there is no later hook that allows aborting the save
and showing an error,
+ // we will have to trigger the parser ourselves.
+ // Fortunately this causes no overhead since the below (copied
from WikiPage::doEditContent,
+ // right after this hook is ran) has guards that lazy-init and
return early if called again
+ // later by the real WikiPage.
+
+ $editInfo = $page->prepareContentForEdit( $content, null,
$user, $serialisation_format = null );
+
+ if ( isset( $editInfo->output->ext_templatedata_status ) ) {
+ $validation =
$editInfo->output->ext_templatedata_status;
+ if ( !$validation->isOK() ) {
+ // Abort edit, show error message from
TemplateDataBlob::getStatus
+ $status->merge( $validation );
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Parser hook for <templatedata>.
+ * If there is any JSON provided, render the template documentation on
the page.
+ *
+ * @param string $input: The content of the tag.
+ * @param array $args: The attributes of the tag.
+ * @param Parser $parser: Parser instance available to render
+ * wikitext into html, or parser methods.
+ * @param PPFrame $frame: Can be used to see what template parameters
("{{{1}}}", etc.) this hook was used with.
+ *
+ * @return string: HTML to insert in the page.
+ */
+ public static function render( $input, $args, $parser, $frame ) {
+ $ti = TemplateDataBlob::newFromJSON( $input );
+ // TODO: Is there a better context?
+ $context = RequestContext::getMain();
+
+ $status = $ti->getStatus();
+ if ( !$status->isOK() ) {
+ $parser->getOutput()->ext_templatedata_status = $status;
+ return '<div class="error">' . $status->getHtml() .
'</div>';
+ }
+
+ $parser->getOutput()->setProperty( 'templatedata',
$ti->getJSON() );
+
+ $parser->getOutput()->addModules( 'ext.templateData' );
+
+ return $ti->getHtml( $context );
+ }
+}
diff --git a/TemplateData.i18n.php b/TemplateData.i18n.php
new file mode 100644
index 0000000..17a6199
--- /dev/null
+++ b/TemplateData.i18n.php
@@ -0,0 +1,44 @@
+<?php
+$messages = array();
+
+/** English
+ * @author Timo Tijhof
+ */
+$messages['en'] = array(
+
+ // Special:Version
+ 'templatedata-desc' => 'Implement data storage for template parameters
(using JSON).',
+
+ // Page output for <templatedata>
+ 'templatedata-doc-params' => 'Template parameters',
+ 'templatedata-doc-param-name' => 'Name',
+ 'templatedata-doc-param-desc' => 'Description',
+ 'templatedata-doc-param-default' => 'Default',
+ 'templatedata-doc-param-status' => 'Status',
+
+ // Error message for edit page
+ 'templatedata-invalid-parse' => 'SyntaxError in JSON.',
+ 'templatedata-invalid-type' => 'Property "$1" is expected to be of type
"$2".',
+ 'templatedata-invalid-missing' => 'Required property "$1" not found.',
+ 'templatedata-invalid-unknown' => 'Unexpected property "$1".',
+ 'templatedata-invalid-value' => 'Invalid value for property "$1".',
+);
+
+/** Message documentation (Message documentation)
+ * @author Timo Tijhof
+ */
+$messages['qqq'] = array(
+ 'templatedata-desc' => '{{desc}}',
+ 'templatedata-invalid-type' => 'Error message when a property is of the
wrong type.
+* $1: Name of property
+* $2: Expected type of property
+ ',
+ 'templatedata-invalid-missing' => 'Error message when a required
property is not found.
+* $1: Name of name
+* $2: Type of property',
+ 'templatedata-invalid-unknown' => 'Error message when an unknown
property is found.
+* $1: Name of property',
+ 'templatedata-invalid-value' => 'Error message when a property that
cannot contain free-form text has an invalid value.
+* $1: Name of property',
+);
+
diff --git a/TemplateData.php b/TemplateData.php
new file mode 100644
index 0000000..8ccbd63
--- /dev/null
+++ b/TemplateData.php
@@ -0,0 +1,48 @@
+<?php
+/**
+ * TemplateInfo extension.
+ *
+ * @file
+ * @ingroup Extensions
+ */
+
+if ( version_compare( $wgVersion, '1.20', '<' ) ) {
+ echo "Extension:TemplateInfo requires MediaWiki 1.20 or higher.\n";
+ exit( 1 );
+}
+
+$wgExtensionCredits['parserhook'][] = array(
+ 'path' => __FILE__,
+ 'name' => 'TemplateData',
+ 'author' => array( 'Timo Tijhof' ),
+ 'version' => '0.1.0',
+ 'url' =>
'https://www.mediawiki.org/wiki/Extension:TemplateData',
+ 'descriptionmsg' => 'templatedata-desc',
+);
+
+/* Setup */
+
+$dir = __DIR__;
+
+// Register files
+$wgExtensionMessagesFiles['TemplateData'] = $dir . '/TemplateData.i18n.php';
+$wgAutoloadClasses['TemplateDataHooks'] = $dir . '/TemplateData.hooks.php';
+$wgAutoloadClasses['TemplateDataBlob'] = $dir . '/TemplateDataBlob.php';
+$wgAutoloadClasses['ApiQueryTemplateData'] = $dir .
'/api/ApiQueryTemplateData.php';
+
+// Register hooks
+$wgHooks['ParserFirstCallInit'][] = 'TemplateDataHooks::onParserFirstCallInit';
+$wgHooks['PageContentSave'][] = 'TemplateDataHooks::onPageContentSave';
+
+// Register API actions
+$wgAPIPropModules['templatedata'] = 'ApiQueryTemplateData';
+
+// Register page_props
+$wgPageProps['templatedata'] = 'Content of <templatedata> tag';
+
+// Register modules
+$wgResourceModules['ext.templateData'] = array(
+ 'styles' => 'resources/ext.templateData.css',
+ 'localBasePath' => $dir,
+ 'remoteExtPath' => 'TemplateData',
+);
diff --git a/TemplateDataBlob.php b/TemplateDataBlob.php
new file mode 100644
index 0000000..b4f4b03
--- /dev/null
+++ b/TemplateDataBlob.php
@@ -0,0 +1,214 @@
+<?php
+/**
+ * @file
+ * @ingroup Extensions
+ */
+
+/**
+ * Represents the information about a template,
+ * coming from the JSON blob in the <templatedata> tags
+ * on wiki pages.
+ *
+ * @class
+ */
+class TemplateDataBlob {
+ /**
+ * @var stdClass
+ */
+ private $data;
+
+ /**
+ * @var Status: Cache of TemplateInfo::validate
+ */
+ private $status;
+
+ /**
+ * @param string $json
+ * @return TemplateInfo
+ */
+ public static function newFromJSON( $json ) {
+ $ti = new self( json_decode( $json ) );
+ $status = $ti->parse();
+
+ // TODO: Normalise `params.*.description` to a plain object.
+
+ if ( !$status->isOK() ) {
+ // Don't save invalid data, clear it.
+ $ti->data = new stdClass();
+ }
+ $ti->status = $status;
+ return $ti;
+ }
+
+ /**
+ * Parse the data, normalise it and validate it.
+ *
+ * See spec.templatedata.json for the expected format of the JSON
object.
+ * @return Status
+ */
+ private function parse() {
+ $data = $this->data;
+
+ if ( $data === null ) {
+ return Status::newFatal( 'templatedata-invalid-parse' );
+ }
+
+ if ( !is_object( $data ) ) {
+ return Status::newFatal( 'templatedata-invalid-type',
'templatedata', 'object' );
+ }
+
+ foreach ( $data as $key => $value ) {
+ if ( !in_array( $key, array( 'params', 'description' )
) ) {
+ return Status::newFatal(
'templatedata-invalid-unknown', $key );
+ }
+ }
+
+ if ( !isset( $data->params ) ) {
+ return Status::newFatal(
'templatedata-invalid-missing', 'params', 'object' );
+ }
+
+ if ( !is_object( $data->params ) ) {
+ return Status::newFatal( 'templatedata-invalid-type',
'params', 'object' );
+ }
+
+ if ( isset( $data->description ) ) {
+ if ( !is_object( $data->params ) ) {
+ return Status::newFatal(
'templatedata-invalid-type', 'params', 'object' );
+ }
+ } else {
+ $data->description = '';
+ }
+
+ foreach ( $data->params as $paramName => $paramObj ) {
+ if ( !is_object( $paramObj ) ) {
+ return Status::newFatal(
'templatedata-invalid-type', 'params.' . $paramName, 'object' );
+ }
+
+ foreach ( $paramObj as $key => $value ) {
+ if ( !in_array( $key, array(
+ 'required',
+ 'description',
+ 'deprecated',
+ 'aliases',
+ 'clones',
+ 'default',
+ ) ) ) {
+ return Status::newFatal(
'templatedata-invalid-unknown', $key );
+ }
+ }
+
+ if ( isset( $paramObj->required ) ) {
+ if ( !is_bool( $paramObj->required ) ) {
+ return Status::newFatal(
'templatedata-invalid-type', 'params.' . $paramName . '.required', 'boolean' );
+ }
+ } else {
+ $paramObj->required = false;
+ }
+
+ if ( isset( $paramObj->description ) ) {
+ if ( !is_object( $paramObj->description ) &&
!is_string( $paramObj->description ) ) {
+ // TODO: Also validate that if it is an
object, the keys are valid lang codes
+ // and the values strings.
+ return Status::newFatal(
'templatedata-invalid-type', 'params.' . $paramName . '.description',
'string|object' );
+ }
+ } else {
+ $paramObj->description = '';
+ }
+
+ if ( isset( $paramObj->deprecated ) ) {
+ if ( $paramObj->deprecated === false ||
is_string( $paramObj->deprecated ) ) {
+ return Status::newFatal(
'templatedata-invalid-type', 'params.' . $paramName . '.deprecated',
'boolean|string' );
+ }
+ } else {
+ $paramObj->deprecated = false;
+ }
+
+ if ( isset( $paramObj->aliases ) ) {
+ if ( !is_array( $paramObj->aliases ) ) {
+ // TODO: Validate the array values.
+ return Status::newFatal(
'templatedata-invalid-type', 'params.' . $paramName . '.aliases', 'array' );
+ }
+ } else {
+ $paramObj->aliases = array();
+ }
+
+ if ( isset( $paramObj->clones ) ) {
+ if ( !is_array( $paramObj->clones ) ) {
+ // TODO: Validate the array values.
+ return Status::newFatal(
'templatedata-invalid-type', 'params.' . $paramName . '.clones', 'array' );
+ }
+ } else {
+ $paramObj->clones = array();
+ }
+
+ if ( isset( $paramObj->default ) ) {
+ if ( !is_string( $paramObj->default ) ) {
+ return Status::newFatal(
'templatedata-invalid-type', 'params.' . $paramName . '.default', 'string' );
+ }
+ } else {
+ $paramObj->default = '';
+ }
+ }
+
+ return Status::newGood();
+ }
+
+ public function getStatus() {
+ return $this->status;
+ }
+
+ public function getJSON() {
+ return json_encode( $this->data );
+ }
+
+ public function getHtml( IContextSource $context ) {
+ $data = $this->data;
+ $html =
+ Html::openElement( 'div', array( 'class' =>
'mw-templatedata-doc-wrap' ) )
+ . Html::element( 'p', array( 'class' =>
'mw-templatedata-doc-desc' ), $data->description )
+ . '<table class="wikitable sortable
mw-templatedata-doc-params">'
+ . Html::element( 'caption', array(), $context->msg(
'templatedata-doc-params' ) )
+ . '<thead><tr>'
+ . Html::element( 'th', array(), $context->msg(
'templatedata-doc-param-name' ) )
+ . Html::element( 'th', array(), $context->msg(
'templatedata-doc-param-desc' ) )
+ . Html::element( 'th', array(), $context->msg(
'templatedata-doc-param-default' ) )
+ . Html::element( 'th', array(), $context->msg(
'templatedata-doc-param-status' ) )
+ . '</tr></thead>'
+ . '<tbody>'
+ ;
+ foreach ( $data->params as $paramName => $paramObj ) {
+ $description = '';
+ $default = '';
+ $html .= '<tr>'
+ . Html::element( 'th', array(), $paramName )
+ // Description
+ . Html::rawElement( 'td', array(
+ 'class' => array(
+ 'mw-templatedata-doc-param-empty' =>
$paramObj->description === '' && $paramObj->deprecated === false
+ )
+ ), $paramObj->description !== '' ?
$paramObj->description : 'no description' )
+ // Default
+ . Html::element( 'td', array(
+ 'class' => array(
+ 'mw-templatedata-doc-param-empty' =>
$paramObj->default === ''
+ )
+ ), $paramObj->default !== '' ? $paramObj->default :
'empty' )
+ // Status
+ . Html::element( 'td', array(),
+ $paramObj->deprecated ? 'deprecated' : (
+ $paramObj->required ? 'required' :
'optional'
+ )
+ )
+ . '</tr>';
+ }
+ $html .= '</tbody></table>'
+ . Html::closeElement( 'div' );
+
+ return $html;
+ }
+
+ private function __construct( stdClass $data = null ) {
+ $this->data = $data;
+ }
+
+}
diff --git a/api/ApiQueryTemplateData.php b/api/ApiQueryTemplateData.php
new file mode 100644
index 0000000..8a0053a
--- /dev/null
+++ b/api/ApiQueryTemplateData.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Implement the 'templatedata' query module in the API.
+ * Format JSON only.
+ *
+ * @ingroup API
+ * @emits error.code templatedata-corrupt
+ */
+class ApiQueryTemplateData extends ApiQueryBase {
+
+ public function __construct( $query, $module ) {
+ parent::__construct( $query, $module, 'td' );
+ }
+
+ /**
+ * TODO: This currently outputs it in an ugly '*' property
+ * and it fails in formats like XML (works in JSON/YAML).
+ */
+ public function execute() {
+ $params = $this->extractRequestParams();
+ $titles = $this->getPageSet()->getGoodTitles(); // page_id =>
Title object
+
+ if ( !count( $titles ) ) {
+ return;
+ }
+
+ $this->addTables( 'page_props' );
+ $this->addFields( array( 'pp_page', 'pp_value' ) );
+ $this->addWhere( array(
+ 'pp_page' => array_keys( $titles ),
+ 'pp_propname' => 'templatedata'
+ ) );
+ $this->addOption( 'ORDER BY', 'pp_page' );
+
+ if ( $params['continue'] !== null ) {
+ $fromid = intval( $params['continue'] );
+ $this->addWhere( "pp_page >= $fromid" );
+ }
+
+ $res = $this->select( __METHOD__ );
+ foreach ( $res as $row ) {
+ $rawData = $row->pp_value;
+ $data = json_decode( $rawData );
+
+ if ( !$data ) {
+ $this->dieUsage( 'Database data is corrupted.',
'templatedata-corrupt' );
+ }
+ $value = array();
+ ApiResult::setContent( $value, $data->params, 'params'
);
+
+ $fit = $this->addPageSubItems( $row->pp_page, $value );
+
+ if ( !$fit ) {
+ $this->setContinueEnumParameter( 'continue',
$row->pp_page );
+ break;
+ }
+ }
+ }
+
+ public function getAllowedParams() {
+ return array(
+ 'continue' => null,
+ );
+ }
+
+ public function getParamDescription() {
+ return array(
+ 'continue' => 'When more results are available, use
this to continue',
+ );
+ }
+
+ public function getDescription() {
+ return 'Data stored by the TemplateData extension
(https://www.mediawiki.org/Extension:TemplateData)';
+ }
+
+ // getPossibleErrors() is provided by ApiQueryBase
+
+ protected function getExamples() {
+ return array(
+
'api.php?action=query&prop=templatedata&titles=Template:Stub|Template:Example',
+ );
+ }
+}
diff --git a/resources/ext.templateData.css b/resources/ext.templateData.css
new file mode 100644
index 0000000..e232649
--- /dev/null
+++ b/resources/ext.templateData.css
@@ -0,0 +1,16 @@
+.mw-templatedata-doc-wrap {
+
+}
+
+.mw-templatedata-doc-desc {
+
+}
+
+.mw-templatedata-doc-params {
+
+}
+
+.mw-templatedata-doc-param-empty {
+ font-style: italic;
+ color: #333;
+}
diff --git a/spec.templatedata.json b/spec.templatedata.json
new file mode 100644
index 0000000..ea09a14
--- /dev/null
+++ b/spec.templatedata.json
@@ -0,0 +1,153 @@
+/*
+ Specification for the JSON descriptor as used in the
+ TemplateData extension for MediaWiki.
+
+ Author: Timo Tijhof
+ Author: Trevor Parscal
+
+@structure {Object} Root
+ @property {InterfaceText} [description]
+ @property {Object} params Contains all parameters.
+ Keyed by parameter name, contains #Param objects.
+ @property {Object} sets Groups of parameters that should be used
+ together. Groups may overlap with each other, though this is not
recommended.
+ Keyed by an internal id, contains #Set objects.
+
+@structure {Object} Param
+ @property {InterfaceText} [label] Defaults to key of object in
`Root.params`.
+ @property {boolean} [required=false]
+ @property {InterfaceText} [description]
+ @property {boolean|string} [deprecated=false] Tooltip for the user
detailing
+ the intent for the deprecated parameters.
+ @property {Array} [aliases] List of aliases.
+ An alias is an alternative name for the parameter that may be used
instead of
+ (not in addition to) the primary name. Aliases are not documented in a
+ separate Param object. If they need more information, they should be
in their
+ own property marked "deprecated".
+ @property {string} [inherits] Key to another object in `Root.params`.
+ The current Param object will inherit from that one, with local
properties
+ overriding the inherited ones.
+ @property {string} [default] The default value or description thereof.
+ @property {Type} [type] The type of the expected parameter value.
+
+@structure {Object} Set
+ @property {InterfaceText} [label] Defaults to key of object in
`Root.sets`.
+ @property {Array} params A subset of the parameter's names that belong
to this set.
+
+@structure {string} Type
+ One of the following:
+ - string
+ Any textual value.
+ - number
+ Any numerical value (without decimal points or thousand separators).
+ - wikipage
+ A valid MediaWiki page name for the current wiki. Doesn't have to
exist,
+ but if not, should be a valid page name to create.
+ - wikiuser
+ The username of an account on the current wiki (regardless of whether
+ that user has an edit count or a user page).
+
+@structure {string|Object} InterfaceText
+ A free-form string (no wikitext) in the content-language of the wiki,
or,
+ an object containing those strings keyed by language code.
+
+
+ Examples:
+ */
+[
+ /**
+ * Template:Unsigned
+ * Example for
+ * {{unsigned|JohnDoe|2012-10-18}}
+ *
{{unsigned|user=JohnDoe|year=2012|month=10|day=18|comment=blabla}}
+ */
+ {
+ "params": {
+ "user": {
+ "label": "Username",
+ "required:": true,
+ "description": "User name of person who forgot
to sign their comment.",
+ "aliases": ["1"]
+ },
+ "date": {
+ "label": {
+ "en": "Date"
+ },
+ "description": {
+ "en": "Timestamp of when the comment
was posted, in YYYY-MM-DD format."
+ },
+ "aliases": ["2"]
+ },
+ "year": {
+ "label": "Year"
+ },
+ "month": {
+ "label": "Month"
+ },
+ "day": {
+ "label": "Day"
+ },
+ "comment": {
+ "required": false
+ }
+ },
+ "sets": {
+ "date": {
+ "label": "Date",
+ "params": ["year", "month", "day"]
+ }
+ }
+ },
+ /**
+ * Template:TemplateBox
+ * Example for:
+ * {{TemplateBox|1d=..|2d=..|10d=..}}
+ */
+ {
+ "description": "Document the documenter.",
+ "params": {
+ "1d": {
+ "label": "Param 1",
+ "description": "Description of the template
parameter",
+ "type": "string"
+
+ },
+ "2d": {
+ "label": "Param 2",
+ "inherits": "1d"
+ },
+ "3d": {
+ "label": "Param 3",
+ "inherits": "1d"
+ },
+ "4d": {
+ "label": "Param 4",
+ "inherits": "1d"
+ },
+ "5d": {
+ "label": "Param 5",
+ "inherits": "1d"
+ },
+ "6d": {
+ "label": "Param 6",
+ "inherits": "1d"
+ },
+ "7d": {
+ "label": "Param 7",
+ "inherits": "1d"
+ },
+ "8d": {
+ "label": "Param 8",
+ "inherits": "1d"
+ },
+ "9d": {
+ "label": "Param 9",
+ "inherits": "1d"
+ },
+ "10d": {
+ "label": "Param 10",
+ "inherits": "1d"
+ }
+ }
+ }
+]
--
To view, visit https://gerrit.wikimedia.org/r/49840
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: Icf305892a9512545a63f5a5280cc0d340c61585f
Gerrit-PatchSet: 7
Gerrit-Project: mediawiki/extensions/TemplateData
Gerrit-Branch: master
Gerrit-Owner: Krinkle <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: Krinkle <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: Trevor Parscal <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits