PageRenderTime 52ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

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

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