Un Wiki stand-alone en PHP (le source)


1 décembre 2007

Composant Wiki en PHP

Voici de manière brute mon composant Wiki écrit en PHP que je décris sur une autre page.

<?
//###TODO : les antislash à la fin d'une ligne c'est facile, et c'est en préprocess

global  $Markers;
$Markers = array(
    "=="=>array("end"=>"\n", "replace"=>"<h2>$0</h2>", "separator"=>false, "recursive"=>true, "TOCLevel"=>1),
    "==="=>array("end"=>"\n", "replace"=>"<h3>$0</h3>", "separator"=>false, "recursive"=>true, "TOCLevel"=>2),
    "===="=>array("end"=>"\n", "replace"=>"<h4>$0</h4>", "separator"=>false, "recursive"=>true, "TOCLevel"=>3),
    "====="=>array("end"=>"\n", "replace"=>"<h5>$0</h5>", "separator"=>false, "recursive"=>true, "TOCLevel"=>4),
    "http://"=>array("end"=>array(" ", "\n"), "replace"=>"<a href='http://$0'>$0</a>", "separator"=>false, "recursive"=>false, "replaceEndMarker"=>false),
    "//"=>array("end"=>"//", "replace"=>"<i>$0</i>", "separator"=>false, "recursive"=>true),
    "**"=>array("end"=>"**", "replace"=>"<b>$0</b>", "separator"=>false, "recursive"=>true),
    "["=>array("end"=>"]", "replace"=>"<a href='$0'>$1+</a>", "separator"=>" ", "recursive"=>false),
    "[{"=>array("end"=>"}]", "replace"=>"<img src='$0' title='$1+' border='0'/>", "separator"=>" ", "recursive"=>false),
    "----"=>array("end"=>false, "replace"=>"<hr/>", "separator"=>false, "recursive"=>false),
    "\n\n"=>array("end"=>false, "replace"=>"<p>", "separator"=>false, "recursive"=>false),
    "\n"=>array("end"=>false, "replace"=>"<br/>", "separator"=>false, "recursive"=>false),
    "`"=>array("end"=>"`", "replace"=>"$0", "separator"=>false, "recursive"=>false),
    "> "=>array("end"=>"\n", "replace"=>"$0<br/>\n", "separator"=>false, "recursive"=>true, "groupBy"=>"<blockquote>$0</blockquote>"),
    "- "=>array("end"=>"\n", "replace"=>"<li>$0</li>", "separator"=>false, "recursive"=>true, "groupBy"=>"<ul>$0</ul>"),
    "# "=>array("end"=>"\n", "replace"=>"<li>$0</li>", "separator"=>false, "recursive"=>true, "groupBy"=>"<ol>$0</ol>"),
    //
    "[[refs]]"=>array("end"=>false, "replacePlugin"=>"WikiPlug_displayRefs"),
    "[[toc]]"=>array("end"=>false, "replacePlugin"=>"WikiPlug_displayTOC", "deferred"=>true),
    "<math>"=>array("end"=>"</math>", "replacePlugin"=>"WikiPlug_displayFormula", "recursive"=>false),
);

global  $WikiTOC;   $WikiTOC = array();
global  $Refs;      $Refs = array();
global  $Debug;     $Debug = false;
global  $Deferred;  $Deferred = array();

function    WikiPlug_displayRefs($src)
{
global  $Refs;
    if (!count($Refs))      return "";
    $Output = "Notes et références :<ul>";
    $RefIndex = 1;
    foreach($Refs as $Ref)      $Output .= "<li>Référence ".$RefIndex++." : <a href='$Ref'>$Ref</a></li>\n";
    $Output .= "</ul>\n";
    return $Output;
}

function    WikiPlug_displayTOC($src)
{
global  $WikiTOC;
    $Output = "";
    foreach($WikiTOC as $Row)       {
        $Output .= "<div style='padding-left:".($Row["level"]*2+1)."em;'><a href='#".$Row["name"]."'>".$Row["title"]."</a></div>";
    }
    return "<div style='padding:0 1em 1em 1em;'><table style='border:1px solid black;'><tr><td><h3>Table des matières</h3>\n$Output</td></tr></table></div>";
}

//Ce script nécessite d'installer le package texgd et la présence de plusieurs répertoires ayant les privilèges d'écriture règlés pour le serveur web (l'utilisateur sous lequel tourne ce script).
function    WikiPlug_displayFormula($exp)
{
    $Formula = str_replace("\\\\", "\\", rawurldecode($exp));
    $PicName = md5($Formula).".png";
    $CacheDir = "mathcache/";
    $PicFilePath = "$CacheDir$PicName";
    $TmpPath = $CacheDir."tmp/";
    //On ne crée l'image que si elle n'existe pas déjà dans le cache
    if (!file_exists($PicFilePath))     {
        $TexGDCommand = "export texgd_src='$Formula'; export texgd_tmpdir=$TmpPath; export texgd_fontdir=".$CacheDir."mathfonts; export texgd_outfile=$PicFilePath; export texgd_texheader=".$CacheDir."header.tex; export texgd_style='\$\$'; export texgd_density=10; export texgd_compressratio='3'; texgd";
        $Ret = exec($TexGDCommand, $Output);
    }
    //On envoie un tag HTML contenant la référence de l'image générée.
    return "<img style='vertical-align:middle;' src='$PicFilePath' border='0' title='$Formula'/>";
}

function    WikiCallPlugin($plugIn, $src)
{
    if (!function_exists($plugIn))      die("Le plug-in de remplacement <b>$plugIn</b> n'existe pas !");
    return call_user_func_array($plugIn, array($src));
}

function    Wiki_flushPreviousBlock($previousMarker, $curBlock, &$output)
{
global  $Markers;
    if (!isset($Markers[$previousMarker]["groupBy"]))       return;
    $BlockPattern = $Markers[$previousMarker]["groupBy"];
    $BlockPattern = str_replace("\$0", $curBlock, $BlockPattern);
    $output .= $BlockPattern;
}

//$level positif indique qu'on est en train de processer le texte Wiki
//$level nul indique qu'on a terminé mais qu'on fait la passe des éléments mis en attente (la TOC par exemple)
function    WikiProcess(&$wikiText, &$output, $level=1)
{
global  $Markers, $Debug, $Refs;
global  $WikiTOC, $Deferred;
if ($Debug)     echo "Entrée dans WikiProcess, level = $level, avec le texte :<blockquote style='border:1px solid red'>".str_replace("\n", "<br/>", $wikiText)."</blockquote>\n";
    //
    $Cursor = 0;
    $StartSegment = 0;
    $WikiTextLn = strlen($wikiText);
    $Marker = "";
    $PreviousMarker = "";
    $MarkerBlock = "";
    while ($Cursor < $WikiTextLn)       {
        //Tout d'abord on détermine le marqueur concerné, si c'en est un
        $MarkerFound = null;
        do  {
            $Char = $wikiText[$Cursor++];
if ($Debug)     echo "on rencontre <b>$Char</b> alors que le marqueur était $Marker.<br/>\n";
            $Marker .= $Char;
            $MarkerLn = strlen($Marker);
            $MarkersFound = array();
            foreach($Markers as $Key=>$Params)      {
                $SubKey = substr($Key, 0, $MarkerLn);
                if ($SubKey == $Marker)     array_push($MarkersFound, $Key);
                if ($Key == $Marker)        $MarkerFound = $Key;
            }
if ($Debug)     {   print_r($MarkersFound); echo "<br/>\n"; }
        }   while($Cursor < $WikiTextLn && count($MarkersFound));
if ($Debug)     if ($Cursor == $WikiTextLn)         echo "Le marqueur ? $MarkerFound, compteur ? ".count($MarkersFound)."<br/>\n";
        if ($MarkerFound)       {       //On revient au marqueur précédent
            if ($Cursor < $WikiTextLn)      $Cursor--;      //On recule d'un caractère
            $SegmentLn = $Cursor-strlen($MarkerFound)-$StartSegment;
            if ($Cursor == $WikiTextLn)     $SegmentLn--;
            $Segment = $SegmentLn > 0 ? substr($wikiText, $StartSegment, $SegmentLn) : "";
if ($Debug)     echo "On a trouvé un marqueur (Taille = ".strlen($MarkerFound).", Cursor = $Cursor, StartSegment = $StartSegment), on émet donc le segment précédent : ".(strlen($Segment) ? htmlentities($Segment) : "<i>Segment vide</i>")."<br/>\n";
            $output .= $Segment;
            //
if ($Debug)     echo "On a trouvé le marqueur <b>".str_replace("\n", "\\n", $MarkerFound)."</b> (Taille : ".strlen($MarkerFound).").<br/>\n";
            $Target =& $Markers[$MarkerFound];
            $EndMarker = $Target["end"];
            if ($EndMarker !== false)       {
                //Cas d'un marqueur qui définit un début et une fin (via un marqueur de fin)
                if (is_array($EndMarker))       {
                    $EndPos = false;
                    foreach($EndMarker as $CurEndMarker)        {
                        $CurEndPos = strpos($wikiText, $CurEndMarker, $Cursor);
                        if ($EndPos === false || $EndPos > $CurEndPos)      {
                            $EndPos = $CurEndPos;
                            $RealEndMarker = $CurEndMarker;
                        }
                    }
                    $EndMarker = $RealEndMarker;
                }   else    $EndPos = strpos($wikiText, $EndMarker, $Cursor);
if ($Debug)     echo "On a trouvé le marqueur de fin (<b>".$Target["end"]."</b>) à la position ".($EndPos === false ? "<i>false</i>" : $EndPos).".<br/>\n";
                $TagContent = substr($wikiText, $Cursor, $EndPos-$Cursor);
                if ($Target["recursive"])   {       //On traite ce marqueur de manière récursive (le contenu marqué peut lui-même être marqué)
                    $TagProcessed = "";
                    WikiProcess($TagContent, $TagProcessed, $level+1);
                }   else    $TagProcessed = $TagContent;
                $Sep = isset($Target["separator"]) ? $Target["separator"] : false;
if ($Debug)     echo "Le séparateur de <b>$MarkerFound</b> est ".($Sep === false ? "<i>false</i>" : $Sep)."<br/>\n";
                if ($Sep === false)     {
if ($Debug)     echo "On va remplacer \$0 par $TagProcessed dans ".htmlentities($Target["replace"])."<br/>\n";
                    if (isset($Target["replacePlugin"]))
                        $Pattern = WikiCallPlugin($Target["replacePlugin"], $TagProcessed);
                    else
                        $Pattern = str_replace("$0", $TagProcessed, $Target["replace"]);
if ($Debug)     echo "<blockquote>$output</blockquote>\n";
                }   else    {
                    $TagProcessed = trim($TagProcessed);
                    $TagItems = explode($Sep, $TagProcessed);
                    $Pattern = $Target["replace"];
                    $Base = $TagItems[0];
                    $Pattern = str_replace('$0', $Base, $Pattern);
                    $Label = "";
                    for($i=1; $i<count($TagItems); $i++)        $Label .= (strlen($Label) ? " " : "").$TagItems[$i];
                    if (!strlen($Label))        {
                        $Refs[] = $Base;
                        $Label = "<sup><span style='font-size:20%;'>[".count($Refs)."]</span></sup>";
                    }
                    $Pattern = str_replace('$1+', $Label, $Pattern);
                }
                //
                if (isset($Target["TOCLevel"]))     {
                    $TOCLevel = $Target["TOCLevel"];
                    $TOCIndex = count($WikiTOC);
                    $SectionName = "section-".md5($TOCIndex);
                    array_push($WikiTOC, array("name"=>$SectionName, "level"=>$TOCLevel, "title"=>$TagProcessed));
                    $Pattern = "<a name='$SectionName'>$Pattern</a>";
                }
                //
                if ($PreviousMarker != $MarkerFound)        {
                    Wiki_flushPreviousBlock($PreviousMarker, $MarkerBlock, $output);
                    $MarkerBlock = "";
                    $BlockPattern = "";
                }
                if (isset($Target["groupBy"]))      $MarkerBlock .= $Pattern;
                else    {
                    $MarkerBlock = "";
                    $output .= $Pattern;
                }
                //
                $Cursor += strlen($TagContent);
                $Cursor += strlen($EndMarker);
                //
                $PreviousMarker = $MarkerFound;
            }   else    {
                //Cas d'un marqueur ponctuel, remplacé simplement
if ($Debug)     echo "On remplace un marqueur ponctuel<br/>\n";
                if (isset($Target["deferred"]) && $Target["deferred"] === true && $level)       {
                    $DeferredIndex = md5(date("c").($level + $Cursor));
                    $Deferred[] = $DeferredIndex;
                    $output .= "{{"."$DeferredIndex:".strlen($MarkerFound).":$MarkerFound"."}}";
                }   else    {
                    if (isset($Target["replacePlugin"]))        $output .= WikiCallPlugin($Target["replacePlugin"], null);
                    else    {
                        $output .= $Target["replace"];
if ($Debug)     echo "Contexte (Cursor = $Cursor) : ".str_replace("\n", "\\n", substr($wikiText, $Cursor, 4))."<br/>\n";
                    }
                }
                $Marker = "";
            }
            if (isset($Target["replaceEndMarker"]) && $Target["replaceEndMarker"] === false)
                $Cursor -= strlen($RealEndMarker);
            $StartSegment = $Cursor;
            $Marker = "";
        }   else    if (!count($MarkersFound))          $Marker = "";
    }
    //
    Wiki_flushPreviousBlock($PreviousMarker, $MarkerBlock, $output);
    $output .= substr($wikiText, $StartSegment);

if ($Debug)     echo "<h3>On est arrivé au bout et le résultat est...</h3><blockquote style='border:1px solid black;'>".htmlentities($output)."</blockquote>\n";
    //On termine le niveau 1, le premier de la récursion, on peut donc passer au niveau 0, celui qui concrétise les éléments en attente.
    if ($level == 1 && count($Deferred))        {
        foreach($Deferred as $Key=>$Index)          {
            $Marker = "{{".$Index.":";
            $EndMarker = "}}";
            $MarkPos = strpos($output, $Marker, 0);
            $ElementPos = strpos($output, ":", $MarkPos+strlen($Marker)) + 1;
            $LnPos = $MarkPos+strlen($Marker);
            $Ln = substr($output, $LnPos, $ElementPos-$LnPos-1);
            $DeferredTxt = substr($output, $ElementPos, $Ln);
            $DeferredProcessed = "";
            WikiProcess($DeferredTxt, $DeferredProcessed, 0);
            $output = substr($output, 0, $MarkPos) . $DeferredProcessed . substr($output, $ElementPos+$Ln+strlen($EndMarker));
        }
    }
}

?>

Un Wiki stand-alone en PHP


7 avril 2007

Présentation

J'avais besoin d'un composant Wiki pour que cette fonctionnalité puisse être inclue dans n'importe quelle application, et pourquoi pas sur ce site même ?

Mon objectif était simple :

  • Un script court et auto-suffisant
  • De la rapidité/performance
  • Une bonne résilience aux données d'entrées invalides
  • La syntaxe Wiki doit être un paramètre du code et pas hardcodée
  • Doit être extensible par le biais de plug-ins (pour de nouveaux objets) ou de templates (pour leurs présentations)

Ce Wiki devait permettre de transformer une chaîne comme :

[[toc]]
==Titre principal

Voici une formule **très importante** pour la compréhension de la suite du chapitre :

<math>I_2=\frac{x-a}{2}f^{\prime\prime}(a)+\int_{a}^x\frac{x-t}{2}f^{(3)}(t)dt</math>

C'est un résultat capital à plus d'un titre :
# Il est élégant
# Il est aisé à dériver
# Il se retient facilement

==Deuxième partie

Texte **totalement** //non-important//

en une présentation HTML (par exemple) comme suit :

Le schéma

Pour obtenir ce résultat, j'aurai pu utiliser un des nombreux wikis disponibles, mais comme d'habitude, j'ai préféré le faire moi-même.

Pour ça, j'ai mixé un lexer et un parseur récursifs dans un même code et mis tout ça sous la forme d'une machine à états, décrite par des données et non pas du code.

Le raisonnement

Le premier constat c'est que les blocs à transformer ont des marqueurs de début et de fin, un mode de concrétisation, ou une template, et des options.

Par exemple pour transformer un bloc en gras le marqueur de début est '**', identique à celui de fin avec la possibilité d'être récursif (ce qui est mis en gras peut lui-même contenir d'autres mises en forme).

Un autre exemple est l'ajoût d'un titre. Cette fois le marqueur de début est '==', celui de fin est la fin de la ligne (\"\\n\") et l'option principale est de référencer le bloc produit dans la table des matières.

Le schéma est donc très simple et j'aboutit à un code de moins de 200 lignes, en comptant les options que je décris ci-dessous.

La syntaxe

'==', '===' etc.pour titres qui seront référencés dans la table des matières.
'http://www.example.com'une adresse web se transforme automatiquement en lien.
//pour mettre en italique un bloc de texte
**pour mettre en gras un bloc de texte
[URL ALT]pour ajouter un texte \"ALT\" qui pointe vers l'URL donnée
[{URL ALT}]pour ajouter une image d'URL donnée qui a \"ALT\" comme texte alternatif
----pour une ligne horizontale (eq. <hr/>)
\\nun simple retour à la ligne (eq. <br/>)
\\n\\ndébut d'un nouveau paragraphe (eq. <p>)
`deux backtick délimitent un bloc qui ne sera pas parsé par le module
\"> \"pour mettre en exergue un bloc de texte multiligne (eq. <blockquote>)
\"- \"un élément d'une liste à puces non-numérotées
\"# \"un élément d'une liste à puces numérotées
[[refs]]indique l'emplacement des notes de bas de page (créés par [URL])
[[toc]]crée la table des matières (contenant des pointeurs vers les titres décrits plus haut)
<math>...</math>insère une formule au format TeX

Les plug-ins

Comme plug-in de base, j'ai inclus celui des formules mathématique que je décris par ailleurs.

Les deux autres sont la possibilité d'avoir des notes en bas de pages (issu des blocs utilisant la notation [...]) et une table des matières automatique, créée à partir des titres.

Applications

Je n'ai pas écrit ce code uniquement pour m'amuser mais parce que l'édition de contenu des applications que je développe à titre professionnel m'imposait d'avoir une façon simple de créer des documents sans outils préalables.

Ainsi des sites commerciaux ou professionnels (donc privés) utilisent ce composant aujourd'hui même.

Reste à faire

La gestion des renvois des notes en bas de page n'est pas élégante du tout, c'est plus du hard-code qu'autre chose.

La gestion des tableaux est à faire, c'est crucial !

Au niveau du visuel, il faut : 1- générer le flux HTML et associer des classes CSS aux éléments comme les titres etc., 2- améliorer les templates existantes (qui sont basiques) et permettre de les modifier ou d'en ajouter de nouvelles, sur le schéma de [[toc]] et [[refs]].

Il faut gérer les antislashes pour indiquer que la ligne se poursuit à la suivante (c'est une pré-passe très courte à faire au début de la fonction principale).

Améliorer les notes en bas de page pour ajouter la possibilité d'ajouter des commentaires aux liens.

Accueil