PageRenderTime 34ms CodeModel.GetById 7ms RepoModel.GetById 0ms app.codeStats 0ms

/gforge/plugins/wiki/www/lib/plugin/CreateToc.php

https://github.com/neymanna/fusionforge
PHP | 567 lines | 373 code | 20 blank | 174 comment | 97 complexity | ec354e0518dfefc16828c550b17fbb74 MD5 | raw file
Possible License(s): GPL-2.0, MPL-2.0-no-copyleft-exception
  1. <?php // -*-php-*-
  2. rcs_id('$Id: CreateToc.php,v 1.34 2007/01/28 22:47:06 rurban Exp $');
  3. /*
  4. Copyright 2004,2005 $ThePhpWikiProgrammingTeam
  5. This file is part of PhpWiki.
  6. PhpWiki is free software; you can redistribute it and/or modify
  7. it under the terms of the GNU General Public License as published by
  8. the Free Software Foundation; either version 2 of the License, or
  9. (at your option) any later version.
  10. PhpWiki is distributed in the hope that it will be useful,
  11. but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. GNU General Public License for more details.
  14. You should have received a copy of the GNU General Public License
  15. along with PhpWiki; if not, write to the Free Software
  16. Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  17. */
  18. /**
  19. * CreateToc: Automatically link to headers
  20. *
  21. * Usage:
  22. * <?plugin CreateToc headers=!!!,!! with_toclink||=1
  23. * jshide||=1 ?>
  24. * @author: Reini Urban
  25. *
  26. * Known problems:
  27. * - MacIE will not work with jshide.
  28. * - it will crash with old markup and Apache2 (?)
  29. * - Certain corner-edges will not work with TOC_FULL_SYNTAX.
  30. * I believe I fixed all of them now, but who knows?
  31. * - bug #969495 "existing labels not honored" seems to be fixed.
  32. */
  33. if (!defined('TOC_FULL_SYNTAX'))
  34. define('TOC_FULL_SYNTAX', 1);
  35. class WikiPlugin_CreateToc
  36. extends WikiPlugin
  37. {
  38. function getName() {
  39. return _("CreateToc");
  40. }
  41. function getDescription() {
  42. return _("Automatically link headers at the top");
  43. }
  44. function getVersion() {
  45. return preg_replace("/[Revision: $]/", '',
  46. "\$Revision: 1.34 $");
  47. }
  48. function getDefaultArguments() {
  49. return array( 'pagename' => '[pagename]', // TOC of another page here?
  50. // or headers=1,2,3 is also possible.
  51. 'headers' => "!!!,!!,!", // "!!!"=>h1, "!!"=>h2, "!"=>h3
  52. 'noheader' => 0, // omit <h1>Table of Contents</h1>
  53. 'position' => 'right', // or left
  54. 'with_toclink' => 0, // link back to TOC
  55. 'jshide' => 0, // collapsed TOC as DHTML button
  56. 'extracollapse' => 1, // provide an entry +/- link to collapse
  57. 'liststyle' => 'dl', // 'dl' or 'ul' or 'ol'
  58. 'indentstr' => '&nbsp;&nbsp;',
  59. 'with_counter' => 0,
  60. );
  61. }
  62. // Initialisation of toc counter
  63. function _initTocCounter() {
  64. $counter = array(1=>1, 2=>0, 3=>0);
  65. return $counter;
  66. }
  67. // Update toc counter with a new title
  68. function _tocCounter(&$counter, $level) {
  69. $counter[$level]++;
  70. $level--;
  71. for($i = $level; $i > 0; $i--) {
  72. $counter[$level] = 0;
  73. }
  74. }
  75. // Get string corresponding to the current title
  76. function _getCounter(&$counter, $level) {
  77. $str=$counter[3];
  78. for($i = 2; $i > 0; $i--) {
  79. if($counter[$i] != 0)
  80. $str .= '.'.$counter[$i];
  81. }
  82. return $str;
  83. }
  84. function preg_quote ($heading) {
  85. return str_replace(array("/",".","?","*"),
  86. array('\/','\.','\?','\*'), $heading);
  87. }
  88. // Get HTML header corresponding to current level (level is set of !)
  89. function _getHeader($level) {
  90. $count = substr_count($level,'!');
  91. switch ($count) {
  92. case 1: $h = "h4"; break;
  93. case 2: $h = "h3"; break;
  94. case 3: $h = "h2"; break;
  95. }
  96. return $h;
  97. }
  98. function _quote($heading) {
  99. if (TOC_FULL_SYNTAX ) {
  100. $theading = TransformInline($heading);
  101. if ($theading)
  102. return preg_quote($theading->asXML(), "/");
  103. else
  104. return XmlContent::_quote(preg_quote($heading, "/"));
  105. } else {
  106. return XmlContent::_quote(preg_quote($heading, "/"));
  107. }
  108. }
  109. /*
  110. * @param $hstart id (in $content) of heading start
  111. * @param $hend id (in $content) of heading end
  112. */
  113. function searchHeader ($content, $start_index, $heading,
  114. $level, &$hstart, &$hend, $basepage=false) {
  115. $hstart = 0;
  116. $hend = 0;
  117. $h = $this->_getHeader($level);
  118. $qheading = $this->_quote($heading);
  119. for ($j=$start_index; $j < count($content); $j++) {
  120. if (is_string($content[$j])) {
  121. if (preg_match("/<$h>$qheading<\/$h>/",
  122. $content[$j]))
  123. return $j;
  124. }
  125. elseif (isa($content[$j], 'cached_link'))
  126. {
  127. if (method_exists($content[$j],'asXML')) {
  128. $content[$j]->_basepage = $basepage;
  129. $content[$j] = $content[$j]->asXML();
  130. } else
  131. $content[$j] = $content[$j]->asString();
  132. // shortcut for single wikiword or link headers
  133. if ($content[$j] == $heading
  134. and substr($content[$j-1],-4,4) == "<$h>"
  135. and substr($content[$j+1],0,5) == "</$h>")
  136. {
  137. $hstart = $j-1;
  138. $hend = $j+1;
  139. return $j; // single wikiword
  140. }
  141. elseif (TOC_FULL_SYNTAX) {
  142. //DONE: To allow "!! WikiWord link" or !! http://anylink/
  143. // Check against joined content (after cached_plugininvocation).
  144. // The first link is the anchor then.
  145. if (preg_match("/<$h>(?!.*<\/$h>)/", $content[$j-1])) {
  146. $hstart = $j-1;
  147. $joined = '';
  148. for ($k=max($j-1,$start_index);$k<count($content);$k++) {
  149. if (is_string($content[$k]))
  150. $joined .= $content[$k];
  151. elseif (method_exists($content[$k],'asXML'))
  152. $joined .= $content[$k]->asXML();
  153. else
  154. $joined .= $content[$k]->asString();
  155. if (preg_match("/<$h>$qheading<\/$h>/",$joined)) {
  156. $hend=$k;
  157. return $k;
  158. }
  159. }
  160. }
  161. }
  162. }
  163. }
  164. trigger_error("Heading <$h> $heading </$h> not found\n", E_USER_NOTICE);
  165. return 0;
  166. }
  167. /** prevent from duplicate anchors,
  168. * beautify spaces: " " => "_" and not "x20."
  169. */
  170. function _nextAnchor($s) {
  171. static $anchors = array();
  172. $s = str_replace(' ','_',$s);
  173. $i = 1;
  174. $anchor = $s;
  175. while (!empty($anchors[$anchor])) {
  176. $anchor = sprintf("%s_%d",$s,$i++);
  177. }
  178. $anchors[$anchor] = $i;
  179. return $anchor;
  180. }
  181. // Feature request: proper nesting; multiple levels (e.g. 1,3)
  182. function extractHeaders (&$content, &$markup, $backlink=0,
  183. $counter=0, $levels=false, $basepage='')
  184. {
  185. if (!$levels) $levels = array(1,2);
  186. $tocCounter = $this->_initTocCounter();
  187. reset($levels);
  188. sort($levels);
  189. $headers = array();
  190. $j = 0;
  191. for ($i=0; $i<count($content); $i++) {
  192. foreach ($levels as $level) {
  193. if ($level < 1 or $level > 3) continue;
  194. if (preg_match('/^\s*(!{'.$level.','.$level.'})([^!].*)$/',
  195. $content[$i], $match))
  196. {
  197. $this->_tocCounter($tocCounter, $level);
  198. if (!strstr($content[$i],'#[')) {
  199. $s = trim($match[2]);
  200. $anchor = $this->_nextAnchor($s);
  201. $manchor = MangleXmlIdentifier($anchor);
  202. $texts = $s;
  203. if($counter) {
  204. $texts = $this->_getCounter($tocCounter, $level).' '.$s;
  205. }
  206. $headers[] = array('text' => $texts,
  207. 'anchor' => $anchor,
  208. 'level' => $level);
  209. // Change original wikitext, but that is useless art...
  210. $content[$i] = $match[1]." #[|$manchor][$s|#TOC]";
  211. // And now change the to be printed markup (XmlTree):
  212. // Search <hn>$s</hn> line in markup
  213. /* Url for backlink */
  214. $url = WikiURL(new WikiPageName($basepage,false,"TOC"));
  215. $j = $this->searchHeader($markup->_content, $j, $s,
  216. $match[1], $hstart, $hend,
  217. $markup->_basepage);
  218. if ($j and isset($markup->_content[$j])) {
  219. $x = $markup->_content[$j];
  220. $qheading = $this->_quote($s);
  221. if ($counter)
  222. $counterString = $this->_getCounter($tocCounter, $level);
  223. if (($hstart === 0) && is_string($markup->_content[$j])) {
  224. if ($backlink) {
  225. if ($counter)
  226. $anchorString = "<a href=\"$url\" name=\"$manchor\">$counterString</a> - \$2";
  227. else
  228. $anchorString = "<a href=\"$url\" name=\"$manchor\">\$2</a>";
  229. } else {
  230. $anchorString = "<a name=\"$manchor\"></a>";
  231. if ($counter)
  232. $anchorString .= "$counterString - ";
  233. }
  234. if ($x = preg_replace('/(<h\d>)('.$qheading.')(<\/h\d>)/',
  235. "\$1$anchorString\$2\$3",$x,1)) {
  236. if ($backlink) {
  237. $x = preg_replace('/(<h\d>)('.$qheading.')(<\/h\d>)/',
  238. "\$1$anchorString\$3",
  239. $markup->_content[$j],1);
  240. }
  241. $markup->_content[$j] = $x;
  242. }
  243. } else {
  244. $x = $markup->_content[$hstart];
  245. $h = $this->_getHeader($match[1]);
  246. if ($backlink) {
  247. if ($counter)
  248. $anchorString = "\$1<a href=\"$url\" name=\"$manchor\">$counterString</a> - ";
  249. else {
  250. /* Not possible to make a backlink on a
  251. * title with a WikiWord */
  252. $anchorString = "\$1<a name=\"$manchor\"></a>";
  253. }
  254. }
  255. else {
  256. $anchorString = "\$1<a name=\"$manchor\"></a>";
  257. if ($counter)
  258. $anchorString .= "$counterString - ";
  259. }
  260. $x = preg_replace("/(<$h>)(?!.*<\/$h>)/",
  261. $anchorString, $x, 1);
  262. if ($backlink) {
  263. $x = preg_replace("/(<$h>)(?!.*<\/$h>)/",
  264. $anchorString,
  265. $markup->_content[$hstart],1);
  266. }
  267. $markup->_content[$hstart] = $x;
  268. }
  269. }
  270. }
  271. }
  272. }
  273. }
  274. return $headers;
  275. }
  276. function run($dbi, $argstr, &$request, $basepage) {
  277. global $WikiTheme;
  278. extract($this->getArgs($argstr, $request));
  279. if ($pagename) {
  280. // Expand relative page names.
  281. $page = new WikiPageName($pagename, $basepage);
  282. $pagename = $page->name;
  283. }
  284. if (!$pagename) {
  285. return $this->error(_("no page specified"));
  286. }
  287. if ($jshide and isBrowserIE() and browserDetect("Mac")) {
  288. //trigger_error(_("jshide set to 0 on Mac IE"), E_USER_NOTICE);
  289. $jshide = 0;
  290. }
  291. $page = $dbi->getPage($pagename);
  292. $current = $page->getCurrentRevision();
  293. //FIXME: I suspect this only to crash with Apache2
  294. if (!$current->get('markup') or $current->get('markup') < 2) {
  295. if (in_array(php_sapi_name(),array('apache2handler','apache2filter'))) {
  296. trigger_error(_("CreateToc disabled for old markup"), E_USER_WARNING);
  297. return '';
  298. }
  299. }
  300. $content = $current->getContent();
  301. $html = HTML::div(array('class' => 'toc', 'id'=>'toc'));
  302. if ($liststyle == 'dl')
  303. $list = HTML::dl(array('id'=>'toclist','class' => 'toc'));
  304. elseif ($liststyle == 'ul')
  305. $list = HTML::ul(array('id'=>'toclist','class' => 'toc'));
  306. elseif ($liststyle == 'ol')
  307. $list = HTML::ul(array('id'=>'toclist','class' => 'toc'));
  308. if (!strstr($headers,",")) {
  309. $headers = array($headers);
  310. } else {
  311. $headers = explode(",",$headers);
  312. }
  313. $levels = array();
  314. foreach ($headers as $h) {
  315. //replace !!! with level 1, ...
  316. if (strstr($h,"!")) {
  317. $hcount = substr_count($h,'!');
  318. $level = min(max(1, $hcount),3);
  319. $levels[] = $level;
  320. } else {
  321. $level = min(max(1, (int) $h), 3);
  322. $levels[] = $level;
  323. }
  324. }
  325. if (TOC_FULL_SYNTAX)
  326. require_once("lib/InlineParser.php");
  327. if ($headers = $this->extractHeaders($content, $dbi->_markup,
  328. $with_toclink, $with_counter,
  329. $levels, $basepage))
  330. {
  331. $h2counter=0;
  332. $h3counter=0;
  333. $h4counter=0;
  334. $previous_level=1;
  335. foreach ($headers as $h) {
  336. // proper heading indent
  337. $level = $h['level'];
  338. $indent = 3 - $level;
  339. if ($level == $previous_level) {
  340. /* increment counter */
  341. if ($level == 3) {
  342. $h2counter++;
  343. } else if ($level == 2) {
  344. $h3counter++;
  345. } else {
  346. $h4counter++;
  347. }
  348. } else {
  349. /* reset counter */
  350. if ((previous_level == 2) && ($level == 3)) {
  351. $h2counter++;
  352. $h3counter=0;
  353. $h4counter=0;
  354. } else if ($previous_level == 1) {
  355. if ($level == 3) {
  356. $h2counter++;
  357. $h3counter=0;
  358. $h4counter=0;
  359. }
  360. if ($level == 2) {
  361. $h3counter++;
  362. $h4counter=0;
  363. }
  364. }
  365. }
  366. $link = new WikiPageName($pagename,$page,$h['anchor']);
  367. $li = WikiLink($link,'known',$h['text']);
  368. if ($liststyle == 'dl') {
  369. $list->pushContent(HTML::dt(HTML::raw
  370. (str_repeat($indentstr,$indent)),$li));
  371. } else if ($liststyle == 'ol') {
  372. if ($level == 3) {
  373. $prefix = $h2counter;
  374. } else if ($level == 2) {
  375. $prefix = $h2counter . "." . $h3counter;
  376. } else {
  377. $prefix = $h2counter . "." . $h3counter . "." . $h4counter;
  378. }
  379. $prefix = str_repeat($indentstr,$indent) . $prefix . "&nbsp;";
  380. $list->pushContent(HTML::li(HTML::raw
  381. ($prefix),$li));
  382. } else {
  383. $list->pushContent(HTML::li(HTML::raw
  384. (str_repeat($indentstr,$indent)),$li));
  385. }
  386. }
  387. }
  388. $list->setAttr('style','display:'.($jshide?'none;':'block;'));
  389. $open = DATA_PATH.'/'.$WikiTheme->_findFile("images/folderArrowOpen.png");
  390. $close = DATA_PATH.'/'.$WikiTheme->_findFile("images/folderArrowClosed.png");
  391. $html->pushContent(Javascript("
  392. function toggletoc(a) {
  393. toc=document.getElementById('toclist')
  394. //toctoggle=document.getElementById('toctoggle')
  395. open='".$open."'
  396. close='".$close."'
  397. if (toc.style.display=='none') {
  398. toc.style.display='block'
  399. a.title='"._("Click to hide the TOC")."'
  400. a.src = open
  401. } else {
  402. toc.style.display='none';
  403. a.title='"._("Click to display")."'
  404. a.src = close
  405. }
  406. }"));
  407. if ($extracollapse)
  408. $toclink = HTML(_("Table of Contents"),
  409. " ",
  410. HTML::a(array('name'=>'TOC')),
  411. HTML::img(array(
  412. 'id'=>'toctoggle',
  413. 'class'=>'wikiaction',
  414. 'title'=>_("Click to display the TOC"),
  415. 'alt'=>_("Click to display the TOC"),
  416. 'onclick'=>"toggletoc(this)",
  417. 'height' => 15,
  418. 'width' => 15,
  419. 'border' => 0,
  420. 'src' => $jshide ? $close : $open )));
  421. else
  422. $toclink = HTML::a(array('name'=>'TOC',
  423. 'class'=>'wikiaction',
  424. 'title'=>_("Click to display"),
  425. 'onclick'=>"toggletoc(this)"),
  426. _("Table Of Contents"),
  427. HTML::span(array('style'=>'display:none',
  428. 'id'=>'toctoggle')," "));
  429. $html->pushContent(HTML::h4($toclink));
  430. $html->pushContent($list);
  431. return $html;
  432. }
  433. };
  434. // $Log: CreateToc.php,v $
  435. // Revision 1.34 2007/01/28 22:47:06 rurban
  436. // fix # back link
  437. //
  438. // Revision 1.33 2007/01/28 22:37:04 rurban
  439. // beautify +/- collapse icon
  440. //
  441. // Revision 1.32 2007/01/20 11:25:30 rurban
  442. // remove align
  443. //
  444. // Revision 1.31 2007/01/09 12:35:05 rurban
  445. // Change align to position. Add extracollapse. js now always active, jshide just denotes the initial state.
  446. //
  447. // Revision 1.30 2006/12/22 17:49:38 rurban
  448. // fix quoting
  449. //
  450. // Revision 1.29 2006/04/15 12:26:54 rurban
  451. // need basepage for subpages like /Remove (within CreateTOC)
  452. //
  453. // Revision 1.28 2005/10/12 06:15:25 rurban
  454. // just aesthetics
  455. //
  456. // Revision 1.27 2005/10/10 19:50:45 rurban
  457. // fix the missing formatting problems, add with_counter arg by ?? (20050106), Thanks to ManuelVacelet for the testcase
  458. //
  459. // Revision 1.26 2004/09/20 14:07:16 rurban
  460. // fix Constant toc_full_syntax already defined warning
  461. //
  462. // Revision 1.25 2004/07/08 20:30:07 rurban
  463. // plugin->run consistency: request as reference, added basepage.
  464. // encountered strange bug in AllPages (and the test) which destroys ->_dbi
  465. //
  466. // Revision 1.24 2004/06/28 13:27:03 rurban
  467. // CreateToc disabled for old markup and Apache2 only
  468. //
  469. // Revision 1.23 2004/06/28 13:13:58 rurban
  470. // CreateToc disabled for old markup
  471. //
  472. // Revision 1.22 2004/06/15 14:56:37 rurban
  473. // more allow_call_time_pass_reference false fixes
  474. //
  475. // Revision 1.21 2004/06/13 09:45:23 rurban
  476. // display bug workaround for MacIE browsers, jshide: 0
  477. //
  478. // Revision 1.20 2004/05/11 13:57:46 rurban
  479. // enable TOC_FULL_SYNTAX per default
  480. // don't <a name>$header</a> to disable css formatting for such anchors
  481. // => <a name></a>$header
  482. //
  483. // Revision 1.19 2004/05/08 16:59:27 rurban
  484. // requires optional TOC_FULL_SYNTAX constnat to enable full link and
  485. // wikiword syntax in headers.
  486. //
  487. // Revision 1.18 2004/04/29 21:55:15 rurban
  488. // fixed TOC backlinks with USE_PATH_INFO false
  489. // with_toclink=1, sf.net bug #940682
  490. //
  491. // Revision 1.17 2004/04/26 19:43:03 rurban
  492. // support most cases of header markup. fixed duplicate MangleXmlIdentifier name
  493. //
  494. // Revision 1.16 2004/04/26 14:46:14 rurban
  495. // better comments
  496. //
  497. // Revision 1.14 2004/04/21 04:29:50 rurban
  498. // write WikiURL consistently (not WikiUrl)
  499. //
  500. // Revision 1.12 2004/03/22 14:13:53 rurban
  501. // fixed links to equal named headers
  502. //
  503. // Revision 1.11 2004/03/15 09:52:59 rurban
  504. // jshide button: dynamic titles
  505. //
  506. // Revision 1.10 2004/03/14 20:30:21 rurban
  507. // jshide button
  508. //
  509. // Revision 1.9 2004/03/09 19:24:20 rurban
  510. // custom indentstr
  511. // h2 toc header
  512. //
  513. // Revision 1.8 2004/03/09 19:05:12 rurban
  514. // new liststyle arg. default: dl (no bullets)
  515. //
  516. // Revision 1.7 2004/03/09 11:51:54 rurban
  517. // support jshide=1: DHTML button hide/unhide TOC
  518. //
  519. // Revision 1.6 2004/03/09 10:25:37 rurban
  520. // slightly better formatted TOC indentation
  521. //
  522. // Revision 1.5 2004/03/09 08:57:10 rurban
  523. // convert space to "_" instead of "x20." in anchors
  524. // proper heading indent
  525. // handle duplicate headers
  526. // allow multiple headers like "!!!,!!" or "1,2"
  527. //
  528. // Revision 1.4 2004/03/02 18:21:29 rurban
  529. // typo: ref=>href
  530. //
  531. // Revision 1.1 2004/03/01 18:10:28 rurban
  532. // first version, without links, anchors and jscript folding
  533. //
  534. //
  535. // For emacs users
  536. // Local Variables:
  537. // mode: php
  538. // tab-width: 8
  539. // c-basic-offset: 4
  540. // c-hanging-comment-ender-p: nil
  541. // indent-tabs-mode: nil
  542. // End:
  543. ?>