feat: only add empty paragraph for wikitext list elements

We don't need the empty paragraph hack for non-list elements.
Related: #151
This commit is contained in:
alistair3149 2024-11-15 17:08:21 -05:00 committed by alistair3149
parent 21ad702ee2
commit f24ddb58ee
5 changed files with 155 additions and 181 deletions

View file

@ -14,20 +14,25 @@ declare( strict_types=1 );
namespace MediaWiki\Extension\TabberNeue;
use Html;
use JsonException;
use MediaWiki\MediaWikiServices;
use Parser;
use PPFrame;
use Sanitizer;
class Tabber {
/**
* Flag that checks if this is a nested tabber
* @var bool
*/
/** @var int */
private static $count = 0;
/** @var bool Flag that checks if this is a nested tabber */
private static $isNested = false;
/** @var bool */
private static $useCodex = false;
/** @var bool */
private static $parseTabName = false;
/**
@ -42,8 +47,11 @@ class Tabber {
*/
public static function parserHook( ?string $input, array $args, Parser $parser, PPFrame $frame ) {
$config = MediaWikiServices::getInstance()->getMainConfig();
$parserOutput = $parser->getOutput();
self::$parseTabName = $config->get( 'TabberNeueParseTabName' );
self::$useCodex = $config->get( 'TabberNeueUseCodex' );
self::$count = count( $parserOutput->getExtensionData( 'tabber-count' ) ?? [] );
$html = self::render( $input ?? '', $parser, $frame );
@ -51,6 +59,8 @@ class Tabber {
return '';
}
$parserOutput->appendExtensionData( 'tabber-count', self::$count++ );
if ( self::$useCodex === true ) {
$parser->getOutput()->addModules( [ 'ext.tabberNeue.codex' ] );
} else {
@ -73,34 +83,36 @@ class Tabber {
*/
public static function render( string $input, Parser $parser, PPFrame $frame ): string {
$arr = explode( '|-|', $input );
$htmlTabs = '';
$tabs = '';
$tabpanels = '';
foreach ( $arr as $tab ) {
$tabData = self::getTabData( $tab, $parser, $frame );
if ( $tabData['label'] === '' ) {
if ( $tabData === [] ) {
continue;
}
if ( self::$useCodex && self::$isNested ) {
$htmlTabs .= self::getCodexNestedTabJSON( $tabData );
$tabpanels .= self::getCodexNestedTabJSON( $tabData );
continue;
}
$htmlTabs .= self::buildTabpanel( $tabData );
$tabs .= self::getTabHTML( $tabData );
$tabpanels .= self::getTabpanelHTML( $tabData );
}
if ( self::$useCodex && self::$isNested ) {
$tab = rtrim( implode( '},', explode( '}', $htmlTabs ) ), ',' );
$tab = strip_tags( html_entity_decode( $tab ) );
$tab = str_replace( ',,', ',', $tab );
$tab = str_replace( ',]', ']', $tab );
return sprintf( '[%s]', $tab );
$tabpanels = rtrim( implode( '},', explode( '}', $tabpanels ) ), ',' );
$tabpanels = strip_tags( html_entity_decode( $tab ) );
$tabpanels = str_replace( ',,', ',', $tabpanels );
$tabpanels = str_replace( ',]', ']', $tabpanels );
return sprintf( '[%s]', $tabpanels );
}
return '<div class="tabber">' .
return '<div class="tabber tabber--init">' .
'<header class="tabber__header"></header>' .
'<section class="tabber__section">' . $htmlTabs . '</section></div>';
// '<header class="tabber__header"><nav class="tabber__tabs" role="tablist">' . $tabs . '</nav></header>' .
'<section class="tabber__section">' . $tabpanels . '</section></div>';
}
/**
@ -125,7 +137,6 @@ class Tabber {
// Might contains HTML
$label = $parser->recursiveTagParseFully( $label );
$label = $parser->stripOuterParagraph( $label );
$label = htmlentities( $label );
}
return $label;
}
@ -144,21 +155,26 @@ class Tabber {
if ( $content === '' ) {
return '';
}
// Fix #151, some wikitext magic
$content = "\n" . $content . "\n";
if ( !self::$useCodex ) {
$content = $parser->recursiveTagParse( $content, $frame );
} else {
// A nested tabber which should return json in codex
if ( strpos( $content, '{{#tag:tabber' ) !== false ) {
$wikitextListMarkers = [ '*', '#', ';', ':' ];
$isWikitextList = in_array( substr( $content, 0, 1 ), $wikitextListMarkers );
if ( $isWikitextList ) {
// Fix #151, some wikitext magic
// Seems like there is no way to get rid of the mw-empty-elt paragraphs sadly
$content = "\n$content\n";
}
return $parser->recursiveTagParse( $content, $frame );
}
// The outermost tabber that must be parsed fully in codex for correct json
if ( strpos( $content, '{{#tag:tabber' ) === false ) {
return $parser->recursiveTagParseFully( $content, $frame );
}
self::$isNested = true;
$content = $parser->recursiveTagParse( $content, $frame );
self::$isNested = false;
// The outermost tabber that must be parsed fully in codex for correct json
} else {
$content = $parser->recursiveTagParseFully( $content, $frame );
}
}
return $content;
}
@ -172,10 +188,7 @@ class Tabber {
* @return array<string, string>
*/
private static function getTabData( string $tab, Parser $parser, PPFrame $frame ): array {
$data = [
'label' => '',
'content' => ''
];
$data = [];
if ( empty( trim( $tab ) ) ) {
return $data;
}
@ -189,28 +202,47 @@ class Tabber {
}
$data['content'] = self::getTabContent( $content, $parser, $frame );
$id = Sanitizer::escapeIdForAttribute( htmlspecialchars( $data['label'] ) ) . '-' . self::$count;
$data['id'] = $id;
return $data;
}
/**
* Build individual tabpanel.
* Get the HTML for a tab.
*
* @param array $tabData Tab data
*
* @return string HTML
*/
private static function buildTabpanel( array $tabData ): string {
$label = $tabData['label'];
$content = $tabData['content'];
private static function getTabHTML( array $tabData ): string {
return Html::rawElement( 'a', [
'class' => 'tabber__tab',
'id' => "tabber-tab-{$tabData['id']}",
'href' => "#tabber-tabpanel-{$tabData['id']}",
'role' => 'tab',
], $tabData['label'] );
}
/**
* Get the HTML for a tabpanel.
*
* @param array $tabData Tab data
*
* @return string HTML
*/
private static function getTabpanelHTML( array $tabData ): string {
$content = $tabData['content'];
$isContentHTML = strpos( $content, '<' ) === 0;
if ( $content && !$isContentHTML ) {
// If $content does not have any HTML element (i.e. just a text node), wrap it in <p/>
$content = '<p>' . $content . '</p>';
$content = Html::rawElement( 'p', [], $content );
}
return '<article class="tabber__panel" data-mw-tabber-title="' . $label .
'">' . $content . "</article>";
return Html::rawElement( 'article', [
'class' => 'tabber__panel',
// 'id' => "tabber-tabpanel-{$tabData['id']}",
'data-mw-tabber-title' => $tabData['label'],
], $content );
}
/**

View file

@ -39,7 +39,7 @@ class TabberTransclude {
}
$parser->getOutput()->addModuleStyles( [ 'ext.tabberNeue.init.styles' ] );
$parser->getOutput()->addModules( [ 'ext.tabberNeue' ] );
//$parser->getOutput()->addModules( [ 'ext.tabberNeue' ] );
$parser->addTrackingCategory( 'tabberneue-tabbertransclude-category' );
return $html;
@ -60,7 +60,7 @@ class TabberTransclude {
$htmlTabs = '';
foreach ( $arr as $tab ) {
$tabData = self::getTabData( $tab );
if ( $tabData['label'] === '' ) {
if ( $tabData === [] ) {
continue;
}
try {
@ -84,10 +84,7 @@ class TabberTransclude {
* @return array<string, string>
*/
private static function getTabData( string $tab ): array {
$data = [
'label' => '',
'content' => ''
];
$data = [];
if ( empty( trim( $tab ) ) ) {
return $data;
}

View file

@ -1,72 +1,92 @@
/**
* Critial rendering styles
*
* Since ext.tabberNeue is loaded a while after page load,
* inline styles are needed to create an inital state and
* avoid potential layout shifts. This should be kept as
* small as possible.
*/
.tabber {
&__header {
box-shadow: inset 0 -1px 0 0 var( --border-color-base, #a2a9b1 );
}
/* stylelint-disable selector-class-pattern */
&__tabs {
display: flex;
overflow: auto hidden;
}
/* Only apply skeleton UI when Tabber will be loaded */
.client-js {
.tabber:not( .tabber--live ) {
.tabber__header {
height: 2.6em;
box-shadow: inset 0 -1px 0 0;
opacity: 0.1;
&__tab {
padding: 0.5em 0.75em;
color: var( --color-base, #202122 );
font-weight: 700;
white-space: nowrap;
&::after {
position: absolute;
width: 16ch;
height: 0.5em;
border-radius: 40px;
margin-top: 1em;
margin-left: 0.75em;
animation-duration: 10s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: skeletonload;
animation-timing-function: linear;
background: #000;
background: linear-gradient( to right, #202122 8%, #54595d 18%, #202122 33% );
/* Use double quote in PHP */
content: '';
&,
&:visited {
color: var( --color-base, #202122 );
}
&:hover {
@media ( hover: hover ) {
color: var( --color-progressive--hover, #447ff5 );
box-shadow: inset 0 -2px 0 0 var( --box-shadow-color-progressive-selected--hover, #447ff5 );
}
}
/**
* Avoid layout shift by assigning the grid property early on
* Because display:block does not take into account of bottom margin of the content
*/
.tabber__section {
&:active {
@media ( hover: hover ) {
color: var( --color-progressive--active, #2a4b8d );
}
}
&,
&:hover,
&:active,
&:focus {
text-decoration: none;
}
}
&__section {
display: grid;
overflow: hidden;
block-size: 100%;
grid-auto-columns: 100%;
grid-auto-flow: column;
scroll-snap-type: x mandatory;
}
/**
* Hide all other panels
* All panels are stacked vertically initially
* then panels are stacked horizontally after Tabber is loaded
* Causing lots of layout shift
*/
.tabber__panel:not( :first-child ) {
&__panel {
height: max-content;
overflow-x: auto;
scroll-snap-align: start;
// Hide edit buttons for non-transclusion tabs since they don't work
/* stylelint-disable-next-line selector-class-pattern */
&:not( [ data-mw-tabber-page-title ] ) .mw-editsection {
display: none;
}
}
}
/* Hide no script message */
.tabber__noscript {
display: none;
// Set tabber height to the height of first tabpanel by
// setting subsequent tabpanels to have 0 height
.client-js {
.tabber {
&__tabs {
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
@keyframes skeletonload {
0% {
background-position: 0 0;
&--init {
.tabber__panel ~ .tabber__panel {
height: 0;
}
}
}
}
100% {
background-position: 100em 0;
// Basic nojs support
.client-nojs {
.tabber__panel {
scroll-padding-top: 3rem;
height: auto;
}
}

View file

@ -655,6 +655,7 @@ class TabberBuilder {
const tabberEvent = new TabberEvent( this.tabber, this.tablist );
tabberEvent.init();
this.tabber.classList.remove( 'tabber--init' );
this.tabber.classList.add( 'tabber--live' );
}
}

View file

@ -16,26 +16,6 @@
overflow: hidden;
flex-direction: column;
&__tabs {
display: flex;
overflow: auto hidden;
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
&__section {
display: grid;
overflow: hidden;
block-size: 100%;
grid-auto-columns: 100%;
grid-auto-flow: column;
scroll-snap-type: x mandatory;
}
&__header {
position: relative;
display: flex;
@ -103,16 +83,6 @@
}
}
&__header,
&__section {
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
&__indicator {
display: none;
margin-top: ~'calc( var( --tabber-height-indicator ) * -1 )';
@ -126,49 +96,17 @@
}
&__tab {
padding: 0.5em 0.75em;
color: var( --tabber-color );
font-weight: bold;
text-decoration: none;
white-space: nowrap;
&:visited {
color: var( --tabber-color );
}
&:hover,
&:active,
&:focus {
text-decoration: none;
}
&[ aria-selected='true' ] {
box-shadow: 0 -2px 0 var( --tabber-color-progressive ) inset;
box-shadow: 0 -2px 0 var( --color-progressive, #36c ) inset;
}
&[ aria-selected='true' ],
&[ aria-selected='true' ]:visited {
color: var( --tabber-color-progressive );
}
}
&__tabs--animate {
.tabber__tab[ aria-selected='true' ] {
box-shadow: none;
color: var( --color-progressive, #36c );
}
}
&__panel {
height: max-content;
overflow-x: auto;
scroll-snap-align: start;
// Hide edit buttons for non-transclusion tabs since they don't work
/* stylelint-disable-next-line selector-class-pattern */
&:not( [ data-mw-tabber-page-title ] ) .mw-editsection {
display: none;
}
&--loading {
.tabber__transclusion {
opacity: 0.1;
@ -213,10 +151,6 @@
scroll-behavior: smooth;
}
&__indicator {
transition: transform 250ms ease, width 250ms ease;
}
&__section,
&__tabs {
@media ( min-width: 720px ) {
@ -244,16 +178,6 @@
@media ( hover: hover ) {
.tabber {
&__tab {
&:hover {
color: var( --tabber-color-progressive--hover );
}
&:active {
color: var( --tabber-color-progressive--active );
}
}
&__header {
&__prev,
&__next {