/lib/scssphp/Formatter.php

https://github.com/sbourget/moodle · PHP · 364 lines · 159 code · 59 blank · 146 comment · 19 complexity · 4a722bb7e44be94f7772d450e2e46d31 MD5 · raw file

  1. <?php
  2. /**
  3. * SCSSPHP
  4. *
  5. * @copyright 2012-2020 Leaf Corcoran
  6. *
  7. * @license http://opensource.org/licenses/MIT MIT
  8. *
  9. * @link http://scssphp.github.io/scssphp
  10. */
  11. namespace ScssPhp\ScssPhp;
  12. use ScssPhp\ScssPhp\Formatter\OutputBlock;
  13. use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
  14. /**
  15. * Base formatter
  16. *
  17. * @author Leaf Corcoran <leafot@gmail.com>
  18. *
  19. * @internal
  20. */
  21. abstract class Formatter
  22. {
  23. /**
  24. * @var int
  25. */
  26. public $indentLevel;
  27. /**
  28. * @var string
  29. */
  30. public $indentChar;
  31. /**
  32. * @var string
  33. */
  34. public $break;
  35. /**
  36. * @var string
  37. */
  38. public $open;
  39. /**
  40. * @var string
  41. */
  42. public $close;
  43. /**
  44. * @var string
  45. */
  46. public $tagSeparator;
  47. /**
  48. * @var string
  49. */
  50. public $assignSeparator;
  51. /**
  52. * @var bool
  53. */
  54. public $keepSemicolons;
  55. /**
  56. * @var \ScssPhp\ScssPhp\Formatter\OutputBlock
  57. */
  58. protected $currentBlock;
  59. /**
  60. * @var int
  61. */
  62. protected $currentLine;
  63. /**
  64. * @var int
  65. */
  66. protected $currentColumn;
  67. /**
  68. * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null
  69. */
  70. protected $sourceMapGenerator;
  71. /**
  72. * @var string
  73. */
  74. protected $strippedSemicolon;
  75. /**
  76. * Initialize formatter
  77. *
  78. * @api
  79. */
  80. abstract public function __construct();
  81. /**
  82. * Return indentation (whitespace)
  83. *
  84. * @return string
  85. */
  86. protected function indentStr()
  87. {
  88. return '';
  89. }
  90. /**
  91. * Return property assignment
  92. *
  93. * @api
  94. *
  95. * @param string $name
  96. * @param mixed $value
  97. *
  98. * @return string
  99. */
  100. public function property($name, $value)
  101. {
  102. return rtrim($name) . $this->assignSeparator . $value . ';';
  103. }
  104. /**
  105. * Return custom property assignment
  106. * differs in that you have to keep spaces in the value as is
  107. *
  108. * @api
  109. *
  110. * @param string $name
  111. * @param mixed $value
  112. *
  113. * @return string
  114. */
  115. public function customProperty($name, $value)
  116. {
  117. return rtrim($name) . trim($this->assignSeparator) . $value . ';';
  118. }
  119. /**
  120. * Output lines inside a block
  121. *
  122. * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
  123. *
  124. * @return void
  125. */
  126. protected function blockLines(OutputBlock $block)
  127. {
  128. $inner = $this->indentStr();
  129. $glue = $this->break . $inner;
  130. $this->write($inner . implode($glue, $block->lines));
  131. if (! empty($block->children)) {
  132. $this->write($this->break);
  133. }
  134. }
  135. /**
  136. * Output block selectors
  137. *
  138. * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
  139. *
  140. * @return void
  141. */
  142. protected function blockSelectors(OutputBlock $block)
  143. {
  144. assert(! empty($block->selectors));
  145. $inner = $this->indentStr();
  146. $this->write($inner
  147. . implode($this->tagSeparator, $block->selectors)
  148. . $this->open . $this->break);
  149. }
  150. /**
  151. * Output block children
  152. *
  153. * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
  154. *
  155. * @return void
  156. */
  157. protected function blockChildren(OutputBlock $block)
  158. {
  159. foreach ($block->children as $child) {
  160. $this->block($child);
  161. }
  162. }
  163. /**
  164. * Output non-empty block
  165. *
  166. * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
  167. *
  168. * @return void
  169. */
  170. protected function block(OutputBlock $block)
  171. {
  172. if (empty($block->lines) && empty($block->children)) {
  173. return;
  174. }
  175. $this->currentBlock = $block;
  176. $pre = $this->indentStr();
  177. if (! empty($block->selectors)) {
  178. $this->blockSelectors($block);
  179. $this->indentLevel++;
  180. }
  181. if (! empty($block->lines)) {
  182. $this->blockLines($block);
  183. }
  184. if (! empty($block->children)) {
  185. $this->blockChildren($block);
  186. }
  187. if (! empty($block->selectors)) {
  188. $this->indentLevel--;
  189. if (! $this->keepSemicolons) {
  190. $this->strippedSemicolon = '';
  191. }
  192. if (empty($block->children)) {
  193. $this->write($this->break);
  194. }
  195. $this->write($pre . $this->close . $this->break);
  196. }
  197. }
  198. /**
  199. * Test and clean safely empty children
  200. *
  201. * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
  202. *
  203. * @return bool
  204. */
  205. protected function testEmptyChildren($block)
  206. {
  207. $isEmpty = empty($block->lines);
  208. if ($block->children) {
  209. foreach ($block->children as $k => &$child) {
  210. if (! $this->testEmptyChildren($child)) {
  211. $isEmpty = false;
  212. continue;
  213. }
  214. if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) {
  215. $child->children = [];
  216. $child->selectors = null;
  217. }
  218. }
  219. }
  220. return $isEmpty;
  221. }
  222. /**
  223. * Entry point to formatting a block
  224. *
  225. * @api
  226. *
  227. * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree
  228. * @param \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null $sourceMapGenerator Optional source map generator
  229. *
  230. * @return string
  231. */
  232. public function format(OutputBlock $block, SourceMapGenerator $sourceMapGenerator = null)
  233. {
  234. $this->sourceMapGenerator = null;
  235. if ($sourceMapGenerator) {
  236. $this->currentLine = 1;
  237. $this->currentColumn = 0;
  238. $this->sourceMapGenerator = $sourceMapGenerator;
  239. }
  240. $this->testEmptyChildren($block);
  241. ob_start();
  242. $this->block($block);
  243. $out = ob_get_clean();
  244. return $out;
  245. }
  246. /**
  247. * Output content
  248. *
  249. * @param string $str
  250. *
  251. * @return void
  252. */
  253. protected function write($str)
  254. {
  255. if (! empty($this->strippedSemicolon)) {
  256. echo $this->strippedSemicolon;
  257. $this->strippedSemicolon = '';
  258. }
  259. /*
  260. * Maybe Strip semi-colon appended by property(); it's a separator, not a terminator
  261. * will be striped for real before a closing, otherwise displayed unchanged starting the next write
  262. */
  263. if (
  264. ! $this->keepSemicolons &&
  265. $str &&
  266. (strpos($str, ';') !== false) &&
  267. (substr($str, -1) === ';')
  268. ) {
  269. $str = substr($str, 0, -1);
  270. $this->strippedSemicolon = ';';
  271. }
  272. if ($this->sourceMapGenerator) {
  273. $lines = explode("\n", $str);
  274. $lastLine = array_pop($lines);
  275. foreach ($lines as $line) {
  276. // If the written line starts is empty, adding a mapping would add it for
  277. // a non-existent column as we are at the end of the line
  278. if ($line !== '') {
  279. $this->sourceMapGenerator->addMapping(
  280. $this->currentLine,
  281. $this->currentColumn,
  282. $this->currentBlock->sourceLine,
  283. //columns from parser are off by one
  284. $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
  285. $this->currentBlock->sourceName
  286. );
  287. }
  288. $this->currentLine++;
  289. $this->currentColumn = 0;
  290. }
  291. if ($lastLine !== '') {
  292. $this->sourceMapGenerator->addMapping(
  293. $this->currentLine,
  294. $this->currentColumn,
  295. $this->currentBlock->sourceLine,
  296. //columns from parser are off by one
  297. $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
  298. $this->currentBlock->sourceName
  299. );
  300. }
  301. $this->currentColumn += \strlen($lastLine);
  302. }
  303. echo $str;
  304. }
  305. }