/fragmentify.class.php

https://github.com/iaindooley/Fragmentify · PHP · 312 lines · 281 code · 29 blank · 2 comment · 54 complexity · 74365faed5463c841c59579518daac73 MD5 · raw file

  1. <?php
  2. class Fragmentify {
  3. private $processed = false;
  4. private $path = null;
  5. private $html = null;
  6. private $base_html = null;
  7. private $doc = null;
  8. private $base_doc = null;
  9. private $xpath = null;
  10. private $base_xpath = null;
  11. private function __construct($path) {
  12. assert(is_string($path));
  13. $this->path = $path;
  14. $this->doctype = null;
  15. }
  16. public function getPath() {
  17. return $this->path;
  18. }
  19. public function getProcessed() {
  20. return $this->processed;
  21. }
  22. public function process() {
  23. $x = $this->getXPath();
  24. $html = $this->getHtml();
  25. if($html) {
  26. $base = $html->getAttribute('base');
  27. }
  28. else {
  29. $base = '';
  30. }
  31. if($base === '') {
  32. $this->processRequires($this->doc->documentElement,
  33. dirname($this->path));
  34. }
  35. else {
  36. $fn = realpath(dirname($this->path).'/'.$base);
  37. $base = new Fragmentify($fn);
  38. $this->createBaseDoc($base);
  39. $this->populateBaseDoc();
  40. $this->processRequires($this->base_doc->documentElement,
  41. dirname($fn));
  42. $this->doctype = $base->doctype;
  43. }
  44. }
  45. private function populateBaseDoc() {
  46. assert(is_object($this->base_doc));
  47. assert(is_object($this->getHtml()));
  48. foreach($this->getHtml()->childNodes as $node) {
  49. if($node->nodeType == 3 || $node->nodeType == 8) {
  50. continue;
  51. }
  52. if($xp = $node->getAttribute('replace')) {
  53. $ts = $this->getTargets($xp);
  54. foreach($ts as $t) {
  55. $i = $this->base_doc->importNode($node, true);
  56. $i->removeAttribute('replace');
  57. $t->parentNode->insertBefore($i, $t);
  58. if($node->getAttribute('keep-contents') == 'true') {
  59. $i->removeAttribute('keep-contents');
  60. while($t->firstChild) {
  61. $i->appendChild($t->firstChild);
  62. }
  63. }
  64. $t->parentNode->removeChild($t);
  65. $this->processRequires($i);
  66. }
  67. }
  68. else if($xp = $node->getAttribute('append')) {
  69. $ts = $this->getTargets($xp);
  70. foreach($ts as $t) {
  71. $i = $this->base_doc->importNode($node, true);
  72. $i->removeAttribute('append');
  73. $t->appendChild($i);
  74. $this->processRequires($i);
  75. }
  76. }
  77. else if($xp = $node->getAttribute('prepend')) {
  78. $ts = $this->getTargets($xp);
  79. foreach($ts as $t) {
  80. $i = $this->base_doc->importNode($node, true);
  81. $i->removeAttribute('prepend');
  82. if($t->firstChild) {
  83. $t->insertBefore($i,$t->firstChild);
  84. }
  85. else {
  86. $t->appendChild($i);
  87. }
  88. $this->processRequires($i);
  89. }
  90. }
  91. else if($xp = $node->getAttribute('before')) {
  92. $ts = $this->getTargets($xp);
  93. foreach($ts as $t) {
  94. $i = $this->base_doc->importNode($node, true);
  95. $i->removeAttribute('before');
  96. $t->parentNode->insertBefore($i,$t);
  97. $this->processRequires($i);
  98. }
  99. }
  100. else if($xp = $node->getAttribute('after')) {
  101. $ts = $this->getTargets($xp);
  102. foreach($ts as $t) {
  103. $i = $this->base_doc->importNode($node, true);
  104. $i->removeAttribute('after');
  105. if($t->nextSibling) {
  106. $t->parentNode->insertBefore($i,$t->nextSibling);
  107. }
  108. else {
  109. $t->parentNode->append($i);
  110. }
  111. $this->processRequires($i);
  112. }
  113. }
  114. else if($xp = $node->getAttribute('surround')) {
  115. $ts = $this->getTargets($xp);
  116. foreach($ts as $t) {
  117. $i = $this->base_doc->importNode($node, true);
  118. $i->removeAttribute('surround');
  119. $i->removeAttribute('where');
  120. $t->parentNode->insertBefore($i,$t);
  121. $where = $node->getAttribute('where');
  122. switch($where) {
  123. case 'top':
  124. if($i->firstChild) {
  125. $i->insertBefore($t,$i->firstChild);
  126. }
  127. else {
  128. $i->appendChild($t);
  129. }
  130. break;
  131. default:
  132. $i->appendChild($t);
  133. break;
  134. }
  135. $this->processRequires($i);
  136. }
  137. }
  138. else if($xp = $node->getAttribute('merge')) {
  139. $ts = $this->getTargets($xp);
  140. foreach($ts as $t) {
  141. $i = $this->base_doc->importNode($node, true);
  142. $i->removeAttribute('merge');
  143. foreach($i->attributes as $att) {
  144. $t->setAttribute($att->name,$att->value);
  145. }
  146. }
  147. }
  148. else if($xp = $node->getAttribute('remove')) {
  149. $ts = $this->getTargets($xp);
  150. foreach($ts as $t) {
  151. $t->parentNode->removeChild($t);
  152. }
  153. }
  154. }
  155. }
  156. private function getTargets($xpath) {
  157. $t = $this->base_xpath->query($xpath);
  158. if(!$t->length) {
  159. $this->error('selector "'.$xpath.
  160. '" did not match anything in base template '.
  161. $this->getHtml()->getAttribute('base'));
  162. exit(1);
  163. }
  164. foreach($t as $node) {
  165. if(!$node->parentNode) {
  166. $this->error('cannot manipulate root node (selector was "'.
  167. $xpath.'")');
  168. exit(1);
  169. }
  170. }
  171. return $t;
  172. }
  173. private function createBaseDoc($src) {
  174. //assert($src->getProcessed());
  175. $this->base_doc = new DomDocument();
  176. $this->base_doc->appendChild($this->base_doc->importNode(
  177. $src->getDocumentElement(), true));
  178. $this->base_xpath = new DomXPath($this->base_doc);
  179. }
  180. private function stripDoctype($m){
  181. $this->doctype = $m[1];
  182. return '';
  183. }
  184. public function getDoc() {
  185. if($this->doc == null) {
  186. $this->doc = new DomDocument();
  187. $data = file_get_contents($this->path);
  188. $data = preg_replace_callback('/<!doctype([^>]*)>/',
  189. 'self::stripDoctype', $data);
  190. $this->doc->loadXML($data);
  191. }
  192. return $this->doc;
  193. }
  194. private function getHtml() {
  195. if($this->html == null) {
  196. $this->html = $this->getXPath()->query('//html')->item(0);
  197. if(!$this->html) {
  198. $this->html = false;
  199. }
  200. }
  201. return $this->html;
  202. }
  203. private function getXPath() {
  204. if($this->base_doc) {
  205. $d = $this->base_doc;
  206. }
  207. else {
  208. $d = $this->getDoc();
  209. }
  210. if($this->xpath == null || $this->xpath->document !== $d) {
  211. $this->xpath = new DomXPath($d);
  212. }
  213. return $this->xpath;
  214. }
  215. public static function render($path) {
  216. $f = new Fragmentify($path);
  217. $f->process();
  218. if($f->base_doc) {
  219. $ret = $f->base_doc->saveXML($f->base_doc->documentElement,
  220. LIBXML_NOEMPTYTAG);
  221. }
  222. else {
  223. $ret = $f->getDoc()->saveXML($f->getDoc()->documentElement,
  224. LIBXML_NOEMPTYTAG);
  225. }
  226. if($f->doctype) {
  227. $ret = '<!doctype'.$f->doctype.'>'.PHP_EOL.$ret;
  228. }
  229. return $ret;
  230. }
  231. public function getDocumentElement() {
  232. //assert($this->processed);
  233. if(!$this->processed) {
  234. $this->process();
  235. }
  236. if($this->base_doc) {
  237. return $this->base_doc->documentElement;
  238. }
  239. else {
  240. return $this->getDoc()->documentElement;
  241. }
  242. }
  243. private function processRequires($node, $path=null) {
  244. if($path == null) {
  245. $path = dirname($this->path);
  246. }
  247. if($node->getAttribute('require')) {
  248. $this->processRequire($node, $path);
  249. }
  250. $x = $this->getXPath();
  251. foreach($x->query('.//*[@require]', $node) as $sub) {
  252. $this->processRequire($sub, $path);
  253. }
  254. }
  255. public function processRequire($node, $dir) {
  256. $fn = $dir.'/'.$node->getAttribute('require');
  257. $query = $node->getAttribute('xpath');
  258. if($query === '') {
  259. $query = '//fragment/node()';
  260. }
  261. $f = new Fragmentify($fn);
  262. $f->process();
  263. $x = $f->getXPath();
  264. $reqs = $x->query('//*[@require]');
  265. $subdir = dirname($f->path);
  266. foreach($reqs as $sub) {
  267. $f->processRequire($sub, $subdir);
  268. }
  269. $to_import = $x->query($query);
  270. if(!$to_import->length) {
  271. $this->error('selector "'.$query.'" did not match any nodes in required file '.$fn);
  272. exit(1);
  273. }
  274. foreach($to_import as $import) {
  275. $import = $node->ownerDocument->importNode($import,true);
  276. $node->parentNode->insertBefore($import,$node);
  277. }
  278. $node->parentNode->removeChild($node);
  279. }
  280. private function error($msg) {
  281. error_log('while processing '.$this->path.':'.PHP_EOL.$msg);
  282. }
  283. }