/common/libraries/plugin/html2pdf/parsingHTML.class.php
PHP | 463 lines | 336 code | 42 blank | 85 comment | 49 complexity | 9bf7285289bdadd619e5079fcfe00853 MD5 | raw file
Possible License(s): GPL-2.0, BSD-3-Clause, LGPL-2.1, LGPL-3.0, GPL-3.0, MIT
- <?php
- /**
- * Logiciel : HTML2PDF - classe ParsingHTML
- *
- * Convertisseur HTML => PDF, utilise TCPDF
- * Distribué sous la licence LGPL.
- *
- * @author Laurent MINGUET <webmaster@html2pdf.fr>
- * @version 4.00
- */
-
- class parsingHTML
- {
- protected $html = ''; // code HTML ? parser
- protected $num = 0; // numéro de table
- protected $level = 0; // niveaux de table
- protected $encoding = ''; // encodage
- public $code = array(); // code HTML parsé
-
- /**
- * Constructeur
- *
- * @return null
- */
- public function __construct($encoding = 'UTF-8')
- {
- $this->num = 0;
- $this->level = array($this->num);
- $this->html = '';
- $this->code = array();
- $this->setEncoding($encoding);
- }
-
- public function setEncoding($encoding)
- {
- $this->encoding = $encoding;
- }
-
- /**
- * Définir le code HTML ? parser
- *
- * @param string code html
- * @return null
- */
- public function setHTML($html)
- {
- $html = preg_replace('/<!--(.*)-->/isU', '', $html);
- $this->html = $html;
- }
-
- /**
- * parser le code HTML
- *
- * @return null
- */
- public function parse()
- {
- $parents = array();
-
- // chercher les balises HTML du code
- $tmp = array();
- $this->searchCode($tmp);
-
- // identifier les balises une ? une
- $pre_in = false;
- $pre_br = array(
- 'name' => 'br',
- 'close' => false,
- 'param' => array(
- 'style' => array(),
- 'num' => 0
- )
- );
-
- $balises_no_closed = array(
- 'br', 'hr', 'img', 'col',
- 'input', 'link', 'option',
- 'circle', 'ellipse', 'path', 'rect', 'line', 'polygon', 'polyline'
- );
- $todos = array();
- foreach($tmp as $part)
- {
- // si c'est un code
- if ($part[0]=='code')
- {
- $res = $this->analiseCode($part[1]);
-
- // si le code est bien un code analisable
- if ($res)
- {
- $res['html_pos'] = $part[2];
- if (!in_array($res['name'], $balises_no_closed))
- {
- if ($res['close'])
- {
- if (count($parents)<1)
- HTML2PDF::makeError(3, __FILE__, __LINE__, $res['name'], $this->getHtmlErrorCode($res['html_pos']));
- else if ($parents[count($parents)-1]!=$res['name'])
- HTML2PDF::makeError(4, __FILE__, __LINE__, $parents, $this->getHtmlErrorCode($res['html_pos']));
- else
- unset($parents[count($parents)-1]);
- }
- else
- {
- if ($res['autoclose'])
- {
- $todos[] = $res;
- $res['params'] = array();
- $res['close'] = true;
- }
- else
- $parents[count($parents)] = $res['name'];
-
- }
- if (($res['name']=='pre' || $res['name']=='code') && !$res['autoclose'])
- $pre_in = !$res['close'];
- }
-
- $todos[] = $res;
- }
- // sinon (code non analisable) => on le transforme en texte
- else
- {
- $part[0]='txt';
- }
- }
- // sinon si c'est un texte
- if ($part[0]=='txt')
- {
- // enregistrer l'action correspondante
- if (!$pre_in)
- {
- // remplacer tous les espaces, tabulations, saufs de ligne multiples par de simples espaces
- $todos[] = array(
- 'name' => 'write',
- 'close' => false,
- 'param' => array('txt' => $this->prepareTxt($part[1])),
- );
- }
- else
- {
- $part[1] = str_replace("\r", '', $part[1]);
- $part[1] = explode("\n", $part[1]);
-
- foreach($part[1] as $k => $txt)
- {
- $txt = str_replace("\t", ' ', $txt);
- $txt = str_replace(' ', ' ', $txt);
- if ($k>0) $todos[] = $pre_br;
-
- $todos[] = array(
- 'name' => 'write',
- 'close' => false,
- 'param' => array('txt' => $this->prepareTxt($txt, false)),
- );
- }
- }
- }
- }
-
- // pour chaque action identifiée, il faut nettoyer le début et la fin des textes
- // en fonction des balises qui l'entourent.
- $balises_clean = array('page', 'page_header', 'page_footer', 'form',
- 'table', 'thead', 'tfoot', 'tr', 'td', 'th', 'br',
- 'div', 'hr', 'p', 'ul', 'ol', 'li',
- 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
- 'bookmark',
- 'draw', 'circle', 'ellipse', 'path', 'rect', 'line', 'g', 'polygon', 'polyline');
- $nb = count($todos);
- for($k=0; $k<$nb; $k++)
- {
- //si c'est un texte
- if ($todos[$k]['name']=='write')
- {
- // et qu'une balise spécifique le préc?de => on nettoye les espaces du début du texte
- if ($k>0 && in_array($todos[$k-1]['name'], $balises_clean))
- $todos[$k]['param']['txt'] = ltrim($todos[$k]['param']['txt']);
-
- // et qu'une balise spécifique le suit => on nettoye les espaces de la fin du texte
- if ($k<count($todos)-1 && in_array($todos[$k+1]['name'], $balises_clean))
- $todos[$k]['param']['txt'] = rtrim($todos[$k]['param']['txt']);
-
- if (!strlen($todos[$k]['param']['txt']))
- unset($todos[$k]);
- }
- }
- if (count($parents)) HTML2PDF::makeError(5, __FILE__, __LINE__, $parents);
-
- // liste des actions sauvée
- $this->code = array_values($todos);
- }
-
- /**
- * preparer le texte une seule fois pour gagner du temps. vient de o_WRITE
- *
- * @param string texte
- * @return string texte
- */
- protected function prepareTxt($txt, $spaces = true)
- {
- if ($spaces) $txt = preg_replace('/\s+/is', ' ', $txt);
- $txt = str_replace('€', '€', $txt);
- $txt = html_entity_decode($txt, ENT_QUOTES, $this->encoding);
- return $txt;
- }
-
- /**
- * parser le code HTML
- *
- * @param &array tableau de retour des données
- * @return null
- */
- protected function searchCode(&$tmp)
- {
- // séparer les balises du texte
- $tmp = array();
- $reg = '/(<[^>]+>)|([^<]+)+/isU';
-
- // pour chaque élément trouvé :
- $str = '';
- $offset = 0;
- while(preg_match($reg, $this->html, $parse, PREG_OFFSET_CAPTURE, $offset))
- {
- // si une balise a été détectée
- if ($parse[1][0])
- {
- // sauvegarde du texte précédent si il existe
- if ($str!=='') $tmp[] = array('txt',$str);
-
- // sauvegarde de la balise
- $tmp[] = array('code',trim($parse[1][0]), $offset);
-
- // initialisation du texte suivant
- $str = '';
- }
- else
- {
- // ajout du texte ? la fin de celui qui est déj? détecté
- $str.= $parse[2][0];
- }
- // Update offset to the end of the match
- $offset = $parse[0][1] + strlen($parse[0][0]);
- unset($parse);
- }
- // si un texte est présent ? la fin, on l'enregistre
- if ($str!='') $tmp[] = array('txt',$str);
- unset($str);
- }
-
- /**
- * analyse une balise HTML
- *
- * @param string code HTML ? identifier
- * @return array action correspondante
- */
- protected function analiseCode($code)
- {
- // nom de la balise et ouverture ou fermeture
- $balise = '<([\/]{0,1})([_a-z0-9]+)([\/>\s]+)';
- if (!preg_match('/'.$balise.'/isU', $code, $match))
- return null;
-
- $close = ($match[1]=='/' ? true : false);
- $autoclose = preg_match('/\/>$/isU', $code);
- $name = strtolower($match[2]);
-
- // param?tres obligatoires en fonction du nom de la balise
- $param = array();
- $param['style'] = '';
- if ($name=='img') { $param['alt'] = ''; $param['src'] = ''; }
- if ($name=='a') { $param['href'] = ''; }
-
- // lecture des paramétres du type nom=valeur
- $prop = '([a-zA-Z0-9_]+)=([^"\'\s>]+)';
- preg_match_all('/'.$prop.'/is', $code, $match);
- for($k=0; $k<count($match[0]); $k++)
- $param[trim(strtolower($match[1][$k]))] = trim($match[2][$k]);
-
- // lecture des paramétres du type nom="valeur"
- $prop = '([a-zA-Z0-9_]+)=["]([^"]*)["]';
- preg_match_all('/'.$prop.'/is', $code, $match);
- for($k=0; $k<count($match[0]); $k++)
- $param[trim(strtolower($match[1][$k]))] = trim($match[2][$k]);
-
- // lecture des paramétres du type nom='valeur'
- $prop = "([a-zA-Z0-9_]+)=[']([^']*)[']";
- preg_match_all('/'.$prop.'/is', $code, $match);
- for($k=0; $k<count($match[0]); $k++)
- $param[trim(strtolower($match[1][$k]))] = trim($match[2][$k]);
-
- // mise en conformité en style de chaque param?tre
- $color = "#000000";
- $border = null;
- foreach($param as $key => $val)
- {
- $key = strtolower($key);
- switch($key)
- {
- case 'width':
- unset($param[$key]);
- $param['style'] = 'width: '.$val.'px; '.$param['style'];
- break;
-
- case 'align':
- if ($name==='img')
- {
- unset($param[$key]);
- $param['style'] = 'float: '.$val.'; '.$param['style'];
- }
- elseif ($name!=='table')
- {
- unset($param[$key]);
- $param['style'] = 'text-align: '.$val.'; '.$param['style'];
- }
- break;
-
- case 'valign':
- unset($param[$key]);
- $param['style'] = 'vertical-align: '.$val.'; '.$param['style'];
- break;
-
- case 'height':
- unset($param[$key]);
- $param['style'] = 'height: '.$val.'px; '.$param['style'];
- break;
-
- case 'bgcolor':
- unset($param[$key]);
- $param['style'] = 'background: '.$val.'; '.$param['style'];
- break;
-
- case 'bordercolor':
- unset($param[$key]);
- $color = $val;
- break;
-
- case 'border':
- unset($param[$key]);
- if (preg_match('/^[0-9]$/isU', $val)) $val = $val.'px';
- $border = $val;
- break;
-
- case 'cellpadding':
- case 'cellspacing':
- if (preg_match('/^([0-9]+)$/isU', $val)) $param[$key] = $val.'px';
- break;
-
- case 'colspan':
- case 'rowspan':
- $val = preg_replace('/[^0-9]/isU', '', $val);
- if (!$val) $val = 1;
- $param[$key] = $val;
- break;
- }
- }
- if ($border!==null)
- {
- if ($border) $border = 'border: solid '.$border.' '.$color;
- else $border = 'border: none';
-
- $param['style'] = $border.'; '.$param['style'];
- $param['border'] = $border;
- }
-
- // lecture des styles - décomposition
- $styles = explode(';', $param['style']);
- $param['style'] = array();
- foreach($styles as $style)
- {
- $tmp = explode(':', $style);
- if (count($tmp)>1)
- {
- $cod = $tmp[0]; unset($tmp[0]); $tmp = implode(':', $tmp);
- $param['style'][trim(strtolower($cod))] = preg_replace('/[\s]+/isU', ' ', trim($tmp));
- }
- }
-
- // détermination du niveau de table pour les ouverture, avec ajout d'un level
- if (in_array($name, array('ul', 'ol', 'table')) && !$close)
- {
- $this->num++;
- $this->level[count($this->level)] = $this->num;
- }
-
- // attribution du niveau de table o? se trouve l'élément
- if (!isset($param['num'])) $param['num'] = $this->level[count($this->level)-1];
-
- // pour les fins de table : suppression d'un level
- if (in_array($name, array('ul', 'ol', 'table')) && $close)
- {
- unset($this->level[count($this->level)-1]);
- }
-
- if (isset($param['value'])) $param['value'] = $this->prepareTxt($param['value']);
- if (isset($param['alt'])) $param['alt'] = $this->prepareTxt($param['alt']);
- if (isset($param['title'])) $param['title'] = $this->prepareTxt($param['title']);
- if (isset($param['class'])) $param['class'] = $this->prepareTxt($param['class']);
-
- // retour de l'action identifiée
- return array('name' => $name, 'close' => $close ? 1 : 0, 'autoclose' => $autoclose, 'param' => $param);
- }
-
- // récupérer un niveau complet d'HTML entre une ouverture de balise et la fermeture correspondante
- public function getLevel($k)
- {
- // si le code n'existe pas : fin
- if (!isset($this->code[$k])) return array();
-
- // quelle balise faudra-t-il détecter
- $detect = $this->code[$k]['name'];
-
- $level = 0; // niveau de profondeur
- $end = false; // etat de fin de recherche
- $code = array(); // code extrait
-
- // tant que c'est pas fini, on boucle
- while (!$end)
- {
- // action courante
- $row = $this->code[$k];
-
- // si write => on ajoute le texte
- if ($row['name']=='write')
- {
- $code[] = $row;
- }
- // sinon, c'est une balise html
- else
- {
- $not = false; // indicateur de non prise en compte de la balise courante
-
- // si c'est la balise que l'on cherche
- if ($row['name']==$detect)
- {
- if ($level==0) { $not = true; } // si on est ? la premiere balise : on l'ignore
- $level+= ($row['close'] ? -1 : 1); // modification du niveau en cours en fonction de l'ouvertre / fermeture
- if ($level==0) { $not = true; $end = true; } // si on est au niveau 0 : on a fini
- }
-
- // si on doit prendre en compte la balise courante
- if (!$not)
- {
- if (isset($row['style']['text-align'])) unset($row['style']['text-align']);
- $code[] = $row;
- }
- }
-
- // on continue tant qu'il y a du code ? analyser...
- if (isset($this->code[$k+1]))
- $k++;
- else
- $end = true;
- }
-
- // retourne le code extrait
- return $code;
- }
-
- public function getHtmlErrorCode($pos)
- {
- return substr($this->html, $pos-30, 70);
- }
- }