<?php /* SVN FILE: $Id: HamlParser.php 117 2010-09-21 09:41:58Z chris.l.yates@gmail.com $ */ /**
* HamlParser class file. * HamlParser allows you to write view files in * {@link http://haml-lang.com/ Haml}. * * Please see the {@link http://haml-lang.com/docs/yardoc/file.Haml_REFERENCE.html#plain_text Haml documentation} for the syntax. * * Credits: * This is a port of Haml to PHP. All the genius comes from the people that * invented and develop Haml; in particular: * + {@link http://hamptoncatlin.com/ Hampton Catlin}, * + {@link http://nex-3.com/ Nathan Weizenbaum}, * + {@link http://chriseppstein.github.com/ Chris Eppstein} * * The bugs are mine. Please report any found at {@link http://code.google.com/p/phamlp/issues/list} * * Notes * <ul> * <li>Debug (addition)<ul> * <li>Source debug - adds comments to the output showing each source line above * the result - ?#s+ to turn on, ?#s- to turn off, ?#s! to toggle</li> * <li>Output debug - shows the output directly in the browser - ?#o+ to turn on, ?#o- to turn off, ?#o! to toggle</li> * <li>Control both at once - ?#so+ to turn on, ?#so- to turn off, ?#so! to toggle</li> * <li>Ugly mode can be controlled by the template</li> * <liUugly mode strips comments in the output by default</li> * <li>Ugly mode is turned off when in debug</li></ul></li> * <li>"-" command (notes)<ul> * <li>PHP does not require ending ";"</li> * <li>PHP control blocks are automatically bracketed</li> * <li>Switch Case statements do not end with ":" * <li>do-while control blocks are written as "do (expression)"</li></ul></li> * </ul> * Comes with filters that run "out of the box": * + <b>plain</b>: useful for large chunks of text to ensure Haml doesn't do anything. * + <b>escaped</b>: like plain but the output is (x)html escaped. * + <b>preserve</b>: like plain but preserves the whitespace. * + <b>cdata</b>: wraps the content in CDATA tags. * + <b>javascript</b>: wraps the content in <script> and CDATA tags. Useful for adding inline JavaScript. * + <b>css</b>: wraps the content in <style> and CDATA tags. Useful for adding inline CSS. * + <b>php</b>: wraps the content in <?php tags. The content is PHP code. * 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. * + <b>markdown</b>: Parses the filtered text with Markdown. * + <b>textile</b>: Parses the filtered text with Textile. * PHP can be used in all the filters (except php) by wrapping expressions in #(). * * @author Chris Yates <chris.l.yates@gmail.com> * @copyright Copyright (c) 2010 PBM Web Development * @license http://phamlp.googlecode.com/files/license.txt * @package PHamlP * @subpackage Haml */
require_once('tree/HamlNode.php'); require_once('HamlHelpers.php'); require_once('HamlException.php');
/**
* HamlParser class. * Parses {@link http://haml-lang.com/ Haml} view files. * @package PHamlP * @subpackage Haml */
class HamlParser {
/**#@+ * Debug modes */ const DEBUG_NONE = 0; const DEBUG_SHOW_SOURCE = 1; const DEBUG_SHOW_OUTPUT = 2; const DEBUG_SHOW_ALL = 3; /**#@-*/ /**#@+ * Regexes used to parse the document */ 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 const REGEX_ATTRIBUTES = '/:?(?:(data)\s*=>?\s*([({].*?[})]))|(\w+(?:[-:]\w*)*)\s*=>?\s*(?(?=\[)(?:\[(.+?)\])|(?(?=([\'"]))(?:[\'"](.*?)\5)|([^\s,]+)))/'; const REGEX_ATTRIBUTE_FUNCTION = '/^\$?[_a-zA-Z]\w*(?(?=->)(->[_a-zA-Z]\w*)+|(::[_a-zA-Z]\w*)?)\(.+\)$/'; // Matches functions and instantiated and static object methods const REGEX_WHITESPACE_REMOVAL = '/(.*?)\s+$/s'; const REGEX_WHITESPACE_REMOVAL_DEBUG = '%(.*?)(?:<br />\s)$%s'; // whitespace control when showing output //const REGEX_CODE_INTERPOLATION = '/(?:(?<!\\\\)#{(.+?(?:\(.*?\).*?)*)})/'; /**#@-*/ const MATCH_INTERPOLATION = '/(?<!\\\\)#\{(.*?)\}/'; const INTERPOLATE = '<?php echo \1; ?>'; const HTML_ATTRS = '/html_attrs\(\s*((?(?=\')(?:.*?)\'|(?:.*?)"))(?:\s*,\s*(.*?))?\)/'; /**#@+ * Haml regex match positions */ const HAML_HAML = 0; const HAML_INDENT = 1; const HAML_SOURCE = 2; const HAML_FILTER = 3; const HAML_TAG = 4; const HAML_CLASS = 5; const HAML_ID = 6; const HAML_OBJECT_REFERENCE = 7; const HAML_OPEN_XML_ATTRIBUTES = 8; const HAML_XML_ATTRIBUTES = 9; const HAML_OPEN_RUBY_ATTRIBUTES = 10; const HAML_RUBY_ATTRIBUTES = 11; const HAML_WHITESPACE_REMOVAL = 12; const HAML_TOKEN = 13; const HAML_CONTENT = 14; const HAML_MULTILINE = 15; /**#@-*/ /**#@+ * Haml tokens */ const DOCTYPE = '!!!'; const HAML_COMMENT = '!(-#|//)!'; const XML_COMMENT = '/'; const SELF_CLOSE_TAG = '/'; const ESCAPE_XML = '&='; const UNESCAPE_XML = '!='; const INSERT_CODE = '='; const INSERT_CODE_PRESERVE_WHITESPACE = '~'; const RUN_CODE = '-'; const INNER_WHITESPACE_REMOVAL = '<'; const OUTER_WHITESPACE_REMOVAL = '>'; const BLOCK_LEFT_OUTER_WHITESPACE_REMOVAL = '|>'; const BLOCK_RIGHT_OUTER_WHITESPACE_REMOVAL = '>|'; /**#@-*/ const MULTILINE= ' |'; /**#@+ * Attribute tokens */ const OPEN_XML_ATTRIBUTES = '('; const CLOSE_XML_ATTRIBUTES = ')'; const OPEN_RUBY_ATTRIBUTES = '{'; const CLOSE_RUBY_ATTRIBUTES = '}'; /**#@-*/ /**#@+ * Directives */ const DIRECTIVE = '?#'; const SOURCE_DEBUG = 's'; const OUTPUT_DEBUG = 'o'; /**#@-*/ const IS_XML_PROLOG = 'XML'; const XML_PROLOG = "<?php echo \"<?xml version='1.0' encoding='{encoding}' ?>\n\"; ?>"; const DEFAULT_XML_ENCODING = 'utf-8'; const XML_ENCODING = '{encoding}'; /** * @var string Doctype format. Determines how the Haml Doctype declaration is * rendered. * @see doctypes */ private $format = 'xhtml'; /** * @var string Custom Doctype. If not null and the Doctype declaration in the * Haml Document is not a built in Doctype this will be used as the Doctype. * This allows Haml to be used for non-(X)HTML documents that are XML compliant. * @see doctypes * @see emptyTags * @see inlineTags * @see minimizedAttributes */ private $doctype; /** * @var boolean whether or not to escape X(HT)ML-sensitive characters in script. * If this is true, = behaves like &=; otherwise, it behaves like !=. * Note that if this is set, != should be used for yielding to subtemplates * and rendering partials. Defaults to false. */ private $escapeHtml = false; /** * @var boolean Whether or not attribute hashes and scripts designated by * = or ~ should be evaluated. If true, the scripts are rendered as empty strings. * Defaults to false. */ private $suppressEval = false; /** * @var string The character that should wrap element attributes. Characters * of this type within attributes will be escaped (e.g. by replacing them with * ') if the character is an apostrophe or a quotation mark. * Defaults to " (an quotation mark). */ private $attrWrapper = '"'; /** * @var array available output styles: * nested: output is nested according to the indent level in the source * expanded: block tags have their own lines as does content which is indented * compact: block tags and their content go on one line * compressed: all unneccessary whitepaces is removed. If ugly is true this style is used. */ private $styles = array('nested', 'expanded', 'compact', 'compressed'); /** * @var string output style. Note: ugly must be false to allow style. */ private $style = 'nested'; /** * @var boolean if true no attempt is made to properly indent or format * the output. Reduces size of output file but is not very readable; * equivalent of style == compressed. Note: ugly must be false to allow style. * Defaults to true. * @see style */ private $ugly = true; /** * @var boolean if true comments are preserved in ugly mode. If not in * ugly mode comments are always output. Defaults to false. */ private $preserveComments = false; /** * @var integer Initial debug setting: * no debug, show source, show output, or show all. * Debug settings can be controlled in the template * Defaults to DEBUG_NONE. */ private $debug = self::DEBUG_NONE; /** * @var string Path to the directory containing user defined filters. If * specified this dirctory will be searched before PHamlP looks for the filter * in it's collection. This allows the default filters to be overridden and * new filters to be installed. Note: No trailing directory separator. */ private $filterDir; /** * @var string Path to the file containing user defined Haml helpers. */ private $helperFile; /** * @var string Haml helper class. This must be an instance of HamlHelpers. */ private $helperClass = 'HamlHelpers'; /** * @var array built in Doctypes * @see format * @see doctype */ private $doctypes = array ( 'html4' => array ( '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">', //HTML 4.01 Transitional 'Strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD 4.01 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">', //HTML 4.01 Strict 'Frameset' => '<!DOCTYPE html PUBLIC "-//W3C//DTD 4.01 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">', //HTML 4.01 Frameset ), 'html5' => array ( '<!DOCTYPE html>', // XHTML 5 ), 'xhtml' => array ( '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">', //XHTML 1.0 Transitional '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 '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 '5' => '<!DOCTYPE html>', // XHTML 5 '1.1' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">', // XHTML 1.1 '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 'Mobile' => '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">', //XHTML Mobile 1.2 ) ); /** * @var array A list of tag names that should be automatically self-closed * if they have no content. */ private $emptyTags = array('meta', 'img', 'link', 'br', 'hr', 'input', 'area', 'param', 'col', 'base'); /** * @var array A list of inline tags. */ private $inlineTags = array('a', 'abbr', 'accronym', 'b', 'big', 'cite', 'code', 'dfn', 'em', 'i', 'kbd', 'q', 'samp', 'small', 'span', 'strike', 'strong', 'tt', 'u', 'var'); /** * @var array attributes that are minimised */ private $minimizedAttributes = array('compact', 'checked', 'declare', 'readonly', 'disabled', 'selected', 'defer', 'ismap', 'nohref', 'noshade', 'nowrap', 'multiple', 'noresize'); /** * @var array A list of tag names that should automatically have their newlines preserved. */ private $preserve = array('pre', 'textarea'); /**#@-*/ /** * @var string the character used for indenting. Space or tab. * @see indentSpaces */ private $indentChar; /** * @var array allowable characters for indenting */ private $indentChars = array(' ', "\t"); /** * @var integer number of spaces for indentation. * Used on source if {@link indentChar} is space. * Used on output if {@link ugly} is false. */ private $indentSpaces; /** * @var array loaded filters */ private $filters = array(); /** * @var boolean whether line is in a filter */ private $inFilter = false; /** * @var boolean whether to show the output in the browser for debug */ private $showOutput; /** * @var boolean whether to show the source in the browser for debug */ private $showSource; /** * @var integer line number of source being parsed */ private $line; /** * @var string name of file being parsed */ private $filename; /** * @var mixed source */ private $source; /** * HamlParser constructor. * @param array options * @return HamlParser */ public function __construct($options = array()) { if (isset($options['language'])) { Phamlp::$language = $options['language']; unset($options['language']); } foreach ($options as $name => $value) { $this->$name = $value; } // foreach if ($this->ugly) { $this->style = 'compressed'; } $this->format = strtolower($this->format); if (is_null($this->doctype) && !array_key_exists($this->format, $this->doctypes)) { throw new HamlException('Invalid {what} ({value}). Must be one of "{options}"', array('{what}'=>'format', '{value}'=>$this->format, '{options}'=>join(', ', array_keys($this->doctypes))), $this); } $this->showSource = $this->debug & HamlParser::DEBUG_SHOW_SOURCE; $this->showOutput = $this->debug & HamlParser::DEBUG_SHOW_OUTPUT; require_once dirname(__FILE__).DIRECTORY_SEPARATOR.'HamlHelpers.php'; if (isset($this->helperFile)) { require_once $this->helperFile; $this->helperClass = basename($this->helperFile, ".php"); if (!is_subclass_of($this->helperClass, 'HamlHelpers')) { throw new HamlException('{what} must extend {base} class', array('{what}'=>$this->helperClass, '{base}'=>'HamlHelpers'), $this); } } } /** * Getter. * @param string name of property to get * @return mixed return value of getter function */ public function __get($name) { $getter = 'get' . ucfirst($name); if (method_exists($this, $getter)) { return $this->$getter(); } throw new HamlException('No getter function for {what}', array('{what}'=>$name)); } public function getFilename() { return $this->filename; } public function getLine() { return $this->line; } public function getSource() { return $this->source; } /** * Parses a Haml file. * If an output directory is given the resulting PHP is cached. * @param string path to file to parse * @param mixed boolean: true to use the default cache directory, false to use * the source file directory. string: path to the cache directory. * null: disable caching * @param string output file extension * @param integer permission for the output directory and file * @return mixed string: the resulting PHP if no output directory is specified * or the output filename if the output directory is specified. * boolean: false if the output file could not be written. */ public function parse($sourceFile, $cacheDir=null, $permission=0755, $sourceExtension='.haml', $outputExtension='.php') { if (is_string($cacheDir) || is_bool($cacheDir)) { if (is_bool($cacheDir)) { $cacheDir = ($cacheDir ? dirname(__FILE__).DIRECTORY_SEPARATOR.'haml-cache' : dirname($sourceFile)); } $outputFile = $cacheDir.DIRECTORY_SEPARATOR. basename($sourceFile, $sourceExtension).$outputExtension; if (@filemtime($sourceFile) > @filemtime($outputFile)) { if (!is_dir($cacheDir)) { @mkdir($cacheDir, $permission); } $return = (file_put_contents($outputFile, $this->haml2PHP($sourceFile)) === false ? false : $outputFile); if ($return !== false) { @chmod($outputFile, $permission); } } else { $return = $outputFile; } } else { $return = $this->haml2PHP($sourceFile); } return $return; } /** * Parses a Haml file into PHP. * @param string path to file to parse * @return string the resulting PHP */ public function haml2PHP($sourceFile) { $this->line = 0; $this->filename = $sourceFile; $helpers = "<?php "; //"<?php\nrequire_once '".dirname(__FILE__).DIRECTORY_SEPARATOR."HamlHelpers.php';\n"; if (isset($this->helperFile)) { $helpers .= "require_once '{$this->helperFile}';\n"; } $helpers .= "?>"; return $helpers . $this->toTree(file_get_contents($sourceFile))->render(); } /** * Parse Haml source into a document tree. * @param string Haml source * @return HamlRootNode the root of this document tree */ private function toTree($source) { $this->source = explode("\n", $source); $this->setIndentChar(); preg_match_all(self::REGEX_HAML, $source, $this->source, PREG_SET_ORDER); unset($source); $root = new HamlRootNode(array( 'format' => $this->format, 'style' => $this->style, 'attrWrapper' => $this->attrWrapper, 'minimizedAttributes' => $this->minimizedAttributes )); $this->buildTree($root); return $root; } /** * Builds a parse tree under the parent node. * @param HamlNode the parent node */ private function buildTree($parent) { while (!empty($this->source) && $this->isChildOf($parent, $this->source[0])) { $line = $this->getNextLine(); if (!empty($line)) { $node = ($this->inFilter ? new HamlNode($line[self::HAML_SOURCE], $parent) : $this->parseLine($line, $parent)); if (!empty($node)) { $node->token = $line; $node->showOutput = $this->showOutput; $node->showSource = $this->showSource; $this->addChildren($node, $line); } } } } /** * Adds children to a node if the current line has children. * @param HamlNode the node to add children to * @param array line to test */ private function addChildren($node, $line) { if ($node instanceof HamlFilterNode) { $this->inFilter = true; } if ($this->hasChild($line, $this->inFilter)) { $this->buildTree($node); if ($node instanceof HamlFilterNode) { $this->inFilter = false; } } } /** * Returns a value indicating if the next line is a child of the parent line * @param array parent line * @param boolean whether to all greater than the current indent * Used if the source line is a comment or a filter. * If true all indented lines are regarded as children; if not the child line * must only be indented by 1 or blank. Defaults to false. * @return boolean true if the next line is a child of the parent line * @throws Exception if the indent is invalid */ private function hasChild($line, $allowGreater = false) { if (!empty($this->source)) { $i = 0; $c = count($this->source); while (empty($nextLine[self::HAML_SOURCE]) && $i <= $c) { $nextLine = $this->source[$i++]; } $level = $this->getLevel($nextLine, $line['line'] + $i); if (($level == $line['level'] + 1) || ($allowGreater && $level > $line['level'])) { return true; } elseif ($level <= $line['level']) { return false; } else { throw new HamlException('Illegal indentation level ({level}); indentation level can only increase by one', array('{level}'=>$level), $this); } } else { return false; } } /** * Returns a value indicating if $line is a child of a node. * A blank line is a child of a node. * @param HamlNode the node * @param array the line to check * @return boolean true if the line is a child of the node, false if not */ private function isChildOf($node, $line) { $haml = trim($line[self::HAML_HAML]); return empty($haml) || $this->getLevel($line, $this->line) > $node->level; } /** * Determine the indent character and indent spaces. * The first character of the first indented line determines the character. * If this is a space the number of spaces determines the indentSpaces; this * is always 1 if the indent character is a tab. * @throws HamlException if the indent is mixed */ private function setIndentChar() { foreach ($this->source as $l=>$source) { if (!empty($source) && in_array($source[0], $this->indentChars)) { $this->indentChar = $source[0]; for ($i = 0, $len = strlen($source); $i < $len && $source[$i] == $this->indentChar; $i++); if ($i < $len && in_array($source[$i], $this->indentChars)) { $this->line = ++$l; $this->source = $source; throw new HamlException('Mixed indentation not allowed', array(), $this); } $this->indentSpaces = ($this->indentChar == ' ' ? $i : 1); return; } } // foreach $this->indentChar = ' '; $this->indentSpaces = 2; } /** * Gets the next line. * @param array remaining source lines * @return array the next line */ private function getNextLine() { $line = array_shift($this->source); // Blank lines ore OK $haml = trim($line[self::HAML_HAML]); if (empty($haml)) { $this->line++; return null; } // The regex will strip off a '<' at the start of a line if ($line[self::HAML_WHITESPACE_REMOVAL] === self::INNER_WHITESPACE_REMOVAL && empty($line[self::HAML_TAG])) { $line[self::HAML_CONTENT] = $line[self::HAML_WHITESPACE_REMOVAL].$line[self::HAML_TOKEN].$line[self::HAML_CONTENT]; } // The regex treats lines starting with [.+] as an object reference; they are just content if (!empty($line[self::HAML_OBJECT_REFERENCE]) && empty($line[self::HAML_TAG])) { unset($line[self::HAML_OBJECT_REFERENCE]); $line[self::HAML_CONTENT] = $line[self::HAML_SOURCE]; } $line['line'] = $this->line++; $line['level'] = $this->getLevel($line, $this->line); $line['filename'] = $this->filename; if ($this->isMultiline($line)) { $line = $this->getMultiline($line); } return $line; } /** * Returns the indent level of the line. * @param array the line * @param integer line number * @return integer the indent level of the line * @throws Exception if the indent level is invalid */ private function getLevel($line, $n) { if ($line[self::HAML_INDENT] && $this->indentChar === ' ') { $indent = strlen($line[self::HAML_INDENT]) / $this->indentSpaces; } else { $indent = strlen($line[self::HAML_INDENT]); } if (!is_integer($indent) || preg_match("/[^{$this->indentChar}]/", $line[self::HAML_INDENT])) { throw new HamlException('Invalid indentation', array(), $this); } return $indent; } /** * Parse a line of Haml into a HamlNode for the document tree * @param array line to parse * @param HamlNode parent node * @return HamlNode */ private function parseLine($line, $parent) { if ($this->isHamlComment($line)) { return $this->parseHamlComment($line); } elseif ($this->isXmlComment($line)) { return $this->parseXmlComment($line, $parent); } elseif ($this->isElement($line)) { return $this->parseElement($line, $parent); } elseif ($this->isHelper($line)) { return $this->parseHelper($line, $parent); } elseif ($this->isCode($line)) { return $this->parseCode($line, $parent); } elseif ($this->isDirective($line)) { return $this->parseDirective($line, $parent); } elseif ($this->isFilter($line)) { return $this->parseFilter($line, $parent); } elseif ($this->isDoctype($line)) { return $this->parseDoctype($line, $parent); } else { return $this->parseContent($line, $parent); } } /** * Return a value indicating if the line has content. * @param array line * @return boolean true if the line has a content, false if not */ private function hasContent($line) { return !empty($line[self::HAML_CONTENT]); } /** * Return a value indicating if the line is code to be run. * @param array line * @return boolean true if the line is code to be run, false if not */ private function isCode($line) { return $line[self::HAML_TOKEN] === self::RUN_CODE; } /** * Return a value indicating if the line is a directive. * @param array line * @return boolean true if the line is a directive, false if not */ private function isDirective($line) { return $line[self::HAML_TOKEN] === self::DIRECTIVE; } /** * Return a value indicating if the line is a doctype. * @param array line * @return boolean true if the line is a doctype, false if not */ private function isDoctype($line) { return $line[self::HAML_TOKEN] === self::DOCTYPE; } /** * Return a value indicating if the line is an element. * Will set the tag to div if it is an implied div. * @param array line * @return boolean true if the line is an element, false if not */ private function isElement(&$line) { if (empty($line[self::HAML_TAG]) && ( !empty($line[self::HAML_CLASS]) || !empty($line[self::HAML_ID]) || !empty($line[self::HAML_XML_ATTRIBUTES]) || !empty($line[self::HAML_RUBY_ATTRIBUTES]) || !empty($line[self::HAML_OBJECT_REFERENCE]) )) { $line[self::HAML_TAG] = 'div'; } return !empty($line[self::HAML_TAG]); } /** * Return a value indicating if the line starts a filter. * @param array line to test * @return boolean true if the line starts a filter, false if not */ private function isFilter($line) { return !empty($line[self::HAML_FILTER]); } /** * Return a value indicating if the line is a Haml comment. * @param array line to test * @return boolean true if the line is a Haml comment, false if not */ private function isHamlComment($line) { return preg_match(self::HAML_COMMENT, $line[self::HAML_TOKEN]) > 0; } /** * Return a value indicating if the line is a HamlHelper. * @param array line to test * @return boolean true if the line is a HamlHelper, false if not */ private function isHelper($line) { return (preg_match(HamlHelperNode::MATCH, $line[self::HAML_CONTENT], $matches) ? method_exists($this->helperClass, $matches[HamlHelperNode::NAME]) : false); } /** * Return a value indicating if the line is an XML comment. * @param array line to test * @return boolean true if theline is an XML comment, false if not */ private function isXmlComment($line) { return $line[self::HAML_SOURCE][0] === self::XML_COMMENT; } /** * Returns a value indicating whether the line is part of a multilne group * @param array the line to test * @return boolean true if the line os part of a multiline group, false if not */ private function isMultiline($line) { return substr($line[self::HAML_SOURCE], -2) === self::MULTILINE; } /** * Return a value indicating if the line's tag is a block level tag. * @param array line * @return boolean true if the line's tag is is a block level tag, false if not */ private function isBlock($line) { return (!in_array($line[self::HAML_TAG], $this->inlineTags)); } /** * Return a value indicating if the line's tag is self-closing. * @param array line * @return boolean true if the line's tag is self-closing, false if not */ private function isSelfClosing($line) { return (in_array($line[self::HAML_TAG], $this->emptyTags) || $line[self::HAML_TOKEN] == self::SELF_CLOSE_TAG); } /** * Gets a filter. * Filters are loaded on first use. * @param string filter name * @throws HamlException if the filter does not exist or does not extend HamlBaseFilter */ private function getFilter($filter) { static $firstRun = true; $imported = false; if (empty($this->filters[$filter])) { if ($firstRun) { require_once('filters/HamlBaseFilter.php'); $firstRun = false; } $filterclass = 'Haml' . ucfirst($filter) . 'Filter'; if (isset($this->filterDir)) { $this->filterDir = (substr($this->filterDir, -1) == DIRECTORY_SEPARATOR? substr($this->filterDir, 0, -1):$this->filterDir); if (file_exists($this->filterDir.DIRECTORY_SEPARATOR."$filterclass.php")) { require_once($this->filterDir.DIRECTORY_SEPARATOR."$filterclass.php"); $imported = true; } } if (!$imported && file_exists(dirname(__FILE__).DIRECTORY_SEPARATOR.'filters'.DIRECTORY_SEPARATOR."$filterclass.php")) { require_once("filters/$filterclass.php"); $imported = true; } if (!$imported) { throw new HamlException('Unable to find {what}: {filename}', array('{what}'=>$filter.' filter', '{filename}'=>$filterclass.'.php'), $this); } $this->filters[$filter] = new $filterclass(); if (!($this->filters[$filter] instanceof HamlBaseFilter)) { throw new HamlException('{what} must extend {base} class', array('{what}'=>$filter, '{base}'=>'HamlBaseFilter'), $this); } $this->filters[$filter]->init(); } return $this->filters[$filter]; } /** * Gets the next line. * @param array first line * @return array the next line */ private function getMultiline($line) { do { $multiLine = array_shift($this->source); $line[self::HAML_CONTENT] .= substr($multiLine[self::HAML_SOURCE], 0, -2); } while(!empty($this->source) && $this->isMultiline($this->source[0])); return $line; } /** * Parse attributes. * @param array line to parse * @return array attributes in name=>value pairs */ private function parseAttributes($line) { $attributes = array(); if (!empty($line[self::HAML_OPEN_XML_ATTRIBUTES])) { if (empty($line[self::HAML_XML_ATTRIBUTES])) { $line[self::HAML_XML_ATTRIBUTES] = $line[self::HAML_CONTENT]; unset($line[self::HAML_CONTENT]); do { $multiLine = array_shift($this->source); $line[self::HAML_XML_ATTRIBUTES] .= $multiLine[self::HAML_CONTENT]; } while (substr($line[self::HAML_XML_ATTRIBUTES], -1) !== self::CLOSE_XML_ATTRIBUTES); } if (preg_match(self::HTML_ATTRS, $line[self::HAML_XML_ATTRIBUTES], $htmlAttrs)) { $line[self::HAML_XML_ATTRIBUTES] = preg_replace(self::HTML_ATTRS, '', $line[self::HAML_XML_ATTRIBUTES]); $attributes = array_merge($attributes, $this->htmlAttrs($htmlAttrs)); } $attributes = array_merge( $attributes, $this->parseAttributeHash($line[self::HAML_XML_ATTRIBUTES]) ); } if (!empty($line[self::HAML_OPEN_RUBY_ATTRIBUTES])) { if (empty($line[self::HAML_RUBY_ATTRIBUTES])) { $line[self::HAML_RUBY_ATTRIBUTES] = $line[self::HAML_CONTENT]; unset($line[self::HAML_CONTENT]); do { $multiLine = array_shift($this->source); $line[self::HAML_RUBY_ATTRIBUTES] .= $multiLine[self::HAML_CONTENT]; } while (substr($line[self::HAML_RUBY_ATTRIBUTES], -1) !== self::CLOSE_RUBY_ATTRIBUTES); } if (preg_match(self::HTML_ATTRS, $line[self::HAML_RUBY_ATTRIBUTES], $htmlAttrs)) { $line[self::HAML_RUBY_ATTRIBUTES] = preg_replace(self::HTML_ATTRS, '', $line[self::HAML_RUBY_ATTRIBUTES]); $attributes = array_merge($attributes, $this->htmlAttrs($htmlAttrs)); } $attributes = array_merge( $attributes, $this->parseAttributeHash($line[self::HAML_RUBY_ATTRIBUTES]) ); } if (!empty($line[self::HAML_OBJECT_REFERENCE])) { $objectRef = explode(',', preg_replace('/,\s*/', ',', $line[self::HAML_OBJECT_REFERENCE])); $prefix = (isset($objectRef[1]) ? $objectRef[1] . '_' : ''); $class = "strtolower(str_replace(' ', '_', preg_replace('/(?<=\w)([ A-Z])/', '_\1', get_class(" . $objectRef[0] . '))))'; $attributes['class'] = "<?php echo '$prefix' . $class; ?>"; $attributes['id'] = "<?php echo '$prefix' . $class . '_' . {$objectRef[0]}->id; ?>"; } else { if (!empty($line[self::HAML_CLASS])) { $classes = explode('.', $line[self::HAML_CLASS]); foreach ($classes as &$class) { if (preg_match(self::MATCH_INTERPOLATION, $class)) { $class = $this->interpolate($class); } } // foreach $attributes['class'] = join(' ', $classes) . (isset($attributes['class']) ? " {$attributes['class']}" : ''); } if (!empty($line[self::HAML_ID])) { $attributes['id'] = (preg_match(self::MATCH_INTERPOLATION, $line[self::HAML_ID]) ? $this->interpolate($line[self::HAML_ID]) : $line[self::HAML_ID]) . (isset($attributes['id']) ? "_{$attributes['id']}" : ''); } } ksort($attributes, SORT_STRING); return $attributes; } /** * Parse attributes. * @param string the attributes * @return array attributes in name=>value pairs */ private function parseAttributeHash($subject) { $subject = substr($subject, 0, -1); $attributes = array(); if (preg_match(self::REGEX_ATTRIBUTE_FUNCTION, $subject)) { $attributes[0] = "<?php echo $subject; ?>"; return $attributes; } preg_match_all(self::REGEX_ATTRIBUTES, $subject, $attrs, PREG_SET_ORDER); foreach ($attrs as $attr) { if (!empty($attr[1])) { // HTML5 Custom Data Attributes $dataAttributes = $this->parseAttributeHash(substr($attr[2], 1)); foreach ($dataAttributes as $key=>$value) { $attributes["data-$key"] = $value; } // foreach } elseif (!empty($attr[4])) { $values = array_map('trim', explode(',', $attr[4])); if ($attr[3] !== 'class' && $attr[3] !== 'id') { throw new HamlException('Attribute must be "class" or "id" with array value', array(), $this); } $attributes[$attr[3]] = '<?php echo ' . join(($attr[3] === 'id' ? ".'_'." : ".' '."), $values) . '; ?>'; } elseif (!empty($attr[6])) { $attributes[$attr[3]] = $this->interpolate($attr[6]); } elseif ($attr[6] === '') { $attributes[$attr[3]] = $attr[6]; } else { switch ($attr[7]) { case 'true': $attributes[$attr[3]] = $attr[3]; break; case 'false': break; default: $attributes[$attr[3]] = "<?php echo {$attr[7]}; ?>"; break; } } } // foreach return $attributes; } /** * Returns an array of attributes for the html element. * @param array arguments for HamlHelpers::html_attrs * @return array attributes for the html element */ private function htmlAttrs($htmlAttrs) { if (empty($htmlAttrs[1]) && empty($htmlAttrs[2])) { return HamlHelpers::html_attrs(); } else { $htmlAttrs[1] = substr($htmlAttrs[1], 1, -1); if (substr($htmlAttrs[1], -1) == ';') { $htmlAttrs[1] = eval("return {$htmlAttrs[1]}"); } if (isset($htmlAttrs[2])) { return HamlHelpers::html_attrs($htmlAttrs[1], eval($htmlAttrs[2] . ';')); } else { return HamlHelpers::html_attrs($htmlAttrs[1]); } } } /** * Parse code * @param array line to parse * @param HamlNode parent node * @return HamlCodeBlockNode */ private function parseCode($line, $parent) { if (preg_match('/^(if|foreach|for|switch|do|while)\b(.*)$/', $line[self::HAML_CONTENT], $block)) { if ($block[1] === 'do') { $node = new HamlCodeBlockNode('<?php do { ?>', $parent); $node->doWhile = 'while' . $block[2] . ';'; } elseif ($block[1] === 'switch') { $node = new HamlCodeBlockNode("<?php {$line[self::HAML_CONTENT]} {", $parent); } else { $node = new HamlCodeBlockNode("<?php {$line[self::HAML_CONTENT]} { ?>", $parent); } } elseif (strpos($line[self::HAML_CONTENT], 'else') === 0) { $node = new HamlCodeBlockNode("<?php } {$line[self::HAML_CONTENT]} { ?>", null); $node->token = $line; $node->showOutput = $this->showOutput; $node->showSource = $this->showSource; $parent->getLastChild()->addElse($node); $this->addChildren($node, $line); $node = null; } elseif (strpos($line[self::HAML_CONTENT], 'case') === 0) { $node = new HamlNode(($parent->hasChildren() ? '<?php ' : '') . "{$line[self::HAML_CONTENT]}: ?>", $parent); } else { $node = new HamlNode("<?php {$line[self::HAML_CONTENT]}; ?>", $parent); } return $node; } /** * Parse content * @param array line to parse * @param HamlNode parent node * @return HamlNode */ private function parseContent($line, $parent) { switch ($line[self::HAML_TOKEN]) { case self::INSERT_CODE: $content = ($this->suppressEval ? '' : '<?php echo ' . ($this->escapeHtml ? 'htmlentities(' . $line[self::HAML_CONTENT] . ')' : $line[self::HAML_CONTENT]) . "; ?>" . ($this->style == HamlRenderer::STYLE_EXPANDED || $this->style == HamlRenderer::STYLE_NESTED ? "\n" : '')); break; case self::INSERT_CODE_PRESERVE_WHITESPACE: $content = ($this->suppressEval ? '' : '<?php echo str_replace("\n", \'
\', ' . ($this->escapeHtml ? 'htmlentities(' . $line[self::HAML_CONTENT] . ')' : $line[self::HAML_CONTENT]) . "; ?>" . ($this->style == HamlRenderer::STYLE_EXPANDED || $this->style == HamlRenderer::STYLE_NESTED ? "\n" : '')); break; default: $content = $line[self::HAML_CONTENT]; break; } // switch return new HamlNode($this->interpolate($content), $parent); } /** * Parse a directive. * Various options are set according to the directive * @param array line to parse * @return null */ private function parseDirective($line) { preg_match('/(\w+)(\+|-)?/', $line[self::HAML_CONTENT], $matches); switch ($matches[1]) { case 's': $this->showSource = ($matches[2] == '+' ? true : ($matches[2] == '-' ? false : $this->showSource)); break; case 'o': $this->showOutput = ($matches[2] == '+' ? true : ($matches[2] == '-' ? false : $this->showOutput)); break; case 'os': case 'so': $this->showSource = ($matches[2] == '+' ? true : ($matches[2] == '-' ? false : $this->showSource)); $this->showOutput = ($matches[2] == '+' ? true : ($matches[2] == '-' ? false : $this->showOutput)); break; default: if (!in_array($matches[1], $this->styles)) { throw new HamlException('Invalid {what} ({value})', array('{what}'=>'directive', '{value}'=>self::DIRECTIVE.$matches[0]), $this); } $this->style = $matches[1]; break; } // switch } /** * Parse a doctype declaration * @param array line to parse * @param HamlNode parent node * @return HamlDoctypeNode */ private function parseDoctype($line, $parent) { $content = explode(' ', $line[self::HAML_CONTENT]); if (!empty($content)) { if ($content[0] === self::IS_XML_PROLOG) { $encoding = isset($content[1]) ? $content[1] : self::DEFAULT_XML_ENCODING; $output = str_replace(self::XML_ENCODING, $encoding, self::XML_PROLOG); } elseif (empty($content[0])) { $output = $this->doctypes[$this->format][0]; } elseif (array_key_exists($content[0], $this->doctypes[$this->format])) { $output = $this->doctypes[$this->format][$content[0]]; } elseif (!empty($this->doctype)) { $output = $this->doctype; } else { $_doctypes = array_keys($this->doctypes[$this->format]); array_shift($_doctypes); throw new HamlException('Invalid {what} ({value}); must be one of "{options}"', array('{what}'=>'doctype', '{value}'=>$content[0], '{options}'=>join(', ', $_doctypes).' or empty'), $this); } } return new HamlDoctypeNode($output, $parent); } /** * Parse a Haml comment. * If the comment is an empty comment eat all child lines. * @param array line to parse */ private function parseHamlComment($line) { if (!$this->hasContent($line)) { while ($this->hasChild($line, true)) { array_shift($this->source); $this->line++; } } } /** * Parse a HamlHelper. * @param array line to parse * @param HamlNode parent node * @return HamlHelperNode */ private function parseHelper($line, $parent) { preg_match(HamlHelperNode::MATCH, $line[self::HAML_CONTENT], $matches); $node = new HamlHelperNode($this->helperClass, $matches[HamlHelperNode::PRE], $matches[HamlHelperNode::NAME], $matches[HamlHelperNode::ARGS], $parent); if (isset($matches[HamlHelperNode::BLOCK])) { new HamlNode($matches[HamlHelperNode::BLOCK], $node); } return $node; } /** * Parse an element. * @param array line to parse * @param HamlNode parent node * @return HamlElementNode tag node and children */ private function parseElement($line, $parent) { $node = new HamlElementNode($line[self::HAML_TAG], $parent); $node->isSelfClosing = $this->isSelfClosing($line); $node->isBlock = $this->isBlock($line); $node->attributes = $this->parseAttributes($line); if ($this->hasContent($line)) { $child = $this->parseContent($line, $node); $child->showOutput = $this->showOutput; $child->showSource = $this->showSource; $child->token = array( self::HAML_SOURCE => $line[self::HAML_SOURCE], 'filename' => $line['filename'], 'line' => $line['line'], 'level' => ($line['level'] + 1) ); } $node->whitespaceControl = $this->parseWhitespaceControl($line); return $node; } /** * Parse a filter. * @param array line to parse * @param HamlNode parent node * @return HamlNode filter node */ private function parseFilter($line, $parent) { $node = new HamlFilterNode($this->getFilter($line[self::HAML_FILTER]), $parent); if ($this->hasContent($line)) { $child = $this->parseContent($line); $child->showOutput = $this->showOutput; $child->showSource = $this->showSource; $child->token = array( 'level' => ($line['level'] + 1), 'line' => $line['line'] ); } return $node; } /** * Parse an Xml comment. * @param array line to parse * @param HamlNode parent node * @return HamlCommentNode */ private function parseXmlComment($line, $parent) { return new HamlCommentNode($line[self::HAML_CONTENT], $parent); } private function parseWhitespaceControl($line) { $whitespaceControl = array('inner' => false, 'outer' => array('left' => false, 'right' => false)); if (!empty($line[self::HAML_WHITESPACE_REMOVAL])) { $whitespaceControl['inner'] = (strpos($line[self::HAML_WHITESPACE_REMOVAL], self::INNER_WHITESPACE_REMOVAL) !== false); if (strpos($line[self::HAML_WHITESPACE_REMOVAL], self::OUTER_WHITESPACE_REMOVAL) !== false) { $whitespaceControl['outer']['left'] = (strpos($line[self::HAML_WHITESPACE_REMOVAL], self::BLOCK_LEFT_OUTER_WHITESPACE_REMOVAL) === false); $whitespaceControl['outer']['right'] = (strpos($line[self::HAML_WHITESPACE_REMOVAL], self::BLOCK_RIGHT_OUTER_WHITESPACE_REMOVAL) === false); } } return $whitespaceControl; } /** * Replace interpolated PHP contained in '#{}'. * @param string the text to interpolate * @return string the interpolated text */ protected function interpolate($string) { return preg_replace(self::MATCH_INTERPOLATION, self::INTERPOLATE, $string); }
}