Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
bbcode_merger
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
9 / 9
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 merge_bbcodes
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 create_bbcode
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 indent_template
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 is_content_bbcode
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 is_optional_bbcode
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 merge_content_bbcode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 merge_optional_bbcode
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 normalize_template
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14namespace phpbb\textformatter\s9e;
15
16use phpbb\textformatter\s9e\factory;
17use s9e\TextFormatter\Configurator\Helpers\TemplateLoader;
18use s9e\TextFormatter\Configurator\Items\UnsafeTemplate;
19
20class bbcode_merger
21{
22    /**
23    * @var \s9e\TextFormatter\Configurator $configurator Configurator instance used to inspect BBCodes
24    */
25    protected $configurator;
26
27    /**
28    * @param factory $factory
29    */
30    public function __construct(factory $factory)
31    {
32        $this->configurator = $factory->get_configurator();
33    }
34
35    /**
36    * Merge two BBCode definitions
37    *
38    * All of the arrays contain a "usage" element and a "template" element
39    *
40    * @throws \InvalidArgumentException if a definition cannot be interpreted
41    * @throws \RuntimeException if something unexpected occurs
42    *
43    * @param  array $without BBCode definition without an attribute
44    * @param  array $with    BBCode definition with an attribute
45    * @return array          Merged definition
46    */
47    public function merge_bbcodes(array $without, array $with)
48    {
49        $without = $this->create_bbcode($without);
50        $with    = $this->create_bbcode($with);
51
52        // Select the appropriate strategy for merging this BBCode
53        if (!$this->is_optional_bbcode($without, $with) && $this->is_content_bbcode($without, $with))
54        {
55            $merged = $this->merge_content_bbcode($without, $with);
56        }
57        else
58        {
59            $merged = $this->merge_optional_bbcode($without, $with);
60        }
61
62        $merged['template'] = $this->normalize_template($merged['template']);
63
64        return $merged;
65    }
66
67    /**
68    * Create a custom BBCode for inspection
69    *
70    * @param  array $definition Original BBCode definition
71    * @return array             Updated definition containing a BBCode object and a Tag
72    */
73    protected function create_bbcode(array $definition)
74    {
75        $bbcode = $this->configurator->BBCodes->addCustom(
76            $definition['usage'],
77            new UnsafeTemplate($definition['template'])
78        );
79
80        $definition['bbcode'] = $bbcode;
81        $definition['tag']    = $this->configurator->tags[$bbcode->tagName];
82
83        return $definition;
84    }
85
86    /**
87    * Indent given template for readability
88    *
89    * @param  string $template
90    * @return string
91    */
92    protected function indent_template($template)
93    {
94        $dom = TemplateLoader::load($template);
95        $dom->formatOutput = true;
96        $template = TemplateLoader::save($dom);
97
98        // Remove the first level of indentation if the template starts with whitespace
99        if (preg_match('(^\\n +)', $template, $m))
100        {
101            $template = str_replace($m[0], "\n", $template);
102        }
103
104        return trim($template);
105    }
106
107    /**
108    * Test whether the two definitions form a "content"-style BBCode
109    *
110    * Such BBCodes include the [url] BBCode, which uses its text content as
111    * attribute if none is provided
112    *
113    * @param  array $without BBCode definition without an attribute
114    * @param  array $with    BBCode definition with an attribute
115    * @return bool
116    */
117    protected function is_content_bbcode(array $without, array $with)
118    {
119        // Test whether we find the same non-TEXT token between "]" and "[" in the usage
120        // as between ">" and "<" in the template
121        return (preg_match('(\\]\\s*(\\{(?!TEXT)[^}]+\\})\\s*\\[)', $without['usage'], $m)
122            && preg_match('(>[^<]*?' . preg_quote($m[1]) . '[^>]*?<)s', $without['template']));
123    }
124
125    /**
126    * Test whether the two definitions form BBCode with an optional attribute
127    *
128    * @param  array $without BBCode definition without an attribute
129    * @param  array $with    BBCode definition with an attribute
130    * @return bool
131    */
132    protected function is_optional_bbcode(array $without, array $with)
133    {
134        // Remove the default attribute from the definition
135        $with['usage'] = preg_replace('(=[^\\]]++)', '', $with['usage']);
136
137        // Test whether both definitions are the same, regardless of case
138        return strcasecmp($without['usage'], $with['usage']) === 0;
139    }
140
141    /**
142    * Merge the two BBCode definitions of a "content"-style BBCode
143    *
144    * @param  array $without BBCode definition without an attribute
145    * @param  array $with    BBCode definition with an attribute
146    * @return array          Merged definition
147    */
148    protected function merge_content_bbcode(array $without, array $with)
149    {
150        // Convert [x={X}] into [x={X;useContent}]
151        $usage = preg_replace('(\\})', ';useContent}', $with['usage'], 1);
152
153        // Use the template from the definition that uses an attribute
154        $template = $with['tag']->template;
155
156        return ['usage' => $usage, 'template' => $template];
157    }
158
159    /**
160    * Merge the two BBCode definitions of a BBCode with an optional argument
161    *
162    * Such BBCodes include the [quote] BBCode, which takes an optional argument
163    * but otherwise does not behave differently
164    *
165    * @param  array $without BBCode definition without an attribute
166    * @param  array $with    BBCode definition with an attribute
167    * @return array          Merged definition
168    */
169    protected function merge_optional_bbcode(array $without, array $with)
170    {
171        // Convert [X={X}] into [X={X?}]
172        $usage = preg_replace('(\\})', '?}', $with['usage'], 1);
173
174        // Build a template for both versions
175        $template = '<xsl:choose><xsl:when test="@' . $with['bbcode']->defaultAttribute . '">' . $with['tag']->template . '</xsl:when><xsl:otherwise>' . $without['tag']->template . '</xsl:otherwise></xsl:choose>';
176
177        return ['usage' => $usage, 'template' => $template];
178    }
179
180    /**
181    * Normalize a template
182    *
183    * @param  string $template
184    * @return string
185    */
186    protected function normalize_template($template)
187    {
188        // Normalize the template to simplify it
189        $template = $this->configurator->templateNormalizer->normalizeTemplate($template);
190
191        // Convert xsl:value-of elements back to {L_} tokens where applicable
192        $template = preg_replace('(<xsl:value-of select="\\$(L_\\w+)"/>)', '{$1}', $template);
193
194        // Beautify the template
195        $template = $this->indent_template($template);
196
197        return $template;
198    }
199}