1 // Scintilla source code edit control
4 ** Lexer for Cascading Style Sheets
5 ** Written by Jakub Vrána
6 ** Improved by Philippe Lhoste (CSS2)
7 ** Improved by Ross McKay (SCSS mode; see http://sass-lang.com/ )
9 // Copyright 1998-2002 by Neil Hodgson <neilh@scintilla.org>
10 // The License.txt file describes the conditions under which this software may be distributed.
12 // TODO: handle SCSS nested properties like font: { weight: bold; size: 1em; }
13 // TODO: handle SCSS interpolation: #{}
14 // TODO: add features for Less if somebody feels like contributing; http://lesscss.org/
15 // TODO: refactor this monster so that the next poor slob can read it!
25 #include "Scintilla.h"
29 #include "LexAccessor.h"
31 #include "StyleContext.h"
32 #include "CharacterSet.h"
33 #include "LexerModule.h"
35 using namespace Scintilla
;
38 static inline bool IsAWordChar(const unsigned int ch
) {
40 * The CSS spec allows "ISO 10646 characters U+00A1 and higher" to be treated as word chars.
41 * Unfortunately, we are only getting string bytes here, and not full unicode characters. We cannot guarantee
42 * that our byte is between U+0080 - U+00A0 (to return false), so we have to allow all characters U+0080 and higher
44 return ch
>= 0x80 || isalnum(ch
) || ch
== '-' || ch
== '_';
47 inline bool IsCssOperator(const int ch
) {
48 if (!((ch
< 0x80) && isalnum(ch
)) &&
49 (ch
== '{' || ch
== '}' || ch
== ':' || ch
== ',' || ch
== ';' ||
50 ch
== '.' || ch
== '#' || ch
== '!' || ch
== '@' ||
52 ch
== '*' || ch
== '>' || ch
== '+' || ch
== '=' || ch
== '~' || ch
== '|' ||
53 ch
== '[' || ch
== ']' || ch
== '(' || ch
== ')')) {
59 // look behind (from start of document to our start position) to determine current nesting level
60 inline int NestingLevelLookBehind(Sci_PositionU startPos
, Accessor
&styler
) {
64 for (Sci_PositionU i
= 0; i
< startPos
; i
++) {
65 ch
= styler
.SafeGetCharAt(i
);
75 static void ColouriseCssDoc(Sci_PositionU startPos
, Sci_Position length
, int initStyle
, WordList
*keywordlists
[], Accessor
&styler
) {
76 WordList
&css1Props
= *keywordlists
[0];
77 WordList
&pseudoClasses
= *keywordlists
[1];
78 WordList
&css2Props
= *keywordlists
[2];
79 WordList
&css3Props
= *keywordlists
[3];
80 WordList
&pseudoElements
= *keywordlists
[4];
81 WordList
&exProps
= *keywordlists
[5];
82 WordList
&exPseudoClasses
= *keywordlists
[6];
83 WordList
&exPseudoElements
= *keywordlists
[7];
85 StyleContext
sc(startPos
, length
, initStyle
, styler
);
87 int lastState
= -1; // before operator
88 int lastStateC
= -1; // before comment
89 int lastStateS
= -1; // before single-quoted/double-quoted string
90 int lastStateVar
= -1; // before variable (SCSS)
91 int lastStateVal
= -1; // before value (SCSS)
92 int op
= ' '; // last operator
93 int opPrev
= ' '; // last operator
94 bool insideParentheses
= false; // true if currently in a CSS url() or similar construct
96 // property lexer.css.scss.language
97 // Set to 1 for Sassy CSS (.scss)
98 bool isScssDocument
= styler
.GetPropertyInt("lexer.css.scss.language") != 0;
100 // property lexer.css.less.language
101 // Set to 1 for Less CSS (.less)
102 bool isLessDocument
= styler
.GetPropertyInt("lexer.css.less.language") != 0;
104 // property lexer.css.hss.language
105 // Set to 1 for HSS (.hss)
106 bool isHssDocument
= styler
.GetPropertyInt("lexer.css.hss.language") != 0;
108 // SCSS/LESS/HSS have the concept of variable
109 bool hasVariables
= isScssDocument
|| isLessDocument
|| isHssDocument
;
112 varPrefix
= isLessDocument
? '@' : '$';
114 // SCSS/LESS/HSS support single-line comments
115 typedef enum _CommentModes
{ eCommentBlock
= 0, eCommentLine
= 1} CommentMode
;
116 CommentMode comment_mode
= eCommentBlock
;
117 bool hasSingleLineComments
= isScssDocument
|| isLessDocument
|| isHssDocument
;
119 // must keep track of nesting level in document types that support it (SCSS/LESS/HSS)
120 bool hasNesting
= false;
121 int nestingLevel
= 0;
122 if (isScssDocument
|| isLessDocument
|| isHssDocument
) {
124 nestingLevel
= NestingLevelLookBehind(startPos
, styler
);
128 for (; sc
.More(); sc
.Forward()) {
129 if (sc
.state
== SCE_CSS_COMMENT
&& ((comment_mode
== eCommentBlock
&& sc
.Match('*', '/')) || (comment_mode
== eCommentLine
&& sc
.atLineEnd
))) {
130 if (lastStateC
== -1) {
131 // backtrack to get last state:
132 // comments are like whitespace, so we must return to the previous state
133 Sci_PositionU i
= startPos
;
135 if ((lastStateC
= styler
.StyleAt(i
-1)) != SCE_CSS_COMMENT
) {
136 if (lastStateC
== SCE_CSS_OPERATOR
) {
137 op
= styler
.SafeGetCharAt(i
-1);
138 opPrev
= styler
.SafeGetCharAt(i
-2);
140 lastState
= styler
.StyleAt(i
-1);
141 if (lastState
!= SCE_CSS_OPERATOR
&& lastState
!= SCE_CSS_COMMENT
)
145 lastState
= SCE_CSS_DEFAULT
;
151 lastStateC
= SCE_CSS_DEFAULT
;
153 if (comment_mode
== eCommentBlock
) {
155 sc
.ForwardSetState(lastStateC
);
156 } else /* eCommentLine */ {
157 sc
.SetState(lastStateC
);
161 if (sc
.state
== SCE_CSS_COMMENT
)
164 if (sc
.state
== SCE_CSS_DOUBLESTRING
|| sc
.state
== SCE_CSS_SINGLESTRING
) {
165 if (sc
.ch
!= (sc
.state
== SCE_CSS_DOUBLESTRING
? '\"' : '\''))
167 Sci_PositionU i
= sc
.currentPos
;
168 while (i
&& styler
[i
-1] == '\\')
170 if ((sc
.currentPos
- i
) % 2 == 1)
172 sc
.ForwardSetState(lastStateS
);
175 if (sc
.state
== SCE_CSS_OPERATOR
) {
177 Sci_PositionU i
= startPos
;
178 op
= styler
.SafeGetCharAt(i
-1);
179 opPrev
= styler
.SafeGetCharAt(i
-2);
181 lastState
= styler
.StyleAt(i
-1);
182 if (lastState
!= SCE_CSS_OPERATOR
&& lastState
!= SCE_CSS_COMMENT
)
188 if (lastState
== SCE_CSS_DEFAULT
|| hasNesting
)
189 sc
.SetState(SCE_CSS_DIRECTIVE
);
193 if (lastState
== SCE_CSS_TAG
|| lastState
== SCE_CSS_CLASS
|| lastState
== SCE_CSS_ID
||
194 lastState
== SCE_CSS_PSEUDOCLASS
|| lastState
== SCE_CSS_EXTENDED_PSEUDOCLASS
|| lastState
== SCE_CSS_UNKNOWN_PSEUDOCLASS
)
195 sc
.SetState(SCE_CSS_DEFAULT
);
198 if (lastState
== SCE_CSS_TAG
|| lastState
== SCE_CSS_DEFAULT
|| lastState
== SCE_CSS_CLASS
|| lastState
== SCE_CSS_ID
||
199 lastState
== SCE_CSS_PSEUDOCLASS
|| lastState
== SCE_CSS_EXTENDED_PSEUDOCLASS
|| lastState
== SCE_CSS_UNKNOWN_PSEUDOCLASS
)
200 sc
.SetState(SCE_CSS_ATTRIBUTE
);
203 if (lastState
== SCE_CSS_ATTRIBUTE
)
204 sc
.SetState(SCE_CSS_TAG
);
210 sc
.SetState(SCE_CSS_DEFAULT
);
213 case SCE_CSS_DIRECTIVE
:
214 sc
.SetState(SCE_CSS_IDENTIFIER
);
219 if (--nestingLevel
< 0)
222 case SCE_CSS_DEFAULT
:
224 case SCE_CSS_IMPORTANT
:
225 case SCE_CSS_IDENTIFIER
:
226 case SCE_CSS_IDENTIFIER2
:
227 case SCE_CSS_IDENTIFIER3
:
229 sc
.SetState(nestingLevel
> 0 ? SCE_CSS_IDENTIFIER
: SCE_CSS_DEFAULT
);
231 sc
.SetState(SCE_CSS_DEFAULT
);
236 if (lastState
== SCE_CSS_PSEUDOCLASS
)
237 sc
.SetState(SCE_CSS_TAG
);
238 else if (lastState
== SCE_CSS_EXTENDED_PSEUDOCLASS
)
239 sc
.SetState(SCE_CSS_EXTENDED_PSEUDOCLASS
);
242 if (lastState
== SCE_CSS_TAG
|| lastState
== SCE_CSS_DEFAULT
|| lastState
== SCE_CSS_CLASS
|| lastState
== SCE_CSS_ID
||
243 lastState
== SCE_CSS_PSEUDOCLASS
|| lastState
== SCE_CSS_EXTENDED_PSEUDOCLASS
|| lastState
== SCE_CSS_UNKNOWN_PSEUDOCLASS
||
244 lastState
== SCE_CSS_PSEUDOELEMENT
|| lastState
== SCE_CSS_EXTENDED_PSEUDOELEMENT
)
245 sc
.SetState(SCE_CSS_TAG
);
250 case SCE_CSS_DEFAULT
:
253 case SCE_CSS_PSEUDOCLASS
:
254 case SCE_CSS_EXTENDED_PSEUDOCLASS
:
255 case SCE_CSS_UNKNOWN_PSEUDOCLASS
:
256 case SCE_CSS_PSEUDOELEMENT
:
257 case SCE_CSS_EXTENDED_PSEUDOELEMENT
:
258 sc
.SetState(SCE_CSS_PSEUDOCLASS
);
260 case SCE_CSS_IDENTIFIER
:
261 case SCE_CSS_IDENTIFIER2
:
262 case SCE_CSS_IDENTIFIER3
:
263 case SCE_CSS_EXTENDED_IDENTIFIER
:
264 case SCE_CSS_UNKNOWN_IDENTIFIER
:
265 case SCE_CSS_VARIABLE
:
266 sc
.SetState(SCE_CSS_VALUE
);
267 lastStateVal
= lastState
;
272 if (lastState
== SCE_CSS_TAG
|| lastState
== SCE_CSS_DEFAULT
|| lastState
== SCE_CSS_CLASS
|| lastState
== SCE_CSS_ID
||
273 lastState
== SCE_CSS_PSEUDOCLASS
|| lastState
== SCE_CSS_EXTENDED_PSEUDOCLASS
|| lastState
== SCE_CSS_UNKNOWN_PSEUDOCLASS
)
274 sc
.SetState(SCE_CSS_CLASS
);
277 if (lastState
== SCE_CSS_TAG
|| lastState
== SCE_CSS_DEFAULT
|| lastState
== SCE_CSS_CLASS
|| lastState
== SCE_CSS_ID
||
278 lastState
== SCE_CSS_PSEUDOCLASS
|| lastState
== SCE_CSS_EXTENDED_PSEUDOCLASS
|| lastState
== SCE_CSS_UNKNOWN_PSEUDOCLASS
)
279 sc
.SetState(SCE_CSS_ID
);
284 if (lastState
== SCE_CSS_TAG
)
285 sc
.SetState(SCE_CSS_DEFAULT
);
289 case SCE_CSS_DIRECTIVE
:
291 sc
.SetState(nestingLevel
> 0 ? SCE_CSS_IDENTIFIER
: SCE_CSS_DEFAULT
);
293 sc
.SetState(SCE_CSS_DEFAULT
);
297 case SCE_CSS_IMPORTANT
:
298 // data URLs can have semicolons; simplistically check for wrapping parentheses and move along
299 if (insideParentheses
) {
300 sc
.SetState(lastState
);
302 if (lastStateVal
== SCE_CSS_VARIABLE
) {
303 sc
.SetState(SCE_CSS_DEFAULT
);
305 sc
.SetState(SCE_CSS_IDENTIFIER
);
309 case SCE_CSS_VARIABLE
:
310 if (lastStateVar
== SCE_CSS_VALUE
) {
311 // data URLs can have semicolons; simplistically check for wrapping parentheses and move along
312 if (insideParentheses
) {
313 sc
.SetState(SCE_CSS_VALUE
);
315 sc
.SetState(SCE_CSS_IDENTIFIER
);
318 sc
.SetState(SCE_CSS_DEFAULT
);
324 if (lastState
== SCE_CSS_VALUE
)
325 sc
.SetState(SCE_CSS_IMPORTANT
);
330 if (sc
.ch
== '*' && sc
.state
== SCE_CSS_DEFAULT
) {
331 sc
.SetState(SCE_CSS_TAG
);
335 // check for inside parentheses (whether part of an "operator" or not)
337 insideParentheses
= true;
338 else if (sc
.ch
== ')')
339 insideParentheses
= false;
341 // SCSS special modes
344 if (sc
.ch
== varPrefix
) {
346 case SCE_CSS_DEFAULT
:
347 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 Sci_PositionU endPos
= startPos
+ length
;
386 for (Sci_PositionU 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(Sci_PositionU startPos
, Sci_Position length
, int, WordList
*[], Accessor
&styler
) {
506 bool foldComment
= styler
.GetPropertyInt("fold.comment") != 0;
507 bool foldCompact
= styler
.GetPropertyInt("fold.compact", 1) != 0;
508 Sci_PositionU endPos
= startPos
+ length
;
509 int visibleChars
= 0;
510 Sci_Position 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 (Sci_PositionU 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
);