diff options
-rw-r--r-- | phpBB/config/default/container/services_text_formatter.yml | 5 | ||||
-rw-r--r-- | phpBB/phpbb/textformatter/s9e/bbcode_merger.php | 180 | ||||
-rw-r--r-- | tests/text_formatter/s9e/bbcode_merger_test.php | 280 |
3 files changed, 465 insertions, 0 deletions
diff --git a/phpBB/config/default/container/services_text_formatter.yml b/phpBB/config/default/container/services_text_formatter.yml index a9f2efdb16..74624ea4e4 100644 --- a/phpBB/config/default/container/services_text_formatter.yml +++ b/phpBB/config/default/container/services_text_formatter.yml @@ -26,6 +26,11 @@ services: text_formatter.utils: alias: text_formatter.s9e.utils + text_formatter.s9e.bbcode_merger: + class: phpbb\textformatter\s9e\bbcode_merger + arguments: + - '@text_formatter.s9e.factory' + text_formatter.s9e.factory: class: phpbb\textformatter\s9e\factory arguments: diff --git a/phpBB/phpbb/textformatter/s9e/bbcode_merger.php b/phpBB/phpbb/textformatter/s9e/bbcode_merger.php new file mode 100644 index 0000000000..72b1473751 --- /dev/null +++ b/phpBB/phpbb/textformatter/s9e/bbcode_merger.php @@ -0,0 +1,180 @@ +<?php +/** +* +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +namespace phpbb\textformatter\s9e; + +use phpbb\textformatter\s9e\factory; +use s9e\TextFormatter\Configurator\Helpers\TemplateHelper; +use s9e\TextFormatter\Configurator\Items\UnsafeTemplate; + +class bbcode_merger +{ + /** + * @var \s9e\TextFormatter\Configurator $configurator Configurator instance used to inspect BBCodes + */ + protected $configurator; + + /** + * @param \phpbb\textformatter\s9e\factory $factory + */ + public function __construct(factory $factory) + { + $this->configurator = $factory->get_configurator(); + } + + /** + * Merge two BBCode definitions + * + * All of the arrays contain a "usage" element and a "template" element + * + * @param array $without BBCode definition without an attribute + * @param array $with BBCode definition with an attribute + * @return array Merged definition + */ + public function merge_bbcodes(array $without, array $with) + { + $without = $this->create_bbcode($without); + $with = $this->create_bbcode($with); + + // Select the appropriate strategy for merging this BBCode + if ($this->is_content_bbcode($without, $with)) + { + $merged = $this->merge_content_bbcode($without, $with); + } + else + { + $merged = $this->merge_optional_bbcode($without, $with); + } + + $merged['template'] = $this->normalize_template($merged['template']); + + return $merged; + } + + /** + * Create a custom BBCode for inspection + * + * @param array $definition Original BBCode definition + * @return array Updated definition containing a BBCode object and a Tag + */ + protected function create_bbcode(array $definition) + { + $bbcode = $this->configurator->BBCodes->addCustom( + $definition['usage'], + new UnsafeTemplate($definition['template']) + ); + + $definition['bbcode'] = $bbcode; + $definition['tag'] = $this->configurator->tags[$bbcode->tagName]; + + return $definition; + } + + /** + * Indent given template for readability + * + * @param string $template + * @return string + */ + protected function indent_template($template) + { + $dom = TemplateHelper::loadTemplate($template); + $dom->formatOutput = true; + $template = TemplateHelper::saveTemplate($dom); + + // Remove the first level of indentation if the template starts with whitespace + if (preg_match('(^\\n +)', $template, $m)) + { + $template = str_replace($m[0], "\n", $template); + } + + return trim($template); + } + + /** + * Test whether the two definitions form a "content"-style BBCode + * + * Such BBCodes include the [URL] BBCode, which uses its text content as + * attribute if none is provided + * + * @param array $without BBCode definition without an attribute + * @param array $with BBCode definition with an attribute + * @return array Merged definition + */ + protected function is_content_bbcode(array $without, array $with) + { + // Test whether we find the same non-TEXT token between "]" and "[" in the usage + // as between ">" and "<" in the template + return (preg_match('(\\]\\s*(\\{(?!TEXT)[^}]+\\})\\s*\\[)', $without['usage'], $m) + && preg_match('(>[^<]*?' . preg_quote($m[1]) . '[^>]*?<)s', $without['template'])); + } + + /** + * Merge the two BBCode definitions of a "content"-style BBCode + * + * @param array $without BBCode definition without an attribute + * @param array $with BBCode definition with an attribute + * @return array Merged definition + */ + protected function merge_content_bbcode(array $without, array $with) + { + // Convert [X={X}] into [X={X;useContent}] + $usage = preg_replace('(\\})', ';useContent}', $with['usage'], 1); + + // Use the template from the definition that uses an attribute + $template = $with['tag']->template; + + return ['usage' => $usage, 'template' => $template]; + } + + /** + * Merge the two BBCode definitions of a BBCode with an optional argument + * + * Such BBCodes include the [QUOTE] BBCode, which takes an optional argument + * but otherwise does not behave differently + * + * @param array $without BBCode definition without an attribute + * @param array $with BBCode definition with an attribute + * @return array Merged definition + */ + protected function merge_optional_bbcode(array $without, array $with) + { + // Convert [X={X}] into [X={X?}] + $usage = preg_replace('(\\})', '?}', $with['usage'], 1); + + // Build a template for both versions + $template = '<xsl:choose><xsl:when test="@' . $with['bbcode']->defaultAttribute . '">' . $with['tag']->template . '</xsl:when><xsl:otherwise>' . $without['tag']->template . '</xsl:otherwise></xsl:choose>'; + + return ['usage' => $usage, 'template' => $template]; + } + + /** + * Normalize a template + * + * @param string $template + * @return string + */ + protected function normalize_template($template) + { + // Normalize the template to simplify it + $template = $this->configurator->templateNormalizer->normalizeTemplate($template); + + // Convert xsl:value-of elements back to {L_} tokens where applicable + $template = preg_replace('(<xsl:value-of select="\\$(L_\\w+)"/>)', '{$1}', $template); + + // Beautify the template + $template = $this->indent_template($template); + + return $template; + } +} diff --git a/tests/text_formatter/s9e/bbcode_merger_test.php b/tests/text_formatter/s9e/bbcode_merger_test.php new file mode 100644 index 0000000000..815539056b --- /dev/null +++ b/tests/text_formatter/s9e/bbcode_merger_test.php @@ -0,0 +1,280 @@ +<?php +/** +* +* This file is part of the phpBB Forum Software package. +* +* @copyright (c) phpBB Limited <https://www.phpbb.com> +* @license GNU General Public License, version 2 (GPL-2.0) +* +* For full copyright and license information, please see +* the docs/CREDITS.txt file. +* +*/ + +class phpbb_textformatter_s9e_bbcode_merger_test extends phpbb_test_case +{ + /** + * @dataProvider get_merge_bbcodes_tests + */ + public function test_merge_bbcodes($usage_without, $template_without, $usage_with, $template_with, $expected_usage, $expected_template) + { + $container = $this->get_test_case_helpers()->set_s9e_services(); + $factory = $container->get('text_formatter.s9e.factory'); + $bbcode_merger = new \phpbb\textformatter\s9e\bbcode_merger($factory); + + $without = ['usage' => $usage_without, 'template' => $template_without]; + $with = ['usage' => $usage_with, 'template' => $template_with]; + $merged = $bbcode_merger->merge_bbcodes($without, $with); + + // Normalize the expected template's whitespace to match the default indentation + $expected_template = str_replace("\n\t\t\t\t", "\n", $expected_template); + $expected_template = str_replace("\t", ' ', $expected_template); + + $this->assertSame($expected_usage, $merged['usage']); + $this->assertSame($expected_template, $merged['template']); + } + + public function get_merge_bbcodes_tests() + { + return [ + [ + '[x]{TEXT}[/x]', + '<b>{TEXT}</b>', + + '[x={TEXT1}]{TEXT}[/x]', + '<b title="{TEXT1}">{TEXT}</b>', + + '[x={TEXT1?}]{TEXT}[/x]', + '<b> + <xsl:if test="@x"> + <xsl:attribute name="title"> + <xsl:value-of select="@x"/> + </xsl:attribute> + </xsl:if> + <xsl:apply-templates/> + </b>' + ], + [ + // The tokens' numbering differs between versions + '[x]{TEXT}[/x]', + '<b>{TEXT}</b>', + + '[x={TEXT1}]{TEXT2}[/x]', + '<b title="{TEXT1}">{TEXT2}</b>', + + '[x={TEXT1?}]{TEXT2}[/x]', + '<b> + <xsl:if test="@x"> + <xsl:attribute name="title"> + <xsl:value-of select="@x"/> + </xsl:attribute> + </xsl:if> + <xsl:apply-templates/> + </b>' + ], + [ + '[x]{URL}[/x]', + '<a href="{URL}">{URL}</a>', + + '[x={URL}]{TEXT}[/x]', + '<a href="{URL}">{TEXT}</a>', + + '[x={URL;useContent}]{TEXT}[/x]', + '<a href="{@x}"> + <xsl:apply-templates/> + </a>' + ], + [ + '[x]{URL}[/x]', + '<a href="{URL}">{L_GO_TO}: {URL}</a>', + + '[x={URL}]{TEXT}[/x]', + '<a href="{URL}">{L_GO_TO}: {TEXT}</a>', + + '[x={URL;useContent}]{TEXT}[/x]', + '<a href="{@x}">{L_GO_TO}: <xsl:apply-templates/></a>' + ], + [ + // Test that unsafe BBCodes can still be merged + '[script]{TEXT}[/script]', + '<script>{TEXT}</script>', + + '[script={TEXT1}]{TEXT2}[/script]', + '<script type="{TEXT1}">{TEXT2}</script>', + + '[script={TEXT1?}]{TEXT2}[/script]', + '<script> + <xsl:if test="@script"> + <xsl:attribute name="type"> + <xsl:value-of select="@script"/> + </xsl:attribute> + </xsl:if> + <xsl:apply-templates/> + </script>' + ], + [ + // https://www.phpbb.com/community/viewtopic.php?p=14848281#p14848281 + '[note]{TEXT}[/note]', + '<span class="prime_bbcode_note_spur" onmouseover="show_note(this);" onmouseout="hide_note(this);" onclick="lock_note(this);"></span><span class="prime_bbcode_note">{TEXT}</span>', + + '[note={TEXT1}]{TEXT2}[/note]', + '<span class="prime_bbcode_note_text" onmouseover="show_note(this);" onmouseout="hide_note(this);" onclick="lock_note(this);">{TEXT1}</span><span class="prime_bbcode_note_spur" onmouseover="show_note(this);" onmouseout="hide_note(this);" onclick="lock_note(this);"></span><span class="prime_bbcode_note">{TEXT2}</span>', + + '[note={TEXT1?}]{TEXT2}[/note]', + '<xsl:if test="@note"> + <span class="prime_bbcode_note_text" onmouseover="show_note(this);" onmouseout="hide_note(this);" onclick="lock_note(this);"> + <xsl:value-of select="@note"/> + </span> + </xsl:if> + <span class="prime_bbcode_note_spur" onmouseover="show_note(this);" onmouseout="hide_note(this);" onclick="lock_note(this);"/> + <span class="prime_bbcode_note"> + <xsl:apply-templates/> + </span>' + ], + [ + // https://www.phpbb.com/community/viewtopic.php?p=14768441#p14768441 + '[MI]{TEXT}[/MI]', + '<span style="color:red">MI:</span> <span style="color:#f6efe2">{TEXT}</span>', + + '[MI={TEXT2}]{TEXT1}[/MI]', + '<span style="color:red">MI for: "{TEXT2}":</span> <span style="color:#f6efe2">{TEXT1}</span>', + + '[MI={TEXT2?}]{TEXT1}[/MI]', + '<span style="color:red">MI<xsl:if test="@mi"> for: "<xsl:value-of select="@mi"/>"</xsl:if>:</span> + <xsl:text> </xsl:text> + <span style="color:#f6efe2"> + <xsl:apply-templates/> + </span>' + ], + [ + // https://www.phpbb.com/community/viewtopic.php?p=14700506#p14700506 + '[spoiler]{TEXT}[/spoiler]', + '<span class="spoiler"> {TEXT}</span>', + + '[spoiler={TEXT1}]{TEXT2}[/spoiler]', + '<div class="spoiler"><small> {TEXT1}</small>{TEXT2}</div>', + + '[spoiler={TEXT1?}]{TEXT2}[/spoiler]', + '<xsl:choose> + <xsl:when test="@spoiler"> + <div class="spoiler"> + <small> + <xsl:text> </xsl:text> + <xsl:value-of select="@spoiler"/> + </small> + <xsl:apply-templates/> + </div> + </xsl:when> + <xsl:otherwise> + <span class="spoiler"> + <xsl:text> </xsl:text> + <xsl:apply-templates/> + </span> + </xsl:otherwise> + </xsl:choose>' + ], + [ + // https://www.phpbb.com/community/viewtopic.php?p=14859676#p14859676 + '[AE]{TEXT}[/AE]', + '<table width="100%" border="1"> + <tr><td width="100%" align="center"> + <table width="100%" border="0"> + <tr> + <td width="100%" bgcolor="#E1E4F2"> + <table width="100%" border="0" bgcolor="#F5F5FF"> + <tr> + <td width="1%" bgcolor="#000000" nowrap align="left"> + <font color="#FFFFFF" face="Arial"><font size="1"><b> ACTIVE EFFECTS & CONDITIONS </b></font></font></td> + <td width="99%"> </td> + </tr> + <tr> + <td width="100%" bgcolor="#FFE5BA" colspan="2"> + <table width="100%" cellpadding="2"> + <tr> + <td width="100%" align="left" valign="top"> + {TEXT} + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td></tr> + </table> + <p> </p>', + + '[AE={TEXT1}]{TEXT2}[/AE]', + '<table width="100%" border="1"> + <tr><td width="100%" align="center"> + <table width="100%" border="0"> + <tr> + <td width="100%" bgcolor="#E1E4F2"> + <table width="100%" border="0" bgcolor="#F5F5FF"> + <tr> + <td width="1%" bgcolor="#000000" nowrap align="left"> + <font color="#FFFFFF" face="Arial"><font size="1"><b> {TEXT1} </b></font></font></td> + <td width="99%"> </td> + </tr> + <tr> + <td width="100%" bgcolor="#FFE5BA" colspan="2"> + <table width="100%" cellpadding="2"> + <tr> + <td width="100%" align="left" valign="top"> + {TEXT2} + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td></tr> + </table> + <p> </p>', + + '[AE={TEXT1?}]{TEXT2}[/AE]', + '<table width="100%" border="1"> + <tr> + <td width="100%" align="center"> + <table width="100%" border="0"> + <tr> + <td width="100%" bgcolor="#E1E4F2"> + <table width="100%" border="0" bgcolor="#F5F5FF"> + <tr> + <td width="1%" bgcolor="#000000" nowrap="nowrap" align="left"> + <font color="#FFFFFF" face="Arial"> + <font size="1"> + <b> <xsl:choose><xsl:when test="@ae"><xsl:text> </xsl:text><xsl:value-of select="@ae"/></xsl:when><xsl:otherwise>ACTIVE EFFECTS & CONDITIONS</xsl:otherwise></xsl:choose> </b> + </font> + </font> + </td> + <td width="99%"> </td> + </tr> + <tr> + <td width="100%" bgcolor="#FFE5BA" colspan="2"> + <table width="100%" cellpadding="2"> + <tr> + <td width="100%" align="left" valign="top"> + <xsl:apply-templates/> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> + </table> + <p> </p>' + ], + ]; + } +} |