rugments 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +52 -0
- data/README.md +195 -0
- data/bin/rugmentize +6 -0
- data/lib/rugments/cli.rb +357 -0
- data/lib/rugments/formatter.rb +29 -0
- data/lib/rugments/formatters/html.rb +142 -0
- data/lib/rugments/formatters/null.rb +17 -0
- data/lib/rugments/formatters/terminal256.rb +174 -0
- data/lib/rugments/lexer.rb +431 -0
- data/lib/rugments/lexers/apache/keywords.yml +453 -0
- data/lib/rugments/lexers/apache.rb +67 -0
- data/lib/rugments/lexers/apple_script.rb +366 -0
- data/lib/rugments/lexers/c.rb +210 -0
- data/lib/rugments/lexers/clojure.rb +109 -0
- data/lib/rugments/lexers/coffeescript.rb +172 -0
- data/lib/rugments/lexers/common_lisp.rb +343 -0
- data/lib/rugments/lexers/conf.rb +22 -0
- data/lib/rugments/lexers/cpp.rb +63 -0
- data/lib/rugments/lexers/csharp.rb +85 -0
- data/lib/rugments/lexers/css.rb +269 -0
- data/lib/rugments/lexers/dart.rb +102 -0
- data/lib/rugments/lexers/diff.rb +39 -0
- data/lib/rugments/lexers/elixir.rb +105 -0
- data/lib/rugments/lexers/erb.rb +54 -0
- data/lib/rugments/lexers/erlang.rb +116 -0
- data/lib/rugments/lexers/factor.rb +300 -0
- data/lib/rugments/lexers/gherkin/keywords.rb +13 -0
- data/lib/rugments/lexers/gherkin.rb +135 -0
- data/lib/rugments/lexers/go.rb +176 -0
- data/lib/rugments/lexers/groovy.rb +102 -0
- data/lib/rugments/lexers/haml.rb +226 -0
- data/lib/rugments/lexers/handlebars.rb +77 -0
- data/lib/rugments/lexers/haskell.rb +181 -0
- data/lib/rugments/lexers/html.rb +92 -0
- data/lib/rugments/lexers/http.rb +78 -0
- data/lib/rugments/lexers/ini.rb +55 -0
- data/lib/rugments/lexers/io.rb +66 -0
- data/lib/rugments/lexers/java.rb +74 -0
- data/lib/rugments/lexers/javascript.rb +258 -0
- data/lib/rugments/lexers/literate_coffeescript.rb +31 -0
- data/lib/rugments/lexers/literate_haskell.rb +34 -0
- data/lib/rugments/lexers/llvm.rb +82 -0
- data/lib/rugments/lexers/lua/builtins.rb +21 -0
- data/lib/rugments/lexers/lua.rb +120 -0
- data/lib/rugments/lexers/make.rb +114 -0
- data/lib/rugments/lexers/markdown.rb +151 -0
- data/lib/rugments/lexers/matlab/builtins.rb +10 -0
- data/lib/rugments/lexers/matlab.rb +70 -0
- data/lib/rugments/lexers/moonscript.rb +108 -0
- data/lib/rugments/lexers/nginx.rb +69 -0
- data/lib/rugments/lexers/nim.rb +149 -0
- data/lib/rugments/lexers/objective_c.rb +188 -0
- data/lib/rugments/lexers/ocaml.rb +109 -0
- data/lib/rugments/lexers/perl.rb +195 -0
- data/lib/rugments/lexers/php/builtins.rb +192 -0
- data/lib/rugments/lexers/php.rb +162 -0
- data/lib/rugments/lexers/plain_text.rb +23 -0
- data/lib/rugments/lexers/prolog.rb +62 -0
- data/lib/rugments/lexers/properties.rb +53 -0
- data/lib/rugments/lexers/puppet.rb +126 -0
- data/lib/rugments/lexers/python.rb +225 -0
- data/lib/rugments/lexers/qml.rb +70 -0
- data/lib/rugments/lexers/r.rb +55 -0
- data/lib/rugments/lexers/racket.rb +540 -0
- data/lib/rugments/lexers/ruby.rb +413 -0
- data/lib/rugments/lexers/rust.rb +188 -0
- data/lib/rugments/lexers/sass/common.rb +172 -0
- data/lib/rugments/lexers/sass.rb +72 -0
- data/lib/rugments/lexers/scala.rb +140 -0
- data/lib/rugments/lexers/scheme.rb +109 -0
- data/lib/rugments/lexers/scss.rb +32 -0
- data/lib/rugments/lexers/sed.rb +167 -0
- data/lib/rugments/lexers/shell.rb +150 -0
- data/lib/rugments/lexers/slim.rb +222 -0
- data/lib/rugments/lexers/smalltalk.rb +114 -0
- data/lib/rugments/lexers/sml.rb +345 -0
- data/lib/rugments/lexers/sql.rb +138 -0
- data/lib/rugments/lexers/swift.rb +153 -0
- data/lib/rugments/lexers/tcl.rb +189 -0
- data/lib/rugments/lexers/tex.rb +70 -0
- data/lib/rugments/lexers/toml.rb +68 -0
- data/lib/rugments/lexers/vb.rb +162 -0
- data/lib/rugments/lexers/viml/keywords.rb +11 -0
- data/lib/rugments/lexers/viml.rb +99 -0
- data/lib/rugments/lexers/xml.rb +57 -0
- data/lib/rugments/lexers/yaml.rb +362 -0
- data/lib/rugments/plugins/redcarpet.rb +28 -0
- data/lib/rugments/regex_lexer.rb +432 -0
- data/lib/rugments/template_lexer.rb +23 -0
- data/lib/rugments/text_analyzer.rb +46 -0
- data/lib/rugments/theme.rb +202 -0
- data/lib/rugments/themes/base16.rb +128 -0
- data/lib/rugments/themes/colorful.rb +65 -0
- data/lib/rugments/themes/github.rb +69 -0
- data/lib/rugments/themes/monokai.rb +88 -0
- data/lib/rugments/themes/monokai_sublime.rb +89 -0
- data/lib/rugments/themes/thankful_eyes.rb +69 -0
- data/lib/rugments/token.rb +180 -0
- data/lib/rugments/util.rb +99 -0
- data/lib/rugments/version.rb +3 -0
- data/lib/rugments.rb +33 -0
- metadata +149 -0
@@ -0,0 +1,432 @@
|
|
1
|
+
module Rugments
|
2
|
+
# @abstract
|
3
|
+
# A stateful lexer that uses sets of regular expressions to
|
4
|
+
# tokenize a string. Most lexers are instances of RegexLexer.
|
5
|
+
class RegexLexer < Lexer
|
6
|
+
# A rule is a tuple of a regular expression to test, and a callback
|
7
|
+
# to perform if the test succeeds.
|
8
|
+
#
|
9
|
+
# @see StateDSL#rule
|
10
|
+
class Rule
|
11
|
+
attr_reader :callback
|
12
|
+
attr_reader :re
|
13
|
+
attr_reader :beginning_of_line
|
14
|
+
def initialize(re, callback)
|
15
|
+
@re = re
|
16
|
+
@callback = callback
|
17
|
+
@beginning_of_line = re.source[0] == '^'
|
18
|
+
end
|
19
|
+
|
20
|
+
def inspect
|
21
|
+
"#<Rule #{@re.inspect}>"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# a State is a named set of rules that can be tested for or
|
26
|
+
# mixed in.
|
27
|
+
#
|
28
|
+
# @see RegexLexer.state
|
29
|
+
class State
|
30
|
+
attr_reader :name, :rules
|
31
|
+
def initialize(name, rules)
|
32
|
+
@name = name
|
33
|
+
@rules = rules
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect
|
37
|
+
"#<#{self.class.name} #{@name.inspect}>"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class StateDSL
|
42
|
+
attr_reader :rules
|
43
|
+
def initialize(name, &defn)
|
44
|
+
@name = name
|
45
|
+
@defn = defn
|
46
|
+
@rules = []
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_state(lexer_class)
|
50
|
+
load!
|
51
|
+
rules = @rules.map do |rule|
|
52
|
+
rule.is_a?(String) ? lexer_class.get_state(rule) : rule
|
53
|
+
end
|
54
|
+
State.new(@name, rules)
|
55
|
+
end
|
56
|
+
|
57
|
+
def prepended(&defn)
|
58
|
+
parent_defn = @defn
|
59
|
+
StateDSL.new(@name) do
|
60
|
+
instance_eval(&defn)
|
61
|
+
instance_eval(&parent_defn)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def appended(&defn)
|
66
|
+
parent_defn = @defn
|
67
|
+
StateDSL.new(@name) do
|
68
|
+
instance_eval(&parent_defn)
|
69
|
+
instance_eval(&defn)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
protected
|
74
|
+
|
75
|
+
# Define a new rule for this state.
|
76
|
+
#
|
77
|
+
# @overload rule(re, token, next_state=nil)
|
78
|
+
# @overload rule(re, &callback)
|
79
|
+
#
|
80
|
+
# @param [Regexp] re
|
81
|
+
# a regular expression for this rule to test.
|
82
|
+
# @param [String] tok
|
83
|
+
# the token type to yield if `re` matches.
|
84
|
+
# @param [#to_s] next_state
|
85
|
+
# (optional) a state to push onto the stack if `re` matches.
|
86
|
+
# If `next_state` is `:pop!`, the state stack will be popped
|
87
|
+
# instead.
|
88
|
+
# @param [Proc] callback
|
89
|
+
# a block that will be evaluated in the context of the lexer
|
90
|
+
# if `re` matches. This block has access to a number of lexer
|
91
|
+
# methods, including {RegexLexer#push}, {RegexLexer#pop!},
|
92
|
+
# {RegexLexer#token}, and {RegexLexer#delegate}. The first
|
93
|
+
# argument can be used to access the match groups.
|
94
|
+
def rule(re, tok = nil, next_state = nil, &callback)
|
95
|
+
if tok.nil? && callback.nil?
|
96
|
+
fail 'please pass `rule` a token to yield or a callback'
|
97
|
+
end
|
98
|
+
|
99
|
+
callback ||= case next_state
|
100
|
+
when :pop!
|
101
|
+
proc do |stream|
|
102
|
+
puts " yielding #{tok.qualname}, #{stream[0].inspect}" if @debug
|
103
|
+
@output_stream.call(tok, stream[0])
|
104
|
+
puts " popping stack: #{1}" if @debug
|
105
|
+
@stack.pop || fail('empty stack!')
|
106
|
+
end
|
107
|
+
when :push
|
108
|
+
proc do |stream|
|
109
|
+
puts " yielding #{tok.qualname}, #{stream[0].inspect}" if @debug
|
110
|
+
@output_stream.call(tok, stream[0])
|
111
|
+
puts " pushing #{@stack.last.name}" if @debug
|
112
|
+
@stack.push(@stack.last)
|
113
|
+
end
|
114
|
+
when Symbol
|
115
|
+
proc do |stream|
|
116
|
+
puts " yielding #{tok.qualname}, #{stream[0].inspect}" if @debug
|
117
|
+
@output_stream.call(tok, stream[0])
|
118
|
+
state = @states[next_state] || self.class.get_state(next_state)
|
119
|
+
puts " pushing #{state.name}" if @debug
|
120
|
+
@stack.push(state)
|
121
|
+
end
|
122
|
+
when nil
|
123
|
+
proc do |stream|
|
124
|
+
puts " yielding #{tok.qualname}, #{stream[0].inspect}" if @debug
|
125
|
+
@output_stream.call(tok, stream[0])
|
126
|
+
end
|
127
|
+
else
|
128
|
+
fail "invalid next state: #{next_state.inspect}"
|
129
|
+
end
|
130
|
+
|
131
|
+
rules << Rule.new(re, callback)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Mix in the rules from another state into this state. The rules
|
135
|
+
# from the mixed-in state will be tried in order before moving on
|
136
|
+
# to the rest of the rules in this state.
|
137
|
+
def mixin(state)
|
138
|
+
rules << state.to_s
|
139
|
+
end
|
140
|
+
|
141
|
+
private
|
142
|
+
|
143
|
+
def load!
|
144
|
+
return if @loaded
|
145
|
+
@loaded = true
|
146
|
+
instance_eval(&@defn)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# The states hash for this lexer.
|
151
|
+
# @see state
|
152
|
+
def self.states
|
153
|
+
@states ||= {}
|
154
|
+
end
|
155
|
+
|
156
|
+
def self.state_definitions
|
157
|
+
@state_definitions ||= InheritableHash.new(superclass.state_definitions)
|
158
|
+
end
|
159
|
+
@state_definitions = {}
|
160
|
+
|
161
|
+
def self.replace_state(name, new_defn)
|
162
|
+
states[name] = nil
|
163
|
+
state_definitions[name] = new_defn
|
164
|
+
end
|
165
|
+
|
166
|
+
# The routines to run at the beginning of a fresh lex.
|
167
|
+
# @see start
|
168
|
+
def self.start_procs
|
169
|
+
@start_procs ||= InheritableList.new(superclass.start_procs)
|
170
|
+
end
|
171
|
+
@start_procs = []
|
172
|
+
|
173
|
+
# Specify an action to be run every fresh lex.
|
174
|
+
#
|
175
|
+
# @example
|
176
|
+
# start { puts "I'm lexing a new string!" }
|
177
|
+
def self.start(&b)
|
178
|
+
start_procs << b
|
179
|
+
end
|
180
|
+
|
181
|
+
# Define a new state for this lexer with the given name.
|
182
|
+
# The block will be evaluated in the context of a {StateDSL}.
|
183
|
+
def self.state(name, &b)
|
184
|
+
name = name.to_s
|
185
|
+
state_definitions[name] = StateDSL.new(name, &b)
|
186
|
+
end
|
187
|
+
|
188
|
+
def self.prepend(name, &b)
|
189
|
+
name = name.to_s
|
190
|
+
dsl = state_definitions[name] or fail "no such state #{name.inspect}"
|
191
|
+
replace_state(name, dsl.prepended(&b))
|
192
|
+
end
|
193
|
+
|
194
|
+
def self.append(_state, &b)
|
195
|
+
name = name.to_s
|
196
|
+
dsl = state_definitions[name] or fail "no such state #{name.inspect}"
|
197
|
+
replace_state(name, dsl.appended(&b))
|
198
|
+
end
|
199
|
+
|
200
|
+
# @private
|
201
|
+
def self.get_state(name)
|
202
|
+
return name if name.is_a? State
|
203
|
+
|
204
|
+
states[name.to_sym] ||= begin
|
205
|
+
defn = state_definitions[name.to_s] or fail "unknown state: #{name.inspect}"
|
206
|
+
defn.to_state(self)
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# @private
|
211
|
+
def get_state(state_name)
|
212
|
+
self.class.get_state(state_name)
|
213
|
+
end
|
214
|
+
|
215
|
+
# The state stack. This is initially the single state `[:root]`.
|
216
|
+
# It is an error for this stack to be empty.
|
217
|
+
# @see #state
|
218
|
+
def stack
|
219
|
+
@stack ||= [get_state(:root)]
|
220
|
+
end
|
221
|
+
|
222
|
+
# The current state - i.e. one on top of the state stack.
|
223
|
+
#
|
224
|
+
# NB: if the state stack is empty, this will throw an error rather
|
225
|
+
# than returning nil.
|
226
|
+
def state
|
227
|
+
stack.last || fail('empty stack!')
|
228
|
+
end
|
229
|
+
|
230
|
+
# reset this lexer to its initial state. This runs all of the
|
231
|
+
# start_procs.
|
232
|
+
def reset!
|
233
|
+
@stack = nil
|
234
|
+
@current_stream = nil
|
235
|
+
|
236
|
+
self.class.start_procs.each do |pr|
|
237
|
+
instance_eval(&pr)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# This implements the lexer protocol, by yielding [token, value] pairs.
|
242
|
+
#
|
243
|
+
# The process for lexing works as follows, until the stream is empty:
|
244
|
+
#
|
245
|
+
# 1. We look at the state on top of the stack (which by default is
|
246
|
+
# `[:root]`).
|
247
|
+
# 2. Each rule in that state is tried until one is successful. If one
|
248
|
+
# is found, that rule's callback is evaluated - which may yield
|
249
|
+
# tokens and manipulate the state stack. Otherwise, one character
|
250
|
+
# is consumed with an `'Error'` token, and we continue at (1.)
|
251
|
+
#
|
252
|
+
# @see #step #step (where (2.) is implemented)
|
253
|
+
def stream_tokens(str, &b)
|
254
|
+
stream = StringScanner.new(str)
|
255
|
+
|
256
|
+
@current_stream = stream
|
257
|
+
@output_stream = b
|
258
|
+
@states = self.class.states
|
259
|
+
@null_steps = 0
|
260
|
+
|
261
|
+
until stream.eos?
|
262
|
+
if @debug
|
263
|
+
puts "lexer: #{self.class.tag}"
|
264
|
+
puts "stack: #{stack.map(&:name).inspect}"
|
265
|
+
puts "stream: #{stream.peek(20).inspect}"
|
266
|
+
end
|
267
|
+
|
268
|
+
success = step(state, stream)
|
269
|
+
|
270
|
+
unless success
|
271
|
+
puts ' no match, yielding Error' if @debug
|
272
|
+
b.call(Token::Tokens::Error, stream.getch)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# The number of successive scans permitted without consuming
|
278
|
+
# the input stream. If this is exceeded, the match fails.
|
279
|
+
MAX_NULL_SCANS = 5
|
280
|
+
|
281
|
+
# Runs one step of the lex. Rules in the current state are tried
|
282
|
+
# until one matches, at which point its callback is called.
|
283
|
+
#
|
284
|
+
# @return true if a rule was tried successfully
|
285
|
+
# @return false otherwise.
|
286
|
+
def step(state, stream)
|
287
|
+
state.rules.each do |rule|
|
288
|
+
if rule.is_a?(State)
|
289
|
+
puts " entering mixin #{rule.name}" if @debug
|
290
|
+
return true if step(rule, stream)
|
291
|
+
puts " exiting mixin #{rule.name}" if @debug
|
292
|
+
else
|
293
|
+
puts " trying #{rule.inspect}" if @debug
|
294
|
+
|
295
|
+
# XXX HACK XXX
|
296
|
+
# StringScanner's implementation of ^ is b0rken.
|
297
|
+
# see http://bugs.ruby-lang.org/issues/7092
|
298
|
+
# TODO: this doesn't cover cases like /(a|^b)/, but it's
|
299
|
+
# the most common, for now...
|
300
|
+
next if rule.beginning_of_line && !stream.beginning_of_line?
|
301
|
+
|
302
|
+
if size = stream.skip(rule.re)
|
303
|
+
puts " got #{stream[0].inspect}" if @debug
|
304
|
+
|
305
|
+
instance_exec(stream, &rule.callback)
|
306
|
+
|
307
|
+
if size.zero?
|
308
|
+
@null_steps += 1
|
309
|
+
if @null_steps > MAX_NULL_SCANS
|
310
|
+
puts ' too many scans without consuming the string!' if @debug
|
311
|
+
return false
|
312
|
+
end
|
313
|
+
else
|
314
|
+
@null_steps = 0
|
315
|
+
end
|
316
|
+
|
317
|
+
return true
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
false
|
323
|
+
end
|
324
|
+
|
325
|
+
# Yield a token.
|
326
|
+
#
|
327
|
+
# @param tok
|
328
|
+
# the token type
|
329
|
+
# @param val
|
330
|
+
# (optional) the string value to yield. If absent, this defaults
|
331
|
+
# to the entire last match.
|
332
|
+
def token(tok, val = @current_stream[0])
|
333
|
+
yield_token(tok, val)
|
334
|
+
end
|
335
|
+
|
336
|
+
# Yield tokens corresponding to the matched groups of the current
|
337
|
+
# match.
|
338
|
+
def groups(*tokens)
|
339
|
+
tokens.each_with_index do |tok, i|
|
340
|
+
yield_token(tok, @current_stream[i + 1])
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# Delegate the lex to another lexer. The #lex method will be called
|
345
|
+
# with `:continue` set to true, so that #reset! will not be called.
|
346
|
+
# In this way, a single lexer can be repeatedly delegated to while
|
347
|
+
# maintaining its own internal state stack.
|
348
|
+
#
|
349
|
+
# @param [#lex] lexer
|
350
|
+
# The lexer or lexer class to delegate to
|
351
|
+
# @param [String] text
|
352
|
+
# The text to delegate. This defaults to the last matched string.
|
353
|
+
def delegate(lexer, text = nil)
|
354
|
+
puts " delegating to #{lexer.inspect}" if @debug
|
355
|
+
text ||= @current_stream[0]
|
356
|
+
|
357
|
+
lexer.lex(text, continue: true) do |tok, val|
|
358
|
+
puts " delegated token: #{tok.inspect}, #{val.inspect}" if @debug
|
359
|
+
yield_token(tok, val)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
def recurse(text = nil)
|
364
|
+
delegate(self.class, text)
|
365
|
+
end
|
366
|
+
|
367
|
+
# Push a state onto the stack. If no state name is given and you've
|
368
|
+
# passed a block, a state will be dynamically created using the
|
369
|
+
# {StateDSL}.
|
370
|
+
def push(state_name = nil, &b)
|
371
|
+
push_state = if state_name
|
372
|
+
get_state(state_name)
|
373
|
+
elsif block_given?
|
374
|
+
StateDSL.new(b.inspect, &b).to_state(self.class)
|
375
|
+
else
|
376
|
+
# use the top of the stack by default
|
377
|
+
state
|
378
|
+
end
|
379
|
+
|
380
|
+
puts " pushing #{push_state.name}" if @debug
|
381
|
+
stack.push(push_state)
|
382
|
+
end
|
383
|
+
|
384
|
+
# Pop the state stack. If a number is passed in, it will be popped
|
385
|
+
# that number of times.
|
386
|
+
def pop!(times = 1)
|
387
|
+
fail 'empty stack!' if stack.empty?
|
388
|
+
|
389
|
+
puts " popping stack: #{times}" if @debug
|
390
|
+
|
391
|
+
stack.pop(times)
|
392
|
+
|
393
|
+
nil
|
394
|
+
end
|
395
|
+
|
396
|
+
# replace the head of the stack with the given state
|
397
|
+
def goto(state_name)
|
398
|
+
fail 'empty stack!' if stack.empty?
|
399
|
+
|
400
|
+
puts " going to state #{state_name} " if @debug
|
401
|
+
stack[-1] = get_state(state_name)
|
402
|
+
end
|
403
|
+
|
404
|
+
# reset the stack back to `[:root]`.
|
405
|
+
def reset_stack
|
406
|
+
puts ' resetting stack' if @debug
|
407
|
+
stack.clear
|
408
|
+
stack.push get_state(:root)
|
409
|
+
end
|
410
|
+
|
411
|
+
# Check if `state_name` is in the state stack.
|
412
|
+
def in_state?(state_name)
|
413
|
+
state_name = state_name.to_s
|
414
|
+
stack.any? do |state|
|
415
|
+
state.name == state_name.to_s
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# Check if `state_name` is the state on top of the state stack.
|
420
|
+
def state?(state_name)
|
421
|
+
state_name.to_s == state.name
|
422
|
+
end
|
423
|
+
|
424
|
+
private
|
425
|
+
|
426
|
+
def yield_token(tok, val)
|
427
|
+
return if val.nil? || val.empty?
|
428
|
+
puts " yielding #{tok.qualname}, #{val.inspect}" if @debug
|
429
|
+
@output_stream.yield(tok, val)
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Rugments
|
2
|
+
# @abstract
|
3
|
+
# A TemplateLexer is one that accepts a :parent option, to specify
|
4
|
+
# which language is being templated. The lexer class can specify its
|
5
|
+
# own default for the parent lexer, which is otherwise defaulted to
|
6
|
+
# HTML.
|
7
|
+
class TemplateLexer < RegexLexer
|
8
|
+
# the parent lexer - the one being templated.
|
9
|
+
def parent
|
10
|
+
return @parent if instance_variable_defined? :@parent
|
11
|
+
@parent = option(:parent) || 'html'
|
12
|
+
if @parent.is_a? ::String
|
13
|
+
lexer_class = Lexer.find(@parent)
|
14
|
+
@parent = lexer_class.new(options)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
start { parent.reset! }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
lib_path = File.expand_path(File.dirname(__FILE__))
|
23
|
+
Dir.glob(File.join(lib_path, 'lexers/*.rb')) { |f| require_relative f }
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Rugments
|
2
|
+
class TextAnalyzer < String
|
3
|
+
# Find a shebang. Returns nil if no shebang is present.
|
4
|
+
def shebang
|
5
|
+
return @shebang if instance_variable_defined? :@shebang
|
6
|
+
|
7
|
+
self =~ /\A\s*#!(.*)$/
|
8
|
+
@shebang = $1
|
9
|
+
end
|
10
|
+
|
11
|
+
# Check if the given shebang is present.
|
12
|
+
#
|
13
|
+
# This normalizes things so that `text.shebang?('bash')` will detect
|
14
|
+
# `#!/bash`, '#!/bin/bash', '#!/usr/bin/env bash', and '#!/bin/bash -x'
|
15
|
+
def shebang?(match)
|
16
|
+
match = /\b#{match}(\s|$)/
|
17
|
+
match === shebang
|
18
|
+
end
|
19
|
+
|
20
|
+
# Return the contents of the doctype tag if present, nil otherwise.
|
21
|
+
def doctype
|
22
|
+
return @doctype if instance_variable_defined? :@doctype
|
23
|
+
|
24
|
+
self =~ %r(\A\s*
|
25
|
+
(?:<\?.*?\?>\s*)? # possible <?xml...?> tag
|
26
|
+
<!DOCTYPE\s+(.+?)>
|
27
|
+
)xm
|
28
|
+
@doctype = $1
|
29
|
+
end
|
30
|
+
|
31
|
+
# Check if the doctype matches a given regexp or string
|
32
|
+
def doctype?(type=//)
|
33
|
+
type === doctype
|
34
|
+
end
|
35
|
+
|
36
|
+
# Return true if the result of lexing with the given lexer contains no
|
37
|
+
# error tokens.
|
38
|
+
def lexes_cleanly?(lexer)
|
39
|
+
lexer.lex(self) do |(tok, _)|
|
40
|
+
return false if tok.name == 'Error'
|
41
|
+
end
|
42
|
+
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,202 @@
|
|
1
|
+
module Rugments
|
2
|
+
class Theme
|
3
|
+
include Token::Tokens
|
4
|
+
|
5
|
+
class Style < Hash
|
6
|
+
def initialize(theme, hsh = {})
|
7
|
+
super()
|
8
|
+
@theme = theme
|
9
|
+
merge!(hsh)
|
10
|
+
end
|
11
|
+
|
12
|
+
[:fg, :bg].each do |mode|
|
13
|
+
define_method mode do
|
14
|
+
return self[mode] unless @theme
|
15
|
+
@theme.palette(self[mode]) if self[mode]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def render(selector, &b)
|
20
|
+
return enum_for(:render, selector).to_a.join("\n") unless b
|
21
|
+
|
22
|
+
return if empty?
|
23
|
+
|
24
|
+
yield "#{selector} {"
|
25
|
+
rendered_rules.each do |rule|
|
26
|
+
yield " #{rule};"
|
27
|
+
end
|
28
|
+
yield '}'
|
29
|
+
end
|
30
|
+
|
31
|
+
def rendered_rules(&b)
|
32
|
+
return enum_for(:rendered_rules) unless b
|
33
|
+
yield "color: #{fg}" if fg
|
34
|
+
yield "background-color: #{bg}" if bg
|
35
|
+
yield 'font-weight: bold' if self[:bold]
|
36
|
+
yield 'font-style: italic' if self[:italic]
|
37
|
+
yield 'text-decoration: underline' if self[:underline]
|
38
|
+
|
39
|
+
(self[:rules] || []).each(&b)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def styles
|
44
|
+
@styles ||= self.class.styles.dup
|
45
|
+
end
|
46
|
+
|
47
|
+
@palette = {}
|
48
|
+
def self.palette(arg = {})
|
49
|
+
@palette ||= InheritableHash.new(superclass.palette)
|
50
|
+
|
51
|
+
if arg.is_a? Hash
|
52
|
+
@palette.merge! arg
|
53
|
+
@palette
|
54
|
+
else
|
55
|
+
case arg
|
56
|
+
when /#[0-9a-f]+/i
|
57
|
+
arg
|
58
|
+
else
|
59
|
+
@palette[arg] || fail("not in palette: #{arg.inspect}")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
@styles = {}
|
65
|
+
def self.styles
|
66
|
+
@styles ||= InheritableHash.new(superclass.styles)
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.render(opts = {}, &b)
|
70
|
+
new(opts).render(&b)
|
71
|
+
end
|
72
|
+
|
73
|
+
class << self
|
74
|
+
def style(*tokens)
|
75
|
+
style = tokens.last.is_a?(Hash) ? tokens.pop : {}
|
76
|
+
|
77
|
+
style = Style.new(self, style)
|
78
|
+
|
79
|
+
tokens.each do |tok|
|
80
|
+
styles[tok] = style
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def get_own_style(token)
|
85
|
+
token.token_chain.each do |anc|
|
86
|
+
return styles[anc] if styles[anc]
|
87
|
+
end
|
88
|
+
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_style(token)
|
93
|
+
get_own_style(token) || base_style
|
94
|
+
end
|
95
|
+
|
96
|
+
def base_style
|
97
|
+
styles[Token::Tokens::Text]
|
98
|
+
end
|
99
|
+
|
100
|
+
def name(n = nil)
|
101
|
+
return @name if n.nil?
|
102
|
+
|
103
|
+
@name = n.to_s
|
104
|
+
Theme.registry[@name] = self
|
105
|
+
end
|
106
|
+
|
107
|
+
def find(n)
|
108
|
+
registry[n.to_s]
|
109
|
+
end
|
110
|
+
|
111
|
+
def registry
|
112
|
+
@registry ||= {}
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
module HasModes
|
118
|
+
def mode(arg = :absent)
|
119
|
+
return @mode if arg == :absent
|
120
|
+
|
121
|
+
@modes ||= {}
|
122
|
+
@modes[arg] ||= get_mode(arg)
|
123
|
+
end
|
124
|
+
|
125
|
+
def get_mode(mode)
|
126
|
+
return self if self.mode == mode
|
127
|
+
|
128
|
+
new_name = "#{name}.#{mode}"
|
129
|
+
Class.new(self) { name(new_name); mode!(mode) }
|
130
|
+
end
|
131
|
+
|
132
|
+
def mode!(arg)
|
133
|
+
@mode = arg
|
134
|
+
send("make_#{arg}!")
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class CSSTheme < Theme
|
139
|
+
def initialize(opts = {})
|
140
|
+
@scope = opts[:scope] || '.highlight'
|
141
|
+
end
|
142
|
+
|
143
|
+
def render(&b)
|
144
|
+
return enum_for(:render).to_a.join("\n") unless b
|
145
|
+
|
146
|
+
# shared styles for tableized line numbers
|
147
|
+
yield "#{@scope} table td { padding: 5px; }"
|
148
|
+
yield "#{@scope} table pre { margin: 0; }"
|
149
|
+
|
150
|
+
styles.each do |tok, style|
|
151
|
+
style.render(css_selector(tok), &b)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def render_base(selector, &b)
|
156
|
+
self.class.base_style.render(selector, &b)
|
157
|
+
end
|
158
|
+
|
159
|
+
def style_for(tok)
|
160
|
+
self.class.get_style(tok)
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
def css_selector(token)
|
166
|
+
inflate_token(token).map do |tok|
|
167
|
+
fail "unknown token: #{tok.inspect}" if tok.shortname.nil?
|
168
|
+
|
169
|
+
single_css_selector(tok)
|
170
|
+
end.join(', ')
|
171
|
+
end
|
172
|
+
|
173
|
+
def single_css_selector(token)
|
174
|
+
return @scope if token == Text
|
175
|
+
|
176
|
+
"#{@scope} .#{token.shortname}"
|
177
|
+
end
|
178
|
+
|
179
|
+
# yield all of the tokens that should be styled the same
|
180
|
+
# as the given token. Essentially this recursively all of
|
181
|
+
# the subtokens, except those which are more specifically
|
182
|
+
# styled.
|
183
|
+
def inflate_token(tok, &b)
|
184
|
+
return enum_for(:inflate_token, tok) unless block_given?
|
185
|
+
|
186
|
+
yield tok
|
187
|
+
tok.sub_tokens.each do |(_, st)|
|
188
|
+
next if styles[st]
|
189
|
+
|
190
|
+
inflate_token(st, &b)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
require_relative 'themes/thankful_eyes'
|
198
|
+
require_relative 'themes/colorful'
|
199
|
+
require_relative 'themes/base16'
|
200
|
+
require_relative 'themes/github'
|
201
|
+
require_relative 'themes/monokai'
|
202
|
+
require_relative 'themes/monokai_sublime'
|