1<?php
2
3/**
4 * toCSS Visitor
5 *
6 * @package Less
7 * @subpackage visitor
8 */
9class Less_Visitor_toCSS extends Less_VisitorReplacing {
10
11	private $charset;
12
13	public function __construct() {
14		parent::__construct();
15	}
16
17	/**
18	 * @param Less_Tree_Ruleset $root
19	 */
20	public function run( $root ) {
21		return $this->visitObj( $root );
22	}
23
24	public function visitRule( $ruleNode ) {
25		if ( $ruleNode->variable ) {
26			return array();
27		}
28		return $ruleNode;
29	}
30
31	public function visitMixinDefinition( $mixinNode ) {
32		// mixin definitions do not get eval'd - this means they keep state
33		// so we have to clear that state here so it isn't used if toCSS is called twice
34		$mixinNode->frames = array();
35		return array();
36	}
37
38	public function visitExtend() {
39		return array();
40	}
41
42	public function visitComment( $commentNode ) {
43		if ( $commentNode->isSilent() ) {
44			return array();
45		}
46		return $commentNode;
47	}
48
49	public function visitMedia( $mediaNode, &$visitDeeper ) {
50		$mediaNode->accept( $this );
51		$visitDeeper = false;
52
53		if ( !$mediaNode->rules ) {
54			return array();
55		}
56		return $mediaNode;
57	}
58
59	public function visitDirective( $directiveNode ) {
60		if ( isset( $directiveNode->currentFileInfo['reference'] ) && ( !property_exists( $directiveNode, 'isReferenced' ) || !$directiveNode->isReferenced ) ) {
61			return array();
62		}
63		if ( $directiveNode->name === '@charset' ) {
64			// Only output the debug info together with subsequent @charset definitions
65			// a comment (or @media statement) before the actual @charset directive would
66			// be considered illegal css as it has to be on the first line
67			if ( isset( $this->charset ) && $this->charset ) {
68
69				// if( $directiveNode->debugInfo ){
70				//	$comment = new Less_Tree_Comment('/* ' . str_replace("\n",'',$directiveNode->toCSS())." */\n");
71				//	$comment->debugInfo = $directiveNode->debugInfo;
72				//	return $this->visit($comment);
73				//}
74
75				return array();
76			}
77			$this->charset = true;
78		}
79		return $directiveNode;
80	}
81
82	public function checkPropertiesInRoot( $rulesetNode ) {
83		if ( !$rulesetNode->firstRoot ) {
84			return;
85		}
86
87		foreach ( $rulesetNode->rules as $ruleNode ) {
88			if ( $ruleNode instanceof Less_Tree_Rule && !$ruleNode->variable ) {
89				$msg = "properties must be inside selector blocks, they cannot be in the root. Index ".$ruleNode->index.( $ruleNode->currentFileInfo ? ( ' Filename: '.$ruleNode->currentFileInfo['filename'] ) : null );
90				throw new Less_Exception_Compiler( $msg );
91			}
92		}
93	}
94
95	public function visitRuleset( $rulesetNode, &$visitDeeper ) {
96		$visitDeeper = false;
97
98		$this->checkPropertiesInRoot( $rulesetNode );
99
100		if ( $rulesetNode->root ) {
101			return $this->visitRulesetRoot( $rulesetNode );
102		}
103
104		$rulesets = array();
105		$rulesetNode->paths = $this->visitRulesetPaths( $rulesetNode );
106
107		// Compile rules and rulesets
108		$nodeRuleCnt = $rulesetNode->rules ? count( $rulesetNode->rules ) : 0;
109		for ( $i = 0; $i < $nodeRuleCnt; ) {
110			$rule = $rulesetNode->rules[$i];
111
112			if ( property_exists( $rule, 'rules' ) ) {
113				// visit because we are moving them out from being a child
114				$rulesets[] = $this->visitObj( $rule );
115				array_splice( $rulesetNode->rules, $i, 1 );
116				$nodeRuleCnt--;
117				continue;
118			}
119			$i++;
120		}
121
122		// accept the visitor to remove rules and refactor itself
123		// then we can decide now whether we want it or not
124		if ( $nodeRuleCnt > 0 ) {
125			$rulesetNode->accept( $this );
126
127			if ( $rulesetNode->rules ) {
128
129				if ( count( $rulesetNode->rules ) > 1 ) {
130					$this->_mergeRules( $rulesetNode->rules );
131					$this->_removeDuplicateRules( $rulesetNode->rules );
132				}
133
134				// now decide whether we keep the ruleset
135				if ( $rulesetNode->paths ) {
136					// array_unshift($rulesets, $rulesetNode);
137					array_splice( $rulesets, 0, 0, array( $rulesetNode ) );
138				}
139			}
140
141		}
142
143		if ( count( $rulesets ) === 1 ) {
144			return $rulesets[0];
145		}
146		return $rulesets;
147	}
148
149	/**
150	 * Helper function for visitiRuleset
151	 *
152	 * return array|Less_Tree_Ruleset
153	 */
154	private function visitRulesetRoot( $rulesetNode ) {
155		$rulesetNode->accept( $this );
156		if ( $rulesetNode->firstRoot || $rulesetNode->rules ) {
157			return $rulesetNode;
158		}
159		return array();
160	}
161
162	/**
163	 * Helper function for visitRuleset()
164	 *
165	 * @return array
166	 */
167	private function visitRulesetPaths( $rulesetNode ) {
168		$paths = array();
169		foreach ( $rulesetNode->paths as $p ) {
170			if ( $p[0]->elements[0]->combinator === ' ' ) {
171				$p[0]->elements[0]->combinator = '';
172			}
173
174			foreach ( $p as $pi ) {
175				if ( $pi->getIsReferenced() && $pi->getIsOutput() ) {
176					$paths[] = $p;
177					break;
178				}
179			}
180		}
181
182		return $paths;
183	}
184
185	protected function _removeDuplicateRules( &$rules ) {
186		// remove duplicates
187		$ruleCache = array();
188		for ( $i = count( $rules ) - 1; $i >= 0; $i-- ) {
189			$rule = $rules[$i];
190			if ( $rule instanceof Less_Tree_Rule || $rule instanceof Less_Tree_NameValue ) {
191
192				if ( !isset( $ruleCache[$rule->name] ) ) {
193					$ruleCache[$rule->name] = $rule;
194				} else {
195					$ruleList =& $ruleCache[$rule->name];
196
197					if ( $ruleList instanceof Less_Tree_Rule || $ruleList instanceof Less_Tree_NameValue ) {
198						$ruleList = $ruleCache[$rule->name] = array( $ruleCache[$rule->name]->toCSS() );
199					}
200
201					$ruleCSS = $rule->toCSS();
202					if ( array_search( $ruleCSS, $ruleList ) !== false ) {
203						array_splice( $rules, $i, 1 );
204					} else {
205						$ruleList[] = $ruleCSS;
206					}
207				}
208			}
209		}
210	}
211
212	protected function _mergeRules( &$rules ) {
213		$groups = array();
214
215		// obj($rules);
216
217		$rules_len = count( $rules );
218		for ( $i = 0; $i < $rules_len; $i++ ) {
219			$rule = $rules[$i];
220
221			if ( ( $rule instanceof Less_Tree_Rule ) && $rule->merge ) {
222
223				$key = $rule->name;
224				if ( $rule->important ) {
225					$key .= ',!';
226				}
227
228				if ( !isset( $groups[$key] ) ) {
229					$groups[$key] = array();
230				} else {
231					array_splice( $rules, $i--, 1 );
232					$rules_len--;
233				}
234
235				$groups[$key][] = $rule;
236			}
237		}
238
239		foreach ( $groups as $parts ) {
240
241			if ( count( $parts ) > 1 ) {
242				$rule = $parts[0];
243				$spacedGroups = array();
244				$lastSpacedGroup = array();
245				$parts_mapped = array();
246				foreach ( $parts as $p ) {
247					if ( $p->merge === '+' ) {
248						if ( $lastSpacedGroup ) {
249							$spacedGroups[] = self::toExpression( $lastSpacedGroup );
250						}
251						$lastSpacedGroup = array();
252					}
253					$lastSpacedGroup[] = $p;
254				}
255
256				$spacedGroups[] = self::toExpression( $lastSpacedGroup );
257				$rule->value = self::toValue( $spacedGroups );
258			}
259		}
260
261	}
262
263	public static function toExpression( $values ) {
264		$mapped = array();
265		foreach ( $values as $p ) {
266			$mapped[] = $p->value;
267		}
268		return new Less_Tree_Expression( $mapped );
269	}
270
271	public static function toValue( $values ) {
272		// return new Less_Tree_Value($values); ??
273
274		$mapped = array();
275		foreach ( $values as $p ) {
276			$mapped[] = $p;
277		}
278		return new Less_Tree_Value( $mapped );
279	}
280}
281