/lib/Adept/Template/Parser.php

https://github.com/yumitsu/adept_framework · PHP · 542 lines · 446 code · 52 blank · 44 comment · 65 complexity · bbdfcf36a947cb390a96e9c2e14a9802 MD5 · raw file

  1. <?php
  2. /**
  3. * Adept Framework
  4. *
  5. * LICENSE
  6. *
  7. * This source file is subject to the BSD license that is bundled
  8. * with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://adept-project.com/license/
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to support@adept-project.com so we can send you a copy immediately.
  14. *
  15. * @category Adept
  16. * @package Adept_Template
  17. * @copyright Copyright (c) 2007-2008 Versus group Inc. (http://www.versus-group.ru)
  18. * @license http://adept-project.com/license/ BSD License
  19. * @version $Id: $
  20. */
  21. class Adept_Template_Parser
  22. {
  23. protected $source;
  24. protected $length;
  25. protected $position;
  26. protected $elementPos;
  27. protected $ignoreExpressions;
  28. protected $templateName;
  29. protected $literalMode = false;
  30. protected $literalEnd = '';
  31. /**
  32. * @var Adept_Template_Location
  33. */
  34. protected $location;
  35. /**
  36. * @var Adept_Template_ParsingObserver
  37. */
  38. protected $observer;
  39. public function __construct($observer = null)
  40. {
  41. $this->location = new Adept_Template_Location();
  42. $this->observer = $observer;
  43. }
  44. /**
  45. * @return Adept_Template_Expression_Factory
  46. */
  47. protected function getExpressionFactory()
  48. {
  49. return Adept_Template_Expression_Factory::getInstance();
  50. }
  51. public function parse($source)
  52. {
  53. $this->source = $source;
  54. $this->length = strlen($source);
  55. $this->position = 0;
  56. $this->ignoreExpressions = false;
  57. do {
  58. $start = $this->position;
  59. if ($this->reachedEndOfFile()) {
  60. return;
  61. }
  62. if ($this->isLiteralMode()) {
  63. $endPos = stripos($this->source, '</' . $this->literalEnd . '>', $start);
  64. if ($endPos === false) {
  65. throw new Adept_Template_Exception('Unexpected end of file in literal block', array(),
  66. clone $this->getCurrentLocation());
  67. }
  68. $literalText = substr($this->source, $this->position, $endPos - $this->position);
  69. $this->observer->characters($literalText);
  70. $this->position += ($endPos - $this->position) + strlen($this->literalEnd) + 3;
  71. $this->setLiteralMode(false);
  72. $this->observer->endElement($this->literalEnd, '</' . $this->literalEnd . '>');
  73. continue;
  74. }
  75. while ($this->position < $this->length && ($this->ignoreExpressions || $this->source[$this->position] != '{')
  76. && $this->source[$this->position] != '<') {
  77. $this->position++;
  78. }
  79. if ($this->reachedEndOfFile()) {
  80. if ($start < $this->length) {
  81. $this->parseCharacters(substr($this->source, $start));
  82. }
  83. return ;
  84. }
  85. // any text before < considered as characters
  86. if ($this->position > $start)
  87. {
  88. $characters = substr($this->source, $start, $this->position - $start);
  89. $this->parseCharacters($characters);
  90. }
  91. // expression or template comment
  92. if ($this->source[$this->position] == '{') {
  93. $this->position++;
  94. if ($this->reachedEndOfFile()) {
  95. return ;
  96. }
  97. $this->elementPos = $this->position;
  98. if ($this->source[$this->elementPos] == '*') {
  99. // Template comment {* ... *}
  100. $position = strpos($this->source, '*}', $this->elementPos);
  101. if ($position === false) {
  102. // Comment is not closed
  103. // TODO Do something
  104. return ;
  105. } else {
  106. $this->position = $position + 2;
  107. }
  108. } else {
  109. // Template expession like {$myVar->getMyProperty()}
  110. $position = strpos($this->source, '}', $this->elementPos);
  111. if ($position === false) {
  112. // Expression is not closed
  113. // TODO Do something
  114. return ;
  115. } else {
  116. $expression = substr($this->source, $this->elementPos, $position - $this->elementPos);
  117. $this->parseExpression($expression);
  118. $this->position = $position + 1;
  119. }
  120. }
  121. } else {
  122. $this->position += 1; // ignore '<' character
  123. if ($this->reachedEndOfFile()) {
  124. return;
  125. }
  126. $this->elementPos = $this->position;
  127. $this->position += 1;
  128. switch($this->source[$this->elementPos]) {
  129. // </tag> cases
  130. case '/':
  131. $start = $this->position;
  132. while ($this->position < $this->length && $this->source[$this->position] != '>') {
  133. $this->position++;
  134. }
  135. if ($this->reachedEndOfFile()) {
  136. return;
  137. }
  138. $tag = substr($this->source, $start, $this->position - $start);
  139. $this->parseEndElement($tag, '</' . $tag . '>');
  140. $this->position += 1; // ignore '>' string
  141. break;
  142. // <?php cases
  143. case '?':
  144. $start = $this->position;
  145. // search instruction type
  146. while ($this->position < $this->length
  147. && strpos(" \n\r\t", $this->source[$this->position]) === false) {
  148. $this->position++;
  149. }
  150. if ($this->reachedEndOfFile()) {
  151. return;
  152. }
  153. $instructionType = substr($this->source, $start, $this->position - $start);
  154. $this->ignoreWhitespace();
  155. // search instruction end and thus the instruction code
  156. $start = $this->position;
  157. $this->position = strpos($this->source, '?>', $start);
  158. if ($this->position === false) {
  159. $this->parseCharacters(substr($this->source, $this->elementPos - 1));
  160. return;
  161. }
  162. $code = substr($this->source, $start, $this->position - $start);
  163. $this->observer->processingInstruction($instructionType, $code);
  164. $this->position += 2; // ignore '? >' string
  165. break;
  166. // <!-- and <% cases
  167. case '!':
  168. $start = $this->position - 2;
  169. if (substr($this->source, $start, 4) == "<!--") {
  170. $position = strpos($this->source, '-->', $start);
  171. if ($position !== false) {
  172. $rawText = substr($this->source, $start, $position - $start + 3);
  173. $this->parseCharacters($rawText);
  174. $this->position = $position + 3;
  175. break;
  176. }
  177. }
  178. if (substr($this->source, $start, 3) == '<![') {
  179. // CDATA section
  180. $position = strpos($this->source, '[', $start + 3);
  181. if ($position === false) {
  182. throw new Adept_Template_Exception('Incorrect CDATA block definition', array(),
  183. $this->getCurrentLocation());
  184. }
  185. $name = substr($this->source, $start + 3, $position - $start - 3);
  186. $endPosition = strpos($this->source, ']]>', $position);
  187. if ($endPosition === false) {
  188. throw new Adept_Template_Exception('Unclosed CDATA block', array(),
  189. $this->getCurrentLocation());
  190. }
  191. $content = substr($this->source, $position + 1, $endPosition - $position - 1);
  192. $cdataSource = substr($this->source, $start, $endPosition + 3 - $start);
  193. $this->getObserver()->cdata($name, $content, $source);
  194. $this->position = $endPosition + 3;
  195. break;
  196. }
  197. while ($this->position < $this->length && $this->source[$this->position] != '<') {
  198. $this->position++;
  199. }
  200. $characters = substr($this->source, $start, $this->position - $start);
  201. $this->parseCharacters($characters);
  202. break;
  203. case '%':
  204. $start = $this->position - 2;
  205. while ($this->position < $this->length && $this->source[$this->position] != '<') {
  206. $this->position++;
  207. }
  208. $characters = substr($this->source, $start, $this->position - $start);
  209. $this->parseCharacters($characters);
  210. break;
  211. // <tag or any < case (e.g. compare operator in javascript block)
  212. case ' ':
  213. case "\n":
  214. case "\n":
  215. case "\r":
  216. case "\t":
  217. case "=":
  218. $start = $this->position - 2;
  219. while ($this->position < $this->length && $this->source[$this->position] != '<') {
  220. $this->position++;
  221. }
  222. $characters = substr($this->source, $start, $this->position - $start);
  223. $this->parseCharacters($characters);
  224. break;
  225. default:
  226. while ($this->position < $this->length
  227. && strpos("/> \n\r\t", $this->source[$this->position]) === false) {
  228. $this->position++;
  229. }
  230. if ($this->reachedEndOfFile()) {
  231. return;
  232. }
  233. $tag = substr($this->source, $this->elementPos, $this->position - $this->elementPos);
  234. $attributes = array();
  235. $this->ignoreWhitespace();
  236. // search end of tag
  237. while ( $this->position < $this->length &&
  238. $this->source[$this->position] != '/' &&
  239. $this->source[$this->position] != '>')
  240. {
  241. $start = $this->position;
  242. while ($this->position < $this->length && strpos("/>= \n\r\t", $this->source[$this->position]) === false) {
  243. $this->position++;
  244. }
  245. if ($this->reachedEndOfFile()) {
  246. return;
  247. }
  248. $attributeName = substr($this->source, $start, $this->position - $start);
  249. $attributeValue = null;
  250. $this->ignoreWhitespace();
  251. if ($this->reachedEndOfFile()) {
  252. return;
  253. }
  254. if ($this->source[$this->position] == '=') {
  255. $attributeValue = "";
  256. $this->position++;
  257. $this->ignoreWhitespace();
  258. if ($this->reachedEndOfFile()) {
  259. return;
  260. }
  261. $quote = $this->source[$this->position];
  262. if ($quote == '"' || $quote == "'") {
  263. $start = $this->position + 1;
  264. $this->position = strpos($this->source, $quote, $start);
  265. if ($this->position === false) {
  266. $this->parseCharacters(substr($this->source, $this->elementPos - 1));
  267. return;
  268. }
  269. $attributeValue = substr($this->source, $start, $this->position - $start);
  270. $this->position++;
  271. if ($this->reachedEndOfFile()) {
  272. return;
  273. }
  274. if (strpos("/> \n\r\t", $this->source[$this->position]) === false) {
  275. throw new Adept_Template_Exception('Invalid tag attribute syntax', array(),
  276. $this->getCurrentLocation());
  277. }
  278. } else {
  279. $start = $this->position;
  280. while ($this->position < $this->length && strpos("/> \n\r\t",
  281. $this->source[$this->position]) === false) {
  282. $this->position++;
  283. }
  284. if ($this->reachedEndOfFile()) {
  285. return;
  286. }
  287. $attributeValue = substr($this->source, $start, $this->position - $start);
  288. }
  289. }
  290. $attributes[$attributeName] = $attributeValue;
  291. $this->ignoreWhitespace();
  292. }
  293. if ($this->reachedEndOfFile()) {
  294. return;
  295. }
  296. if ($this->source[$this->position] == '/') {
  297. $this->position += 1;
  298. if ($this->reachedEndOfFile()) {
  299. return;
  300. }
  301. if ($this->source[$this->position] != '>') {
  302. throw new Adept_Template_Exception('Invalid tag syntax', array(),
  303. clone $this->getCurrentLocation());
  304. }
  305. $tagSource = '<' . substr($this->source, $this->elementPos,
  306. $this->position - $this->elementPos) . '>';
  307. $this->parseStartElement($tag, $attributes, true, $tagSource);
  308. } else {
  309. $tagSource = '<' . substr($this->source, $this->elementPos,
  310. $this->position - $this->elementPos) . '>';
  311. $this->parseStartElement($tag, $attributes, false, $tagSource);
  312. }
  313. $this->position += 1;
  314. break;
  315. }
  316. }
  317. } while ($this->position < $this->length);
  318. }
  319. protected function parseCharacters($text)
  320. {
  321. // parse expressions in the text
  322. $text = str_replace('&ld;', '{', $text);
  323. $text = str_replace('&rd;', '}', $text);
  324. $this->observer->characters($text);
  325. }
  326. protected function parseExpression($expression)
  327. {
  328. $this->observer->expression($expression);
  329. }
  330. public function removeComments($string)
  331. {
  332. return preg_replace('/(\{\*(?:[^\*]*)\*\})/is', '', $string);
  333. }
  334. /**
  335. * @param string $attribute
  336. * @param bool $ignoreExpression
  337. * @return Adept_Template_TagAttribute
  338. */
  339. protected function parseAttributeValue($attribute, $ignoreExpression = false)
  340. {
  341. $attribute = $this->removeComments($attribute);
  342. $result = new Adept_Template_TagAttribute();
  343. if ($ignoreExpression) {
  344. $result->addTextFragment($attribute);
  345. return $result;
  346. }
  347. $pattern = '~\{([^\}]*)\}~';
  348. preg_match_all($pattern, $attribute, $matches);
  349. $expressions = $matches[1];
  350. $strings = preg_split($pattern, $attribute);
  351. if (count($expressions) == 0) {
  352. $result->addTextFragment($attribute);
  353. return $result;
  354. }
  355. $parts = array();
  356. for ($i = 0; $i <= count($expressions); $i++) {
  357. if (strlen($strings[$i]) > 0) {
  358. $result->addTextFragment($strings[$i]);
  359. }
  360. if (isset($expressions[$i]) && strlen($expressions[$i]) > 0) {
  361. $result->addExpressionFragment($expressions[$i]);
  362. }
  363. }
  364. return $result;
  365. }
  366. protected function parseStartElement($tag, $attributes, $closed, $tagSource)
  367. {
  368. $this->getObserver()->startElement($tag,
  369. $this->parseAttributes($attributes), $closed, $tagSource);
  370. }
  371. protected function parseEndElement($tag, $tagSource)
  372. {
  373. $this->getObserver()->endElement($tag, $tagSource);
  374. }
  375. protected function parseAttributes($attributes)
  376. {
  377. $result = array();
  378. foreach ($attributes as $name => $stringValue) {
  379. $result[$name] = $this->parseAttributeValue($stringValue);
  380. }
  381. return $result;
  382. }
  383. protected function ignoreWhitespace()
  384. {
  385. while ($this->position < $this->length && strpos(" \n\r\t", $this->source[$this->position]) !== false) {
  386. $this->position++;
  387. }
  388. }
  389. protected function reachedEndOfFile()
  390. {
  391. if ($this->position >= $this->length) {
  392. // $this->parseCharacters(substr($this->source, $this->elementPos - 1), $this->getCurrentLocation());
  393. return true;
  394. } else {
  395. return false;
  396. }
  397. }
  398. public function literalTag($literalEnd)
  399. {
  400. $this->setLiteralMode(true);
  401. $this->setLiteralEnd($literalEnd);
  402. }
  403. protected function updateLocation()
  404. {
  405. $this->location->setLineNumber(1 + substr_count(substr($this->source, 0, $this->position), "\n"));
  406. }
  407. public function getLocation()
  408. {
  409. $this->updateLocation();
  410. return $this->location;
  411. }
  412. public function getCurrentLocation()
  413. {
  414. $this->updateLocation();
  415. return clone $this->location;
  416. }
  417. public function getObserver()
  418. {
  419. return $this->observer;
  420. }
  421. public function setObserver($observer)
  422. {
  423. $this->observer = $observer;
  424. }
  425. public function isLiteralMode()
  426. {
  427. return $this->literalMode;
  428. }
  429. public function setLiteralMode($literalMode)
  430. {
  431. $this->literalMode = $literalMode;
  432. }
  433. public function getLiteralEnd()
  434. {
  435. return $this->literalEnd;
  436. }
  437. public function setLiteralEnd($literalEnd)
  438. {
  439. $this->literalEnd = $literalEnd;
  440. }
  441. public function getTemplateName()
  442. {
  443. return $this->location->templateName;
  444. }
  445. public function setTemplateName($templateName)
  446. {
  447. $this->location->setFileName($templateName);
  448. }
  449. }