Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
35 / 35 |
|
100.00% |
9 / 9 |
CRAP | |
100.00% |
1 / 1 |
bbcode_merger | |
100.00% |
35 / 35 |
|
100.00% |
9 / 9 |
13 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
merge_bbcodes | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
create_bbcode | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
indent_template | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
is_content_bbcode | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
is_optional_bbcode | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
merge_content_bbcode | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
merge_optional_bbcode | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
normalize_template | |
100.00% |
4 / 4 |
|
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 | |
14 | namespace phpbb\textformatter\s9e; |
15 | |
16 | use phpbb\textformatter\s9e\factory; |
17 | use s9e\TextFormatter\Configurator\Helpers\TemplateLoader; |
18 | use s9e\TextFormatter\Configurator\Items\UnsafeTemplate; |
19 | |
20 | class 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 | } |