aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoshyPHP <s9e.dev@gmail.com>2017-11-27 21:12:19 +0100
committerJoshyPHP <s9e.dev@gmail.com>2017-12-10 02:57:07 +0100
commit75e7e7b293c812645942427fd8156a160225d0d5 (patch)
tree6dfd6a0ceb2436b3ca6f650f980d554131b5334c
parentb9dce3fa65a508baa02bc66e86e68b931e0ba670 (diff)
downloadforums-75e7e7b293c812645942427fd8156a160225d0d5.tar
forums-75e7e7b293c812645942427fd8156a160225d0d5.tar.gz
forums-75e7e7b293c812645942427fd8156a160225d0d5.tar.bz2
forums-75e7e7b293c812645942427fd8156a160225d0d5.tar.xz
forums-75e7e7b293c812645942427fd8156a160225d0d5.zip
[ticket/15468] Add a service to merge duplicate BBCodes
PHPBB3-15468
-rw-r--r--phpBB/config/default/container/services_text_formatter.yml5
-rw-r--r--phpBB/phpbb/textformatter/s9e/bbcode_merger.php180
-rw-r--r--tests/text_formatter/s9e/bbcode_merger_test.php280
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>&nbsp;ACTIVE EFFECTS & CONDITIONS&nbsp;</b></font></font></td>
+ <td width="99%">&nbsp;</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>&nbsp;</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>&nbsp; {TEXT1}&nbsp;</b></font></font></td>
+ <td width="99%">&nbsp;</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>&nbsp;</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 &amp; 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>'
+ ],
+ ];
+ }
+}