]> git.saurik.com Git - wxWidgets.git/blob - contrib/src/stc/scintilla/src/LexPerl.cxx
Updated wxSTC from Scintilla 1.40 to Scintilla 1.45
[wxWidgets.git] / contrib / src / stc / scintilla / src / LexPerl.cxx
1 // Scintilla source code edit control
2 /** @file LexPerl.cxx
3 ** Lexer for subset of Perl.
4 **/
5 // Copyright 1998-2001 by Neil Hodgson <neilh@scintilla.org>
6 // The License.txt file describes the conditions under which this software may be distributed.
7
8 #include <stdlib.h>
9 #include <string.h>
10 #include <ctype.h>
11 #include <stdio.h>
12 #include <stdarg.h>
13
14 #include "Platform.h"
15
16 #include "PropSet.h"
17 #include "Accessor.h"
18 #include "KeyWords.h"
19 #include "Scintilla.h"
20 #include "SciLexer.h"
21
22 static inline bool isEOLChar(char ch) {
23 return (ch == '\r') || (ch == '\n');
24 }
25
26 static bool isSingleCharOp(char ch) {
27 char strCharSet[2];
28 strCharSet[0] = ch;
29 strCharSet[1] = '\0';
30 return (NULL != strstr("rwxoRWXOezsfdlpSbctugkTBMAC", strCharSet));
31 }
32
33 static inline bool isPerlOperator(char ch) {
34 if (isalnum(ch))
35 return false;
36 // '.' left out as it is used to make up numbers
37 if (ch == '%' || ch == '^' || ch == '&' || ch == '*' || ch == '\\' ||
38 ch == '(' || ch == ')' || ch == '-' || ch == '+' ||
39 ch == '=' || ch == '|' || ch == '{' || ch == '}' ||
40 ch == '[' || ch == ']' || ch == ':' || ch == ';' ||
41 ch == '<' || ch == '>' || ch == ',' || ch == '/' ||
42 ch == '?' || ch == '!' || ch == '.' || ch == '~')
43 return true;
44 return false;
45 }
46
47 static int classifyWordPerl(unsigned int start, unsigned int end, WordList &keywords, Accessor &styler) {
48 char s[100];
49 bool wordIsNumber = isdigit(styler[start]) || (styler[start] == '.');
50 for (unsigned int i = 0; i < end - start + 1 && i < 30; i++) {
51 s[i] = styler[start + i];
52 s[i + 1] = '\0';
53 }
54 char chAttr = SCE_PL_IDENTIFIER;
55 if (wordIsNumber)
56 chAttr = SCE_PL_NUMBER;
57 else {
58 if (keywords.InList(s))
59 chAttr = SCE_PL_WORD;
60 }
61 styler.ColourTo(end, chAttr);
62 return chAttr;
63 }
64
65 static inline bool isEndVar(char ch) {
66 return !isalnum(ch) && ch != '#' && ch != '$' &&
67 ch != '_' && ch != '\'';
68 }
69
70 static bool isMatch(Accessor &styler, int lengthDoc, int pos, const char *val) {
71 if ((pos + static_cast<int>(strlen(val))) >= lengthDoc) {
72 return false;
73 }
74 while (*val) {
75 if (*val != styler[pos++]) {
76 return false;
77 }
78 val++;
79 }
80 return true;
81 }
82
83 static char opposite(char ch) {
84 if (ch == '(')
85 return ')';
86 if (ch == '[')
87 return ']';
88 if (ch == '{')
89 return '}';
90 if (ch == '<')
91 return '>';
92 return ch;
93 }
94
95 static void ColourisePerlDoc(unsigned int startPos, int length, int initStyle,
96 WordList *keywordlists[], Accessor &styler) {
97
98 // Lexer for perl often has to backtrack to start of current style to determine
99 // which characters are being used as quotes, how deeply nested is the
100 // start position and what the termination string is for here documents
101
102 WordList &keywords = *keywordlists[0];
103
104 class HereDocCls {
105 public:
106 int State; // 0: '<<' encountered
107 // 1: collect the delimiter
108 // 2: here doc text (lines after the delimiter)
109 char Quote; // the char after '<<'
110 bool Quoted; // true if Quote in ('\'','"','`')
111 int DelimiterLength; // strlen(Delimiter)
112 char Delimiter[256]; // the Delimiter, 256: sizeof PL_tokenbuf
113 HereDocCls() {
114 State = 0;
115 DelimiterLength = 0;
116 Delimiter[0] = '\0';
117 }
118 };
119 HereDocCls HereDoc; // TODO: FIFO for stacked here-docs
120
121 class QuoteCls {
122 public:
123 int Rep;
124 int Count;
125 char Up;
126 char Down;
127 QuoteCls() {
128 this->New(1);
129 }
130 void New(int r) {
131 Rep = r;
132 Count = 0;
133 Up = '\0';
134 Down = '\0';
135 }
136 void Open(char u) {
137 Count++;
138 Up = u;
139 Down = opposite(Up);
140 }
141 };
142 QuoteCls Quote;
143
144 char sooked[100];
145 int sookedpos = 0;
146 bool preferRE = true;
147 sooked[sookedpos] = '\0';
148 int state = initStyle;
149 unsigned int lengthDoc = startPos + length;
150
151 // If in a long distance lexical state, seek to the beginning to find quote characters
152 if (state == SCE_PL_HERE_Q || state == SCE_PL_HERE_QQ || state == SCE_PL_HERE_QX) {
153 while ((startPos > 1) && (styler.StyleAt(startPos) != SCE_PL_HERE_DELIM)) {
154 startPos--;
155 }
156 startPos = styler.LineStart(styler.GetLine(startPos));
157 state = styler.StyleAt(startPos - 1);
158 }
159 if ( state == SCE_PL_STRING_Q
160 || state == SCE_PL_STRING_QQ
161 || state == SCE_PL_STRING_QX
162 || state == SCE_PL_STRING_QR
163 || state == SCE_PL_STRING_QW
164 || state == SCE_PL_REGEX
165 || state == SCE_PL_REGSUBST
166 ) {
167 while ((startPos > 1) && (styler.StyleAt(startPos - 1) == state)) {
168 startPos--;
169 }
170 state = SCE_PL_DEFAULT;
171 }
172
173 styler.StartAt(startPos);
174 char chPrev = styler.SafeGetCharAt(startPos - 1);
175 if (startPos == 0)
176 chPrev = '\n';
177 char chNext = styler[startPos];
178 styler.StartSegment(startPos);
179
180 for (unsigned int i = startPos; i < lengthDoc; i++) {
181 char ch = chNext;
182 chNext = styler.SafeGetCharAt(i + 1);
183 char chNext2 = styler.SafeGetCharAt(i + 2);
184
185 if (styler.IsLeadByte(ch)) {
186 chNext = styler.SafeGetCharAt(i + 2);
187 chPrev = ' ';
188 i += 1;
189 continue;
190 }
191 if ((chPrev == '\r' && ch == '\n')) { // skip on DOS/Windows
192 chPrev = ch;
193 continue;
194 }
195
196 if (HereDoc.State == 1 && isEOLChar(ch)) {
197 // Begin of here-doc (the line after the here-doc delimiter):
198 HereDoc.State = 2;
199 styler.ColourTo(i - 1, state);
200 if (HereDoc.Quoted) {
201 if (state == SCE_PL_HERE_DELIM) {
202 // Missing quote at end of string! We are stricter than perl.
203 state = SCE_PL_ERROR;
204 } else {
205 switch (HereDoc.Quote) {
206 case '\'':
207 state = SCE_PL_HERE_Q ;
208 break;
209 case '"':
210 state = SCE_PL_HERE_QQ;
211 break;
212 case '`':
213 state = SCE_PL_HERE_QX;
214 break;
215 }
216 }
217 } else {
218 switch (HereDoc.Quote) {
219 case '\\':
220 state = SCE_PL_HERE_Q ;
221 break;
222 default :
223 state = SCE_PL_HERE_QQ;
224 }
225 }
226 }
227
228 if (state == SCE_PL_DEFAULT) {
229 if (iswordstart(ch)) {
230 styler.ColourTo(i - 1, state);
231 if (ch == 's' && !isalnum(chNext)) {
232 state = SCE_PL_REGSUBST;
233 Quote.New(2);
234 } else if (ch == 'm' && !isalnum(chNext)) {
235 state = SCE_PL_REGEX;
236 Quote.New(1);
237 } else if (ch == 'q' && !isalnum(chNext)) {
238 state = SCE_PL_STRING_Q;
239 Quote.New(1);
240 } else if (ch == 'y' && !isalnum(chNext)) {
241 state = SCE_PL_REGSUBST;
242 Quote.New(2);
243 } else if (ch == 't' && chNext == 'r' && !isalnum(chNext2)) {
244 state = SCE_PL_REGSUBST;
245 Quote.New(2);
246 i++;
247 chNext = chNext2;
248 } else if (ch == 'q' && (chNext == 'q' || chNext == 'r' || chNext == 'w' || chNext == 'x') && !isalnum(chNext2)) {
249 if (chNext == 'q') state = SCE_PL_STRING_QQ;
250 else if (chNext == 'x') state = SCE_PL_STRING_QX;
251 else if (chNext == 'r') state = SCE_PL_STRING_QR;
252 else if (chNext == 'w') state = SCE_PL_STRING_QW;
253 i++;
254 chNext = chNext2;
255 Quote.New(1);
256 } else {
257 state = SCE_PL_WORD;
258 preferRE = false;
259 if ((!iswordchar(chNext) && chNext != '\'')
260 || (chNext == '.' && chNext2 == '.')) {
261 // We need that if length of word == 1!
262 // This test is copied from the SCE_PL_WORD handler.
263 classifyWordPerl(styler.GetStartSegment(), i, keywords, styler);
264 state = SCE_PL_DEFAULT;
265 }
266 }
267 } else if (ch == '#') {
268 styler.ColourTo(i - 1, state);
269 state = SCE_PL_COMMENTLINE;
270 } else if (ch == '\"') {
271 styler.ColourTo(i - 1, state);
272 state = SCE_PL_STRING;
273 Quote.New(1);
274 Quote.Open(ch);
275 } else if (ch == '\'') {
276 if (chPrev == '&') {
277 // Archaic call
278 styler.ColourTo(i, state);
279 } else {
280 styler.ColourTo(i - 1, state);
281 state = SCE_PL_CHARACTER;
282 Quote.New(1);
283 Quote.Open(ch);
284 }
285 } else if (ch == '`') {
286 styler.ColourTo(i - 1, state);
287 state = SCE_PL_BACKTICKS;
288 Quote.New(1);
289 Quote.Open(ch);
290 } else if (ch == '$') {
291 preferRE = false;
292 styler.ColourTo(i - 1, state);
293 if ((chNext == '{') || isspacechar(chNext)) {
294 styler.ColourTo(i, SCE_PL_SCALAR);
295 } else {
296 state = SCE_PL_SCALAR;
297 i++;
298 ch = chNext;
299 chNext = chNext2;
300 }
301 } else if (ch == '@') {
302 preferRE = false;
303 styler.ColourTo(i - 1, state);
304 if (isalpha(chNext) || chNext == '#' || chNext == '$' || chNext == '_') {
305 state = SCE_PL_ARRAY;
306 } else if (chNext != '{' && chNext != '[') {
307 styler.ColourTo(i, SCE_PL_ARRAY);
308 i++;
309 ch = ' ';
310 } else {
311 styler.ColourTo(i, SCE_PL_ARRAY);
312 }
313 } else if (ch == '%') {
314 preferRE = false;
315 styler.ColourTo(i - 1, state);
316 if (isalpha(chNext) || chNext == '#' || chNext == '$' || chNext == '_') {
317 state = SCE_PL_HASH;
318 } else if (chNext == '{') {
319 styler.ColourTo(i, SCE_PL_HASH);
320 } else {
321 styler.ColourTo(i, SCE_PL_OPERATOR);
322 }
323 } else if (ch == '*') {
324 styler.ColourTo(i - 1, state);
325 state = SCE_PL_SYMBOLTABLE;
326 } else if (ch == '/' && preferRE) {
327 styler.ColourTo(i - 1, state);
328 state = SCE_PL_REGEX;
329 Quote.New(1);
330 Quote.Open(ch);
331 } else if (ch == '<' && chNext == '<') {
332 styler.ColourTo(i - 1, state);
333 state = SCE_PL_HERE_DELIM;
334 HereDoc.State = 0;
335 } else if (ch == '='
336 && isalpha(chNext)
337 && (isEOLChar(chPrev))) {
338 styler.ColourTo(i - 1, state);
339 state = SCE_PL_POD;
340 sookedpos = 0;
341 sooked[sookedpos] = '\0';
342 } else if (ch == '-'
343 && isSingleCharOp(chNext)
344 && !isalnum((chNext2 = styler.SafeGetCharAt(i+2)))) {
345 styler.ColourTo(i - 1, state);
346 styler.ColourTo(i + 1, SCE_PL_WORD);
347 state = SCE_PL_DEFAULT;
348 preferRE = false;
349 i += 2;
350 ch = chNext2;
351 chNext = chNext2 = styler.SafeGetCharAt(i + 1);
352 } else if (isPerlOperator(ch)) {
353 if (ch == ')' || ch == ']')
354 preferRE = false;
355 else
356 preferRE = true;
357 styler.ColourTo(i - 1, state);
358 styler.ColourTo(i, SCE_PL_OPERATOR);
359 }
360 } else if (state == SCE_PL_WORD) {
361 if ((!iswordchar(chNext) && chNext != '\'')
362 || (chNext == '.' && chNext2 == '.')) {
363 // ".." is always an operator if preceded by a SCE_PL_WORD.
364 // Archaic Perl has quotes inside names
365 if (isMatch(styler, lengthDoc, styler.GetStartSegment(), "__DATA__")) {
366 styler.ColourTo(i, SCE_PL_DATASECTION);
367 state = SCE_PL_DATASECTION;
368 } else if (isMatch(styler, lengthDoc, styler.GetStartSegment(), "__END__")) {
369 styler.ColourTo(i, SCE_PL_DATASECTION);
370 state = SCE_PL_DATASECTION;
371 } else {
372 if (classifyWordPerl(styler.GetStartSegment(), i, keywords, styler) == SCE_PL_WORD)
373 preferRE = true;
374 state = SCE_PL_DEFAULT;
375 ch = ' ';
376 }
377 }
378 } else {
379 if (state == SCE_PL_COMMENTLINE) {
380 if (isEOLChar(ch)) {
381 styler.ColourTo(i - 1, state);
382 state = SCE_PL_DEFAULT;
383 }
384 } else if (state == SCE_PL_HERE_DELIM) {
385 //
386 // From perldata.pod:
387 // ------------------
388 // A line-oriented form of quoting is based on the shell ``here-doc''
389 // syntax.
390 // Following a << you specify a string to terminate the quoted material,
391 // and all lines following the current line down to the terminating
392 // string are the value of the item.
393 // The terminating string may be either an identifier (a word),
394 // or some quoted text.
395 // If quoted, the type of quotes you use determines the treatment of
396 // the text, just as in regular quoting.
397 // An unquoted identifier works like double quotes.
398 // There must be no space between the << and the identifier.
399 // (If you put a space it will be treated as a null identifier,
400 // which is valid, and matches the first empty line.)
401 // The terminating string must appear by itself (unquoted and with no
402 // surrounding whitespace) on the terminating line.
403 //
404 if (HereDoc.State == 0) { // '<<' encountered
405 HereDoc.State = 1;
406 HereDoc.Quote = chNext;
407 HereDoc.Quoted = false;
408 HereDoc.DelimiterLength = 0;
409 HereDoc.Delimiter[HereDoc.DelimiterLength] = '\0';
410 if (chNext == '\'' || chNext == '"' || chNext == '`') { // a quoted here-doc delimiter
411 i++;
412 ch = chNext;
413 chNext = chNext2;
414 HereDoc.Quoted = true;
415 } else if (chNext == '\\') { // ref?
416 i++;
417 ch = chNext;
418 chNext = chNext2;
419 } else if (isalnum(chNext) || chNext == '_') { // an unquoted here-doc delimiter
420 }
421 else if (isspacechar(chNext)) { // deprecated here-doc delimiter || TODO: left shift operator
422 }
423 else { // TODO: ???
424 }
425
426 } else if (HereDoc.State == 1) { // collect the delimiter
427 if (HereDoc.Quoted) { // a quoted here-doc delimiter
428 if (ch == HereDoc.Quote) { // closing quote => end of delimiter
429 styler.ColourTo(i, state);
430 state = SCE_PL_DEFAULT;
431 i++;
432 ch = chNext;
433 chNext = chNext2;
434 } else {
435 if (ch == '\\' && chNext == HereDoc.Quote) { // escaped quote
436 i++;
437 ch = chNext;
438 chNext = chNext2;
439 }
440 HereDoc.Delimiter[HereDoc.DelimiterLength++] = ch;
441 HereDoc.Delimiter[HereDoc.DelimiterLength] = '\0';
442 }
443 } else { // an unquoted here-doc delimiter
444 if (isalnum(ch) || ch == '_') {
445 HereDoc.Delimiter[HereDoc.DelimiterLength++] = ch;
446 HereDoc.Delimiter[HereDoc.DelimiterLength] = '\0';
447 } else {
448 styler.ColourTo(i - 1, state);
449 state = SCE_PL_DEFAULT;
450 }
451 }
452 if (HereDoc.DelimiterLength >= static_cast<int>(sizeof(HereDoc.Delimiter)) - 1) {
453 styler.ColourTo(i - 1, state);
454 state = SCE_PL_ERROR;
455 }
456 }
457 } else if (HereDoc.State == 2) {
458 // state == SCE_PL_HERE_Q || state == SCE_PL_HERE_QQ || state == SCE_PL_HERE_QX
459 if (isEOLChar(chPrev) && isMatch(styler, lengthDoc, i, HereDoc.Delimiter)) {
460 i += HereDoc.DelimiterLength;
461 chNext = styler.SafeGetCharAt(i);
462 if (isEOLChar(chNext)) {
463 styler.ColourTo(i - 1, state);
464 state = SCE_PL_DEFAULT;
465 HereDoc.State = 0;
466 }
467 ch = chNext;
468 chNext = styler.SafeGetCharAt(i + 1);
469 }
470 } else if (state == SCE_PL_POD) {
471 if (ch == '=' && isEOLChar(chPrev)) {
472 if (isMatch(styler, lengthDoc, i, "=cut")) {
473 styler.ColourTo(i - 1 + 4, state);
474 i += 4;
475 state = SCE_PL_DEFAULT;
476 ch = styler.SafeGetCharAt(i);
477 chNext = styler.SafeGetCharAt(i + 1);
478 }
479 }
480 } else if (state == SCE_PL_SCALAR) {
481 if (isEndVar(ch)) {
482 if (i == (styler.GetStartSegment() + 1)) {
483 // Special variable: $(, $_ etc.
484 styler.ColourTo(i, state);
485 } else {
486 styler.ColourTo(i - 1, state);
487 }
488 state = SCE_PL_DEFAULT;
489 }
490 } else if (state == SCE_PL_ARRAY) {
491 if (isEndVar(ch)) {
492 styler.ColourTo(i - 1, state);
493 state = SCE_PL_DEFAULT;
494 }
495 } else if (state == SCE_PL_HASH) {
496 if (isEndVar(ch)) {
497 styler.ColourTo(i - 1, state);
498 state = SCE_PL_DEFAULT;
499 }
500 } else if (state == SCE_PL_SYMBOLTABLE) {
501 if (isEndVar(ch)) {
502 styler.ColourTo(i - 1, state);
503 state = SCE_PL_DEFAULT;
504 }
505 } else if (state == SCE_PL_REGEX
506 || state == SCE_PL_STRING_QR
507 ) {
508 if (!Quote.Up && !isspacechar(ch)) {
509 Quote.Open(ch);
510 } else if (ch == '\\' && Quote.Up != '\\') {
511 // SG: Is it save to skip *every* escaped char?
512 i++;
513 ch = chNext;
514 chNext = styler.SafeGetCharAt(i + 1);
515 } else {
516 if (ch == Quote.Down /*&& chPrev != '\\'*/) {
517 Quote.Count--;
518 if (Quote.Count == 0) {
519 Quote.Rep--;
520 if (Quote.Up == Quote.Down) {
521 Quote.Count++;
522 }
523 }
524 if (!isalpha(chNext)) {
525 if (Quote.Rep <= 0) {
526 styler.ColourTo(i, state);
527 state = SCE_PL_DEFAULT;
528 ch = ' ';
529 }
530 }
531 } else if (ch == Quote.Up /*&& chPrev != '\\'*/) {
532 Quote.Count++;
533 } else if (!isalpha(chNext)) {
534 if (Quote.Rep <= 0) {
535 styler.ColourTo(i, state);
536 state = SCE_PL_DEFAULT;
537 ch = ' ';
538 }
539 }
540 }
541 } else if (state == SCE_PL_REGSUBST) {
542 if (!Quote.Up && !isspacechar(ch)) {
543 Quote.Open(ch);
544 } else if (ch == '\\' && Quote.Up != '\\') {
545 // SG: Is it save to skip *every* escaped char?
546 i++;
547 ch = chNext;
548 chNext = styler.SafeGetCharAt(i + 1);
549 } else {
550 if (Quote.Count == 0 && Quote.Rep == 1) {
551 /* We matched something like s(...) or tr{...}
552 * and are looking for the next matcher characters,
553 * which could be either bracketed ({...}) or non-bracketed
554 * (/.../).
555 *
556 * Number-signs are problematic. If they occur after
557 * the close of the first part, treat them like
558 * a Quote.Up char, even if they actually start comments.
559 *
560 * If we find an alnum, we end the regsubst, and punt.
561 *
562 * Eric Promislow ericp@activestate.com Aug 9,2000
563 */
564 if (isspacechar(ch)) {
565 // Keep going
566 }
567 else if (isalnum(ch)) {
568 styler.ColourTo(i, state);
569 state = SCE_PL_DEFAULT;
570 ch = ' ';
571 } else {
572 Quote.Open(ch);
573 }
574 } else if (ch == Quote.Down /*&& chPrev != '\\'*/) {
575 Quote.Count--;
576 if (Quote.Count == 0) {
577 Quote.Rep--;
578 }
579 if (!isalpha(chNext)) {
580 if (Quote.Rep <= 0) {
581 styler.ColourTo(i, state);
582 state = SCE_PL_DEFAULT;
583 ch = ' ';
584 }
585 }
586 if (Quote.Up == Quote.Down) {
587 Quote.Count++;
588 }
589 } else if (ch == Quote.Up /*&& chPrev != '\\'*/) {
590 Quote.Count++;
591 } else if (!isalpha(chNext)) {
592 if (Quote.Rep <= 0) {
593 styler.ColourTo(i, state);
594 state = SCE_PL_DEFAULT;
595 ch = ' ';
596 }
597 }
598 }
599 } else if (state == SCE_PL_STRING_Q
600 || state == SCE_PL_STRING_QQ
601 || state == SCE_PL_STRING_QX
602 || state == SCE_PL_STRING_QW
603 || state == SCE_PL_STRING
604 || state == SCE_PL_CHARACTER
605 || state == SCE_PL_BACKTICKS
606 ) {
607 if (!Quote.Down && !isspacechar(ch)) {
608 Quote.Open(ch);
609 } else if (ch == '\\' && Quote.Up != '\\') {
610 i++;
611 ch = chNext;
612 chNext = styler.SafeGetCharAt(i + 1);
613 } else if (ch == Quote.Down) {
614 Quote.Count--;
615 if (Quote.Count == 0) {
616 Quote.Rep--;
617 if (Quote.Rep <= 0) {
618 styler.ColourTo(i, state);
619 state = SCE_PL_DEFAULT;
620 ch = ' ';
621 }
622 if (Quote.Up == Quote.Down) {
623 Quote.Count++;
624 }
625 }
626 } else if (ch == Quote.Up) {
627 Quote.Count++;
628 }
629 }
630
631 if (state == SCE_PL_DEFAULT) { // One of the above succeeded
632 if (ch == '#') {
633 state = SCE_PL_COMMENTLINE;
634 } else if (ch == '\"') {
635 state = SCE_PL_STRING;
636 Quote.New(1);
637 Quote.Open(ch);
638 } else if (ch == '\'') {
639 state = SCE_PL_CHARACTER;
640 Quote.New(1);
641 Quote.Open(ch);
642 } else if (iswordstart(ch)) {
643 state = SCE_PL_WORD;
644 preferRE = false;
645 } else if (isPerlOperator(ch)) {
646 if (ch == ')' || ch == ']')
647 preferRE = false;
648 else
649 preferRE = true;
650 styler.ColourTo(i, SCE_PL_OPERATOR);
651 }
652 }
653 }
654 if (state == SCE_PL_ERROR) {
655 break;
656 }
657 chPrev = ch;
658 }
659 styler.ColourTo(lengthDoc - 1, state);
660 }
661
662 LexerModule lmPerl(SCLEX_PERL, ColourisePerlDoc, "perl");