22
33namespace Sabberworm \CSS \CSSList ;
44
5+ use Sabberworm \CSS \Comment \Commentable ;
6+ use Sabberworm \CSS \Parsing \ParserState ;
7+ use Sabberworm \CSS \Parsing \SourceException ;
8+ use Sabberworm \CSS \Parsing \UnexpectedTokenException ;
9+ use Sabberworm \CSS \Property \AtRule ;
10+ use Sabberworm \CSS \Property \Charset ;
11+ use Sabberworm \CSS \Property \CSSNamespace ;
12+ use Sabberworm \CSS \Property \Import ;
13+ use Sabberworm \CSS \Property \Selector ;
514use Sabberworm \CSS \Renderable ;
15+ use Sabberworm \CSS \RuleSet \AtRuleSet ;
616use Sabberworm \CSS \RuleSet \DeclarationBlock ;
717use Sabberworm \CSS \RuleSet \RuleSet ;
8- use Sabberworm \CSS \Property \Selector ;
9- use Sabberworm \CSS \Comment \Commentable ;
18+ use Sabberworm \CSS \Value \CSSString ;
19+ use Sabberworm \CSS \Value \URL ;
20+ use Sabberworm \CSS \Value \Value ;
1021
1122/**
1223 * A CSSList is the most generic container available. Its contents include RuleSet as well as other CSSList objects.
@@ -24,34 +35,187 @@ public function __construct($iLineNo = 0) {
2435 $ this ->iLineNo = $ iLineNo ;
2536 }
2637
38+ public static function parseList (ParserState $ oParserState , CSSList $ oList ) {
39+ $ bIsRoot = $ oList instanceof Document;
40+ if (is_string ($ oParserState )) {
41+ $ oParserState = new ParserState ($ oParserState );
42+ }
43+ $ bLenientParsing = $ oParserState ->getSettings ()->bLenientParsing ;
44+ while (!$ oParserState ->isEnd ()) {
45+ $ comments = $ oParserState ->consumeWhiteSpace ();
46+ $ oListItem = null ;
47+ if ($ bLenientParsing ) {
48+ try {
49+ $ oListItem = self ::parseListItem ($ oParserState , $ oList );
50+ } catch (UnexpectedTokenException $ e ) {
51+ $ oListItem = false ;
52+ }
53+ } else {
54+ $ oListItem = self ::parseListItem ($ oParserState , $ oList );
55+ }
56+ if ($ oListItem === null ) {
57+ // List parsing finished
58+ return ;
59+ }
60+ if ($ oListItem ) {
61+ $ oListItem ->setComments ($ comments );
62+ $ oList ->append ($ oListItem );
63+ }
64+ $ oParserState ->consumeWhiteSpace ();
65+ }
66+ if (!$ bIsRoot && !$ bLenientParsing ) {
67+ throw new SourceException ("Unexpected end of document " , $ oParserState ->currentLine ());
68+ }
69+ }
70+
71+ private static function parseListItem (ParserState $ oParserState , CSSList $ oList ) {
72+ $ bIsRoot = $ oList instanceof Document;
73+ if ($ oParserState ->comes ('@ ' )) {
74+ $ oAtRule = self ::parseAtRule ($ oParserState );
75+ if ($ oAtRule instanceof Charset) {
76+ if (!$ bIsRoot ) {
77+ throw new UnexpectedTokenException ('@charset may only occur in root document ' , '' , 'custom ' , $ oParserState ->currentLine ());
78+ }
79+ if (count ($ oList ->getContents ()) > 0 ) {
80+ throw new UnexpectedTokenException ('@charset must be the first parseable token in a document ' , '' , 'custom ' , $ oParserState ->currentLine ());
81+ }
82+ $ oParserState ->setCharset ($ oAtRule ->getCharset ()->getString ());
83+ }
84+ return $ oAtRule ;
85+ } else if ($ oParserState ->comes ('} ' )) {
86+ $ oParserState ->consume ('} ' );
87+ if ($ bIsRoot ) {
88+ if ($ oParserState ->getSettings ()->bLenientParsing ) {
89+ while ($ oParserState ->comes ('} ' )) $ oParserState ->consume ('} ' );
90+ return DeclarationBlock::parse ($ oParserState );
91+ } else {
92+ throw new SourceException ("Unopened { " , $ oParserState ->currentLine ());
93+ }
94+ } else {
95+ return null ;
96+ }
97+ } else {
98+ return DeclarationBlock::parse ($ oParserState );
99+ }
100+ }
101+
102+ private static function parseAtRule (ParserState $ oParserState ) {
103+ $ oParserState ->consume ('@ ' );
104+ $ sIdentifier = $ oParserState ->parseIdentifier ();
105+ $ iIdentifierLineNum = $ oParserState ->currentLine ();
106+ $ oParserState ->consumeWhiteSpace ();
107+ if ($ sIdentifier === 'import ' ) {
108+ $ oLocation = URL ::parse ($ oParserState );
109+ $ oParserState ->consumeWhiteSpace ();
110+ $ sMediaQuery = null ;
111+ if (!$ oParserState ->comes ('; ' )) {
112+ $ sMediaQuery = $ oParserState ->consumeUntil ('; ' );
113+ }
114+ $ oParserState ->consume ('; ' );
115+ return new Import ($ oLocation , $ sMediaQuery , $ iIdentifierLineNum );
116+ } else if ($ sIdentifier === 'charset ' ) {
117+ $ sCharset = CSSString::parse ($ oParserState );
118+ $ oParserState ->consumeWhiteSpace ();
119+ $ oParserState ->consume ('; ' );
120+ return new Charset ($ sCharset , $ iIdentifierLineNum );
121+ } else if (self ::identifierIs ($ sIdentifier , 'keyframes ' )) {
122+ $ oResult = new KeyFrame ($ iIdentifierLineNum );
123+ $ oResult ->setVendorKeyFrame ($ sIdentifier );
124+ $ oResult ->setAnimationName (trim ($ oParserState ->consumeUntil ('{ ' , false , true )));
125+ CSSList::parseList ($ oParserState , $ oResult );
126+ return $ oResult ;
127+ } else if ($ sIdentifier === 'namespace ' ) {
128+ $ sPrefix = null ;
129+ $ mUrl = Value::parsePrimitiveValue ($ oParserState );
130+ if (!$ oParserState ->comes ('; ' )) {
131+ $ sPrefix = $ mUrl ;
132+ $ mUrl = Value::parsePrimitiveValue ($ oParserState );
133+ }
134+ $ oParserState ->consume ('; ' );
135+ if ($ sPrefix !== null && !is_string ($ sPrefix )) {
136+ throw new UnexpectedTokenException ('Wrong namespace prefix ' , $ sPrefix , 'custom ' , $ iIdentifierLineNum );
137+ }
138+ if (!($ mUrl instanceof CSSString || $ mUrl instanceof URL )) {
139+ throw new UnexpectedTokenException ('Wrong namespace url of invalid type ' , $ mUrl , 'custom ' , $ iIdentifierLineNum );
140+ }
141+ return new CSSNamespace ($ mUrl , $ sPrefix , $ iIdentifierLineNum );
142+ } else {
143+ //Unknown other at rule (font-face or such)
144+ $ sArgs = trim ($ oParserState ->consumeUntil ('{ ' , false , true ));
145+ if (substr_count ($ sArgs , "( " ) != substr_count ($ sArgs , ") " )) {
146+ if ($ oParserState ->getSettings ()->bLenientParsing ) {
147+ return NULL ;
148+ } else {
149+ throw new SourceException ("Unmatched brace count in media query " , $ oParserState ->currentLine ());
150+ }
151+ }
152+ $ bUseRuleSet = true ;
153+ foreach (explode ('/ ' , AtRule::BLOCK_RULES ) as $ sBlockRuleName ) {
154+ if (self ::identifierIs ($ sIdentifier , $ sBlockRuleName )) {
155+ $ bUseRuleSet = false ;
156+ break ;
157+ }
158+ }
159+ if ($ bUseRuleSet ) {
160+ $ oAtRule = new AtRuleSet ($ sIdentifier , $ sArgs , $ iIdentifierLineNum );
161+ RuleSet::parseRuleSet ($ oParserState , $ oAtRule );
162+ } else {
163+ $ oAtRule = new AtRuleBlockList ($ sIdentifier , $ sArgs , $ iIdentifierLineNum );
164+ CSSList::parseList ($ oParserState , $ oAtRule );
165+ }
166+ return $ oAtRule ;
167+ }
168+ }
169+
170+ /**
171+ * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too.
172+ */
173+ private static function identifierIs ($ sIdentifier , $ sMatch ) {
174+ return (strcasecmp ($ sIdentifier , $ sMatch ) === 0 )
175+ ?: preg_match ("/^(- \\w+-)? $ sMatch$/i " , $ sIdentifier ) === 1 ;
176+ }
177+
178+
27179 /**
28180 * @return int
29181 */
30182 public function getLineNo () {
31183 return $ this ->iLineNo ;
32184 }
33185
186+ /**
187+ * Prepend item to list of contents.
188+ *
189+ * @param object $oItem Item.
190+ */
191+ public function prepend ($ oItem ) {
192+ array_unshift ($ this ->aContents , $ oItem );
193+ }
194+
195+ /**
196+ * Append item to list of contents.
197+ *
198+ * @param object $oItem Item.
199+ */
34200 public function append ($ oItem ) {
35201 $ this ->aContents [] = $ oItem ;
36202 }
37203
38204 /**
39- * Insert an item before its sibling .
205+ * Splice the list of contents .
40206 *
41- * @param mixed $oItem The item.
42- * @param mixed $oSibling The sibling.
207+ * @param int $iOffset Offset.
208+ * @param int $iLength Length. Optional.
209+ * @param RuleSet[] $mReplacement Replacement. Optional.
43210 */
44- public function insert ($ oItem , $ oSibling ) {
45- $ iIndex = array_search ($ oSibling , $ this ->aContents );
46- if ($ iIndex === false ) {
47- return $ this ->append ($ oItem );
48- }
49- array_splice ($ this ->aContents , $ iIndex , 0 , array ($ oItem ));
211+ public function splice ($ iOffset , $ iLength = null , $ mReplacement = null ) {
212+ array_splice ($ this ->aContents , $ iOffset , $ iLength , $ mReplacement );
50213 }
51214
52215 /**
53216 * Removes an item from the CSS list.
54217 * @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
218+ * @return bool Whether the item was removed.
55219 */
56220 public function remove ($ oItemToRemove ) {
57221 $ iKey = array_search ($ oItemToRemove , $ this ->aContents , true );
@@ -62,6 +226,19 @@ public function remove($oItemToRemove) {
62226 return false ;
63227 }
64228
229+ /**
230+ * Replaces an item from the CSS list.
231+ * @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
232+ */
233+ public function replace ($ oOldItem , $ oNewItem ) {
234+ $ iKey = array_search ($ oOldItem , $ this ->aContents , true );
235+ if ($ iKey !== false ) {
236+ array_splice ($ this ->aContents , $ iKey , 1 , $ oNewItem );
237+ return true ;
238+ }
239+ return false ;
240+ }
241+
65242 /**
66243 * Set the contents.
67244 * @param array $aContents Objects to set as content.
0 commit comments