Krinkle has uploaded a new change for review. https://gerrit.wikimedia.org/r/49840
Change subject: (DRAFT) initial commit ...................................................................... (DRAFT) initial commit Change-Id: Icf305892a9512545a63f5a5280cc0d340c61585f --- A EXAMPLE.js A TemplateData.hooks.php A TemplateData.i18n.php A TemplateData.php A TemplateDataItem.php A api/ApiQueryTemplateData.php A resources/ext.templateData.css 7 files changed, 664 insertions(+), 0 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/TemplateData refs/changes/40/49840/1 diff --git a/EXAMPLE.js b/EXAMPLE.js new file mode 100644 index 0000000..9674252 --- /dev/null +++ b/EXAMPLE.js @@ -0,0 +1,152 @@ +/* + Specification for the JSON descriptor as used in the + TemplateData extension for MediaWiki. + + Author: Timo Tijhof + Author: Trevor Parscal + Latest version: https://gist.github.com/Krinkle/a47844a677d76c815998 + +@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 intend for the deprecated parameters. + @property {Array} [aliases] List of aliases. + An alias is an alternative name for the parameter that may be used instead + (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. + + */ + +/** + * 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" + } + } +} diff --git a/TemplateData.hooks.php b/TemplateData.hooks.php new file mode 100644 index 0000000..d9b7d95 --- /dev/null +++ b/TemplateData.hooks.php @@ -0,0 +1,90 @@ +<?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 TemplateDataItem::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 ) { + // If this call is contained in a transcluded page or template, display nothing. + // TODO: Why? + if ( $input === '' || !$frame->title->equals( $parser->getTitle() ) ) { + return; + } + + $ti = TemplateDataItem::newFromJSON( $input ); + + $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(); + } +} diff --git a/TemplateData.i18n.php b/TemplateData.i18n.php new file mode 100644 index 0000000..9d523c4 --- /dev/null +++ b/TemplateData.i18n.php @@ -0,0 +1,37 @@ +<?php +$messages = array(); + +/** English + * @author Timo Tijhof + */ +$messages['en'] = array( + + // Special:Version + 'templatedata-desc' => 'Implement data storage for template parameters (using JSON).', + + // 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..dcce1f4 --- /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['TemplateDataItem'] = $dir . '/TemplateDataItem.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.css1', + 'localBasePath' => $dir, + 'remoteExtPath' => 'TemplateData', +); diff --git a/TemplateDataItem.php b/TemplateDataItem.php new file mode 100644 index 0000000..cebf152 --- /dev/null +++ b/TemplateDataItem.php @@ -0,0 +1,242 @@ +<?php +/** + * TemplateDataItem class + * + * @file + * @ingroup Extensions + */ + +class TemplateDataItem { + /** + * @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. + * + * Specification for the JSON object: + * + * @structure root + * @property {string} [description] + * @property {Object} params Contains all parameters. Keyed by parameter name, contains Param objects. + * + * @structure Param + * @property {string} [inherits] + * @property {boolean} [required=false] + * @property {string|Object} [description] Free-form description of this parameter. + * @property {boolean|string} [deprecated=false] Tooltip for the user detailing the intended + * action on deprecated parameters. + * @property {Array} [aliases] List of aliases. An alias is an alternative name for the parameter + * that may be used instead (not in addition) to the primary name. + * Aliases are not documented in the params object further. If they + * need more information, they should be in their own property as "deprecated". + * @property {string} [default] The default value or description thereof. + * + * Example for {{unsigned|JohnDoe|2012-10-18}}, {{unsigned|user=JohnDoe|date=2012-10-18}} + * <templatedata> + * { + * "params": { + * "user": { + * "required:": true, + * "description": "User name of person who forgot to sign their comment.", + * "aliases": ["1"] + * }, + * "date": { + * "description": "Timestamp of when the comment was posted, in YYYY-MM-DD format.", + * "aliases": ["2"] + * } + * } + * } + * </templatedata> + * @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() { + $data = $this->data; + $html = + Html::openElement( 'div', array( 'mw-templatedata-wrap' ) ) + . Html::element( 'p', array( 'mw-templatedata-desc' ), $data->description ) + . '<table class="wikitable sortable mw-templatedata-params">' + . '<caption>Template parameters</caption>' + . '<thead><tr>' + . '<th>Name</th>' + . '<th>Description</th>' + . '<th>Default</th>' + . '<th>Status</th>' + . '</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-field-empty' => $paramObj->description === '' && $paramObj->deprecated === false + ) + ), $paramObj->description !== '' ? $paramObj->description : 'no description' ) + // Default + . Html::element( 'td', array( + 'class' => array( + 'mw-templatedata-field-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..fcc8302 --- /dev/null +++ b/api/ApiQueryTemplateData.php @@ -0,0 +1,79 @@ +<?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' ); + } + + 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..00ad7f6 --- /dev/null +++ b/resources/ext.templateData.css @@ -0,0 +1,16 @@ +.mw-templatedata-wrap { + +} + +.mw-templatedata-desc { + +} + +.mw-templatedata-params { + +} + +.mw-templatedata-field-empty { + font-style: italic; + color: #333; +} -- To view, visit https://gerrit.wikimedia.org/r/49840 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Icf305892a9512545a63f5a5280cc0d340c61585f Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/TemplateData Gerrit-Branch: master Gerrit-Owner: Krinkle <[email protected]> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
