Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 373
- Log:
Initial import of Radiant 0.9.1, which is now packaged as a gem. This is an
import of the tagged 0.9.1 source checked out from GitHub, which isn't quite
the same as the gem distribution - but it doesn't seem to be available in an
archived form and the installed gem already has modifications, so this is
the closest I can get.
- Author:
- rool
- Date:
- Mon Mar 21 13:40:05 +0000 2011
- Size:
- 5918 Bytes
1 | require 'strscan' |
2 | |
3 | module Sass |
4 | module Script |
5 | # The lexical analyzer for SassScript. |
6 | # It takes a raw string and converts it to individual tokens |
7 | # that are easier to parse. |
8 | class Lexer |
9 | # A struct containing information about an individual token. |
10 | # |
11 | # `type`: \[`Symbol`\] |
12 | # : The type of token. |
13 | # |
14 | # `value`: \[`Object`\] |
15 | # : The Ruby object corresponding to the value of the token. |
16 | # |
17 | # `line`: \[`Fixnum`\] |
18 | # : The line of the source file on which the token appears. |
19 | # |
20 | # `offset`: \[`Fixnum`\] |
21 | # : The number of bytes into the line the SassScript token appeared. |
22 | Token = Struct.new(:type, :value, :line, :offset) |
23 | |
24 | # A hash from operator strings to the corresponding token types. |
25 | # @private |
26 | OPERATORS = { |
27 | '+' => :plus, |
28 | '-' => :minus, |
29 | '*' => :times, |
30 | '/' => :div, |
31 | '%' => :mod, |
32 | '=' => :single_eq, |
33 | '(' => :lparen, |
34 | ')' => :rparen, |
35 | ',' => :comma, |
36 | 'and' => :and, |
37 | 'or' => :or, |
38 | 'not' => :not, |
39 | '==' => :eq, |
40 | '!=' => :neq, |
41 | '>=' => :gte, |
42 | '<=' => :lte, |
43 | '>' => :gt, |
44 | '<' => :lt, |
45 | '#{' => :begin_interpolation, |
46 | '}' => :end_interpolation, |
47 | } |
48 | |
49 | # A list of operator strings ordered with longer names first |
50 | # so that `>` and `<` don't clobber `>=` and `<=`. |
51 | # @private |
52 | OP_NAMES = OPERATORS.keys.sort_by {|o| -o.size} |
53 | |
54 | # A hash of regular expressions that are used for tokenizing. |
55 | # @private |
56 | REGULAR_EXPRESSIONS = { |
57 | :whitespace => /\s*/, |
58 | :variable => /!(\w+)/, |
59 | :ident => /(\\.|[^\s\\+\-*\/%(),=!])+/, |
60 | :string_end => /((?:\\.|\#(?!\{)|[^"\\#])*)(?:"|(?=#\{))/, |
61 | :number => /(-)?(?:(\d*\.\d+)|(\d+))([a-zA-Z%]+)?/, |
62 | :color => /\##{"([0-9a-fA-F]{1,2})" * 3}|(#{Color::HTML4_COLORS.keys.join("|")})/, |
63 | :bool => /(true|false)\b/, |
64 | :op => %r{(#{Regexp.union(*OP_NAMES.map{|s| Regexp.new(Regexp.escape(s) + (s =~ /\w$/ ? '(?:\b|$)' : ''))})})} |
65 | } |
66 | |
67 | # @param str [String, StringScanner] The source text to lex |
68 | # @param line [Fixnum] The line on which the SassScript appears. |
69 | # Used for error reporting |
70 | # @param offset [Fixnum] The number of characters in on which the SassScript appears. |
71 | # Used for error reporting |
72 | def initialize(str, line, offset, filename) |
73 | @scanner = str.is_a?(StringScanner) ? str : StringScanner.new(str) |
74 | @line = line |
75 | @offset = offset |
76 | @filename = filename |
77 | @prev = nil |
78 | end |
79 | |
80 | # Moves the lexer forward one token. |
81 | # |
82 | # @return [Token] The token that was moved past |
83 | def next |
84 | @tok ||= read_token |
85 | @tok, tok = nil, @tok |
86 | @prev = tok |
87 | return tok |
88 | end |
89 | |
90 | # Returns the next token without moving the lexer forward. |
91 | # |
92 | # @return [Token] The next token |
93 | def peek |
94 | @tok ||= read_token |
95 | end |
96 | |
97 | # @return [Boolean] Whether or not there's more source text to lex. |
98 | def done? |
99 | whitespace unless after_interpolation? |
100 | @scanner.eos? && @tok.nil? |
101 | end |
102 | |
103 | private |
104 | |
105 | def read_token |
106 | return if done? |
107 | |
108 | value = token |
109 | unless value |
110 | raise SyntaxError.new("Syntax error in '#{@scanner.string}' at character #{current_position}.") |
111 | end |
112 | Token.new(value.first, value.last, @line, last_match_position) |
113 | end |
114 | |
115 | def whitespace |
116 | @scanner.scan(REGULAR_EXPRESSIONS[:whitespace]) |
117 | end |
118 | |
119 | def token |
120 | return string('') if after_interpolation? |
121 | variable || string || number || color || bool || op || ident |
122 | end |
123 | |
124 | def variable |
125 | return unless @scanner.scan(REGULAR_EXPRESSIONS[:variable]) |
126 | [:const, @scanner[1]] |
127 | end |
128 | |
129 | def ident |
130 | return unless s = @scanner.scan(REGULAR_EXPRESSIONS[:ident]) |
131 | [:ident, s.gsub(/\\(.)/, '\1')] |
132 | end |
133 | |
134 | def string(start_char = '"') |
135 | return unless @scanner.scan(/#{start_char}#{REGULAR_EXPRESSIONS[:string_end]}/) |
136 | [:string, Script::String.new(@scanner[1].gsub(/\\([^0-9a-f])/, '\1').gsub(/\\([0-9a-f]{1,4})/, "\\\\\\1"))] |
137 | end |
138 | |
139 | def begin_interpolation |
140 | @scanner.scan |
141 | end |
142 | |
143 | def number |
144 | return unless @scanner.scan(REGULAR_EXPRESSIONS[:number]) |
145 | value = @scanner[2] ? @scanner[2].to_f : @scanner[3].to_i |
146 | value = -value if @scanner[1] |
147 | [:number, Script::Number.new(value, Array(@scanner[4]))] |
148 | end |
149 | |
150 | def color |
151 | return unless @scanner.scan(REGULAR_EXPRESSIONS[:color]) |
152 | value = if @scanner[4] |
153 | color = Color::HTML4_COLORS[@scanner[4].downcase] |
154 | else |
155 | (1..3).map {|i| @scanner[i]}.map {|num| num.ljust(2, num).to_i(16)} |
156 | end |
157 | [:color, Script::Color.new(value)] |
158 | end |
159 | |
160 | def bool |
161 | return unless s = @scanner.scan(REGULAR_EXPRESSIONS[:bool]) |
162 | [:bool, Script::Bool.new(s == 'true')] |
163 | end |
164 | |
165 | def op |
166 | prev_chr = @scanner.string[@scanner.pos - 1].chr |
167 | return unless op = @scanner.scan(REGULAR_EXPRESSIONS[:op]) |
168 | if @prev && op == '-' && prev_chr !~ /\s/ && |
169 | [:bool, :ident, :const].include?(@prev.type) |
170 | warn(<<END) |
171 | DEPRECATION WARNING: |
172 | On line #{@line}, character #{last_match_position}#{" of '#{@filename}'" if @filename} |
173 | - will be allowed as part of variable names in version 3.0. |
174 | Please add whitespace to separate it from the previous token. |
175 | END |
176 | end |
177 | |
178 | [OPERATORS[op]] |
179 | end |
180 | |
181 | def current_position |
182 | @offset + @scanner.pos + 1 |
183 | end |
184 | |
185 | def last_match_position |
186 | current_position - @scanner.matched_size |
187 | end |
188 | |
189 | def after_interpolation? |
190 | @prev && @prev.type == :end_interpolation |
191 | end |
192 | end |
193 | end |
194 | end |