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 =
=> 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
";
}
# Correction de la syntaxe FR dans tous sauf pre et hr
# Sur idée de Christophe Bonijol
# Changement de regex (Nicolas Chachereau)
if ($this->getOpt('active_fr_syntax') && $type != null && $type != 'pre' && $type != 'hr') {
$line = preg_replace('%[ ]+([:?!;\x{00BB}](\s|$))%u', ' $1', $line);
$line = preg_replace('%(\x{00AB})[ ]+%u', '$1 ', $line);
}
$res .= $line;
}
return trim($res);
}
private function __getLine($i, &$type, &$mode)
{
$pre_type = $type;
$pre_mode = $mode;
$type = $mode = null;
if (empty($this->T[$i])) {
return false;
}
$line = htmlspecialchars($this->T[$i], ENT_NOQUOTES);
# Ligne vide
if (empty($line)) {
$type = null;
} elseif ($this->getOpt('active_empty') && preg_match('/^øøø(.*)$/', $line, $cap)) {
$type = null;
$line = trim($cap[1]);
}
# Titre
elseif ($this->getOpt('active_title') && preg_match('/^([!]{1,4})(.*)$/', $line, $cap)) {
$type = 'title';
$mode = strlen($cap[1]);
$line = trim($cap[2]);
}
# Ligne HR
elseif ($this->getOpt('active_hr') && preg_match('/^[-]{4}[- ]*$/', $line)) {
$type = 'hr';
$line = null;
}
# Blockquote
elseif ($this->getOpt('active_quote') && preg_match('/^(>|;:)(.*)$/', $line, $cap)) {
$type = 'blockquote';
$line = trim($cap[2]);
}
# Liste
elseif ($this->getOpt('active_lists') && preg_match('/^([*#]+)(.*)$/', $line, $cap)) {
$type = 'list';
$mode = $cap[1];
$valid = true;
# Vérification d'intégrité
$dl = ($type != $pre_type) ? 0 : strlen($pre_mode);
$d = strlen($mode);
$delta = $d - $dl;
if ($delta < 0 && strpos($pre_mode, $mode) !== 0) {
$valid = false;
}
if ($delta > 0 && $type == $pre_type && strpos($mode, $pre_mode) !== 0) {
$valid = false;
}
if ($delta == 0 && $mode != $pre_mode) {
$valid = false;
}
if ($delta > 1) {
$valid = false;
}
if (!$valid) {
$type = 'p';
$mode = null;
$line = '
' . $line;
} else {
$line = trim($cap[2]);
}
} elseif ($this->getOpt('active_defl') && preg_match('/^([=|:]{1}) (.*)$/', $line, $cap)) {
$type = 'defl';
$mode = $cap[1];
$line = trim($cap[2]);
}
# Préformaté
elseif ($this->getOpt('active_pre') && preg_match('/^[ ]{1}(.*)$/', $line, $cap)) {
$type = 'pre';
$line = $cap[1];
}
# Aside
elseif ($this->getOpt('active_aside') && preg_match('/^[\)]{1}(.*)$/', $line, $cap)) {
$type = 'aside';
$line = trim($cap[1]);
}
# Paragraphe
else {
$type = 'p';
if (preg_match('/^\\\((?:(' . implode('|', $this->linetags) . ')).*)$/', $line, $cap)) {
$line = $cap[1];
}
$line = trim($line);
}
return $line;
}
private function __openLine($type, $mode, $pre_type, $pre_mode)
{
$open = ($type != $pre_type);
if ($open && $type == 'p') {
return "\n
"; } elseif ($open && $type == 'blockquote') { return "\n
\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 .= '' . $tag . '>'; } $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 = '"; } elseif (($open || $mode != $pre_mode) && $type == 'title') { $fl = $this->getOpt('first_title_level'); $fl = $fl + 3; $l = $fl - $mode; return "\n
\n"; } elseif ($close && $pre_type == 'blockquote') { return "'; } elseif ($open && $type == 'pre') { return "\n "; } elseif ($open && $type == 'aside') { return "\n
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 = '