PageRenderTime 48ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/common/libraries/plugin/html2pdf/parsingHTML.class.php

https://bitbucket.org/chamilo/chamilo/
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
  1. <?php
  2. /**
  3. * Logiciel : HTML2PDF - classe ParsingHTML
  4. *
  5. * Convertisseur HTML => PDF, utilise TCPDF
  6. * Distribué sous la licence LGPL.
  7. *
  8. * @author Laurent MINGUET <webmaster@html2pdf.fr>
  9. * @version 4.00
  10. */
  11. class parsingHTML
  12. {
  13. protected $html = ''; // code HTML ? parser
  14. protected $num = 0; // numéro de table
  15. protected $level = 0; // niveaux de table
  16. protected $encoding = ''; // encodage
  17. public $code = array(); // code HTML parsé
  18. /**
  19. * Constructeur
  20. *
  21. * @return null
  22. */
  23. public function __construct($encoding = 'UTF-8')
  24. {
  25. $this->num = 0;
  26. $this->level = array($this->num);
  27. $this->html = '';
  28. $this->code = array();
  29. $this->setEncoding($encoding);
  30. }
  31. public function setEncoding($encoding)
  32. {
  33. $this->encoding = $encoding;
  34. }
  35. /**
  36. * Définir le code HTML ? parser
  37. *
  38. * @param string code html
  39. * @return null
  40. */
  41. public function setHTML($html)
  42. {
  43. $html = preg_replace('/<!--(.*)-->/isU', '', $html);
  44. $this->html = $html;
  45. }
  46. /**
  47. * parser le code HTML
  48. *
  49. * @return null
  50. */
  51. public function parse()
  52. {
  53. $parents = array();
  54. // chercher les balises HTML du code
  55. $tmp = array();
  56. $this->searchCode($tmp);
  57. // identifier les balises une ? une
  58. $pre_in = false;
  59. $pre_br = array(
  60. 'name' => 'br',
  61. 'close' => false,
  62. 'param' => array(
  63. 'style' => array(),
  64. 'num' => 0
  65. )
  66. );
  67. $balises_no_closed = array(
  68. 'br', 'hr', 'img', 'col',
  69. 'input', 'link', 'option',
  70. 'circle', 'ellipse', 'path', 'rect', 'line', 'polygon', 'polyline'
  71. );
  72. $todos = array();
  73. foreach($tmp as $part)
  74. {
  75. // si c'est un code
  76. if ($part[0]=='code')
  77. {
  78. $res = $this->analiseCode($part[1]);
  79. // si le code est bien un code analisable
  80. if ($res)
  81. {
  82. $res['html_pos'] = $part[2];
  83. if (!in_array($res['name'], $balises_no_closed))
  84. {
  85. if ($res['close'])
  86. {
  87. if (count($parents)<1)
  88. HTML2PDF::makeError(3, __FILE__, __LINE__, $res['name'], $this->getHtmlErrorCode($res['html_pos']));
  89. else if ($parents[count($parents)-1]!=$res['name'])
  90. HTML2PDF::makeError(4, __FILE__, __LINE__, $parents, $this->getHtmlErrorCode($res['html_pos']));
  91. else
  92. unset($parents[count($parents)-1]);
  93. }
  94. else
  95. {
  96. if ($res['autoclose'])
  97. {
  98. $todos[] = $res;
  99. $res['params'] = array();
  100. $res['close'] = true;
  101. }
  102. else
  103. $parents[count($parents)] = $res['name'];
  104. }
  105. if (($res['name']=='pre' || $res['name']=='code') && !$res['autoclose'])
  106. $pre_in = !$res['close'];
  107. }
  108. $todos[] = $res;
  109. }
  110. // sinon (code non analisable) => on le transforme en texte
  111. else
  112. {
  113. $part[0]='txt';
  114. }
  115. }
  116. // sinon si c'est un texte
  117. if ($part[0]=='txt')
  118. {
  119. // enregistrer l'action correspondante
  120. if (!$pre_in)
  121. {
  122. // remplacer tous les espaces, tabulations, saufs de ligne multiples par de simples espaces
  123. $todos[] = array(
  124. 'name' => 'write',
  125. 'close' => false,
  126. 'param' => array('txt' => $this->prepareTxt($part[1])),
  127. );
  128. }
  129. else
  130. {
  131. $part[1] = str_replace("\r", '', $part[1]);
  132. $part[1] = explode("\n", $part[1]);
  133. foreach($part[1] as $k => $txt)
  134. {
  135. $txt = str_replace("\t", ' ', $txt);
  136. $txt = str_replace(' ', '&nbsp;', $txt);
  137. if ($k>0) $todos[] = $pre_br;
  138. $todos[] = array(
  139. 'name' => 'write',
  140. 'close' => false,
  141. 'param' => array('txt' => $this->prepareTxt($txt, false)),
  142. );
  143. }
  144. }
  145. }
  146. }
  147. // pour chaque action identifiée, il faut nettoyer le début et la fin des textes
  148. // en fonction des balises qui l'entourent.
  149. $balises_clean = array('page', 'page_header', 'page_footer', 'form',
  150. 'table', 'thead', 'tfoot', 'tr', 'td', 'th', 'br',
  151. 'div', 'hr', 'p', 'ul', 'ol', 'li',
  152. 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
  153. 'bookmark',
  154. 'draw', 'circle', 'ellipse', 'path', 'rect', 'line', 'g', 'polygon', 'polyline');
  155. $nb = count($todos);
  156. for($k=0; $k<$nb; $k++)
  157. {
  158. //si c'est un texte
  159. if ($todos[$k]['name']=='write')
  160. {
  161. // et qu'une balise spécifique le préc?de => on nettoye les espaces du début du texte
  162. if ($k>0 && in_array($todos[$k-1]['name'], $balises_clean))
  163. $todos[$k]['param']['txt'] = ltrim($todos[$k]['param']['txt']);
  164. // et qu'une balise spécifique le suit => on nettoye les espaces de la fin du texte
  165. if ($k<count($todos)-1 && in_array($todos[$k+1]['name'], $balises_clean))
  166. $todos[$k]['param']['txt'] = rtrim($todos[$k]['param']['txt']);
  167. if (!strlen($todos[$k]['param']['txt']))
  168. unset($todos[$k]);
  169. }
  170. }
  171. if (count($parents)) HTML2PDF::makeError(5, __FILE__, __LINE__, $parents);
  172. // liste des actions sauvée
  173. $this->code = array_values($todos);
  174. }
  175. /**
  176. * preparer le texte une seule fois pour gagner du temps. vient de o_WRITE
  177. *
  178. * @param string texte
  179. * @return string texte
  180. */
  181. protected function prepareTxt($txt, $spaces = true)
  182. {
  183. if ($spaces) $txt = preg_replace('/\s+/is', ' ', $txt);
  184. $txt = str_replace('&euro;', '&#x20AC;', $txt);
  185. $txt = html_entity_decode($txt, ENT_QUOTES, $this->encoding);
  186. return $txt;
  187. }
  188. /**
  189. * parser le code HTML
  190. *
  191. * @param &array tableau de retour des données
  192. * @return null
  193. */
  194. protected function searchCode(&$tmp)
  195. {
  196. // séparer les balises du texte
  197. $tmp = array();
  198. $reg = '/(<[^>]+>)|([^<]+)+/isU';
  199. // pour chaque élément trouvé :
  200. $str = '';
  201. $offset = 0;
  202. while(preg_match($reg, $this->html, $parse, PREG_OFFSET_CAPTURE, $offset))
  203. {
  204. // si une balise a été détectée
  205. if ($parse[1][0])
  206. {
  207. // sauvegarde du texte précédent si il existe
  208. if ($str!=='') $tmp[] = array('txt',$str);
  209. // sauvegarde de la balise
  210. $tmp[] = array('code',trim($parse[1][0]), $offset);
  211. // initialisation du texte suivant
  212. $str = '';
  213. }
  214. else
  215. {
  216. // ajout du texte ? la fin de celui qui est déj? détecté
  217. $str.= $parse[2][0];
  218. }
  219. // Update offset to the end of the match
  220. $offset = $parse[0][1] + strlen($parse[0][0]);
  221. unset($parse);
  222. }
  223. // si un texte est présent ? la fin, on l'enregistre
  224. if ($str!='') $tmp[] = array('txt',$str);
  225. unset($str);
  226. }
  227. /**
  228. * analyse une balise HTML
  229. *
  230. * @param string code HTML ? identifier
  231. * @return array action correspondante
  232. */
  233. protected function analiseCode($code)
  234. {
  235. // nom de la balise et ouverture ou fermeture
  236. $balise = '<([\/]{0,1})([_a-z0-9]+)([\/>\s]+)';
  237. if (!preg_match('/'.$balise.'/isU', $code, $match))
  238. return null;
  239. $close = ($match[1]=='/' ? true : false);
  240. $autoclose = preg_match('/\/>$/isU', $code);
  241. $name = strtolower($match[2]);
  242. // param?tres obligatoires en fonction du nom de la balise
  243. $param = array();
  244. $param['style'] = '';
  245. if ($name=='img') { $param['alt'] = ''; $param['src'] = ''; }
  246. if ($name=='a') { $param['href'] = ''; }
  247. // lecture des paramétres du type nom=valeur
  248. $prop = '([a-zA-Z0-9_]+)=([^"\'\s>]+)';
  249. preg_match_all('/'.$prop.'/is', $code, $match);
  250. for($k=0; $k<count($match[0]); $k++)
  251. $param[trim(strtolower($match[1][$k]))] = trim($match[2][$k]);
  252. // lecture des paramétres du type nom="valeur"
  253. $prop = '([a-zA-Z0-9_]+)=["]([^"]*)["]';
  254. preg_match_all('/'.$prop.'/is', $code, $match);
  255. for($k=0; $k<count($match[0]); $k++)
  256. $param[trim(strtolower($match[1][$k]))] = trim($match[2][$k]);
  257. // lecture des paramétres du type nom='valeur'
  258. $prop = "([a-zA-Z0-9_]+)=[']([^']*)[']";
  259. preg_match_all('/'.$prop.'/is', $code, $match);
  260. for($k=0; $k<count($match[0]); $k++)
  261. $param[trim(strtolower($match[1][$k]))] = trim($match[2][$k]);
  262. // mise en conformité en style de chaque param?tre
  263. $color = "#000000";
  264. $border = null;
  265. foreach($param as $key => $val)
  266. {
  267. $key = strtolower($key);
  268. switch($key)
  269. {
  270. case 'width':
  271. unset($param[$key]);
  272. $param['style'] = 'width: '.$val.'px; '.$param['style'];
  273. break;
  274. case 'align':
  275. if ($name==='img')
  276. {
  277. unset($param[$key]);
  278. $param['style'] = 'float: '.$val.'; '.$param['style'];
  279. }
  280. elseif ($name!=='table')
  281. {
  282. unset($param[$key]);
  283. $param['style'] = 'text-align: '.$val.'; '.$param['style'];
  284. }
  285. break;
  286. case 'valign':
  287. unset($param[$key]);
  288. $param['style'] = 'vertical-align: '.$val.'; '.$param['style'];
  289. break;
  290. case 'height':
  291. unset($param[$key]);
  292. $param['style'] = 'height: '.$val.'px; '.$param['style'];
  293. break;
  294. case 'bgcolor':
  295. unset($param[$key]);
  296. $param['style'] = 'background: '.$val.'; '.$param['style'];
  297. break;
  298. case 'bordercolor':
  299. unset($param[$key]);
  300. $color = $val;
  301. break;
  302. case 'border':
  303. unset($param[$key]);
  304. if (preg_match('/^[0-9]$/isU', $val)) $val = $val.'px';
  305. $border = $val;
  306. break;
  307. case 'cellpadding':
  308. case 'cellspacing':
  309. if (preg_match('/^([0-9]+)$/isU', $val)) $param[$key] = $val.'px';
  310. break;
  311. case 'colspan':
  312. case 'rowspan':
  313. $val = preg_replace('/[^0-9]/isU', '', $val);
  314. if (!$val) $val = 1;
  315. $param[$key] = $val;
  316. break;
  317. }
  318. }
  319. if ($border!==null)
  320. {
  321. if ($border) $border = 'border: solid '.$border.' '.$color;
  322. else $border = 'border: none';
  323. $param['style'] = $border.'; '.$param['style'];
  324. $param['border'] = $border;
  325. }
  326. // lecture des styles - décomposition
  327. $styles = explode(';', $param['style']);
  328. $param['style'] = array();
  329. foreach($styles as $style)
  330. {
  331. $tmp = explode(':', $style);
  332. if (count($tmp)>1)
  333. {
  334. $cod = $tmp[0]; unset($tmp[0]); $tmp = implode(':', $tmp);
  335. $param['style'][trim(strtolower($cod))] = preg_replace('/[\s]+/isU', ' ', trim($tmp));
  336. }
  337. }
  338. // détermination du niveau de table pour les ouverture, avec ajout d'un level
  339. if (in_array($name, array('ul', 'ol', 'table')) && !$close)
  340. {
  341. $this->num++;
  342. $this->level[count($this->level)] = $this->num;
  343. }
  344. // attribution du niveau de table o? se trouve l'élément
  345. if (!isset($param['num'])) $param['num'] = $this->level[count($this->level)-1];
  346. // pour les fins de table : suppression d'un level
  347. if (in_array($name, array('ul', 'ol', 'table')) && $close)
  348. {
  349. unset($this->level[count($this->level)-1]);
  350. }
  351. if (isset($param['value'])) $param['value'] = $this->prepareTxt($param['value']);
  352. if (isset($param['alt'])) $param['alt'] = $this->prepareTxt($param['alt']);
  353. if (isset($param['title'])) $param['title'] = $this->prepareTxt($param['title']);
  354. if (isset($param['class'])) $param['class'] = $this->prepareTxt($param['class']);
  355. // retour de l'action identifiée
  356. return array('name' => $name, 'close' => $close ? 1 : 0, 'autoclose' => $autoclose, 'param' => $param);
  357. }
  358. // récupérer un niveau complet d'HTML entre une ouverture de balise et la fermeture correspondante
  359. public function getLevel($k)
  360. {
  361. // si le code n'existe pas : fin
  362. if (!isset($this->code[$k])) return array();
  363. // quelle balise faudra-t-il détecter
  364. $detect = $this->code[$k]['name'];
  365. $level = 0; // niveau de profondeur
  366. $end = false; // etat de fin de recherche
  367. $code = array(); // code extrait
  368. // tant que c'est pas fini, on boucle
  369. while (!$end)
  370. {
  371. // action courante
  372. $row = $this->code[$k];
  373. // si write => on ajoute le texte
  374. if ($row['name']=='write')
  375. {
  376. $code[] = $row;
  377. }
  378. // sinon, c'est une balise html
  379. else
  380. {
  381. $not = false; // indicateur de non prise en compte de la balise courante
  382. // si c'est la balise que l'on cherche
  383. if ($row['name']==$detect)
  384. {
  385. if ($level==0) { $not = true; } // si on est ? la premiere balise : on l'ignore
  386. $level+= ($row['close'] ? -1 : 1); // modification du niveau en cours en fonction de l'ouvertre / fermeture
  387. if ($level==0) { $not = true; $end = true; } // si on est au niveau 0 : on a fini
  388. }
  389. // si on doit prendre en compte la balise courante
  390. if (!$not)
  391. {
  392. if (isset($row['style']['text-align'])) unset($row['style']['text-align']);
  393. $code[] = $row;
  394. }
  395. }
  396. // on continue tant qu'il y a du code ? analyser...
  397. if (isset($this->code[$k+1]))
  398. $k++;
  399. else
  400. $end = true;
  401. }
  402. // retourne le code extrait
  403. return $code;
  404. }
  405. public function getHtmlErrorCode($pos)
  406. {
  407. return substr($this->html, $pos-30, 70);
  408. }
  409. }