Verzeichnisstruktur phpBB-3.3.15
- Veröffentlicht
- 28.08.2024
So funktioniert es
|
Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück |
Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis. |
|
(Beispiel Datei-Icons)
|
Auf das Icon klicken um den Quellcode anzuzeigen |
factory.php
001 <?php
002 /**
003 *
004 * This file is part of the phpBB Forum Software package.
005 *
006 * @copyright (c) phpBB Limited <https://www.phpbb.com>
007 * @license GNU General Public License, version 2 (GPL-2.0)
008 *
009 * For full copyright and license information, please see
010 * the docs/CREDITS.txt file.
011 *
012 */
013
014 namespace phpbb\textformatter\s9e;
015
016 use s9e\TextFormatter\Configurator;
017 use s9e\TextFormatter\Configurator\Items\AttributeFilters\RegexpFilter;
018 use s9e\TextFormatter\Configurator\Items\UnsafeTemplate;
019
020 /**
021 * Creates s9e\TextFormatter objects
022 */
023 class factory implements \phpbb\textformatter\cache_interface
024 {
025 /**
026 * @var \phpbb\textformatter\s9e\link_helper
027 */
028 protected $link_helper;
029
030 /**
031 * @var \phpbb\cache\driver\driver_interface
032 */
033 protected $cache;
034
035 /**
036 * @var string Path to the cache dir
037 */
038 protected $cache_dir;
039
040 /**
041 * @var string Cache key used for the parser
042 */
043 protected $cache_key_parser;
044
045 /**
046 * @var string Cache key used for the renderer
047 */
048 protected $cache_key_renderer;
049
050 /**
051 * @var \phpbb\config\config
052 */
053 protected $config;
054
055 /**
056 * @var array Custom tokens used in bbcode.html and their corresponding token from the definition
057 */
058 protected $custom_tokens = array(
059 'email' => array('{DESCRIPTION}' => '{TEXT}'),
060 'flash' => array('{WIDTH}' => '{NUMBER1}', '{HEIGHT}' => '{NUMBER2}'),
061 'img' => array('{URL}' => '{IMAGEURL}'),
062 'list' => array('{LIST_TYPE}' => '{HASHMAP}'),
063 'quote' => array('{USERNAME}' => '{TEXT1}'),
064 'size' => array('{SIZE}' => '{FONTSIZE}'),
065 'url' => array('{DESCRIPTION}' => '{TEXT}'),
066 );
067
068 /**
069 * @var \phpbb\textformatter\data_access
070 */
071 protected $data_access;
072
073 /**
074 * @var array Default BBCode definitions
075 */
076 protected $default_definitions = array(
077 'attachment' => '[ATTACHMENT index={NUMBER} filename={TEXT;useContent}]',
078 'b' => '[B]{TEXT}[/B]',
079 'code' => '[CODE lang={IDENTIFIER;optional}]{TEXT}[/CODE]',
080 'color' => '[COLOR={COLOR}]{TEXT}[/COLOR]',
081 'email' => '[EMAIL={EMAIL;useContent} subject={TEXT1;optional;postFilter=rawurlencode} body={TEXT2;optional;postFilter=rawurlencode}]{TEXT}[/EMAIL]',
082 'flash' => '[FLASH={NUMBER1},{NUMBER2} width={NUMBER1;postFilter=#flashwidth} height={NUMBER2;postFilter=#flashheight} url={URL;useContent} /]',
083 'i' => '[I]{TEXT}[/I]',
084 'img' => '[IMG src={IMAGEURL;useContent}]',
085 'list' => '[LIST type={HASHMAP=1:decimal,a:lower-alpha,A:upper-alpha,i:lower-roman,I:upper-roman;optional;postFilter=#simpletext} #createChild=LI]{TEXT}[/LIST]',
086 'li' => '[* $tagName=LI]{TEXT}[/*]',
087 'quote' =>
088 "[QUOTE
089 author={TEXT1;optional}
090 post_id={UINT;optional}
091 post_url={URL;optional;postFilter=#false}
092 msg_id={UINT;optional}
093 msg_url={URL;optional;postFilter=#false}
094 profile_url={URL;optional;postFilter=#false}
095 time={UINT;optional}
096 url={URL;optional}
097 user_id={UINT;optional}
098 author={PARSE=/^\\[url=(?'url'.*?)](?'author'.*)\\[\\/url]$/i}
099 author={PARSE=/^\\[url](?'author'(?'url'.*?))\\[\\/url]$/i}
100 author={PARSE=/(?'url'https?:\\/\\/[^[\\]]+)/i}
101 ]{TEXT2}[/QUOTE]",
102 'size' => '[SIZE={FONTSIZE}]{TEXT}[/SIZE]',
103 'u' => '[U]{TEXT}[/U]',
104 'url' => '[URL={URL;useContent} $forceLookahead=true]{TEXT}[/URL]',
105 );
106
107 /**
108 * @var array Default templates, taken from bbcode::bbcode_tpl()
109 */
110 protected $default_templates = array(
111 'b' => '<span style="font-weight: bold"><xsl:apply-templates/></span>',
112 'i' => '<span style="font-style: italic"><xsl:apply-templates/></span>',
113 'u' => '<span style="text-decoration: underline"><xsl:apply-templates/></span>',
114 'img' => '<img src="{IMAGEURL}" class="postimage" alt="{L_IMAGE}"/>',
115 'size' => '<span><xsl:attribute name="style"><xsl:text>font-size: </xsl:text><xsl:value-of select="substring(@size, 1, 4)"/><xsl:text>%; line-height: normal</xsl:text></xsl:attribute><xsl:apply-templates/></span>',
116 'color' => '<span style="color: {COLOR}"><xsl:apply-templates/></span>',
117 'email' => '<a>
118 <xsl:attribute name="href">
119 <xsl:text>mailto:</xsl:text>
120 <xsl:value-of select="@email"/>
121 <xsl:if test="@subject or @body">
122 <xsl:text>?</xsl:text>
123 <xsl:if test="@subject">subject=<xsl:value-of select="@subject"/></xsl:if>
124 <xsl:if test="@body"><xsl:if test="@subject">&</xsl:if>body=<xsl:value-of select="@body"/></xsl:if>
125 </xsl:if>
126 </xsl:attribute>
127 <xsl:apply-templates/>
128 </a>',
129 );
130
131 /**
132 * @var \phpbb\event\dispatcher_interface
133 */
134 protected $dispatcher;
135
136 /**
137 * @var \phpbb\log\log_interface
138 */
139 protected $log;
140
141 /**
142 * Constructor
143 *
144 * @param \phpbb\textformatter\data_access $data_access
145 * @param \phpbb\cache\driver\driver_interface $cache
146 * @param \phpbb\event\dispatcher_interface $dispatcher
147 * @param \phpbb\config\config $config
148 * @param \phpbb\textformatter\s9e\link_helper $link_helper
149 * @param \phpbb\log\log_interface $log
150 * @param string $cache_dir Path to the cache dir
151 * @param string $cache_key_parser Cache key used for the parser
152 * @param string $cache_key_renderer Cache key used for the renderer
153 */
154 public function __construct(\phpbb\textformatter\data_access $data_access, \phpbb\cache\driver\driver_interface $cache, \phpbb\event\dispatcher_interface $dispatcher, \phpbb\config\config $config, \phpbb\textformatter\s9e\link_helper $link_helper, \phpbb\log\log_interface $log, $cache_dir, $cache_key_parser, $cache_key_renderer)
155 {
156 $this->link_helper = $link_helper;
157 $this->cache = $cache;
158 $this->cache_dir = $cache_dir;
159 $this->cache_key_parser = $cache_key_parser;
160 $this->cache_key_renderer = $cache_key_renderer;
161 $this->config = $config;
162 $this->data_access = $data_access;
163 $this->dispatcher = $dispatcher;
164 $this->log = $log;
165 }
166
167 /**
168 * {@inheritdoc}
169 */
170 public function invalidate()
171 {
172 $this->regenerate();
173 }
174
175 /**
176 * {@inheritdoc}
177 *
178 * Will remove old renderers from the cache dir but won't touch the current renderer
179 */
180 public function tidy()
181 {
182 // Get the name of current renderer
183 $renderer_data = $this->cache->get($this->cache_key_renderer);
184 $renderer_file = ($renderer_data) ? $renderer_data['class'] . '.php' : null;
185
186 foreach (glob($this->cache_dir . 's9e_*') as $filename)
187 {
188 // Only remove the file if it's not the current renderer
189 if (!$renderer_file || substr($filename, -strlen($renderer_file)) !== $renderer_file)
190 {
191 unlink($filename);
192 }
193 }
194 }
195
196 /**
197 * Generate and return a new configured instance of s9e\TextFormatter\Configurator
198 *
199 * @return Configurator
200 */
201 public function get_configurator()
202 {
203 // Create a new Configurator
204 $configurator = new Configurator;
205
206 /**
207 * Modify the s9e\TextFormatter configurator before the default settings are set
208 *
209 * @event core.text_formatter_s9e_configure_before
210 * @var Configurator configurator Configurator instance
211 * @since 3.2.0-a1
212 */
213 $vars = array('configurator');
214 extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_before', compact($vars)));
215
216 // Reset the list of allowed schemes
217 foreach ($configurator->urlConfig->getAllowedSchemes() as $scheme)
218 {
219 $configurator->urlConfig->disallowScheme($scheme);
220 }
221 foreach (array_filter(explode(',', $this->config['allowed_schemes_links'])) as $scheme)
222 {
223 $configurator->urlConfig->allowScheme(trim($scheme));
224 }
225
226 // Convert newlines to br elements by default
227 $configurator->rootRules->enableAutoLineBreaks();
228
229 // Don't automatically ignore text in places where text is not allowed
230 $configurator->rulesGenerator->remove('IgnoreTextIfDisallowed');
231
232 // Don't remove comments and instead convert them to xsl:comment elements
233 $configurator->templateNormalizer->remove('RemoveComments');
234 $configurator->templateNormalizer->add('TransposeComments');
235
236 // Set the rendering engine and configure it to save to the cache dir
237 $configurator->rendering->engine = 'PHP';
238 $configurator->rendering->engine->cacheDir = $this->cache_dir;
239 $configurator->rendering->engine->defaultClassPrefix = 's9e_renderer_';
240 $configurator->rendering->engine->enableQuickRenderer = true;
241
242 // Create custom filters for BBCode tokens that are supported in phpBB but not in
243 // s9e\TextFormatter
244 $filter = new RegexpFilter('#^' . get_preg_expression('relative_url') . '$#Du');
245 $configurator->attributeFilters->add('#local_url', $filter);
246 $configurator->attributeFilters->add('#relative_url', $filter);
247
248 // INTTEXT regexp from acp_bbcodes
249 $filter = new RegexpFilter('!^([\p{L}\p{N}\-+,_. ]+)$!Du');
250 $configurator->attributeFilters->add('#inttext', $filter);
251
252 // Create custom filters for Flash restrictions, which use the same values as the image
253 // restrictions but have their own error message
254 $configurator->attributeFilters
255 ->add('#flashheight', __NAMESPACE__ . '\\parser::filter_flash_height')
256 ->addParameterByName('max_img_height')
257 ->addParameterByName('logger');
258
259 $configurator->attributeFilters
260 ->add('#flashwidth', __NAMESPACE__ . '\\parser::filter_flash_width')
261 ->addParameterByName('max_img_width')
262 ->addParameterByName('logger');
263
264 // Create a custom filter for phpBB's per-mode font size limits
265 $configurator->attributeFilters
266 ->add('#fontsize', __NAMESPACE__ . '\\parser::filter_font_size')
267 ->addParameterByName('max_font_size')
268 ->addParameterByName('logger')
269 ->markAsSafeInCSS();
270
271 // Create a custom filter for image URLs
272 $configurator->attributeFilters
273 ->add('#imageurl', __NAMESPACE__ . '\\parser::filter_img_url')
274 ->addParameterByName('urlConfig')
275 ->addParameterByName('logger')
276 ->markAsSafeAsURL()
277 ->setJS('UrlFilter.filter');
278
279 // Add default BBCodes
280 foreach ($this->get_default_bbcodes($configurator) as $bbcode)
281 {
282 $this->add_bbcode($configurator, $bbcode['usage'], $bbcode['template']);
283 }
284 if (isset($configurator->tags['QUOTE']))
285 {
286 // Remove the nesting limit and let other services remove quotes at parsing time
287 $configurator->tags['QUOTE']->nestingLimit = PHP_INT_MAX;
288 }
289
290 // Modify the template to disable images/flash depending on user's settings
291 foreach (array('FLASH', 'IMG') as $name)
292 {
293 $tag = $configurator->tags[$name];
294 $tag->template = '<xsl:choose><xsl:when test="$S_VIEW' . $name . '">' . $tag->template . '</xsl:when><xsl:otherwise><xsl:apply-templates/></xsl:otherwise></xsl:choose>';
295 }
296
297 // Load custom BBCodes
298 foreach ($this->data_access->get_bbcodes() as $row)
299 {
300 // Insert the board's URL before {LOCAL_URL} tokens
301 $tpl = preg_replace_callback(
302 '#\\{LOCAL_URL\\d*\\}#',
303 function ($m)
304 {
305 return generate_board_url() . '/' . $m[0];
306 },
307 $row['bbcode_tpl']
308 );
309 $this->add_bbcode($configurator, $row['bbcode_match'], $tpl);
310 }
311
312 // Load smilies
313 foreach ($this->data_access->get_smilies() as $row)
314 {
315 $configurator->Emoticons->set(
316 $row['code'],
317 '<img class="smilies" src="{$T_SMILIES_PATH}/' . $this->escape_html_attribute($row['smiley_url']) . '" width="' . $row['smiley_width'] . '" height="' . $row['smiley_height'] . '" alt="{.}" title="' . $this->escape_html_attribute($row['emotion']) . '"/>'
318 );
319 }
320
321 if (isset($configurator->Emoticons))
322 {
323 // Force emoticons to be rendered as text if $S_VIEWSMILIES is not set
324 $configurator->Emoticons->notIfCondition = 'not($S_VIEWSMILIES)';
325
326 // Only parse emoticons at the beginning of the text or if they're preceded by any
327 // one of: a new line, a space, a dot, or a right square bracket
328 $configurator->Emoticons->notAfter = '[^\\n .\\]]';
329
330 // Ignore emoticons that are immediately followed by a "word" character
331 $configurator->Emoticons->notBefore = '\\w';
332 }
333
334 // Load the censored words
335 $censor = $this->data_access->get_censored_words();
336 if (!empty($censor))
337 {
338 // Use a namespaced tag to avoid collisions
339 $configurator->plugins->load('Censor', array('tagName' => 'censor:tag'));
340 foreach ($censor as $row)
341 {
342 $configurator->Censor->add($row['word'], $row['replacement']);
343 }
344 }
345
346 // Load the magic links plugins. We do that after BBCodes so that they use the same tags
347 $this->configure_autolink($configurator);
348
349 // Register some vars with a default value. Those should be set at runtime by whatever calls
350 // the parser
351 $configurator->registeredVars['max_font_size'] = 0;
352 $configurator->registeredVars['max_img_height'] = 0;
353 $configurator->registeredVars['max_img_width'] = 0;
354
355 // Load the Emoji plugin and modify its tag's template to obey viewsmilies
356 $tag = $configurator->Emoji->getTag();
357 $tag->template = '<xsl:choose>
358 <xsl:when test="@tseq">
359 <img alt="{.}" class="emoji" draggable="false" src="//cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/{@tseq}.svg"/>
360 </xsl:when>
361 <xsl:otherwise>
362 <img alt="{.}" class="emoji" draggable="false" src="//cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/{@seq}.svg"/>
363 </xsl:otherwise>
364 </xsl:choose>';
365 $tag->template = '<xsl:choose><xsl:when test="$S_VIEWSMILIES">' . str_replace('class="emoji"', 'class="emoji smilies"', $tag->template) . '</xsl:when><xsl:otherwise><xsl:value-of select="."/></xsl:otherwise></xsl:choose>';
366
367 /**
368 * Modify the s9e\TextFormatter configurator after the default settings are set
369 *
370 * @event core.text_formatter_s9e_configure_after
371 * @var Configurator configurator Configurator instance
372 * @since 3.2.0-a1
373 */
374 $vars = array('configurator');
375 extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_after', compact($vars)));
376
377 return $configurator;
378 }
379
380 /**
381 * Regenerate and cache a new parser and renderer
382 *
383 * @return array Associative array with at least two elements: "parser" and "renderer"
384 */
385 public function regenerate()
386 {
387 $configurator = $this->get_configurator();
388
389 // Get the censor helper and remove the Censor plugin if applicable
390 if (isset($configurator->Censor))
391 {
392 $censor = $configurator->Censor->getHelper();
393 unset($configurator->Censor);
394 unset($configurator->tags['censor:tag']);
395 }
396
397 $objects = $configurator->finalize();
398
399 /**
400 * Access the objects returned by finalize() before they are saved to cache
401 *
402 * @event core.text_formatter_s9e_configure_finalize
403 * @var array objects Array containing a "parser" object, a "renderer" object and optionally a "js" string
404 * @since 3.2.2-RC1
405 */
406 $vars = array('objects');
407 extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_finalize', compact($vars)));
408
409 $parser = $objects['parser'];
410 $renderer = $objects['renderer'];
411
412 // Cache the parser as-is
413 $this->cache->put($this->cache_key_parser, $parser);
414
415 // We need to cache the name of the renderer's generated class
416 $renderer_data = array('class' => get_class($renderer));
417 if (isset($censor))
418 {
419 $renderer_data['censor'] = $censor;
420 }
421 $this->cache->put($this->cache_key_renderer, $renderer_data);
422
423 return array('parser' => $parser, 'renderer' => $renderer);
424 }
425
426 /**
427 * Add a BBCode to given configurator
428 *
429 * @param Configurator $configurator
430 * @param string $usage
431 * @param string $template
432 * @return void
433 */
434 protected function add_bbcode(Configurator $configurator, $usage, $template)
435 {
436 try
437 {
438 $configurator->BBCodes->addCustom($usage, new UnsafeTemplate($template));
439 }
440 catch (\Exception $e)
441 {
442 $this->log->add('critical', null, null, 'LOG_BBCODE_CONFIGURATION_ERROR', false, [$usage, $e->getMessage()]);
443 }
444 }
445
446 /**
447 * Configure the Autolink / Autoemail plugins used to linkify text
448 *
449 * @param Configurator $configurator
450 * @return void
451 */
452 protected function configure_autolink(Configurator $configurator)
453 {
454 $configurator->plugins->load('Autoemail');
455 $configurator->plugins->load('Autolink', array('matchWww' => true));
456
457 // Add a tag filter that creates a tag that stores and replace the
458 // content of a link created by the Autolink plugin
459 $configurator->Autolink->getTag()->filterChain
460 ->add(array($this->link_helper, 'generate_link_text_tag'))
461 ->resetParameters()
462 ->addParameterByName('tag')
463 ->addParameterByName('parser');
464
465 // Create a tag that will be used to display the truncated text by
466 // replacing the original content with the content of the @text attribute
467 $tag = $configurator->tags->add('LINK_TEXT');
468 $tag->attributes->add('text');
469 $tag->template = '<xsl:value-of select="@text"/>';
470
471 $board_url = generate_board_url() . '/';
472 $tag->filterChain
473 ->add(array($this->link_helper, 'truncate_local_url'))
474 ->resetParameters()
475 ->addParameterByName('tag')
476 ->addParameterByValue($board_url);
477 $tag->filterChain
478 ->add(array($this->link_helper, 'truncate_local_url'))
479 ->resetParameters()
480 ->addParameterByName('tag')
481 ->addParameterByValue(preg_replace('(^\\w+:)', '', $board_url));
482 $tag->filterChain
483 ->add(array($this->link_helper, 'truncate_text'))
484 ->resetParameters()
485 ->addParameterByName('tag');
486 $tag->filterChain
487 ->add(array($this->link_helper, 'cleanup_tag'))
488 ->resetParameters()
489 ->addParameterByName('tag')
490 ->addParameterByName('parser');
491 }
492
493 /**
494 * Escape a literal to be used in an HTML attribute in an XSL template
495 *
496 * Escapes "HTML special chars" for obvious reasons and curly braces to avoid them
497 * being interpreted as an attribute value template
498 *
499 * @param string $value Original string
500 * @return string Escaped string
501 */
502 protected function escape_html_attribute($value)
503 {
504 return htmlspecialchars(strtr($value, ['{' => '{{', '}' => '}}']), ENT_COMPAT | ENT_XML1, 'UTF-8');
505 }
506
507 /**
508 * Return the default BBCodes configuration
509 *
510 * @return array 2D array. Each element has a 'usage' key, a 'template' key, and an optional 'options' key
511 */
512 protected function get_default_bbcodes($configurator)
513 {
514 // For each BBCode, build an associative array matching style_ids to their template
515 $templates = array();
516 foreach ($this->data_access->get_styles_templates() as $style_id => $data)
517 {
518 foreach ($this->extract_templates($data['template']) as $bbcode_name => $template)
519 {
520 $templates[$bbcode_name][$style_id] = $template;
521 }
522
523 // Add default templates wherever missing, or for BBCodes that were not specified in
524 // this template's bitfield. For instance, prosilver has a custom template for b but its
525 // bitfield does not enable it so the default template is used instead
526 foreach ($this->default_templates as $bbcode_name => $template)
527 {
528 if (!isset($templates[$bbcode_name][$style_id]) || !in_array($bbcode_name, $data['bbcodes'], true))
529 {
530 $templates[$bbcode_name][$style_id] = $template;
531 }
532 }
533 }
534
535 // Replace custom tokens and normalize templates
536 foreach ($templates as $bbcode_name => $style_templates)
537 {
538 foreach ($style_templates as $i => $template)
539 {
540 if (isset($this->custom_tokens[$bbcode_name]))
541 {
542 $template = strtr($template, $this->custom_tokens[$bbcode_name]);
543 }
544
545 $templates[$bbcode_name][$i] = $configurator->templateNormalizer->normalizeTemplate($template);
546 }
547 }
548
549 $bbcodes = array();
550 foreach ($this->default_definitions as $bbcode_name => $usage)
551 {
552 $bbcodes[$bbcode_name] = array(
553 'usage' => $usage,
554 'template' => $this->merge_templates($templates[$bbcode_name]),
555 );
556 }
557
558 return $bbcodes;
559 }
560
561 /**
562 * Extract and recompose individual BBCode templates from a style's template file
563 *
564 * @param string $template Style template (bbcode.html)
565 * @return array Associative array matching BBCode names to their template
566 */
567 protected function extract_templates($template)
568 {
569 // Capture the template fragments
570 // Allow either phpBB template or the Twig syntax
571 preg_match_all('#<!-- BEGIN (.*?) -->(.*?)<!-- END .*? -->#s', $template, $matches, PREG_SET_ORDER) ?:
572 preg_match_all('#{% for (.*?) in .*? %}(.*?){% endfor %}#s', $template, $matches, PREG_SET_ORDER);
573
574 $fragments = array();
575 foreach ($matches as $match)
576 {
577 // Normalize the whitespace
578 $fragment = preg_replace('#>\\n\\t*<#', '><', trim($match[2]));
579
580 $fragments[$match[1]] = $fragment;
581 }
582
583 // Automatically recompose templates split between *_open and *_close
584 foreach ($fragments as $fragment_name => $fragment)
585 {
586 if (preg_match('#^(\\w+)_close$#', $fragment_name, $match))
587 {
588 $bbcode_name = $match[1];
589
590 if (isset($fragments[$bbcode_name . '_open']))
591 {
592 $templates[$bbcode_name] = $fragments[$bbcode_name . '_open'] . '<xsl:apply-templates/>' . $fragment;
593 }
594 }
595 }
596
597 // Manually recompose and overwrite irregular templates
598 $templates['list'] =
599 '<xsl:choose>
600 <xsl:when test="not(@type)">
601 ' . $fragments['ulist_open_default'] . '<xsl:apply-templates/>' . $fragments['ulist_close'] . '
602 </xsl:when>
603 <xsl:when test="contains(\'upperlowerdecim\',substring(@type,1,5))">
604 ' . $fragments['olist_open'] . '<xsl:apply-templates/>' . $fragments['olist_close'] . '
605 </xsl:when>
606 <xsl:otherwise>
607 ' . $fragments['ulist_open'] . '<xsl:apply-templates/>' . $fragments['ulist_close'] . '
608 </xsl:otherwise>
609 </xsl:choose>';
610
611 $templates['li'] = $fragments['listitem'] . '<xsl:apply-templates/>' . $fragments['listitem_close'];
612
613 // Replace the regular quote template with the extended quote template if available
614 if (isset($fragments['quote_extended']))
615 {
616 $templates['quote'] = $fragments['quote_extended'];
617 }
618
619 // The [attachment] BBCode uses the inline_attachment template to output a comment that
620 // is post-processed by parse_attachments()
621 $templates['attachment'] = $fragments['inline_attachment_open'] . '<xsl:comment> ia<xsl:value-of select="@index"/> </xsl:comment><xsl:value-of select="@filename"/><xsl:comment> ia<xsl:value-of select="@index"/> </xsl:comment>' . $fragments['inline_attachment_close'];
622
623 // Add fragments as templates
624 foreach ($fragments as $fragment_name => $fragment)
625 {
626 if (preg_match('#^\\w+$#', $fragment_name))
627 {
628 $templates[$fragment_name] = $fragment;
629 }
630 }
631
632 // Keep only templates that are named after an existing BBCode
633 $templates = array_intersect_key($templates, $this->default_definitions);
634
635 return $templates;
636 }
637
638 /**
639 * Merge the templates from any number of styles into one BBCode template
640 *
641 * When multiple templates are available for the same BBCode (because of multiple styles) we
642 * merge them into a single template that uses an xsl:choose construct that determines which
643 * style to use at rendering time.
644 *
645 * @param array $style_templates Associative array matching style_ids to their template
646 * @return string
647 */
648 protected function merge_templates(array $style_templates)
649 {
650 // Return the template as-is if there's only one style or all styles share the same template
651 if (count(array_unique($style_templates)) === 1)
652 {
653 return end($style_templates);
654 }
655
656 // Group identical templates together
657 $grouped_templates = array();
658 foreach ($style_templates as $style_id => $style_template)
659 {
660 $grouped_templates[$style_template][] = '$STYLE_ID=' . $style_id;
661 }
662
663 // Sort templates by frequency descending
664 $templates_cnt = array_map('sizeof', $grouped_templates);
665 array_multisort($grouped_templates, $templates_cnt);
666
667 // Remove the most frequent template from the list; It becomes the default
668 reset($grouped_templates);
669 $default_template = key($grouped_templates);
670 unset($grouped_templates[$default_template]);
671
672 // Build an xsl:choose switch
673 $template = '<xsl:choose>';
674 foreach ($grouped_templates as $style_template => $exprs)
675 {
676 $template .= '<xsl:when test="' . implode(' or ', $exprs) . '">' . $style_template . '</xsl:when>';
677 }
678 $template .= '<xsl:otherwise>' . $default_template . '</xsl:otherwise></xsl:choose>';
679
680 return $template;
681 }
682 }
683