SassParser.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  1. <?php
  2. /* SVN FILE: $Id: SassParser.php 118 2010-09-21 09:45:11Z chris.l.yates@gmail.com $ */
  3. /**
  4. * SassParser class file.
  5. * See the {@link http://sass-lang.com/docs Sass documentation}
  6. * for details of Sass.
  7. *
  8. * Credits:
  9. * This is a port of Sass to PHP. All the genius comes from the people that
  10. * invented and develop Sass; in particular:
  11. * + {@link http://hamptoncatlin.com/ Hampton Catlin},
  12. * + {@link http://nex-3.com/ Nathan Weizenbaum},
  13. * + {@link http://chriseppstein.github.com/ Chris Eppstein}
  14. *
  15. * The bugs are mine. Please report any found at {@link http://code.google.com/p/phamlp/issues/list}
  16. *
  17. * @author Chris Yates <chris.l.yates@gmail.com>
  18. * @copyright Copyright (c) 2010 PBM Web Development
  19. * @license http://phamlp.googlecode.com/files/license.txt
  20. * @package PHamlP
  21. * @subpackage Sass
  22. */
  23. require_once('SassFile.php');
  24. require_once('SassException.php');
  25. require_once('tree/SassNode.php');
  26. /**
  27. * SassParser class.
  28. * Parses {@link http://sass-lang.com/ .sass and .sccs} files.
  29. * @package PHamlP
  30. * @subpackage Sass
  31. */
  32. class SassParser {
  33. /**#@+
  34. * Default option values
  35. */
  36. const CACHE = true;
  37. const CACHE_LOCATION = './sass-cache';
  38. const CSS_LOCATION = './css';
  39. const TEMPLATE_LOCATION = './sass-templates';
  40. const BEGIN_COMMENT = '/';
  41. const BEGIN_CSS_COMMENT = '/*';
  42. const END_CSS_COMMENT = '*/';
  43. const BEGIN_SASS_COMMENT= '//';
  44. const BEGIN_INTERPOLATION = '#';
  45. const BEGIN_INTERPOLATION_BLOCK = '#{';
  46. const BEGIN_BLOCK = '{';
  47. const END_BLOCK = '}';
  48. const END_STATEMENT = ';';
  49. const DOUBLE_QUOTE = '"';
  50. const SINGLE_QUOTE = "'";
  51. /**
  52. * @var string the character used for indenting
  53. * @see indentChars
  54. * @see indentSpaces
  55. */
  56. private $indentChar;
  57. /**
  58. * @var array allowable characters for indenting
  59. */
  60. private $indentChars = array(' ', "\t");
  61. /**
  62. * @var integer number of spaces for indentation.
  63. * Used to calculate {@link Level} if {@link indentChar} is space.
  64. */
  65. private $indentSpaces = 2;
  66. /**
  67. * @var string source
  68. */
  69. private $source;
  70. /**#@+
  71. * Option
  72. */
  73. /**
  74. * cache:
  75. * @var boolean Whether parsed Sass files should be cached, allowing greater
  76. * speed.
  77. *
  78. * Defaults to true.
  79. */
  80. private $cache;
  81. /**
  82. * cache_location:
  83. * @var string The path where the cached sassc files should be written to.
  84. *
  85. * Defaults to './sass-cache'.
  86. */
  87. private $cache_location;
  88. /**
  89. * css_location:
  90. * @var string The path where CSS output should be written to.
  91. *
  92. * Defaults to './css'.
  93. */
  94. private $css_location;
  95. /**
  96. * debug_info:
  97. * @var boolean When true the line number and file where a selector is defined
  98. * is emitted into the compiled CSS in a format that can be understood by the
  99. * {@link https://addons.mozilla.org/en-US/firefox/addon/103988/
  100. * FireSass Firebug extension}.
  101. * Disabled when using the compressed output style.
  102. *
  103. * Defaults to false.
  104. * @see style
  105. */
  106. private $debug_info;
  107. /**
  108. * extensions:
  109. * @var array Sass extensions, e.g. Compass. An associative array of the form
  110. * $name => $options where $name is the name of the extension and $options
  111. * is an array of name=>value options pairs.
  112. */
  113. protected $extensions;
  114. /**
  115. * filename:
  116. * @var string The filename of the file being rendered.
  117. * This is used solely for reporting errors.
  118. */
  119. protected $filename;
  120. /**
  121. * function_paths:
  122. * @var array An array of filesystem paths which should be searched for
  123. * SassScript functions.
  124. */
  125. private $function_paths;
  126. /**
  127. * line:
  128. * @var integer The number of the first line of the Sass template. Used for
  129. * reporting line numbers for errors. This is useful to set if the Sass
  130. * template is embedded.
  131. *
  132. * Defaults to 1.
  133. */
  134. private $line;
  135. /**
  136. * line_numbers:
  137. * @var boolean When true the line number and filename where a selector is
  138. * defined is emitted into the compiled CSS as a comment. Useful for debugging
  139. * especially when using imports and mixins.
  140. * Disabled when using the compressed output style or the debug_info option.
  141. *
  142. * Defaults to false.
  143. * @see debug_info
  144. * @see style
  145. */
  146. private $line_numbers;
  147. /**
  148. * load_paths:
  149. * @var array An array of filesystem paths which should be searched for
  150. * Sass templates imported with the @import directive.
  151. *
  152. * Defaults to './sass-templates'.
  153. */
  154. private $load_paths;
  155. /**
  156. * property_syntax:
  157. * @var string Forces the document to use one syntax for
  158. * properties. If the correct syntax isn't used, an error is thrown.
  159. * Value can be:
  160. * + new - forces the use of a colon or equals sign after the property name.
  161. * For example color: #0f3 or width: $main_width.
  162. * + old - forces the use of a colon before the property name.
  163. * For example: :color #0f3 or :width = $main_width.
  164. *
  165. * By default, either syntax is valid.
  166. *
  167. * Ignored for SCSS files which alaways use the new style.
  168. */
  169. private $property_syntax;
  170. /**
  171. * quiet:
  172. * @var boolean When set to true, causes warnings to be disabled.
  173. * Defaults to false.
  174. */
  175. private $quiet;
  176. /**
  177. * style:
  178. * @var string the style of the CSS output.
  179. * Value can be:
  180. * + nested - Nested is the default Sass style, because it reflects the
  181. * structure of the document in much the same way Sass does. Each selector
  182. * and rule has its own line with indentation is based on how deeply the rule
  183. * is nested. Nested style is very useful when looking at large CSS files as
  184. * it allows you to very easily grasp the structure of the file without
  185. * actually reading anything.
  186. * + expanded - Expanded is the typical human-made CSS style, with each selector
  187. * and property taking up one line. Selectors are not indented; properties are
  188. * indented within the rules.
  189. * + compact - Each CSS rule takes up only one line, with every property defined
  190. * on that line. Nested rules are placed with each other while groups of rules
  191. * are separated by a blank line.
  192. * + compressed - Compressed has no whitespace except that necessary to separate
  193. * selectors and properties. It's not meant to be human-readable.
  194. *
  195. * Defaults to 'nested'.
  196. */
  197. private $style;
  198. /**
  199. * syntax:
  200. * @var string The syntax of the input file.
  201. * 'sass' for the indented syntax and 'scss' for the CSS-extension syntax.
  202. *
  203. * This is set automatically when parsing a file, else defaults to 'sass'.
  204. */
  205. private $syntax;
  206. /**
  207. * template_location:
  208. * @var string Path to the root sass template directory for your
  209. * application.
  210. */
  211. private $template_location;
  212. /**
  213. * vendor_properties:
  214. * If enabled a property need only be written in the standard form and vendor
  215. * specific versions will be added to the style sheet.
  216. * @var mixed array: vendor properties, merged with the built-in vendor
  217. * properties, to automatically apply.
  218. * Boolean true: use built in vendor properties.
  219. *
  220. * Defaults to vendor_properties disabled.
  221. * @see _vendorProperties
  222. */
  223. private $vendor_properties = array();
  224. /**#@-*/
  225. /**
  226. * Defines the build-in vendor properties
  227. * @var array built-in vendor properties
  228. * @see vendor_properties
  229. */
  230. private $_vendorProperties = array(
  231. 'border-radius' => array(
  232. '-moz-border-radius',
  233. '-webkit-border-radius',
  234. '-khtml-border-radius'
  235. ),
  236. 'border-top-right-radius' => array(
  237. '-moz-border-radius-topright',
  238. '-webkit-border-top-right-radius',
  239. '-khtml-border-top-right-radius'
  240. ),
  241. 'border-bottom-right-radius' => array(
  242. '-moz-border-radius-bottomright',
  243. '-webkit-border-bottom-right-radius',
  244. '-khtml-border-bottom-right-radius'
  245. ),
  246. 'border-bottom-left-radius' => array(
  247. '-moz-border-radius-bottomleft',
  248. '-webkit-border-bottom-left-radius',
  249. '-khtml-border-bottom-left-radius'
  250. ),
  251. 'border-top-left-radius' => array(
  252. '-moz-border-radius-topleft',
  253. '-webkit-border-top-left-radius',
  254. '-khtml-border-top-left-radius'
  255. ),
  256. 'box-shadow' => array('-moz-box-shadow', '-webkit-box-shadow'),
  257. 'box-sizing' => array('-moz-box-sizing', '-webkit-box-sizing'),
  258. 'opacity' => array('-moz-opacity', '-webkit-opacity', '-khtml-opacity'),
  259. );
  260. /**
  261. * Constructor.
  262. * Sets parser options
  263. * @param array $options
  264. * @return SassParser
  265. */
  266. public function __construct($options = array()) {
  267. if (!is_array($options)) {
  268. throw new SassException('{what} must be a {type}', array('{what}'=>'options', '{type}'=>'array'));
  269. }
  270. if (!empty($options['language'])) {
  271. Phamlp::$language = $options['language'];
  272. }
  273. if (!empty($options['extensions'])) {
  274. foreach ($options['extensions'] as $extension=>$extOptions) {
  275. include dirname(__FILE__).DIRECTORY_SEPARATOR.'extensions'.DIRECTORY_SEPARATOR.$extension.DIRECTORY_SEPARATOR.'config.php';
  276. $configClass = 'SassExtentions'.$extension.'Config';
  277. $config = new $configClass;
  278. $config->config($extOptions);
  279. $lp = dirname(__FILE__).DIRECTORY_SEPARATOR.'extensions'.DIRECTORY_SEPARATOR.$extension.DIRECTORY_SEPARATOR.'frameworks';
  280. $fp = dirname(__FILE__).DIRECTORY_SEPARATOR.'extensions'.DIRECTORY_SEPARATOR.$extension.DIRECTORY_SEPARATOR.'functions';
  281. $options['load_paths'] = (empty($options['load_paths']) ?
  282. array($lp) : array_merge($options['load_paths'], $lp));
  283. $options['function_paths'] = (empty($options['function_paths']) ?
  284. array($fp) : array_merge($options['function_paths'], $fp));
  285. }
  286. }
  287. if (!empty($options['vendor_properties'])) {
  288. if ($options['vendor_properties'] === true) {
  289. $this->vendor_properties = $this->_vendorProperties;
  290. }
  291. elseif (is_array($options['vendor_properties'])) {
  292. $this->vendor_properties = array_merge($this->vendor_properties, $this->_vendorProperties);
  293. }
  294. }
  295. unset($options['language'], $options['vendor_properties']);
  296. $defaultOptions = array(
  297. 'cache' => self::CACHE,
  298. 'cache_location' => dirname(__FILE__) . DIRECTORY_SEPARATOR . self::CACHE_LOCATION,
  299. 'css_location' => dirname(__FILE__) . DIRECTORY_SEPARATOR . self::CSS_LOCATION,
  300. 'debug_info' => false,
  301. 'filename' => array('dirname' => '', 'basename' => ''),
  302. 'function_paths' => array(),
  303. 'load_paths' => array(dirname(__FILE__) . DIRECTORY_SEPARATOR . self::TEMPLATE_LOCATION),
  304. 'line' => 1,
  305. 'line_numbers' => false,
  306. 'style' => SassRenderer::STYLE_NESTED,
  307. 'syntax' => SassFile::SASS
  308. );
  309. foreach (array_merge($defaultOptions, $options) as $name=>$value) {
  310. if (property_exists($this, $name)) {
  311. $this->$name = $value;
  312. }
  313. }
  314. }
  315. /**
  316. * Getter.
  317. * @param string name of property to get
  318. * @return mixed return value of getter function
  319. */
  320. public function __get($name) {
  321. $getter = 'get' . ucfirst($name);
  322. if (method_exists($this, $getter)) {
  323. return $this->$getter();
  324. }
  325. throw new SassException('No getter function for {what}', array('{what}'=>$name));
  326. }
  327. public function getCache() {
  328. return $this->cache;
  329. }
  330. public function getCache_location() {
  331. return $this->cache_location;
  332. }
  333. public function getCss_location() {
  334. return $this->css_location;
  335. }
  336. public function getDebug_info() {
  337. return $this->debug_info;
  338. }
  339. public function getFilename() {
  340. return $this->filename;
  341. }
  342. public function getLine() {
  343. return $this->line;
  344. }
  345. public function getSource() {
  346. return $this->source;
  347. }
  348. public function getLine_numbers() {
  349. return $this->line_numbers;
  350. }
  351. public function getFunction_paths() {
  352. return $this->function_paths;
  353. }
  354. public function getLoad_paths() {
  355. return $this->load_paths;
  356. }
  357. public function getProperty_syntax() {
  358. return $this->property_syntax;
  359. }
  360. public function getQuiet() {
  361. return $this->quiet;
  362. }
  363. public function getStyle() {
  364. return $this->style;
  365. }
  366. public function getSyntax() {
  367. return $this->syntax;
  368. }
  369. public function getTemplate_location() {
  370. return $this->template_location;
  371. }
  372. public function getVendor_properties() {
  373. return $this->vendor_properties;
  374. }
  375. public function getOptions() {
  376. return array(
  377. 'cache' => $this->cache,
  378. 'cache_location' => $this->cache_location,
  379. 'css_location' => $this->css_location,
  380. 'filename' => $this->filename,
  381. 'function_paths' => $this->function_paths,
  382. 'line' => $this->line,
  383. 'line_numbers' => $this->line_numbers,
  384. 'load_paths' => $this->load_paths,
  385. 'property_syntax' => $this->property_syntax,
  386. 'quiet' => $this->quiet,
  387. 'style' => $this->style,
  388. 'syntax' => $this->syntax,
  389. 'template_location' => $this->template_location,
  390. 'vendor_properties' => $this->vendor_properties
  391. );
  392. }
  393. /**
  394. * Parse a sass file or Sass source code and returns the CSS.
  395. * @param string name of source file or Sass source
  396. * @return string CSS
  397. */
  398. public function toCss($source, $isFile = true) {
  399. return $this->parse($source, $isFile)->render();
  400. }
  401. /**
  402. * Parse a sass file or Sass source code and
  403. * returns the document tree that can then be rendered.
  404. * The file will be searched for in the directories specified by the
  405. * load_paths option.
  406. * If caching is enabled a cached version will be used if possible or the
  407. * compiled version cached if not.
  408. * @param string name of source file or Sass source
  409. * @return SassRootNode Root node of document tree
  410. */
  411. public function parse($source, $isFile = true) {
  412. if ($isFile) {
  413. $this->filename = SassFile::getFile($source, $this);
  414. if ($isFile) {
  415. $this->syntax = substr($this->filename, -4);
  416. }
  417. elseif ($this->syntax !== SassFile::SASS && $this->syntax !== SassFile::SCSS) {
  418. throw new SassException('Invalid {what}', array('{what}'=>'syntax option'));
  419. }
  420. if ($this->cache) {
  421. $cached = SassFile::getCachedFile($this->filename, $this->cache_location);
  422. if ($cached !== false) {
  423. return $cached;
  424. }
  425. }
  426. $tree = $this->toTree(file_get_contents($this->filename));
  427. if ($this->cache) {
  428. SassFile::setCachedFile($tree, $this->filename, $this->cache_location);
  429. }
  430. return $tree;
  431. }
  432. else {
  433. return $this->toTree($source);
  434. }
  435. }
  436. /**
  437. * Parse Sass source into a document tree.
  438. * If the tree is already created return that.
  439. * @param string Sass source
  440. * @return SassRootNode the root of this document tree
  441. */
  442. private function toTree($source) {
  443. if ($this->syntax === SassFile::SASS) {
  444. $this->source = explode("\n", $source);
  445. $this->setIndentChar();
  446. }
  447. else {
  448. $this->source = $source;
  449. }
  450. unset($source);
  451. $root = new SassRootNode($this);
  452. $this->buildTree($root);
  453. return $root;
  454. }
  455. /**
  456. * Builds a parse tree under the parent node.
  457. * Called recursivly until the source is parsed.
  458. * @param SassNode the node
  459. */
  460. private function buildTree($parent) {
  461. $node = $this->getNode($parent);
  462. while (is_object($node) && $node->isChildOf($parent)) {
  463. $parent->addChild($node);
  464. $node = $this->buildTree($node);
  465. }
  466. return $node;
  467. }
  468. /**
  469. * Creates and returns the next SassNode.
  470. * The tpye of SassNode depends on the content of the SassToken.
  471. * @return SassNode a SassNode of the appropriate type. Null when no more
  472. * source to parse.
  473. */
  474. private function getNode($node) {
  475. $token = $this->getToken();
  476. if (empty($token)) return null;
  477. switch (true) {
  478. case SassDirectiveNode::isa($token):
  479. return $this->parseDirective($token, $node);
  480. break;
  481. case SassCommentNode::isa($token):
  482. return new SassCommentNode($token);
  483. break;
  484. case SassVariableNode::isa($token):
  485. return new SassVariableNode($token);
  486. break;
  487. case SassPropertyNode::isa($token, $this->property_syntax):
  488. return new SassPropertyNode($token, $this->property_syntax);
  489. break;
  490. case SassMixinDefinitionNode::isa($token):
  491. if ($this->syntax === SassFile::SCSS) {
  492. throw new SassException('Mixin {which} shortcut not allowed in SCSS', array('{which}'=>'definition'), $this);
  493. }
  494. return new SassMixinDefinitionNode($token);
  495. break;
  496. case SassMixinNode::isa($token):
  497. if ($this->syntax === SassFile::SCSS) {
  498. throw new SassException('Mixin {which} shortcut not allowed in SCSS', array('{which}'=>'include'), $this);
  499. }
  500. return new SassMixinNode($token);
  501. break;
  502. default:
  503. return new SassRuleNode($token);
  504. break;
  505. } // switch
  506. }
  507. /**
  508. * Returns a token object that contains the next source statement and
  509. * meta data about it.
  510. * @return object
  511. */
  512. private function getToken() {
  513. return ($this->syntax === SassFile::SASS ? $this->sass2Token() : $this->scss2Token());
  514. }
  515. /**
  516. * Returns an object that contains the next source statement and meta data
  517. * about it from SASS source.
  518. * Sass statements are passed over. Statements spanning multiple lines, e.g.
  519. * CSS comments and selectors, are assembled into a single statement.
  520. * @return object Statement token. Null if end of source.
  521. */
  522. private function sass2Token() {
  523. $statement = ''; // source line being tokenised
  524. $token = null;
  525. while (is_null($token) && !empty($this->source)) {
  526. while (empty($statement) && !empty($this->source)) {
  527. $source = array_shift($this->source);
  528. $statement = trim($source);
  529. $this->line++;
  530. }
  531. if (empty($statement)) {
  532. break;
  533. }
  534. $level = $this->getLevel($source);
  535. // Comment statements can span multiple lines
  536. if ($statement[0] === self::BEGIN_COMMENT) {
  537. // Consume Sass comments
  538. if (substr($statement, 0, strlen(self::BEGIN_SASS_COMMENT))
  539. === self::BEGIN_SASS_COMMENT) {
  540. unset($statement);
  541. while($this->getLevel($this->source[0]) > $level) {
  542. array_shift($this->source);
  543. $this->line++;
  544. }
  545. continue;
  546. }
  547. // Build CSS comments
  548. elseif (substr($statement, 0, strlen(self::BEGIN_CSS_COMMENT))
  549. === self::BEGIN_CSS_COMMENT) {
  550. while($this->getLevel($this->source[0]) > $level) {
  551. $statement .= "\n" . ltrim(array_shift($this->source));
  552. $this->line++;
  553. }
  554. }
  555. else {
  556. $this->source = $statement;
  557. throw new SassException('Illegal comment type', array(), $this);
  558. }
  559. }
  560. // Selector statements can span multiple lines
  561. elseif (substr($statement, -1) === SassRuleNode::CONTINUED) {
  562. // Build the selector statement
  563. while($this->getLevel($this->source[0]) === $level) {
  564. $statement .= ltrim(array_shift($this->source));
  565. $this->line++;
  566. }
  567. }
  568. $token = (object) array(
  569. 'source' => $statement,
  570. 'level' => $level,
  571. 'filename' => $this->filename,
  572. 'line' => $this->line - 1,
  573. );
  574. }
  575. return $token;
  576. }
  577. /**
  578. * Returns the level of the line.
  579. * Used for .sass source
  580. * @param string the source
  581. * @return integer the level of the source
  582. * @throws Exception if the source indentation is invalid
  583. */
  584. private function getLevel($source) {
  585. $indent = strlen($source) - strlen(ltrim($source));
  586. $level = $indent/$this->indentSpaces;
  587. if (!is_int($level) ||
  588. preg_match("/[^{$this->indentChar}]/", substr($source, 0, $indent))) {
  589. $this->source = $source;
  590. throw new SassException('Invalid indentation', array(), $this);
  591. }
  592. return $level;
  593. }
  594. /**
  595. * Returns an object that contains the next source statement and meta data
  596. * about it from SCSS source.
  597. * @return object Statement token. Null if end of source.
  598. */
  599. private function scss2Token() {
  600. static $srcpos = 0; // current position in the source stream
  601. static $srclen; // the length of the source stream
  602. $statement = '';
  603. $token = null;
  604. if (empty($srclen)) {
  605. $srclen = strlen($this->source);
  606. }
  607. while (is_null($token) && $srcpos < $srclen) {
  608. $c = $this->source[$srcpos++];
  609. switch ($c) {
  610. case self::BEGIN_COMMENT:
  611. if (substr($this->source, $srcpos-1, strlen(self::BEGIN_SASS_COMMENT))
  612. === self::BEGIN_SASS_COMMENT) {
  613. while ($this->source[$srcpos++] !== "\n");
  614. $statement .= "\n";
  615. }
  616. elseif (substr($this->source, $srcpos-1, strlen(self::BEGIN_CSS_COMMENT))
  617. === self::BEGIN_CSS_COMMENT) {
  618. if (ltrim($statement)) {
  619. throw new SassException('Invalid {what}', array('{what}'=>'comment'), (object) array(
  620. 'source' => $statement,
  621. 'filename' => $this->filename,
  622. 'line' => $this->line,
  623. ));
  624. }
  625. $statement .= $c.$this->source[$srcpos++];
  626. while (substr($this->source, $srcpos, strlen(self::END_CSS_COMMENT))
  627. !== self::END_CSS_COMMENT) {
  628. $statement .= $this->source[$srcpos++];
  629. }
  630. $srcpos += strlen(self::END_CSS_COMMENT);
  631. $token = $this->createToken($statement.self::END_CSS_COMMENT);
  632. }
  633. else {
  634. $statement .= $c;
  635. }
  636. break;
  637. case self::DOUBLE_QUOTE:
  638. case self::SINGLE_QUOTE:
  639. $statement .= $c;
  640. while ($this->source[$srcpos] !== $c) {
  641. $statement .= $this->source[$srcpos++];
  642. }
  643. $statement .= $this->source[$srcpos++];
  644. break;
  645. case self::BEGIN_INTERPOLATION:
  646. $statement .= $c;
  647. if (substr($this->source, $srcpos-1, strlen(self::BEGIN_INTERPOLATION_BLOCK))
  648. === self::BEGIN_INTERPOLATION_BLOCK) {
  649. while ($this->source[$srcpos] !== self::END_BLOCK) {
  650. $statement .= $this->source[$srcpos++];
  651. }
  652. $statement .= $this->source[$srcpos++];
  653. }
  654. break;
  655. case self::BEGIN_BLOCK:
  656. case self::END_BLOCK:
  657. case self::END_STATEMENT:
  658. $token = $this->createToken($statement . $c);
  659. if (is_null($token)) $statement = '';
  660. break;
  661. default:
  662. $statement .= $c;
  663. break;
  664. }
  665. }
  666. if (is_null($token))
  667. $srclen = $srcpos = 0;
  668. return $token;
  669. }
  670. /**
  671. * Returns an object that contains the source statement and meta data about
  672. * it.
  673. * If the statement is just and end block we update the meta data and return null.
  674. * @param string source statement
  675. * @return SassToken
  676. */
  677. private function createToken($statement) {
  678. static $level = 0;
  679. $this->line += substr_count($statement, "\n");
  680. $statement = trim($statement);
  681. if (substr($statement, 0, strlen(self::BEGIN_CSS_COMMENT)) !== self::BEGIN_CSS_COMMENT) {
  682. $statement = str_replace(array("\n","\r"), '', $statement);
  683. }
  684. $last = substr($statement, -1);
  685. // Trim the statement removing whitespace, end statement (;), begin block ({), and (unless the statement ends in an interpolation block) end block (})
  686. $statement = rtrim($statement, ' '.self::BEGIN_BLOCK.self::END_STATEMENT);
  687. $statement = (preg_match('/#\{.+?\}$/i', $statement) ? $statement : rtrim($statement, self::END_BLOCK));
  688. $token = ($statement ? (object) array(
  689. 'source' => $statement,
  690. 'level' => $level,
  691. 'filename' => $this->filename,
  692. 'line' => $this->line,
  693. ) : null);
  694. $level += ($last === self::BEGIN_BLOCK ? 1 : ($last === self::END_BLOCK ? -1 : 0));
  695. return $token;
  696. }
  697. /**
  698. * Parses a directive
  699. * @param SassToken token to parse
  700. * @param SassNode parent node
  701. * @return SassNode a Sass directive node
  702. */
  703. private function parseDirective($token, $parent) {
  704. switch (SassDirectiveNode::extractDirective($token)) {
  705. case '@extend':
  706. return new SassExtendNode($token);
  707. break;
  708. case '@mixin':
  709. return new SassMixinDefinitionNode($token);
  710. break;
  711. case '@include':
  712. return new SassMixinNode($token);
  713. break;
  714. case '@import':
  715. if ($this->syntax == SassFile::SASS) {
  716. $i = 0;
  717. $source = '';
  718. while (!empty($this->source) && empty($source)) {
  719. $source = $this->source[$i++];
  720. }
  721. if (!empty($source) && $this->getLevel($source) > $token->level) {
  722. throw new SassException('Nesting not allowed beneath {what}', array('{what}'=>'@import directive'), $token);
  723. }
  724. }
  725. return new SassImportNode($token);
  726. break;
  727. case '@for':
  728. return new SassForNode($token);
  729. break;
  730. case '@if':
  731. return new SassIfNode($token);
  732. break;
  733. case '@else': // handles else and else if directives
  734. return new SassElseNode($token);
  735. break;
  736. case '@do':
  737. case '@while':
  738. return new SassWhileNode($token);
  739. break;
  740. case '@debug':
  741. return new SassDebugNode($token);
  742. break;
  743. case '@warn':
  744. return new SassDebugNode($token, true);
  745. break;
  746. default:
  747. return new SassDirectiveNode($token);
  748. break;
  749. }
  750. }
  751. /**
  752. * Determine the indent character and indent spaces.
  753. * The first character of the first indented line determines the character.
  754. * If this is a space the number of spaces determines the indentSpaces; this
  755. * is always 1 if the indent character is a tab.
  756. * Only used for .sass files.
  757. * @throws SassException if the indent is mixed or
  758. * the indent character can not be determined
  759. */
  760. private function setIndentChar() {
  761. foreach ($this->source as $l=>$source) {
  762. if (!empty($source) && in_array($source[0], $this->indentChars)) {
  763. $this->indentChar = $source[0];
  764. for ($i = 0, $len = strlen($source); $i < $len && $source[$i] == $this->indentChar; $i++);
  765. if ($i < $len && in_array($source[$i], $this->indentChars)) {
  766. $this->line = ++$l;
  767. $this->source = $source;
  768. throw new SassException('Mixed indentation not allowed', array(), $this);
  769. }
  770. $this->indentSpaces = ($this->indentChar == ' ' ? $i : 1);
  771. return;
  772. }
  773. } // foreach
  774. $this->indentChar = ' ';
  775. $this->indentSpaces = 2;
  776. }
  777. }