Def lists required at least a space after : or = 3.2.17 - Franck => Added ££text|lang££ support which gives an 3.2.16 - Franck => Added _indice_ support 3.2.15 - Franck => Added ^exponant^ support 3.2.14 - Franck => Ajout de la gestion d'un fichier externe d'acronymes (fusionné avec le fichier existant) 3.2.13 - Franck => Added = , : support (definition list) 3.2.12 - Franck => PHP 7.2 compliance 3.2.11 - Franck => Added ) aside block support (HTML5 only) 3.2.10 - Franck => Added ""marked text"" support (HTML5 only) 3.2.9 - Franck => est remplacé par pour assurer la compatibilité avec HTML5 3.2.8 - Franck => est remplacé par pour assurer la compatibilité avec HTML5 3.2.7 - Franck => Les styles d'alignement des images sont modifiables via les options 3.2.6 - Franck => Added ``inline html`` support 3.2.5 - Franck => Changed longdesc by title in images 3.2.4 - Olivier => Auto links => Code cleanup 3.2.3 - Olivier => PHP5 Strict 3.2.2 - Olivier => Changement de la gestion des URL spéciales 3.2.1 - Olivier => Changement syntaxe des macros 3.2 - Olivier => Changement de fonctionnement des macros => Passage de fonctions externes pour les macros et les mots wiki 3.1d - Jérôme Lipowicz => antispam - Olivier => centrage d'image 3.1c - Olivier => Possibilité d'échaper les | dans les marqueurs avec \ 3.1b - Nicolas Chachereau => Changement de regexp pour la correction syntaxique 3.1a - Olivier => Bug du Call-time pass-by-reference 3.1 - Olivier => Ajout des macros «««..»»» => Ajout des blocs vides øøø => Ajout du niveau de titre paramétrable => Option de blocage du parseur dans les
=> Titres au format setext (experimental, désactivé)

3.0 - Olivier
=> Récriture du parseur inline, plus d'erreur XHTML
=> Ajout d'une vérification d'intégrité pour les listes
=> Les acronymes sont maintenant dans un fichier texte
=> Ajout d'un tag images ((..)), del --..-- et ins ++..++
=> Plus possible de faire des liens JS [lien|javascript:...]
=> Ajout des notes de bas de page §§...§§
=> Ajout des mots wiki

2.5 - Olivier
=> Récriture du code, plus besoin du saut de ligne entre blocs !=

2.0 - Stephanie
=> correction des PCRE et ajout de fonctionnalités
- Mathieu
=> ajout du strip-tags, implementation des options, reconnaissance automatique d'url, etc.
- Olivier
=> changement de active_link en active_urls
=> ajout des options pour les blocs
=> intégration de l'aide dans le code, avec les options
=> début de quelque chose pour la reconnaissance auto d'url (avec Mat)
 */

class wiki2xhtml
{
    public $__version__ = '3.2.17';

    public $T;
    public $opt;
    public $line;
    public $acro_table;
    public $foot_notes;
    public $macros;
    public $functions;

    public $tags;
    public $open_tags;
    public $close_tags;
    public $custom_tags = [];
    public $all_tags;
    public $tag_pattern;
    public $escape_table;
    public $allowed_inline = [];

    public function __construct()
    {
        # Mise en place des options
        $this->setOpt('active_title', 1); # Activation des titres !!!
        $this->setOpt('active_setext_title', 0); # Activation des titres setext (EXPERIMENTAL)
        $this->setOpt('active_hr', 1); # Activation des 
$this->setOpt('active_lists', 1); # Activation des listes $this->setOpt('active_defl', 1); # Activation des listes de définition $this->setOpt('active_quote', 1); # Activation du
$this->setOpt('active_pre', 1); # Activation du
        $this->setOpt('active_empty', 1); # Activation du bloc vide øøø
        $this->setOpt('active_auto_urls', 0); # Activation de la reconnaissance d'url
        $this->setOpt('active_auto_br', 0); # Activation du saut de ligne automatique (dans les paragraphes)
        $this->setOpt('active_antispam', 1); # Activation de l'antispam pour les emails
        $this->setOpt('active_urls', 1); # Activation des liens []
        $this->setOpt('active_auto_img', 1); # Activation des images automatiques dans les liens []
        $this->setOpt('active_img', 1); # Activation des images (())
        $this->setOpt('active_anchor', 1); # Activation des ancres ~...~
        $this->setOpt('active_em', 1); # Activation du  ''...''
        $this->setOpt('active_strong', 1); # Activation du  __...__
        $this->setOpt('active_br', 1); # Activation du 
%%% $this->setOpt('active_q', 1); # Activation du {{...}} $this->setOpt('active_code', 1); # Activation du @@...@@ $this->setOpt('active_acronym', 1); # Activation des acronymes $this->setOpt('active_ins', 1); # Activation des ++..++ $this->setOpt('active_del', 1); # Activation des --..-- $this->setOpt('active_inline_html', 1); # Activation du HTML inline ``...`` $this->setOpt('active_footnotes', 1); # Activation des notes de bas de page $this->setOpt('active_wikiwords', 0); # Activation des mots wiki $this->setOpt('active_macros', 1); # Activation des macros /// /// $this->setOpt('active_mark', 1); # Activation des "".."" $this->setOpt('active_aside', 1); # Activation du
\n"; } elseif (($close || $mode != $pre_mode) && $pre_type == 'title') { $fl = $this->getOpt('first_title_level'); $fl = $fl + 3; $l = $fl - $pre_mode; return '\n"; } elseif ($close && $pre_type == 'pre') { return "
\n"; } elseif ($close && $pre_type == 'aside') { return "

\n"; } elseif ($close && $pre_type == 'list') { $res = ''; for ($j = 0; $j < strlen($pre_mode); $j++) { if (substr($pre_mode, (0 - $j - 1), 1) == '*') { $res .= "\n\n"; } else { $res .= "\n\n"; } } return $res; } elseif ($close && $pre_type == 'defl') { $res = ''; if ($pre_mode == '=') { $res .= "\n\n"; } else { $res .= "\n\n"; } return $res; } else { return "\n"; } } /* Inline --------------------------------------------------- */ private function __inlineWalk($str, $allow_only = null) { $tree = preg_split($this->tag_pattern, $str, -1, PREG_SPLIT_DELIM_CAPTURE); $res = ''; for ($i = 0; $i < count($tree); $i++) { $attr = ''; if (in_array($tree[$i], array_values($this->open_tags)) && ($allow_only == null || in_array(array_search($tree[$i], $this->open_tags), $allow_only))) { $tag = array_search($tree[$i], $this->open_tags); $tag_type = 'open'; if (($tidy = $this->__makeTag($tree, $tag, $i, $i, $attr, $tag_type)) !== false) { if ($tag != '') { $res .= '<' . $tag . $attr; $res .= ($tag_type == 'open') ? '>' : ' />'; } $res .= $tidy; } else { $res .= $tree[$i]; } } else { $res .= $tree[$i]; } } # Suppression des echappements $res = str_replace($this->escape_table, $this->all_tags, $res); return $res; } private function __makeTag(&$tree, &$tag, $position, &$j, &$attr, &$type) { $res = ''; $closed = false; $itag = $this->close_tags[$tag]; # Recherche fermeture for ($i = $position + 1; $i < count($tree); $i++) { if ($tree[$i] == $itag) { $closed = true; break; } } # Résultat if ($closed) { for ($i = $position + 1; $i < count($tree); $i++) { if ($tree[$i] != $itag) { $res .= $tree[$i]; } else { switch ($tag) { case 'a': $res = $this->__parseLink($res, $tag, $attr, $type); break; case 'img': $type = 'close'; if (($res = $this->__parseImg($res, $attr, $tag)) !== null) { $type = 'open'; } break; case 'abbr': $res = $this->__parseAcronym($res, $attr); break; case 'q': $res = $this->__parseQ($res, $attr); break; case 'i': $res = $this->__parseI($res, $attr); break; case 'anchor': $tag = 'a'; $res = $this->__parseAnchor($res, $attr); break; case 'note': $tag = ''; $res = $this->__parseNote($res); break; case 'inline': $tag = ''; $res = $this->__parseInlineHTML($res); break; case 'word': $res = $this->parseWikiWord($res, $tag, $attr, $type); break; default: $res = $this->__inlineWalk($res); break; } if ($type == 'open' && $tag != '') { $res .= ''; } $j = $i; break; } } return $res; } else { return false; } } private function __splitTagsAttr($str) { $res = preg_split('/(? $v) { $res[$k] = str_replace("\|", '|', $v); } return $res; } # Antispam (Jérôme Lipowicz) private function __antiSpam($str) { $encoded = bin2hex($str); $encoded = chunk_split($encoded, 2, '%'); $encoded = '%' . substr($encoded, 0, strlen($encoded) - 1); return $encoded; } private function __parseLink($str, &$tag, &$attr, &$type) { $n_str = $this->__inlineWalk($str, ['abbr', 'img', 'em', 'strong']); $data = $this->__splitTagsAttr($n_str); $no_image = false; # Only URL in data if (count($data) == 1) { $url = trim($str); $content = strlen($url) > 35 ? substr($url, 0, 35) . '...' : $url; $lang = ''; $title = $url; } elseif (count($data) > 1) { $url = trim($data[1]); $content = $data[0]; $lang = (!empty($data[2])) ? $this->protectAttr($data[2], true) : ''; $title = (!empty($data[3])) ? $data[3] : ''; $no_image = (!empty($data[4])) ? (boolean) $data[4] : false; } # Remplacement si URL spéciale $this->__specialUrls($url, $content, $lang, $title); # On vire les   dans l'url $url = str_replace(' ', ' ', $url); if (preg_match('/^(.+)[.](gif|jpg|jpeg|png)$/', $url) && !$no_image && $this->getOpt('active_auto_img')) { # On ajoute les dimensions de l'image si locale # Idée de Stephanie $img_size = null; if (!preg_match('#[a-zA-Z]+://#', $url)) { if (preg_match('#^/#', $url)) { $path_img = $_SERVER['DOCUMENT_ROOT'] . $url; } else { $path_img = $url; } $img_size = @getimagesize($path_img); } $attr = ' src="' . $this->protectAttr($this->protectUrls($url)) . '"' . $attr .= (count($data) > 1) ? ' alt="' . $this->protectAttr($content) . '"' : ' alt=""'; $attr .= ($lang) ? ' lang="' . $lang . '"' : ''; $attr .= ($title) ? ' title="' . $this->protectAttr($title) . '"' : ''; $attr .= (is_array($img_size)) ? ' ' . $img_size[3] : ''; $tag = 'img'; $type = 'close'; return; } else { if ($this->getOpt('active_antispam') && preg_match('/^mailto:/', $url)) { $content = $content == $url ? preg_replace('%^mailto:%', '', $content) : $content; $url = 'mailto:' . $this->__antiSpam(substr($url, 7)); } $attr = ' href="' . $this->protectAttr($this->protectUrls($url)) . '"'; $attr .= ($lang) ? ' hreflang="' . $lang . '"' : ''; $attr .= ($title) ? ' title="' . $this->protectAttr($title) . '"' : ''; return $content; } } private function __specialUrls(&$url, &$content, &$lang, &$title) { foreach ($this->functions as $k => $v) { if (strpos($k, 'url:') === 0 && strpos($url, substr($k, 4)) === 0) { $res = call_user_func($v, $url, $content); $url = isset($res['url']) ? $res['url'] : $url; $content = isset($res['content']) ? $res['content'] : $content; $lang = isset($res['lang']) ? $res['lang'] : $lang; $title = isset($res['title']) ? $res['title'] : $title; break; } } } private function __parseImg($str, &$attr, &$tag) { $data = $this->__splitTagsAttr($str); $alt = ''; $attr = ''; $align_attr = ''; $url = $data[0]; if (!empty($data[1])) { $alt = $data[1]; } $attr = ' src="' . $this->protectAttr($this->protectUrls($url)) . '"'; $attr .= ' alt="' . $this->protectAttr($alt) . '"'; if (!empty($data[2])) { $data[2] = strtoupper($data[2]); $style = ''; if ($data[2] == 'G' || $data[2] == 'L') { $style = $this->getOpt('img_style_left'); } elseif ($data[2] == 'D' || $data[2] == 'R') { $style = $this->getOpt('img_style_right'); } elseif ($data[2] == 'C') { $style = $this->getOpt('img_style_center'); } if ($style != '') { $align_attr = ' style="' . $style . '"'; } } if (empty($data[4])) { $attr .= $align_attr; } if (!empty($data[3])) { $attr .= ' title="' . $this->protectAttr($data[3]) . '"'; } if (!empty($data[4])) { $tag = 'figure'; $img = ''; $img .= '
' . $this->protectAttr($data[4]) . '
'; $attr = $align_attr; return $img; } return; } private function __parseQ($str, &$attr) { $str = $this->__inlineWalk($str); $data = $this->__splitTagsAttr($str); $content = $data[0]; $lang = (!empty($data[1])) ? $this->protectAttr($data[1], true) : ''; $attr .= (!empty($lang)) ? ' lang="' . $lang . '"' : ''; $attr .= (!empty($data[2])) ? ' cite="' . $this->protectAttr($this->protectUrls($data[2])) . '"' : ''; return $content; } private function __parseI($str, &$attr) { $str = $this->__inlineWalk($str); $data = $this->__splitTagsAttr($str); $content = $data[0]; $lang = (!empty($data[1])) ? $this->protectAttr($data[1], true) : ''; $attr .= (!empty($lang)) ? ' lang="' . $lang . '"' : ''; return $content; } private function __parseAnchor($str, &$attr) { $name = $this->protectAttr($str, true); if ($name != '') { $attr = ' id="' . $name . '"'; } return; } private function __parseNote($str) { $i = count($this->foot_notes) + 1; $id = $this->getOpt('note_prefix') . '-' . $i; $this->foot_notes[$id] = $this->__inlineWalk($str); return '\[' . $i . '\]'; } private function __parseInlineHTML($str) { return str_replace(['>', '<'], ['>', '<'], $str); } # Obtenir un acronyme private function __parseAcronym($str, &$attr) { $data = $this->__splitTagsAttr($str); $acronym = $data[0]; $title = $lang = ''; if (count($data) > 1) { $title = $data[1]; $lang = (!empty($data[2])) ? $this->protectAttr($data[2], true) : ''; } if ($title == '' && !empty($this->acro_table[$acronym])) { $title = $this->acro_table[$acronym]; } $attr = ($title) ? ' title="' . $this->protectAttr($title) . '"' : ''; $attr .= ($lang) ? ' lang="' . $lang . '"' : ''; return $acronym; } # Définition des acronymes, dans le fichier acronyms.txt private function __getAcronyms() { $file = $this->getOpt('acronyms_file'); $res = []; if (file_exists($file)) { if (($fc = @file($file)) !== false) { foreach ($fc as $v) { $v = trim($v); if ($v != '') { $p = strpos($v, ':'); $K = (string) trim(substr($v, 0, $p)); $V = (string) trim(substr($v, ($p + 1))); if ($K) { $res[$K] = $V; } } } } } return $res; } # Mots wiki (pour héritage) private function parseWikiWord($str, &$tag, &$attr, &$type) { $tag = $attr = ''; if (isset($this->functions['wikiword'])) { return call_user_func($this->functions['wikiword'], $str); } return $str; } /* Protection des attributs */ private function protectAttr($str, $name = false) { if ($name && !preg_match('/^[A-Za-z][A-Za-z0-9_:.-]*$/', $str)) { return ''; } return str_replace(["'", '"'], [''', '"'], $str); } /* Protection des urls */ private function protectUrls($str) { if (preg_match('/^javascript:/', $str)) { $str = '#'; } return $str; } /* Auto BR */ private function __autoBR($m) { return $m[1] . str_replace("\n", "
\n", $m[2]) . $m[3]; } /* Macro --------------------------------------------------- */ private function __getMacro($s) { $s = is_array($s) ? $s[1] : $s; $this->macros[] = str_replace('\"', '"', $s); return 'øøø##########MACRO#' . (count($this->macros) - 1) . '#'; } private function __putMacro($id) { $id = is_array($id) ? (integer) $id[1] : (integer) $id; if (isset($this->macros[$id])) { $content = str_replace("\r", '', $this->macros[$id]); $c = explode("\n", $content); # première ligne, premier mot $fl = trim($c[0]); $fw = $fl; if ($fl) { if (strpos($fl, ' ') !== false) { $fw = substr($fl, 0, strpos($fl, ' ')); } $content = implode("\n", array_slice($c, 1)); } if ($c[0] == "\n") { $content = implode("\n", array_slice($c, 1)); } if ($fw) { if (isset($this->functions['macro:' . $fw])) { return call_user_func($this->functions['macro:' . $fw], $content, $fl); } } # Si on n'a rien pu faire, on retourne le tout sous # forme de
            return '
' . htmlspecialchars($this->macros[$id]) . '
'; } return; } private function __macroHTML($s) { return $s; } /* Aide et debug --------------------------------------------------- */ public function help() { $help['b'] = []; $help['i'] = []; $help['b'][] = 'Laisser une ligne vide entre chaque bloc de même nature.'; $help['b'][] = 'Paragraphe : du texte et une ligne vide'; if ($this->getOpt('active_title')) { $help['b'][] = 'Titre : !!!, !!, ' . '! pour des titres plus ou moins importants'; } if ($this->getOpt('active_hr')) { $help['b'][] = 'Trait horizontal : ----'; } if ($this->getOpt('active_lists')) { $help['b'][] = 'Liste : ligne débutant par * ou ' . '#. Il est possible de mélanger les listes ' . '(*#*) pour faire des listes de plusieurs niveaux. ' . 'Respecter le style de chaque niveau'; } if ($this->getOpt('active_defl')) { $help['b'][] = 'Liste de définitions : terme(s) débutant(s) par =, ' . 'définition(s) débutant(s) par :.'; } if ($this->getOpt('active_pre')) { $help['b'][] = 'Texte préformaté : espace devant chaque ligne de texte'; } if ($this->getOpt('active_quote')) { $help['b'][] = 'Bloc de citation : > ou ' . ';: devant chaque ligne de texte'; } if ($this->getOpt('active_aside')) { $help['b'][] = ' : ) devant chaque ligne de texte'; } if ($this->getOpt('active_fr_syntax')) { $help['i'][] = 'La correction de ponctuation est active. Un espace ' . 'insécable remplacera automatiquement tout espace ' . 'précédant les marques ";","?",":" et "!".'; } if ($this->getOpt('active_em')) { $help['i'][] = 'Emphase : deux apostrophes \'\'texte\'\''; } if ($this->getOpt('active_strong')) { $help['i'][] = 'Forte emphase : deux soulignés __texte__'; } if ($this->getOpt('active_br')) { $help['i'][] = 'Retour forcé à la ligne : %%%'; } if ($this->getOpt('active_ins')) { $help['i'][] = 'Insertion : deux plus ++texte++'; } if ($this->getOpt('active_del')) { $help['i'][] = 'Suppression : deux moins --texte--'; } if ($this->getOpt('active_mark')) { $help['i'][] = 'Texte marqué : deux guillemets ""texte""'; } if ($this->getOpt('active_sup')) { $help['i'][] = 'Exposant : un accent circonflexe ^texte^'; } if ($this->getOpt('active_sub')) { $help['i'][] = 'Indice : un souligné _texte_'; } if ($this->getOpt('active_urls')) { $help['i'][] = 'Lien : [url], [nom|url], ' . '[nom|url|langue] ou [nom|url|langue|titre].'; $help['i'][] = 'Image : comme un lien mais avec une extension d\'image.' . '
Pour désactiver la reconnaissance d\'image mettez 0 dans un dernier ' . 'argument. Par exemple [image|image.gif||0] fera un lien vers l\'image au ' . 'lieu de l\'afficher.' . '
Il est conseillé d\'utiliser la nouvelle syntaxe.'; } if ($this->getOpt('active_img')) { $help['i'][] = 'Image (nouvelle syntaxe) : ' . '((url|texte alternatif)), ' . '((url|texte alternatif|position)) ou ' . '((url|texte alternatif|position|description longue)). ' . '
La position peut prendre les valeur L ou G (gauche), R ou D (droite) ou C (centré).'; } if ($this->getOpt('active_anchor')) { $help['i'][] = 'Ancre : ~ancre~'; } if ($this->getOpt('active_acronym')) { $help['i'][] = 'Acronyme : ??acronyme?? ou ' . '??acronyme|titre??'; } if ($this->getOpt('active_q')) { $help['i'][] = 'Citation : {{citation}}, ' . '{{citation|langue}} ou {{citation|langue|url}}'; } if ($this->getOpt('active_i')) { $help['i'][] = 'texte différencié : ££texte différenci飣, ' . '££texte différencié|langue££'; } if ($this->getOpt('active_code')) { $help['i'][] = 'Code : @@code ici@@'; } if ($this->getOpt('active_footnotes')) { $help['i'][] = 'Note de bas de page : $$Corps de la note$$'; } $res = '
'; $res .= '
Blocs
'; if (count($help['b']) > 0) { $res .= '
  • '; $res .= implode(' ;
  • ', $help['b']); $res .= '.
'; } $res .= '
'; $res .= '
Éléments en ligne
'; if (count($help['i']) > 0) { $res .= '
  • '; $res .= implode(' ;
  • ', $help['i']); $res .= '.
'; } $res .= '
'; $res .= '
'; return $res; } }