A stylesheet language written in TypeScript that compiles to CSS
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

lexer.ts 4.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import { Error } from './error';
  2. import Token, { TokenTypes } from './token';
  3. export interface LexerError {}
  4. export class LexerError implements Error {
  5. public line: number;
  6. public message: string;
  7. public module?: string;
  8. public constructor(line: number, message: string, module?: string) {
  9. this.line = line;
  10. this.message = message;
  11. this.module = module;
  12. }
  13. }
  14. export type LexerResult = { tokens: Token[]; module?: string } | LexerError;
  15. export default class Lexer {
  16. private source: string;
  17. private module?: string;
  18. private line: number;
  19. private position: number;
  20. private tokens: Token[];
  21. private allowWhitespace: boolean;
  22. public constructor(source: string, module?: string) {
  23. this.source = source;
  24. this.module = module;
  25. this.line = 1;
  26. this.position = 0;
  27. this.tokens = [];
  28. this.allowWhitespace = true;
  29. }
  30. public scan(): LexerResult {
  31. while (this.position < this.source.length) {
  32. const result = this.getToken();
  33. if (!(result instanceof Token)) return result;
  34. if (
  35. result.type !== TokenTypes.WHITESPACE &&
  36. result.type !== TokenTypes.COMMENT
  37. ) {
  38. this.tokens.push(result);
  39. }
  40. if (result.type !== TokenTypes.LITERAL) {
  41. this.position += result.value.length;
  42. }
  43. if (this.hasSigil(result)) {
  44. this.position += 1;
  45. }
  46. }
  47. this.tokens.push(this.token(TokenTypes.EOF, 'eof'));
  48. return {
  49. tokens: this.tokens,
  50. module: this.module,
  51. };
  52. }
  53. private getToken(): Token | LexerError {
  54. const c = this.currentChar();
  55. const source = this.source.slice(this.position);
  56. if (c === '(') {
  57. this.allowWhitespace = true;
  58. return this.token(TokenTypes.LPAREN, '(');
  59. } else if (c === ')') {
  60. this.allowWhitespace = true;
  61. return this.token(TokenTypes.RPAREN, ')');
  62. } else if (c === ',') {
  63. return this.token(TokenTypes.COMMA, ',');
  64. } else if (c === ':') {
  65. const match = source.match(/^:([a-z][a-zA-Z0-9_-]*)/);
  66. if (match === null) {
  67. return this.error(
  68. `Unexpected character ${this.source[this.position + 1]}`
  69. );
  70. }
  71. return this.token(TokenTypes.PROPERTY, match[1]);
  72. } else if (c === '$') {
  73. const match = source.match(/^\$([a-z][a-zA-Z0-9_-]*)/);
  74. if (match === null) {
  75. return this.error(
  76. `Unexpected character ${this.source[this.position + 1]}`
  77. );
  78. }
  79. return this.token(TokenTypes.IDENTIFIER, match[1]);
  80. } else if (c === '@') {
  81. this.allowWhitespace = false;
  82. const match = source.match(/^\@([a-z][a-zA-Z0-9_-]*)/);
  83. if (match === null) {
  84. return this.error(
  85. `Unexpected character ${this.source[this.position + 1]}`
  86. );
  87. }
  88. return this.token(TokenTypes.FUNCTION_NAME, match[1]);
  89. } else if (c.match(/^\s/)) {
  90. if (c === '\n') this.line += 1;
  91. return this.token(TokenTypes.WHITESPACE, c);
  92. } else if (c === ';') {
  93. while (this.currentChar() !== '\n') {
  94. this.position++;
  95. }
  96. return this.token(TokenTypes.COMMENT, '');
  97. } else {
  98. let literal = '';
  99. const endPattern = this.allowWhitespace ? /^[\(\)\n\,]/ : /^[\(\)\n\, ]/;
  100. while (
  101. this.position < this.source.length &&
  102. !this.source.slice(this.position).match(/^ [\:\$][a-z]/) &&
  103. !this.source.slice(this.position).match(endPattern) &&
  104. !(this.currentChar() === ' ' && this.nextChar() === '(')
  105. ) {
  106. literal += this.currentChar();
  107. this.position += 1;
  108. }
  109. return this.token(TokenTypes.LITERAL, literal);
  110. }
  111. }
  112. private token(type: TokenTypes, value: string): Token {
  113. return new Token(type, value, this.line, this.module);
  114. }
  115. private error(message: string): LexerError {
  116. return new LexerError(this.line, message, this.module);
  117. }
  118. private currentChar(): string {
  119. return this.source[this.position];
  120. }
  121. private nextChar(): string {
  122. return this.source[this.position + 1];
  123. }
  124. private hasSigil(token: Token): boolean {
  125. return [
  126. TokenTypes.IDENTIFIER,
  127. TokenTypes.PROPERTY,
  128. TokenTypes.FUNCTION_NAME,
  129. ].includes(token.type);
  130. }
  131. }