From 883a2db096a0cd1775de39debe231033ed0ad1e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Turek-Brzozowski?= Date: Sat, 16 May 2026 18:14:14 +0200 Subject: [PATCH] PLATFORM-11770 pvxcode - render utility --- classes/PvXCode.php | 20 +++++- gwbbcode/gwbbcode.inc.php | 137 +++++++++++++++++++++++++++++++++++++- 2 files changed, 151 insertions(+), 6 deletions(-) diff --git a/classes/PvXCode.php b/classes/PvXCode.php index 4f62b90..34b41fc 100644 --- a/classes/PvXCode.php +++ b/classes/PvXCode.php @@ -1,5 +1,7 @@ getOutput()->addModuleStyles( [ 'ext.PvXCode.css' ] ); - $parser->getOutput()->addModules( [ 'ext.PvXCode.js' ] ); + $config = MediaWikiServices::getInstance()->getMainConfig(); + $utilityMode = $config->get( 'PvxCodeUtility' ) + && $config->get( FandomConfigNames::EnableUtilityFramework ); + + if ( !$utilityMode ) { + $parser->getOutput()->addModuleStyles( [ 'ext.PvXCode.css' ] ); + $parser->getOutput()->addModules( [ 'ext.PvXCode.js' ] ); + } + $title = TitleValue::newFromPage( $parser->getPage() ); $text = $title->getText(); @@ -34,6 +43,11 @@ public static function parserRender( string $input, array $args, Parser $parser, // in a div and the associated processing time hidden HTML comment $parsed_input = $parser->recursiveTagParse( $input, $frame = false ); - return parseGwbbcode( $parsed_input, $text ); + $output = parseGwbbcode( $parsed_input, $text, $utilityMode ); + + // In utility mode the output contains tags + // that must be processed by ParserTagHookHandler — that only happens if + // we run them through the parser again. + return $utilityMode ? $parser->recursiveTagParse( $output, false ) : $output; } } diff --git a/gwbbcode/gwbbcode.inc.php b/gwbbcode/gwbbcode.inc.php index 4b32148..acaab93 100644 --- a/gwbbcode/gwbbcode.inc.php +++ b/gwbbcode/gwbbcode.inc.php @@ -46,11 +46,18 @@ /** * Prepares the page and then replaces gwBBCode with HTML * This function is directly called by: extension\classes\PvXCode.php - * @param $text - * @param bool $build_name + * @param string $text + * @param string|false $build_name + * @param bool $utilityMode When true, builds and skills are rendered as + * tags via {@see parseGwbbcodeUtility} instead of + * the legacy HTML pipeline. * @return array|string|string[]|null */ -function parseGwbbcode( $text, $build_name = false ) { +function parseGwbbcode( $text, $build_name = false, bool $utilityMode = false ) { + if ( $utilityMode ) { + return parseGwbbcodeUtility( $text, $build_name ); + } + // Timer for the gwBBCode parse duration $start = microtime( true ); @@ -102,6 +109,47 @@ function parseGwbbcode( $text, $build_name = false ) { return $text; } +/** + * Utility-Framework variant of {@see parseGwbbcode}. Runs the same pipeline, + * but routes [build] and [skill] to the utility-mode callbacks that emit + * tags. Other constructs ([Random Skill], [rand], + * [build=…], [skillset=…], [pre], [nobb], [gwbbcode …]) keep their legacy + * behaviour per the spec clarifications. + * + * @param string $text + * @param string|false $build_name + * @return array|string|string[]|null + */ +function parseGwbbcodeUtility( $text, $build_name = false ) { + $start = microtime( true ); + + if ( !empty( $build_name ) && !preg_match( '#(\[build[^\]]*?) name="[^\]"]+"#isS', $text ) ) { + $text = preg_replace( '#(\[build )#is', "\\1name=\"$build_name\" ", $text ); + } + + $text = preg_replace_callback( '#\[pre\](.*?)\[\/pre\]#isS', 'pre_replace', $text ); + $text = preg_replace_callback( '#\[nobb\](.*?)\[\/nobb\]#isS', 'pre_replace', $text ); + $text = preg_replace_callback( '#\[Random Skill(.*?)\]#is', 'random_skill_replace', $text ); + $text = preg_replace_callback( '#\[rand([^\]]*)\]#isS', 'rand_replace', $text ); + $text = preg_replace_callback( '#\[build=([^\]]*)\]\]?(\[/build\])?\r?\n?#isS', 'build_id_replace', $text ); + $text = preg_replace_callback( '#\[(([^]\r\n]+)(;)([^];\r\n]+))\]\r?\n?#isS', 'build_id_replace', $text ); + $text = preg_replace_callback( '#\[(.+)\]#isSU', 'skill_name_replace', $text ); + $text = preg_replace_callback( '#\[build ([^\]]*)\](.*?)\[/build\]\r?\n?#isS', 'build_replace_utility', $text ); + $text = preg_replace_callback( '#\[(\[?)skillset=(.*?)\]#isS', 'skillset_replace', $text ); + $text = preg_replace_callback( '#\[skill([^\]]*)\](.*?)\[/skill\]#isS', 'skill_replace_utility', $text ); + + $text = preg_replace( '@\[gwbbcode version\]@i', GWBBCODE_VERSION, $text ); + if ( preg_match( '@\[gwbbcode runtime\]@i', $text ) !== false ) { + $text = preg_replace( + '@\[gwbbcode runtime\]@i', + 'Runtime = ' . round( microtime( true ) - $start, 3 ) . ' seconds', + $text + ); + } + + return $text; +} + /*************************************************************************** * HELPER REPLACEMENT FUNCTIONS @@ -2332,3 +2380,86 @@ function bin_to_template( $bin ) { return $ret; } + + +/*************************************************************************** + * UTILITY-FRAMEWORK MODE REPLACEMENT FUNCTIONS + * + * When $wgPvxCodeUtility (and $wgEnableUtilityFramework) are on, [build] and + * [skill] bbcode is rendered as tags that the + * Fandom\GameUtility\ParserTagHookHandler turns into a placeholder for the + * @fandom-utility/pvx-code Utility Framework package. + ***************************************************************************/ + +/** + * Builds a self-closing tag from the given + * attribute map. Empty / null / false values are dropped so attributes never + * render as ` foo=""`. Values are HTML-attribute-escaped. + * + * @param array $attrs + * @return string + */ +function gwbbcode_utility_tag( array $attrs ): string { + $rendered = ''; + foreach ( $attrs as $name => $value ) { + if ( $value === null || $value === false || $value === '' ) { + continue; + } + $rendered .= ' ' . $name . '="' . htmlspecialchars( (string)$value, ENT_QUOTES, 'UTF-8' ) . '"'; + } + return ''; +} + +/** + * Utility-mode replacement for [build]...[/build]. Emits a single + * tag. + * + * @param array $reg preg match: [ full, attribute string, skills body ] + * @return string + */ +function build_replace_utility( $reg ): string { + [ , $att, $skills_body ] = $reg; + $att = html_safe_decode( $att ); + + $prof = gws_build_profession( $att ); + $primary = $prof !== false ? $prof['professions'] : ''; + + // Preserve user-typed short forms ("hea=8 smi=2 ..."). attribute_list_raw + // canonicalises to long names; convert back via gws_attribute_name. + $attrPairs = []; + foreach ( attribute_list_raw( $att ) as $longName => $value ) { + $short = gws_attribute_name( $longName ); + if ( $short !== false ) { + $attrPairs[] = $short . '=' . $value; + } + } + + $skills = []; + if ( preg_match_all( '#\[skill[^\]]*\](.*?)\[/skill\]#isS', $skills_body, $matches ) ) { + foreach ( $matches[1] as $name ) { + $skills[] = html_safe_decode( $name ); + } + } + + return gwbbcode_utility_tag( [ + 'primary' => $primary, + 'attributes' => implode( ' ', $attrPairs ), + 'skills' => implode( '|', $skills ), + 'name' => gws_build_name( $att ), + ] ); +} + +/** + * Utility-mode replacement for [skill]...[/skill]. Per-skill bbcode attributes + * (noicon, show, attribute overrides) are intentionally dropped — the utility + * re-derives them from the surrounding build context. + * + * @param array $reg preg match: [ full, attribute string, skill name ] + * @return string + */ +function skill_replace_utility( $reg ): string { + [ , , $name ] = $reg; + $name = html_safe_decode( $name ); + + return gwbbcode_utility_tag( [ 'skill' => $name ] ); +}