Verzeichnisstruktur phpBB-3.2.0


Veröffentlicht
06.01.2017

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: 09.10.2024, 12:54 - Dateigröße: 20.69 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={TEXT;optional;postFilter=rawurlencode} body={TEXT;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                  profile_url={URL;optional;postFilter=#false}
093                  time={UINT;optional}
094                  url={URL;optional}
095                  user_id={UINT;optional}
096                  author={PARSE=/^\\[url=(?'url'.*?)](?'author'.*)\\[\\/url]$/i}
097                  author={PARSE=/^\\[url](?'author'(?'url'.*?))\\[\\/url]$/i}
098                  author={PARSE=/(?'url'https?:\\/\\/[^[\\]]+)/i}
099              ]{TEXT2}[/QUOTE]",
100          'size'  => '[SIZE={FONTSIZE}]{TEXT}[/SIZE]',
101          'u'     => '[U]{TEXT}[/U]',
102          'url'   => '[URL={URL;useContent} $forceLookahead=true]{TEXT}[/URL]',
103      );
104   
105      /**
106      * @var array Default templates, taken from bbcode::bbcode_tpl()
107      */
108      protected $default_templates = array(
109          'b'     => '<span style="font-weight: bold"><xsl:apply-templates/></span>',
110          'i'     => '<span style="font-style: italic"><xsl:apply-templates/></span>',
111          'u'     => '<span style="text-decoration: underline"><xsl:apply-templates/></span>',
112          'img'   => '<img src="{IMAGEURL}" class="postimage" alt="{L_IMAGE}"/>',
113          'size'  => '<span style="font-size: {FONTSIZE}%; line-height: normal"><xsl:apply-templates/></span>',
114          'color' => '<span style="color: {COLOR}"><xsl:apply-templates/></span>',
115          'email' => '<a>
116              <xsl:attribute name="href">
117                  <xsl:text>mailto:</xsl:text>
118                  <xsl:value-of select="@email"/>
119                  <xsl:if test="@subject or @body">
120                      <xsl:text>?</xsl:text>
121                      <xsl:if test="@subject">subject=<xsl:value-of select="@subject"/></xsl:if>
122                      <xsl:if test="@body"><xsl:if test="@subject">&amp;</xsl:if>body=<xsl:value-of select="@body"/></xsl:if>
123                  </xsl:if>
124              </xsl:attribute>
125              <xsl:apply-templates/>
126          </a>',
127      );
128   
129      /**
130      * @var \phpbb\event\dispatcher_interface
131      */
132      protected $dispatcher;
133   
134      /**
135      * Constructor
136      *
137      * @param \phpbb\textformatter\data_access $data_access
138      * @param \phpbb\cache\driver\driver_interface $cache
139      * @param \phpbb\event\dispatcher_interface $dispatcher
140      * @param \phpbb\config\config $config
141      * @param \phpbb\textformatter\s9e\link_helper $link_helper
142      * @param string $cache_dir          Path to the cache dir
143      * @param string $cache_key_parser   Cache key used for the parser
144      * @param string $cache_key_renderer Cache key used for the renderer
145      */
146      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, $cache_dir, $cache_key_parser, $cache_key_renderer)
147      {
148          $this->link_helper = $link_helper;
149          $this->cache = $cache;
150          $this->cache_dir = $cache_dir;
151          $this->cache_key_parser = $cache_key_parser;
152          $this->cache_key_renderer = $cache_key_renderer;
153          $this->config = $config;
154          $this->data_access = $data_access;
155          $this->dispatcher = $dispatcher;
156      }
157   
158      /**
159      * {@inheritdoc}
160      */
161      public function invalidate()
162      {
163          $this->regenerate();
164      }
165   
166      /**
167      * {@inheritdoc}
168      *
169      * Will remove old renderers from the cache dir but won't touch the current renderer
170      */
171      public function tidy()
172      {
173          // Get the name of current renderer
174          $renderer_data = $this->cache->get($this->cache_key_renderer);
175          $renderer_file = ($renderer_data) ? $renderer_data['class'] . '.php' : null;
176   
177          foreach (glob($this->cache_dir . 's9e_*') as $filename)
178          {
179              // Only remove the file if it's not the current renderer
180              if (!$renderer_file || substr($filename, -strlen($renderer_file)) !== $renderer_file)
181              {
182                  unlink($filename);
183              }
184          }
185      }
186   
187      /**
188      * Generate and return a new configured instance of s9e\TextFormatter\Configurator
189      *
190      * @return Configurator
191      */
192      public function get_configurator()
193      {
194          // Create a new Configurator
195          $configurator = new Configurator;
196   
197          /**
198          * Modify the s9e\TextFormatter configurator before the default settings are set
199          *
200          * @event core.text_formatter_s9e_configure_before
201          * @var \s9e\TextFormatter\Configurator configurator Configurator instance
202          * @since 3.2.0-a1
203          */
204          $vars = array('configurator');
205          extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_before', compact($vars)));
206   
207          // Reset the list of allowed schemes
208          foreach ($configurator->urlConfig->getAllowedSchemes() as $scheme)
209          {
210              $configurator->urlConfig->disallowScheme($scheme);
211          }
212          foreach (explode(',', $this->config['allowed_schemes_links']) as $scheme)
213          {
214              $configurator->urlConfig->allowScheme(trim($scheme));
215          }
216   
217          // Convert newlines to br elements by default
218          $configurator->rootRules->enableAutoLineBreaks();
219   
220          // Don't automatically ignore text in places where text is not allowed
221          $configurator->rulesGenerator->remove('IgnoreTextIfDisallowed');
222   
223          // Don't remove comments and instead convert them to xsl:comment elements
224          $configurator->templateNormalizer->remove('RemoveComments');
225          $configurator->templateNormalizer->add('TransposeComments');
226   
227          // Set the rendering engine and configure it to save to the cache dir
228          $configurator->rendering->engine = 'PHP';
229          $configurator->rendering->engine->cacheDir = $this->cache_dir;
230          $configurator->rendering->engine->defaultClassPrefix = 's9e_renderer_';
231          $configurator->rendering->engine->enableQuickRenderer = true;
232   
233          // Create custom filters for BBCode tokens that are supported in phpBB but not in
234          // s9e\TextFormatter
235          $filter = new RegexpFilter('#^' . get_preg_expression('relative_url') . '$#Du');
236          $configurator->attributeFilters->add('#local_url', $filter);
237          $configurator->attributeFilters->add('#relative_url', $filter);
238   
239          // INTTEXT regexp from acp_bbcodes
240          $filter = new RegexpFilter('!^([\p{L}\p{N}\-+,_. ]+)$!Du');
241          $configurator->attributeFilters->add('#inttext', $filter);
242   
243          // Create custom filters for Flash restrictions, which use the same values as the image
244          // restrictions but have their own error message
245          $configurator->attributeFilters
246              ->add('#flashheight', __NAMESPACE__ . '\\parser::filter_flash_height')
247              ->addParameterByName('max_img_height')
248              ->addParameterByName('logger');
249   
250          $configurator->attributeFilters
251              ->add('#flashwidth', __NAMESPACE__ . '\\parser::filter_flash_width')
252              ->addParameterByName('max_img_width')
253              ->addParameterByName('logger');
254   
255          // Create a custom filter for phpBB's per-mode font size limits
256          $configurator->attributeFilters
257              ->add('#fontsize', __NAMESPACE__ . '\\parser::filter_font_size')
258              ->addParameterByName('max_font_size')
259              ->addParameterByName('logger')
260              ->markAsSafeInCSS();
261   
262          // Create a custom filter for image URLs
263          $configurator->attributeFilters
264              ->add('#imageurl', __NAMESPACE__ . '\\parser::filter_img_url')
265              ->addParameterByName('urlConfig')
266              ->addParameterByName('logger')
267              ->addParameterByName('max_img_height')
268              ->addParameterByName('max_img_width')
269              ->markAsSafeAsURL();
270   
271          // Add default BBCodes
272          foreach ($this->get_default_bbcodes($configurator) as $bbcode)
273          {
274              $configurator->BBCodes->addCustom($bbcode['usage'], $bbcode['template']);
275          }
276   
277          // Modify the template to disable images/flash depending on user's settings
278          foreach (array('FLASH', 'IMG') as $name)
279          {
280              $tag = $configurator->tags[$name];
281              $tag->template = '<xsl:choose><xsl:when test="$S_VIEW' . $name . '">' . $tag->template . '</xsl:when><xsl:otherwise><xsl:apply-templates/></xsl:otherwise></xsl:choose>';
282          }
283   
284          // Load custom BBCodes
285          foreach ($this->data_access->get_bbcodes() as $row)
286          {
287              // Insert the board's URL before {LOCAL_URL} tokens
288              $tpl = preg_replace_callback(
289                  '#\\{LOCAL_URL\\d*\\}#',
290                  function ($m)
291                  {
292                      return generate_board_url() . '/' . $m[0];
293                  },
294                  $row['bbcode_tpl']
295              );
296   
297              try
298              {
299                  $configurator->BBCodes->addCustom($row['bbcode_match'], new UnsafeTemplate($tpl));
300              }
301              catch (\Exception $e)
302              {
303                  /**
304                  * @todo log an error?
305                  */
306              }
307          }
308   
309          // Load smilies
310          foreach ($this->data_access->get_smilies() as $row)
311          {
312              $configurator->Emoticons->set(
313                  $row['code'],
314                  '<img class="smilies" src="{$T_SMILIES_PATH}/' . htmlspecialchars($row['smiley_url']) . '" width="' . $row['smiley_width'] . '" height="' . $row['smiley_height'] . '" alt="{.}" title="' . htmlspecialchars($row['emotion']) . '"/>'
315              );
316          }
317   
318          if (isset($configurator->Emoticons))
319          {
320              // Force emoticons to be rendered as text if $S_VIEWSMILIES is not set
321              $configurator->Emoticons->notIfCondition = 'not($S_VIEWSMILIES)';
322   
323              // Only parse emoticons at the beginning of the text or if they're preceded by any
324              // one of: a new line, a space, a dot, or a right square bracket
325              $configurator->Emoticons->notAfter = '[^\\n .\\]]';
326          }
327   
328          // Load the censored words
329          $censor = $this->data_access->get_censored_words();
330          if (!empty($censor))
331          {
332              // Use a namespaced tag to avoid collisions
333              $configurator->plugins->load('Censor', array('tagName' => 'censor:tag'));
334              foreach ($censor as $row)
335              {
336                  // NOTE: words are stored as HTML, we need to decode them to plain text
337                  $configurator->Censor->add(htmlspecialchars_decode($row['word']),  htmlspecialchars_decode($row['replacement']));
338              }
339          }
340   
341          // Load the magic links plugins. We do that after BBCodes so that they use the same tags
342          $this->configure_autolink($configurator);
343   
344          // Register some vars with a default value. Those should be set at runtime by whatever calls
345          // the parser
346          $configurator->registeredVars['max_font_size'] = 0;
347          $configurator->registeredVars['max_img_height'] = 0;
348          $configurator->registeredVars['max_img_width'] = 0;
349   
350          // Load the Emoji plugin and modify its tag's template to obey viewsmilies
351          $configurator->Emoji->omitImageSize();
352          $configurator->Emoji->useSVG();
353          $tag = $configurator->Emoji->getTag();
354          $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>';
355   
356          /**
357          * Modify the s9e\TextFormatter configurator after the default settings are set
358          *
359          * @event core.text_formatter_s9e_configure_after
360          * @var \s9e\TextFormatter\Configurator configurator Configurator instance
361          * @since 3.2.0-a1
362          */
363          $vars = array('configurator');
364          extract($this->dispatcher->trigger_event('core.text_formatter_s9e_configure_after', compact($vars)));
365   
366          return $configurator;
367      }
368   
369      /**
370      * Regenerate and cache a new parser and renderer
371      *
372      * @return array Associative array with at least two elements: "parser" and "renderer"
373      */
374      public function regenerate()
375      {
376          $configurator = $this->get_configurator();
377   
378          // Get the censor helper and remove the Censor plugin if applicable
379          if (isset($configurator->Censor))
380          {
381              $censor = $configurator->Censor->getHelper();
382              unset($configurator->Censor);
383              unset($configurator->tags['censor:tag']);
384          }
385   
386          $objects  = $configurator->finalize();
387          $parser   = $objects['parser'];
388          $renderer = $objects['renderer'];
389   
390          // Cache the parser as-is
391          $this->cache->put($this->cache_key_parser, $parser);
392   
393          // We need to cache the name of the renderer's generated class
394          $renderer_data = array('class' => get_class($renderer));
395          if (isset($censor))
396          {
397              $renderer_data['censor'] = $censor;
398          }
399          $this->cache->put($this->cache_key_renderer, $renderer_data);
400   
401          return array('parser' => $parser, 'renderer' => $renderer);
402      }
403   
404      /**
405      * Configure the Autolink / Autoemail plugins used to linkify text
406      *
407      * @param  \s9e\TextFormatter\Configurator $configurator
408      * @return void
409      */
410      protected function configure_autolink(Configurator $configurator)
411      {
412          $configurator->plugins->load('Autoemail');
413          $configurator->plugins->load('Autolink', array('matchWww' => true));
414   
415          // Add a tag filter that creates a tag that stores and replace the
416          // content of a link created by the Autolink plugin
417          $configurator->Autolink->getTag()->filterChain
418              ->add(array($this->link_helper, 'generate_link_text_tag'))
419              ->resetParameters()
420              ->addParameterByName('tag')
421              ->addParameterByName('parser');
422   
423          // Create a tag that will be used to display the truncated text by
424          // replacing the original content with the content of the @text attribute
425          $tag = $configurator->tags->add('LINK_TEXT');
426          $tag->attributes->add('text');
427          $tag->template = '<xsl:value-of select="@text"/>';
428   
429          $tag->filterChain
430              ->add(array($this->link_helper, 'truncate_local_url'))
431              ->resetParameters()
432              ->addParameterByName('tag')
433              ->addParameterByValue(generate_board_url() . '/');
434          $tag->filterChain
435              ->add(array($this->link_helper, 'truncate_text'))
436              ->resetParameters()
437              ->addParameterByName('tag');
438          $tag->filterChain
439              ->add(array($this->link_helper, 'cleanup_tag'))
440              ->resetParameters()
441              ->addParameterByName('tag')
442              ->addParameterByName('parser');
443      }
444   
445      /**
446      * Return the default BBCodes configuration
447      *
448      * @return array 2D array. Each element has a 'usage' key, a 'template' key, and an optional 'options' key
449      */
450      protected function get_default_bbcodes($configurator)
451      {
452          // For each BBCode, build an associative array matching style_ids to their template
453          $templates = array();
454          foreach ($this->data_access->get_styles_templates() as $style_id => $data)
455          {
456              foreach ($this->extract_templates($data['template']) as $bbcode_name => $template)
457              {
458                  $templates[$bbcode_name][$style_id] = $template;
459              }
460   
461              // Add default templates wherever missing, or for BBCodes that were not specified in
462              // this template's bitfield. For instance, prosilver has a custom template for b but its
463              // bitfield does not enable it so the default template is used instead
464              foreach ($this->default_templates as $bbcode_name => $template)
465              {
466                  if (!isset($templates[$bbcode_name][$style_id]) || !in_array($bbcode_name, $data['bbcodes'], true))
467                  {
468                      $templates[$bbcode_name][$style_id] = $template;
469                  }
470              }
471          }
472   
473          // Replace custom tokens and normalize templates
474          foreach ($templates as $bbcode_name => $style_templates)
475          {
476              foreach ($style_templates as $i => $template)
477              {
478                  if (isset($this->custom_tokens[$bbcode_name]))
479                  {
480                      $template = strtr($template, $this->custom_tokens[$bbcode_name]);
481                  }
482   
483                  $templates[$bbcode_name][$i] = $configurator->templateNormalizer->normalizeTemplate($template);
484              }
485          }
486   
487          $bbcodes = array();
488          foreach ($this->default_definitions as $bbcode_name => $usage)
489          {
490              $bbcodes[$bbcode_name] = array(
491                  'usage'    => $usage,
492                  'template' => $this->merge_templates($templates[$bbcode_name]),
493              );
494          }
495   
496          return $bbcodes;
497      }
498   
499      /**
500      * Extract and recompose individual BBCode templates from a style's template file
501      *
502      * @param  string $template Style template (bbcode.html)
503      * @return array Associative array matching BBCode names to their template
504      */
505      protected function extract_templates($template)
506      {
507          // Capture the template fragments
508          preg_match_all('#<!-- BEGIN (.*?) -->(.*?)<!-- END .*? -->#s', $template, $matches, PREG_SET_ORDER);
509   
510          $fragments = array();
511          foreach ($matches as $match)
512          {
513              // Normalize the whitespace
514              $fragment = preg_replace('#>\\n\\t*<#', '><', trim($match[2]));
515   
516              $fragments[$match[1]] = $fragment;
517          }
518   
519          // Automatically recompose templates split between *_open and *_close
520          foreach ($fragments as $fragment_name => $fragment)
521          {
522              if (preg_match('#^(\\w+)_close$#', $fragment_name, $match))
523              {
524                  $bbcode_name = $match[1];
525   
526                  if (isset($fragments[$bbcode_name . '_open']))
527                  {
528                      $templates[$bbcode_name] = $fragments[$bbcode_name . '_open'] . '<xsl:apply-templates/>' . $fragment;
529                  }
530              }
531          }
532   
533          // Manually recompose and overwrite irregular templates
534          $templates['list'] =
535              '<xsl:choose>
536                  <xsl:when test="not(@type)">
537                      ' . $fragments['ulist_open_default'] . '<xsl:apply-templates/>' . $fragments['ulist_close'] . '
538                  </xsl:when>
539                  <xsl:when test="contains(\'upperlowerdecim\',substring(@type,1,5))">
540                      ' . $fragments['olist_open'] . '<xsl:apply-templates/>' . $fragments['olist_close'] . '
541                  </xsl:when>
542                  <xsl:otherwise>
543                      ' . $fragments['ulist_open'] . '<xsl:apply-templates/>' . $fragments['ulist_close'] . '
544                  </xsl:otherwise>
545              </xsl:choose>';
546   
547          $templates['li'] = $fragments['listitem'] . '<xsl:apply-templates/>' . $fragments['listitem_close'];
548   
549          // Replace the regular quote template with the extended quote template if available
550          if (isset($fragments['quote_extended']))
551          {
552              $templates['quote'] = $fragments['quote_extended'];
553          }
554   
555          // The [attachment] BBCode uses the inline_attachment template to output a comment that
556          // is post-processed by parse_attachments()
557          $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'];
558   
559          // Add fragments as templates
560          foreach ($fragments as $fragment_name => $fragment)
561          {
562              if (preg_match('#^\\w+$#', $fragment_name))
563              {
564                  $templates[$fragment_name] = $fragment;
565              }
566          }
567   
568          // Keep only templates that are named after an existing BBCode
569          $templates = array_intersect_key($templates, $this->default_definitions);
570   
571          return $templates;
572      }
573   
574      /**
575      * Merge the templates from any number of styles into one BBCode template
576      *
577      * When multiple templates are available for the same BBCode (because of multiple styles) we
578      * merge them into a single template that uses an xsl:choose construct that determines which
579      * style to use at rendering time.
580      *
581      * @param  array  $style_templates Associative array matching style_ids to their template
582      * @return string
583      */
584      protected function merge_templates(array $style_templates)
585      {
586          // Return the template as-is if there's only one style or all styles share the same template
587          if (count(array_unique($style_templates)) === 1)
588          {
589              return end($style_templates);
590          }
591   
592          // Group identical templates together
593          $grouped_templates = array();
594          foreach ($style_templates as $style_id => $style_template)
595          {
596              $grouped_templates[$style_template][] = '$STYLE_ID=' . $style_id;
597          }
598   
599          // Sort templates by frequency descending
600          $templates_cnt = array_map('sizeof', $grouped_templates);
601          array_multisort($grouped_templates, $templates_cnt);
602   
603          // Remove the most frequent template from the list; It becomes the default
604          reset($grouped_templates);
605          $default_template = key($grouped_templates);
606          unset($grouped_templates[$default_template]);
607   
608          // Build an xsl:choose switch
609          $template = '<xsl:choose>';
610          foreach ($grouped_templates as $style_template => $exprs)
611          {
612              $template .= '<xsl:when test="' . implode(' or ', $exprs) . '">' . $style_template . '</xsl:when>';
613          }
614          $template .= '<xsl:otherwise>' . $default_template . '</xsl:otherwise></xsl:choose>';
615   
616          return $template;
617      }
618  }
619