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 ] );
+}