Commit | Line | Data |
---|---|---|
fbe18cc0 FM |
1 | <?php |
2 | ||
3 | namespace Sabberworm\CSS; | |
4 | ||
5 | use Sabberworm\CSS\Parsing\OutputException; | |
6 | ||
376eb156 MM |
7 | /** |
8 | * Class OutputFormat | |
9 | * | |
10 | * @method OutputFormat setSemicolonAfterLastRule( bool $bSemicolonAfterLastRule ) Set whether semicolons are added after last rule. | |
11 | */ | |
fbe18cc0 FM |
12 | class OutputFormat { |
13 | /** | |
14 | * Value format | |
15 | */ | |
16 | // " means double-quote, ' means single-quote | |
17 | public $sStringQuotingType = '"'; | |
18 | // Output RGB colors in hash notation if possible | |
19 | public $bRGBHashNotation = true; | |
20 | ||
21 | /** | |
22 | * Declaration format | |
23 | */ | |
24 | // Semicolon after the last rule of a declaration block can be omitted. To do that, set this false. | |
25 | public $bSemicolonAfterLastRule = true; | |
26 | ||
27 | /** | |
28 | * Spacing | |
29 | * Note that these strings are not sanity-checked: the value should only consist of whitespace | |
30 | * Any newline character will be indented according to the current level. | |
31 | * The triples (After, Before, Between) can be set using a wildcard (e.g. `$oFormat->set('Space*Rules', "\n");`) | |
32 | */ | |
33 | public $sSpaceAfterRuleName = ' '; | |
34 | ||
35 | public $sSpaceBeforeRules = ''; | |
36 | public $sSpaceAfterRules = ''; | |
37 | public $sSpaceBetweenRules = ''; | |
38 | ||
39 | public $sSpaceBeforeBlocks = ''; | |
40 | public $sSpaceAfterBlocks = ''; | |
41 | public $sSpaceBetweenBlocks = "\n"; | |
42 | ||
376eb156 MM |
43 | // Content injected in and around @-rule blocks. |
44 | public $sBeforeAtRuleBlock = ''; | |
45 | public $sAfterAtRuleBlock = ''; | |
46 | ||
fbe18cc0 FM |
47 | // This is what’s printed before and after the comma if a declaration block contains multiple selectors. |
48 | public $sSpaceBeforeSelectorSeparator = ''; | |
49 | public $sSpaceAfterSelectorSeparator = ' '; | |
50 | // This is what’s printed after the comma of value lists | |
51 | public $sSpaceBeforeListArgumentSeparator = ''; | |
52 | public $sSpaceAfterListArgumentSeparator = ''; | |
53 | ||
54 | public $sSpaceBeforeOpeningBrace = ' '; | |
376eb156 MM |
55 | |
56 | // Content injected in and around declaration blocks. | |
57 | public $sBeforeDeclarationBlock = ''; | |
58 | public $sAfterDeclarationBlockSelectors = ''; | |
59 | public $sAfterDeclarationBlock = ''; | |
60 | ||
fbe18cc0 FM |
61 | /** |
62 | * Indentation | |
63 | */ | |
64 | // Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings. | |
65 | public $sIndentation = "\t"; | |
66 | ||
67 | /** | |
68 | * Output exceptions. | |
69 | */ | |
70 | public $bIgnoreExceptions = false; | |
71 | ||
72 | ||
73 | private $oFormatter = null; | |
74 | private $oNextLevelFormat = null; | |
75 | private $iIndentationLevel = 0; | |
76 | ||
77 | public function __construct() { | |
78 | } | |
79 | ||
80 | public function get($sName) { | |
81 | $aVarPrefixes = array('a', 's', 'm', 'b', 'f', 'o', 'c', 'i'); | |
82 | foreach($aVarPrefixes as $sPrefix) { | |
83 | $sFieldName = $sPrefix.ucfirst($sName); | |
84 | if(isset($this->$sFieldName)) { | |
85 | return $this->$sFieldName; | |
86 | } | |
87 | } | |
88 | return null; | |
89 | } | |
90 | ||
91 | public function set($aNames, $mValue) { | |
92 | $aVarPrefixes = array('a', 's', 'm', 'b', 'f', 'o', 'c', 'i'); | |
93 | if(is_string($aNames) && strpos($aNames, '*') !== false) { | |
94 | $aNames = array(str_replace('*', 'Before', $aNames), str_replace('*', 'Between', $aNames), str_replace('*', 'After', $aNames)); | |
95 | } else if(!is_array($aNames)) { | |
96 | $aNames = array($aNames); | |
97 | } | |
98 | foreach($aVarPrefixes as $sPrefix) { | |
99 | $bDidReplace = false; | |
100 | foreach($aNames as $sName) { | |
101 | $sFieldName = $sPrefix.ucfirst($sName); | |
102 | if(isset($this->$sFieldName)) { | |
103 | $this->$sFieldName = $mValue; | |
104 | $bDidReplace = true; | |
105 | } | |
106 | } | |
107 | if($bDidReplace) { | |
108 | return $this; | |
109 | } | |
110 | } | |
111 | // Break the chain so the user knows this option is invalid | |
112 | return false; | |
113 | } | |
114 | ||
115 | public function __call($sMethodName, $aArguments) { | |
116 | if(strpos($sMethodName, 'set') === 0) { | |
117 | return $this->set(substr($sMethodName, 3), $aArguments[0]); | |
118 | } else if(strpos($sMethodName, 'get') === 0) { | |
119 | return $this->get(substr($sMethodName, 3)); | |
120 | } else if(method_exists('\\Sabberworm\\CSS\\OutputFormatter', $sMethodName)) { | |
121 | return call_user_func_array(array($this->getFormatter(), $sMethodName), $aArguments); | |
122 | } else { | |
123 | throw new \Exception('Unknown OutputFormat method called: '.$sMethodName); | |
124 | } | |
125 | } | |
126 | ||
127 | public function indentWithTabs($iNumber = 1) { | |
128 | return $this->setIndentation(str_repeat("\t", $iNumber)); | |
129 | } | |
130 | ||
131 | public function indentWithSpaces($iNumber = 2) { | |
132 | return $this->setIndentation(str_repeat(" ", $iNumber)); | |
133 | } | |
134 | ||
135 | public function nextLevel() { | |
136 | if($this->oNextLevelFormat === null) { | |
137 | $this->oNextLevelFormat = clone $this; | |
138 | $this->oNextLevelFormat->iIndentationLevel++; | |
139 | $this->oNextLevelFormat->oFormatter = null; | |
140 | } | |
141 | return $this->oNextLevelFormat; | |
142 | } | |
143 | ||
144 | public function beLenient() { | |
145 | $this->bIgnoreExceptions = true; | |
146 | } | |
147 | ||
148 | public function getFormatter() { | |
149 | if($this->oFormatter === null) { | |
150 | $this->oFormatter = new OutputFormatter($this); | |
151 | } | |
152 | return $this->oFormatter; | |
153 | } | |
154 | ||
155 | public function level() { | |
156 | return $this->iIndentationLevel; | |
157 | } | |
376eb156 MM |
158 | |
159 | /** | |
160 | * Create format. | |
161 | * | |
162 | * @return OutputFormat Format. | |
163 | */ | |
fbe18cc0 FM |
164 | public static function create() { |
165 | return new OutputFormat(); | |
166 | } | |
376eb156 MM |
167 | |
168 | /** | |
169 | * Create compact format. | |
170 | * | |
171 | * @return OutputFormat Format. | |
172 | */ | |
fbe18cc0 | 173 | public static function createCompact() { |
376eb156 MM |
174 | $format = self::create(); |
175 | $format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator(''); | |
176 | return $format; | |
fbe18cc0 | 177 | } |
376eb156 MM |
178 | |
179 | /** | |
180 | * Create pretty format. | |
181 | * | |
182 | * @return OutputFormat Format. | |
183 | */ | |
fbe18cc0 | 184 | public static function createPretty() { |
376eb156 MM |
185 | $format = self::create(); |
186 | $format->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' ')); | |
187 | return $format; | |
fbe18cc0 FM |
188 | } |
189 | } | |
190 | ||
191 | class OutputFormatter { | |
192 | private $oFormat; | |
193 | ||
194 | public function __construct(OutputFormat $oFormat) { | |
195 | $this->oFormat = $oFormat; | |
196 | } | |
197 | ||
198 | public function space($sName, $sType = null) { | |
199 | $sSpaceString = $this->oFormat->get("Space$sName"); | |
200 | // If $sSpaceString is an array, we have multple values configured depending on the type of object the space applies to | |
201 | if(is_array($sSpaceString)) { | |
202 | if($sType !== null && isset($sSpaceString[$sType])) { | |
203 | $sSpaceString = $sSpaceString[$sType]; | |
204 | } else { | |
205 | $sSpaceString = reset($sSpaceString); | |
206 | } | |
207 | } | |
208 | return $this->prepareSpace($sSpaceString); | |
209 | } | |
210 | ||
211 | public function spaceAfterRuleName() { | |
212 | return $this->space('AfterRuleName'); | |
213 | } | |
214 | ||
215 | public function spaceBeforeRules() { | |
216 | return $this->space('BeforeRules'); | |
217 | } | |
218 | ||
219 | public function spaceAfterRules() { | |
220 | return $this->space('AfterRules'); | |
221 | } | |
222 | ||
223 | public function spaceBetweenRules() { | |
224 | return $this->space('BetweenRules'); | |
225 | } | |
226 | ||
227 | public function spaceBeforeBlocks() { | |
228 | return $this->space('BeforeBlocks'); | |
229 | } | |
230 | ||
231 | public function spaceAfterBlocks() { | |
232 | return $this->space('AfterBlocks'); | |
233 | } | |
234 | ||
235 | public function spaceBetweenBlocks() { | |
236 | return $this->space('BetweenBlocks'); | |
237 | } | |
238 | ||
239 | public function spaceBeforeSelectorSeparator() { | |
240 | return $this->space('BeforeSelectorSeparator'); | |
241 | } | |
242 | ||
243 | public function spaceAfterSelectorSeparator() { | |
244 | return $this->space('AfterSelectorSeparator'); | |
245 | } | |
246 | ||
247 | public function spaceBeforeListArgumentSeparator($sSeparator) { | |
248 | return $this->space('BeforeListArgumentSeparator', $sSeparator); | |
249 | } | |
250 | ||
251 | public function spaceAfterListArgumentSeparator($sSeparator) { | |
252 | return $this->space('AfterListArgumentSeparator', $sSeparator); | |
253 | } | |
254 | ||
255 | public function spaceBeforeOpeningBrace() { | |
256 | return $this->space('BeforeOpeningBrace'); | |
257 | } | |
258 | ||
259 | /** | |
260 | * Runs the given code, either swallowing or passing exceptions, depending on the bIgnoreExceptions setting. | |
261 | */ | |
262 | public function safely($cCode) { | |
263 | if($this->oFormat->get('IgnoreExceptions')) { | |
264 | // If output exceptions are ignored, run the code with exception guards | |
265 | try { | |
266 | return $cCode(); | |
267 | } catch (OutputException $e) { | |
268 | return null; | |
269 | } //Do nothing | |
270 | } else { | |
271 | // Run the code as-is | |
272 | return $cCode(); | |
273 | } | |
274 | } | |
275 | ||
276 | /** | |
277 | * Clone of the implode function but calls ->render with the current output format instead of __toString() | |
278 | */ | |
279 | public function implode($sSeparator, $aValues, $bIncreaseLevel = false) { | |
280 | $sResult = ''; | |
281 | $oFormat = $this->oFormat; | |
282 | if($bIncreaseLevel) { | |
283 | $oFormat = $oFormat->nextLevel(); | |
284 | } | |
285 | $bIsFirst = true; | |
286 | foreach($aValues as $mValue) { | |
287 | if($bIsFirst) { | |
288 | $bIsFirst = false; | |
289 | } else { | |
290 | $sResult .= $sSeparator; | |
291 | } | |
292 | if($mValue instanceof \Sabberworm\CSS\Renderable) { | |
293 | $sResult .= $mValue->render($oFormat); | |
294 | } else { | |
295 | $sResult .= $mValue; | |
296 | } | |
297 | } | |
298 | return $sResult; | |
299 | } | |
300 | ||
301 | public function removeLastSemicolon($sString) { | |
302 | if($this->oFormat->get('SemicolonAfterLastRule')) { | |
303 | return $sString; | |
304 | } | |
305 | $sString = explode(';', $sString); | |
306 | if(count($sString) < 2) { | |
307 | return $sString[0]; | |
308 | } | |
309 | $sLast = array_pop($sString); | |
310 | $sNextToLast = array_pop($sString); | |
311 | array_push($sString, $sNextToLast.$sLast); | |
312 | return implode(';', $sString); | |
313 | } | |
314 | ||
315 | private function prepareSpace($sSpaceString) { | |
316 | return str_replace("\n", "\n".$this->indent(), $sSpaceString); | |
317 | } | |
318 | ||
319 | private function indent() { | |
320 | return str_repeat($this->oFormat->sIndentation, $this->oFormat->level()); | |
321 | } | |
376eb156 | 322 | } |