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.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

(Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

factory.php

Zuletzt modifiziert: 02.04.2025, 15:02 - Dateigröße: 23.03 KiB


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">&amp;</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