PageRenderTime 63ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/system/classes/format.php

https://github.com/HabariMag/habarimag-old
PHP | 657 lines | 438 code | 61 blank | 158 comment | 108 complexity | ab18cfa7cd781bf80689ae99fb0d8fa6 MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. /**
  3. * @package Habari
  4. *
  5. */
  6. /**
  7. * Habari Format Class
  8. *
  9. * Provides formatting functions for use in themes. Extendable.
  10. *
  11. */
  12. class Format
  13. {
  14. private static $formatters = null;
  15. /**
  16. * Called to register a format function to a plugin hook, only passing the hook's first parameter to the Format function.
  17. * @param string $format A function name that exists in a Format class
  18. * @param string $onwhat A plugin hook to apply that Format function to as a filter
  19. */
  20. public static function apply( $format, $onwhat )
  21. {
  22. if ( self::$formatters == null ) {
  23. self::load_all();
  24. }
  25. foreach ( self::$formatters as $formatobj ) {
  26. if ( method_exists( $formatobj, $format ) ) {
  27. $index = array_search( $formatobj, self::$formatters );
  28. $func = '$o = Format::by_index(' . $index . ');return $o->' . $format . '($a';
  29. $args = func_get_args();
  30. if ( count( $args ) > 2 ) {
  31. $func.= ', ';
  32. $args = array_map( create_function( '$a', 'return "\'{$a}\'";' ), array_slice( $args, 2 ) );
  33. $func .= implode( ', ', $args );
  34. }
  35. $func .= ');';
  36. $lambda = create_function( '$a', $func );
  37. Plugins::register( $lambda, 'filter', $onwhat );
  38. break; // We only look for one matching format function to apply.
  39. }
  40. }
  41. }
  42. /**
  43. *
  44. *
  45. */
  46. public static function apply_with_hook_serialize( $arg )
  47. {
  48. $arg = serialize( $arg );
  49. return "'{$arg}'";
  50. }
  51. public static function apply_with_hook_unserialize( $arg )
  52. {
  53. $arg = unserialize( $arg );
  54. return $arg;
  55. }
  56. /**
  57. * Called to register a format function to a plugin hook, and passes all of the hook's parameters to the Format function.
  58. * @param string $format A function name that exists in a Format class
  59. * @param string $onwhat A plugin hook to apply that Format function to as a filter
  60. */
  61. public static function apply_with_hook_params( $format, $onwhat )
  62. {
  63. if ( self::$formatters == null ) {
  64. self::load_all();
  65. }
  66. foreach ( self::$formatters as $formatobj ) {
  67. if ( method_exists( $formatobj, $format ) ) {
  68. $index = array_search( $formatobj, self::$formatters );
  69. $func = '$o = Format::by_index(' . $index . ');';
  70. $func .= '$args = func_get_args();';
  71. $func .= '$args = array_merge( $args';
  72. $args = func_get_args();
  73. if ( count( $args ) > 2 ) {
  74. $func .= ', array_map( array( "Format", "apply_with_hook_unserialize" ),';
  75. $args = array_map( array( "Format", "apply_with_hook_serialize" ), array_slice( $args, 2 ) );
  76. $func .= 'array( ' . implode( ', ', $args ) . ' ))';
  77. }
  78. $func .= ');';
  79. $func .= 'return call_user_func_array(array($o, "' . $format . '"), $args);';
  80. $lambda = create_function( '$a', $func );
  81. Plugins::register( $lambda, 'filter', $onwhat );
  82. break; // We only look for one matching format function to apply.
  83. }
  84. }
  85. }
  86. /**
  87. * function by_index
  88. * Returns an indexed formatter object, for use by lambda functions created
  89. * to supply additional parameters to plugin filters.
  90. * @param integer $index The index of the formatter object to return.
  91. * @return Format The formatter object requested
  92. */
  93. public static function by_index( $index )
  94. {
  95. return self::$formatters[$index];
  96. }
  97. /**
  98. * function load_all
  99. * Loads and stores an instance of all declared Format classes for future use
  100. */
  101. public static function load_all()
  102. {
  103. self::$formatters = array();
  104. $classes = get_declared_classes();
  105. foreach ( $classes as $class ) {
  106. if ( ( get_parent_class( $class ) == 'Format' ) || ( $class == 'Format' ) ) {
  107. self::$formatters[] = new $class();
  108. }
  109. }
  110. self::$formatters = array_merge( self::$formatters, Plugins::get_by_interface( 'FormatPlugin' ) );
  111. self::$formatters = array_reverse( self::$formatters, true );
  112. }
  113. /** DEFAULT FORMAT FUNCTIONS **/
  114. /**
  115. * function autop
  116. * Converts non-HTML paragraphs separated with 2 or more new lines into HTML paragraphs
  117. * while preserving any internal HTML.
  118. * New lines within the text of block elements are converted to linebreaks.
  119. * New lines before and after tags are stripped.
  120. *
  121. * If you make changes to this, PLEASE add test cases here:
  122. * http://svn.habariproject.org/habari/trunk/tests/data/autop/
  123. *
  124. * @param string $value The string to apply the formatting
  125. * @returns string The formatted string
  126. */
  127. public static function autop( $value )
  128. {
  129. $value = str_replace( "\r\n", "\n", $value );
  130. $value = trim( $value );
  131. $ht = new HtmlTokenizer( $value, false );
  132. $set = $ht->parse();
  133. $value = '';
  134. // should never autop ANY content in these items
  135. $no_auto_p = array(
  136. 'pre','code','ul','h1','h2','h3','h4','h5','h6',
  137. 'table','ul','ol','li','i','b','em','strong','script'
  138. );
  139. $block_elements = array(
  140. 'address','blockquote','center','dir','div','dl','fieldset','form',
  141. 'h1','h2','h3','h4','h5','h6','hr','isindex','menu','noframes',
  142. 'noscript','ol','p','pre','table','ul'
  143. );
  144. $token = $set->current();
  145. // There are no tokens in the text being formatted
  146. if ( $token === false ) {
  147. return $value;
  148. }
  149. $open_p = false;
  150. do {
  151. if ( $open_p ) {
  152. if ( ( $token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_EMPTY || $token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN || $token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE ) && in_array( strtolower( $token['name'] ), $block_elements ) ) {
  153. if ( strtolower( $token['name'] ) != 'p' || $token['type'] != HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE ) {
  154. $value .= '</p>';
  155. }
  156. $open_p = false;
  157. }
  158. }
  159. if ( $token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN && !in_array( strtolower( $token['name'] ), $block_elements ) && $value == '' ) {
  160. // first element, is not a block element
  161. $value = '<p>';
  162. $open_p = true;
  163. }
  164. // no-autop, pass them through verbatim
  165. if ( $token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN && in_array( strtolower( $token['name'] ), $no_auto_p ) ) {
  166. $nested_token = $token;
  167. do {
  168. $value .= HtmlTokenSet::token_to_string( $nested_token, false );
  169. if (
  170. ( $nested_token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE
  171. && strtolower( $nested_token['name'] ) == strtolower( $token['name'] ) ) // found closing element
  172. ) {
  173. break;
  174. }
  175. } while ( $nested_token = $set->next() );
  176. continue;
  177. }
  178. // anything that's not a text node should get passed through
  179. if ( $token['type'] != HTMLTokenizer::NODE_TYPE_TEXT ) {
  180. $value .= HtmlTokenSet::token_to_string( $token, true );
  181. // If the token itself is p, we need to set $open_p
  182. if ( strtolower( $token['name'] ) == 'p' && $token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN ) {
  183. $open_p = true;
  184. }
  185. continue;
  186. }
  187. // if we get this far, token type is text
  188. $local_value = $token['value'];
  189. if ( MultiByte::strlen( $local_value ) ) {
  190. if ( !$open_p ) {
  191. $local_value = '<p>' . ltrim( $local_value );
  192. $open_p = true;
  193. }
  194. $local_value = preg_replace( '/\s*(\n\s*){2,}/u', "</p><p>", $local_value ); // at least two \n in a row (allow whitespace in between)
  195. $local_value = str_replace( "\n", "<br>", $local_value ); // nl2br
  196. }
  197. $value .= $local_value;
  198. } while ( $token = $set->next() );
  199. $value = preg_replace( '#\s*<p></p>\s*#u', '', $value ); // replace <p></p>
  200. $value = preg_replace( '/<p><!--(.*?)--><\/p>/', "<!--\\1-->", $value ); // replace <p></p> around comments
  201. if ( $open_p ) {
  202. $value .= '</p>';
  203. }
  204. return $value;
  205. }
  206. /**
  207. * function and_list
  208. * Turns an array of strings into a friendly delimited string separated by commas and an "and"
  209. * @param array $array An array of strings
  210. * @param string $between Text to put between each element
  211. * @param string $between_last Text to put between the next-to-last element and the last element
  212. * @reutrn string The constructed string
  213. */
  214. public static function and_list( $array, $between = ', ', $between_last = null )
  215. {
  216. if ( ! is_array( $array ) ) {
  217. $array = array( $array );
  218. }
  219. if ( $between_last === null ) {
  220. $between_last = _t( ' and ' );
  221. }
  222. $last = array_pop( $array );
  223. $out = implode( ', ', $array );
  224. $out .= ($out == '') ? $last : $between_last . $last;
  225. return $out;
  226. }
  227. /**
  228. * function tag_and_list
  229. * Formatting function (should be in Format class?)
  230. * Turns an array of tag names into an HTML-linked list with commas and an "and".
  231. * @param array $array An array of tag names
  232. * @param string $between Text to put between each element
  233. * @param string $between_last Text to put between the next to last element and the last element
  234. * @param boolean $sort_alphabetical Should the tags be sorted alphabetically by `term` first?
  235. * @return string HTML links with specified separators.
  236. */
  237. public static function tag_and_list( $terms, $between = ', ', $between_last = null, $sort_alphabetical = false )
  238. {
  239. $array = array();
  240. if ( !$terms instanceof Terms ) {
  241. $terms = new Terms( $terms );
  242. }
  243. foreach ( $terms as $term ) {
  244. $array[$term->term] = $term->term_display;
  245. }
  246. if ( $sort_alphabetical ) {
  247. ksort( $array );
  248. }
  249. if ( $between_last === null ) {
  250. $between_last = _t( ' and ' );
  251. }
  252. $fn = create_function( '$a,$b', 'return "<a href=\\"" . URL::get("display_entries_by_tag", array( "tag" => $b) ) . "\\" rel=\\"tag\\">" . $a . "</a>";' );
  253. $array = array_map( $fn, $array, array_keys( $array ) );
  254. $last = array_pop( $array );
  255. $out = implode( $between, $array );
  256. $out .= ( $out == '' ) ? $last : $between_last . $last;
  257. return $out;
  258. }
  259. /**
  260. * Format a date using a specially formatted string
  261. * Useful for using a single string to format multiple date components.
  262. * Example:
  263. * If $dt is a HabariDateTime for December 10, 2008...
  264. * echo $dt->format_date('<div><span class="month">{F}</span> {j}, {Y}</div>');
  265. * // Output: <div><span class="month">December</span> 10, 2008</div>
  266. *
  267. * @param HabariDateTime $date The date to format
  268. * @param string $format A string with date()-like letters within braces to replace with date components
  269. * @return string The formatted string
  270. */
  271. public static function format_date( $date, $format )
  272. {
  273. if ( !( $date instanceOf HabariDateTime ) ) {
  274. $date = HabariDateTime::date_create( $date );
  275. }
  276. preg_match_all( '%\{(\w)\}%iu', $format, $matches );
  277. $components = array();
  278. foreach ( $matches[1] as $format_component ) {
  279. $components['{'.$format_component.'}'] = $date->format( $format_component );
  280. }
  281. return strtr( $format, $components );
  282. }
  283. /**
  284. * function nice_date
  285. * Formats a date using a date format string
  286. * @param HabariDateTime A date as a HabariDateTime object
  287. * @param string A date format string
  288. * @returns string The date formatted as a string
  289. */
  290. public static function nice_date( $date, $dateformat = 'F j, Y' )
  291. {
  292. if ( !( $date instanceOf HabariDateTime ) ) {
  293. $date = HabariDateTime::date_create( $date );
  294. }
  295. return $date->format( $dateformat );
  296. }
  297. /**
  298. * function nice_time
  299. * Formats a time using a date format string
  300. * @param HabariDateTime A date as a HabariDateTime object
  301. * @param string A date format string
  302. * @returns string The time formatted as a string
  303. */
  304. public static function nice_time( $date, $dateformat = 'H:i:s' )
  305. {
  306. if ( !( $date instanceOf HabariDateTime ) ) {
  307. $date = HabariDateTime::date_create( $date );
  308. }
  309. return $date->format( $dateformat );
  310. }
  311. /**
  312. * Returns a shortened version of whatever is passed in.
  313. * @param string $value A string to shorten
  314. * @param integer $count Maximum words to display [100]
  315. * @param integer $max_paragraphs Maximum paragraphs to display [1]
  316. * @return string The string, shortened
  317. */
  318. public static function summarize( $text, $count = 100, $max_paragraphs = 1 )
  319. {
  320. $ellipsis = '&hellip;';
  321. $showmore = false;
  322. $ht = new HtmlTokenizer($text, false);
  323. $set = $ht->parse();
  324. $stack = array();
  325. $para = 0;
  326. $token = $set->current();
  327. $summary = new HTMLTokenSet();
  328. $set->rewind();
  329. $remaining_words = $count;
  330. // $bail lets the loop end naturally and close all open elements without adding new ones.
  331. $bail = false;
  332. for ( $token = $set->current(); $set->valid(); $token = $set->next() ) {
  333. if ( !$bail && $token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_OPEN ) {
  334. $stack[] = $token;
  335. }
  336. if ( !$bail ) {
  337. switch ( $token['type'] ) {
  338. case HTMLTokenizer::NODE_TYPE_TEXT:
  339. $words = preg_split( '/(\\s+)/u', $token['value'], -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
  340. // word count is doubled because spaces between words are captured as their own array elements via PREG_SPLIT_DELIM_CAPTURE
  341. $words = array_slice( $words, 0, $remaining_words * 2 );
  342. $remaining_words -= count( $words ) / 2;
  343. $token['value'] = implode( '', $words );
  344. if ( $remaining_words <= 0 ) {
  345. $token['value'] .= $ellipsis;
  346. $summary[] = $token;
  347. $bail = true;
  348. }
  349. else {
  350. $summary[] = $token;
  351. }
  352. break;
  353. case HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE;
  354. // don't handle this case here
  355. break;
  356. default:
  357. $summary[] = $token;
  358. break;
  359. }
  360. }
  361. if ( $token['type'] == HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE ) {
  362. do {
  363. $end = array_pop( $stack );
  364. $end['type'] = HTMLTokenizer::NODE_TYPE_ELEMENT_CLOSE;
  365. $end['attrs'] = null;
  366. $end['value'] = null;
  367. $summary[] = $end;
  368. } while ( ( $bail || $end['name'] != $token['name'] ) && count( $stack ) > 0 );
  369. if ( count( $stack ) == 0 ) {
  370. $para++;
  371. }
  372. if ( $bail || $para >= $max_paragraphs ) {
  373. break;
  374. }
  375. }
  376. }
  377. return (string) $summary;
  378. }
  379. /**
  380. * Returns a truncated version of post content when the post isn't being displayed on its own.
  381. * Posts are split either at the comment <!--more--> or at the specified maximums.
  382. * Use only after applying autop or other paragrpah styling methods.
  383. * Apply to posts using:
  384. * <code>Format::apply_with_hook_params( 'more', 'post_content_out' );</code>
  385. * @param string $content The post content
  386. * @param Post $post The Post object of the post
  387. * @param string $more_text The text to use in the "read more" link.
  388. * @param integer $max_words null or the maximum number of words to use before showing the more link
  389. * @param integer $max_paragraphs null or the maximum number of paragraphs to use before showing the more link
  390. * @return string The post content, suitable for display
  391. */
  392. public static function more( $content, $post, $properties = array() )
  393. {
  394. // If the post requested is the post under consideration, always return the full post
  395. if ( $post->slug == Controller::get_var( 'slug' ) ) {
  396. return $content;
  397. }
  398. elseif ( is_string( $properties ) ) {
  399. $args = func_get_args();
  400. $more_text = $properties;
  401. $max_words = ( isset( $args[3] ) ? $args[3] : null );
  402. $max_paragraphs = ( isset( $args[4] ) ? $args[4] : null );
  403. $paramstring = "";
  404. }
  405. else {
  406. $paramstring = "";
  407. $paramarray = Utils::get_params( $properties );
  408. $more_text = ( isset( $paramarray['more_text'] ) ? $paramarray['more_text'] : 'Read More' );
  409. $max_words = ( isset( $paramarray['max_words'] ) ? $paramarray['max_words'] : null );
  410. $max_paragraphs = ( isset( $paramarray['max_paragraphs'] ) ? $paramarray['max_paragraphs'] : null );
  411. if ( isset( $paramarray['title:before'] ) || isset( $paramarray['title'] ) || isset( $paramarray['title:after'] ) ) {
  412. $paramstring .= 'title="';
  413. if ( isset( $paramarray['title:before'] ) ) {
  414. $paramstring .= $paramarray['title:before'];
  415. }
  416. if ( isset( $paramarray['title'] ) ) {
  417. $paramstring .= $post->title;
  418. }
  419. if ( isset( $paramarray['title:after'] ) ) {
  420. $paramstring .= $paramarray['title:after'];
  421. }
  422. $paramstring .= '" ';
  423. }
  424. if ( isset( $paramarray['class'] ) ) {
  425. $paramstring .= 'class="' . $paramarray['class'] . '" ';
  426. }
  427. }
  428. $matches = preg_split( '/<!--\s*more\s*-->/isu', $content, 2, PREG_SPLIT_NO_EMPTY );
  429. if ( count( $matches ) > 1 ) {
  430. return ( $more_text != '' ) ? reset( $matches ) . ' <a ' . $paramstring . 'href="' . $post->permalink . '">' . $more_text . '</a>' : reset( $matches );
  431. }
  432. elseif ( isset( $max_words ) || isset( $max_paragraphs ) ) {
  433. $max_words = empty( $max_words ) ? 9999999 : intval( $max_words );
  434. $max_paragraphs = empty( $max_paragraphs ) ? 9999999 : intval( $max_paragraphs );
  435. $summary = Format::summarize( $content, $max_words, $max_paragraphs );
  436. if ( MultiByte::strlen( $summary ) >= MultiByte::strlen( $content ) ) {
  437. return $content;
  438. }
  439. else {
  440. if ( strlen( $more_text ) ) {
  441. // Tokenize the summary and link
  442. $ht = new HTMLTokenizer( $summary );
  443. $summary_set = $ht->parse();
  444. $ht = new HTMLTokenizer( '<a ' . $paramstring . ' href="' . $post->permalink . '">' . $more_text . '</a>' );
  445. $link_set= $ht->parse();
  446. // Find out where to put the link
  447. $end = $summary_set->end();
  448. $key = $summary_set->key();
  449. // Inject the link
  450. $summary_set->insert( $link_set, $key );
  451. return (string)$summary_set;
  452. }
  453. else {
  454. return $summary;
  455. }
  456. }
  457. }
  458. return $content;
  459. }
  460. /**
  461. * html_messages
  462. * Creates an HTML unordered list of an array of messages
  463. * @param array $notices a list of success messages
  464. * @param array $errors a list of error messages
  465. * @return string HTML output
  466. */
  467. public static function html_messages( $notices, $errors )
  468. {
  469. $output = '';
  470. if ( count( $errors ) ) {
  471. $output.= '<ul class="error">';
  472. foreach ( $errors as $error ) {
  473. $output.= '<li>' . $error . '</li>';
  474. }
  475. $output.= '</ul>';
  476. }
  477. if ( count( $notices ) ) {
  478. $output.= '<ul class="success">';
  479. foreach ( $notices as $notice ) {
  480. $output.= '<li>' . $notice . '</li>';
  481. }
  482. $output.= '</ul>';
  483. }
  484. return $output;
  485. }
  486. /**
  487. * humane_messages
  488. * Creates JS calls to display session messages
  489. * @param array $notices a list of success messages
  490. * @param array $errors a list of error messages
  491. * @return string JS output
  492. */
  493. public static function humane_messages( $notices, $errors )
  494. {
  495. $output = '';
  496. if ( count( $errors ) ) {
  497. foreach ( $errors as $error ) {
  498. $error = addslashes( $error );
  499. $output .= "human_msg.display_msg(\"{$error}\");";
  500. }
  501. }
  502. if ( count( $notices ) ) {
  503. foreach ( $notices as $notice ) {
  504. $notice = addslashes( $notice );
  505. $output .= "human_msg.display_msg(\"{$notice}\");";
  506. }
  507. }
  508. return $output;
  509. }
  510. /**
  511. * json_messages
  512. * Creates a JSON list of session messages
  513. * @param array $notices a list of success messages
  514. * @param array $errors a list of error messages
  515. * @return string JS output
  516. */
  517. public static function json_messages( $notices, $errors )
  518. {
  519. $messages = array_merge( $errors, $notices );
  520. return json_encode( $messages );
  521. }
  522. /**
  523. * function term_tree
  524. * Create nested HTML lists from a hierarchical vocabulary.
  525. *
  526. * Turns Terms or an array of terms from a hierarchical vocabulary into a ordered HTML list with list items for each term.
  527. * @param mixed $terms An array of Term objects or a Terms object.
  528. * @param string $tree_name The name of the tree, used for unique node id's
  529. * @param array $config an array of values to use to configure the output of this function
  530. * @return string The transformed vocabulary.
  531. */
  532. public static function term_tree( $terms, $tree_name, $config = array() )
  533. {
  534. $defaults = array(
  535. 'treestart' => '<ol %s>',
  536. 'treeattr' => array('class' => 'tree', 'id' => Utils::slugify('tree_' . $tree_name)),
  537. 'treeend' => '</ol>',
  538. 'liststart' => '<ol %s>',
  539. 'listattr' => array(),
  540. 'listend' => '</ol>',
  541. 'itemstart' => '<li %s>',
  542. 'itemattr' => array(),
  543. 'itemend' => '</li>',
  544. 'wrapper' => '<div>%s</div>',
  545. 'linkcallback' => null,
  546. 'itemcallback' => null,
  547. 'listcallback' => null,
  548. );
  549. $config = array_merge($defaults, $config);
  550. $out = sprintf($config['treestart'], Utils::html_attr($config['treeattr']));
  551. $stack = array();
  552. $tree_name = Utils::slugify($tree_name);
  553. if ( !$terms instanceof Terms ) {
  554. $terms = new Terms( $terms );
  555. }
  556. foreach ( $terms as $term ) {
  557. if(count($stack)) {
  558. if($term->mptt_left - end($stack)->mptt_left == 1) {
  559. if(isset($config['listcallback'])) {
  560. $config = call_user_func($config['listcallback'], $term, $config);
  561. }
  562. $out .= sprintf($config['liststart'], Utils::html_attr($config['listattr']));
  563. }
  564. while(count($stack) && $term->mptt_left > end($stack)->mptt_right) {
  565. $out .= $config['itemend'] . $config['listend'] . "\n";
  566. array_pop($stack);
  567. }
  568. }
  569. $config['itemattr']['id'] = $tree_name . '_' . $term->id;
  570. if(isset($config['itemcallback'])) {
  571. $config = call_user_func($config['itemcallback'], $term, $config);
  572. }
  573. $out .= sprintf($config['itemstart'], Utils::html_attr($config['itemattr']));
  574. if(isset($config['linkcallback'])) {
  575. $display = call_user_func($config['linkcallback'], $term, $config);
  576. }
  577. else {
  578. $display = $term->term_display;
  579. }
  580. $out .= sprintf( $config['wrapper'], $display );
  581. if($term->mptt_right - $term->mptt_left > 1) {
  582. $stack[] = $term;
  583. }
  584. else {
  585. $out .= $config['itemend'] ."\n";
  586. }
  587. }
  588. while(count($stack)) {
  589. $out .= $config['itemend'] . $config['listend'] . "\n";
  590. array_pop($stack);
  591. }
  592. $out .= $config['treeend'];
  593. return $out;
  594. }
  595. }
  596. ?>