diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/widget |
first commit
Diffstat (limited to 'www/wiki/includes/widget')
23 files changed, 2230 insertions, 0 deletions
diff --git a/www/wiki/includes/widget/AUTHORS.txt b/www/wiki/includes/widget/AUTHORS.txt new file mode 100644 index 00000000..2490b9d8 --- /dev/null +++ b/www/wiki/includes/widget/AUTHORS.txt @@ -0,0 +1,13 @@ +Authors (alphabetically) + +Alex Monk <krenair@wikimedia.org> +Bartosz DziewoĆski <bdziewonski@wikimedia.org> +Brad Jorsch <bjorsch@wikimedia.org> +Ed Sanders <esanders@wikimedia.org> +Florian Schmidt <florian.schmidt.welzow@t-online.de> +Geoffrey Mon <geofbot@gmail.com> +James D. Forrester <jforrester@wikimedia.org> +Roan Kattouw <roan@wikimedia.org> +Sucheta Ghoshal <sghoshal@wikimedia.org> +Timo Tijhof <krinklemail@gmail.com> +Trevor Parscal <trevor@wikimedia.org> diff --git a/www/wiki/includes/widget/ComplexNamespaceInputWidget.php b/www/wiki/includes/widget/ComplexNamespaceInputWidget.php new file mode 100644 index 00000000..5f5d1cd1 --- /dev/null +++ b/www/wiki/includes/widget/ComplexNamespaceInputWidget.php @@ -0,0 +1,117 @@ +<?php + +namespace MediaWiki\Widget; + +/** + * Namespace input widget. Displays a dropdown box with the choice of available namespaces, plus two + * checkboxes to include associated namespace or to invert selection. + * + * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license MIT + */ +class ComplexNamespaceInputWidget extends \OOUI\Widget { + + protected $config; + protected $namespace; + protected $associated = null; + protected $associatedLabel = null; + protected $invert = null; + protected $invertLabel = null; + + /** + * @param array $config Configuration options + * - array $config['namespace'] Configuration for the NamespaceInputWidget + * dropdown with list of namespaces + * - string $config['namespace']['includeAllValue'] If specified, + * add an "all namespaces" option to the dropdown, and use this as the input value for it + * - array|null $config['invert'] Configuration for the "invert selection" + * CheckboxInputWidget. If null, the checkbox will not be generated. + * - array|null $config['associated'] Configuration for the "include associated namespace" + * CheckboxInputWidget. If null, the checkbox will not be generated. + * - array $config['invertLabel'] Configuration for the FieldLayout with label + * wrapping the "invert selection" checkbox + * - string $config['invertLabel']['label'] Label text for the label + * - array $config['associatedLabel'] Configuration for the FieldLayout with label + * wrapping the "include associated namespace" checkbox + * - string $config['associatedLabel']['label'] Label text for the label + */ + public function __construct( array $config = [] ) { + // Configuration initialization + $config = array_merge( + [ + // Config options for nested widgets + 'namespace' => [], + 'invert' => [], + 'invertLabel' => [], + 'associated' => [], + 'associatedLabel' => [], + ], + $config + ); + + parent::__construct( $config ); + + // Properties + $this->config = $config; + + $this->namespace = new NamespaceInputWidget( $config['namespace'] ); + if ( $config['associated'] !== null ) { + $this->associated = new \OOUI\CheckboxInputWidget( array_merge( + [ 'value' => '1' ], + $config['associated'] + ) ); + // TODO Should use a LabelWidget? But they don't work like HTML <label>s yet + $this->associatedLabel = new \OOUI\FieldLayout( + $this->associated, + array_merge( + [ 'align' => 'inline' ], + $config['associatedLabel'] + ) + ); + } + if ( $config['invert'] !== null ) { + $this->invert = new \OOUI\CheckboxInputWidget( array_merge( + [ 'value' => '1' ], + $config['invert'] + ) ); + // TODO Should use a LabelWidget? But they don't work like HTML <label>s yet + $this->invertLabel = new \OOUI\FieldLayout( + $this->invert, + array_merge( + [ 'align' => 'inline' ], + $config['invertLabel'] + ) + ); + } + + // Initialization + $this + ->addClasses( [ 'mw-widget-complexNamespaceInputWidget' ] ) + ->appendContent( $this->namespace, $this->associatedLabel, $this->invertLabel ); + } + + protected function getJavaScriptClassName() { + return 'mw.widgets.ComplexNamespaceInputWidget'; + } + + public function getConfig( &$config ) { + $config = array_merge( + $config, + array_intersect_key( + $this->config, + array_fill_keys( + [ + 'namespace', + 'invert', + 'invertLabel', + 'associated', + 'associatedLabel' + ], + true + ) + ) + ); + $config['namespace']['dropdown']['$overlay'] = true; + return parent::getConfig( $config ); + } +} diff --git a/www/wiki/includes/widget/ComplexTitleInputWidget.php b/www/wiki/includes/widget/ComplexTitleInputWidget.php new file mode 100644 index 00000000..ca6c8484 --- /dev/null +++ b/www/wiki/includes/widget/ComplexTitleInputWidget.php @@ -0,0 +1,66 @@ +<?php + +namespace MediaWiki\Widget; + +/** + * Complex title input widget. + * + * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license MIT + */ +class ComplexTitleInputWidget extends \OOUI\Widget { + + protected $namespace = null; + protected $title = null; + + /** + * Like TitleInputWidget, but the namespace has to be input through a separate dropdown field. + * + * @param array $config Configuration options + * - array $config['namespace'] Configuration for the NamespaceInputWidget dropdown + * with list of namespaces + * - array $config['title'] Configuration for the TitleInputWidget text field + */ + public function __construct( array $config = [] ) { + // Configuration initialization + $config = array_merge( + [ + 'namespace' => [], + 'title' => [], + ], + $config + ); + + parent::__construct( $config ); + + // Properties + $this->config = $config; + $this->namespace = new NamespaceInputWidget( $config['namespace'] ); + $this->title = new TitleInputWidget( array_merge( + $config['title'], + [ + 'relative' => true, + 'namespace' => isset( $config['namespace']['value'] ) ? + $config['namespace']['value'] : + null, + ] + ) ); + + // Initialization + $this + ->addClasses( [ 'mw-widget-complexTitleInputWidget' ] ) + ->appendContent( $this->namespace, $this->title ); + } + + protected function getJavaScriptClassName() { + return 'mw.widgets.ComplexTitleInputWidget'; + } + + public function getConfig( &$config ) { + $config['namespace'] = $this->config['namespace']; + $config['namespace']['dropdown']['$overlay'] = true; + $config['title'] = $this->config['title']; + $config['title']['$overlay'] = true; + return parent::getConfig( $config ); + } +} diff --git a/www/wiki/includes/widget/DateInputWidget.php b/www/wiki/includes/widget/DateInputWidget.php new file mode 100644 index 00000000..975f8e9c --- /dev/null +++ b/www/wiki/includes/widget/DateInputWidget.php @@ -0,0 +1,160 @@ +<?php + +namespace MediaWiki\Widget; + +use DateTime; + +/** + * Date input widget. + * + * @since 1.29 + * @copyright 2016 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license MIT + */ +class DateInputWidget extends \OOUI\TextInputWidget { + + protected $inputFormat = null; + protected $displayFormat = null; + protected $longDisplayFormat = null; + protected $placeholderLabel = null; + protected $placeholderDateFormat = null; + protected $precision = null; + protected $mustBeAfter = null; + protected $mustBeBefore = null; + + /** + * @param array $config Configuration options + * - string $config['inputFormat'] Date format string to use for the textual input field. + * Displayed while the widget is active, and the user can type in a date in this format. + * Should be short and easy to type. (default: 'YYYY-MM-DD' or 'YYYY-MM', depending on + * `precision`) + * - string $config['displayFormat'] Date format string to use for the clickable label. + * while the widget is inactive. Should be as unambiguous as possible (for example, prefer + * to spell out the month, rather than rely on the order), even if that makes it longer. + * Applicable only if the widget is infused. (default: language-specific) + * - string $config['longDisplayFormat'] If a custom displayFormat is not specified, use + * unabbreviated day of the week and month names in the default language-specific + * displayFormat. (default: false) + * - string $config['placeholderLabel'] Placeholder text shown when the widget is not + * selected. Applicable only if the widget is infused. (default: taken from message + * `mw-widgets-dateinput-no-date`) + * - string $config['placeholderDateFormat'] User-visible date format string displayed + * in the textual input field when it's empty. Should be the same as `inputFormat`, but + * translated to the user's language. (default: 'YYYY-MM-DD' or 'YYYY-MM', depending on + * `precision`) + * - string $config['precision'] Date precision to use, 'day' or 'month' (default: 'day') + * - string $config['mustBeAfter'] Validates the date to be after this. + * In the 'YYYY-MM-DD' or 'YYYY-MM' format, depending on `precision`. + * - string $config['mustBeBefore'] Validates the date to be before this. + * In the 'YYYY-MM-DD' or 'YYYY-MM' format, depending on `precision`. + */ + public function __construct( array $config = [] ) { + $config = array_merge( [ + // Default config values + 'precision' => 'day', + 'longDisplayFormat' => false, + ], $config ); + + // Properties + if ( isset( $config['inputFormat'] ) ) { + $this->inputFormat = $config['inputFormat']; + } + if ( isset( $config['placeholderDateFormat'] ) ) { + $this->placeholderDateFormat = $config['placeholderDateFormat']; + } + $this->precision = $config['precision']; + if ( isset( $config['mustBeAfter'] ) ) { + $this->mustBeAfter = $config['mustBeAfter']; + } + if ( isset( $config['mustBeBefore'] ) ) { + $this->mustBeBefore = $config['mustBeBefore']; + } + + // Properties stored for the infused JS widget + if ( isset( $config['displayFormat'] ) ) { + $this->displayFormat = $config['displayFormat']; + } + if ( isset( $config['longDisplayFormat'] ) ) { + $this->longDisplayFormat = $config['longDisplayFormat']; + } + if ( isset( $config['placeholderLabel'] ) ) { + $this->placeholderLabel = $config['placeholderLabel']; + } + + // Set up placeholder text visible if the browser doesn't override it (logic taken from JS) + if ( $this->placeholderDateFormat !== null ) { + $placeholder = $this->placeholderDateFormat; + } elseif ( $this->inputFormat !== null ) { + // We have no way to display a translated placeholder for custom formats + $placeholder = ''; + } else { + $placeholder = wfMessage( "mw-widgets-dateinput-placeholder-$this->precision" )->text(); + } + + $config = array_merge( [ + // Processed config values + 'placeholder' => $placeholder, + ], $config ); + + parent::__construct( $config ); + + // Calculate min/max attributes (which are skipped by TextInputWidget) and add to <input> + // min/max attributes are inclusive, but mustBeAfter/Before are exclusive + if ( $this->mustBeAfter !== null ) { + $min = new DateTime( $this->mustBeAfter ); + $min = $min->modify( '+1 day' ); + $min = $min->format( 'Y-m-d' ); + $this->input->setAttributes( [ 'min' => $min ] ); + } + if ( $this->mustBeBefore !== null ) { + $max = new DateTime( $this->mustBeBefore ); + $max = $max->modify( '-1 day' ); + $max = $max->format( 'Y-m-d' ); + $this->input->setAttributes( [ 'max' => $max ] ); + } + + // Initialization + $this->addClasses( [ 'mw-widget-dateInputWidget' ] ); + } + + protected function getJavaScriptClassName() { + return 'mw.widgets.DateInputWidget'; + } + + public function getConfig( &$config ) { + if ( $this->inputFormat !== null ) { + $config['inputFormat'] = $this->inputFormat; + } + if ( $this->displayFormat !== null ) { + $config['displayFormat'] = $this->displayFormat; + } + if ( $this->longDisplayFormat !== null ) { + $config['longDisplayFormat'] = $this->longDisplayFormat; + } + if ( $this->placeholderLabel !== null ) { + $config['placeholderLabel'] = $this->placeholderLabel; + } + if ( $this->placeholderDateFormat !== null ) { + $config['placeholderDateFormat'] = $this->placeholderDateFormat; + } + if ( $this->precision !== null ) { + $config['precision'] = $this->precision; + } + if ( $this->mustBeAfter !== null ) { + $config['mustBeAfter'] = $this->mustBeAfter; + } + if ( $this->mustBeBefore !== null ) { + $config['mustBeBefore'] = $this->mustBeBefore; + } + $config['$overlay'] = true; + return parent::getConfig( $config ); + } + + public function getInputElement( $config ) { + // Inserts date/month type attribute + return parent::getInputElement( $config ) + ->setAttributes( [ + 'type' => ( $config['precision'] === 'month' ) ? 'month' : 'date' + ] ); + } +} diff --git a/www/wiki/includes/widget/DateTimeInputWidget.php b/www/wiki/includes/widget/DateTimeInputWidget.php new file mode 100644 index 00000000..21e3d793 --- /dev/null +++ b/www/wiki/includes/widget/DateTimeInputWidget.php @@ -0,0 +1,73 @@ +<?php + +namespace MediaWiki\Widget; + +use OOUI\Tag; + +/** + * Date-time input widget. + * + * @copyright 2016 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license MIT + */ +class DateTimeInputWidget extends \OOUI\InputWidget { + + protected $type = null; + protected $min = null; + protected $max = null; + protected $clearable = null; + + /** + * @param array $config Configuration options + * - string $config['type'] 'date', 'time', or 'datetime' + * - string $config['min'] Minimum date, time, or datetime + * - string $config['max'] Maximum date, time, or datetime + * - bool $config['clearable'] Whether to provide for blanking the value. + */ + public function __construct( array $config = [] ) { + // We need $this->type set before calling the parent constructor + if ( isset( $config['type'] ) ) { + $this->type = $config['type']; + } else { + throw new \InvalidArgumentException( '$config[\'type\'] must be specified' ); + } + + parent::__construct( $config ); + + // Properties, which are ignored in PHP and just shipped back to JS + if ( isset( $config['min'] ) ) { + $this->min = $config['min']; + } + if ( isset( $config['max'] ) ) { + $this->max = $config['max']; + } + if ( isset( $config['clearable'] ) ) { + $this->clearable = $config['clearable']; + } + + // Initialization + $this->addClasses( [ 'mw-widgets-datetime-dateTimeInputWidget' ] ); + } + + protected function getJavaScriptClassName() { + return 'mw.widgets.datetime.DateTimeInputWidget'; + } + + public function getConfig( &$config ) { + $config['type'] = $this->type; + if ( $this->min !== null ) { + $config['min'] = $this->min; + } + if ( $this->max !== null ) { + $config['max'] = $this->max; + } + if ( $this->clearable !== null ) { + $config['clearable'] = $this->clearable; + } + return parent::getConfig( $config ); + } + + protected function getInputElement( $config ) { + return ( new Tag( 'input' ) )->setAttributes( [ 'type' => $this->type ] ); + } +} diff --git a/www/wiki/includes/widget/LICENSE.txt b/www/wiki/includes/widget/LICENSE.txt new file mode 100644 index 00000000..b03ca801 --- /dev/null +++ b/www/wiki/includes/widget/LICENSE.txt @@ -0,0 +1,25 @@ +Copyright (c) 2011-2015 MediaWiki Widgets Team and others under the +terms of The MIT License (MIT), as follows: + +This software consists of voluntary contributions made by many +individuals (AUTHORS.txt) For exact contribution history, see the +revision history and logs, available at https://gerrit.wikimedia.org + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/www/wiki/includes/widget/NamespaceInputWidget.php b/www/wiki/includes/widget/NamespaceInputWidget.php new file mode 100644 index 00000000..0840886a --- /dev/null +++ b/www/wiki/includes/widget/NamespaceInputWidget.php @@ -0,0 +1,64 @@ +<?php + +namespace MediaWiki\Widget; + +/** + * Namespace input widget. Displays a dropdown box with the choice of available namespaces. + * + * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license MIT + */ +class NamespaceInputWidget extends \OOUI\DropdownInputWidget { + + protected $includeAllValue = null; + + /** + * @param array $config Configuration options + * - string $config['includeAllValue'] If specified, add a "all namespaces" option to the + * namespace dropdown, and use this as the input value for it + * - int[] $config['exclude'] List of namespace numbers to exclude from the selector + */ + public function __construct( array $config = [] ) { + // Configuration initialization + $config['options'] = $this->getNamespaceDropdownOptions( $config ); + + parent::__construct( $config ); + + // Properties + $this->includeAllValue = isset( $config['includeAllValue'] ) ? $config['includeAllValue'] : null; + $this->exclude = isset( $config['exclude'] ) ? $config['exclude'] : []; + + // Initialization + $this->addClasses( [ 'mw-widget-namespaceInputWidget' ] ); + } + + protected function getNamespaceDropdownOptions( array $config ) { + $namespaceOptionsParams = [ + 'all' => isset( $config['includeAllValue'] ) ? $config['includeAllValue'] : null, + 'exclude' => isset( $config['exclude'] ) ? $config['exclude'] : null + ]; + $namespaceOptions = \Html::namespaceSelectorOptions( $namespaceOptionsParams ); + + $options = []; + foreach ( $namespaceOptions as $id => $name ) { + $options[] = [ + 'data' => (string)$id, + 'label' => $name, + ]; + } + + return $options; + } + + protected function getJavaScriptClassName() { + return 'mw.widgets.NamespaceInputWidget'; + } + + public function getConfig( &$config ) { + $config['includeAllValue'] = $this->includeAllValue; + $config['exclude'] = $this->exclude; + // Skip DropdownInputWidget's getConfig(), we don't need 'options' config + $config['dropdown']['$overlay'] = true; + return \OOUI\InputWidget::getConfig( $config ); + } +} diff --git a/www/wiki/includes/widget/SearchInputWidget.php b/www/wiki/includes/widget/SearchInputWidget.php new file mode 100644 index 00000000..6fed7942 --- /dev/null +++ b/www/wiki/includes/widget/SearchInputWidget.php @@ -0,0 +1,72 @@ +<?php + +namespace MediaWiki\Widget; + +/** + * Search input widget. + * + * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license MIT + */ +class SearchInputWidget extends TitleInputWidget { + + protected $pushPending = false; + protected $performSearchOnClick = true; + protected $validateTitle = false; + protected $highlightFirst = false; + protected $dataLocation = 'header'; + + /** + * @param array $config Configuration options + * - int|null $config['pushPending'] Whether the input should be visually marked as + * "pending", while requesting suggestions (default: false) + * - bool|null $config['performSearchOnClick'] If true, the script will start a search + * whenever a user hits a suggestion. If false, the text of the suggestion is inserted into + * the text field only (default: true) + * - string $config['dataLocation'] Where the search input field will be + * used (header or content, default: header) + */ + public function __construct( array $config = [] ) { + $config = array_merge( [ + 'maxLength' => null, + 'icon' => 'search', + ], $config ); + + parent::__construct( $config ); + + // Properties, which are ignored in PHP and just shipped back to JS + if ( isset( $config['pushPending'] ) ) { + $this->pushPending = $config['pushPending']; + } + + if ( isset( $config['performSearchOnClick'] ) ) { + $this->performSearchOnClick = $config['performSearchOnClick']; + } + + if ( isset( $config['dataLocation'] ) ) { + // identifies the location of the search bar for tracking purposes + $this->dataLocation = $config['dataLocation']; + } + + // Initialization + $this->addClasses( [ 'mw-widget-searchInputWidget' ] ); + } + + protected function getInputElement( $config ) { + return ( new \OOUI\Tag( 'input' ) )->setAttributes( [ 'type' => 'search' ] ); + } + + protected function getJavaScriptClassName() { + return 'mw.widgets.SearchInputWidget'; + } + + public function getConfig( &$config ) { + $config['pushPending'] = $this->pushPending; + $config['performSearchOnClick'] = $this->performSearchOnClick; + if ( $this->dataLocation ) { + $config['dataLocation'] = $this->dataLocation; + } + $config['$overlay'] = true; + return parent::getConfig( $config ); + } +} diff --git a/www/wiki/includes/widget/SelectWithInputWidget.php b/www/wiki/includes/widget/SelectWithInputWidget.php new file mode 100644 index 00000000..5ceed4c4 --- /dev/null +++ b/www/wiki/includes/widget/SelectWithInputWidget.php @@ -0,0 +1,63 @@ +<?php + +namespace MediaWiki\Widget; + +use OOUI\DropdownInputWidget; +use OOUI\TextInputWidget; + +/** + * Select and input widget. + * + * @copyright 2011-2017 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license MIT + */ +class SelectWithInputWidget extends \OOUI\Widget { + + protected $textinput = null; + protected $dropdowninput = null; + + /** + * A version of the SelectWithInputWidget, with `or` set to true. + * + * @param array $config Configuration options + * - array $config['textinput'] Configuration for the TextInputWidget + * - array $config['dropdowninput'] Configuration for the DropdownInputWidget + * - bool $config['or'] Configuration for whether the widget is dropdown AND input + * or dropdown OR input + */ + public function __construct( array $config = [] ) { + // Configuration initialization + $config = array_merge( + [ + 'textinput' => [], + 'dropdowninput' => [], + 'or' => false + ], + $config + ); + + parent::__construct( $config ); + + // Properties + $this->config = $config; + $this->textinput = new TextInputWidget( $config['textinput'] ); + $this->dropdowninput = new DropdownInputWidget( $config['dropdowninput'] ); + + // Initialization + $this + ->addClasses( [ 'mw-widget-selectWithInputWidget' ] ) + ->appendContent( $this->dropdowninput, $this->textinput ); + } + + protected function getJavaScriptClassName() { + return 'mw.widgets.SelectWithInputWidget'; + } + + public function getConfig( &$config ) { + $config['textinput'] = $this->config['textinput']; + $config['dropdowninput'] = $this->config['dropdowninput']; + $config['dropdowninput']['dropdown']['$overlay'] = true; + $config['or'] = $this->config['or']; + return parent::getConfig( $config ); + } +} diff --git a/www/wiki/includes/widget/SizeFilterWidget.php b/www/wiki/includes/widget/SizeFilterWidget.php new file mode 100644 index 00000000..c4d1dfc8 --- /dev/null +++ b/www/wiki/includes/widget/SizeFilterWidget.php @@ -0,0 +1,75 @@ +<?php + +namespace MediaWiki\Widget; + +use \OOUI\RadioSelectInputWidget; +use \OOUI\TextInputWidget; +use \OOUI\LabelWidget; + +/** + * Select and input widget. + * + * @copyright 2011-2018 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ +class SizeFilterWidget extends \OOUI\Widget { + + protected $radioselectinput = null; + protected $textinput = null; + + /** + * RadioSelectInputWidget and a TextInputWidget to set minimum or maximum byte size + * + * @param array $config Configuration options + * - array $config['textinput'] Configuration for the TextInputWidget + * - array $config['radioselectinput'] Configuration for the RadioSelectWidget + * - bool $congif['selectMin'] Whether to select 'min', false would select 'max' + */ + public function __construct( array $config = [] ) { + // Configuration initialization + $config = array_merge( [ + 'selectMin' => true, + 'textinput' => [], + 'radioselectinput' => [] + ], $config ); + $config['textinput'] = array_merge( [ + 'type' => 'number' + ], $config['textinput'] ); + $config['radioselectinput'] = array_merge( [ 'options' => [ + [ + 'data' => 'min', + 'label' => wfMessage( 'minimum-size' )->text() + ], + [ + 'data' => 'max', + 'label' => wfMessage( 'maximum-size' )->text() + ] + ] ], $config['radioselectinput'] ); + + // Parent constructor + parent::__construct( $config ); + + // Properties + $this->config = $config; + $this->radioselectinput = new RadioSelectInputWidget( $config[ 'radioselectinput'] ); + $this->textinput = new TextInputWidget( $config[ 'textinput' ] ); + $this->label = new LabelWidget( [ 'label' => wfMessage( 'pagesize' )->text() ] ); + + // Initialization + $this->radioselectinput->setValue( $config[ 'selectMin' ] ? 'min' : 'max' ); + $this + ->addClasses( [ 'mw-widget-sizeFilterWidget' ] ) + ->appendContent( $this->radioselectinput, $this->textinput, $this->label ); + } + + protected function getJavaScriptClassName() { + return 'mw.widgets.SizeFilterWidget'; + } + + public function getConfig( &$config ) { + $config['textinput'] = $this->config['textinput']; + $config['radioselectinput'] = $this->config['radioselectinput']; + $config['selectMin'] = $this->config['selectMin']; + return parent::getConfig( $config ); + } +} diff --git a/www/wiki/includes/widget/TitleInputWidget.php b/www/wiki/includes/widget/TitleInputWidget.php new file mode 100644 index 00000000..db1ea0b2 --- /dev/null +++ b/www/wiki/includes/widget/TitleInputWidget.php @@ -0,0 +1,79 @@ +<?php + +namespace MediaWiki\Widget; + +/** + * Title input widget. + * + * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license MIT + */ +class TitleInputWidget extends \OOUI\TextInputWidget { + + protected $namespace = null; + protected $relative = null; + protected $suggestions = null; + protected $highlightFirst = null; + protected $validateTitle = null; + + /** + * @param array $config Configuration options + * - int|null $config['namespace'] Namespace to prepend to queries + * - bool|null $config['relative'] If a namespace is set, + * return a title relative to it (default: true) + * - bool|null $config['suggestions'] Display search suggestions (default: true) + * - bool|null $config['highlightFirst'] Automatically highlight + * the first result (default: true) + * - bool|null $config['validateTitle'] Whether the input must + * be a valid title (default: true) + */ + public function __construct( array $config = [] ) { + parent::__construct( + array_merge( [ 'maxLength' => 255 ], $config ) + ); + + // Properties, which are ignored in PHP and just shipped back to JS + if ( isset( $config['namespace'] ) ) { + $this->namespace = $config['namespace']; + } + if ( isset( $config['relative'] ) ) { + $this->relative = $config['relative']; + } + if ( isset( $config['suggestions'] ) ) { + $this->suggestions = $config['suggestions']; + } + if ( isset( $config['highlightFirst'] ) ) { + $this->highlightFirst = $config['highlightFirst']; + } + if ( isset( $config['validateTitle'] ) ) { + $this->validateTitle = $config['validateTitle']; + } + + // Initialization + $this->addClasses( [ 'mw-widget-titleInputWidget' ] ); + } + + protected function getJavaScriptClassName() { + return 'mw.widgets.TitleInputWidget'; + } + + public function getConfig( &$config ) { + if ( $this->namespace !== null ) { + $config['namespace'] = $this->namespace; + } + if ( $this->relative !== null ) { + $config['relative'] = $this->relative; + } + if ( $this->suggestions !== null ) { + $config['suggestions'] = $this->suggestions; + } + if ( $this->highlightFirst !== null ) { + $config['highlightFirst'] = $this->highlightFirst; + } + if ( $this->validateTitle !== null ) { + $config['validateTitle'] = $this->validateTitle; + } + $config['$overlay'] = true; + return parent::getConfig( $config ); + } +} diff --git a/www/wiki/includes/widget/UserInputWidget.php b/www/wiki/includes/widget/UserInputWidget.php new file mode 100644 index 00000000..36f63c15 --- /dev/null +++ b/www/wiki/includes/widget/UserInputWidget.php @@ -0,0 +1,31 @@ +<?php + +namespace MediaWiki\Widget; + +/** + * User input widget. + * + * @copyright 2011-2015 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license MIT + */ +class UserInputWidget extends \OOUI\TextInputWidget { + + /** + * @param array $config Configuration options + */ + public function __construct( array $config = [] ) { + parent::__construct( $config ); + + // Initialization + $this->addClasses( [ 'mw-widget-userInputWidget' ] ); + } + + protected function getJavaScriptClassName() { + return 'mw.widgets.UserInputWidget'; + } + + public function getConfig( &$config ) { + $config['$overlay'] = true; + return parent::getConfig( $config ); + } +} diff --git a/www/wiki/includes/widget/UsersMultiselectWidget.php b/www/wiki/includes/widget/UsersMultiselectWidget.php new file mode 100644 index 00000000..68cdad66 --- /dev/null +++ b/www/wiki/includes/widget/UsersMultiselectWidget.php @@ -0,0 +1,66 @@ +<?php + +namespace MediaWiki\Widget; + +use OOUI\MultilineTextInputWidget; + +/** + * Widget to select multiple users. + * + * @copyright 2017 MediaWiki Widgets Team and others; see AUTHORS.txt + * @license MIT + */ +class UsersMultiselectWidget extends \OOUI\Widget { + + protected $usersArray = []; + protected $inputName = null; + protected $inputPlaceholder = null; + + /** + * @param array $config Configuration options + * - array $config['users'] Array of usernames to use as preset data + * - array $config['placeholder'] Placeholder message for input + * - array $config['name'] Name attribute (used in forms) + */ + public function __construct( array $config = [] ) { + parent::__construct( $config ); + + // Properties + if ( isset( $config['default'] ) ) { + $this->usersArray = $config['default']; + } + if ( isset( $config['name'] ) ) { + $this->inputName = $config['name']; + } + if ( isset( $config['placeholder'] ) ) { + $this->inputPlaceholder = $config['placeholder']; + } + + $textarea = new MultilineTextInputWidget( [ + 'name' => $this->inputName, + 'value' => implode( "\n", $this->usersArray ), + 'rows' => 25, + ] ); + $this->prependContent( $textarea ); + } + + protected function getJavaScriptClassName() { + return 'mw.widgets.UsersMultiselectWidget'; + } + + public function getConfig( &$config ) { + if ( $this->usersArray !== null ) { + $config['selected'] = $this->usersArray; + } + if ( $this->inputName !== null ) { + $config['name'] = $this->inputName; + } + if ( $this->inputPlaceholder !== null ) { + $config['placeholder'] = $this->inputPlaceholder; + } + + $config['$overlay'] = true; + return parent::getConfig( $config ); + } + +} diff --git a/www/wiki/includes/widget/search/BasicSearchResultSetWidget.php b/www/wiki/includes/widget/search/BasicSearchResultSetWidget.php new file mode 100644 index 00000000..e2366405 --- /dev/null +++ b/www/wiki/includes/widget/search/BasicSearchResultSetWidget.php @@ -0,0 +1,134 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use Message; +use SearchResultSet; +use SpecialSearch; +use Status; + +/** + * Renders the search result area. Handles Title and Full-Text search results, + * along with inline and sidebar secondary (interwiki) results. + */ +class BasicSearchResultSetWidget { + /** @var SpecialSearch */ + protected $specialPage; + /** @var SearchResultWidget */ + protected $resultWidget; + /** @var InterwikiSearchResultSetWidget */ + protected $sidebarWidget; + + public function __construct( + SpecialSearch $specialPage, + SearchResultWidget $resultWidget, + SearchResultSetWidget $sidebarWidget + ) { + $this->specialPage = $specialPage; + $this->resultWidget = $resultWidget; + $this->sidebarWidget = $sidebarWidget; + } + + /** + * @param string $term The search term to highlight + * @param int $offset The offset of the first result in the result set + * @param SearchResultSet|null $titleResultSet Results of searching only page titles + * @param SearchResultSet|null $textResultSet Results of general full text search. + * @return string HTML + */ + public function render( + $term, + $offset, + SearchResultSet $titleResultSet = null, + SearchResultSet $textResultSet = null + ) { + global $wgContLang; + + $hasTitle = $titleResultSet ? $titleResultSet->numRows() > 0 : false; + $hasText = $textResultSet ? $textResultSet->numRows() > 0 : false; + $hasSecondary = $textResultSet + ? $textResultSet->hasInterwikiResults( SearchResultSet::SECONDARY_RESULTS ) + : false; + $hasSecondaryInline = $textResultSet + ? $textResultSet->hasInterwikiResults( SearchResultSet::INLINE_RESULTS ) + : false; + + if ( !$hasTitle && !$hasText && !$hasSecondary && !$hasSecondaryInline ) { + return ''; + } + + $out = ''; + if ( $hasTitle ) { + $out .= $this->header( $this->specialPage->msg( 'titlematches' ) ) + . $this->renderResultSet( $titleResultSet, $offset ); + } + + if ( $hasText ) { + if ( $hasTitle ) { + $out .= "<div class='mw-search-visualclear'></div>" . + $this->header( $this->specialPage->msg( 'textmatches' ) ); + } + $out .= $this->renderResultSet( $textResultSet, $offset ); + } + + if ( $hasSecondaryInline ) { + $iwResults = $textResultSet->getInterwikiResults( SearchResultSet::INLINE_RESULTS ); + foreach ( $iwResults as $interwiki => $results ) { + if ( $results instanceof Status || $results->numRows() === 0 ) { + // ignore bad interwikis for now + continue; + } + $out .= + "<h2 class='mw-search-interwiki-header mw-search-visualclear'>" . + $this->specialPage->msg( "search-interwiki-results-{$interwiki}" )->parse() . + "</h2>"; + $out .= $this->renderResultSet( $results, $offset ); + } + } + + if ( $hasSecondary ) { + $out .= $this->sidebarWidget->render( + $term, + $textResultSet->getInterwikiResults( SearchResultSet::SECONDARY_RESULTS ) + ); + } + + // Convert the whole thing to desired language variant + // TODO: Move this up to Special:Search? + return $wgContLang->convert( $out ); + } + + /** + * Generate a headline for a section of the search results. In prior + * implementations this was rendering wikitext of '==$1==', but seems + * a waste to call the full parser to generate this tiny bit of html + * + * @param Message $msg i18n message to use as header + * @return string HTML + */ + protected function header( Message $msg ) { + return "<h2>" . + "<span class='mw-headline'>" . $msg->escaped() . "</span>" . + "</h2>"; + } + + /** + * @param SearchResultSet $resultSet The search results to render + * @param int $offset Offset of the first result in $resultSet + * @return string HTML + */ + protected function renderResultSet( SearchResultSet $resultSet, $offset ) { + global $wgContLang; + + $terms = $wgContLang->convertForSearchResult( $resultSet->termMatches() ); + + $hits = []; + $result = $resultSet->next(); + while ( $result ) { + $hits[] .= $this->resultWidget->render( $result, $terms, $offset++ ); + $result = $resultSet->next(); + } + + return "<ul class='mw-search-results'>" . implode( '', $hits ) . "</ul>"; + } +} diff --git a/www/wiki/includes/widget/search/DidYouMeanWidget.php b/www/wiki/includes/widget/search/DidYouMeanWidget.php new file mode 100644 index 00000000..4e5b76b6 --- /dev/null +++ b/www/wiki/includes/widget/search/DidYouMeanWidget.php @@ -0,0 +1,105 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use HtmlArmor; +use SearchResultSet; +use SpecialSearch; + +/** + * Renders a suggested search for the user, or tells the user + * a suggested search was run instead of the one provided. + */ +class DidYouMeanWidget { + /** @var SpecialSearch */ + protected $specialSearch; + + public function __construct( SpecialSearch $specialSearch ) { + $this->specialSearch = $specialSearch; + } + + /** + * @param string $term The user provided search term + * @param SearchResultSet $resultSet + * @return string HTML + */ + public function render( $term, SearchResultSet $resultSet ) { + if ( $resultSet->hasRewrittenQuery() ) { + $html = $this->rewrittenHtml( $term, $resultSet ); + } elseif ( $resultSet->hasSuggestion() ) { + $html = $this->suggestionHtml( $resultSet ); + } else { + return ''; + } + + return "<div class='searchdidyoumean'>$html</div>"; + } + + /** + * Generates HTML shown to user when their query has been internally + * rewritten, and the results of the rewritten query are being returned. + * + * @param string $term The users search input + * @param SearchResultSet $resultSet The response to the search request + * @return string HTML Links the user to their original $term query, and the + * one suggested by $resultSet + */ + protected function rewrittenHtml( $term, SearchResultSet $resultSet ) { + $params = [ + 'search' => $resultSet->getQueryAfterRewrite(), + // Don't magic this link into a 'go' link, it should always + // show search results. + 'fultext' => 1, + ]; + $stParams = array_merge( $params, $this->specialSearch->powerSearchOptions() ); + + $linkRenderer = $this->specialSearch->getLinkRenderer(); + $snippet = $resultSet->getQueryAfterRewriteSnippet(); + $rewritten = $linkRenderer->makeKnownLink( + $this->specialSearch->getPageTitle(), + $snippet ? new HtmlArmor( $snippet ) : null, + [ 'id' => 'mw-search-DYM-rewritten' ], + $stParams + ); + + $stParams['search'] = $term; + $stParams['runsuggestion'] = 0; + $original = $linkRenderer->makeKnownLink( + $this->specialSearch->getPageTitle(), + $term, + [ 'id' => 'mwsearch-DYM-original' ], + $stParams + ); + + return $this->specialSearch->msg( 'search-rewritten' ) + ->rawParams( $rewritten, $original ) + ->escaped(); + } + + /** + * Generates HTML shown to the user when we have a suggestion about + * a query that might give more/better results than their current + * query. + * + * @param SearchResultSet $resultSet + * @return string HTML + */ + protected function suggestionHtml( SearchResultSet $resultSet ) { + $params = [ + 'search' => $resultSet->getSuggestionQuery(), + 'fulltext' => 1, + ]; + $stParams = array_merge( $params, $this->specialSearch->powerSearchOptions() ); + + $snippet = $resultSet->getSuggestionSnippet(); + $suggest = $this->specialSearch->getLinkRenderer()->makeKnownLink( + $this->specialSearch->getPageTitle(), + $snippet ? new HtmlArmor( $snippet ) : null, + [ 'id' => 'mw-search-DYM-suggestion' ], + $stParams + ); + + return $this->specialSearch->msg( 'search-suggest' ) + ->rawParams( $suggest )->parse(); + } +} diff --git a/www/wiki/includes/widget/search/FullSearchResultWidget.php b/www/wiki/includes/widget/search/FullSearchResultWidget.php new file mode 100644 index 00000000..af1e0275 --- /dev/null +++ b/www/wiki/includes/widget/search/FullSearchResultWidget.php @@ -0,0 +1,285 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use Category; +use Hooks; +use HtmlArmor; +use MediaWiki\Linker\LinkRenderer; +use SearchResult; +use SpecialSearch; +use Title; + +/** + * Renders a 'full' multi-line search result with metadata. + * + * The Title + * some *highlighted* *text* about the search result + * 5KB (651 words) - 12:40, 6 Aug 2016 + */ +class FullSearchResultWidget implements SearchResultWidget { + /** @var SpecialSearch */ + protected $specialPage; + /** @var LinkRenderer */ + protected $linkRenderer; + + public function __construct( SpecialSearch $specialPage, LinkRenderer $linkRenderer ) { + $this->specialPage = $specialPage; + $this->linkRenderer = $linkRenderer; + } + + /** + * @param SearchResult $result The result to render + * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet) + * @param int $position The result position, including offset + * @return string HTML + */ + public function render( SearchResult $result, $terms, $position ) { + // If the page doesn't *exist*... our search index is out of date. + // The least confusing at this point is to drop the result. + // You may get less results, but... on well. :P + if ( $result->isBrokenTitle() || $result->isMissingRevision() ) { + return ''; + } + + $link = $this->generateMainLinkHtml( $result, $terms, $position ); + // If page content is not readable, just return ths title. + // This is not quite safe, but better than showing excerpts from + // non-readable pages. Note that hiding the entry entirely would + // screw up paging (really?). + if ( !$result->getTitle()->userCan( 'read', $this->specialPage->getUser() ) ) { + return "<li>{$link}</li>"; + } + + $redirect = $this->generateRedirectHtml( $result ); + $section = $this->generateSectionHtml( $result ); + $category = $this->generateCategoryHtml( $result ); + $date = $this->specialPage->getLanguage()->userTimeAndDate( + $result->getTimestamp(), + $this->specialPage->getUser() + ); + list( $file, $desc, $thumb ) = $this->generateFileHtml( $result ); + $snippet = $result->getTextSnippet( $terms ); + if ( $snippet ) { + $extract = "<div class='searchresult'>$snippet</div>"; + } else { + $extract = ''; + } + + if ( $thumb === null ) { + // If no thumb, then the description is about size + $desc = $this->generateSizeHtml( $result ); + + // Let hooks do their own final construction if desired. + // FIXME: Not sure why this is only for results without thumbnails, + // but keeping it as-is for now to prevent breaking hook consumers. + $html = null; + $score = ''; + $related = ''; + if ( !Hooks::run( 'ShowSearchHit', [ + $this->specialPage, $result, $terms, + &$link, &$redirect, &$section, &$extract, + &$score, &$desc, &$date, &$related, &$html + ] ) ) { + return $html; + } + } + + // All the pieces have been collected. Now generate the final HTML + $joined = "{$link} {$redirect} {$category} {$section} {$file}"; + $meta = $this->buildMeta( $desc, $date ); + + if ( $thumb === null ) { + $html = + "<div class='mw-search-result-heading'>{$joined}</div>" . + "{$extract} {$meta}"; + } else { + $html = + "<table class='searchResultImage'>" . + "<tr>" . + "<td style='width: 120px; text-align: center; vertical-align: top'>" . + $thumb . + "</td>" . + "<td style='vertical-align: top'>" . + "{$joined} {$extract} {$meta}" . + "</td>" . + "</tr>" . + "</table>"; + } + + return "<li>{$html}</li>"; + } + + /** + * Generates HTML for the primary call to action. It is + * typically the article title, but the search engine can + * return an exact snippet to use (typically the article + * title with highlighted words). + * + * @param SearchResult $result + * @param string $terms + * @param int $position + * @return string HTML + */ + protected function generateMainLinkHtml( SearchResult $result, $terms, $position ) { + $snippet = $result->getTitleSnippet(); + if ( $snippet === '' ) { + $snippet = null; + } else { + $snippet = new HtmlArmor( $snippet ); + } + + // clone to prevent hook from changing the title stored inside $result + $title = clone $result->getTitle(); + $query = []; + + $attributes = [ 'data-serp-pos' => $position ]; + Hooks::run( 'ShowSearchHitTitle', + [ &$title, &$snippet, $result, $terms, $this->specialPage, &$query, &$attributes ] ); + + $link = $this->linkRenderer->makeLink( + $title, + $snippet, + $attributes, + $query + ); + + return $link; + } + + /** + * Generates an alternate title link, such as (redirect from <a>Foo</a>). + * + * @param string $msgKey i18n message used to wrap title + * @param Title|null $title The title to link to, or null to generate + * the message without a link. In that case $text must be non-null. + * @param string|null $text The text snippet to display, or null + * to use the title + * @return string HTML + */ + protected function generateAltTitleHtml( $msgKey, Title $title = null, $text ) { + $inner = $title === null + ? $text + : $this->linkRenderer->makeLink( $title, $text ? new HtmlArmor( $text ) : null ); + + return "<span class='searchalttitle'>" . + $this->specialPage->msg( $msgKey )->rawParams( $inner )->parse() + . "</span>"; + } + + /** + * @param SearchResult $result + * @return string HTML + */ + protected function generateRedirectHtml( SearchResult $result ) { + $title = $result->getRedirectTitle(); + return $title === null + ? '' + : $this->generateAltTitleHtml( 'search-redirect', $title, $result->getRedirectSnippet() ); + } + + /** + * @param SearchResult $result + * @return string HTML + */ + protected function generateSectionHtml( SearchResult $result ) { + $title = $result->getSectionTitle(); + return $title === null + ? '' + : $this->generateAltTitleHtml( 'search-section', $title, $result->getSectionSnippet() ); + } + + /** + * @param SearchResult $result + * @return string HTML + */ + protected function generateCategoryHtml( SearchResult $result ) { + $snippet = $result->getCategorySnippet(); + return $snippet + ? $this->generateAltTitleHtml( 'search-category', null, $snippet ) + : ''; + } + + /** + * @param SearchResult $result + * @return string HTML + */ + protected function generateSizeHtml( SearchResult $result ) { + $title = $result->getTitle(); + if ( $title->getNamespace() === NS_CATEGORY ) { + $cat = Category::newFromTitle( $title ); + return $this->specialPage->msg( 'search-result-category-size' ) + ->numParams( $cat->getPageCount(), $cat->getSubcatCount(), $cat->getFileCount() ) + ->escaped(); + // TODO: This is a bit odd...but requires changing the i18n message to fix + } elseif ( $result->getByteSize() !== null || $result->getWordCount() > 0 ) { + $lang = $this->specialPage->getLanguage(); + $bytes = $lang->formatSize( $result->getByteSize() ); + $words = $result->getWordCount(); + + return $this->specialPage->msg( 'search-result-size', $bytes ) + ->numParams( $words ) + ->escaped(); + } + + return ''; + } + + /** + * @param SearchResult $result + * @return array Three element array containing the main file html, + * a text description of the file, and finally the thumbnail html. + * If no thumbnail is available the second and third will be null. + */ + protected function generateFileHtml( SearchResult $result ) { + $title = $result->getTitle(); + if ( $title->getNamespace() !== NS_FILE ) { + return [ '', null, null ]; + } + + if ( $result->isFileMatch() ) { + $html = "<span class='searchalttitle'>" . + $this->specialPage->msg( 'search-file-match' )->escaped() . + "</span>"; + } else { + $html = ''; + } + + $descHtml = null; + $thumbHtml = null; + + $img = $result->getFile() ?: wfFindFile( $title ); + if ( $img ) { + $thumb = $img->transform( [ 'width' => 120, 'height' => 120 ] ); + if ( $thumb ) { + $descHtml = $this->specialPage->msg( 'parentheses' ) + ->rawParams( $img->getShortDesc() ) + ->escaped(); + $thumbHtml = $thumb->toHtml( [ 'desc-link' => true ] ); + } + } + + return [ $html, $descHtml, $thumbHtml ]; + } + + /** + * @param string $desc HTML description of result, ex: size in bytes, or empty string + * @param string $date HTML representation of last edit date, or empty string + * @return string HTML A div combining $desc and $date with a separator in a <div>. + * If either is missing only one will be represented. If both are missing an empty + * string will be returned. + */ + protected function buildMeta( $desc, $date ) { + if ( $desc && $date ) { + $meta = "{$desc} - {$date}"; + } elseif ( $desc ) { + $meta = $desc; + } elseif ( $date ) { + $meta = $date; + } else { + return ''; + } + + return "<div class='mw-search-result-data'>{$meta}</div>"; + } +} diff --git a/www/wiki/includes/widget/search/InterwikiSearchResultSetWidget.php b/www/wiki/includes/widget/search/InterwikiSearchResultSetWidget.php new file mode 100644 index 00000000..b4e34148 --- /dev/null +++ b/www/wiki/includes/widget/search/InterwikiSearchResultSetWidget.php @@ -0,0 +1,190 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use MediaWiki\Interwiki\InterwikiLookup; +use MediaWiki\Linker\LinkRenderer; +use SearchResultSet; +use SpecialSearch; +use Title; +use Html; +use OOUI; + +/** + * Renders one or more SearchResultSets into a sidebar grouped by + * interwiki prefix. Includes a per-wiki header indicating where + * the results are from. + */ +class InterwikiSearchResultSetWidget implements SearchResultSetWidget { + /** @var SpecialSearch */ + protected $specialSearch; + /** @var SearchResultWidget */ + protected $resultWidget; + /** @var string[]|null */ + protected $customCaptions; + /** @var LinkRenderer */ + protected $linkRenderer; + /** @var InterwikiLookup */ + protected $iwLookup; + /** @var $output */ + protected $output; + /** @var bool $showMultimedia */ + protected $showMultimedia; + + public function __construct( + SpecialSearch $specialSearch, + SearchResultWidget $resultWidget, + LinkRenderer $linkRenderer, + InterwikiLookup $iwLookup, + $showMultimedia = false + ) { + $this->specialSearch = $specialSearch; + $this->resultWidget = $resultWidget; + $this->linkRenderer = $linkRenderer; + $this->iwLookup = $iwLookup; + $this->output = $specialSearch->getOutput(); + $this->showMultimedia = $showMultimedia; + } + /** + * @param string $term User provided search term + * @param SearchResultSet|SearchResultSet[] $resultSets List of interwiki + * results to render. + * @return string HTML + */ + public function render( $term, $resultSets ) { + if ( !is_array( $resultSets ) ) { + $resultSets = [ $resultSets ]; + } + + $this->loadCustomCaptions(); + + if ( $this->showMultimedia ) { + $this->output->addModules( 'mediawiki.special.search.commonsInterwikiWidget' ); + } + $this->output->addModuleStyles( 'mediawiki.special.search.interwikiwidget.styles' ); + + $iwResults = []; + foreach ( $resultSets as $resultSet ) { + $result = $resultSet->next(); + while ( $result ) { + if ( !$result->isBrokenTitle() ) { + $iwResults[$result->getTitle()->getInterwiki()][] = $result; + } + $result = $resultSet->next(); + } + } + + $iwResultSetPos = 1; + $iwResultListOutput = ''; + + foreach ( $iwResults as $iwPrefix => $results ) { + // TODO: Assumes interwiki results are never paginated + $position = 0; + $iwResultItemOutput = ''; + + foreach ( $results as $result ) { + $iwResultItemOutput .= $this->resultWidget->render( $result, $term, $position++ ); + } + + $footerHtml = $this->footerHtml( $term, $iwPrefix ); + $iwResultListOutput .= Html::rawElement( 'li', + [ + 'class' => 'iw-resultset', + 'data-iw-resultset-pos' => $iwResultSetPos, + 'data-iw-resultset-source' => $iwPrefix + ], + + $iwResultItemOutput . + $footerHtml + ); + + $iwResultSetPos++; + } + + return Html::rawElement( + 'div', + [ 'id' => 'mw-interwiki-results' ], + Html::rawElement( + 'p', + [ 'class' => 'iw-headline' ], + $this->specialSearch->msg( 'search-interwiki-caption' )->parse() + ) . + Html::rawElement( + 'ul', [ 'class' => 'iw-results', ], $iwResultListOutput + ) + ); + } + + /** + * Generates an HTML footer for the given interwiki prefix + * + * @param string $term User provided search term + * @param string $iwPrefix Interwiki prefix of wiki to show footer for + * @return string HTML + */ + protected function footerHtml( $term, $iwPrefix ) { + $href = Title::makeTitle( NS_SPECIAL, 'Search', null, $iwPrefix )->getLocalURL( + [ 'search' => $term, 'fulltext' => 1 ] + ); + + $interwiki = $this->iwLookup->fetch( $iwPrefix ); + $parsed = wfParseUrl( wfExpandUrl( $interwiki ? $interwiki->getURL() : '/' ) ); + + if ( isset( $this->customCaptions[$iwPrefix] ) ) { + $caption = $this->customCaptions[$iwPrefix]; + } else { + $caption = $this->specialSearch->msg( 'search-interwiki-default', $parsed['host'] )->escaped(); + } + + $searchLink = Html::rawElement( 'em', null, + Html::rawElement( 'a', [ 'href' => $href, 'target' => '_blank' ], $caption ) + ); + + return Html::rawElement( 'div', + [ 'class' => 'iw-result__footer' ], + $this->iwIcon( $iwPrefix ) . $searchLink ); + } + + protected function loadCustomCaptions() { + if ( $this->customCaptions !== null ) { + return; + } + + $this->customCaptions = []; + $customLines = explode( "\n", $this->specialSearch->msg( 'search-interwiki-custom' )->escaped() ); + foreach ( $customLines as $line ) { + $parts = explode( ':', $line, 2 ); + if ( count( $parts ) === 2 ) { + $this->customCaptions[$parts[0]] = $parts[1]; + } + } + } + + /** + * Generates a custom OOUI icon element with a favicon as the image. + * The favicon image URL is generated by parsing the interwiki URL + * and returning the default location of the favicon for that domain, + * which is assumed to be '/favicon.ico'. + * + * @param string $iwPrefix Interwiki prefix + * @return OOUI\IconWidget + */ + protected function iwIcon( $iwPrefix ) { + $interwiki = $this->iwLookup->fetch( $iwPrefix ); + $parsed = wfParseUrl( wfExpandUrl( $interwiki ? $interwiki->getURL() : '/' ) ); + + $iwIconUrl = $parsed['scheme'] . + $parsed['delimiter'] . + $parsed['host'] . + ( isset( $parsed['port'] ) ? ':' . $parsed['port'] : '' ) . + '/favicon.ico'; + + $iwIcon = new OOUI\IconWidget( [ + 'icon' => 'favicon' + ] ); + + $iwIcon->setAttributes( [ 'style' => "background-image:url($iwIconUrl);" ] ); + + return $iwIcon; + } +} diff --git a/www/wiki/includes/widget/search/InterwikiSearchResultWidget.php b/www/wiki/includes/widget/search/InterwikiSearchResultWidget.php new file mode 100644 index 00000000..4eead5e7 --- /dev/null +++ b/www/wiki/includes/widget/search/InterwikiSearchResultWidget.php @@ -0,0 +1,66 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use HtmlArmor; +use MediaWiki\Linker\LinkRenderer; +use SearchResult; +use SpecialSearch; +use Html; + +/** + * Renders an enhanced interwiki result + */ +class InterwikiSearchResultWidget implements SearchResultWidget { + /** @var SpecialSearch */ + protected $specialSearch; + /** @var LinkRenderer */ + protected $linkRenderer; + + public function __construct( SpecialSearch $specialSearch, LinkRenderer $linkRenderer ) { + $this->specialSearch = $specialSearch; + $this->linkRenderer = $linkRenderer; + } + + /** + * @param SearchResult $result The result to render + * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet) + * @param int $position The result position, including offset + * @return string HTML + */ + public function render( SearchResult $result, $terms, $position ) { + $title = $result->getTitle(); + $iwPrefix = $result->getTitle()->getInterwiki(); + $titleSnippet = $result->getTitleSnippet(); + $snippet = $result->getTextSnippet( $terms ); + + if ( $titleSnippet ) { + $titleSnippet = new HtmlArmor( $titleSnippet ); + } else { + $titleSnippet = null; + } + + $link = $this->linkRenderer->makeLink( $title, $titleSnippet ); + + $redirectTitle = $result->getRedirectTitle(); + $redirect = ''; + if ( $redirectTitle !== null ) { + $redirectText = $result->getRedirectSnippet(); + + if ( $redirectText ) { + $redirectText = new HtmlArmor( $redirectText ); + } else { + $redirectText = null; + } + + $redirect = Html::rawElement( 'span', [ 'class' => 'iw-result__redirect' ], + $this->specialSearch->msg( 'search-redirect' )->rawParams( + $this->linkRenderer->makeLink( $redirectTitle, $redirectText ) + )->escaped() + ); + } + + return Html::rawElement( 'div', [ 'class' => 'iw-result__title' ], $link . ' ' . $redirect ) . + Html::rawElement( 'div', [ 'class' => 'iw-result__content' ], $snippet ); + } +} diff --git a/www/wiki/includes/widget/search/SearchFormWidget.php b/www/wiki/includes/widget/search/SearchFormWidget.php new file mode 100644 index 00000000..2c885630 --- /dev/null +++ b/www/wiki/includes/widget/search/SearchFormWidget.php @@ -0,0 +1,314 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use Hooks; +use Html; +use MediaWiki\Widget\SearchInputWidget; +use MWNamespace; +use SearchEngineConfig; +use SpecialSearch; +use Xml; + +class SearchFormWidget { + /** @var SpecialSearch */ + protected $specialSearch; + /** @var SearchEngineConfig */ + protected $searchConfig; + /** @var array */ + protected $profiles; + + /** + * @param SpecialSearch $specialSearch + * @param SearchEngineConfig $searchConfig + * @param array $profiles + */ + public function __construct( + SpecialSearch $specialSearch, + SearchEngineConfig $searchConfig, + array $profiles + ) { + $this->specialSearch = $specialSearch; + $this->searchConfig = $searchConfig; + $this->profiles = $profiles; + } + + /** + * @param string $profile The current search profile + * @param string $term The current search term + * @param int $numResults The number of results shown + * @param int $totalResults The total estimated results found + * @param int $offset Current offset in search results + * @param bool $isPowerSearch Is the 'advanced' section open? + * @return string HTML + */ + public function render( + $profile, + $term, + $numResults, + $totalResults, + $offset, + $isPowerSearch + ) { + return Xml::openElement( + 'form', + [ + 'id' => $isPowerSearch ? 'powersearch' : 'search', + 'method' => 'get', + 'action' => wfScript(), + ] + ) . + '<div id="mw-search-top-table">' . + $this->shortDialogHtml( $profile, $term, $numResults, $totalResults, $offset ) . + '</div>' . + "<div class='mw-search-visualclear'></div>" . + "<div class='mw-search-profile-tabs'>" . + $this->profileTabsHtml( $profile, $term ) . + "<div style='clear:both'></div>" . + "</div>" . + $this->optionsHtml( $term, $isPowerSearch, $profile ) . + '</form>'; + } + + /** + * @param string $profile The current search profile + * @param string $term The current search term + * @param int $numResults The number of results shown + * @param int $totalResults The total estimated results found + * @param int $offset Current offset in search results + * @return string HTML + */ + protected function shortDialogHtml( $profile, $term, $numResults, $totalResults, $offset ) { + $html = ''; + + $searchWidget = new SearchInputWidget( [ + 'id' => 'searchText', + 'name' => 'search', + 'autofocus' => trim( $term ) === '', + 'value' => $term, + 'dataLocation' => 'content', + 'infusable' => true, + ] ); + + $layout = new \OOUI\ActionFieldLayout( $searchWidget, new \OOUI\ButtonInputWidget( [ + 'type' => 'submit', + 'label' => $this->specialSearch->msg( 'searchbutton' )->text(), + 'flags' => [ 'progressive', 'primary' ], + ] ), [ + 'align' => 'top', + ] ); + + $html .= $layout; + + if ( $totalResults > 0 && $offset < $totalResults ) { + $html .= Xml::tags( + 'div', + [ + 'class' => 'results-info', + 'data-mw-num-results-offset' => $offset, + 'data-mw-num-results-total' => $totalResults + ], + $this->specialSearch->msg( 'search-showingresults' ) + ->numParams( $offset + 1, $offset + $numResults, $totalResults ) + ->numParams( $numResults ) + ->parse() + ); + } + + $html .= + Html::hidden( 'title', $this->specialSearch->getPageTitle()->getPrefixedText() ) . + Html::hidden( 'profile', $profile ) . + Html::hidden( 'fulltext', '1' ); + + return $html; + } + + /** + * Generates HTML for the list of available search profiles. + * + * @param string $profile The currently selected profile + * @param string $term The user provided search terms + * @return string HTML + */ + protected function profileTabsHtml( $profile, $term ) { + $bareterm = $this->startsWithImage( $term ) + ? substr( $term, strpos( $term, ':' ) + 1 ) + : $term; + $lang = $this->specialSearch->getLanguage(); + $items = []; + foreach ( $this->profiles as $id => $profileConfig ) { + $profileConfig['parameters']['profile'] = $id; + $tooltipParam = isset( $profileConfig['namespace-messages'] ) + ? $lang->commaList( $profileConfig['namespace-messages'] ) + : null; + $items[] = Xml::tags( + 'li', + [ 'class' => $profile === $id ? 'current' : 'normal' ], + $this->makeSearchLink( + $bareterm, + $this->specialSearch->msg( $profileConfig['message'] )->text(), + $this->specialSearch->msg( $profileConfig['tooltip'], $tooltipParam )->text(), + $profileConfig['parameters'] + ) + ); + } + + return "<div class='search-types'>" . + "<ul>" . implode( '', $items ) . "</ul>" . + "</div>"; + } + + /** + * Check if query starts with image: prefix + * + * @param string $term The string to check + * @return bool + */ + protected function startsWithImage( $term ) { + global $wgContLang; + + $parts = explode( ':', $term ); + return count( $parts ) > 1 + ? $wgContLang->getNsIndex( $parts[0] ) === NS_FILE + : false; + } + + /** + * Make a search link with some target namespaces + * + * @param string $term The term to search for + * @param string $label Link's text + * @param string $tooltip Link's tooltip + * @param array $params Query string parameters + * @return string HTML fragment + */ + protected function makeSearchLink( $term, $label, $tooltip, array $params = [] ) { + $params += [ + 'search' => $term, + 'fulltext' => 1, + ]; + + return Xml::element( + 'a', + [ + 'href' => $this->specialSearch->getPageTitle()->getLocalURL( $params ), + 'title' => $tooltip, + ], + $label + ); + } + + /** + * Generates HTML for advanced options available with the currently + * selected search profile. + * + * @param string $term User provided search term + * @param bool $isPowerSearch Is the advanced search profile enabled? + * @param string $profile The current search profile + * @return string HTML + */ + protected function optionsHtml( $term, $isPowerSearch, $profile ) { + $html = ''; + + if ( $isPowerSearch ) { + $html .= $this->powerSearchBox( $term, [] ); + } else { + $form = ''; + Hooks::run( 'SpecialSearchProfileForm', [ + $this->specialSearch, &$form, $profile, $term, [] + ] ); + $html .= $form; + } + + return $html; + } + + /** + * @param string $term The current search term + * @param array $opts Additional key/value pairs that will be submitted + * with the generated form. + * @return string HTML + */ + protected function powerSearchBox( $term, array $opts ) { + global $wgContLang; + + $rows = []; + $activeNamespaces = $this->specialSearch->getNamespaces(); + foreach ( $this->searchConfig->searchableNamespaces() as $namespace => $name ) { + $subject = MWNamespace::getSubject( $namespace ); + if ( !isset( $rows[$subject] ) ) { + $rows[$subject] = ""; + } + + $name = $wgContLang->getConverter()->convertNamespace( $namespace ); + if ( $name === '' ) { + $name = $this->specialSearch->msg( 'blanknamespace' )->text(); + } + + $rows[$subject] .= + '<td>' . + Xml::checkLabel( + $name, + "ns{$namespace}", + "mw-search-ns{$namespace}", + in_array( $namespace, $activeNamespaces ) + ) . + '</td>'; + } + + // Lays out namespaces in multiple floating two-column tables so they'll + // be arranged nicely while still accomodating diferent screen widths + $tableRows = []; + foreach ( $rows as $row ) { + $tableRows[] = "<tr>{$row}</tr>"; + } + $namespaceTables = []; + foreach ( array_chunk( $tableRows, 4 ) as $chunk ) { + $namespaceTables[] = implode( '', $chunk ); + } + + $showSections = [ + 'namespaceTables' => "<table>" . implode( '</table><table>', $namespaceTables ) . '</table>', + ]; + Hooks::run( 'SpecialSearchPowerBox', [ &$showSections, $term, $opts ] ); + + $hidden = ''; + foreach ( $opts as $key => $value ) { + $hidden .= Html::hidden( $key, $value ); + } + + $divider = "<div class='divider'></div>"; + + // Stuff to feed SpecialSearch::saveNamespaces() + $user = $this->specialSearch->getUser(); + $remember = ''; + if ( $user->isLoggedIn() ) { + $remember = $divider . Xml::checkLabel( + $this->specialSearch->msg( 'powersearch-remember' )->text(), + 'nsRemember', + 'mw-search-powersearch-remember', + false, + // The token goes here rather than in a hidden field so it + // is only sent when necessary (not every form submission) + [ 'value' => $user->getEditToken( + 'searchnamespace', + $this->specialSearch->getRequest() + ) ] + ); + } + + return "<fieldset id='mw-searchoptions'>" . + "<legend>" . $this->specialSearch->msg( 'powersearch-legend' )->escaped() . '</legend>' . + "<h4>" . $this->specialSearch->msg( 'powersearch-ns' )->parse() . '</h4>' . + // populated by js if available + "<div id='mw-search-togglebox'></div>" . + $divider . + implode( + $divider, + $showSections + ) . + $hidden . + $remember . + "</fieldset>"; + } +} diff --git a/www/wiki/includes/widget/search/SearchResultSetWidget.php b/www/wiki/includes/widget/search/SearchResultSetWidget.php new file mode 100644 index 00000000..6df6e65c --- /dev/null +++ b/www/wiki/includes/widget/search/SearchResultSetWidget.php @@ -0,0 +1,18 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use SearchResultSet; + +/** + * Renders a set of search results to HTML + */ +interface SearchResultSetWidget { + /** + * @param string $term User provided search term + * @param SearchResultSet|SearchResultSet[] $resultSets List of interwiki + * results to render. + * @return string HTML + */ + public function render( $term, $resultSets ); +} diff --git a/www/wiki/includes/widget/search/SearchResultWidget.php b/www/wiki/includes/widget/search/SearchResultWidget.php new file mode 100644 index 00000000..3fbdbef2 --- /dev/null +++ b/www/wiki/includes/widget/search/SearchResultWidget.php @@ -0,0 +1,18 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use SearchResult; + +/** + * Renders a single search result to HTML + */ +interface SearchResultWidget { + /** + * @param SearchResult $result The result to render + * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet) + * @param int $position The zero indexed result position, including offset + * @return string HTML + */ + public function render( SearchResult $result, $terms, $position ); +} diff --git a/www/wiki/includes/widget/search/SimpleSearchResultSetWidget.php b/www/wiki/includes/widget/search/SimpleSearchResultSetWidget.php new file mode 100644 index 00000000..d0c259fe --- /dev/null +++ b/www/wiki/includes/widget/search/SimpleSearchResultSetWidget.php @@ -0,0 +1,133 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use MediaWiki\Interwiki\InterwikiLookup; +use MediaWiki\Linker\LinkRenderer; +use SearchResultSet; +use SpecialSearch; +use Title; +use Html; + +/** + * Renders one or more SearchResultSets into a sidebar grouped by + * interwiki prefix. Includes a per-wiki header indicating where + * the results are from. + * + * @deprecated since 1.31. Use InterwikiSearchResultSetWidget + */ +class SimpleSearchResultSetWidget implements SearchResultSetWidget { + /** @var SpecialSearch */ + protected $specialSearch; + /** @var SearchResultWidget */ + protected $resultWidget; + /** @var string[]|null */ + protected $customCaptions; + /** @var LinkRenderer */ + protected $linkRenderer; + /** @var InterwikiLookup */ + protected $iwLookup; + + public function __construct( + SpecialSearch $specialSearch, + SearchResultWidget $resultWidget, + LinkRenderer $linkRenderer, + InterwikiLookup $iwLookup + ) { + wfDeprecated( __METHOD__, '1.31' ); + $this->specialSearch = $specialSearch; + $this->resultWidget = $resultWidget; + $this->linkRenderer = $linkRenderer; + $this->iwLookup = $iwLookup; + } + + /** + * @param string $term User provided search term + * @param SearchResultSet|SearchResultSet[] $resultSets List of interwiki + * results to render. + * @return string HTML + */ + public function render( $term, $resultSets ) { + if ( !is_array( $resultSets ) ) { + $resultSets = [ $resultSets ]; + } + + $this->loadCustomCaptions(); + + $iwResults = []; + foreach ( $resultSets as $resultSet ) { + $result = $resultSet->next(); + while ( $result ) { + if ( !$result->isBrokenTitle() ) { + $iwResults[$result->getTitle()->getInterwiki()][] = $result; + } + $result = $resultSet->next(); + } + } + + $out = ''; + foreach ( $iwResults as $iwPrefix => $results ) { + $out .= $this->headerHtml( $iwPrefix, $term ); + $out .= "<ul class='mw-search-iwresults'>"; + // TODO: Assumes interwiki results are never paginated + $position = 0; + foreach ( $results as $result ) { + $out .= $this->resultWidget->render( $result, $term, $position++ ); + } + $out .= "</ul>"; + } + + return "<div id='mw-search-interwiki'>" . + "<div id='mw-search-interwiki-caption'>" . + $this->specialSearch->msg( 'search-interwiki-caption' )->parse() . + '</div>' . + $out . + "</div>"; + } + + /** + * Generates an appropriate HTML header for the given interwiki prefix + * + * @param string $iwPrefix Interwiki prefix of wiki to show header for + * @param string $term User provided search term + * @return string HTML + */ + protected function headerHtml( $iwPrefix, $term ) { + if ( isset( $this->customCaptions[$iwPrefix] ) ) { + $caption = $this->customCaptions[$iwPrefix]; + } else { + $interwiki = $this->iwLookup->fetch( $iwPrefix ); + $parsed = wfParseUrl( wfExpandUrl( $interwiki ? $interwiki->getURL() : '/' ) ); + $caption = $this->specialSearch->msg( 'search-interwiki-default', $parsed['host'] )->escaped(); + } + + $href = Title::makeTitle( NS_SPECIAL, 'Search', null, $iwPrefix )->getLocalURL( + [ 'search' => $term, 'fulltext' => 1 ] + ); + $searchLink = Html::rawElement( + 'a', + [ 'href' => $href ], + $this->specialSearch->msg( 'search-interwiki-more' )->escaped() + ); + + return "<div class='mw-search-interwiki-project'>" . + "<span class='mw-search-interwiki-more'>{$searchLink}</span>" . + $caption . + "</div>"; + } + + protected function loadCustomCaptions() { + if ( $this->customCaptions !== null ) { + return; + } + + $this->customCaptions = []; + $customLines = explode( "\n", $this->specialSearch->msg( 'search-interwiki-custom' )->escaped() ); + foreach ( $customLines as $line ) { + $parts = explode( ':', $line, 2 ); + if ( count( $parts ) === 2 ) { + $this->customCaptions[$parts[0]] = $parts[1]; + } + } + } +} diff --git a/www/wiki/includes/widget/search/SimpleSearchResultWidget.php b/www/wiki/includes/widget/search/SimpleSearchResultWidget.php new file mode 100644 index 00000000..552cbaf8 --- /dev/null +++ b/www/wiki/includes/widget/search/SimpleSearchResultWidget.php @@ -0,0 +1,63 @@ +<?php + +namespace MediaWiki\Widget\Search; + +use HtmlArmor; +use MediaWiki\Linker\LinkRenderer; +use SearchResult; +use SpecialSearch; + +/** + * Renders a simple one-line result + * + * @deprecated since 1.31. Use other result widgets. + */ +class SimpleSearchResultWidget implements SearchResultWidget { + /** @var SpecialSearch */ + protected $specialSearch; + /** @var LinkRenderer */ + protected $linkRenderer; + + public function __construct( SpecialSearch $specialSearch, LinkRenderer $linkRenderer ) { + wfDeprecated( __METHOD__, '1.31' ); + $this->specialSearch = $specialSearch; + $this->linkRenderer = $linkRenderer; + } + + /** + * @param SearchResult $result The result to render + * @param string $terms Terms to be highlighted (@see SearchResult::getTextSnippet) + * @param int $position The result position, including offset + * @return string HTML + */ + public function render( SearchResult $result, $terms, $position ) { + $title = $result->getTitle(); + $titleSnippet = $result->getTitleSnippet(); + if ( $titleSnippet ) { + $titleSnippet = new HtmlArmor( $titleSnippet ); + } else { + $titleSnippet = null; + } + + $link = $this->linkRenderer->makeLink( $title, $titleSnippet ); + + $redirectTitle = $result->getRedirectTitle(); + $redirect = ''; + if ( $redirectTitle !== null ) { + $redirectText = $result->getRedirectSnippet(); + if ( $redirectText ) { + $redirectText = new HtmlArmor( $redirectText ); + } else { + $redirectText = null; + } + $redirect = + "<span class='searchalttitle'>" . + $this->specialSearch->msg( 'search-redirect' )->rawParams( + $this->linkRenderer->makeLink( $redirectTitle, $redirectText ) + )->parse() . + "</span>"; + } + + return "<li>{$link} {$redirect}</li>"; + } +} |