PageRenderTime 55ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/haml/HamlParser.php

http://phamlp.googlecode.com/
PHP | 1249 lines | 733 code | 79 blank | 437 comment | 118 complexity | 39353056e623a028dcb81f9ee916b3fc MD5 | raw file
  1. <?php
  2. /* SVN FILE: $Id: HamlParser.php 117 2010-09-21 09:41:58Z chris.l.yates@gmail.com $ */
  3. /**
  4. * HamlParser class file.
  5. * HamlParser allows you to write view files in
  6. * {@link http://haml-lang.com/ Haml}.
  7. *
  8. * Please see the {@link http://haml-lang.com/docs/yardoc/file.Haml_REFERENCE.html#plain_text Haml documentation} for the syntax.
  9. *
  10. * Credits:
  11. * This is a port of Haml to PHP. All the genius comes from the people that
  12. * invented and develop Haml; in particular:
  13. * + {@link http://hamptoncatlin.com/ Hampton Catlin},
  14. * + {@link http://nex-3.com/ Nathan Weizenbaum},
  15. * + {@link http://chriseppstein.github.com/ Chris Eppstein}
  16. *
  17. * The bugs are mine. Please report any found at {@link http://code.google.com/p/phamlp/issues/list}
  18. *
  19. * Notes
  20. * <ul>
  21. * <li>Debug (addition)<ul>
  22. * <li>Source debug - adds comments to the output showing each source line above
  23. * the result - ?#s+ to turn on, ?#s- to turn off, ?#s! to toggle</li>
  24. * <li>Output debug - shows the output directly in the browser - ?#o+ to turn on, ?#o- to turn off, ?#o! to toggle</li>
  25. * <li>Control both at once - ?#so+ to turn on, ?#so- to turn off, ?#so! to toggle</li>
  26. * <li>Ugly mode can be controlled by the template</li>
  27. * <liUugly mode strips comments in the output by default</li>
  28. * <li>Ugly mode is turned off when in debug</li></ul></li>
  29. * <li>"-" command (notes)<ul>
  30. * <li>PHP does not require ending ";"</li>
  31. * <li>PHP control blocks are automatically bracketed</li>
  32. * <li>Switch Case statements do not end with ":"
  33. * <li>do-while control blocks are written as "do (expression)"</li></ul></li>
  34. * </ul>
  35. * Comes with filters that run "out of the box":
  36. * + <b>plain</b>: useful for large chunks of text to ensure Haml doesn't do anything.
  37. * + <b>escaped</b>: like plain but the output is (x)html escaped.
  38. * + <b>preserve</b>: like plain but preserves the whitespace.
  39. * + <b>cdata</b>: wraps the content in CDATA tags.
  40. * + <b>javascript</b>: wraps the content in <script> and CDATA tags. Useful for adding inline JavaScript.
  41. * + <b>css</b>: wraps the content in <style> and CDATA tags. Useful for adding inline CSS.
  42. * + <b>php</b>: wraps the content in <?php tags. The content is PHP code.
  43. * There are two filters that require external classes to work. See {@link http://code.google.com/p/phamlp/wiki/PredefinedFilters PredefinedFilters on the PHamlP wiki} for details of how to use them.
  44. * + <b>markdown</b>: Parses the filtered text with Markdown.
  45. * + <b>textile</b>: Parses the filtered text with Textile.
  46. * PHP can be used in all the filters (except php) by wrapping expressions in #().
  47. *
  48. * @author Chris Yates <chris.l.yates@gmail.com>
  49. * @copyright Copyright (c) 2010 PBM Web Development
  50. * @license http://phamlp.googlecode.com/files/license.txt
  51. * @package PHamlP
  52. * @subpackage Haml
  53. */
  54. require_once('tree/HamlNode.php');
  55. require_once('HamlHelpers.php');
  56. require_once('HamlException.php');
  57. /**
  58. * HamlParser class.
  59. * Parses {@link http://haml-lang.com/ Haml} view files.
  60. * @package PHamlP
  61. * @subpackage Haml
  62. */
  63. class HamlParser {
  64. /**#@+
  65. * Debug modes
  66. */
  67. const DEBUG_NONE = 0;
  68. const DEBUG_SHOW_SOURCE = 1;
  69. const DEBUG_SHOW_OUTPUT = 2;
  70. const DEBUG_SHOW_ALL = 3;
  71. /**#@-*/
  72. /**#@+
  73. * Regexes used to parse the document
  74. */
  75. const REGEX_HAML = '/(?m)^([ \x09]*)((?::(\w*))?(?:%([\w:-]*))?(?:\.((?:(?:[-_:a-zA-Z]|#\{.+?\})+(?:[-:\w]|#\{.+?\})*(?:\.?))*))?(?:#((?:[_:a-zA-Z]|#\{.+?\})+(?:[-:\w]|#\{.+?\})*))?(?:\[(.+)\])?(?:(\()((?:(?:html_attrs\(.*?\)|data[\t ]*=[\t ]*\{.+?\}|(?:[_:a-zA-Z]+[-:\w]*)[\t ]*=[\t ]*.+)[\t ]*)+\)))?(?:(\{)((?::(?:html_attrs\(.*?\)|data[\t ]*=>[\t ]*\{.+?\}|(?:[_:a-zA-Z]+[-:\w]*)[\t ]*=>?[\t ]*.+)(?:,?[\t ]*)?)+\}))?(\|?>?\|?<?) *((?:\?#)|!!!|\/\/|\/|-#|!=|&=|!|&|=|-|~|\\\\\\\\)? *(.*?)(?:\s(\|)?)?)$/'; // Haml line
  76. const REGEX_ATTRIBUTES = '/:?(?:(data)\s*=>?\s*([({].*?[})]))|(\w+(?:[-:]\w*)*)\s*=>?\s*(?(?=\[)(?:\[(.+?)\])|(?(?=([\'"]))(?:[\'"](.*?)\5)|([^\s,]+)))/';
  77. const REGEX_ATTRIBUTE_FUNCTION = '/^\$?[_a-zA-Z]\w*(?(?=->)(->[_a-zA-Z]\w*)+|(::[_a-zA-Z]\w*)?)\(.+\)$/'; // Matches functions and instantiated and static object methods
  78. const REGEX_WHITESPACE_REMOVAL = '/(.*?)\s+$/s';
  79. const REGEX_WHITESPACE_REMOVAL_DEBUG = '%(.*?)(?:<br />\s)$%s'; // whitespace control when showing output
  80. //const REGEX_CODE_INTERPOLATION = '/(?:(?<!\\\\)#{(.+?(?:\(.*?\).*?)*)})/';
  81. /**#@-*/
  82. const MATCH_INTERPOLATION = '/(?<!\\\\)#\{(.*?)\}/';
  83. const INTERPOLATE = '<?php echo \1; ?>';
  84. const HTML_ATTRS = '/html_attrs\(\s*((?(?=\')(?:.*?)\'|(?:.*?)"))(?:\s*,\s*(.*?))?\)/';
  85. /**#@+
  86. * Haml regex match positions
  87. */
  88. const HAML_HAML = 0;
  89. const HAML_INDENT = 1;
  90. const HAML_SOURCE = 2;
  91. const HAML_FILTER = 3;
  92. const HAML_TAG = 4;
  93. const HAML_CLASS = 5;
  94. const HAML_ID = 6;
  95. const HAML_OBJECT_REFERENCE = 7;
  96. const HAML_OPEN_XML_ATTRIBUTES = 8;
  97. const HAML_XML_ATTRIBUTES = 9;
  98. const HAML_OPEN_RUBY_ATTRIBUTES = 10;
  99. const HAML_RUBY_ATTRIBUTES = 11;
  100. const HAML_WHITESPACE_REMOVAL = 12;
  101. const HAML_TOKEN = 13;
  102. const HAML_CONTENT = 14;
  103. const HAML_MULTILINE = 15;
  104. /**#@-*/
  105. /**#@+
  106. * Haml tokens
  107. */
  108. const DOCTYPE = '!!!';
  109. const HAML_COMMENT = '!(-#|//)!';
  110. const XML_COMMENT = '/';
  111. const SELF_CLOSE_TAG = '/';
  112. const ESCAPE_XML = '&=';
  113. const UNESCAPE_XML = '!=';
  114. const INSERT_CODE = '=';
  115. const INSERT_CODE_PRESERVE_WHITESPACE = '~';
  116. const RUN_CODE = '-';
  117. const INNER_WHITESPACE_REMOVAL = '<';
  118. const OUTER_WHITESPACE_REMOVAL = '>';
  119. const BLOCK_LEFT_OUTER_WHITESPACE_REMOVAL = '|>';
  120. const BLOCK_RIGHT_OUTER_WHITESPACE_REMOVAL = '>|';
  121. /**#@-*/
  122. const MULTILINE= ' |';
  123. /**#@+
  124. * Attribute tokens
  125. */
  126. const OPEN_XML_ATTRIBUTES = '(';
  127. const CLOSE_XML_ATTRIBUTES = ')';
  128. const OPEN_RUBY_ATTRIBUTES = '{';
  129. const CLOSE_RUBY_ATTRIBUTES = '}';
  130. /**#@-*/
  131. /**#@+
  132. * Directives
  133. */
  134. const DIRECTIVE = '?#';
  135. const SOURCE_DEBUG = 's';
  136. const OUTPUT_DEBUG = 'o';
  137. /**#@-*/
  138. const IS_XML_PROLOG = 'XML';
  139. const XML_PROLOG = "<?php echo \"<?xml version='1.0' encoding='{encoding}' ?>\n\"; ?>";
  140. const DEFAULT_XML_ENCODING = 'utf-8';
  141. const XML_ENCODING = '{encoding}';
  142. /**
  143. * @var string Doctype format. Determines how the Haml Doctype declaration is
  144. * rendered.
  145. * @see doctypes
  146. */
  147. private $format = 'xhtml';
  148. /**
  149. * @var string Custom Doctype. If not null and the Doctype declaration in the
  150. * Haml Document is not a built in Doctype this will be used as the Doctype.
  151. * This allows Haml to be used for non-(X)HTML documents that are XML compliant.
  152. * @see doctypes
  153. * @see emptyTags
  154. * @see inlineTags
  155. * @see minimizedAttributes
  156. */
  157. private $doctype;
  158. /**
  159. * @var boolean whether or not to escape X(HT)ML-sensitive characters in script.
  160. * If this is true, = behaves like &=; otherwise, it behaves like !=.
  161. * Note that if this is set, != should be used for yielding to subtemplates
  162. * and rendering partials. Defaults to false.
  163. */
  164. private $escapeHtml = false;
  165. /**
  166. * @var boolean Whether or not attribute hashes and scripts designated by
  167. * = or ~ should be evaluated. If true, the scripts are rendered as empty strings.
  168. * Defaults to false.
  169. */
  170. private $suppressEval = false;
  171. /**
  172. * @var string The character that should wrap element attributes. Characters
  173. * of this type within attributes will be escaped (e.g. by replacing them with
  174. * &apos;) if the character is an apostrophe or a quotation mark.
  175. * Defaults to " (an quotation mark).
  176. */
  177. private $attrWrapper = '"';
  178. /**
  179. * @var array available output styles:
  180. * nested: output is nested according to the indent level in the source
  181. * expanded: block tags have their own lines as does content which is indented
  182. * compact: block tags and their content go on one line
  183. * compressed: all unneccessary whitepaces is removed. If ugly is true this style is used.
  184. */
  185. private $styles = array('nested', 'expanded', 'compact', 'compressed');
  186. /**
  187. * @var string output style. Note: ugly must be false to allow style.
  188. */
  189. private $style = 'nested';
  190. /**
  191. * @var boolean if true no attempt is made to properly indent or format
  192. * the output. Reduces size of output file but is not very readable;
  193. * equivalent of style == compressed. Note: ugly must be false to allow style.
  194. * Defaults to true.
  195. * @see style
  196. */
  197. private $ugly = true;
  198. /**
  199. * @var boolean if true comments are preserved in ugly mode. If not in
  200. * ugly mode comments are always output. Defaults to false.
  201. */
  202. private $preserveComments = false;
  203. /**
  204. * @var integer Initial debug setting:
  205. * no debug, show source, show output, or show all.
  206. * Debug settings can be controlled in the template
  207. * Defaults to DEBUG_NONE.
  208. */
  209. private $debug = self::DEBUG_NONE;
  210. /**
  211. * @var string Path to the directory containing user defined filters. If
  212. * specified this dirctory will be searched before PHamlP looks for the filter
  213. * in it's collection. This allows the default filters to be overridden and
  214. * new filters to be installed. Note: No trailing directory separator.
  215. */
  216. private $filterDir;
  217. /**
  218. * @var string Path to the file containing user defined Haml helpers.
  219. */
  220. private $helperFile;
  221. /**
  222. * @var string Haml helper class. This must be an instance of HamlHelpers.
  223. */
  224. private $helperClass = 'HamlHelpers';
  225. /**
  226. * @var array built in Doctypes
  227. * @see format
  228. * @see doctype
  229. */
  230. private $doctypes = array (
  231. 'html4' => array (
  232. '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">', //HTML 4.01 Transitional
  233. 'Strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD 4.01 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">', //HTML 4.01 Strict
  234. 'Frameset' => '<!DOCTYPE html PUBLIC "-//W3C//DTD 4.01 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">', //HTML 4.01 Frameset
  235. ),
  236. 'html5' => array (
  237. '<!DOCTYPE html>', // XHTML 5
  238. ),
  239. 'xhtml' => array (
  240. '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">', //XHTML 1.0 Transitional
  241. 'Strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">', //XHTML 1.0 Strict
  242. 'Frameset' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">', //XHTML 1.0 Frameset
  243. '5' => '<!DOCTYPE html>', // XHTML 5
  244. '1.1' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">', // XHTML 1.1
  245. 'Basic' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">', //XHTML Basic 1.1
  246. 'Mobile' => '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">', //XHTML Mobile 1.2
  247. )
  248. );
  249. /**
  250. * @var array A list of tag names that should be automatically self-closed
  251. * if they have no content.
  252. */
  253. private $emptyTags = array('meta', 'img', 'link', 'br', 'hr', 'input', 'area', 'param', 'col', 'base');
  254. /**
  255. * @var array A list of inline tags.
  256. */
  257. private $inlineTags = array('a', 'abbr', 'accronym', 'b', 'big', 'cite', 'code', 'dfn', 'em', 'i', 'kbd', 'q', 'samp', 'small', 'span', 'strike', 'strong', 'tt', 'u', 'var');
  258. /**
  259. * @var array attributes that are minimised
  260. */
  261. private $minimizedAttributes = array('compact', 'checked', 'declare', 'readonly', 'disabled', 'selected', 'defer', 'ismap', 'nohref', 'noshade', 'nowrap', 'multiple', 'noresize');
  262. /**
  263. * @var array A list of tag names that should automatically have their newlines preserved.
  264. */
  265. private $preserve = array('pre', 'textarea');
  266. /**#@-*/
  267. /**
  268. * @var string the character used for indenting. Space or tab.
  269. * @see indentSpaces
  270. */
  271. private $indentChar;
  272. /**
  273. * @var array allowable characters for indenting
  274. */
  275. private $indentChars = array(' ', "\t");
  276. /**
  277. * @var integer number of spaces for indentation.
  278. * Used on source if {@link indentChar} is space.
  279. * Used on output if {@link ugly} is false.
  280. */
  281. private $indentSpaces;
  282. /**
  283. * @var array loaded filters
  284. */
  285. private $filters = array();
  286. /**
  287. * @var boolean whether line is in a filter
  288. */
  289. private $inFilter = false;
  290. /**
  291. * @var boolean whether to show the output in the browser for debug
  292. */
  293. private $showOutput;
  294. /**
  295. * @var boolean whether to show the source in the browser for debug
  296. */
  297. private $showSource;
  298. /**
  299. * @var integer line number of source being parsed
  300. */
  301. private $line;
  302. /**
  303. * @var string name of file being parsed
  304. */
  305. private $filename;
  306. /**
  307. * @var mixed source
  308. */
  309. private $source;
  310. /**
  311. * HamlParser constructor.
  312. * @param array options
  313. * @return HamlParser
  314. */
  315. public function __construct($options = array()) {
  316. if (isset($options['language'])) {
  317. Phamlp::$language = $options['language'];
  318. unset($options['language']);
  319. }
  320. foreach ($options as $name => $value) {
  321. $this->$name = $value;
  322. } // foreach
  323. if ($this->ugly) {
  324. $this->style = 'compressed';
  325. }
  326. $this->format = strtolower($this->format);
  327. if (is_null($this->doctype) &&
  328. !array_key_exists($this->format, $this->doctypes)) {
  329. throw new HamlException('Invalid {what} ({value}). Must be one of "{options}"', array('{what}'=>'format', '{value}'=>$this->format, '{options}'=>join(', ', array_keys($this->doctypes))), $this);
  330. }
  331. $this->showSource = $this->debug & HamlParser::DEBUG_SHOW_SOURCE;
  332. $this->showOutput = $this->debug & HamlParser::DEBUG_SHOW_OUTPUT;
  333. require_once dirname(__FILE__).DIRECTORY_SEPARATOR.'HamlHelpers.php';
  334. if (isset($this->helperFile)) {
  335. require_once $this->helperFile;
  336. $this->helperClass = basename($this->helperFile, ".php");
  337. if (!is_subclass_of($this->helperClass, 'HamlHelpers')) {
  338. throw new HamlException('{what} must extend {base} class', array('{what}'=>$this->helperClass, '{base}'=>'HamlHelpers'), $this);
  339. }
  340. }
  341. }
  342. /**
  343. * Getter.
  344. * @param string name of property to get
  345. * @return mixed return value of getter function
  346. */
  347. public function __get($name) {
  348. $getter = 'get' . ucfirst($name);
  349. if (method_exists($this, $getter)) {
  350. return $this->$getter();
  351. }
  352. throw new HamlException('No getter function for {what}', array('{what}'=>$name));
  353. }
  354. public function getFilename() {
  355. return $this->filename;
  356. }
  357. public function getLine() {
  358. return $this->line;
  359. }
  360. public function getSource() {
  361. return $this->source;
  362. }
  363. /**
  364. * Parses a Haml file.
  365. * If an output directory is given the resulting PHP is cached.
  366. * @param string path to file to parse
  367. * @param mixed boolean: true to use the default cache directory, false to use
  368. * the source file directory. string: path to the cache directory.
  369. * null: disable caching
  370. * @param string output file extension
  371. * @param integer permission for the output directory and file
  372. * @return mixed string: the resulting PHP if no output directory is specified
  373. * or the output filename if the output directory is specified.
  374. * boolean: false if the output file could not be written.
  375. */
  376. public function parse($sourceFile, $cacheDir=null, $permission=0755, $sourceExtension='.haml', $outputExtension='.php') {
  377. if (is_string($cacheDir) || is_bool($cacheDir)) {
  378. if (is_bool($cacheDir)) {
  379. $cacheDir =
  380. ($cacheDir ? dirname(__FILE__).DIRECTORY_SEPARATOR.'haml-cache' :
  381. dirname($sourceFile));
  382. }
  383. $outputFile = $cacheDir.DIRECTORY_SEPARATOR.
  384. basename($sourceFile, $sourceExtension).$outputExtension;
  385. if (@filemtime($sourceFile) > @filemtime($outputFile)) {
  386. if (!is_dir($cacheDir)) {
  387. @mkdir($cacheDir, $permission);
  388. }
  389. $return = (file_put_contents($outputFile, $this->haml2PHP($sourceFile))
  390. === false ? false : $outputFile);
  391. if ($return !== false) {
  392. @chmod($outputFile, $permission);
  393. }
  394. }
  395. else {
  396. $return = $outputFile;
  397. }
  398. }
  399. else {
  400. $return = $this->haml2PHP($sourceFile);
  401. }
  402. return $return;
  403. }
  404. /**
  405. * Parses a Haml file into PHP.
  406. * @param string path to file to parse
  407. * @return string the resulting PHP
  408. */
  409. public function haml2PHP($sourceFile) {
  410. $this->line = 0;
  411. $this->filename = $sourceFile;
  412. $helpers = "<?php\nrequire_once '".dirname(__FILE__).DIRECTORY_SEPARATOR."HamlHelpers.php';\n";
  413. if (isset($this->helperFile)) {
  414. $helpers .= "require_once '{$this->helperFile}';\n";
  415. }
  416. $helpers .= "?>";
  417. return $helpers . $this->toTree(file_get_contents($sourceFile))->render();
  418. }
  419. /**
  420. * Parse Haml source into a document tree.
  421. * @param string Haml source
  422. * @return HamlRootNode the root of this document tree
  423. */
  424. private function toTree($source) {
  425. $this->source = explode("\n", $source);
  426. $this->setIndentChar();
  427. preg_match_all(self::REGEX_HAML, $source, $this->source, PREG_SET_ORDER);
  428. unset($source);
  429. $root = new HamlRootNode(array(
  430. 'format' => $this->format,
  431. 'style' => $this->style,
  432. 'attrWrapper' => $this->attrWrapper,
  433. 'minimizedAttributes' => $this->minimizedAttributes
  434. ));
  435. $this->buildTree($root);
  436. return $root;
  437. }
  438. /**
  439. * Builds a parse tree under the parent node.
  440. * @param HamlNode the parent node
  441. */
  442. private function buildTree($parent) {
  443. while (!empty($this->source) && $this->isChildOf($parent, $this->source[0])) {
  444. $line = $this->getNextLine();
  445. if (!empty($line)) {
  446. $node = ($this->inFilter ?
  447. new HamlNode($line[self::HAML_SOURCE], $parent) :
  448. $this->parseLine($line, $parent));
  449. if (!empty($node)) {
  450. $node->token = $line;
  451. $node->showOutput = $this->showOutput;
  452. $node->showSource = $this->showSource;
  453. $this->addChildren($node, $line);
  454. }
  455. }
  456. }
  457. }
  458. /**
  459. * Adds children to a node if the current line has children.
  460. * @param HamlNode the node to add children to
  461. * @param array line to test
  462. */
  463. private function addChildren($node, $line) {
  464. if ($node instanceof HamlFilterNode) {
  465. $this->inFilter = true;
  466. }
  467. if ($this->hasChild($line, $this->inFilter)) {
  468. $this->buildTree($node);
  469. if ($node instanceof HamlFilterNode) {
  470. $this->inFilter = false;
  471. }
  472. }
  473. }
  474. /**
  475. * Returns a value indicating if the next line is a child of the parent line
  476. * @param array parent line
  477. * @param boolean whether to all greater than the current indent
  478. * Used if the source line is a comment or a filter.
  479. * If true all indented lines are regarded as children; if not the child line
  480. * must only be indented by 1 or blank. Defaults to false.
  481. * @return boolean true if the next line is a child of the parent line
  482. * @throws Exception if the indent is invalid
  483. */
  484. private function hasChild($line, $allowGreater = false) {
  485. if (!empty($this->source)) {
  486. $i = 0;
  487. $c = count($this->source);
  488. while (empty($nextLine[self::HAML_SOURCE]) && $i <= $c) {
  489. $nextLine = $this->source[$i++];
  490. }
  491. $level = $this->getLevel($nextLine, $line['line'] + $i);
  492. if (($level == $line['level'] + 1) ||
  493. ($allowGreater && $level > $line['level'])) {
  494. return true;
  495. }
  496. elseif ($level <= $line['level']) {
  497. return false;
  498. }
  499. else {
  500. throw new HamlException('Illegal indentation level ({level}); indentation level can only increase by one', array('{level}'=>$level), $this);
  501. }
  502. }
  503. else {
  504. return false;
  505. }
  506. }
  507. /**
  508. * Returns a value indicating if $line is a child of a node.
  509. * A blank line is a child of a node.
  510. * @param HamlNode the node
  511. * @param array the line to check
  512. * @return boolean true if the line is a child of the node, false if not
  513. */
  514. private function isChildOf($node, $line) {
  515. $haml = trim($line[self::HAML_HAML]);
  516. return empty($haml) || $this->getLevel($line, $this->line) >
  517. $node->level;
  518. }
  519. /**
  520. * Determine the indent character and indent spaces.
  521. * The first character of the first indented line determines the character.
  522. * If this is a space the number of spaces determines the indentSpaces; this
  523. * is always 1 if the indent character is a tab.
  524. * @throws HamlException if the indent is mixed
  525. */
  526. private function setIndentChar() {
  527. foreach ($this->source as $l=>$source) {
  528. if (!empty($source) && in_array($source[0], $this->indentChars)) {
  529. $this->indentChar = $source[0];
  530. for ($i = 0, $len = strlen($source); $i < $len && $source[$i] == $this->indentChar; $i++);
  531. if ($i < $len && in_array($source[$i], $this->indentChars)) {
  532. $this->line = ++$l;
  533. $this->source = $source;
  534. throw new HamlException('Mixed indentation not allowed', array(), $this);
  535. }
  536. $this->indentSpaces = ($this->indentChar == ' ' ? $i : 1);
  537. return;
  538. }
  539. } // foreach
  540. $this->indentChar = ' ';
  541. $this->indentSpaces = 2;
  542. }
  543. /**
  544. * Gets the next line.
  545. * @param array remaining source lines
  546. * @return array the next line
  547. */
  548. private function getNextLine() {
  549. $line = array_shift($this->source);
  550. // Blank lines ore OK
  551. $haml = trim($line[self::HAML_HAML]);
  552. if (empty($haml)) {
  553. $this->line++;
  554. return null;
  555. }
  556. // The regex will strip off a '<' at the start of a line
  557. if ($line[self::HAML_WHITESPACE_REMOVAL] ===
  558. self::INNER_WHITESPACE_REMOVAL && empty($line[self::HAML_TAG])) {
  559. $line[self::HAML_CONTENT] =
  560. $line[self::HAML_WHITESPACE_REMOVAL].$line[self::HAML_TOKEN].$line[self::HAML_CONTENT];
  561. }
  562. // The regex treats lines starting with [.+] as an object reference; they are just content
  563. if (!empty($line[self::HAML_OBJECT_REFERENCE]) && empty($line[self::HAML_TAG])) {
  564. unset($line[self::HAML_OBJECT_REFERENCE]);
  565. $line[self::HAML_CONTENT] = $line[self::HAML_SOURCE];
  566. }
  567. $line['line'] = $this->line++;
  568. $line['level'] = $this->getLevel($line, $this->line);
  569. $line['filename'] = $this->filename;
  570. if ($this->isMultiline($line)) {
  571. $line = $this->getMultiline($line);
  572. }
  573. return $line;
  574. }
  575. /**
  576. * Returns the indent level of the line.
  577. * @param array the line
  578. * @param integer line number
  579. * @return integer the indent level of the line
  580. * @throws Exception if the indent level is invalid
  581. */
  582. private function getLevel($line, $n) {
  583. if ($line[self::HAML_INDENT] && $this->indentChar === ' ') {
  584. $indent = strlen($line[self::HAML_INDENT]) / $this->indentSpaces;
  585. }
  586. else {
  587. $indent = strlen($line[self::HAML_INDENT]);
  588. }
  589. if (!is_integer($indent) ||
  590. preg_match("/[^{$this->indentChar}]/", $line[self::HAML_INDENT])) {
  591. throw new HamlException('Invalid indentation', array(), $this);
  592. }
  593. return $indent;
  594. }
  595. /**
  596. * Parse a line of Haml into a HamlNode for the document tree
  597. * @param array line to parse
  598. * @param HamlNode parent node
  599. * @return HamlNode
  600. */
  601. private function parseLine($line, $parent) {
  602. if ($this->isHamlComment($line)) {
  603. return $this->parseHamlComment($line);
  604. }
  605. elseif ($this->isXmlComment($line)) {
  606. return $this->parseXmlComment($line, $parent);
  607. }
  608. elseif ($this->isElement($line)) {
  609. return $this->parseElement($line, $parent);
  610. }
  611. elseif ($this->isHelper($line)) {
  612. return $this->parseHelper($line, $parent);
  613. }
  614. elseif ($this->isCode($line)) {
  615. return $this->parseCode($line, $parent);
  616. }
  617. elseif ($this->isDirective($line)) {
  618. return $this->parseDirective($line, $parent);
  619. }
  620. elseif ($this->isFilter($line)) {
  621. return $this->parseFilter($line, $parent);
  622. }
  623. elseif ($this->isDoctype($line)) {
  624. return $this->parseDoctype($line, $parent);
  625. }
  626. else {
  627. return $this->parseContent($line, $parent);
  628. }
  629. }
  630. /**
  631. * Return a value indicating if the line has content.
  632. * @param array line
  633. * @return boolean true if the line has a content, false if not
  634. */
  635. private function hasContent($line) {
  636. return !empty($line[self::HAML_CONTENT]);
  637. }
  638. /**
  639. * Return a value indicating if the line is code to be run.
  640. * @param array line
  641. * @return boolean true if the line is code to be run, false if not
  642. */
  643. private function isCode($line) {
  644. return $line[self::HAML_TOKEN] === self::RUN_CODE;
  645. }
  646. /**
  647. * Return a value indicating if the line is a directive.
  648. * @param array line
  649. * @return boolean true if the line is a directive, false if not
  650. */
  651. private function isDirective($line) {
  652. return $line[self::HAML_TOKEN] === self::DIRECTIVE;
  653. }
  654. /**
  655. * Return a value indicating if the line is a doctype.
  656. * @param array line
  657. * @return boolean true if the line is a doctype, false if not
  658. */
  659. private function isDoctype($line) {
  660. return $line[self::HAML_TOKEN] === self::DOCTYPE;
  661. }
  662. /**
  663. * Return a value indicating if the line is an element.
  664. * Will set the tag to div if it is an implied div.
  665. * @param array line
  666. * @return boolean true if the line is an element, false if not
  667. */
  668. private function isElement(&$line) {
  669. if (empty($line[self::HAML_TAG]) && (
  670. !empty($line[self::HAML_CLASS]) ||
  671. !empty($line[self::HAML_ID]) ||
  672. !empty($line[self::HAML_XML_ATTRIBUTES]) ||
  673. !empty($line[self::HAML_RUBY_ATTRIBUTES]) ||
  674. !empty($line[self::HAML_OBJECT_REFERENCE])
  675. )) {
  676. $line[self::HAML_TAG] = 'div';
  677. }
  678. return !empty($line[self::HAML_TAG]);
  679. }
  680. /**
  681. * Return a value indicating if the line starts a filter.
  682. * @param array line to test
  683. * @return boolean true if the line starts a filter, false if not
  684. */
  685. private function isFilter($line) {
  686. return !empty($line[self::HAML_FILTER]);
  687. }
  688. /**
  689. * Return a value indicating if the line is a Haml comment.
  690. * @param array line to test
  691. * @return boolean true if the line is a Haml comment, false if not
  692. */
  693. private function isHamlComment($line) {
  694. return preg_match(self::HAML_COMMENT, $line[self::HAML_TOKEN]) > 0;
  695. }
  696. /**
  697. * Return a value indicating if the line is a HamlHelper.
  698. * @param array line to test
  699. * @return boolean true if the line is a HamlHelper, false if not
  700. */
  701. private function isHelper($line) {
  702. return (preg_match(HamlHelperNode::MATCH, $line[self::HAML_CONTENT], $matches)
  703. ? method_exists($this->helperClass, $matches[HamlHelperNode::NAME]) : false);
  704. }
  705. /**
  706. * Return a value indicating if the line is an XML comment.
  707. * @param array line to test
  708. * @return boolean true if theline is an XML comment, false if not
  709. */
  710. private function isXmlComment($line) {
  711. return $line[self::HAML_SOURCE][0] === self::XML_COMMENT;
  712. }
  713. /**
  714. * Returns a value indicating whether the line is part of a multilne group
  715. * @param array the line to test
  716. * @return boolean true if the line os part of a multiline group, false if not
  717. */
  718. private function isMultiline($line) {
  719. return substr($line[self::HAML_SOURCE], -2) === self::MULTILINE;
  720. }
  721. /**
  722. * Return a value indicating if the line's tag is a block level tag.
  723. * @param array line
  724. * @return boolean true if the line's tag is is a block level tag, false if not
  725. */
  726. private function isBlock($line) {
  727. return (!in_array($line[self::HAML_TAG], $this->inlineTags));
  728. }
  729. /**
  730. * Return a value indicating if the line's tag is self-closing.
  731. * @param array line
  732. * @return boolean true if the line's tag is self-closing, false if not
  733. */
  734. private function isSelfClosing($line) {
  735. return (in_array($line[self::HAML_TAG], $this->emptyTags) ||
  736. $line[self::HAML_TOKEN] == self::SELF_CLOSE_TAG);
  737. }
  738. /**
  739. * Gets a filter.
  740. * Filters are loaded on first use.
  741. * @param string filter name
  742. * @throws HamlException if the filter does not exist or does not extend HamlBaseFilter
  743. */
  744. private function getFilter($filter) {
  745. static $firstRun = true;
  746. $imported = false;
  747. if (empty($this->filters[$filter])) {
  748. if ($firstRun) {
  749. require_once('filters/HamlBaseFilter.php');
  750. $firstRun = false;
  751. }
  752. $filterclass = 'Haml' . ucfirst($filter) . 'Filter';
  753. if (isset($this->filterDir)) {
  754. $this->filterDir = (substr($this->filterDir, -1) == DIRECTORY_SEPARATOR?
  755. substr($this->filterDir, 0, -1):$this->filterDir);
  756. if (file_exists($this->filterDir.DIRECTORY_SEPARATOR."$filterclass.php")) {
  757. require_once($this->filterDir.DIRECTORY_SEPARATOR."$filterclass.php");
  758. $imported = true;
  759. }
  760. }
  761. if (!$imported && file_exists(dirname(__FILE__).DIRECTORY_SEPARATOR.'filters'.DIRECTORY_SEPARATOR."$filterclass.php")) {
  762. require_once("filters/$filterclass.php");
  763. $imported = true;
  764. }
  765. if (!$imported) {
  766. throw new HamlException('Unable to find {what}: {filename}', array('{what}'=>$filter.' filter', '{filename}'=>$filterclass.'.php'), $this);
  767. }
  768. $this->filters[$filter] = new $filterclass();
  769. if (!($this->filters[$filter] instanceof HamlBaseFilter)) {
  770. throw new HamlException('{what} must extend {base} class', array('{what}'=>$filter, '{base}'=>'HamlBaseFilter'), $this);
  771. }
  772. $this->filters[$filter]->init();
  773. }
  774. return $this->filters[$filter];
  775. }
  776. /**
  777. * Gets the next line.
  778. * @param array first line
  779. * @return array the next line
  780. */
  781. private function getMultiline($line) {
  782. do {
  783. $multiLine = array_shift($this->source);
  784. $line[self::HAML_CONTENT] .= substr($multiLine[self::HAML_SOURCE], 0, -2);
  785. } while(!empty($this->source) && $this->isMultiline($this->source[0]));
  786. return $line;
  787. }
  788. /**
  789. * Parse attributes.
  790. * @param array line to parse
  791. * @return array attributes in name=>value pairs
  792. */
  793. private function parseAttributes($line) {
  794. $attributes = array();
  795. if (!empty($line[self::HAML_OPEN_XML_ATTRIBUTES])) {
  796. if (empty($line[self::HAML_XML_ATTRIBUTES])) {
  797. $line[self::HAML_XML_ATTRIBUTES] = $line[self::HAML_CONTENT];
  798. unset($line[self::HAML_CONTENT]);
  799. do {
  800. $multiLine = array_shift($this->source);
  801. $line[self::HAML_XML_ATTRIBUTES] .= $multiLine[self::HAML_CONTENT];
  802. } while (substr($line[self::HAML_XML_ATTRIBUTES], -1) !==
  803. self::CLOSE_XML_ATTRIBUTES);
  804. }
  805. if (preg_match(self::HTML_ATTRS, $line[self::HAML_XML_ATTRIBUTES], $htmlAttrs)) {
  806. $line[self::HAML_XML_ATTRIBUTES] = preg_replace(self::HTML_ATTRS, '', $line[self::HAML_XML_ATTRIBUTES]);
  807. $attributes = array_merge($attributes, $this->htmlAttrs($htmlAttrs));
  808. }
  809. $attributes = array_merge(
  810. $attributes,
  811. $this->parseAttributeHash($line[self::HAML_XML_ATTRIBUTES])
  812. );
  813. }
  814. if (!empty($line[self::HAML_OPEN_RUBY_ATTRIBUTES])) {
  815. if (empty($line[self::HAML_RUBY_ATTRIBUTES])) {
  816. $line[self::HAML_RUBY_ATTRIBUTES] = $line[self::HAML_CONTENT];
  817. unset($line[self::HAML_CONTENT]);
  818. do {
  819. $multiLine = array_shift($this->source);
  820. $line[self::HAML_RUBY_ATTRIBUTES] .= $multiLine[self::HAML_CONTENT];
  821. } while (substr($line[self::HAML_RUBY_ATTRIBUTES], -1) !==
  822. self::CLOSE_RUBY_ATTRIBUTES);
  823. }
  824. if (preg_match(self::HTML_ATTRS, $line[self::HAML_RUBY_ATTRIBUTES], $htmlAttrs)) {
  825. $line[self::HAML_RUBY_ATTRIBUTES] = preg_replace(self::HTML_ATTRS, '', $line[self::HAML_RUBY_ATTRIBUTES]);
  826. $attributes = array_merge($attributes, $this->htmlAttrs($htmlAttrs));
  827. }
  828. $attributes = array_merge(
  829. $attributes,
  830. $this->parseAttributeHash($line[self::HAML_RUBY_ATTRIBUTES])
  831. );
  832. }
  833. if (!empty($line[self::HAML_OBJECT_REFERENCE])) {
  834. $objectRef = explode(',', preg_replace('/,\s*/', ',', $line[self::HAML_OBJECT_REFERENCE]));
  835. $prefix = (isset($objectRef[1]) ? $objectRef[1] . '_' : '');
  836. $class = "strtolower(str_replace(' ', '_', preg_replace('/(?<=\w)([ A-Z])/', '_\1', get_class(" . $objectRef[0] . '))))';
  837. $attributes['class'] = "<?php echo '$prefix' . $class; ?>";
  838. $attributes['id'] = "<?php echo '$prefix' . $class . '_' . {$objectRef[0]}->id; ?>";
  839. }
  840. else {
  841. if (!empty($line[self::HAML_CLASS])) {
  842. $classes = explode('.', $line[self::HAML_CLASS]);
  843. foreach ($classes as &$class) {
  844. if (preg_match(self::MATCH_INTERPOLATION, $class)) {
  845. $class = $this->interpolate($class);
  846. }
  847. } // foreach
  848. $attributes['class'] = join(' ', $classes) .
  849. (isset($attributes['class']) ? " {$attributes['class']}" : '');
  850. }
  851. if (!empty($line[self::HAML_ID])) {
  852. $attributes['id'] =
  853. (preg_match(self::MATCH_INTERPOLATION, $line[self::HAML_ID]) ?
  854. $this->interpolate($line[self::HAML_ID]) : $line[self::HAML_ID]) .
  855. (isset($attributes['id']) ? "_{$attributes['id']}" : '');
  856. }
  857. }
  858. ksort($attributes, SORT_STRING);
  859. return $attributes;
  860. }
  861. /**
  862. * Parse attributes.
  863. * @param string the attributes
  864. * @return array attributes in name=>value pairs
  865. */
  866. private function parseAttributeHash($subject) {
  867. $subject = substr($subject, 0, -1);
  868. $attributes = array();
  869. if (preg_match(self::REGEX_ATTRIBUTE_FUNCTION, $subject)) {
  870. $attributes[0] = "<?php echo $subject; ?>";
  871. return $attributes;
  872. }
  873. preg_match_all(self::REGEX_ATTRIBUTES, $subject, $attrs, PREG_SET_ORDER);
  874. foreach ($attrs as $attr) {
  875. if (!empty($attr[1])) { // HTML5 Custom Data Attributes
  876. $dataAttributes = $this->parseAttributeHash(substr($attr[2], 1));
  877. foreach ($dataAttributes as $key=>$value) {
  878. $attributes["data-$key"] = $value;
  879. } // foreach
  880. }
  881. elseif (!empty($attr[4])) {
  882. $values = array_map('trim', explode(',', $attr[4]));
  883. if ($attr[3] !== 'class' && $attr[3] !== 'id') {
  884. throw new HamlException('Attribute must be "class" or "id" with array value', array(), $this);
  885. }
  886. $attributes[$attr[3]] = '<?php echo ' . join(($attr[3] === 'id' ? ".'_'." : ".' '."), $values) . '; ?>';
  887. }
  888. elseif (!empty($attr[6])) {
  889. $attributes[$attr[3]] = $this->interpolate($attr[6]);
  890. }
  891. elseif ($attr[6] === '') {
  892. $attributes[$attr[3]] = $attr[6];
  893. }
  894. else {
  895. switch ($attr[7]) {
  896. case 'true':
  897. $attributes[$attr[3]] = $attr[3];
  898. break;
  899. case 'false':
  900. break;
  901. default:
  902. $attributes[$attr[3]] = "<?php echo {$attr[7]}; ?>";
  903. break;
  904. }
  905. }
  906. } // foreach
  907. return $attributes;
  908. }
  909. /**
  910. * Returns an array of attributes for the html element.
  911. * @param array arguments for HamlHelpers::html_attrs
  912. * @return array attributes for the html element
  913. */
  914. private function htmlAttrs($htmlAttrs) {
  915. if (empty($htmlAttrs[1]) && empty($htmlAttrs[2])) {
  916. return HamlHelpers::html_attrs();
  917. }
  918. else {
  919. $htmlAttrs[1] = substr($htmlAttrs[1], 1, -1);
  920. if (substr($htmlAttrs[1], -1) == ';') {
  921. $htmlAttrs[1] = eval("return {$htmlAttrs[1]}");
  922. }
  923. if (isset($htmlAttrs[2])) {
  924. return HamlHelpers::html_attrs($htmlAttrs[1], eval($htmlAttrs[2] . ';'));
  925. }
  926. else {
  927. return HamlHelpers::html_attrs($htmlAttrs[1]);
  928. }
  929. }
  930. }
  931. /**
  932. * Parse code
  933. * @param array line to parse
  934. * @param HamlNode parent node
  935. * @return HamlCodeBlockNode
  936. */
  937. private function parseCode($line, $parent) {
  938. if (preg_match('/^(if|foreach|for|switch|do|while)\b(.*)$/',
  939. $line[self::HAML_CONTENT], $block)) {
  940. if ($block[1] === 'do') {
  941. $node = new HamlCodeBlockNode('<?php do { ?>', $parent);
  942. $node->doWhile = 'while' . $block[2] . ';';
  943. }
  944. elseif ($block[1] === 'switch') {
  945. $node = new HamlCodeBlockNode("<?php {$line[self::HAML_CONTENT]} {", $parent);
  946. }
  947. else {
  948. $node = new HamlCodeBlockNode("<?php {$line[self::HAML_CONTENT]} { ?>", $parent);
  949. }
  950. }
  951. elseif (strpos($line[self::HAML_CONTENT], 'else') === 0) {
  952. $node = new HamlCodeBlockNode("<?php } {$line[self::HAML_CONTENT]} { ?>", null);
  953. $node->token = $line;
  954. $node->showOutput = $this->showOutput;
  955. $node->showSource = $this->showSource;
  956. $parent->getLastChild()->addElse($node);
  957. $this->addChildren($node, $line);
  958. $node = null;
  959. }
  960. elseif (strpos($line[self::HAML_CONTENT], 'case') === 0) {
  961. $node = new HamlNode(($parent->hasChildren() ? '<?php ' : '') .
  962. "{$line[self::HAML_CONTENT]}: ?>", $parent);
  963. }
  964. else {
  965. $node = new HamlNode("<?php {$line[self::HAML_CONTENT]}; ?>", $parent);
  966. }
  967. return $node;
  968. }
  969. /**
  970. * Parse content
  971. * @param array line to parse
  972. * @param HamlNode parent node
  973. * @return HamlNode
  974. */
  975. private function parseContent($line, $parent) {
  976. switch ($line[self::HAML_TOKEN]) {
  977. case self::INSERT_CODE:
  978. $content = ($this->suppressEval ? '' :
  979. '<?php echo ' . ($this->escapeHtml ?
  980. 'htmlentities(' . $line[self::HAML_CONTENT] . ')' :
  981. $line[self::HAML_CONTENT]) .
  982. "; ?>" .
  983. ($this->style == HamlRenderer::STYLE_EXPANDED ||
  984. $this->style == HamlRenderer::STYLE_NESTED ? "\n" : ''));
  985. break;
  986. case self::INSERT_CODE_PRESERVE_WHITESPACE:
  987. $content = ($this->suppressEval ? '' :
  988. '<?php echo str_replace("\n", \'&#x000a\', ' . ($this->escapeHtml ?
  989. 'htmlentities(' . $line[self::HAML_CONTENT] . ')' :
  990. $line[self::HAML_CONTENT]) .
  991. "; ?>" .
  992. ($this->style == HamlRenderer::STYLE_EXPANDED ||
  993. $this->style == HamlRenderer::STYLE_NESTED ? "\n" : ''));
  994. break;
  995. default:
  996. $content = $line[self::HAML_CONTENT];
  997. break;
  998. } // switch
  999. return new HamlNode($this->interpolate($content), $parent);
  1000. }
  1001. /**
  1002. * Parse a directive.
  1003. * Various options are set according to the directive
  1004. * @param array line to parse
  1005. * @return null
  1006. */
  1007. private function parseDirective($line) {
  1008. preg_match('/(\w+)(\+|-)?/', $line[self::HAML_CONTENT], $matches);
  1009. switch ($matches[1]) {
  1010. case 's':
  1011. $this->showSource = ($matches[2] == '+' ? true :
  1012. ($matches[2] == '-' ? false : $this->showSource));
  1013. break;
  1014. case 'o':
  1015. $this->showOutput = ($matches[2] == '+' ? true :
  1016. ($matches[2] == '-' ? false : $this->showOutput));
  1017. break;
  1018. case 'os':
  1019. case 'so':
  1020. $this->showSource = ($matches[2] == '+' ? true :
  1021. ($matches[2] == '-' ? false : $this->showSource));
  1022. $this->showOutput = ($matches[2] == '+' ? true :
  1023. ($matches[2] == '-' ? false : $this->showOutput));
  1024. break;
  1025. default:
  1026. if (!in_array($matches[1], $this->styles)) {
  1027. throw new HamlException('Invalid {what} ({value})', array('{what}'=>'directive', '{value}'=>self::DIRECTIVE.$matches[0]), $this);
  1028. }
  1029. $this->style = $matches[1];
  1030. break;
  1031. } // switch
  1032. }
  1033. /**
  1034. * Parse a doctype declaration
  1035. * @param array line to parse
  1036. * @param HamlNode parent node
  1037. * @return HamlDoctypeNode
  1038. */
  1039. private function parseDoctype($line, $parent) {
  1040. $content = explode(' ', $line[self::HAML_CONTENT]);
  1041. if (!empty($content)) {
  1042. if ($content[0] === self::IS_XML_PROLOG) {
  1043. $encoding = isset($content[1]) ? $content[1] : self::DEFAULT_XML_ENCODING;
  1044. $output = str_replace(self::XML_ENCODING, $encoding, self::XML_PROLOG);
  1045. }
  1046. elseif (empty($content[0])) {
  1047. $output = $this->doctypes[$this->format][0];
  1048. }
  1049. elseif (array_key_exists($content[0],
  1050. $this->doctypes[$this->format])) {
  1051. $output = $this->doctypes[$this->format][$content[0]];
  1052. }
  1053. elseif (!empty($this->doctype)) {
  1054. $output = $this->doctype;
  1055. }
  1056. else {
  1057. $_doctypes = array_keys($this->doctypes[$this->format]);
  1058. array_shift($_doctypes);
  1059. throw new HamlException('Invalid {what} ({value}); must be one of "{options}"', array('{what}'=>'doctype', '{value}'=>$content[0], '{options}'=>join(', ', $_doctypes).' or empty'), $this);
  1060. }
  1061. }
  1062. return new HamlDoctypeNode($output, $parent);
  1063. }
  1064. /**
  1065. * Parse a Haml comment.
  1066. * If the comment is an empty comment eat all child lines.
  1067. * @param array line to parse
  1068. */
  1069. private function parseHamlComment($line) {
  1070. if (!$this->hasContent($line)) {
  1071. while ($this->hasChild($line, true)) {
  1072. array_shift($this->source);
  1073. $this->line++;
  1074. }
  1075. }
  1076. }
  1077. /**
  1078. * Parse a HamlHelper.
  1079. * @param array line to parse
  1080. * @param HamlNode parent node
  1081. * @return HamlHelperNode
  1082. */
  1083. private function parseHelper($line, $parent) {
  1084. preg_match(HamlHelperNode::MATCH, $line[self::HAML_CONTENT], $matches);
  1085. $node = new HamlHelperNode($this->helperClass, $matches[HamlHelperNode::PRE], $matches[HamlHelperNode::NAME], $matches[HamlHelperNode::ARGS], $parent);
  1086. if (isset($matches[HamlHelperNode::BLOCK])) {
  1087. new HamlNode($matches[HamlHelperNode::BLOCK], $node);
  1088. }
  1089. return $node;
  1090. }
  1091. /**
  1092. * Parse an element.
  1093. * @param array line to parse
  1094. * @param HamlNode parent node
  1095. * @return HamlElementNode tag node and children
  1096. */
  1097. private function parseElement($line, $parent) {
  1098. $node = new HamlElementNode($line[self::HAML_TAG], $parent);
  1099. $node->isSelfClosing = $this->isSelfClosing($line);
  1100. $node->isBlock = $this->isBlock($line);
  1101. $node->attributes = $this->parseAttributes($line);
  1102. if ($this->hasContent($line)) {
  1103. $child = $this->parseContent($line, $node);
  1104. $child->showOutput = $this->showOutput;
  1105. $child->showSource = $this->showSource;
  1106. $child->token = array(
  1107. self::HAML_SOURCE => $line[self::HAML_SOURCE],
  1108. 'filename' => $line['filename'],
  1109. 'line' => $line['line'],
  1110. 'level' => ($line['level'] + 1)
  1111. );
  1112. }
  1113. $node->whitespaceControl = $this->parseWhitespaceControl($line);
  1114. return $node;
  1115. }
  1116. /**
  1117. * Parse a filter.
  1118. * @param array line to parse
  1119. * @param HamlNode parent node
  1120. * @return HamlNode filter node
  1121. */
  1122. private function parseFilter($line, $parent) {
  1123. $node = new HamlFilterNode($this->getFilter($line[self::HAML_FILTER]), $parent);
  1124. if ($this->hasContent($line)) {
  1125. $child = $this->parseContent($line);
  1126. $child->showOutput = $this->showOutput;
  1127. $child->showSource = $this->showSource;
  1128. $child->token = array(
  1129. 'level' => ($line['level'] + 1),
  1130. 'line' => $line['line']
  1131. );
  1132. }
  1133. return $node;
  1134. }
  1135. /**
  1136. * Parse an Xml comment.
  1137. * @param array line to parse
  1138. * @param HamlNode parent node
  1139. * @return HamlCommentNode
  1140. */
  1141. private function parseXmlComment($line, $parent) {
  1142. return new HamlCommentNode($line[self::HAML_CONTENT], $parent);
  1143. }
  1144. private function parseWhitespaceControl($line) {
  1145. $whitespaceControl = array('inner' => false, 'outer' => array('left' => false, 'right' => false));
  1146. if (!empty($line[self::HAML_WHITESPACE_REMOVAL])) {
  1147. $whitespaceControl['inner'] =
  1148. (strpos($line[self::HAML_WHITESPACE_REMOVAL],
  1149. self::INNER_WHITESPACE_REMOVAL) !== false);
  1150. if (strpos($line[self::HAML_WHITESPACE_REMOVAL],
  1151. self::OUTER_WHITESPACE_REMOVAL) !== false) {
  1152. $whitespaceControl['outer']['left'] =
  1153. (strpos($line[self::HAML_WHITESPACE_REMOVAL],
  1154. self::BLOCK_LEFT_OUTER_WHITESPACE_REMOVAL) === false);
  1155. $whitespaceControl['outer']['right'] =
  1156. (strpos($line[self::HAML_WHITESPACE_REMOVAL],
  1157. self::BLOCK_RIGHT_OUTER_WHITESPACE_REMOVAL) === false);
  1158. }
  1159. }
  1160. return $whitespaceControl;
  1161. }
  1162. /**
  1163. * Replace interpolated PHP contained in '#{}'.
  1164. * @param string the text to interpolate
  1165. * @return string the interpolated text
  1166. */
  1167. protected function interpolate($string) {
  1168. return preg_replace(self::MATCH_INTERPOLATION, self::INTERPOLATE, $string);
  1169. }
  1170. }