h2d_htmlconverter.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783
  1. <?php
  2. /**
  3. * @file
  4. * HTMLtodocx
  5. * HTML to docx Converter
  6. * - HTML converter for use with PHPWord
  7. * Copyright (C) 2011 Commtap CIC
  8. *
  9. * This program is free software; you can redistribute it and/or
  10. * modify it under the terms of the GNU General Public License
  11. * as published by the Free Software Foundation; either version 2.1
  12. * of the License, or (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU General Public License
  20. * along with this program; if not, write to the Free Software
  21. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  22. *
  23. * @copyright Copyright (c) 2011 Commtap CIC
  24. * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt LGPL
  25. * @version Devel 0.5.0, 22.11.2011
  26. *
  27. */
  28. // Functions for converting and adding HTML into PHPWord objects
  29. // for creating a docx document.
  30. /**
  31. *
  32. * These are the elements which can be processed by this converter
  33. *
  34. * This will tell us when to stop when parsing HTML.
  35. * Anything still remaining after a stop (i.e. no more
  36. * parsable tags) to be returned as is (with any tags filtered out).
  37. *
  38. * @param string $tag
  39. * (optional) - the tag for the element for which
  40. * its possible children are required.
  41. * @return
  42. * array of allowed children
  43. */
  44. function htmltodocx_html_allowed_children($tag = NULL) {
  45. $allowed_children = array(
  46. 'body' => array('p', 'ul', 'ol', 'table', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'),
  47. 'h1' => array('a', 'em', 'i', 'strong', 'b', 'br', 'span', 'code', 'u', 'sup', 'text'),
  48. 'h2' => array('a', 'em', 'i', 'strong', 'b', 'br', 'span', 'code', 'u', 'sup', 'text'),
  49. 'h3' => array('a', 'em', 'i', 'strong', 'b', 'br', 'span', 'code', 'u', 'sup', 'text'),
  50. 'h4' => array('a', 'em', 'i', 'strong', 'b', 'br', 'span', 'code', 'u', 'sup', 'text'),
  51. 'h5' => array('a', 'em', 'i', 'strong', 'b', 'br', 'span', 'code', 'u', 'sup', 'text'),
  52. 'h6' => array('a', 'em', 'i', 'strong', 'b', 'br', 'span', 'code', 'u', 'sup', 'text'),
  53. 'p' => array('a', 'em', 'i', 'strong', 'b', 'ul', 'ol', 'img', 'table', 'br', 'span', 'code', 'u', 'sup', 'text', 'div', 'p', 'strike', 'del', 's'), // p does not nest - simple_html_dom will create a flat set of paragraphs if it finds nested ones.
  54. 'div' => array('a', 'em', 'i', 'strong', 'b', 'ul', 'ol', 'img', 'table', 'br', 'span', 'code', 'u', 'sup', 'text', 'div', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strike', 'del'),
  55. 'a' => array('text'), // PHPWord doesn't allow elements to be placed in link elements
  56. 'em' => array('a', 'strong', 'b', 'br', 'span', 'code', 'u', 'sup', 'text'), // Italic
  57. 'i' => array('a', 'strong', 'b', 'br', 'span', 'code', 'u', 'sup', 'text'), // Italic
  58. 'strong' => array('a', 'em', 'i', 'br', 'span', 'code', 'u', 'sup', 'text'), // Bold
  59. 'b' => array('a', 'em', 'i', 'br', 'span', 'code', 'u', 'sup', 'text'), // Bold
  60. 'sup' => array('a', 'em', 'i', 'br', 'span', 'code', 'u', 'text'), // Superscript
  61. 'u' => array('a', 'em', 'strong', 'b', 'i', 'br', 'span', 'code', 'sup', 'text'), // Underline - deprecated - but could be encountered.
  62. 'ul' => array('li'),
  63. 'ol' => array('li'),
  64. 'li' => array('a', 'em', 'i', 'strong', 'b', 'ul', 'ol', 'img', 'br', 'span', 'code', 'u', 'sup', 'text'),
  65. 'img' => array(),
  66. 'table' => array('tbody', 'tr'),
  67. 'tbody' => array('tr'),
  68. 'tr' => array('td', 'th'),
  69. 'td' => array('p', 'a', 'em', 'i', 'strong', 'b', 'ul', 'ol', 'img', 'br', 'span', 'code', 'u', 'sup', 'text', 'table'), // PHPWord does not allow you to insert a table into a table cell
  70. 'th' => array('p', 'a', 'em', 'i', 'strong', 'b', 'ul', 'ol', 'img', 'br', 'span', 'code', 'u', 'sup', 'text', 'table'), // PHPWord does not allow you to insert a table into a table cell
  71. 'br' => array(),
  72. 'code' => array(), // Note, elements nested inside the code element do not work! (Perhaps simpleHTMLDom isn't recognising them).
  73. 'span' => array('a', 'em', 'i', 'strong', 'b', 'img', 'br', 'span', 'code', 'sup', 'text', 'del', 'strike', 's'), // Used for styles - underline
  74. 'strike' => array('a', 'em', 'i', 'strong', 'b', 'img', 'br', 'span', 'code', 'sup', 'text'),
  75. 'del' => array('a', 'em', 'i', 'strong', 'b', 'img', 'br', 'span', 'code', 'sup', 'text'),
  76. 's' => array('a', 'em', 'i', 'strong', 'b', 'img', 'br', 'span', 'code', 'sup', 'text'),
  77. 'text' => array(), // The tag name used for elements containing just text in SimpleHtmlDom.
  78. );
  79. if (!$tag) {
  80. return $allowed_children;
  81. }
  82. elseif (isset($allowed_children[$tag])) {
  83. return $allowed_children[$tag];
  84. }
  85. else {
  86. return array();
  87. }
  88. }
  89. /**
  90. * Clean up text:
  91. *
  92. * @param string $text
  93. *
  94. */
  95. function htmltodocx_clean_text($text) {
  96. // Replace each &nbsp; with a single space:
  97. $text = str_replace('&nbsp;', ' ', $text);
  98. if (strpos($text, '<') !== FALSE) {
  99. // We only run strip_tags if it looks like there might be some tags in the text
  100. // as strip_tags is expensive:
  101. $text = strip_tags($text);
  102. }
  103. // Strip out extra spaces:
  104. $text = preg_replace('/\s+/u', ' ', $text);
  105. // Convert entities:
  106. $text = html_entity_decode($text, ENT_COMPAT, 'UTF-8');
  107. return $text;
  108. }
  109. /**
  110. * Compute the styles that should be applied for the
  111. * current element.
  112. * We start with the default style, and successively override
  113. * this with the current style, style set for the tag, classes
  114. * and inline styles.
  115. *
  116. */
  117. function _htmltodocx_get_style($element, $state) {
  118. $style_sheet = $state['style_sheet'];
  119. // Get the default styles
  120. $phpword_style = $style_sheet['default'];
  121. // Update with the current style
  122. $current_style = $state['current_style'];
  123. // Remove uninheritable items:
  124. $inheritable_props = htmltodocx_inheritable_props();
  125. foreach ($current_style as $property => $value) {
  126. if (!in_array($property, $inheritable_props)) {
  127. unset($current_style[$property]);
  128. }
  129. }
  130. $phpword_style = array_merge($phpword_style, $current_style);
  131. // Update with any styles defined by the element tag
  132. $tag_style = isset($style_sheet['elements'][$element->tag]) ? $style_sheet['elements'][$element->tag] : array();
  133. $phpword_style = array_merge($phpword_style, $tag_style);
  134. // Find any classes defined for this element:
  135. $class_list = array();
  136. if (!empty($element->class)) {
  137. $classes = explode(' ', $element->class);
  138. foreach ($classes as $class) {
  139. $class_list[] = trim($class);
  140. }
  141. }
  142. // Look for any style definitions for these classes:
  143. $classes_style = array();
  144. if (!empty($class_list) && !empty($style_sheet['classes'])) {
  145. foreach ($style_sheet['classes'] as $class => $attributes) {
  146. if (in_array($class, $class_list)) {
  147. $classes_style = array_merge($classes_style, $attributes);
  148. }
  149. }
  150. }
  151. $phpword_style = array_merge($phpword_style, $classes_style);
  152. // Find any inline styles:
  153. $inline_style_list = array();
  154. if (!empty($element->attr['style'])) {
  155. $inline_styles = explode(';', rtrim(rtrim($element->attr['style']), ';'));
  156. foreach ($inline_styles as $inline_style) {
  157. $style_pair = explode(':', $inline_style);
  158. $inline_style_list[] = trim($style_pair[0]) . ': ' . trim($style_pair[1]);
  159. }
  160. }
  161. // Look for style definitions of these inline styles:
  162. $inline_styles = array();
  163. if (!empty($inline_style_list) && !empty($style_sheet['inline'])) {
  164. foreach ($style_sheet['inline'] as $inline_style => $attributes) {
  165. if (in_array($inline_style, $inline_style_list)) {
  166. $inline_styles = array_merge($inline_styles, $attributes);
  167. }
  168. }
  169. }
  170. $phpword_style = array_merge($phpword_style, $inline_styles);
  171. return $phpword_style;
  172. }
  173. /**
  174. * PHPWord style properties which are inheritable for the purposes of our
  175. * conversion:
  176. *
  177. */
  178. function htmltodocx_inheritable_props() {
  179. return array(
  180. 'size',
  181. 'name',
  182. 'bold',
  183. 'italic',
  184. 'superScript',
  185. 'subScript',
  186. 'underline',
  187. 'strike',
  188. 'strikethrough',
  189. 'color',
  190. 'fgColor',
  191. 'align',
  192. 'spacing',
  193. 'listType',
  194. 'spaceAfter'
  195. );
  196. }
  197. /**
  198. * Wrapper for htmltodocx_insert_html_recursive()
  199. * - inserts the initial defaults.
  200. *
  201. * @param $phpword_element
  202. * PHPWord object
  203. * @param mixed $html_dom_array
  204. * SimpleHTMLDom object
  205. * @param mixed $state
  206. * State
  207. */
  208. function htmltodocx_insert_html(&$phpword_element, $html_dom_array, &$state = array()) {
  209. // Set up initial defaults:
  210. // Lists:
  211. $state['pseudo_list'] = TRUE;
  212. // This converter only supports "pseudo" lists at present.
  213. $state['pseudo_list_indicator_font_name'] = isset($state['pseudo_list_indicator_font_name']) ? $state['pseudo_list_indicator_font_name'] : 'Wingdings'; // Bullet indicator font
  214. $state['pseudo_list_indicator_font_size'] = isset($state['pseudo_list_indicator_font_size']) ? $state['pseudo_list_indicator_font_size'] : '7'; // Bullet indicator size
  215. $state['pseudo_list_indicator_character'] = isset($state['pseudo_list_indicator_character']) ? $state['pseudo_list_indicator_character'] : 'l '; // Gives a circle bullet point with wingdings
  216. // "Style sheet":
  217. $state['style_sheet'] = isset($state['style_sheet']) ? $state['style_sheet'] : array();
  218. $state['style_sheet']['default'] = isset($state['style_sheet']['default']) ? $state['style_sheet']['default'] : array();
  219. // Current style:
  220. $state['current_style'] = isset($state['current_style']) ? $state['current_style'] : array('size' => '11');
  221. // Parents:
  222. $state['parents'] = isset($state['parents']) ? $state['parents'] : array(0 => 'body');
  223. $state['list_depth'] = isset($state['list_depth']) ? $state['list_depth'] : 0;
  224. $state['context'] = isset($state['context']) ? $state['context'] : 'section';
  225. // Possible values - section, footer or header.
  226. // Tables:
  227. if (in_array('td', $state['parents']) || in_array('th', $state['parents']) || (isset($state['table_allowed']) && !$state['table_allowed'])) {
  228. $state['table_allowed'] = FALSE;
  229. }
  230. else {
  231. $state['table_allowed'] = TRUE;
  232. }
  233. // Headings option:
  234. $state['structure_document'] = isset($state['structure_document']) ? $state['structure_document'] : FALSE;
  235. if ($state['structure_document']) {
  236. $state['structure_headings'] = array('h1' => 1, 'h2' => 2, 'h3' => 3, 'h4' => 4, 'h5' => 5, 'h6' => 6);
  237. }
  238. if (!$state['structure_document'] || !isset($state['table_of_contents_id'])) {
  239. $state['table_of_contents_id'] = FALSE;
  240. }
  241. // Treatment of divs:
  242. // The default is to treat a div like a paragraph - that is we insert a new
  243. // line each time we encounter a new div.
  244. $state['treat_div_as_paragraph'] = isset($state['treat_div_as_paragraph']) ? $state['treat_div_as_paragraph'] : TRUE;
  245. // Recurse through the HTML Dom inserting elements into the phpword object as
  246. // we go:
  247. htmltodocx_insert_html_recursive($phpword_element, $html_dom_array, $state);
  248. }
  249. /**
  250. * Populate PHPWord element
  251. * This recursive function processes all the elements and child elements
  252. * from the DOM array of objects created by SimpleHTMLDom.
  253. *
  254. * @param object phpword_element
  255. * PHPWord object to add in the converted html
  256. * @param array $html_dom_array
  257. * Array of nodes generated by simple HTML dom
  258. * @param array $state
  259. * Parameters for the current run
  260. */
  261. function htmltodocx_insert_html_recursive(&$phpword_element, $html_dom_array, &$state = array()) {
  262. // Go through the html_dom_array, adding bits to go in the PHPWord element
  263. $allowed_children = htmltodocx_html_allowed_children($state['parents'][0]);
  264. // Go through each element:
  265. foreach ($html_dom_array as $element) {
  266. $old_style = $state['current_style'];
  267. $state['current_style'] = _htmltodocx_get_style($element, $state);
  268. if(in_array($element->tag, ['del', 's', 'strike'])) $state['current_style']['strikethrough'] = true;
  269. switch ($element->tag) {
  270. case 'p':
  271. case 'div': // Treat a div as a paragraph
  272. case 'h1':
  273. case 'h2':
  274. case 'h3':
  275. case 'h4':
  276. case 'h5':
  277. case 'h6':
  278. if ($state['structure_document'] && in_array($element->tag, array('h1', 'h2', 'h3', 'h4', 'h5', 'h6')) && is_object($state['phpword_object'])) {
  279. // If the structure_document option has been enabled, then headings
  280. // are used to create Word heading styles. Note, in this case, any
  281. // nested elements within the heading are displayed as text only.
  282. // Additionally we don't now add a text break after a heading where
  283. // sizeAfter has not been set.
  284. $state['phpword_object']->addTitleStyle($state['structure_headings'][$element->tag], $state['current_style']);
  285. $phpword_element->addTitle(htmltodocx_clean_text($element->innertext), $state['structure_headings'][$element->tag]);
  286. break;
  287. }
  288. if ($element->tag == 'div' && $state['table_of_contents_id'] && $element->id == $state['table_of_contents_id']) {
  289. // Replace this div with a table of contents:
  290. $phpword_element->addTOC($state['current_style'], $state['current_style']);
  291. break;
  292. }
  293. // Everything in this element should be in the same text run
  294. // we need to initiate a text run here and pass it on. Starting one of
  295. // these elements will cause a new line to be added in the Word
  296. // document. In the case of divs this might not always be what is
  297. // wanted the setting 'treat_div_as_paragraph' determines whether or
  298. // not to add new lines for divs.
  299. if ($element->tag != 'div' || $state['treat_div_as_paragraph'] || !isset($state['textrun'])) {
  300. $state['textrun'] = $phpword_element->createTextRun($state['current_style']);
  301. }
  302. // For better usability for the end user of the Word document, we
  303. // separate paragraphs and headings with an empty line. You can
  304. // override this behaviour by setting the spaceAfter parameter for
  305. // the current element.
  306. // If the spaceAfter parameter is not set, we set it temporarily to 0
  307. // here and record that it wasn't set in the style. Later we will add
  308. // an empty line. Word 2007 and later have a non-zero default for
  309. // paragraph separation, so without setting that spacing to 0 here we
  310. // would end up with a large gap between paragraphs (the document
  311. // template default plus the extra line).
  312. $space_after_set = TRUE;
  313. if (!isset($state['current_style']['spaceAfter'])) {
  314. $state['current_style']['spaceAfter'] = 0;
  315. $space_after_set = FALSE;
  316. }
  317. if (in_array($element->tag, $allowed_children)) {
  318. array_unshift($state['parents'], $element->tag);
  319. htmltodocx_insert_html_recursive($phpword_element, $element->nodes, $state);
  320. array_shift($state['parents']);
  321. }
  322. else {
  323. $state['textrun']->addText(htmltodocx_clean_text($element->innertext), $state['current_style']);
  324. }
  325. unset($state['textrun']);
  326. if (!$space_after_set) {
  327. // Add the text break here - where the spaceAfter parameter hadn't
  328. // been set initially - also unset the spaceAfter parameter we just
  329. // set:
  330. $phpword_element->addTextBreak();
  331. unset($state['current_style']['spaceAfter']);
  332. }
  333. break;
  334. case 'table':
  335. if (in_array('table', $allowed_children)) {
  336. $old_table_state = $state['table_allowed'];
  337. if (!$state['table_allowed'] || in_array('td', $state['parents']) || in_array('th', $state['parents'])) {
  338. $state['table_allowed'] = FALSE; // This is a PHPWord constraint
  339. }
  340. else {
  341. $state['table_allowed'] = TRUE;
  342. // PHPWord allows table_styles to be passed in a couple of
  343. // different ways either using an array of properties, or by
  344. // defining a full table style on the PHPWord object:
  345. if (is_object($state['phpword_object']) && method_exists($state['phpword_object'], 'addTableStyle')) {
  346. $state['phpword_object']->addTableStyle('temp_table_style', $state['current_style']);
  347. $table_style = 'temp_table_style';
  348. }
  349. else {
  350. $table_style = $state['current_style'];
  351. }
  352. $table_style['unit'] = \PhpOffice\PhpWord\Style\Table::WIDTH_PERCENT;
  353. $table_style['width'] = 5000;
  354. $state['table'] = $phpword_element->addTable($table_style);
  355. }
  356. array_unshift($state['parents'], 'table');
  357. htmltodocx_insert_html_recursive($phpword_element, $element->nodes, $state);
  358. array_shift($state['parents']);
  359. // Reset table state to what it was before a table was added:
  360. $state['table_allowed'] = $old_table_state;
  361. $phpword_element->addTextBreak();
  362. }
  363. else {
  364. $state['textrun'] = $phpword_element->createTextRun();
  365. $state['textrun']->addText(htmltodocx_clean_text($element->innertext), $state['current_style']);
  366. }
  367. break;
  368. case 'tbody':
  369. if (in_array('tbody', $allowed_children)) {
  370. array_unshift($state['parents'], 'tbody');
  371. htmltodocx_insert_html_recursive($phpword_element, $element->nodes, $state);
  372. array_shift($state['parents']);
  373. }
  374. else {
  375. $state['textrun'] = $phpword_element->createTextRun();
  376. $state['textrun']->addText(htmltodocx_clean_text($element->innertext), $state['current_style']);
  377. }
  378. break;
  379. case 'tr':
  380. if (in_array('tr', $allowed_children)) {
  381. if ($state['table_allowed']) {
  382. $state['table']->addRow();
  383. }
  384. else {
  385. // Simply add a new line if a table is not possible in this
  386. // context:
  387. $state['textrun'] = $phpword_element->createTextRun();
  388. }
  389. array_unshift($state['parents'], 'tr');
  390. htmltodocx_insert_html_recursive($phpword_element, $element->nodes, $state);
  391. array_shift($state['parents']);
  392. }
  393. else {
  394. $state['textrun'] = $phpword_element->createTextRun();
  395. $state['textrun']->addText(htmltodocx_clean_text($element->innertext), $state['current_style']);
  396. }
  397. break;
  398. case 'td':
  399. case 'th':
  400. if (in_array($element->tag, $allowed_children) && $state['table_allowed']) {
  401. unset($state['textrun']);
  402. if (isset($state['current_style']['width'])) {
  403. $cell_width = $state['current_style']['width'];
  404. }
  405. elseif (isset($element->width) and is_numeric($element->width)) {
  406. $cell_width = $element->width * 15;
  407. // Converting at 15 TWIPS per pixel.
  408. }
  409. else {
  410. $cell_width = 800;
  411. }
  412. $colspan = $element->getAttribute('colspan');
  413. if(is_numeric($colspan) && $colspan > 1) $state['current_style']['gridSpan'] = $colspan;
  414. $state['table_cell'] = $state['table']->addCell($cell_width, $state['current_style']);
  415. array_unshift($state['parents'], $element->tag);
  416. htmltodocx_insert_html_recursive($state['table_cell'], $element->nodes, $state);
  417. array_shift($state['parents']);
  418. }
  419. else {
  420. if (!isset($state['textrun'])) {
  421. $state['textrun'] = $phpword_element->createTextRun();
  422. }
  423. $state['textrun']->addText(htmltodocx_clean_text($element->innertext), $state['current_style']);
  424. }
  425. break;
  426. case 'a':
  427. // Create a new text run if we aren't in one already:
  428. if (!isset($state['textrun'])) {
  429. $state['textrun'] = $phpword_element->createTextRun();
  430. }
  431. if ($state['context'] == 'section') {
  432. if (strpos($element->href, 'http://') === 0) {
  433. $href = $element->href;
  434. }
  435. elseif (strpos($element->href, '/') === 0) {
  436. $href = $state['base_root'] . $element->href;
  437. }
  438. else {
  439. $href = $state['base_root'] . $state['base_path'] . $element->href;
  440. }
  441. // Replace any spaces in url with %20 - to prevent errors in the Word
  442. // document:
  443. $state['textrun']->addLink(htmltodocx_url_encode_chars($href), htmltodocx_clean_text($element->innertext), $state['current_style']);
  444. }
  445. else {
  446. // Links can't seem to be included in headers or footers with
  447. // PHPWord: trying to include them causes an error which stops Word
  448. // from opening the file - in Word 2003 with the converter at least.
  449. // So add the link styled as a link only.
  450. $state['textrun']->addText(htmltodocx_clean_text($element->innertext), $state['current_style']);
  451. }
  452. break;
  453. case 'ul':
  454. $state['list_total_count'] = count($element->children);
  455. // We use this to be able to add the ordered list spaceAfter onto the
  456. // last list element. All ol children should be li elements.
  457. _htmltodocx_add_list_start_end_spacing_style($state);
  458. $state['list_number'] = 0; // Reset list number.
  459. if (in_array('ul', $allowed_children)) {
  460. if (!isset($state['pseudo_list'])) {
  461. // Unset any existing text run:
  462. unset($state['textrun']);
  463. // PHPWord lists cannot appear in a text run. If we leave a text
  464. // run active then subsequent text will go in that text run (if it
  465. // isn't re-initialised), which would mean that text after this
  466. // list would appear before it in the Word document.
  467. }
  468. array_unshift($state['parents'], 'ul');
  469. htmltodocx_insert_html_recursive($phpword_element, $element->nodes, $state);
  470. array_shift($state['parents']);
  471. }
  472. else {
  473. $state['textrun'] = $phpword_element->createTextRun();
  474. $state['textrun']->addText(htmltodocx_clean_text($element->innertext), $state['current_style']);
  475. }
  476. break;
  477. case 'ol':
  478. $state['list_total_count'] = count($element->children);
  479. // We use this to be able to add the ordered list spaceAfter onto the
  480. // last list element. All ol children should be li elements.
  481. _htmltodocx_add_list_start_end_spacing_style($state);
  482. $state['list_number'] = 0; // Reset list number.
  483. if (in_array('ol', $allowed_children)) {
  484. if (!isset($state['pseudo_list'])) {
  485. // Unset any existing text run:
  486. unset($state['textrun']);
  487. // Lists cannot appear in a text run. If we leave a text run active
  488. // then subsequent text will go in that text run (if it isn't
  489. // re-initialised), which would mean that text after this list
  490. // would appear before it in the Word document.
  491. }
  492. array_unshift($state['parents'], 'ol');
  493. htmltodocx_insert_html_recursive($phpword_element, $element->nodes, $state);
  494. array_shift($state['parents']);
  495. }
  496. else {
  497. $state['textrun'] = $phpword_element->createTextRun();
  498. $state['textrun']->addText(htmltodocx_clean_text($element->innertext), $state['current_style']);
  499. }
  500. break;
  501. case 'li':
  502. // You cannot style individual pieces of text in a list element so we do it
  503. // with text runs instead. This does not allow us to indent lists at all, so
  504. // we can't show nesting.
  505. // Before and after spacings:
  506. if ($state['list_number'] === 0) {
  507. $state['current_style'] = array_merge($state['current_style'], $state['list_style_before']);
  508. }
  509. $last_item = FALSE;
  510. if ($state['list_number'] == $state['list_total_count'] - 1) {
  511. $last_item = TRUE;
  512. if (empty($state['list_style_after'])) {
  513. $state['current_style']['spaceAfter'] = 0;
  514. // Set to 0 if not defined so we can add a text break without
  515. // ending up within too much space in Word2007+.
  516. // *Needs further testing on Word 2007+*
  517. }
  518. $state['current_style'] = array_merge($state['current_style'], $state['list_style_after']);
  519. }
  520. // We create a new text run for each element:
  521. $state['textrun'] = $phpword_element->createTextRun($state['current_style']);
  522. if (in_array('li', $allowed_children)) {
  523. $state['list_number']++;
  524. if ($state['parents'][0] == 'ol') {
  525. $item_indicator = $state['list_number'] . '. ';
  526. $style = $state['current_style'];
  527. }
  528. else {
  529. $style = $state['current_style'];
  530. $style['name'] = $state['pseudo_list_indicator_font_name'];
  531. $style['size'] = $state['pseudo_list_indicator_font_size'];
  532. $item_indicator = $state['pseudo_list_indicator_character'];
  533. }
  534. array_unshift($state['parents'], 'li');
  535. $state['textrun']->addText($item_indicator, $style);
  536. htmltodocx_insert_html_recursive($phpword_element, $element->nodes, $state);
  537. array_shift($state['parents']);
  538. }
  539. else {
  540. $state['textrun']->addText(htmltodocx_clean_text($element->innertext), $state['current_style']);
  541. }
  542. if ($last_item && empty($state['list_style_after'])) {
  543. $phpword_element->addTextBreak();
  544. // Add an empty line after the list if no spacing after has been
  545. // defined.
  546. }
  547. unset($state['textrun']);
  548. break;
  549. case 'text':
  550. // We may get some empty text nodes - containing just a space - in
  551. // simple HTML dom - we want to exclude those, as these can cause extra
  552. // line returns. However we don't want to exclude spaces between styling
  553. // elements (these will be within a text run).
  554. if (!isset($state['textrun'])) {
  555. $text = htmltodocx_clean_text(trim($element->innertext));
  556. }
  557. else {
  558. $text = htmltodocx_clean_text($element->innertext);
  559. }
  560. if (!empty($text)) {
  561. if (!isset($state['textrun'])) {
  562. $state['textrun'] = $phpword_element->createTextRun();
  563. }
  564. $state['textrun']->addText($text, $state['current_style']);
  565. }
  566. break;
  567. // Style tags:
  568. case 'strong':
  569. case 'b':
  570. case 'sup': // Not working in PHPWord
  571. case 'em':
  572. case 'i':
  573. case 'u':
  574. case 'span':
  575. case 'strike':
  576. case 's':
  577. case 'del':
  578. case 'code':
  579. // Create a new text run if we aren't in one already:
  580. if (!isset($state['textrun'])) {
  581. $state['textrun'] = $phpword_element->createTextRun();
  582. }
  583. if (in_array($element->tag, $allowed_children)) {
  584. array_unshift($state['parents'], $element->tag);
  585. htmltodocx_insert_html_recursive($phpword_element, $element->nodes, $state);
  586. array_shift($state['parents']);
  587. }
  588. else {
  589. $state['textrun']->addText(htmltodocx_clean_text($element->innertext), $state['current_style']);
  590. }
  591. break;
  592. case 'pre':
  593. $codeFontStyle = array('name' => 'Courier New', 'size' => 10, 'color' => '000000');
  594. $codeParagraphStyle = array('align' => 'left', 'spaceAfter' => 0, 'spaceBefore' => 0, 'spacing' => 120);
  595. if(strpos($element->innertext, '<code>') !== false)
  596. {
  597. $element->innertext = str_replace('<code>', '', $element->innertext);
  598. $element->innertext = str_replace('</code>', '', $element->innertext);
  599. }
  600. /* 使用普通段落处理 pre 标签,保留换行和空格。 */
  601. /* Use normal paragraph to handle pre tag, keep line breaks and spaces. */
  602. $lines = explode("\n", $element->innertext);
  603. foreach($lines as $line)
  604. {
  605. /* 为每一行创建一个新的段落 */
  606. /* Create a new paragraph for each line */
  607. $textrun = $phpword_element->createTextRun($codeParagraphStyle);
  608. $textrun->addText($line, $codeFontStyle);
  609. }
  610. break;
  611. // NB, Simple HTML Dom might not be picking up <br> tags.
  612. case 'br':
  613. // Simply create a new text run:
  614. $state['textrun'] = $phpword_element->createTextRun();
  615. break;
  616. case 'img':
  617. $image_style = array();
  618. if ($element->height && $element->width) {
  619. $state['current_style']['height'] = $element->height;
  620. $state['current_style']['width'] = $element->width;
  621. }
  622. if (strpos($element->src, $state['base_root']) === 0) {
  623. // The image source is a full url, but nevertheless it is on this
  624. // server.
  625. $element_src = substr($element->src, strlen($state['base_root']));
  626. }
  627. else {
  628. $element_src = $element->src;
  629. }
  630. if(strpos($element_src, 'http://') === 0 or is_file($element_src)) {
  631. // The image url is from another site. Most probably the image won't
  632. // appear in the Word document.
  633. $src = $element_src;
  634. }
  635. elseif (strpos($element_src, '/') === 0) {
  636. $src = htmltodocx_doc_root() . $element_src;
  637. }
  638. else {
  639. $src = htmltodocx_doc_root() . $state['base_path'] . $element_src;
  640. }
  641. $phpword_element->addImage($src, $state['current_style']);
  642. break;
  643. default:
  644. $state['textrun'] = $phpword_element->createTextRun();
  645. $state['textrun']->addText(htmltodocx_clean_text($element->innertext), $state['current_style']);
  646. break;
  647. }
  648. // Reset the style back to what it was:
  649. $state['current_style'] = $old_style;
  650. }
  651. }
  652. /**
  653. * Before/after styles for list elements - recorded
  654. * for use by the first or last item in a list.
  655. *
  656. */
  657. function _htmltodocx_add_list_start_end_spacing_style(&$state) {
  658. $state['list_style_after'] = isset($state['current_style']['spaceAfter']) ? array('spaceAfter' => $state['current_style']['spaceAfter']) : array();
  659. $state['list_style_before'] = isset($state['current_style']['spaceBefore']) ? array('spaceBefore' => $state['current_style']['spaceBefore']) : array();
  660. }
  661. /**
  662. * Get the document root.
  663. *
  664. */
  665. function htmltodocx_doc_root() {
  666. $local_path = getenv("SCRIPT_NAME");
  667. // Should be available on both Apache and non Apache servers.
  668. $local_dir = substr($local_path, 0, strrpos($local_path, '/'));
  669. if (empty($local_dir)) {
  670. return $_SERVER['DOCUMENT_ROOT'];
  671. }
  672. else {
  673. return dirname($_SERVER['SCRIPT_FILENAME']);
  674. //return substr(realpath(''), 0, -1 * strlen($local_dir));
  675. }
  676. }
  677. /**
  678. * Encodes selected characters in a url to prevent errors in the created Word
  679. * document. Note: if there is a space in the url and there isn't a forward
  680. * slash preceding it at some point, the resulting Word document will be
  681. * corrupted (even where the space has been urlencoded). We convert spaces to
  682. * %20 which stops this corruption in circumstances where a forward slash is
  683. * present.
  684. *
  685. */
  686. function htmltodocx_url_encode_chars($url) {
  687. // List the characters in this array to be encoded:
  688. $encode_chars = array(' ');
  689. foreach ($encode_chars as $char) {
  690. $encoded_chars[] = rawurlencode($char);
  691. }
  692. $encoded_url = str_replace($encode_chars, $encoded_chars, $url);
  693. return $encoded_url;
  694. }