1 // Scintilla source code edit control
3 ** Lexer for Cascading Style Sheets
4 ** Written by Jakub Vrána
5 ** Improved by Philippe Lhoste (CSS2)
6 ** Improved by Ross McKay (SCSS mode; see http://sass-lang.com/ )
8 // Copyright 1998-2002 by Neil Hodgson <neilh@scintilla.org>
9 // The License.txt file describes the conditions under which this software may be distributed.
11 // TODO: handle SCSS nested properties like font: { weight: bold; size: 1em; }
12 // TODO: handle SCSS interpolation: #{}
13 // TODO: add features for Less if somebody feels like contributing; http://lesscss.org/
14 // TODO: refactor this monster so that the next poor slob can read it!
24 #include "Scintilla.h"
28 #include "LexAccessor.h"
30 #include "StyleContext.h"
31 #include "CharacterSet.h"
32 #include "LexerModule.h"
35 using namespace Scintilla
;
39 static inline bool IsAWordChar(const unsigned int ch
) {
41 * The CSS spec allows "ISO 10646 characters U+00A1 and higher" to be treated as word chars.
42 * Unfortunately, we are only getting string bytes here, and not full unicode characters. We cannot guarantee
43 * that our byte is between U+0080 - U+00A0 (to return false), so we have to allow all characters U+0080 and higher
45 return ch
>= 0x80 || isalnum(ch
) || ch
== '-' || ch
== '_';
48 inline bool IsCssOperator(const int ch
) {
49 if (!((ch
< 0x80) && isalnum(ch
)) &&
50 (ch
== '{' || ch
== '}' || ch
== ':' || ch
== ',' || ch
== ';' ||
51 ch
== '.' || ch
== '#' || ch
== '!' || ch
== '@' ||
53 ch
== '*' || ch
== '>' || ch
== '+' || ch
== '=' || ch
== '~' || ch
== '|' ||
54 ch
== '[' || ch
== ']' || ch
== '(' || ch
== ')')) {
60 // look behind (from start of document to our start position) to determine current nesting level
61 inline int NestingLevelLookBehind(unsigned int startPos
, Accessor
&styler
) {
65 for (unsigned int i
= 0; i
< startPos
; i
++) {
66 ch
= styler
.SafeGetCharAt(i
);
76 static void ColouriseCssDoc(unsigned int startPos
, int length
, int initStyle
, WordList
*keywordlists
[], Accessor
&styler
) {
77 WordList
&css1Props
= *keywordlists
[0];
78 WordList
&pseudoClasses
= *keywordlists
[1];
79 WordList
&css2Props
= *keywordlists
[2];
80 WordList
&css3Props
= *keywordlists
[3];
81 WordList
&pseudoElements
= *keywordlists
[4];
82 WordList
&exProps
= *keywordlists
[5];
83 WordList
&exPseudoClasses
= *keywordlists
[6];
84 WordList
&exPseudoElements
= *keywordlists
[7];
86 StyleContext
sc(startPos
, length
, initStyle
, styler
);
88 int lastState
= -1; // before operator
89 int lastStateC
= -1; // before comment
90 int lastStateS
= -1; // before single-quoted/double-quoted string
91 int lastStateVar
= -1; // before variable (SCSS)
92 int lastStateVal
= -1; // before value (SCSS)
93 int op
= ' '; // last operator
94 int opPrev
= ' '; // last operator
95 bool insideParentheses
= false; // true if currently in a CSS url() or similar construct
97 // property lexer.css.scss.language
98 // Set to 1 for Sassy CSS (.scss)
99 bool isScssDocument
= styler
.GetPropertyInt("lexer.css.scss.language") != 0;
101 // property lexer.css.less.language
102 // Set to 1 for Less CSS (.less)
103 bool isLessDocument
= styler
.GetPropertyInt("lexer.css.less.language") != 0;
105 // property lexer.css.hss.language
106 // Set to 1 for HSS (.hss)
107 bool isHssDocument
= styler
.GetPropertyInt("lexer.css.hss.language") != 0;
109 // SCSS/LESS/HSS have the concept of variable
110 bool hasVariables
= isScssDocument
|| isLessDocument
|| isHssDocument
;
113 varPrefix
= isLessDocument
? '@' : '$';
115 // SCSS/LESS/HSS support single-line comments
116 typedef enum _CommentModes
{ eCommentBlock
= 0, eCommentLine
= 1} CommentMode
;
117 CommentMode comment_mode
= eCommentBlock
;
118 bool hasSingleLineComments
= isScssDocument
|| isLessDocument
|| isHssDocument
;
120 // must keep track of nesting level in document types that support it (SCSS/LESS/HSS)
121 bool hasNesting
= false;
122 int nestingLevel
= 0;
123 if (isScssDocument
|| isLessDocument
|| isHssDocument
) {
125 nestingLevel
= NestingLevelLookBehind(startPos
, styler
);
129 for (; sc
.More(); sc
.Forward()) {
130 if (sc
.state
== SCE_CSS_COMMENT
&& ((comment_mode
== eCommentBlock
&& sc
.Match('*', '/')) || (comment_mode
== eCommentLine
&& sc
.atLineEnd
))) {
131 if (lastStateC
== -1) {
132 // backtrack to get last state:
133 // comments are like whitespace, so we must return to the previous state
134 unsigned int i
= startPos
;
136 if ((lastStateC
= styler
.StyleAt(i
-1)) != SCE_CSS_COMMENT
) {
137 if (lastStateC
== SCE_CSS_OPERATOR
) {
138 op
= styler
.SafeGetCharAt(i
-1);
139 opPrev
= styler
.SafeGetCharAt(i
-2);
141 lastState
= styler
.StyleAt(i
-1);
142 if (lastState
!= SCE_CSS_OPERATOR
&& lastState
!= SCE_CSS_COMMENT
)
146 lastState
= SCE_CSS_DEFAULT
;
152 lastStateC
= SCE_CSS_DEFAULT
;
154 if (comment_mode
== eCommentBlock
) {
156 sc
.ForwardSetState(lastStateC
);
157 } else /* eCommentLine */ {
158 sc
.SetState(lastStateC
);
162 if (sc
.state
== SCE_CSS_COMMENT
)
165 if (sc
.state
== SCE_CSS_DOUBLESTRING
|| sc
.state
== SCE_CSS_SINGLESTRING
) {
166 if (sc
.ch
!= (sc
.state
== SCE_CSS_DOUBLESTRING
? '\"' : '\''))
168 unsigned int i
= sc
.currentPos
;
169 while (i
&& styler
[i
-1] == '\\')
171 if ((sc
.currentPos
- i
) % 2 == 1)
173 sc
.ForwardSetState(lastStateS
);
176 if (sc
.state
== SCE_CSS_OPERATOR
) {
178 unsigned int i
= startPos
;
179 op
= styler
.SafeGetCharAt(i
-1);
180 opPrev
= styler
.SafeGetCharAt(i
-2);
182 lastState
= styler
.StyleAt(i
-1);
183 if (lastState
!= SCE_CSS_OPERATOR
&& lastState
!= SCE_CSS_COMMENT
)
189 if (lastState
== SCE_CSS_DEFAULT
|| hasNesting
)
190 sc
.SetState(SCE_CSS_DIRECTIVE
);
194 if (lastState
== SCE_CSS_TAG
|| lastState
== SCE_CSS_CLASS
|| lastState
== SCE_CSS_ID
||
195 lastState
== SCE_CSS_PSEUDOCLASS
|| lastState
== SCE_CSS_EXTENDED_PSEUDOCLASS
|| lastState
== SCE_CSS_UNKNOWN_PSEUDOCLASS
)
196 sc
.SetState(SCE_CSS_DEFAULT
);
199 if (lastState
== SCE_CSS_TAG
|| lastState
== SCE_CSS_DEFAULT
|| lastState
== SCE_CSS_CLASS
|| lastState
== SCE_CSS_ID
||
200 lastState
== SCE_CSS_PSEUDOCLASS
|| lastState
== SCE_CSS_EXTENDED_PSEUDOCLASS
|| lastState
== SCE_CSS_UNKNOWN_PSEUDOCLASS
)
201 sc
.SetState(SCE_CSS_ATTRIBUTE
);
204 if (lastState
== SCE_CSS_ATTRIBUTE
)
205 sc
.SetState(SCE_CSS_TAG
);
211 sc
.SetState(SCE_CSS_DEFAULT
);
214 case SCE_CSS_DIRECTIVE
:
215 sc
.SetState(SCE_CSS_IDENTIFIER
);
220 if (--nestingLevel
< 0)
223 case SCE_CSS_DEFAULT
:
225 case SCE_CSS_IMPORTANT
:
226 case SCE_CSS_IDENTIFIER
:
227 case SCE_CSS_IDENTIFIER2
:
228 case SCE_CSS_IDENTIFIER3
:
230 sc
.SetState(nestingLevel
> 0 ? SCE_CSS_IDENTIFIER
: SCE_CSS_DEFAULT
);
232 sc
.SetState(SCE_CSS_DEFAULT
);
237 if (lastState
== SCE_CSS_PSEUDOCLASS
)
238 sc
.SetState(SCE_CSS_TAG
);
239 else if (lastState
== SCE_CSS_EXTENDED_PSEUDOCLASS
)
240 sc
.SetState(SCE_CSS_EXTENDED_PSEUDOCLASS
);
243 if (lastState
== SCE_CSS_TAG
|| lastState
== SCE_CSS_DEFAULT
|| lastState
== SCE_CSS_CLASS
|| lastState
== SCE_CSS_ID
||
244 lastState
== SCE_CSS_PSEUDOCLASS
|| lastState
== SCE_CSS_EXTENDED_PSEUDOCLASS
|| lastState
== SCE_CSS_UNKNOWN_PSEUDOCLASS
||
245 lastState
== SCE_CSS_PSEUDOELEMENT
|| lastState
== SCE_CSS_EXTENDED_PSEUDOELEMENT
)
246 sc
.SetState(SCE_CSS_TAG
);
251 case SCE_CSS_DEFAULT
:
254 case SCE_CSS_PSEUDOCLASS
:
255 case SCE_CSS_EXTENDED_PSEUDOCLASS
:
256 case SCE_CSS_UNKNOWN_PSEUDOCLASS
:
257 case SCE_CSS_PSEUDOELEMENT
:
258 case SCE_CSS_EXTENDED_PSEUDOELEMENT
:
259 sc
.SetState(SCE_CSS_PSEUDOCLASS
);
261 case SCE_CSS_IDENTIFIER
:
262 case SCE_CSS_IDENTIFIER2
:
263 case SCE_CSS_IDENTIFIER3
:
264 case SCE_CSS_EXTENDED_IDENTIFIER
:
265 case SCE_CSS_UNKNOWN_IDENTIFIER
:
266 case SCE_CSS_VARIABLE
:
267 sc
.SetState(SCE_CSS_VALUE
);
268 lastStateVal
= lastState
;
273 if (lastState
== SCE_CSS_TAG
|| lastState
== SCE_CSS_DEFAULT
|| lastState
== SCE_CSS_CLASS
|| lastState
== SCE_CSS_ID
||
274 lastState
== SCE_CSS_PSEUDOCLASS
|| lastState
== SCE_CSS_EXTENDED_PSEUDOCLASS
|| lastState
== SCE_CSS_UNKNOWN_PSEUDOCLASS
)
275 sc
.SetState(SCE_CSS_CLASS
);
278 if (lastState
== SCE_CSS_TAG
|| lastState
== SCE_CSS_DEFAULT
|| lastState
== SCE_CSS_CLASS
|| lastState
== SCE_CSS_ID
||
279 lastState
== SCE_CSS_PSEUDOCLASS
|| lastState
== SCE_CSS_EXTENDED_PSEUDOCLASS
|| lastState
== SCE_CSS_UNKNOWN_PSEUDOCLASS
)
280 sc
.SetState(SCE_CSS_ID
);
285 if (lastState
== SCE_CSS_TAG
)
286 sc
.SetState(SCE_CSS_DEFAULT
);
290 case SCE_CSS_DIRECTIVE
:
292 sc
.SetState(nestingLevel
> 0 ? SCE_CSS_IDENTIFIER
: SCE_CSS_DEFAULT
);
294 sc
.SetState(SCE_CSS_DEFAULT
);
298 case SCE_CSS_IMPORTANT
:
299 // data URLs can have semicolons; simplistically check for wrapping parentheses and move along
300 if (insideParentheses
) {
301 sc
.SetState(lastState
);
303 if (lastStateVal
== SCE_CSS_VARIABLE
) {
304 sc
.SetState(SCE_CSS_DEFAULT
);
306 sc
.SetState(SCE_CSS_IDENTIFIER
);
310 case SCE_CSS_VARIABLE
:
311 if (lastStateVar
== SCE_CSS_VALUE
) {
312 // data URLs can have semicolons; simplistically check for wrapping parentheses and move along
313 if (insideParentheses
) {
314 sc
.SetState(SCE_CSS_VALUE
);
316 sc
.SetState(SCE_CSS_IDENTIFIER
);
319 sc
.SetState(SCE_CSS_DEFAULT
);
325 if (lastState
== SCE_CSS_VALUE
)
326 sc
.SetState(SCE_CSS_IMPORTANT
);
331 if (sc
.ch
== '*' && sc
.state
== SCE_CSS_DEFAULT
) {
332 sc
.SetState(SCE_CSS_TAG
);
336 // check for inside parentheses (whether part of an "operator" or not)
338 insideParentheses
= true;
339 else if (sc
.ch
== ')')
340 insideParentheses
= false;
342 // SCSS special modes
345 if (sc
.ch
== varPrefix
) {
347 case SCE_CSS_DEFAULT
:
348 if (isLessDocument
) // give priority to pseudo elements
351 lastStateVar
= sc
.state
;
352 sc
.SetState(SCE_CSS_VARIABLE
);
356 if (sc
.state
== SCE_CSS_VARIABLE
) {
357 if (IsAWordChar(sc
.ch
)) {
358 // still looking at the variable name
361 if (lastStateVar
== SCE_CSS_VALUE
) {
362 // not looking at the variable name any more, and it was part of a value
363 sc
.SetState(SCE_CSS_VALUE
);
367 // nested rule parent selector
370 case SCE_CSS_DEFAULT
:
371 case SCE_CSS_IDENTIFIER
:
372 sc
.SetState(SCE_CSS_TAG
);
378 // nesting rules that apply to SCSS and Less
380 // check for nested rule selector
381 if (sc
.state
== SCE_CSS_IDENTIFIER
&& (IsAWordChar(sc
.ch
) || sc
.ch
== ':' || sc
.ch
== '.' || sc
.ch
== '#')) {
382 // look ahead to see whether { comes before next ; and }
383 unsigned int endPos
= startPos
+ length
;
386 for (unsigned int i
= sc
.currentPos
; i
< endPos
; i
++) {
387 ch
= styler
.SafeGetCharAt(i
);
388 if (ch
== ';' || ch
== '}')
391 sc
.SetState(SCE_CSS_DEFAULT
);
399 if (IsAWordChar(sc
.ch
)) {
400 if (sc
.state
== SCE_CSS_DEFAULT
)
401 sc
.SetState(SCE_CSS_TAG
);
405 if (IsAWordChar(sc
.chPrev
) && (
406 sc
.state
== SCE_CSS_IDENTIFIER
|| sc
.state
== SCE_CSS_IDENTIFIER2
||
407 sc
.state
== SCE_CSS_IDENTIFIER3
|| sc
.state
== SCE_CSS_EXTENDED_IDENTIFIER
||
408 sc
.state
== SCE_CSS_UNKNOWN_IDENTIFIER
||
409 sc
.state
== SCE_CSS_PSEUDOCLASS
|| sc
.state
== SCE_CSS_PSEUDOELEMENT
||
410 sc
.state
== SCE_CSS_EXTENDED_PSEUDOCLASS
|| sc
.state
== SCE_CSS_EXTENDED_PSEUDOELEMENT
||
411 sc
.state
== SCE_CSS_UNKNOWN_PSEUDOCLASS
||
412 sc
.state
== SCE_CSS_IMPORTANT
||
413 sc
.state
== SCE_CSS_DIRECTIVE
416 sc
.GetCurrentLowered(s
, sizeof(s
));
418 while (*s2
&& !IsAWordChar(*s2
))
421 case SCE_CSS_IDENTIFIER
:
422 case SCE_CSS_IDENTIFIER2
:
423 case SCE_CSS_IDENTIFIER3
:
424 case SCE_CSS_EXTENDED_IDENTIFIER
:
425 case SCE_CSS_UNKNOWN_IDENTIFIER
:
426 if (css1Props
.InList(s2
))
427 sc
.ChangeState(SCE_CSS_IDENTIFIER
);
428 else if (css2Props
.InList(s2
))
429 sc
.ChangeState(SCE_CSS_IDENTIFIER2
);
430 else if (css3Props
.InList(s2
))
431 sc
.ChangeState(SCE_CSS_IDENTIFIER3
);
432 else if (exProps
.InList(s2
))
433 sc
.ChangeState(SCE_CSS_EXTENDED_IDENTIFIER
);
435 sc
.ChangeState(SCE_CSS_UNKNOWN_IDENTIFIER
);
437 case SCE_CSS_PSEUDOCLASS
:
438 case SCE_CSS_PSEUDOELEMENT
:
439 case SCE_CSS_EXTENDED_PSEUDOCLASS
:
440 case SCE_CSS_EXTENDED_PSEUDOELEMENT
:
441 case SCE_CSS_UNKNOWN_PSEUDOCLASS
:
442 if (op
== ':' && opPrev
!= ':' && pseudoClasses
.InList(s2
))
443 sc
.ChangeState(SCE_CSS_PSEUDOCLASS
);
444 else if (opPrev
== ':' && pseudoElements
.InList(s2
))
445 sc
.ChangeState(SCE_CSS_PSEUDOELEMENT
);
446 else if ((op
== ':' || (op
== '(' && lastState
== SCE_CSS_EXTENDED_PSEUDOCLASS
)) && opPrev
!= ':' && exPseudoClasses
.InList(s2
))
447 sc
.ChangeState(SCE_CSS_EXTENDED_PSEUDOCLASS
);
448 else if (opPrev
== ':' && exPseudoElements
.InList(s2
))
449 sc
.ChangeState(SCE_CSS_EXTENDED_PSEUDOELEMENT
);
451 sc
.ChangeState(SCE_CSS_UNKNOWN_PSEUDOCLASS
);
453 case SCE_CSS_IMPORTANT
:
454 if (strcmp(s2
, "important") != 0)
455 sc
.ChangeState(SCE_CSS_VALUE
);
457 case SCE_CSS_DIRECTIVE
:
458 if (op
== '@' && strcmp(s2
, "media") == 0)
459 sc
.ChangeState(SCE_CSS_MEDIA
);
464 if (sc
.ch
!= '.' && sc
.ch
!= ':' && sc
.ch
!= '#' && (
465 sc
.state
== SCE_CSS_CLASS
|| sc
.state
== SCE_CSS_ID
||
466 (sc
.ch
!= '(' && sc
.ch
!= ')' && ( /* This line of the condition makes it possible to extend pseudo-classes with parentheses */
467 sc
.state
== SCE_CSS_PSEUDOCLASS
|| sc
.state
== SCE_CSS_PSEUDOELEMENT
||
468 sc
.state
== SCE_CSS_EXTENDED_PSEUDOCLASS
|| sc
.state
== SCE_CSS_EXTENDED_PSEUDOELEMENT
||
469 sc
.state
== SCE_CSS_UNKNOWN_PSEUDOCLASS
472 sc
.SetState(SCE_CSS_TAG
);
474 if (sc
.Match('/', '*')) {
475 lastStateC
= sc
.state
;
476 comment_mode
= eCommentBlock
;
477 sc
.SetState(SCE_CSS_COMMENT
);
479 } else if (hasSingleLineComments
&& sc
.Match('/', '/') && !insideParentheses
) {
480 // note that we've had to treat ([...]// as the start of a URL not a comment, e.g. url(http://example.com), url(//example.com)
481 lastStateC
= sc
.state
;
482 comment_mode
= eCommentLine
;
483 sc
.SetState(SCE_CSS_COMMENT
);
485 } else if ((sc
.state
== SCE_CSS_VALUE
|| sc
.state
== SCE_CSS_ATTRIBUTE
)
486 && (sc
.ch
== '\"' || sc
.ch
== '\'')) {
487 lastStateS
= sc
.state
;
488 sc
.SetState((sc
.ch
== '\"' ? SCE_CSS_DOUBLESTRING
: SCE_CSS_SINGLESTRING
));
489 } else if (IsCssOperator(sc
.ch
)
490 && (sc
.state
!= SCE_CSS_ATTRIBUTE
|| sc
.ch
== ']')
491 && (sc
.state
!= SCE_CSS_VALUE
|| sc
.ch
== ';' || sc
.ch
== '}' || sc
.ch
== '!')
492 && ((sc
.state
!= SCE_CSS_DIRECTIVE
&& sc
.state
!= SCE_CSS_MEDIA
) || sc
.ch
== ';' || sc
.ch
== '{')
494 if (sc
.state
!= SCE_CSS_OPERATOR
)
495 lastState
= sc
.state
;
496 sc
.SetState(SCE_CSS_OPERATOR
);
505 static void FoldCSSDoc(unsigned int startPos
, int length
, int, WordList
*[], Accessor
&styler
) {
506 bool foldComment
= styler
.GetPropertyInt("fold.comment") != 0;
507 bool foldCompact
= styler
.GetPropertyInt("fold.compact", 1) != 0;
508 unsigned int endPos
= startPos
+ length
;
509 int visibleChars
= 0;
510 int lineCurrent
= styler
.GetLine(startPos
);
511 int levelPrev
= styler
.LevelAt(lineCurrent
) & SC_FOLDLEVELNUMBERMASK
;
512 int levelCurrent
= levelPrev
;
513 char chNext
= styler
[startPos
];
514 bool inComment
= (styler
.StyleAt(startPos
-1) == SCE_CSS_COMMENT
);
515 for (unsigned int i
= startPos
; i
< endPos
; i
++) {
517 chNext
= styler
.SafeGetCharAt(i
+ 1);
518 int style
= styler
.StyleAt(i
);
519 bool atEOL
= (ch
== '\r' && chNext
!= '\n') || (ch
== '\n');
521 if (!inComment
&& (style
== SCE_CSS_COMMENT
))
523 else if (inComment
&& (style
!= SCE_CSS_COMMENT
))
525 inComment
= (style
== SCE_CSS_COMMENT
);
527 if (style
== SCE_CSS_OPERATOR
) {
530 } else if (ch
== '}') {
536 if (visibleChars
== 0 && foldCompact
)
537 lev
|= SC_FOLDLEVELWHITEFLAG
;
538 if ((levelCurrent
> levelPrev
) && (visibleChars
> 0))
539 lev
|= SC_FOLDLEVELHEADERFLAG
;
540 if (lev
!= styler
.LevelAt(lineCurrent
)) {
541 styler
.SetLevel(lineCurrent
, lev
);
544 levelPrev
= levelCurrent
;
547 if (!isspacechar(ch
))
550 // Fill in the real level of the next line, keeping the current flags as they will be filled in later
551 int flagsNext
= styler
.LevelAt(lineCurrent
) & ~SC_FOLDLEVELNUMBERMASK
;
552 styler
.SetLevel(lineCurrent
, levelPrev
| flagsNext
);
555 static const char * const cssWordListDesc
[] = {
561 "Browser-Specific CSS Properties",
562 "Browser-Specific Pseudo-classes",
563 "Browser-Specific Pseudo-elements",
567 LexerModule
lmCss(SCLEX_CSS
, ColouriseCssDoc
, "css", FoldCSSDoc
, cssWordListDesc
);