From 4a7bc3ee313745b78e13c32c9df8aabef36f4443 Mon Sep 17 00:00:00 2001 From: "Moritz Schubotz (physikerwelt)" Date: Fri, 19 Jul 2019 22:46:20 +0200 Subject: [PATCH] Add special page and API endpoint that show information from math Wikibase items Add a special page and an API endpoint to fetch data from Wikibase items with a given qID. The special page summarizes information from Wikibase. The API endpoint allows to request the information directly. Both, the API endpoint and the special page, fetch the data from a new helper class for consistency. Bug: T208758 Bug: T229939 Change-Id: Idd22057a88312bf1a1cb5546d0a6edca5678d80d --- .phan/config.php | 6 +- Math.alias.noTranslate.php | 3 +- extension.json | 30 +++- i18n/en.json | 23 ++- i18n/qqq.json | 23 ++- modules/ext.math.wikibase.js | 17 +++ src/ApiMathWikibaseExtracts.php | 154 +++++++++++++++++++ src/MathMathML.php | 14 ++ src/MathWikibaseConfig.php | 133 ++++++++++++++++ src/MathWikibaseConnector.php | 232 ++++++++++++++++++++++++++++ src/MathWikibaseInfo.php | 199 ++++++++++++++++++++++++ src/SpecialMathWikibase.php | 258 ++++++++++++++++++++++++++++++++ 12 files changed, 1081 insertions(+), 11 deletions(-) create mode 100644 modules/ext.math.wikibase.js create mode 100644 src/ApiMathWikibaseExtracts.php create mode 100644 src/MathWikibaseConfig.php create mode 100644 src/MathWikibaseConnector.php create mode 100644 src/MathWikibaseInfo.php create mode 100644 src/SpecialMathWikibase.php diff --git a/.phan/config.php b/.phan/config.php index ea45573d1..0703b721f 100644 --- a/.phan/config.php +++ b/.phan/config.php @@ -8,8 +8,7 @@ $cfg['directory_list'] = array_merge( $cfg['directory_list'], [ '../../extensions/VisualEditor', - '../../extensions/Wikibase/repo', - '../../extensions/Wikibase/lib', + '../../extensions/Wikibase' ] ); @@ -17,8 +16,7 @@ $cfg['exclude_analysis_directory_list'] = array_merge( $cfg['exclude_analysis_directory_list'], [ '../../extensions/VisualEditor', - '../../extensions/Wikibase/repo', - '../../extensions/Wikibase/lib', + '../../extensions/Wikibase' ] ); diff --git a/Math.alias.noTranslate.php b/Math.alias.noTranslate.php index b751ed396..c4d9607e1 100644 --- a/Math.alias.noTranslate.php +++ b/Math.alias.noTranslate.php @@ -14,5 +14,6 @@ $specialPageAliases = []; /** English (English) */ $specialPageAliases['en'] = [ - 'MathShowImage' => [ 'MathShowImage' ] + 'MathShowImage' => [ 'MathShowImage' ], + 'MathWikibase' => [ 'MathWikibase' ] ]; diff --git a/extension.json b/extension.json index 34bd8801a..aa701ebff 100644 --- a/extension.json +++ b/extension.json @@ -5,7 +5,8 @@ "Tomasz Wegrzanowski", "Brion Vibber", "Moritz Schubotz", - "Derk-Jan Hartman" + "Derk-Jan Hartman", + "André Greiner-Petter" ], "url": "https://www.mediawiki.org/wiki/Extension:Math", "descriptionmsg": "math-desc", @@ -29,16 +30,26 @@ "MathInputCheckRestbase": "src/MathInputCheckRestbase.php", "SpecialMathShowImage": "src/SpecialMathShowImage.php", "SpecialMathStatus": "src/SpecialMathStatus.php", + "SpecialMathWikibase": "src/SpecialMathWikibase.php", "MathValidator": "src/MathValidator.php", "MathFormatter": "src/MathFormatter.php", "MathWikibaseHook": "src/MathWikibaseHook.php", "MathMLRdfBuilder": "src/MathMLRdfBuilder.php", "MathPng": "src/MathPng.php", - "MathDataUpdater": "src/MathDataUpdater.php" + "MathDataUpdater": "src/MathDataUpdater.php", + "MathWikibaseConfig": "src/MathWikibaseConfig.php", + "MathWikibaseConnector": "src/MathWikibaseConnector.php", + "MathWikibaseInfo": "src/MathWikibaseInfo.php", + "ApiMathWikibaseExtracts": "src/ApiMathWikibaseExtracts.php" }, "DefaultUserOptions": { "math": "mathml" }, + "APIPropModules": { + "mathwbextracts": { + "class": "ApiMathWikibaseExtracts" + } + }, "ExtensionMessagesFiles": { "MathAlias": "Math.alias.php", "MathAliasNoTranslate": "Math.alias.noTranslate.php" @@ -108,7 +119,11 @@ "source", "mathml" ], - "MathEnableWikibaseDataType": true + "MathEnableWikibaseDataType": true, + "MathEnableFormulaLinks": false, + "MathWikibasePropertyIdHasPart": "P527", + "MathWikibasePropertyIdDefiningFormula": "P2534", + "MathWikibasePropertyIdQuantitySymbol": "P416" }, "VisualEditorPluginModules": [ "ext.math.visualEditor" @@ -129,6 +144,12 @@ "ext.math.scripts": { "scripts": "ext.math.js" }, + "ext.math.wikibase.scripts": { + "scripts": "ext.math.wikibase.js", + "dependencies": [ + "jquery.wikibase.entityselector" + ] + }, "ext.math.visualEditor": { "scripts": [ "ve-math/ve.dm.MWLatexNode.js", @@ -246,7 +267,8 @@ }, "SpecialPages": { "MathShowImage": "SpecialMathShowImage", - "MathStatus": "SpecialMathStatus" + "MathStatus": "SpecialMathStatus", + "MathWikibase": "SpecialMathWikibase" }, "TrackingCategories": [ "math-tracking-category-error", diff --git a/i18n/en.json b/i18n/en.json index 5af9c0321..681800b09 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -95,7 +95,28 @@ "mw_math_source": "LaTeX source (for text browsers)", "prefs-math": "Math", "mathstatus": "Math status", + "mathwikibase": "Math Formula Information", "datatypes-type-math": "Mathematical expression", "wikibase-listdatatypes-math-head": "Mathematical expression", - "wikibase-listdatatypes-math-body": "Literal data field for mathematical expressions, formula, equations and such, expressed in a variant of LaTeX." + "wikibase-listdatatypes-math-body": "Literal data field for mathematical expressions, formula, equations and such, expressed in a variant of LaTeX.", + "math-wikibase-header": "Information of Mathematical Items", + "math-wikibase-formula": "Formula", + "math-wikibase-formula-name": "Name", + "math-wikibase-formula-type": "Type", + "math-wikibase-formula-header-format": "'''$1:''' $2", + "math-wikibase-formula-description": "Description", + "math-wikibase-formula-information": "Math Formula Information", + "math-wikibase-formula-link-header": "Data Source", + "math-wikibase-formula-elements-header": "Elements of the Forumla", + "math-wikibase-special-form-header": "Specify the name of the formula:", + "math-wikibase-special-form-placeholder": "Title of the formula", + "math-wikibase-special-form-button": "Request Information", + "math-wikibase-special-error-header": "Error", + "math-wikibase-special-error-invalid-argument": "Your specified Wikibase item ID does not exist.", + "math-wikibase-special-error-unknown": "An unknown error occured due fetching data from Wikibase.", + "math-wikibase-special-error-no-wikibase": "The Wikibase extension is required in order to use this special page.", + "apihelp-query+mathwbextracts-example-1": "Requests a summary of mathematical Wikibase items.", + "apihelp-query+mathwbextracts-summary": "Returns a summary of a mathematical Wikibase item in HTML.", + "apihelp-query+mathwbextracts-param-qid": "The QID of the Wikibase item that will be requested.", + "apihelp-query+mathwbextracts-param-uselang": "The language that should be requested for the Wikibase item. English is the default value." } diff --git a/i18n/qqq.json b/i18n/qqq.json index 3279b2a50..8bc97adc0 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -95,7 +95,28 @@ "mw_math_source": "In user preferences (math). All mw_math_* messages MUST be different, things will break otherwise!\n\nUsed as label for source radio button.\n\nSee also:\n* {{msg-mw|Mw math png}}\n* {{msg-mw|Mw math latexml}}", "prefs-math": "Used in user preferences as a section heading.\n{{Identical|Math}}", "mathstatus": "Title of a special page that displays information about the enabled math rendering modes.", + "mathwikibase": "Title of a special page that displays information about a mathematical Wikibase item.", "datatypes-type-math": "The name of a data type.\n{{related|Datatypes-type}}\n{{Identical|Mathematical expression}}", "wikibase-listdatatypes-math-head": "{{Wikibase-datatype-head|Mathematical expression|math}}\n{{Identical|Mathematical expression}}", - "wikibase-listdatatypes-math-body": "{{Wikibase-datatype-body|Mathematical expression}}" + "wikibase-listdatatypes-math-body": "{{Wikibase-datatype-body|Mathematical expression}}", + "apihelp-query+mathwbextracts-example-1": "{{doc-apihelp-example|query+mathwbextracts}}", + "apihelp-query+mathwbextracts-summary": "{{doc-apihelp-summary|query+mathwbextracts}}", + "apihelp-query+mathwbextracts-param-qid": "{{doc-apihelp-param|query+mathwbextracts|qid}}", + "apihelp-query+mathwbextracts-param-uselang": "{{doc-apihelp-param|query+mathwbextracts|uselang}}", + "math-wikibase-formula": "Label for the inline header for formulae", + "math-wikibase-formula-name": "Label for the name of the formula", + "math-wikibase-formula-type": "Label for the type of the formula", + "math-wikibase-formula-description": "Label for the description of the formula", + "math-wikibase-formula-information": "Header for information section of the formula", + "math-wikibase-formula-link-header": "Header for the hyperlink section to wikibase of the formula", + "math-wikibase-formula-elements-header": "Header for the elements section of the forumla", + "math-wikibase-formula-header-format": "The pattern to organize label $1 with information $2", + "math-wikibase-header": "wikibase Information of Mathematical Items", + "math-wikibase-special-form-header": "The header of the form to request a wikibase id from user.", + "math-wikibase-special-form-placeholder": "The placeholder of input field where a user can request a wikibase id.", + "math-wikibase-special-form-button": "The button label to submits the form to request a wikibase id.", + "math-wikibase-special-error-header": "The header of the error page. Will be shown when an error occured on the special page.", + "math-wikibase-special-error-invalid-argument": "The error text if the requested wikibase does not exist.", + "math-wikibase-special-error-unknown": "The error text when an unkown error occurred during fetching the information.\nFurther information about the error can be found in the logs.", + "math-wikibase-special-error-no-wikibase": "The error text that show up when the required Wikibase extension is missing." } diff --git a/modules/ext.math.wikibase.js b/modules/ext.math.wikibase.js new file mode 100644 index 000000000..8daa2c1a7 --- /dev/null +++ b/modules/ext.math.wikibase.js @@ -0,0 +1,17 @@ +( function () { + 'use strict'; + + var serverUrl = mw.config.get( 'wgServer' ), + scriptPath = mw.config.get( 'wgScriptPath' ), + repoApiUrl = serverUrl + scriptPath + '/api.php', + contLang = mw.config.get( 'wgContentLanguage' ); + + // eslint-disable-next-line no-jquery/no-global-selector + $( '.mwe-math-wikibase-entityselector-input input' ).entityselector( { + url: repoApiUrl, + language: contLang, + type: 'item' + } ).on( 'entityselectorselected', function ( event, entityId ) { + this.value = entityId; + } ); +}() ); diff --git a/src/ApiMathWikibaseExtracts.php b/src/ApiMathWikibaseExtracts.php new file mode 100644 index 000000000..52c11d7d8 --- /dev/null +++ b/src/ApiMathWikibaseExtracts.php @@ -0,0 +1,154 @@ +wikibase = new MathWikibaseConnector( + MathWikibaseConfig::getDefaultMathWikibaseConfig() + ); + } + + /** + * @throws ApiUsageException + */ + public function execute() { + $result = $this->getResult(); + $params = $this->extractRequestParams(); + + $qid = $params['qid']; + $lang = $params['uselang'] ?: 'en'; + + try { + $info = $this->wikibase->fetchWikibaseFromId( $qid, $lang ); + $result->addValue( + [ 'query', 'mathwbextracts' ], + $qid, + $this->buildResponse( $qid, $lang, $info ) + ); + } catch ( MWException $e ) { + // impossible to fetch the data. Keep the answer empty + $result->addValue( + [ 'query', 'mathwbextracts' ], + $qid, + [] + ); + } + } + + /** + * @param string $qid + * @param string $lang language code + * @param MathWikibaseInfo $info information from MathWikibaseConnector + * @return array response + */ + private function buildResponse( $qid, $lang, $info ) { + $specialPageTitle = Title::newFromText( "Special:MathWikibase" ); + $url = $specialPageTitle->getLocalURL( [ + 'qid' => $qid + ] ); + + try { + $langObj = Language::factory( $lang ); + } catch ( MWException $e ) { + throw new InvalidArgumentException( "Invalid language code." ); + } + + return [ + "title" => $info->getLabel(), + "extract" => self::buildHTMLRepresentation( $info ), + "canonicalurl" => $url, + "fullurl" => $url, + "contentmodel" => "html", + "pagelanguage" => $langObj->getCode(), + "pagelanguagedir" => $langObj->getDir(), + "pagelanguagehtmlcode" => $langObj->getHtmlCode() + ]; + } + + /** + * Generates an HTML string from the given data. + * @param MathWikibaseInfo $info an info object generated by fetchWikibaseFromId + * @return string an HTML representation of the given info object + */ + private function buildHTMLRepresentation( $info ) { + $output = Html::openElement( + "div", + [ "style" => "display: flex; flex-direction: row; align-items: flex-start; flow-wrap: wrap;" ] + ); + + if ( $info->hasParts() ) { + $output .= Html::openElement( "div", [ "style" => "width: 70%;" ] ); + } else { + $output .= Html::openElement( "div", [ "style" => "width: 100%;" ] ); + } + + $output .= Html::element( "span", [ "style" => "font-weight: bold;" ], $info->getLabel() ); + $output .= Html::element( "span", [], " (" . $info->getDescription() . ")" ); + $output .= Html::closeElement( "div" ); + + if ( $info->hasParts() ) { + $output .= Html::openElement( + "div", + [ "style" => "width: 30%; display: flex; justify-content: center;" ] + ); + $output .= $info->generateSmallTableOfParts(); + $output .= Html::closeElement( "div" ); + } + + $output .= Html::closeElement( "div" ); + return $output; + } + + /** + * Defines the parameters for this API endpoint + * @return array + */ + public function getAllowedParams() { + return [ + 'qid' => [ + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ], + 'uselang' => [ + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => false + ] + ]; + } + + /** + * @see ApiBase::getExamplesMessages() + * @return array + */ + protected function getExamplesMessages() { + return [ + 'action=query&prop=mathwbextracts&mathqid=Q35875&mathuselang=en' + => 'apihelp-query+mathwbextracts-example-1', + ]; + } + + /** + * @see ApiBase::getHelpUrls() + * @return string + */ + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/Extension:Math#API'; + } +} diff --git a/src/MathMathML.php b/src/MathMathML.php index 408623ac2..c6a3ab303 100644 --- a/src/MathMathML.php +++ b/src/MathMathML.php @@ -7,6 +7,7 @@ */ use MediaWiki\Logger\LoggerFactory; +use MediaWiki\MediaWikiServices; /** * Converts LaTeX to MathML using the mathoid-server @@ -471,6 +472,8 @@ class MathMathML extends MathRenderer { * @return string Html output that is embedded in the page */ public function getHtmlOutput() { + $config = MediaWikiServices::getInstance()->getMainConfig(); + $enableLinks = $config->get( "MathEnableFormulaLinks" ); if ( $this->getMathStyle() == 'display' ) { $element = 'div'; } else { @@ -480,10 +483,18 @@ class MathMathML extends MathRenderer { if ( $this->getID() !== '' ) { $attribs['id'] = $this->getID(); } + $hyperlink = null; if ( isset( $this->params['qid'] ) && preg_match( '/Q\d+/', $this->params['qid'] ) ) { $attribs['data-qid'] = $this->params['qid']; + // TODO: SpecialPage::getTitleFor uses the depcrated method Title::newFromTitleValue and + // declares a never thrown MWException. Once this SpecialPage is updated, update the next line. + $titleObj = Title::newFromLinkTarget( SpecialPage::getTitleValueFor( 'MathWikibase' ) ); + $hyperlink = $titleObj->getLocalURL( [ 'qid' => $this->params['qid'] ] ); } $output = Html::openElement( $element, $attribs ); + if ( $hyperlink && $enableLinks ) { + $output .= Html::openElement( 'a', [ 'href' => $hyperlink, 'style' => 'color:inherit;' ] ); + } // MathML has to be wrapped into a div or span in order to be able to hide it. // Remove displayStyle attributes set by the MathML converter // (Beginning from Mathoid 0.2.5 block is the default layout.) @@ -497,6 +508,9 @@ class MathMathML extends MathRenderer { 'class' => $this->getClassName(), 'style' => 'display: none;' ], $mml ); $output .= $this->getFallbackImage(); + if ( $hyperlink && $enableLinks ) { + $output .= Html::closeElement( 'a' ); + } $output .= Html::closeElement( $element ); return $output; } diff --git a/src/MathWikibaseConfig.php b/src/MathWikibaseConfig.php new file mode 100644 index 000000000..cf08624b3 --- /dev/null +++ b/src/MathWikibaseConfig.php @@ -0,0 +1,133 @@ +idParser = $entityIdParser; + $this->entityRevisionLookup = $entityRevisionLookup; + $this->labelLookupFactory = $labelDescriptionLookupFactory; + + $config = MediaWikiServices::getInstance()->getMainConfig(); + $this->propertyIdHasPart = $this->idParser->parse( + $config->get( "MathWikibasePropertyIdHasPart" ) + ); + $this->propertyIdDefiningFormula = $this->idParser->parse( + $config->get( "MathWikibasePropertyIdDefiningFormula" ) + ); + $this->propertyIdQuantitySymbol = $this->idParser->parse( + $config->get( "MathWikibasePropertyIdQuantitySymbol" ) + ); + } + + /** + * @return EntityIdParser + */ + public function getIdParser() : EntityIdParser { + return $this->idParser; + } + + /** + * @return EntityRevisionLookup + */ + public function getEntityRevisionLookup() : EntityRevisionLookup { + return $this->entityRevisionLookup; + } + + /** + * @return LanguageFallbackLabelDescriptionLookupFactory + */ + public function getLabelLookupFactory() : LanguageFallbackLabelDescriptionLookupFactory { + return $this->labelLookupFactory; + } + + /** + * @return PropertyId + */ + public function getPropertyIdHasPart() : PropertyId { + return $this->propertyIdHasPart; + } + + /** + * @return PropertyId + */ + public function getPropertyIdQuantitySymbol() : PropertyId { + return $this->propertyIdQuantitySymbol; + } + + /** + * @return PropertyId + */ + public function getPropertyIdDefiningFormula() : PropertyId { + return $this->propertyIdDefiningFormula; + } + + /** + * @return MathWikibaseConfig default config + */ + public static function getDefaultMathWikibaseConfig() : MathWikibaseConfig { + if ( !self::$defaultConfig ) { + $wikibaseRepo = WikibaseRepo::getDefaultInstance(); + self::$defaultConfig = new MathWikibaseConfig( + $wikibaseRepo->getEntityIdParser(), + $wikibaseRepo->getEntityRevisionLookup(), + $wikibaseRepo->getLanguageFallbackLabelDescriptionLookupFactory() + ); + } + return self::$defaultConfig; + } +} diff --git a/src/MathWikibaseConnector.php b/src/MathWikibaseConnector.php new file mode 100644 index 000000000..8f4b8532f --- /dev/null +++ b/src/MathWikibaseConnector.php @@ -0,0 +1,232 @@ +config = $config; + } + + /** + * @param string $qid + * @param string $langCode the language to fetch data + * (may fallback if requested language does not exist) + * + * @return MathWikibaseInfo the object may be empty if no information can be fetched. + * @throws InvalidArgumentException if the language code does not exist or the given + * id does not exist + */ + public function fetchWikibaseFromId( $qid, $langCode ) { + try { + $lang = Language::factory( $langCode ); + } catch ( MWException $e ) { + throw new InvalidArgumentException( "Invalid language code specified." ); + } + + $langLookupFactory = $this->config->getLabelLookupFactory(); + $langLookup = $langLookupFactory->newLabelDescriptionLookup( $lang ); + + $idParser = $this->config->getIdParser(); + $entityRevisionLookup = $this->config->getEntityRevisionLookup(); + + try { + $entityId = $idParser->parse( $qid ); // exception if the given ID is invalid + $entityRevision = $entityRevisionLookup->getEntityRevision( $entityId ); + } catch ( EntityIdParsingException $e ) { + throw new InvalidArgumentException( "Invalid Wikibase ID." ); + } catch ( RevisionedUnresolvedRedirectException $e ) { + throw new InvalidArgumentException( "Non-existing Wikibase ID." ); + } catch ( StorageException $e ) { + throw new InvalidArgumentException( "Non-existing Wikibase ID." ); + } + + if ( !$entityId || !$entityRevision ) { + throw new InvalidArgumentException( "Non-existing Wikibase ID." ); + } + + $entity = $entityRevision->getEntity(); + $output = new MathWikibaseInfo( $entityId ); + + if ( $entity instanceof Item ) { + $this->fetchLabelDescription( $output, $langLookup ); + $this->fetchStatements( $output, $entity, $langLookup ); + return $output; + } else { // we only allow Wikibase items + throw new InvalidArgumentException( "The specified Wikibase ID does not represented an item." ); + } + } + + /** + * Fetches only label and description from an entity. + * @param MathWikibaseInfo $output the entity id of the entity + * @param LabelDescriptionLookup $langLookup a lookup handler to fetch right languages + * @return MathWikibaseInfo filled up with label and description + */ + private function fetchLabelDescription( + MathWikibaseInfo $output, + LabelDescriptionLookup $langLookup ) { + $label = $langLookup->getLabel( $output->getId() ); + $desc = $langLookup->getDescription( $output->getId() ); + + if ( $label ) { + $output->setLabel( $label->getText() ); + } + + if ( $desc ) { + $output->setDescription( $desc->getText() ); + } + + return $output; + } + + /** + * Fetches 'has part' statements from a given item element with a defined lookup object for + * the languages. + * @param MathWikibaseInfo $output the output element + * @param Item $item item to fetch statements from + * @param LabelDescriptionLookup $langLookup + * @return MathWikibaseInfo the updated $output object + */ + private function fetchStatements( + MathWikibaseInfo $output, + Item $item, + LabelDescriptionLookup $langLookup ) { + $statements = $item->getStatements(); + + $hasPartStatements = $statements->getByPropertyId( $this->config->getPropertyIdHasPart() ); + $this->fetchHasPartSnaks( $output, $hasPartStatements, $langLookup ); + + $symbolStatement = $statements->getByPropertyId( $this->config->getPropertyIdDefiningFormula() ); + if ( $symbolStatement->count() < 1 ) { // if it's not a formula, it might be a symbol + $symbolStatement = $statements->getByPropertyId( $this->config->getPropertyIdQuantitySymbol() ); + } + $this->fetchSymbol( $output, $symbolStatement ); + return $output; + } + + /** + * Fetches the symbol or defining formula from a statement list and adds the symbol to the + * given info object + * @param MathWikibaseInfo $output + * @param StatementList $statements + * @return MathWikibaseInfo updated object + */ + private function fetchSymbol( MathWikibaseInfo $output, StatementList $statements ) { + foreach ( $statements as $statement ) { + $snak = $statement->getMainSnak(); + if ( $snak instanceof PropertyValueSnak && $this->isQualifierDefinien( $snak ) ) { + $dataVal = $snak->getDataValue(); + $symbol = new StringValue( $dataVal->getValue() ); + $output->setSymbol( $symbol ); + return $output; + } + } + + return $output; + } + + /** + * Fetches single snaks from 'has part' statements + * + * @param MathWikibaseInfo $output + * @param StatementList $statements the 'has part' statements + * @param LabelDescriptionLookup $langLookup + * @return MathWikibaseInfo + *@todo refactor this method once Wikibase has a more convenient way to handle snaks + */ + private function fetchHasPartSnaks( + MathWikibaseInfo $output, + StatementList $statements, + LabelDescriptionLookup $langLookup ) { + foreach ( $statements as $statement ) { + $snaks = $statement->getAllSnaks(); + $innerInfo = null; + $symbol = null; + + foreach ( $snaks as $snak ) { + if ( $snak instanceof PropertyValueSnak ) { + if ( $this->isQualifierDefinien( $snak ) ) { + $dataVal = $snak->getDataValue(); + $symbol = new StringValue( $dataVal->getValue() ); + } elseif ( $snak->getPropertyId()->equals( $this->config->getPropertyIdHasPart() ) ) { + $dataVal = $snak->getDataValue(); + $entityIdValue = $dataVal->getValue(); + if ( $entityIdValue instanceof EntityIdValue ) { + $innerEntityId = $entityIdValue->getEntityId(); + $innerInfo = new MathWikibaseInfo( $innerEntityId ); + $this->fetchLabelDescription( $innerInfo, $langLookup ); + } + } + } + } + + if ( $innerInfo ) { + $innerInfo->setSymbol( $symbol ); + $output->addHasPartElement( $innerInfo ); + } + } + + return $output; + } + + /** + * @param Snak $snak + * @return bool true if the given snak is either a defining formula or a quantity symbol + */ + private function isQualifierDefinien( Snak $snak ) { + return $snak->getPropertyId()->equals( $this->config->getPropertyIdQuantitySymbol() ) || + $snak->getPropertyId()->equals( $this->config->getPropertyIdDefiningFormula() ); + } + + /** + * @todo should be refactored once there is an easier way to get the URL + * @param string $qID + * @return string + */ + public static function buildURL( $qID ) { + $baseurl = WikibaseClient::getDefaultInstance() + ->getSettings()->getSetting( 'repoUrl' ); + $articlePath = WikibaseClient::getDefaultInstance() + ->getSettings()->getSetting( 'repoArticlePath' ); + $namespaces = WikibaseClient::getDefaultInstance() + ->getSettings()->getSetting( 'repoNamespaces' ); + + $url = $baseurl . $articlePath; + + if ( $namespaces && $namespaces["item"] ) { + $articleId = $namespaces["item"] . ":" . $qID; + } else { + $articleId = $qID; + } + + // repoArticlePath contains the placeholder $1 for the page title + // see: https://www.mediawiki.org/wiki/Manual:$wgArticlePath + return str_replace( '$1', $articleId, $url ); + } +} diff --git a/src/MathWikibaseInfo.php b/src/MathWikibaseInfo.php new file mode 100644 index 000000000..6cdbd1027 --- /dev/null +++ b/src/MathWikibaseInfo.php @@ -0,0 +1,199 @@ +id = $entityId; + $this->mathFormatter = new MathFormatter( SnakFormatter::FORMAT_HTML ); + } + + /** + * @param string $label + */ + public function setLabel( $label ) { + $this->label = $label; + } + + /** + * @param string $description + */ + public function setDescription( $description ) { + $this->description = $description; + } + + /** + * @param StringValue $symbol + */ + public function setSymbol( $symbol ) { + $this->symbol = $symbol; + } + + /** + * @param MathWikibaseInfo $info + */ + public function addHasPartElement( MathWikibaseInfo $info ) { + array_push( $this->hasParts, $info ); + } + + /** + * @param MathWikibaseInfo[] $infos + */ + public function addHasPartElements( $infos ) { + array_push( $this->hasParts, ...$infos ); + } + + /** + * @return EntityId id + */ + public function getId() { + return $this->id; + } + + /** + * @return string label + */ + public function getLabel() { + return $this->label; + } + + /** + * @return string description + */ + public function getDescription() { + return $this->description; + } + + /** + * @return StringValue symbol + */ + public function getSymbol() { + return $this->symbol; + } + + /** + * @return string|null html formatted version of the symbol + */ + public function getFormattedSymbol() { + if ( $this->symbol ) { + return $this->mathFormatter->format( $this->getSymbol() ); + } else { + return null; + } + } + + /** + * @return MathWikibaseInfo[] hasparts + */ + public function getParts() { + return $this->hasParts; + } + + /** + * Does this info object has elements? + * @return bool true if there are elements otherwise false + */ + public function hasParts() { + if ( !$this->hasParts ) { + return false; + } + return $this->hasParts !== []; + } + + /** + * Generates an HTML table representation of the has-parts elements + * @return string + */ + public function generateTableOfParts() { + $lang = MediaWikiServices::getInstance()->getContentLanguage(); + $labelAlign = $lang->isRTL() ? 'left' : 'right'; + $labelAlignOpposite = !$lang->isRTL() ? 'left' : 'right'; + + $output = HTML::openElement( "table", [ "style" => "padding: 5px" ] ); + $output .= HTML::openElement( "tbody" ); + + foreach ( $this->hasParts as $part ) { + $output .= HTML::openElement( "tr" ); + $output .= HTML::element( + "td", + [ "style" => "font-weight: bold; text-align:$labelAlign;" ], + $part->getLabel() + ); + $output .= HTML::rawElement( + "td", + [ "style" => "text-align:center; padding: 2px; padding-left: 10px; padding-right: 10px;" ], + $part->getFormattedSymbol() + ); + $output .= HTML::element( + "td", + [ "style" => "font-style: italic; text-align:$labelAlignOpposite;" ], + $part->getDescription() + ); + $output .= HTML::closeElement( "tr" ); + } + + $output .= HTML::closeElement( "tbody" ); + $output .= HTML::closeElement( "table" ); + return $output; + } + + /** + * Generates a minimalized HTML representation of the has-parts elements. + * @return string + */ + public function generateSmallTableOfParts() { + $output = HTML::openElement( "table" ); + $output .= HTML::openElement( "tbody" ); + + foreach ( $this->hasParts as $part ) { + $output .= HTML::openElement( "tr" ); + $output .= HTML::rawElement( + "td", + [ "style" => "text-align:right;" ], + $part->getFormattedSymbol() + ); + $output .= HTML::element( "td", [ "style" => "text-align:left;" ], $part->getLabel() ); + $output .= HTML::closeElement( "tr" ); + } + + $output .= HTML::closeElement( "tbody" ); + $output .= HTML::closeElement( "table" ); + return $output; + } +} diff --git a/src/SpecialMathWikibase.php b/src/SpecialMathWikibase.php new file mode 100644 index 000000000..42ceb0717 --- /dev/null +++ b/src/SpecialMathWikibase.php @@ -0,0 +1,258 @@ +logger = LoggerFactory::getInstance( 'Math' ); + } + + /** + * @inheritDoc + */ + public function execute( $par ) { + global $wgContLanguageCode; + + if ( !$this->isWikibaseAvailable() ) { + $out = $this->getOutput(); + + $out->setPageTitle( + $this->getPlainText( 'math-wikibase-special-error-header' ) + ); + $out->addHTML( + wfMessage( 'math-wikibase-special-error-no-wikibase' )->inContentLanguage() + ); + return; + } + + if ( !$this->wikibase ) { + $this->wikibase = new MathWikibaseConnector( + MathWikibaseConfig::getDefaultMathWikibaseConfig() + ); + } + + $request = $this->getRequest(); + $output = $this->getOutput(); + $output->enableOOUI(); + + $this->setHeaders(); + $output->addModules( [ 'ext.math.wikibase.scripts' ] ); + + $output->setPageTitle( + $this->getPlainText( 'math-wikibase-header' ) + ); + + // Get request + $requestId = $request->getText( self::PARAMETER ); + + // if there is no id requested, show the request form + if ( !$requestId ) { + $this->showForm(); + } else { + $this->logger->debug( "Request qID: " . $requestId ); + try { + $info = $this->wikibase->fetchWikibaseFromId( $requestId, $wgContLanguageCode ); + $this->logger->debug( "Successfully fetched information for qID: " . $requestId ); + self::buildPageRepresentation( $info, $requestId, $output ); + } catch ( Exception $e ) { + $this->showError( $e ); + } + } + } + + /** + * Shows the form to request information for a specific Wikibase id + */ + private function showForm() { + $actionField = new \OOUI\ActionFieldLayout( + new \OOUI\TextInputWidget( [ + 'name' => self::PARAMETER, + 'placeholder' => $this->getPlainText( 'math-wikibase-special-form-placeholder' ), + 'required' => true, + 'autocomplete' => false, + 'classes' => [ 'mwe-math-wikibase-entityselector-input' ] + ] ), + new OOUI\ButtonInputWidget( [ + 'name' => 'request-qid', + 'label' => $this->getPlainText( 'math-wikibase-special-form-button' ), + 'type' => 'submit', + 'flags' => [ 'primary', 'progressive' ], + 'icon' => 'check', + ] ), + [ + 'label' => $this->getPlainText( 'math-wikibase-special-form-header' ), + 'align' => 'top' + ] + ); + + $formLayout = new \OOUI\FormLayout( [ + 'method' => 'POST', + 'items' => [ $actionField ] + ] ); + + $this->getOutput()->addHTML( $formLayout ); + } + + /** + * Shows an error message for the user and writes information to $logger + * @param Exception $e can potentially be any exception. + */ + private function showError( Exception $e ) { + $this->getOutput()->setPageTitle( + $this->getPlainText( 'math-wikibase-special-error-header' ) + ); + + if ( $e instanceof InvalidArgumentException ) { + $this->logger->warning( "An invalid ID was specified. Reason: " . $e->getMessage() ); + $this->getOutput()->addHTML( + wfMessage( 'math-wikibase-special-error-invalid-argument' )->inContentLanguage() + ); + } else { + $this->logger->error( "An unknown error occured due fetching data from Wikibase.", [ + 'exception' => $e + ] ); + $this->getOutput()->addHTML( + wfMessage( 'math-wikibase-special-error-unknown' )->inContentLanguage() + ); + } + } + + /** + * Helper function to shorten i18n text processing + * @param string $key + * @return string the plain text in current content language + */ + private function getPlainText( $key ) { + return wfMessage( $key )->inContentLanguage()->plain(); + } + + /** + * @param MathWikibaseInfo $info + * @param string $qid + * @param OutputPage $output + */ + public static function buildPageRepresentation( + MathWikibaseInfo $info, + $qid, + OutputPage $output ) { + $output->setPageTitle( $info->getLabel() ); + + // if 'instance of' is specified, it can be found in the description before a colon + preg_match( '/(.*):\s*(.*)/', $info->getDescription(), $matches ); + + if ( count( $matches ) > 1 ) { + $output->setSubtitle( $matches[1] ); + } + + // add formula information + $header = wfMessage( 'math-wikibase-formula-information' )->inContentLanguage(); + $output->addHTML( self::createHTMLHeader( $header ) ); + + if ( $info->getSymbol() ) { + $math = $info->getFormattedSymbol(); + $formulaInfo = new Message( 'math-wikibase-formula-header-format' ); + $formulaInfo->rawParams( + wfMessage( 'math-wikibase-formula' )->inContentLanguage(), + $math + ); + $output->addHTML( HTML::rawElement( "p", [], $formulaInfo->inContentLanguage() ) ); + } + + $labelName = wfMessage( + 'math-wikibase-formula-header-format', + wfMessage( 'math-wikibase-formula-name' )->inContentLanguage(), + $info->getLabel() + )->inContentLanguage(); + $output->addHTML( HTML::rawElement( "p", [], $labelName ) ); + + if ( count( $matches ) > 2 ) { + $labelType = wfMessage( + 'math-wikibase-formula-header-format', + wfMessage( 'math-wikibase-formula-type' )->inContentLanguage(), + $matches[1] + )->inContentLanguage(); + + $labelDesc = wfMessage( + 'math-wikibase-formula-header-format', + wfMessage( 'math-wikibase-formula-description' )->inContentLanguage(), + $matches[2] + )->inContentLanguage(); + + $output->addHTML( HTML::rawElement( "p", [], $labelType ) ); + $output->addHTML( HTML::rawElement( "p", [], $labelDesc ) ); + } else { + $labelDesc = wfMessage( + 'math-wikibase-formula-header-format', + wfMessage( 'math-wikibase-formula-description' )->inContentLanguage(), + $info->getDescription() + )->inContentLanguage(); + $output->addHTML( HTML::rawElement( "p", [], $labelDesc ) ); + } + + // add parts of formula + if ( $info->hasParts() ) { + $elementsHeader = wfMessage( 'math-wikibase-formula-elements-header' ) + ->inContentLanguage()->escaped(); + $output->addHTML( self::createHTMLHeader( $elementsHeader ) ); + $output->addHTML( $info->generateTableOfParts() ); + } + + // add link information + $wikibaseHeader = wfMessage( + 'math-wikibase-formula-link-header', + $info->getDescription() + )->inContentLanguage(); + + $output->addHTML( self::createHTMLHeader( $wikibaseHeader ) ); + + $url = MathWikibaseConnector::buildURL( $qid ); + $link = HTML::linkButton( $url, [ "href" => $url ] ); + $output->addHTML( HTML::rawElement( "p", [], $link ) ); + } + + /** + * Generates a header as HTML + * @param $header + * @return string + */ + private static function createHTMLHeader( $header ) { + $headerOut = HTML::openElement( "h2" ); + $headerOut .= HTML::rawElement( "span", [ "class" => "mw-headline" ], $header ); + $headerOut .= HTML::closeElement( "h2" ); + return $headerOut; + } + + /** + * Check whether Wikibase is available or not + * @return true|false + */ + public static function isWikibaseAvailable() { + return class_exists( '\Wikibase\Client\WikibaseClient' ) && + class_exists( '\Wikibase\Repo\WikibaseRepo' ); + } +}