PicoTableOfContent.php 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. <?php
  2. /**
  3. * @package Pico
  4. * @subpackage PicoTableOfContent
  5. * @author {notabug,framagit}.org/ohnonot
  6. */
  7. class PicoTableOfContent extends AbstractPicoPlugin {
  8. const API_VERSION = 3;
  9. // only act if this var is set to True
  10. private $yeah = False;
  11. // default settings
  12. private $depth = 4;
  13. private $min_headers = 2;
  14. private $top_txt = ' ∆';
  15. private $top_txt_title = 'Home';
  16. private $caption = "Table of Contents";
  17. private $template = "post"; // config to valid twig template to only act on that template - or "all" for all.
  18. // internal
  19. private $toc = '';
  20. private $xpQuery = '';
  21. private $content;
  22. private $css= 'PicoTableOfContent.css';
  23. private $css_alt= '';
  24. public function onConfigLoaded(array &$config)
  25. {
  26. if(isset($config['PicoTableOfContent']['depth'])) $this->depth = &$config['PicoTableOfContent']['depth'];
  27. if(isset($config['PicoTableOfContent']['min_headers'])) $this->min_headers = &$config['PicoTableOfContent']['min_headers'];
  28. if(isset($config['PicoTableOfContent']['top_txt'])) $this->top_txt = &$config['PicoTableOfContent']['top_txt'];
  29. if(isset($config['PicoTableOfContent']['top_txt_title'])) $this->top_txt_title = &$config['PicoTableOfContent']['top_txt_title'];
  30. if(isset($config['PicoTableOfContent']['caption'])) $this->caption = &$config['PicoTableOfContent']['caption'];
  31. if(isset($config['PicoTableOfContent']['template'])) $this->template = &$config['PicoTableOfContent']['template'];
  32. // building the xpath query for all desired headers, e.g. '//h1|//h2|//h3'
  33. for ($i=1; $i <= $this->depth; $i++) {
  34. $this->xpQuery = $this->xpQuery.'//h'.$i;
  35. ( $i < $this->depth ) and $this->xpQuery = $this->xpQuery.'|';
  36. }
  37. }
  38. public function onMetaParsed(array &$meta)
  39. {
  40. if(isset($meta['toc_disabled']) && $meta['toc_disabled'] === TRUE) { $this->yeah = False; return; }
  41. if(isset($meta['toc_css'])) { $this->css_alt = $meta['toc_css']; }
  42. if($meta['template'] === $this->template || $this->template === "all") $this->yeah = True;
  43. }
  44. public function onContentParsed(&$content)
  45. {
  46. // only continue if we want TOC for this page
  47. if($this->yeah != True) return;
  48. if(trim($content)=="") { $this->yeah = False; return; }
  49. $dom = new DOMDocument();
  50. $dom->preserveWhiteSpace = true;
  51. // encoding problems require us to jump through hoops here...
  52. // please read this: stackoverflow.com/a/11310258
  53. // $content is a html fragment. We like to modify its content with DOMDocument, but it prefers complete documents and completes them
  54. // if necessary. This leads to strangeness. The best thing I found is to give it a complete document with all required headers and tags,
  55. // then strip those off in the end. That way we DOMDocument doesn't meddle, and we have control. Hah, hopefully.
  56. $dom->loadHTML('<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"></head><body>'.$content.'</body></html>',
  57. LIBXML_NONET|LIBXML_COMPACT|LIBXML_HTML_NODEFDTD|LIBXML_NOWARNING);
  58. //~ LIBXML_NOEMPTYTAG|LIBXML_NOENT|LIBXML_HTML_NOIMPLIED|LIBXML_NOERROR);
  59. $trim_before=92; // What we need to trim when saving (the above string added to $content)
  60. $trim_after=-15; // - " -
  61. $xp = new DOMXPath($dom);
  62. $nodes =$xp->query($this->xpQuery);
  63. // DEBUG
  64. //~ echo '<pre>';
  65. //~ foreach ($nodes as $node) {
  66. //~ echo $node->nodeValue;
  67. //~ }
  68. //~ var_dump($nodes);
  69. //~ array_walk(iterator_to_array($nodes),'var_dump');
  70. //~ echo '</pre>';
  71. if($nodes->length < $this->min_headers)
  72. { $this->yeah = False; return; }
  73. // add id's to the h tags and at the same time build the TOC
  74. foreach($nodes as $i => $sort)
  75. {
  76. if (isset($sort->tagName) && $sort->tagName !== '')
  77. {
  78. if($sort->getAttribute('id') === "")
  79. {
  80. $text = preg_replace('~[^\\pL0-9_]+~u', '-', $sort->nodeValue);
  81. $text = trim($text,'-');
  82. $text = strtolower($text);
  83. // build TOC before manipulatng the nodes
  84. $this->toc = $this->toc.'<li class="toc'.substr($sort->nodeName, 1).'"><a href="#'.$text.'">'.$sort->nodeValue.'</a></li>';
  85. $sort->setAttribute('id',$text);
  86. $a = $dom->createElement('a', $this->top_txt);
  87. $a->setAttribute('title', $this->top_txt_title);
  88. $a->setAttribute('href', '#');
  89. $a->setAttribute('class', 'toc-nav');
  90. $sort->appendChild($a);
  91. }
  92. }
  93. }
  94. // we now have a full html document in $dom. Let's extract only what is between
  95. // <body> tags,as it was before. - stackoverflow.com/a/18090774
  96. //~ $content='';
  97. //~ foreach($dom->getElementsByTagName("body")->item(0)->childNodes as $child) {
  98. //~ $content .= $dom->saveHTML($child);
  99. //~ }
  100. $content = $dom->saveHTML();
  101. // remove the tag we added at loadHTML
  102. $content = substr($content,$trim_before,$trim_after);
  103. //~ // please read this: stackoverflow.com/a/20675396
  104. //~ $content = $dom->saveHTML($dom->documentElement);
  105. //~ $len = strlen($utf8); if(substr($content,0,$len) === $utf8) $content = substr($content,$len);
  106. $cap = $this->caption =='' ? "" : '<p id="toc-header">'.$this->caption.'</p>';
  107. $this->toc = '<div id="toc">'.$cap.'<ul>'.$this->toc.'</ul></div>';
  108. }
  109. public function onPageRendering(&$templateName, array &$twigVariables)
  110. {
  111. if($this->yeah != True) return;
  112. // adding the correct stylesheet
  113. if($this->css_alt != "" and substr($this->css_alt,0,1) === '/') $this->css=$twigVariables['base_url'].$this->css_alt;
  114. //~ else {
  115. //~ $test=$twigVariables['theme_url'].'/'.$this->css_alt;
  116. //~ if(file_exists($_SERVER['DOCUMENT_ROOT'].$test)) $this->css=$test;
  117. //~ else $this->css=$twigVariables['assets_url'].'/'.$this->css_alt;
  118. //~ }
  119. else {
  120. $test=$twigVariables['assets_url'].'/'.$this->css;
  121. if(file_exists($_SERVER['DOCUMENT_ROOT'].$test)) $this->css=$test;
  122. else $this->css=$twigVariables['plugins_url'].'/PicoTableOfContent/'.$this->css;
  123. }
  124. $this->toc='<link rel="stylesheet" href="'.$this->css.'" type="text/css" />'.$this->toc;
  125. $twigVariables['TableOfContent'] = new Twig_Markup($this->toc, 'UTF-8'); // tells twig to render this as raw html
  126. }
  127. }