Simplewiki  1.1
simplewiki.php
Go to the documentation of this file.
1 <?php
2 // Muster Software Copyright (c) Henrik Bechmann, Toronto, Canada 2009-2012. All rights reserved.
3 // See "musterlicence.txt" for licencing information.
4 // mustersoftware.net
5 
6 namespace Muster;
7 
8 use StdClass;
9 use
13 
142 #==========================================================================
143 #-----------------------------[ SIMPLE WIKI ]------------------------------
144 #==========================================================================
145 
154 {
155  protected $_parser;
156  protected $_allow_html = TRUE;
157  protected $_emitter;
158  protected $_footnotes;
160  protected $_working_parser;
161 
178  public function __construct($text = NULL)
179  {
181  $this->_parser = new Parser($text);
183  $this->_emitter = new Emitter();
185  $this->register_class_callbacks(
186  array(
187  'span' => array(
188  'subscript'=> array($this,'callback_span_subscript'),
189  'superscript'=> array($this,'callback_span_superscript'),
190  'footnote'=> array($this,'callback_span_footnote'),
191  'comment'=> array($this,'callback_span_comment')),
192  'link' => array(
193  'newwin' => array($this,'callback_link_newwin')),
194  'image' => array(
195  'lframe'=> array($this,'callback_image_frame'),
196  'rframe'=> array($this,'callback_image_frame')),
197  'paragraph' => array(
198  'nop'=> array($this,'callback_paragraph_nop'),
199  'div'=> array($this,'callback_paragraph_div')),
200  'blockdef' => array(
201  'lframe'=> array($this,'callback_blockdef_frame'),
202  'rframe'=> array($this,'callback_blockdef_frame')),
203  'preformatted' => array(
204  'html'=> array($this,'callback_pre_html')),
205  'code' => array(
206  'html'=> array($this,'callback_code_html'))
207  )
208  );
210  $this->register_macro_callbacks(
211  array(
212  'quicktoc' => array($this,'macro_quicktoc')
213  )
214  );
216  $this->register_symlinks(array('Anchor'=>'','Local'=>''));
217  }
218  public function __clone()
219  {
220  $this->_parser = clone $this->_parser;
221  $this->_emitter = clone $this->_emitter;
222  $this->_working_parser = NULL;
223  }
232  public function allow_html($bool = NULL)
233  {
234  if (!is_null($bool))
235  $this->_allow_html = $bool;
236  return $this->_allow_html;
237  }
253  public function register_class_callbacks($callbacks)
254  {
255  $emitter = $this->_emitter;
256  $emitter->register_class_callouts($callbacks);
257  }
273  public function register_property_callbacks($callbacks)
274  {
275  $emitter = $this->_emitter;
276  $emitter->register_property_callouts($callbacks);
277  }
290  public function register_symlinks($symlinks)
291  {
292  $emitter = $this->_emitter;
293  $emitter->register_symlinks($symlinks);
294  }
307  public function register_symlink_handler($handler)
308  {
309  $emitter = $this->_emitter;
310  $emitter->register_symlink_handler($handler);
311  }
317  public function register_rawlink_handler($handler)
318  {
319  $emitter = $this->_emitter;
320  $emitter->register_rawlink_handler($handler);
321  }
328  public function register_charfilter_handler($handler)
329  {
330  $emitter = $this->_emitter;
331  $emitter->register_charfilter_handler($handler);
332  }
338  public function register_blockdef_handler($handler)
339  {
340  $emitter = $this->_emitter;
341  $emitter->register_blockdef_handler($handler);
342  }
354  public function register_macro_callbacks($callbacks)
355  {
356  $emitter = $this->_emitter;
357  $emitter->register_macro_callouts($callbacks);
358  }
372  public function register_events($callbacks)
373  {
374  $emitter = $this->_emitter;
375  $emitter->register_events($callbacks);
376  }
377 
387  public function prepare($raw)
388  {
389  $this->_parser->prepare($raw);
390  return $this;
391  }
395  public function get_html($markup = NULL)
396  {
397  if ($markup) $this->prepare($markup);
398  global $profiles;
399  $profile = array();
400  $profile['parse']['start'] = microtime(true);
401  $dom = $this->_parser->parse();
402  $profile['parse']['end'] = microtime(true);
403  $profiles[] = $profile;
404  $profile = array();
405  $profile['emit']['start'] = microtime(true);
406  $retval = $this->_emitter->emit($dom);
407  $profile['emit']['end'] = microtime(true);
408  $profiles[] = $profile;
409  return $retval;
410  }
422  public function parser($parser = NULL)
423  {
424  if (!is_null($parser))
425  $this->_parser = $parser;
426  return $this->_parser;
427  }
434  public function emitter($emitter = NULL )
435  {
436  if (!is_null($emitter))
437  $this->_emitter = $parser;
438  return $this->_emitter;
439  }
445  public function get_metadata()
446  {
447  return $this->_parser->metadata();
448  }
453  public function get_markerdata()
454  {
455  return $this->_parser->markerdata();
456  }
461  public function get_preprocessed_markup()
462  {
463  return $this->_parser->preprocessed_markup();
464  }
467  #-----------------------------[ STANDARD CLASS CALLBACKS ]-----------------------------#
468 
478  public function callback_paragraph_nop($node)
479  {
480  $node->opentag_head = '';
481  $node->opentag_tail = '';
482  $node->closetag = '';
483  $node->decoration = new StdClass;
484  return $node;
485  }
490  public function callback_paragraph_div($node)
491  {
492  $node->opentag_head = '<div';
493  $node->opentag_tail = '>';
494  $node->closetag = '</div>';
495  unset($node->decoration->classes[array_search('div',$node->decoration->classes)]);
496  return $node;
497  }
502  public function callback_code_html($node)
503  {
504  if ($this->_allow_html)
505  {
506  $node->opentag_head = '';
507  $node->opentag_tail = '';
508  $node->closetag = '';
509  $node->escapecontent = FALSE;
510  $node->decoration = new StdClass;
511  }
512  return $node;
513  }
518  public function callback_blockdef_frame($node)
519  {
520  if ($node->blocktag != 'div') return $node;
521  $node->decoration->classes[] = 'frame';
522  return $node;
523  }
528  public function callback_image_frame($node)
529  {
530  $lframeindex = array_search('lframe',$node->decoration->classes);
531  $rframeindex = array_search('rframe',$node->decoration->classes);
532  if ($lframeindex === FALSE) // must be rframe
533  {
534  unset($node->decoration->classes[$rframeindex]);
535  $orientation = 'rframe';
536  }
537  else // must be lframe;
538  {
539  unset($node->decoration->classes[$lframeindex]);
540  $orientation = 'lframe';
541  }
542  $opentag_head = $node->opentag_head;
543  $opentag_tail = $node->opentag_tail;
544  $opentag_head = "<div class='frame $orientation'>" . $opentag_head;
545  $opentag_tail .= "<br>{$node->caption}</div>";
546  $node->opentag_head = $opentag_head;
547  $node->opentag_tail = $opentag_tail;
548  return $node;
549  }
554  public function callback_pre_html($node)
555  {
556  if ($this->_allow_html)
557  {
558  $node->opentag_head = '';
559  $node->opentag_tail = '';
560  $node->closetag = '';
561  $node->escapecontent = FALSE;
562  $node->decoration = new StdClass;
563  }
564  return $node;
565  }
570  public function callback_span_comment($node)
571  {
572  $node->opentag_head = '';
573  $node->opentag_tail = '';
574  $node->closetag = '';
575  $node->decoration = new StdClass;
576  $node->elementcontent = '';
577  return $node;
578  }
583  public function callback_span_footnote($node)
584  {
585  $footnotes = $this->_footnotes;
586  if (empty($footnotes)) // inititalize footnote system
587  {
588  $footnotes = new StdClass;
589  $footnotes->count = 0;
590  $footnotes->list = array();
591  if (empty($this->_working_parser))
592  $this->_working_parser = new Parser('');
593  $this->register_events(
594  array('onafteremit' =>
595  array($this,'render_footnotes')));
596  }
597  // set aside footnote
598  $footnote = $footnotes->list[] = $node;
599  $count = $footnote->id = ++$footnotes->count;
600  // generate markup for link
601  $parser = $this->_working_parser;
602  $markup =
603  '%s superscript%[[#footnotemarker'
604  . $count
605  . ']][[#footnote'
606  . $count
607  . '|['
608  . $count.']]]%%';
609  $dom = $parser->prepare($markup)->parse();
610  // replace footnote body with reference in body of text
611  $span = $dom->children[0]->children[0]; // document/paragraph
612  $span->parent = $footnote->parent;
613  $footnote->parent = NULL;
614  $this->_footnotes = $footnotes;
615  // create lookups for referenced footnotes
616  $footnote->rendered = FALSE;
617  $footnotereference = @$footnote->decoration->attributes['footnotereference'];
618  if (!empty($footnotereference))
619  {
620  $this->_footnotereferences[$footnotereference][] = $footnote;
621  }
622  // fix and return footnote link span
623  $span->elementcontent = $this->_emitter->emit_children($span);
624  $span = $this->callback_span_superscript($span); // set html elements
625  return $span;
626  }
631  public function callback_span_superscript($node)
632  {
633  $node->opentag_head = '<sup';
634  $node->opentag_tail = '>';
635  $node->closetag = '</sup>';
636  unset($node->decoration->classes[array_search('superscript',$node->decoration->classes)]);
637  return $node;
638  }
643  public function callback_span_subscript($node)
644  {
645  $node->opentag_head = '<sub';
646  $node->opentag_tail = '>';
647  $node->closetag = '</sub>';
648  unset($node->decoration->classes[array_search('subscript',$node->decoration->classes)]);
649  return $node;
650  }
655  public function callback_link_newwin($node)
656  {
657  $node->decoration->attributes['target'] = '_blank';
658  $node->decoration->attributedelimiters['target'] = '"';
659  unset($node->decoration->classes[array_search('newwin',$node->decoration->classes)]);
660  return $node;
661  }
664  #-----------------------------[ DEFAULT MACROS ]----------------------------------#
665 
670  public function macro_quicktoc($node)
671  {
672  $caption = $node->caption;
673  if (!$caption) $caption = 'Table of Contents';
674  # move to root of document
675  $document = $this->_parser->get_selected_ancestor($node,array('document'));
676  # collect all headings
677  $contents = array();
678  $contents = $this->macro_quicktoc_assemble_headings($document,$contents);
679  # set data for content line items
680  $contentheadings = array();
681  $count = 0;
682  foreach ($contents as $heading)
683  {
684  // assign id
685  $count++;
686  $sessionid = 'heading' . $count;
687  $headingid = @$heading->decoration->attributes['id'];
688  if (is_null($headingid))
689  {
690  $headingid = $heading->decoration->attributes['id'] = $sessionid;
691  $heading->decoration->attributedelimiters['id'] = '"';
692  }
693  $heading->decoration->attributes['contentsid'] = $sessionid;
694  $heading->decoration->attributedelimiters['contentsid'] = '"';
695  $contentheading = new StdClass;
696  $contentheading->id = $headingid;
697  // assign text
698  $contentheading->text = $this->_emitter->emit_node_text($heading);
699  // assign level
700  $contentheading->level = $heading->level;
701  $contentheading->nesting = $heading->nesting;
702  $contentheadings[] = $contentheading;
703  }
704  # generate markup for table of contents
705  if (!empty($contentheadings))
706  {
707  $markup = '';
708  // calculate relative depth, beginning with 1
709  $contentdepth = 1;
710  $previouscontentdepth = $contentdepth;
711  $contentdepthstack = array();
712  $flooroffset = 0; // lowest depth, controlled by nesting
713  $flooroffsetstack = array();
714  // make sure there is no change for first item
715  // - markup requires starting depth of 1
716  $previouslevel = $contentheadings[0]->level;
717  $previouslevelstack = array();
718  $previousnestinglevel = $contentheadings[0]->nesting;
719  // process collected elements
720  foreach ($contentheadings as $contentheading)
721  {
722  // calculate depth
723  $level = $contentheading->level;
724  $nestinglevel = $contentheading->nesting;
725  if ($nestinglevel > $previousnestinglevel)
726  { // save state
727  array_push($flooroffsetstack,$flooroffset);
728  array_push($contentdepthstack,$contentdepth);
729  array_push($previouslevelstack,$previouslevel);
730  // set floor
731  $flooroffset = $contentdepth;
732  }
733  elseif ($nestinglevel < $previousnestinglevel)
734  { // restore state
735  if (!empty($flooroffsetstack))
736  {
737  $flooroffset = array_pop($flooroffsetstack);
738  $contentdepth = array_pop($contentdepthstack);
739  $previouslevel = array_pop($previouslevelstack);
740  }
741  }
742  if ($level > $previouslevel)
743  $contentdepth++;
744  elseif ($level < $previouslevel)
745  $contentdepth--;
746  $contentdepth = min($level,$contentdepth);
747  $contentdepth = max($contentdepth,1);
748  $previouslevel = $level;
749  $previousnestinglevel = $nestinglevel;
750  $previouscontentdepth = $contentdepth;
751  // generate markup
752  $markup .=
753  str_repeat('*',$contentdepth + $flooroffset)
754  . '[[#'
755  . $contentheading->id
756  . '|'
757  . $contentheading->text
758  . "]]\n";
759  }
760  // enclose markup
761  $caption = preg_replace('/\\n/','',$caption); // to allow \\ (newline) markup in caption
762  $markup =
763 "(:div id=quicktoc-platform:)\n
764  (:div id=quicktoc-header:)\n
765  |:p div id=quicktoc-caption quicktoc-closed:|%c html%{{{" . $caption . "}}}\n
766  (:divend:)\n
767  (:div id=quicktoc-body:)\n"
768  . $markup . "\n
769  (:divend:)\n
770 (:divend:)\n";
771  # generate html for table of contents
772  // allow html
773  $allowhtml = $this->allow_html();
774  $this->allow_html(TRUE);
775  $wiki = new SimpleWiki($markup);
776  $wiki->register_symlinks($this->emitter()->symlinks());
777  $wiki->register_symlink_handler($this->emitter()->symlink_handler());
778  $node->output = $wiki->get_html();
779  $this->allow_html($allowhtml);
780  }
781  return $node;
782  }
784  protected function macro_quicktoc_assemble_headings($node,$contents)
785  {
786  static $nesting = -1;
787  $nesting++;
788  $isheading = false;
789  if ($node->type == DocNode::HEADING)
790  {
791  $notoc = @$node->decoration->attributes['toc'] == 'no';
792  if ($notoc)
793  {
794  unset($node->decoration->attributes['toc']);
795  }
796  else
797  {
798  $isheading = TRUE;
799  $node->nesting = $nesting;
800  $contents[] = $node;
801  }
802  }
803  if (!$isheading)
804  {
805  $children = $node->children;
806  if (!empty($children))
807  {
808  foreach ($children as $child)
809  {
810  $contents = $this->macro_quicktoc_assemble_headings($child,$contents);
811  }
812  }
813  }
814  $nesting--;
815  return $contents;
816  }
817  #----------------------[ NATIVE EVENT CALLBACKS ]--------------------------#
818 
819  # triggered at onafteremit event...
820  public function render_footnotes($document)
821  {
822  $footnotes = $this->_footnotes->list;
823  $markup = '';
824  foreach ($footnotes as $footnote)
825  {
826  if ($footnote->rendered) continue; // has been rendered as reference to other
827  // render base footnote
828  $id = $footnote->id;
829  $markup .=
830  '* [[#footnotemarker' . $id . '|^]][' . $id .'][[#footnote' . $id . ']]';
831  $footnotename = @$footnote->decoration->attributes['footnotename'];
832  if (!empty($footnotename)) // possibly referenced by others
833  {
834  $references = @$this->_footnotereferences[$footnotename];
835  if (!empty($references)) // add references
836  {
837  foreach ($references as $reference)
838  {
839  $ref = $reference->id;
840  $markup .=
841  ' [[#footnotemarker'
842  . $ref
843  . '|^]]['
844  . $ref
845  .'][[#footnote'
846  . $ref
847  . ']]';
848  $reference->rendered=true;
849  }
850  }
851  }
852  $elementcontent = $footnote->elementcontent;
853  $elementcontent = preg_replace('/\\n/','',$elementcontent); // to allow \\ (newline) markup in footnotes
854  $markup .=
855  ' %c html%{{{'
856  . $elementcontent
857  . "}}}\n";
858  }
859  // wrap footnote block
860  $markup =
861  "\n|:b divider:|\n----\n"
862  . "(:div footnoteblock:)\n======Footnotes:======\n"
863  . $markup
864  . "(:divend:)\n";
865  // allow html
866  $allowhtml = $this->allow_html();
867  $this->allow_html(TRUE);
868  $wiki = new SimpleWiki($markup);
869  $wiki->register_symlinks($this->emitter()->symlinks());
870  $wiki->register_symlink_handler($this->emitter()->symlink_handler());
871  $document->closetag .= $wiki->get_html();
872  $this->allow_html($allowhtml);
873  return $document;
874  }
875  // $wiki->register_events(array('onemit' =>array($wiki,'auto_quicktoc')));
876  public function auto_quicktoc($document)
877  {
878  $markup = '<<quicktoc>>';
879  if (empty($this->_working_parser))
880  $this->_working_parser = new Parser('');
881  $parser = $this->_working_parser;
882  $dom = $parser->prepare($markup)->parse();
883  $tocnode = $dom->children[0];
884  $tocnode->parent = $document;
885  array_unshift($document->children,$tocnode);
886  return $document;
887  }
888 }