Revision [9170]

This is an old revision of ImprovedFormatter made by JavaWoman on 2005-06-12 21:01:42.

 

Improved Formatter

Installed as a WikkaBetaFeatures beta feature on this server as of 2005-06-12.

This is the development page for an improved version of "the Formatter", specifically, the code in ./formatters/wakka.php (as opposed to the AdvancedFormatter page which deals with "advanced" formatting in other ways as well, such as standardized code generation utilities).
 

Why?


While our current (version 1.1.6.0) Formatter is quite capable, it has some quirks and bugs, doesn't always generate valid XHTML (though it tries hard), and misses a few things that would be nice to have or that would enable things that would be nice to have (such as a TableofcontentsAction page TOCs). The improved version presented here tries to address some of these issues (with more likely to follow).


What?


Here is a short summary of what has changed (details below):

The code presented below is still considered a beta version and as such contains many lines of (commented-out) debug code. These will of course be removed before final release. Any reference to line numbers is (for now) to the new (beta) code since this is a complete drop-in replacement for the original file.


Closing open tags


The current version (Wikka 1.1.6.0) of the Formatter has a bit of code contributed by DotMG to close any left-open tags at the very end of a page. While that can solve some problems with rendering and including pages, the code was incomplete in which open tags were closed. A particular problem was still-open lists and indents which weren't handled at all (see "List parsing bug?" on WikkaBugs). Also, this code would directly echo output instead of returning a string as the rest of the Formatter's main function does.

The new version addresses all of these problems.

Closing of indents and (open) lists was already happening when encountering a newline that doesn't start with a TAB or a ~, so this bit is separated out as a function:

  1. if (!function_exists('close_indents'))
  2. {
  3.     function close_indents(&$indentClosers,&$oldIndentLevel,&$oldIndentLength,&$newIndentSpace)
  4.     {
  5.         $result='';
  6.  
  7.         $c = count($indentClosers);
  8.         for ($i = 0; $i < $c; $i++)
  9.         {
  10.             $result .= array_pop($indentClosers);
  11.             $br = 0;
  12.         }
  13.         $oldIndentLevel = 0;
  14.         $oldIndentLength= 0;
  15.         $newIndentSpace=array();
  16.  
  17.         return $result;
  18.     }
  19. }

The section that handles newlines now only needs to call this function:
  1.             $result .= close_indents($indentClosers,$oldIndentLevel,$oldIndentLength,$newIndentSpace);
  2.  
  3.             $result .= ($br) ? "<br />\n" : "\n";
  4.             $br = 1;
  5.             return $result;


To close open tags at the end of the page, the new code now calls this function first, and then handles all other open tags, in an order to at least minimize incorrect tag nesting (but see "Not a compete solution!" below):

  1.         if ((!is_array($things)) && ($things == 'closetags'))
  2.         {
  3.             $result .= close_indents($indentClosers,$oldIndentLevel,$oldIndentLength,$newIndentSpace);
  4.  
  5.             if ($trigger_bold % 2) $result .= '</strong>';
  6.             if ($trigger_italic % 2) $result .= '</em>';
  7.             if ($trigger_keys % 2) $result .= '</kbd>';
  8.             if ($trigger_monospace % 2) $result .= '</tt>';
  9.  
  10.             if ($trigger_underline % 2) $result .= '</span>';
  11.             if ($trigger_notes % 2) $result .= '</span>';
  12.             if ($trigger_strike % 2) $result .= '</span>';
  13.             if ($trigger_inserted % 2) $result .= '</span>';
  14.             if ($trigger_deleted % 2) $result .= '</span>';
  15.  
  16.             if ($trigger_center % 2) $result .= '</div>';
  17.             if ($trigger_floatl % 2) $result .= '</div>';
  18.             if ($trigger_floatr % 2) $result .= '</div>';                   # JW added
  19.             for ($i = 1; $i<=5; $i ++)
  20.             {
  21.                 if ($trigger_l[$i] % 2) $result .= ("</h$i>");
  22.             }
  23.  
  24.             $trigger_bold = $trigger_italic = $trigger_keys = $trigger_monospace = 0;
  25.             $trigger_underline = $trigger_notes = $trigger_strike = $trigger_inserted = $trigger_deleted = 0;
  26.             $trigger_center = $trigger_floatl = $trigger_floatr = 0;
  27.             $trigger_l = array(-1, 0, 0, 0, 0, 0);
  28.             return $result;
  29.         }
  30.         else
  31.         {
  32.             $thing = $things[1];
  33.         }

This is now used like this:
  1. $text .= wakka2callback('closetags');                   # JW changed logic


Not a complete solution!
A big problem remains, however: in order to produce valid (X)HTML, open tags cannot just be closed anywhere: there are rules for which elements can contain which other elements. For instance, an inline element (like <em>) can never contain a block element (like a list). So if the inline element is left open (which happens if someone types // to start emphasized text but doesn't close it before starting an indent or list), closing the generated opening <em> tag at the end of the page may prevent display problems in some browsers, but the result is still not valid (X)HTML. This type of problem can only be really addressed with completely different mechanism for a formatter. This should definitely be tackled at some time, but is outside the scope of the current improvements which are designed to work within the current Formatter's mechanism.

Escaping single ampersands


While there are a few cases where it's actually allowed to use a plain & in HTML, in most cases where an ampersand is not part of an entity reference it needs to be escaped as &amp;. The current (version 1.1.6.0) Formatter escapes the < and > special characters, but not &, so the result may be invalid XHTML.

We need to find the ampersands that are not part of an entity reference. So we first build a RegEx to recorgnize the part of an entity reference that follows the ampersand that starts it; it can be a named entity, or a decimal or a hex numerical entity; and it can be terminated by a semicolon (;) in most cases, but there are a few cases where it's legal to leave off the terminating semicolon. To make it easier to read, we build the RegEx to express all that from its constituent parts:
  1. // define entity patterns
  2. // NOTE most also used in wikka.php for htmlentities_ent(): REGEX library!
  3. $alpha  = '[a-z]+';                         # character entity reference
  4. $numdec = '#[0-9]+';                        # numeric character reference (decimal)
  5. $numhex = '#x[0-9a-f]+';                    # numeric character reference (hexadecimal)
  6. $terminator = ';|(?=($|[\n<]|&lt;))';       # semicolon; or end-of-string, newline or tag
  7. $entitypat = '('.$alpha.'|'.$numdec.'|'.$numhex.')('.$terminator.')';   # defines entity pattern without the starting &
  8. $entityref = '&'.$entitypat;                # entity reference

So now we can define a 'lone' ampersand as one that is not followed by the expression $entitypat:
  1. $loneamp = '&(?!'.$entitypat.')';               # ampersand NOT part of an entity


This then becomes part of the big expression that's used in the preg_replace_callback() near the end of the file, as the last thing to consider before a newline:
  1.     '<|>|'.                                                                             # HTML special chars - after wiki markup!
  2.     $loneamp.'|'.                                                                       # HTML special chars - ampersand NOT part of an enity
  3.     '\n'.                                                                               # new line


Now we can "escape" all HTML special characters, as we should:
  1.         // convert HTML thingies (including ampersand NOT part of entity)
  2.         if ($thing == '<')
  3.             return '&lt;';
  4.         else if ($thing == '>')
  5.             return '&gt;';
  6.         else if ($thing == '&')
  7.             return '&amp;';


Nesting floats


I happened to find that the code for a left float (<<) would terminate a right float (>>) and vice versa. Which would of course likely leave unclosed tags. It turned out that by solving that it actually became possible to nest unlike floats - one level deep, at least. No great feature, but it could be handy at times.The solution is actually quite simple: there was just a single "trigger" to keep track of start and end of a float; keeping a separate trigger for left and right floats (an not generating newlines) is all that's needed:

  1.         // JW 2005-05-23: changed floats handling so they can be nested (one type within another only)
  2.         // float box left
  3.         else if ($thing == '<<')
  4.         {
  5.             #return (++$trigger_floatl % 2 ? '<div class="floatl">'."\n" : "\n</div>\n");
  6.             return (++$trigger_floatl % 2 ? '<div class="floatl">' : '</div>'); # JW changed (no newline)
  7.         }
  8.         // float box right
  9.         else if ($thing == '>>')
  10.         {
  11.             #return (++$trigger_floatl % 2 ? '<div class="floatr">'."\n" : "\n</div>\n");
  12.             return (++$trigger_floatr  % 2 ? '<div class="floatr">' : '</div>');    # JW changed (trigger, no newline)
  13.         }

Note line 111 where we now use a $trigger_floatr instead of $trigger_floatl: this solves the bug and creates a new micro-feature at the same time.

Ids in embedded code


Since in ID must be unique in a page, embedding HTML code, and combining that with generated code, creates a problem. In order to to ensure that the page is valid XHTML, every id attribute must have unique value, regardless where it's coming from.

When generating code that should contain ids, this is simple: just use the GenerateUniqueId makeId() method to generate one, with or without specifying parameters. Still, the result couldconflict with id attributes in embedded HTML code so we must handle those as well.

We analyze the whole block of embedded code, run each id through makeId(); if this method detects the id already exists, it will return an amended value with a sequence suffix; if it finds the id value wasn't valid, it will create a new one and return that. The formatter then replaces every id for which a different value was returned:

  1.         // escaped text
  2.         else if (preg_match('/^""(.*)""$/s', $thing, $matches))
  3.         {
  4. /*
  5. echo 'embedded content<br/>';
  6. */
  7.             // get config
  8. #           $allowed_double_doublequote_html = $wakka->GetConfigValue('double_doublequote_html');
  9.             $ddquotes_policy = $wakka->config['double_doublequote_html'];
  10. /*
  11. echo 'double quotes: '.$ddquotes_policy.'<br/>';
  12. */
  13.             // get embedded code
  14.             $embedded = $matches[1];
  15.             // handle embedded id attributes for 'safe' and 'raw'
  16.             if ($ddquotes_policy == 'safe' || $ddquotes_policy == 'raw')
  17.             {
  18.                 // get tags with id attributes
  19.                 $patTagWithId = '((<[a-z].*?)(id=("|\')(.*?)\\4)(.*?>))';
  20.                 // with PREG_SET_ORDER we get an array for each match: easy to use with list()!
  21.                 // we do the match case-insensitive so we catch uppercase HTML as well;
  22.                 // SafeHTML will treat this but 'raw' may end up with invalid code!
  23.                 $tags2 = preg_match_all('/'.$patTagWithId.'/i',$embedded,$matches2,PREG_SET_ORDER); # use backref to match both single and double quotes
  24. /*
  25. echo '# of matches (2): '.$tags2.'<br/>';
  26. echo '<!--found (set order):'."\n";
  27. print_r($matches2);
  28. echo '-->'."\n";
  29. */
  30.                 // step through code, replacing tags with ids with tags with new ('repaired') ids
  31.                 $tmpembedded = $embedded;
  32.                 $newembedded = '';
  33.                 for ($i=0; $i < $tags2; $i++)
  34.                 {
  35.                     list(,$tag,$tagstart,$attrid,$quote,$id,$tagend) = $matches2[$i];   # $attrid not needed, just for clarity
  36.                     $parts = explode($tag,$tmpembedded,2);              # split in two at matched tag
  37.                     if ($id != ($newid = $wakka->makeId('embed',$id)))  # replace if we got a new value
  38.                     {
  39. /*
  40. echo 'replacing tag - old id: '.$id.' new id: '.$newid.'<br/>';
  41. */
  42.                         $tag = $tagstart.'id='.$quote.$newid.$quote.$tagend;
  43.                     }
  44. /*
  45. echo "<!--old: $tag -->\n";
  46. echo "<!--new: $replacetag -->\n";
  47. */
  48.                     $newembedded .= $parts[0].$tag;                     # append (replacement) tag to first part
  49.                     $tmpembedded  = $parts[1];                          # after tag: next bit to handle
  50.                 }
  51.                 $newembedded .= $tmpembedded;                           # add last part
  52. /*
  53. echo '<!--translation:'."\n";
  54. echo $newembedded;
  55. echo '-->'."\n";
  56. */
  57.             }
  58.             // return (treated) embedded content according to config
  59.             // NOTE: we apply SafeHTML *after* id treatment so it won't be throwing away invalid ids that we're repairing instead!
  60.             switch ($ddquotes_policy)
  61.             {
  62.                 case 'safe':
  63.                     return $wakka->ReturnSafeHTML($newembedded);
  64.                 case 'raw':
  65.                     return $newembedded;                                # may still be invalid code - 'raw' will not be corrected!
  66.                 default:
  67.                     return $this->htmlspecialchars_ent($embedded);      # display only
  68.             }
  69.         }


As long as ids in the embedded code are valid and unique, they remain unchanged because makeId() is called with the 'embed' parameter which tells it not to add an id group prefix.

Still, it is important to remember that ids can only truly be guaranteed to be unique if every bit of code that generates HTML with ids is actually using the makeId() method - and that includes user-contributed extensions.

Heading ids


Creating ids for headings is (you guessed it) the first (and necessary) piece of the puzzle to enable generating TableofcontentsAction page TOCs, but other bits will be needed for that as well, such as actually gathering the references to headings (and their levels), and the ability to link to page fragments (something our WikkaCore current core does not support yet). So: we cannot generate TOCs - yet - but we are getting there; the code is also designed to make it possible to extend it to generate TOCs not just for headings, but also for things like images, tables and code blocks.

A method for generating a TOC has not been decided yet (we may even provide alternatives), but one thing we certainly need is ids for headings (see TableofcontentsAction for more background on this); and even if we do not (yet) generate a TOC, being able to link to a page fragment (the obvious next step) will be useful in itself.

Some thought went into the method of generating the ids: Ideally they should be 'recognizable' so creating links to a page fragment with a heading wil be easy, and they should be as 'constant' as possible so a link to a section remains a link to that section, even if that is moved to a different position on the page, or another is inserted before it. This implies that all methods that simply generate a sequential id will not fulfill our requirements. We also don't burden the writer with coming up with ids (or even needing to think about them): they should be able to just concentrate on the content. Instead, we use following approach:


All this is implemented as an "afterburner" type of formatter which is applied after all basic formatting has already taken place and we already have the XHTML output of that process. This ensures that all headings are taken into account, whether they are generated from Wikak markup or from embedded HTML code. The afterburner preg_replace_callback() function is designed to be extended with other types of code fragments we might want to generate ids (and maybe TableofcontentsAction page TOCs...) for.

The 'afterburner' function is defined like this:
  1. if (!function_exists('wakka3callback'))
  2. {
  3.     /**
  4.      * "Afterburner" formatting: extra handling of already-generated XHTML code.
  5.      *
  6.      * 1.
  7.      * Ensure every heading has an id, either specified or generated. (May be
  8.      * extended to generate section TOC data.)
  9.      * If an id is specified, that is used without any modification.
  10.      * If no id is specified, it is generated on the basis of the heading context:
  11.      * - any image tag is replaced by its alt text (if specified)
  12.      * - all tags are stripped
  13.      * - all characters that are not valid in an id are stripped (except whitespace)
  14.      * - the resulting string is then used by makedId() to generate an id out of it
  15.      *
  16.      * @access  private
  17.      * @uses    Wakka::makeId()
  18.      *
  19.      * @param   array   $things required: matches of the regex in the preg_replace_callback
  20.      * @return  string  heading with an id attribute
  21.      */
  22.     function wakka3callback($things)
  23.     {
  24.         global $wakka;
  25.         $thing = $things[1];
  26.  
  27.         // heading
  28.         if (preg_match('#^<(h[1-6])(.*?)>(.*?)</\\1>$#s', $thing, $matches))    # note that we don't match headings that are not valid XHTML!
  29.         {
  30. /*
  31. echo 'heading:<pre>';
  32. print_r($matches);
  33. echo '</pre>';
  34. */
  35.             list($element,$tagname,$attribs,$heading) = $matches;
  36.  
  37.             #if (preg_match('/(id=("|\')(.*?)\\2)/',$attribs,$matches)) # use backref to match both single and double quotes
  38.             if (preg_match('/(id=("|\')(.*?)\\2)/',$attribs))           # use backref to match both single and double quotes
  39.             {
  40.                 // existing id attribute: nothing to do (assume already treated as embedded code)
  41.                 // @@@ we *may* want to gather ids and heading text for a TOC here ...
  42.                 // heading text should then get partly the same treatment as when we're creating ids:
  43.                 // at least replace images and strip tags - we can leave entities etc. alone - so we end up with
  44.                 // plain text-only
  45.                 // do this if we have a condition set to generate a TOC
  46.                 return $element;
  47.             }
  48.             else
  49.             {
  50.                 // no id: we'll have to create one
  51. #echo 'no id provided - create one<br/>';
  52.                 $tmpheading = trim($heading);
  53.                 // first find and replace any image with its alt text
  54.                 // @@@ can we use preg_match_all here? would it help?
  55.                 while (preg_match('/(<img.*?alt=("|\')(.*?)\\2.*?>)/',$tmpheading,$matches))
  56.                 {
  57. #echo 'image found: '.$tmpheading.'<br/>';
  58.                     # 1 = whole element
  59.                     # 3 = alt text
  60.                     list(,$element, ,$alttext) = $matches;
  61. /*
  62. echo 'embedded image:<pre>';
  63. print_r($matches);
  64. echo '</pre>';
  65. */
  66.                     // gather data for replacement
  67.                     $search  = '/'.str_replace('/','\/',$element).'/';  # whole element (delimiter chars escaped!) @@@ use preg_quote as well?
  68.                     $replace = trim($alttext);                          # alt text
  69. /*
  70. echo 'pat_repl:<pre>';
  71. echo 'search: '.$search.'<br/>';
  72. echo 'search: '.$replace.'<br/>';
  73. echo '</pre>';
  74. */
  75.                     // now replace img tag by corresponding alt text
  76.                     $tmpheading = preg_replace($search,$replace,$tmpheading);   # replace image by alt text
  77.                 }
  78.                 $headingtext = $tmpheading;
  79. #echo 'headingtext (no img): '.$headingtext.'<br/>';
  80.         // @@@ 2005-05-27 now first replace linebreaks <br/> with spaces!!
  81.                 // remove all other tags
  82.                 $headingtext = strip_tags($headingtext);
  83. #echo 'headingtext (no tags): '.$headingtext.'<br/>';
  84.                 // @@@ this all-text result is usable for a TOC!!!
  85.                 // do this if we have a condition set to generate a TOC
  86.  
  87.                 // replace entities that can be interpreted
  88.                 // use default charset ISO-8859-1 because other chars won't be valid for an id anyway
  89.                 $headingtext = html_entity_decode($headingtext,ENT_NOQUOTES);
  90.                 // remove any remaining entities (so we don't end up with strange words and numbers in the id text)
  91.                 $headingtext = preg_replace('/&[#]?.+?;/','',$headingtext);
  92. #echo 'headingtext (entities decoded/removed): '.$headingtext.'<br/>';
  93.                 // finally remove non-id characters (except whitespace which is handled by makeId())
  94.                 $headingtext = preg_replace('/[^A-Za-z0-9_:.-\s]/','',$headingtext);
  95. #echo 'headingtext (id-ready): '.$headingtext.'<br/>';
  96.                 // now create id based on resulting heading text
  97.                 $id = $wakka->makeId('hn',$headingtext);
  98. #echo 'id: '.$id.'<br/>';
  99.  
  100.                 // rebuild element, adding id
  101.                 return '<'.$tagname.$attribs.' id="'.$id.'">'.$heading.'</'.$tagname.'>';
  102.             }
  103.         }
  104.         // other elements to be treated go here (tables, images, code sections...)
  105.     }
  106. }
  107. )


This is called (after the primary formatter) as follows:
  1. // add ids to heading elements
  2. // @@@ LATER:
  3. // - extend with other elements (tables, images, code blocks)
  4. // - also create array(s) for TOC(s)
  5. $idstart = getmicrotime();
  6.     '#('.
  7.     '<h[1-6].*?>.*?</h[1-6]>'.
  8.     // other elements to be treated go here
  9.     ')#ms','wakka3callback',$text);
  10. printf('<!-- Header id generation took %.6f seconds -->'."\n", (getmicrotime() - $idstart));


The result is an id that is almost always derived directly from the heading content, giving a high chance that it will remain constant even if the page content is re-arranged: thus it provides a reliable target for a link.


The Code


Here's the code (all of it). This replaces the file ./formatters/wakka.php.
This incorporates the small change needed to support DarTar's GrabCodeHandler, slightly extended to take advantage of the ability of the new AdvancedFormOpen advanced FormOpen() method to add a class to a form (lines 335-344) so the form can be properly styled.
If you want to test this improved formatter, you should either also grab DarTar's GrabCodeHandler or comment out these lines and uncomment line 334.


  1. <?php
  2.  
  3. // This may look a bit strange, but all possible formatting tags have to be in a single regular expression for this to work correctly. Yup!
  4.  
  5. // #dotmg [many lines] : Unclosed tags fix! For more info, m.randimbisoa@dotmg.net
  6. // JavaWoman - corrected and improved unclosed tags handling, including missing ones and indents
  7.  
  8. if (!function_exists('close_indents'))
  9. {
  10.     function close_indents(&$indentClosers,&$oldIndentLevel,&$oldIndentLength,&$newIndentSpace)
  11.     {
  12.         $result='';
  13.  
  14.         $c = count($indentClosers);
  15.         for ($i = 0; $i < $c; $i++)
  16.         {
  17.             $result .= array_pop($indentClosers);
  18.             $br = 0;
  19.         }
  20.         $oldIndentLevel = 0;
  21.         $oldIndentLength= 0;
  22.         $newIndentSpace=array();
  23.  
  24.         return $result;
  25.     }
  26. }
  27.  
  28. if (!function_exists('wakka2callback'))
  29. {
  30.     function wakka2callback($things)
  31.     {
  32.         $result='';
  33.  
  34.         static $oldIndentLevel = 0;
  35.         static $oldIndentLength= 0;
  36.         static $indentClosers = array();
  37.         static $newIndentSpace= array();
  38.  
  39.         static $br = 1;
  40.  
  41.         static $trigger_bold = 0;
  42.         static $trigger_italic = 0;
  43.         static $trigger_keys = 0;
  44.         static $trigger_monospace = 0;
  45.  
  46.         static $trigger_underline = 0;
  47.         static $trigger_notes = 0;
  48.         static $trigger_strike = 0;
  49.         static $trigger_inserted = 0;
  50.         static $trigger_deleted = 0;
  51.  
  52.         static $trigger_center = 0;
  53.         static $trigger_floatl = 0;
  54.         static $trigger_floatr = 0;                                     # JW added
  55.         static $trigger_l = array(-1, 0, 0, 0, 0, 0);
  56.  
  57.         global $wakka;                                                  # @@@ should be capitalized but requires change in wikka.php (etc.)
  58.  
  59.         if ((!is_array($things)) && ($things == 'closetags'))
  60.         {
  61.             $result .= close_indents($indentClosers,$oldIndentLevel,$oldIndentLength,$newIndentSpace);
  62.  
  63.             if ($trigger_bold % 2) $result .= '</strong>';
  64.             if ($trigger_italic % 2) $result .= '</em>';
  65.             if ($trigger_keys % 2) $result .= '</kbd>';
  66.             if ($trigger_monospace % 2) $result .= '</tt>';
  67.  
  68.             if ($trigger_underline % 2) $result .= '</span>';
  69.             if ($trigger_notes % 2) $result .= '</span>';
  70.             if ($trigger_strike % 2) $result .= '</span>';
  71.             if ($trigger_inserted % 2) $result .= '</span>';
  72.             if ($trigger_deleted % 2) $result .= '</span>';
  73.  
  74.             if ($trigger_center % 2) $result .= '</div>';
  75.             if ($trigger_floatl % 2) $result .= '</div>';
  76.             if ($trigger_floatr % 2) $result .= '</div>';                   # JW added
  77.             for ($i = 1; $i<=5; $i ++)
  78.             {
  79.                 if ($trigger_l[$i] % 2) $result .= ("</h$i>");
  80.             }
  81.  
  82.             $trigger_bold = $trigger_italic = $trigger_keys = $trigger_monospace = 0;
  83.             $trigger_underline = $trigger_notes = $trigger_strike = $trigger_inserted = $trigger_deleted = 0;
  84.             $trigger_center = $trigger_floatl = $trigger_floatr = 0;
  85.             $trigger_l = array(-1, 0, 0, 0, 0, 0);
  86.             return $result;
  87.         }
  88.         else
  89.         {
  90.             $thing = $things[1];
  91.         }
  92.  
  93.         // convert HTML thingies (including ampersand NOT part of entity)
  94.         if ($thing == '<')
  95.             return '&lt;';
  96.         else if ($thing == '>')
  97.             return '&gt;';
  98.         else if ($thing == '&')
  99.             return '&amp;';
  100.         // JW 2005-05-23: changed floats handling so they can be nested (one type within another only)
  101.         // float box left
  102.         else if ($thing == '<<')
  103.         {
  104.             #return (++$trigger_floatl % 2 ? '<div class="floatl">'."\n" : "\n</div>\n");
  105.             return (++$trigger_floatl % 2 ? '<div class="floatl">' : '</div>'); # JW changed (no newline)
  106.         }
  107.         // float box right
  108.         else if ($thing == '>>')
  109.         {
  110.             #return (++$trigger_floatl % 2 ? '<div class="floatr">'."\n" : "\n</div>\n");
  111.             return (++$trigger_floatr % 2 ? '<div class="floatr">' : '</div>'); # JW changed (trigger, no newline)
  112.         }
  113.         // clear floated box
  114.         else if ($thing == '::c::')
  115.         {
  116.             return ('<div class="clear">&nbsp;</div>'."\n");
  117.         }
  118.         // keyboard
  119.         else if ($thing == '#%')
  120.         {
  121.             return (++$trigger_keys % 2 ? '<kbd class="keys">' : '</kbd>');
  122.         }
  123.         // bold
  124.         else if ($thing == '**')
  125.         {
  126.             return (++$trigger_bold % 2 ? '<strong>' : '</strong>');
  127.         }
  128.         // italic
  129.         else if ($thing == '//')
  130.         {
  131.             return (++$trigger_italic % 2 ? '<em>' : '</em>');
  132.         }
  133.         // monospace
  134.         else if ($thing == '##')
  135.         {
  136.             return (++$trigger_monospace % 2 ? '<tt>' : '</tt>');
  137.         }
  138.         // underline
  139.         else if ($thing == '__')
  140.         {
  141.             return (++$trigger_underline % 2 ? '<span class="underline">' : '</span>');
  142.         }
  143.         // notes
  144.         else if ($thing == "''")
  145.         {
  146.             return (++$trigger_notes % 2 ? '<span class="notes">' : '</span>');
  147.         }
  148.         // strikethrough
  149.         else if ($thing == '++')
  150.         {
  151.             return (++$trigger_strike % 2 ? '<span class="strikethrough">' : '</span>');
  152.         }
  153.         // additions
  154.         else if ($thing == '&pound;&pound;')
  155.         {
  156.             return (++$trigger_inserted % 2 ? '<span class="additions">' : '</span>');
  157.         }
  158.         // deletions
  159.         else if ($thing == '&yen;&yen;')
  160.         {
  161.             return (++$trigger_deleted % 2 ? '<span class="deletions">' : '</span>');
  162.         }
  163.         // center
  164.         else if ($thing == '@@')
  165.         {
  166.             return (++$trigger_center % 2 ? '<div class="center">'."\n" : "\n</div>\n");
  167.         }
  168.         // urls
  169.         else if (preg_match('/^([a-z]+:\/\/\S+?)([^[:alnum:]^\/])?$/', $thing, $matches))
  170.         {
  171.             $url = $matches[1];
  172.             if (preg_match('/^(.*)\.(gif|jpg|png)/si', $url)) {
  173.                 return '<img src="$url" alt="image" />'.$matches[2];
  174.             } else
  175.             // Mind Mapping Mod
  176.             if (preg_match('/^(.*)\.(mm)/si', $url)) {
  177.                 return $wakka->Action('mindmap '.$url);
  178.             } else
  179.                 return $wakka->Link($url).$matches[2];
  180.         }
  181.         // header level 5
  182.         else if ($thing == '==')
  183.         {
  184.                 $br = 0;
  185.                 return (++$trigger_l[5] % 2 ? '<h5>' : "</h5>\n");
  186.         }
  187.         // header level 4
  188.         else if ($thing == '===')
  189.         {
  190.                 $br = 0;
  191.                 return (++$trigger_l[4] % 2 ? '<h4>' : "</h4>\n");
  192.         }
  193.         // header level 3
  194.         else if ($thing == '====')
  195.         {
  196.                 $br = 0;
  197.                 return (++$trigger_l[3] % 2 ? '<h3>' : "</h3>\n");
  198.         }
  199.         // header level 2
  200.         else if ($thing == '=====')
  201.         {
  202.                 $br = 0;
  203.                 return (++$trigger_l[2] % 2 ? '<h2>' : "</h2>\n");
  204.         }
  205.         // header level 1
  206.         else if ($thing == '======')
  207.         {
  208.                 $br = 0;
  209.                 return (++$trigger_l[1] % 2 ? '<h1>' : "</h1>\n");
  210.         }
  211.         // forced line breaks
  212.         else if ($thing == "---")
  213.         {
  214.             return '<br />';
  215.         }
  216.         // escaped text
  217.         else if (preg_match('/^""(.*)""$/s', $thing, $matches))
  218.         {
  219. /*
  220. echo 'embedded content<br/>';
  221. */
  222.             // get config
  223. #           $allowed_double_doublequote_html = $wakka->GetConfigValue('double_doublequote_html');
  224.             $ddquotes_policy = $wakka->config['double_doublequote_html'];
  225. /*
  226. echo 'double quotes: '.$ddquotes_policy.'<br/>';
  227. */
  228.             // get embedded code
  229.             $embedded = $matches[1];
  230.             // handle embedded id attributes for 'safe' and 'raw'
  231.             if ($ddquotes_policy == 'safe' || $ddquotes_policy == 'raw')
  232.             {
  233.                 // get tags with id attributes
  234.                 $patTagWithId = '((<[a-z].*?)(id=("|\')(.*?)\\4)(.*?>))';
  235.                 // with PREG_SET_ORDER we get an array for each match: easy to use with list()!
  236.                 // we do the match case-insensitive so we catch uppercase HTML as well;
  237.                 // SafeHTML will treat this but 'raw' may end up with invalid code!
  238.                 $tags2 = preg_match_all('/'.$patTagWithId.'/i',$embedded,$matches2,PREG_SET_ORDER); # use backref to match both single and double quotes
  239. /*
  240. echo '# of matches (2): '.$tags2.'<br/>';
  241. echo '<!--found (set order):'."\n";
  242. print_r($matches2);
  243. echo '-->'."\n";
  244. */
  245.                 // step through code, replacing tags with ids with tags with new ('repaired') ids
  246.                 $tmpembedded = $embedded;
  247.                 $newembedded = '';
  248.                 for ($i=0; $i < $tags2; $i++)
  249.                 {
  250.                     list(,$tag,$tagstart,$attrid,$quote,$id,$tagend) = $matches2[$i];   # $attrid not needed, just for clarity
  251.                     $parts = explode($tag,$tmpembedded,2);              # split in two at matched tag
  252.                     if ($id != ($newid = $wakka->makeId('embed',$id)))  # replace if we got a new value
  253.                     {
  254. /*
  255. echo 'replacing tag - old id: '.$id.' new id: '.$newid.'<br/>';
  256. */
  257.                         $tag = $tagstart.'id='.$quote.$newid.$quote.$tagend;
  258.                     }
  259. /*
  260. echo "<!--old: $tag -->\n";
  261. echo "<!--new: $replacetag -->\n";
  262. */
  263.                     $newembedded .= $parts[0].$tag;                     # append (replacement) tag to first part
  264.                     $tmpembedded  = $parts[1];                          # after tag: next bit to handle
  265.                 }
  266.                 $newembedded .= $tmpembedded;                           # add last part
  267. /*
  268. echo '<!--translation:'."\n";
  269. echo $newembedded;
  270. echo '-->'."\n";
  271. */
  272.             }
  273.             // return (treated) embedded content according to config
  274.             // NOTE: we apply SafeHTML *after* id treatment so it won't be throwing away invalid ids that we're repairing instead!
  275.             switch ($ddquotes_policy)
  276.             {
  277.                 case 'safe':
  278.                     return $wakka->ReturnSafeHTML($newembedded);
  279.                 case 'raw':
  280.                     return $newembedded;                                # may still be invalid code - 'raw' will not be corrected!
  281.                 default:
  282.                     return $this->htmlspecialchars_ent($embedded);      # display only
  283.             }
  284.         }
  285.         // code text
  286.         else if (preg_match('/^% %(.*?)% %$/s', $thing, $matches))  # @@@ REMOVE SPACE between % and %!
  287.         {
  288.             /*
  289.              * Note: this routine is rewritten such that (new) language formatters
  290.              * will automatically be found, whether they are GeSHi language config files
  291.              * or "internal" Wikka formatters.
  292.              * Path to GeSHi language files and Wikka formatters MUST be defined in config.
  293.              * For line numbering (GeSHi only) a starting line can be specified after the language
  294.              * code, separated by a ; e.g., % %(php;27)....% %. # @@@ REMOVE SPACE between % and %!
  295.              * Specifying >= 1 turns on line numbering if this is enabled in the configuration.
  296.              */
  297.             $code = $matches[1];
  298.             // if configuration path isn't set, make sure we'll get an invalid path so we
  299.             // don't match anything in the home directory
  300.             $geshi_hi_path = isset($wakka->config['geshi_languages_path']) ? $wakka->config['geshi_languages_path'] : '/:/';
  301.             $wikka_hi_path = isset($wakka->config['wikka_highlighters_path']) ? $wakka->config['wikka_highlighters_path'] : '/:/';
  302.             // check if a language (and starting line) has been specified
  303.             if (preg_match("/^\((.+?)(;([0-9]+))??\)(.*)$/s", $code, $matches))
  304.             {
  305.                 list(, $language, , $start, $code) = $matches;
  306.             }
  307.             // get rid of newlines at start and end (and preceding/following whitespace)
  308.             // Note: unlike trim(), this preserves any tabs at the start of the first "real" line
  309.             $code = preg_replace('/^\s*\n+|\n+\s*$/','',$code);
  310.  
  311.             // check if GeSHi path is set and we have a GeSHi hilighter for this language
  312.             if (isset($language) && isset($wakka->config['geshi_path']) && file_exists($geshi_hi_path.'/'.$language.'.php'))
  313.             {
  314.                 // use GeSHi for hilighting
  315.                 $output = $wakka->GeSHi_Highlight($code, $language, $start);
  316.             }
  317.             // check Wikka highlighter path is set and if we have an internal Wikka hilighter
  318.             elseif (isset($language) && isset($wakka->config['wikka_formatter_path']) && file_exists($wikka_hi_path.'/'.$language.'.php') && 'wakka' != $language)
  319.             {
  320.                 // use internal Wikka hilighter
  321.                 $output = '<div class="code">'."\n";
  322.                 $output .= $wakka->Format($code, $language);
  323.                 $output .= "</div>\n";
  324.             }
  325.             // no language defined or no formatter found: make default code block;
  326.             // IncludeBuffered() will complain if 'code' formatter doesn't exist
  327.             else
  328.             {
  329.                 $output = '<div class="code">'."\n";
  330.                 $output .= $wakka->Format($code, 'code');
  331.                 $output .= "</div>\n";
  332.             }
  333.  
  334.             #return $output;
  335.             // START DarTar modified 2005-02-17
  336.             // slight mod JavaWoman 2005-06-12: coding style, class for form
  337.                 //build form
  338.                 $form  = $wakka->FormOpen('grabcode','','post','','grabcode');
  339.                 $form .= '<input type="submit" name="save" class="grabcodebutton" style="line-height:10px; float:right; vertical-align: middle; margin-right:20px; margin-top:0px; font-size: 10px; color: #000; font-weight: normal; font-family: Verdana, Arial, sans-serif; background-color: #DDD; text-decoration: none; height:18px;" value="Grab" title="Download this code" />';
  340.                 $form .= '<input type="hidden" name="code" value="'.urlencode($code).'" />';
  341.                 $form .= $wakka->FormClose();
  342.                 // output
  343.                 return $output."\n".$form;
  344.             // END DarTar modified 2005-02-17
  345.  
  346.         }
  347.         // forced links
  348.         // \S : any character that is not a whitespace character
  349.         // \s : any whitespace character
  350.         else if (preg_match('/^\[\[(\S*)(\s+(.+))?\]\]$/s', $thing, $matches))      # recognize forced links across lines
  351.         {
  352.             list(, $url, , $text) = $matches;
  353.             if ($url)
  354.             {
  355.                 //if ($url!=($url=(preg_replace("/@@|&pound;&pound;||\[\[/","",$url))))$result="</span>";
  356.                 if (!$text) $text = $url;
  357.                 //$text=preg_replace("/@@|&pound;&pound;|\[\[/","",$text);
  358.                 return $result.$wakka->Link($url,'', $text);
  359.             }
  360.             else
  361.             {
  362.                 return '';
  363.             }
  364.         }
  365.         // indented text
  366.         elseif (preg_match('/\n([\t~]+)(-|&|([0-9a-zA-ZÄÖÜßäöü]+)\))?(\n|$)/s', $thing, $matches))
  367.         {
  368.             // new line
  369.             $result .= ($br) ? "<br />\n" : "\n";
  370.  
  371.             // we definitely want no line break in this one.
  372.             $br = 0;
  373.  
  374.             // find out which indent type we want
  375.             $newIndentType = $matches[2];
  376.             if (!$newIndentType) { $opener = '<div class="indent">'; $closer = '</div>'; $br = 1; }
  377.             elseif ($newIndentType == '-') { $opener = '<ul><li>'; $closer = '</li></ul>'; $li = 1; }
  378.             elseif ($newIndentType == '&') { $opener = '<ul class="thread"><li>'; $closer = '</li></ul>'; $li = 1; } #inline comments
  379.             else { $opener = '<ol type="'.substr($newIndentType, 0, 1).'"><li>'; $closer = '</li></ol>'; $li = 1; }
  380.  
  381.             // get new indent level
  382.             $newIndentLevel = strlen($matches[1]);
  383.             if ($newIndentLevel > $oldIndentLevel)
  384.             {
  385.                 for ($i = 0; $i < $newIndentLevel - $oldIndentLevel; $i++)
  386.                 {
  387.                     $result .= $opener;
  388.                     array_push($indentClosers, $closer);
  389.                 }
  390.             }
  391.             else if ($newIndentLevel < $oldIndentLevel)
  392.             {
  393.                 for ($i = 0; $i < $oldIndentLevel - $newIndentLevel; $i++)
  394.                 {
  395.                     $result .= array_pop($indentClosers);
  396.                 }
  397.             }
  398.  
  399.             $oldIndentLevel = $newIndentLevel;
  400.  
  401.             if (isset($li) && !preg_match('/'.str_replace(')','\)',$opener).'$/', $result))
  402.             {
  403.                 $result .= '</li><li>';
  404.             }
  405.  
  406.             return $result;
  407.         }
  408.         // new lines
  409.         else if ($thing == "\n")
  410.         {
  411.             // if we got here, there was no tab in the next line; this means that we can close all open indents.
  412.             // @@@ JW: we need to do the same thing at the end of the page to close indents NOT followed by newline
  413.             /*
  414.             $c = count($indentClosers);
  415.             for ($i = 0; $i < $c; $i++)
  416.             {
  417.                 $result .= array_pop($indentClosers);
  418.                 $br = 0;
  419.             }
  420.             $oldIndentLevel = 0;
  421.             $oldIndentLength= 0;
  422.             $newIndentSpace=array();
  423.             */
  424.             $result .= close_indents($indentClosers,$oldIndentLevel,$oldIndentLength,$newIndentSpace);
  425.  
  426.             $result .= ($br) ? "<br />\n" : "\n";
  427.             $br = 1;
  428.             return $result;
  429.         }
  430.         // Actions
  431.         else if (preg_match('/^\{\{(.*?)\}\}$/s', $thing, $matches))
  432.         {
  433.             if ($matches[1])
  434.                 return $wakka->Action($matches[1]);
  435.             else
  436.                 return '{{}}';
  437.         }
  438.         // interwiki links!
  439.         else if (preg_match('/^[A-ZÄÖÜ][A-Za-zÄÖÜßäöü]+[:]\S*$/s', $thing))
  440.         {
  441.             return $wakka->Link($thing);
  442.         }
  443.         // wiki links!
  444.         else if (preg_match('/^[A-ZÄÖÜ]+[a-zßäöü]+[A-Z0-9ÄÖÜ][A-Za-z0-9ÄÖÜßäöü]*$/s', $thing))
  445.         {
  446.             return $wakka->Link($thing);
  447.         }
  448.         // separators
  449.         else if (preg_match('/-{4,}/', $thing, $matches))
  450.         {
  451.             // TODO: This could probably be improved for situations where someone puts text on the same line as a separator.
  452.             //       Which is a stupid thing to do anyway! HAW HAW! Ahem.
  453.             $br = 0;
  454.             return "<hr />\n";
  455.         }
  456.         // mind map xml
  457.         else if (preg_match('/^<map.*<\/map>$/s', $thing))
  458.         {
  459.             return $wakka->Action('mindmap '.$wakka->Href().'/mindmap.mm');
  460.         }
  461.         // if we reach this point, it must have been an accident.
  462.         // @@@ JW: or a detailed regex that excludes something that was included in the
  463.         //     preg_replace_callback expression
  464.         return $thing;
  465.     }
  466. }
  467.  
  468. if (!function_exists('wakka3callback'))
  469. {
  470.     /**
  471.      * "Afterburner" formatting: extra handling of already-generated XHTML code.
  472.      *
  473.      * 1.
  474.      * Ensure every heading has an id, either specified or generated. (May be
  475.      * extended to generate section TOC data.)
  476.      * If an id is specified, that is used without any modification.
  477.      * If no id is specified, it is generated on the basis of the heading context:
  478.      * - any image tag is replaced by its alt text (if specified)
  479.      * - all tags are stripped
  480.      * - all characters that are not valid in an id are stripped (except whitespace)
  481.      * - the resulting string is then used by makedId() to generate an id out of it
  482.      *
  483.      * @access  private
  484.      * @uses    Wakka::makeId()
  485.      *
  486.      * @param   array   $things required: matches of the regex in the preg_replace_callback
  487.      * @return  string  heading with an id attribute
  488.      */
  489.     function wakka3callback($things)
  490.     {
  491.         global $wakka;
  492.         $thing = $things[1];
  493.  
  494.         // heading
  495.         if (preg_match('#^<(h[1-6])(.*?)>(.*?)</\\1>$#s', $thing, $matches))    # note that we don't match headings that are not valid XHTML!
  496.         {
  497. /*
  498. echo 'heading:<pre>';
  499. print_r($matches);
  500. echo '</pre>';
  501. */
  502.             list($element,$tagname,$attribs,$heading) = $matches;
  503.  
  504.             #if (preg_match('/(id=("|\')(.*?)\\2)/',$attribs,$matches)) # use backref to match both single and double quotes
  505.             if (preg_match('/(id=("|\')(.*?)\\2)/',$attribs))           # use backref to match both single and double quotes
  506.             {
  507.                 // existing id attribute: nothing to do (assume already treated as embedded code)
  508.                 // @@@ we *may* want to gather ids and heading text for a TOC here ...
  509.                 // heading text should then get partly the same treatment as when we're creating ids:
  510.                 // at least replace images and strip tags - we can leave entities etc. alone - so we end up with
  511.                 // plain text-only
  512.                 // do this if we have a condition set to generate a TOC
  513.                 return $element;
  514.             }
  515.             else
  516.             {
  517.                 // no id: we'll have to create one
  518. #echo 'no id provided - create one<br/>';
  519.                 $tmpheading = trim($heading);
  520.                 // first find and replace any image with its alt text
  521.                 // @@@ can we use preg_match_all here? would it help?
  522.                 while (preg_match('/(<img.*?alt=("|\')(.*?)\\2.*?>)/',$tmpheading,$matches))
  523.                 {
  524. #echo 'image found: '.$tmpheading.'<br/>';
  525.                     # 1 = whole element
  526.                     # 3 = alt text
  527.                     list(,$element, ,$alttext) = $matches;
  528. /*
  529. echo 'embedded image:<pre>';
  530. print_r($matches);
  531. echo '</pre>';
  532. */
  533.                     // gather data for replacement
  534.                     $search  = '/'.str_replace('/','\/',$element).'/';  # whole element (delimiter chars escaped!) @@@ use preg_quote as well?
  535.                     $replace = trim($alttext);                          # alt text
  536. /*
  537. echo 'pat_repl:<pre>';
  538. echo 'search: '.$search.'<br/>';
  539. echo 'search: '.$replace.'<br/>';
  540. echo '</pre>';
  541. */
  542.                     // now replace img tag by corresponding alt text
  543.                     $tmpheading = preg_replace($search,$replace,$tmpheading);   # replace image by alt text
  544.                 }
  545.                 $headingtext = $tmpheading;
  546. #echo 'headingtext (no img): '.$headingtext.'<br/>';
  547.         // @@@ 2005-05-27 now first replace linebreaks <br/> with spaces!!
  548.                 // remove all other tags
  549.                 $headingtext = strip_tags($headingtext);
  550. #echo 'headingtext (no tags): '.$headingtext.'<br/>';
  551.                 // @@@ this all-text result is usable for a TOC!!!
  552.                 // do this if we have a condition set to generate a TOC
  553.  
  554.                 // replace entities that can be interpreted
  555.                 // use default charset ISO-8859-1 because other chars won't be valid for an id anyway
  556.                 $headingtext = html_entity_decode($headingtext,ENT_NOQUOTES);
  557.                 // remove any remaining entities (so we don't end up with strange words and numbers in the id text)
  558.                 $headingtext = preg_replace('/&[#]?.+?;/','',$headingtext);
  559. #echo 'headingtext (entities decoded/removed): '.$headingtext.'<br/>';
  560.                 // finally remove non-id characters (except whitespace which is handled by makeId())
  561.                 $headingtext = preg_replace('/[^A-Za-z0-9_:.-\s]/','',$headingtext);
  562. #echo 'headingtext (id-ready): '.$headingtext.'<br/>';
  563.                 // now create id based on resulting heading text
  564.                 $id = $wakka->makeId('hn',$headingtext);
  565. #echo 'id: '.$id.'<br/>';
  566.  
  567.                 // rebuild element, adding id
  568.                 return '<'.$tagname.$attribs.' id="'.$id.'">'.$heading.'</'.$tagname.'>';
  569.             }
  570.         }
  571.         // other elements to be treated go here (tables, images, code sections...)
  572.     }
  573. }
  574.  
  575.  
  576. $text = str_replace("\r\n", "\n", $text);
  577.  
  578. // replace 4 consecutive spaces at the beginning of a line with tab character
  579. // $text = preg_replace("/\n[ ]{4}/", "\n\t", $text); // moved to edit.php
  580.  
  581. if ($this->method == 'show') $mind_map_pattern = '<map.*?<\/map>|'; else $mind_map_pattern = '';
  582.  
  583. // define entity patterns
  584. // NOTE most also used in wikka.php for htmlentities_ent(): REGEX library!
  585. $alpha  = '[a-z]+';                         # character entity reference
  586. $numdec = '#[0-9]+';                        # numeric character reference (decimal)
  587. $numhex = '#x[0-9a-f]+';                    # numeric character reference (hexadecimal)
  588. $terminator = ';|(?=($|[\n<]|&lt;))';       # semicolon; or end-of-string, newline or tag
  589. $entitypat = '('.$alpha.'|'.$numdec.'|'.$numhex.')('.$terminator.')';   # defines entity pattern without the starting &
  590. $entityref = '&'.$entitypat;                # entity reference
  591. $loneamp = '&(?!'.$entitypat.')';           # ampersand NOT part of an entity
  592.  
  593.     '/('.
  594.     '% %.*?% %|'.                                                                           # code  # @@@ REMOVE SPACE between % and %!
  595.     '"".*?""|'.                                                                         # literal
  596.     $mind_map_pattern.
  597.     '\[\[[^\[]*?\]\]|'.                                                                 # forced link
  598.     '-{4,}|---|'.                                                                       # separator, new line
  599.     '\b[a-z]+:\/\/\S+|'.                                                                # URL
  600.     '\*\*|\'\'|\#\#|\#\%|@@|::c::|\>\>|\<\<|&pound;&pound;|&yen;&yen;|\+\+|__|\/\/|'.   # Wiki markup
  601.     '======|=====|====|===|==|'.                                                        # headings
  602.     '\n([\t~]+)(-|&|[0-9a-zA-Z]+\))?|'.                                                 # indents and lists
  603.     '\{\{.*?\}\}|'.                                                                     # action
  604.     '\b[A-ZÄÖÜ][A-Za-zÄÖÜßäöü]+[:](?![=_])\S*\b|'.                                        # InterWiki link
  605.     '\b([A-ZÄÖÜ]+[a-zßäöü]+[A-Z0-9ÄÖÜ][A-Za-z0-9ÄÖÜßäöü]*)\b|'.                            # CamelWords
  606.     '<|>|'.                                                                             # HTML special chars - after wiki markup!
  607.     $loneamp.'|'.                                                                       # HTML special chars - ampersand NOT part of an enity
  608.     '\n'.                                                                               # new line
  609.     ')/ms','wakka2callback',$text);
  610.  
  611. // we're cutting the last <br />
  612. $text = preg_replace('/<br \/>$/','',$text);
  613. $text .= wakka2callback('closetags');                   # JW changed logic
  614.  
  615. // add ids to heading elements
  616. // @@@ LATER:
  617. // - extend with other elements (tables, images, code blocks)
  618. // - also create array(s) for TOC(s)
  619. $idstart = getmicrotime();
  620.     '#('.
  621.     '<h[1-6].*?>.*?</h[1-6]>'.
  622.     // other elements to be treated go here
  623.     ')#ms','wakka3callback',$text);
  624. printf('<!-- Header id generation took %.6f seconds -->'."\n", (getmicrotime() - $idstart));
  625.  
  626. echo $text;
  627.  
  628. ?>


Make sure you replace every occurrence of '% %' in this code with '%%'!


Supporting code


Only a single new WikkaCore core method is needed for this improved formatter (other new functions are part of the formatter script itself):

makeId()


Used here to both for handling ids in embedded HTML code and to generate a unique id for headings; see GenerateUniqueId for the code and where to insert it.


Todo




Test? Comments?


Go ahead and test it - either on your own Wikka installation or on this site where it is now Installed as a WikkaBetaFeatures beta feature.

Comments and suggestions are more than welcome, as always.



CategoryDevelopmentFormatters
There are 7 comments on this page. [Show comments]
Valid XHTML :: Valid CSS: :: Powered by WikkaWiki