lib/includes/Formatters/WikibaseValueFormatterBuilders.php
<?php
declare( strict_types = 1 );
namespace Wikibase\Lib\Formatters;
use DataValues\Geo\Formatters\GlobeCoordinateFormatter;
use DataValues\Geo\Formatters\LatLongFormatter;
use InvalidArgumentException;
use MediaWiki\Context\RequestContext;
use MediaWiki\Languages\LanguageFactory;
use MediaWiki\Parser\ParserOptions;
use ValueFormatters\DecimalFormatter;
use ValueFormatters\FormatterOptions;
use ValueFormatters\QuantityFormatter;
use ValueFormatters\QuantityHtmlFormatter;
use ValueFormatters\StringFormatter;
use ValueFormatters\ValueFormatter;
use Wikibase\DataModel\Entity\EntityIdParser;
use Wikibase\DataModel\Services\EntityId\EntityIdLabelFormatter;
use Wikibase\DataModel\Services\Lookup\EntityLookup;
use Wikibase\DataModel\Services\Lookup\EntityRetrievingTermLookup;
use Wikibase\Lib\LanguageNameLookup;
use Wikibase\Lib\LanguageNameLookupFactory;
use Wikibase\Lib\Store\CachingFallbackLabelDescriptionLookup;
use Wikibase\Lib\Store\EntityExistenceChecker;
use Wikibase\Lib\Store\EntityRedirectChecker;
use Wikibase\Lib\Store\EntityTitleLookup;
use Wikibase\Lib\Store\EntityTitleTextLookup;
use Wikibase\Lib\Store\EntityUrlLookup;
use Wikibase\Lib\Store\LanguageFallbackLabelDescriptionLookup;
use Wikibase\Lib\Store\RedirectResolvingLatestRevisionLookup;
use Wikibase\Lib\TermFallbackCache\TermFallbackCacheFacade;
use Wikibase\Lib\TermLanguageFallbackChain;
/**
* Low level factory for ValueFormatters for well known data types.
*
* @warning: This is a low level factory for use by bootstrap code only!
* Program logic should use an instance of OutputFormatValueFormatterFactory
* resp. OutputFormatSnakFormatterFactory.
*
* @license GPL-2.0-or-later
* @author Daniel Kinzler
*/
class WikibaseValueFormatterBuilders {
/**
* @var FormatterLabelDescriptionLookupFactory
*/
private $labelDescriptionLookupFactory;
/**
* @var LanguageNameLookupFactory
*/
private $languageNameLookupFactory;
/**
* @var EntityIdParser
*/
private $itemUriParser;
/**
* @var string
*/
private $geoShapeStorageBaseUrl;
/**
* @var EntityTitleLookup|null
*/
private $entityTitleLookup;
/**
* Unit URIs that represent "unitless" or "one".
*
* @todo make this configurable
*
* @var string[]
*/
private $unitOneUris = [
'http://www.wikidata.org/entity/Q199',
'http://qudt.org/vocab/unit#Unitless',
];
/**
* @var string
*/
private $tabularDataStorageBaseUrl;
/**
* @var EntityLookup
*/
private $entityLookup;
/**
* @var RedirectResolvingLatestRevisionLookup
*/
private $redirectResolvingLatestRevisionLookup;
/**
* @var TermFallbackCacheFacade
*/
private $cache;
/**
* @var SnakFormat
*/
private $snakFormat;
/**
* @var CachingKartographerEmbeddingHandler|null
*/
private $kartographerEmbeddingHandler;
/**
* @var bool
*/
private $useKartographerMaplinkInWikitext;
/**
* @var array
*/
private $thumbLimits;
/**
* @var EntityExistenceChecker
*/
private $entityExistenceChecker;
/**
* @var EntityTitleTextLookup
*/
private $entityTitleTextLookup;
/**
* @var EntityUrlLookup
*/
private $entityUrlLookup;
/**
* @var EntityRedirectChecker
*/
private $entityRedirectChecker;
/**
* @var LanguageFactory
*/
private $languageFactory;
public function __construct(
FormatterLabelDescriptionLookupFactory $labelDescriptionLookupFactory,
LanguageNameLookupFactory $languageNameLookupFactory,
EntityIdParser $itemUriParser,
string $geoShapeStorageBaseUrl,
string $tabularDataStorageBaseUrl,
TermFallbackCacheFacade $termFallbackCacheFacade,
EntityLookup $entityLookup,
RedirectResolvingLatestRevisionLookup $redirectResolvingLatestRevisionLookup,
EntityExistenceChecker $entityExistenceChecker,
EntityTitleTextLookup $entityTitleTextLookup,
EntityUrlLookup $entityUrlLookup,
EntityRedirectChecker $entityRedirectChecker,
LanguageFactory $languageFactory,
EntityTitleLookup $entityTitleLookup = null,
CachingKartographerEmbeddingHandler $kartographerEmbeddingHandler = null,
bool $useKartographerMaplinkInWikitext = false,
array $thumbLimits = []
) {
$this->labelDescriptionLookupFactory = $labelDescriptionLookupFactory;
$this->languageNameLookupFactory = $languageNameLookupFactory;
$this->itemUriParser = $itemUriParser;
$this->geoShapeStorageBaseUrl = $geoShapeStorageBaseUrl;
$this->tabularDataStorageBaseUrl = $tabularDataStorageBaseUrl;
$this->entityTitleLookup = $entityTitleLookup;
$this->redirectResolvingLatestRevisionLookup = $redirectResolvingLatestRevisionLookup;
$this->entityLookup = $entityLookup;
$this->cache = $termFallbackCacheFacade;
$this->snakFormat = new SnakFormat();
$this->kartographerEmbeddingHandler = $kartographerEmbeddingHandler;
$this->useKartographerMaplinkInWikitext = $useKartographerMaplinkInWikitext;
$this->thumbLimits = $thumbLimits;
$this->entityExistenceChecker = $entityExistenceChecker;
$this->entityTitleTextLookup = $entityTitleTextLookup;
$this->entityUrlLookup = $entityUrlLookup;
$this->entityRedirectChecker = $entityRedirectChecker;
$this->languageFactory = $languageFactory;
}
private function newPlainEntityIdFormatter( FormatterOptions $options ) {
$labelDescriptionLookup = $this->labelDescriptionLookupFactory->getLabelDescriptionLookup( $options );
return new EntityIdValueFormatter(
new EntityIdLabelFormatter( $labelDescriptionLookup )
);
}
/**
* @param string $format The desired target format, see SnakFormatter::FORMAT_XXX
*
* @throws InvalidArgumentException
* @return bool True if $format is one of the SnakFormatter::FORMAT_HTML_XXX formats.
*/
private function isHtmlFormat( $format ) {
return $this->snakFormat->getBaseFormat( $format ) === SnakFormatter::FORMAT_HTML;
}
/**
* Wraps the given formatter in an EscapingValueFormatter if necessary.
*
* @param string $format The desired target format, see SnakFormatter::FORMAT_XXX
* @param ValueFormatter $formatter The plain text formatter to wrap.
*
* @return ValueFormatter
*/
private function escapeValueFormatter( $format, ValueFormatter $formatter ) {
switch ( $this->snakFormat->getBaseFormat( $format ) ) {
case SnakFormatter::FORMAT_HTML:
return new EscapingValueFormatter( $formatter, static function ( string $string ): string {
return htmlspecialchars( $string, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5 );
} );
case SnakFormatter::FORMAT_WIKI:
return new EscapingValueFormatter( $formatter, 'wfEscapeWikiText' );
default:
return $formatter;
}
}
/**
* @param string $format The desired target format, see SnakFormatter::FORMAT_XXX
* @param FormatterOptions $options
*
* @return ValueFormatter
*/
public function newEntityIdFormatter( $format, FormatterOptions $options ) {
if ( $this->isHtmlFormat( $format ) && $this->entityTitleLookup ) {
return new EntityIdValueFormatter(
$this->newLabelsProviderEntityIdHtmlLinkFormatter( $options )
);
} elseif ( $format === SnakFormatter::FORMAT_WIKI && $this->entityTitleLookup ) {
return new EntityIdValueFormatter(
new EntityIdSiteLinkFormatter(
$this->entityTitleLookup,
$this->labelDescriptionLookupFactory->getLabelDescriptionLookup( $options )
)
);
}
$plainFormatter = $this->newPlainEntityIdFormatter( $options );
return $this->escapeValueFormatter( $format, $plainFormatter );
}
public function newPropertyIdHtmlLinkFormatter( FormatterOptions $options ) {
return new ItemPropertyIdHtmlLinkFormatter(
$this->getLabelDescriptionLookup( $options ),
$this->entityTitleLookup,
$this->newLanguageNameLookup( $options ),
new NonExistingEntityIdHtmlBrokenLinkFormatter(
'wikibase-deletedentity-',
$this->entityTitleTextLookup,
$this->entityUrlLookup
)
);
}
public function newItemIdHtmlLinkFormatter( FormatterOptions $options ) {
return new ItemPropertyIdHtmlLinkFormatter(
$this->getLabelDescriptionLookup( $options ),
$this->entityTitleLookup,
$this->newLanguageNameLookup( $options ),
new NonExistingEntityIdHtmlFormatter( 'wikibase-deletedentity-' )
);
}
private function getNonCachingLookup( FormatterOptions $options ) {
return new LanguageFallbackLabelDescriptionLookup(
new EntityRetrievingTermLookup( $this->entityLookup ),
$options->getOption( FormatterLabelDescriptionLookupFactory::OPT_LANGUAGE_FALLBACK_CHAIN )
);
}
private function getLabelDescriptionLookup( FormatterOptions $options ) {
return new CachingFallbackLabelDescriptionLookup(
$this->cache,
$this->redirectResolvingLatestRevisionLookup,
$this->getNonCachingLookup( $options ),
$options->getOption( FormatterLabelDescriptionLookupFactory::OPT_LANGUAGE_FALLBACK_CHAIN )
);
}
/**
* @param string $format The desired target format, see SnakFormatter::FORMAT_XXX
*
* @return ValueFormatter
*/
public function newStringFormatter( $format ) {
return $this->escapeValueFormatter( $format, new StringFormatter() );
}
/**
* @param string $format The desired target format, see SnakFormatter::FORMAT_XXX
* @param FormatterOptions $options
*
* @return ValueFormatter
*/
public function newUnDeserializableValueFormatter( $format, FormatterOptions $options ) {
return $this->escapeValueFormatter( $format, new UnDeserializableValueFormatter( $options ) );
}
/**
* @param string $format The desired target format, see SnakFormatter::FORMAT_XXX
* @param FormatterOptions $options
*
* @return ValueFormatter
*/
public function newUrlFormatter( $format, FormatterOptions $options ) {
switch ( $this->snakFormat->getBaseFormat( $format ) ) {
case SnakFormatter::FORMAT_HTML:
return new HtmlUrlFormatter( $options );
case SnakFormatter::FORMAT_WIKI:
// Use the string formatter without escaping!
return new StringFormatter();
default:
return $this->newStringFormatter( $format );
}
}
/**
* @param string $format The desired target format, see SnakFormatter::FORMAT_XXX
* @param FormatterOptions $options
*
* @return ValueFormatter
*/
public function newCommonsMediaFormatter( $format, FormatterOptions $options ) {
if ( $this->snakFormat->isPossibleFormat( SnakFormatter::FORMAT_HTML_VERBOSE, $format ) ) {
return new CommonsInlineImageFormatter(
ParserOptions::newFromContext( RequestContext::getMain() ),
$this->thumbLimits,
$this->languageFactory,
$options
);
}
switch ( $this->snakFormat->getBaseFormat( $format ) ) {
case SnakFormatter::FORMAT_HTML:
return new CommonsLinkFormatter( $options );
case SnakFormatter::FORMAT_WIKI:
return new CommonsThumbnailFormatter();
default:
return $this->newStringFormatter( $format );
}
}
/**
* @param string $format The desired target format, see SnakFormatter::FORMAT_XXX
*
* @return ValueFormatter
*/
public function newGeoShapeFormatter( $format ) {
switch ( $this->snakFormat->getBaseFormat( $format ) ) {
case SnakFormatter::FORMAT_HTML:
return new InterWikiLinkHtmlFormatter( $this->geoShapeStorageBaseUrl );
case SnakFormatter::FORMAT_WIKI:
return new InterWikiLinkWikitextFormatter( $this->geoShapeStorageBaseUrl );
default:
return $this->newStringFormatter( $format );
}
}
/**
* @param string $format The desired target format, see SnakFormatter::FORMAT_XXX
* @param FormatterOptions $options
*
* @return ValueFormatter
*/
public function newTabularDataFormatter( $format, FormatterOptions $options ) {
switch ( $this->snakFormat->getBaseFormat( $format ) ) {
case SnakFormatter::FORMAT_HTML:
return new InterWikiLinkHtmlFormatter( $this->tabularDataStorageBaseUrl );
case SnakFormatter::FORMAT_WIKI:
return new InterWikiLinkWikitextFormatter( $this->tabularDataStorageBaseUrl );
default:
return $this->newStringFormatter( $format );
}
}
/**
* @param string $format The desired target format, see SnakFormatter::FORMAT_XXX
* @param FormatterOptions $options
*
* @return ValueFormatter
*/
public function newTimeFormatter( $format, FormatterOptions $options ) {
if ( $this->snakFormat->isPossibleFormat( SnakFormatter::FORMAT_HTML_DIFF, $format ) ) {
return new TimeDetailsFormatter(
$options,
new HtmlTimeFormatter(
$options,
new MwTimeIsoFormatter( $this->languageFactory, $options ),
new ShowCalendarModelDecider()
)
);
} elseif ( $this->isHtmlFormat( $format ) ) {
return new HtmlTimeFormatter(
$options,
new MwTimeIsoFormatter( $this->languageFactory, $options ),
new ShowCalendarModelDecider()
);
} else {
return $this->escapeValueFormatter(
$format,
new PlaintextTimeFormatter(
$options,
new MwTimeIsoFormatter( $this->languageFactory, $options ),
new ShowCalendarModelDecider()
)
);
}
}
/**
* @param FormatterOptions $options
*
* @return MediaWikiNumberLocalizer
*/
private function getNumberLocalizer( FormatterOptions $options ) {
$language = $this->languageFactory->getLanguage( $options->getOption( ValueFormatter::OPT_LANG ) );
return new MediaWikiNumberLocalizer( $language );
}
/**
* @param FormatterOptions $options
*
* @return VocabularyUriFormatter
*/
private function getVocabularyUriFormatter( FormatterOptions $options ) {
$labelLookup = $this->labelDescriptionLookupFactory->getLabelDescriptionLookup( $options );
return new VocabularyUriFormatter( $this->itemUriParser, $labelLookup, $this->unitOneUris );
}
/**
* @param string $format The desired target format, see SnakFormatter::FORMAT_XXX
* @param FormatterOptions $options
*
* @return ValueFormatter
*/
public function newQuantityFormatter( $format, FormatterOptions $options ) {
$vocabularyUriFormatter = $this->getVocabularyUriFormatter( $options );
if ( $this->snakFormat->isPossibleFormat( SnakFormatter::FORMAT_HTML_DIFF, $format ) ) {
$localizer = $this->getNumberLocalizer( $options );
return new QuantityDetailsFormatter( $localizer, $vocabularyUriFormatter, $options );
} elseif ( $this->isHtmlFormat( $format ) ) {
$decimalFormatter = new DecimalFormatter( $options, $this->getNumberLocalizer( $options ) );
return new QuantityHtmlFormatter( $options, $decimalFormatter, $vocabularyUriFormatter );
} else {
$decimalFormatter = new DecimalFormatter( $options, $this->getNumberLocalizer( $options ) );
$plainFormatter = new QuantityFormatter( $options, $decimalFormatter, $vocabularyUriFormatter );
return $this->escapeValueFormatter( $format, $plainFormatter );
}
}
/**
* @param string $format The desired target format, see SnakFormatter::FORMAT_XXX
* @param FormatterOptions $options
*
* @return ValueFormatter
*/
public function newGlobeCoordinateFormatter( $format, FormatterOptions $options ) {
$isHtmlVerboseFormat = $this->snakFormat->isPossibleFormat( SnakFormatter::FORMAT_HTML_VERBOSE, $format );
if ( $isHtmlVerboseFormat && $this->kartographerEmbeddingHandler ) {
$isPreview = $format === SnakFormatter::FORMAT_HTML_VERBOSE_PREVIEW;
return new GlobeCoordinateKartographerFormatter(
$options,
$this->newGlobeCoordinateFormatter( SnakFormatter::FORMAT_HTML, $options ),
$this->kartographerEmbeddingHandler,
$this->languageFactory,
$isPreview
);
}
if ( $this->snakFormat->isPossibleFormat( SnakFormatter::FORMAT_HTML_DIFF, $format ) ) {
return new GlobeCoordinateDetailsFormatter(
$this->getVocabularyUriFormatter( $options ),
$options
);
}
$options->setOption( LatLongFormatter::OPT_FORMAT, LatLongFormatter::TYPE_DMS );
$options->setOption( LatLongFormatter::OPT_SPACING_LEVEL, [
LatLongFormatter::OPT_SPACE_LATLONG,
] );
$options->setOption( LatLongFormatter::OPT_DIRECTIONAL, true );
$plainFormatter = new GlobeCoordinateFormatter( $options );
if ( $format === SnakFormatter::FORMAT_WIKI && $this->useKartographerMaplinkInWikitext ) {
return new GlobeCoordinateInlineWikitextKartographerFormatter( $plainFormatter );
} else {
return $this->escapeValueFormatter( $format, $plainFormatter );
}
}
/**
* @param string $format The desired target format, see SnakFormatter::FORMAT_XXX
* @param FormatterOptions $options
*
* @return MonolingualHtmlFormatter|MonolingualWikitextFormatter|MonolingualTextFormatter
*/
public function newMonolingualFormatter( $format, FormatterOptions $options ) {
switch ( $this->snakFormat->getBaseFormat( $format ) ) {
case SnakFormatter::FORMAT_HTML:
return new MonolingualHtmlFormatter(
$this->newLanguageNameLookup( $options )
);
case SnakFormatter::FORMAT_WIKI:
return new MonolingualWikitextFormatter();
default:
return new MonolingualTextFormatter();
}
}
public function newLabelsProviderEntityIdHtmlLinkFormatter( FormatterOptions $options ) {
$lookup = $this->labelDescriptionLookupFactory->getLabelDescriptionLookup( $options );
return new LabelsProviderEntityIdHtmlLinkFormatter(
$lookup,
$this->newLanguageNameLookup( $options ),
$this->entityExistenceChecker,
$this->entityTitleTextLookup,
$this->entityUrlLookup,
$this->entityRedirectChecker
);
}
private function newLanguageNameLookup( FormatterOptions $options ): LanguageNameLookup {
if ( $options->hasOption( ValueFormatter::OPT_LANG ) ) {
return $this->languageNameLookupFactory->getForLanguageCode(
$options->getOption( ValueFormatter::OPT_LANG )
);
} elseif ( $options->hasOption( FormatterLabelDescriptionLookupFactory::OPT_LANGUAGE_FALLBACK_CHAIN ) ) {
/** @var TermLanguageFallbackChain $chain */
$chain = $options->getOption( FormatterLabelDescriptionLookupFactory::OPT_LANGUAGE_FALLBACK_CHAIN );
'@phan-var TermLanguageFallbackChain $chain';
return $this->languageNameLookupFactory->getForLanguageCode(
$chain->getFallbackChain()[0]->getLanguageCode()
);
} else {
throw new InvalidArgumentException( 'FormatterOptions must have OPT_LANG or OPT_LANGUAGE_FALLBACK_CHAIN' );
}
}
}