shoes-highlighter 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 06db840874b7b465b090576dc13bba0274542c67
4
+ data.tar.gz: 0252050fbd61257f89648d4076fd41338b399e12
5
+ SHA512:
6
+ metadata.gz: ee92054f48c6b63d9618c38e3246c6e814fbdcbb6d90466c7b29ae272c1efe966db378299d3011b37060683d33697b67f3048f05d1ac71127adc5afbd8fa52c2
7
+ data.tar.gz: 7b4a67cded1f6f77106419e48e5b46e3ad4fa428a4123f95ed56ad0f194c79143cd9b46e96500fb5ecb37c4a7359da856d6492ba037da1539c6e577a8c7480e7
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in shoes-highlighter.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,31 @@
1
+ Copyright (C) 2011 Steve Klabnik, Peter Fitzgibbons
2
+ Copyright (c) 2008 why the lucky stiff
3
+ Except:
4
+ fonts/Coolvetica.ttf (c) 1999 Ray Larabie
5
+ fonts/Lacuna.ttf (c) 2003 Glashaus, designed by Peter Hoffman
6
+ samples/expert-minesweeper.rb (c) 2008 que
7
+ samples/expert-othello.rb (c) 2008 Tieg Zaharia
8
+ samples/expert-tankspank.rb (c) 2008 Kevin C.
9
+ samples/good-clock.rb (c) 2008 Thomas Bell
10
+ samples/good-reminder.rb (c) 2008 Oliver Smith
11
+
12
+ Permission is hereby granted, free of charge, to any person
13
+ obtaining a copy of this software and associated documentation
14
+ files (the "Software"), to deal in the Software without restriction,
15
+ including without limitation the rights to use, copy, modify, merge,
16
+ publish, distribute, sublicense, and/or sell copies of the Software,
17
+ and to permit persons to whom the Software is furnished to do so,
18
+ subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be
21
+ included in all copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
24
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
25
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
26
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
27
+ SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
28
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
29
+ OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
30
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
31
+ SOFTWARE.
@@ -0,0 +1,31 @@
1
+ # Shoes::Highlighter
2
+
3
+ A syntax highlighting gem, extracted from Shoes and Hackety Hack.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'shoes-highlighter'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install shoes-highlighter
20
+
21
+ ## Usage
22
+
23
+ See https://github.com/shoes/shoes4/blob/master/lib/shoes/ui/help.rb
24
+
25
+ ## Contributing
26
+
27
+ 1. Fork it ( https://github.com/shoes/shoes-highlighter/fork )
28
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
29
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
30
+ 4. Push to the branch (`git push origin my-new-feature`)
31
+ 5. Create a new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,8 @@
1
+ require "shoes/highlighter/version"
2
+ require "shoes/highlighter/common"
3
+ require "shoes/highlighter/markup"
4
+
5
+ class Shoes
6
+ module Highlighter
7
+ end
8
+ end
@@ -0,0 +1,192 @@
1
+ require 'strscan'
2
+
3
+ class Shoes
4
+ module Highlighter
5
+ module Syntax
6
+ # A single token extracted by a tokenizer. It is simply the lexeme
7
+ # itself, decorated with a 'group' attribute to identify the type of the
8
+ # lexeme.
9
+ class Token < String
10
+ # the type of the lexeme that was extracted.
11
+ attr_reader :group
12
+
13
+ # the instruction associated with this token (:none, :region_open, or
14
+ # :region_close)
15
+ attr_reader :instruction
16
+
17
+ # Create a new Token representing the given text, and belonging to the
18
+ # given group.
19
+ def initialize(text, group, instruction = :none)
20
+ super text
21
+ @group = group
22
+ @instruction = instruction
23
+ end
24
+ end
25
+
26
+ # The base class of all tokenizers. It sets up the scanner and manages the
27
+ # looping until all tokens have been extracted. It also provides convenience
28
+ # methods to make sure adjacent tokens of identical groups are returned as
29
+ # a single token.
30
+ class Tokenizer
31
+ # The current group being processed by the tokenizer
32
+ attr_reader :group
33
+
34
+ # The current chunk of text being accumulated
35
+ attr_reader :chunk
36
+
37
+ # Start tokenizing. This sets up the state in preparation for tokenization,
38
+ # such as creating a new scanner for the text and saving the callback block.
39
+ # The block will be invoked for each token extracted.
40
+ def start(text, &block)
41
+ @chunk = ""
42
+ @group = :normal
43
+ @callback = block
44
+ @text = StringScanner.new(text)
45
+ setup
46
+ end
47
+
48
+ # Subclasses may override this method to provide implementation-specific
49
+ # setup logic.
50
+ def setup
51
+ end
52
+
53
+ # Finish tokenizing. This flushes the buffer, yielding any remaining text
54
+ # to the client.
55
+ def finish
56
+ start_group nil
57
+ teardown
58
+ end
59
+
60
+ # Subclasses may override this method to provide implementation-specific
61
+ # teardown logic.
62
+ def teardown
63
+ end
64
+
65
+ # Subclasses must implement this method, which is called for each iteration
66
+ # of the tokenization process. This method may extract multiple tokens.
67
+ def step
68
+ fail NotImplementedError, "subclasses must implement #step"
69
+ end
70
+
71
+ # Begins tokenizing the given text, calling #step until the text has been
72
+ # exhausted.
73
+ def tokenize(text, &block)
74
+ start text, &block
75
+ step until @text.eos?
76
+ finish
77
+ end
78
+
79
+ # Specify a set of tokenizer-specific options. Each tokenizer may (or may
80
+ # not) publish any options, but if a tokenizer does those options may be
81
+ # used to specify optional behavior.
82
+ def set(opts = {})
83
+ (@options ||= {}).update opts
84
+ end
85
+
86
+ # Get the value of the specified option.
87
+ def option(opt)
88
+ @options ? @options[opt] : nil
89
+ end
90
+
91
+ private
92
+
93
+ EOL = /(?=\r\n?|\n|$)/
94
+
95
+ # A convenience for delegating method calls to the scanner.
96
+ def self.delegate(sym)
97
+ define_method(sym) { |*a| @text.__send__(sym, *a) }
98
+ end
99
+
100
+ delegate :bol?
101
+ delegate :eos?
102
+ delegate :scan
103
+ delegate :scan_until
104
+ delegate :check
105
+ delegate :check_until
106
+ delegate :getch
107
+ delegate :matched
108
+ delegate :pre_match
109
+ delegate :peek
110
+ delegate :pos
111
+
112
+ # Access the n-th subgroup from the most recent match.
113
+ def subgroup(n)
114
+ @text[n]
115
+ end
116
+
117
+ # Append the given data to the currently active chunk.
118
+ def append(data)
119
+ @chunk << data
120
+ end
121
+
122
+ # Request that a new group be started. If the current group is the same
123
+ # as the group being requested, a new group will not be created. If a new
124
+ # group is created and the current chunk is not empty, the chunk's
125
+ # contents will be yielded to the client as a token, and then cleared.
126
+ #
127
+ # After the new group is started, if +data+ is non-nil it will be appended
128
+ # to the chunk.
129
+ def start_group(gr, data = nil)
130
+ flush_chunk if gr != @group
131
+ @group = gr
132
+ @chunk << data if data
133
+ end
134
+
135
+ def start_region(gr, data = nil)
136
+ flush_chunk
137
+ @group = gr
138
+ @callback.call(Token.new(data || "", @group, :region_open))
139
+ end
140
+
141
+ def end_region(gr, data = nil)
142
+ flush_chunk
143
+ @group = gr
144
+ @callback.call(Token.new(data || "", @group, :region_close))
145
+ end
146
+
147
+ def flush_chunk
148
+ @callback.call(Token.new(@chunk, @group)) unless @chunk.empty?
149
+ @chunk = ""
150
+ end
151
+
152
+ def subtokenize(syntax, text)
153
+ tokenizer = Syntax.load(syntax)
154
+ tokenizer.set @options if @options
155
+ flush_chunk
156
+ tokenizer.tokenize(text, &@callback)
157
+ end
158
+ end
159
+
160
+ # A default tokenizer for handling syntaxes that are not explicitly handled
161
+ # elsewhere. It simply yields the given text as a single token.
162
+ class Default
163
+ # Yield the given text as a single token.
164
+ def tokenize(text)
165
+ yield Token.new(text, :normal)
166
+ end
167
+ end
168
+
169
+ # A hash for registering syntax implementations.
170
+ SYNTAX = Hash.new(Default)
171
+
172
+ # Load the implementation of the requested syntax. If the syntax cannot be
173
+ # found, or if it cannot be loaded for whatever reason, the Default syntax
174
+ # handler will be returned.
175
+ def load(syntax)
176
+ begin
177
+ require_relative "lang/#{syntax}"
178
+ rescue LoadError
179
+ end
180
+ SYNTAX[syntax].new
181
+ end
182
+ module_function :load
183
+
184
+ # Return an array of the names of supported syntaxes.
185
+ def all
186
+ lang_dir = File.join(File.dirname(__FILE__), "syntax", "lang")
187
+ Dir["#{lang_dir}/*.rb"].map { |path| File.basename(path, ".rb") }
188
+ end
189
+ module_function :all
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,316 @@
1
+ class Shoes
2
+ module Highlighter
3
+ module Syntax
4
+ # A tokenizer for the Ruby language. It recognizes all common syntax
5
+ # (and some less common syntax) but because it is not a true lexer, it
6
+ # will make mistakes on some ambiguous cases.
7
+ class Ruby < Tokenizer
8
+ # The list of all identifiers recognized as keywords.
9
+ KEYWORDS =
10
+ %w(if then elsif else end begin do rescue ensure while for
11
+ class module def yield raise until unless and or not when
12
+ case super undef break next redo retry in return alias
13
+ defined?)
14
+
15
+ # Perform ruby-specific setup
16
+ def setup
17
+ @selector = false
18
+ @allow_operator = false
19
+ @heredocs = []
20
+ end
21
+
22
+ # Step through a single iteration of the tokenization process.
23
+ def step
24
+ case
25
+ when bol? && check(/=begin/)
26
+ start_group(:comment, scan_until(/^=end#{EOL}/))
27
+ when bol? && check(/__END__#{EOL}/)
28
+ start_group(:comment, scan_until(/\Z/))
29
+ else
30
+ case
31
+ when check(/def\s+/)
32
+ start_group :keyword, scan(/def\s+/)
33
+ start_group :method, scan_until(/(?=[;(\s]|#{EOL})/)
34
+ when check(/class\s+/)
35
+ start_group :keyword, scan(/class\s+/)
36
+ start_group :class, scan_until(/(?=[;\s<]|#{EOL})/)
37
+ when check(/module\s+/)
38
+ start_group :keyword, scan(/module\s+/)
39
+ start_group :module, scan_until(/(?=[;\s]|#{EOL})/)
40
+ when check(/::/)
41
+ start_group :punct, scan(/::/)
42
+ when check(/:"/)
43
+ start_group :symbol, scan(/:/)
44
+ scan_delimited_region :symbol, :symbol, "", true
45
+ @allow_operator = true
46
+ when check(/:'/)
47
+ start_group :symbol, scan(/:/)
48
+ scan_delimited_region :symbol, :symbol, "", false
49
+ @allow_operator = true
50
+ when scan(/:[_a-zA-Z@$][$@\w]*[=!?]?/)
51
+ start_group :symbol, matched
52
+ @allow_operator = true
53
+ when scan(/\?(\\[^\n\r]|[^\\\n\r\s])/)
54
+ start_group :char, matched
55
+ @allow_operator = true
56
+ when check(/(__FILE__|__LINE__|true|false|nil|self)[?!]?/)
57
+ if @selector || matched[-1] == '?' || matched[-1] == '!'
58
+ start_group :ident,
59
+ scan(/(__FILE__|__LINE__|true|false|nil|self)[?!]?/)
60
+ else
61
+ start_group :constant,
62
+ scan(/(__FILE__|__LINE__|true|false|nil|self)/)
63
+ end
64
+ @selector = false
65
+ @allow_operator = true
66
+ when scan(/0([bB][01]+|[oO][0-7]+|[dD][0-9]+|[xX][0-9a-fA-F]+)/)
67
+ start_group :number, matched
68
+ @allow_operator = true
69
+ else
70
+ case peek(2)
71
+ when "%r"
72
+ scan_delimited_region :punct, :regex, scan(/../), true
73
+ @allow_operator = true
74
+ when "%w", "%q"
75
+ scan_delimited_region :punct, :string, scan(/../), false
76
+ @allow_operator = true
77
+ when "%s"
78
+ scan_delimited_region :punct, :symbol, scan(/../), false
79
+ @allow_operator = true
80
+ when "%W", "%Q", "%x"
81
+ scan_delimited_region :punct, :string, scan(/../), true
82
+ @allow_operator = true
83
+ when /%[^\sa-zA-Z0-9]/
84
+ scan_delimited_region :punct, :string, scan(/./), true
85
+ @allow_operator = true
86
+ when "<<"
87
+ saw_word = (chunk[-1, 1] =~ /[\w!?]/)
88
+ start_group :punct, scan(/<</)
89
+ if saw_word
90
+ @allow_operator = false
91
+ return
92
+ end
93
+
94
+ float_right = scan(/-/)
95
+ append "-" if float_right
96
+ if (type = scan(/['"]/))
97
+ append type
98
+ delim = scan_until(/(?=#{type})/)
99
+ if delim.nil?
100
+ append scan_until(/\Z/)
101
+ return
102
+ end
103
+ else
104
+ delim = scan(/\w+/) or return
105
+ end
106
+ start_group :constant, delim
107
+ start_group :punct, scan(/#{type}/) if type
108
+ @heredocs << [float_right, type, delim]
109
+ @allow_operator = true
110
+ else
111
+ case peek(1)
112
+ when /[\n\r]/
113
+ unless @heredocs.empty?
114
+ scan_heredoc(*@heredocs.shift)
115
+ else
116
+ start_group :normal, scan(/\s+/)
117
+ end
118
+ @allow_operator = false
119
+ when /\s/
120
+ start_group :normal, scan(/\s+/)
121
+ when "#"
122
+ start_group :comment, scan(/#[^\n\r]*/)
123
+ when /[A-Z]/
124
+ start_group @selector ? :ident : :constant, scan(/\w+/)
125
+ @allow_operator = true
126
+ when /[a-z_]/
127
+ word = scan(/\w+[?!]?/)
128
+ if !@selector && KEYWORDS.include?(word)
129
+ start_group :keyword, word
130
+ @allow_operator = false
131
+ elsif
132
+ start_group :ident, word
133
+ @allow_operator = true
134
+ end
135
+ @selector = false
136
+ when /\d/
137
+ start_group :number,
138
+ scan(/[\d_]+(\.[\d_]+)?([eE][\d_]+)?/)
139
+ @allow_operator = true
140
+ when '"'
141
+ scan_delimited_region :punct, :string, "", true
142
+ @allow_operator = true
143
+ when '/'
144
+ if @allow_operator
145
+ start_group :punct, scan(%r{/})
146
+ @allow_operator = false
147
+ else
148
+ scan_delimited_region :punct, :regex, "", true
149
+ @allow_operator = true
150
+ end
151
+ when "'"
152
+ scan_delimited_region :punct, :string, "", false
153
+ @allow_operator = true
154
+ when "."
155
+ dots = scan(/\.{1,3}/)
156
+ start_group :punct, dots
157
+ @selector = (dots.length == 1)
158
+ when /[@]/
159
+ start_group :attribute, scan(/@{1,2}\w*/)
160
+ @allow_operator = true
161
+ when /[$]/
162
+ start_group :global, scan(/\$/)
163
+ start_group :global, scan(/\w+|./) if check(/./)
164
+ @allow_operator = true
165
+ when /[-!?*\/+=<>(\[\{}:;,&|%]/
166
+ start_group :punct, scan(/./)
167
+ @allow_operator = false
168
+ when /[)\]]/
169
+ start_group :punct, scan(/./)
170
+ @allow_operator = true
171
+ else
172
+ # all else just falls through this, to prevent
173
+ # infinite loops...
174
+ append getch
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ private
182
+
183
+ # Scan a delimited region of text. This handles the simple cases (strings
184
+ # delimited with quotes) as well as the more complex cases of %-strings
185
+ # and here-documents.
186
+ #
187
+ # * +delim_group+ is the group to use to classify the delimiters of the
188
+ # region
189
+ # * +inner_group+ is the group to use to classify the contents of the
190
+ # region
191
+ # * +starter+ is the text to use as the starting delimiter
192
+ # * +exprs+ is a boolean flag indicating whether the region is an
193
+ # interpolated string or not
194
+ # * +delim+ is the text to use as the delimiter of the region. If +nil+,
195
+ # the next character will be treated as the delimiter.
196
+ # * +heredoc+ is either +false+, meaning the region is not a heredoc, or
197
+ # <tt>:flush</tt> (meaning the delimiter must be flushed left), or
198
+ # <tt>:float</tt> (meaning the delimiter doens't have to be flush left).
199
+ def scan_delimited_region(delim_group, inner_group, starter, exprs,
200
+ delim = nil, heredoc = false)
201
+ # begin
202
+ unless delim
203
+ start_group delim_group, starter
204
+ delim = scan(/./)
205
+ append delim
206
+
207
+ delim = case delim
208
+ when '{' then '}'
209
+ when '(' then ')'
210
+ when '[' then ']'
211
+ when '<' then '>'
212
+ else delim
213
+ end
214
+ end
215
+
216
+ start_region inner_group
217
+
218
+ items = "\\\\|"
219
+ if heredoc
220
+ items << "(^"
221
+ items << '\s*' if heredoc == :float
222
+ items << "#{Regexp.escape(delim)}\s*?)#{EOL}"
223
+ else
224
+ items << "#{Regexp.escape(delim)}"
225
+ end
226
+ items << "|#(\\$|@@?|\\{)" if exprs
227
+ items = Regexp.new(items)
228
+
229
+ loop do
230
+ p = pos
231
+ match = scan_until(items)
232
+ if match.nil?
233
+ start_group inner_group, scan_until(/\Z/)
234
+ break
235
+ else
236
+ text = pre_match[p..-1]
237
+ start_group inner_group, text if text.length > 0
238
+ case matched.strip
239
+ when "\\"
240
+ unless exprs
241
+ case peek(1)
242
+ when "'"
243
+ scan(/./)
244
+ start_group :escape, "\\'"
245
+ when "\\"
246
+ scan(/./)
247
+ start_group :escape, "\\\\"
248
+ else
249
+ start_group inner_group, "\\"
250
+ end
251
+ else
252
+ start_group :escape, "\\"
253
+ c = getch
254
+ append c
255
+ case c
256
+ when 'x'
257
+ append scan(/[a-fA-F0-9]{1,2}/)
258
+ when /[0-7]/
259
+ append scan(/[0-7]{0,2}/)
260
+ end
261
+ end
262
+ when delim
263
+ end_region inner_group
264
+ start_group delim_group, matched
265
+ break
266
+ when /^#/
267
+ do_highlight = (option(:expressions) == :highlight)
268
+ start_region :expr if do_highlight
269
+ start_group :expr, matched
270
+ case matched[1]
271
+ when '{'
272
+ depth = 1
273
+ content = ""
274
+ while depth > 0
275
+ p = pos
276
+ c = scan_until(/[\{}]/)
277
+ if c.nil?
278
+ content << scan_until(/\Z/)
279
+ break
280
+ else
281
+ depth += (matched == "{" ? 1 : -1)
282
+ content << pre_match[p..-1]
283
+ content << matched if depth > 0
284
+ end
285
+ end
286
+ if do_highlight
287
+ subtokenize "ruby", content
288
+ start_group :expr, "}"
289
+ else
290
+ append content + "}"
291
+ end
292
+ when '$', '@'
293
+ append scan(/\w+/)
294
+ end
295
+ end_region :expr if do_highlight
296
+ else fail "unexpected match on #{matched}"
297
+ end
298
+ end
299
+ end
300
+ end
301
+
302
+ # Scan a heredoc beginning at the current position.
303
+ #
304
+ # * +float+ indicates whether the delimiter may be floated to the right
305
+ # * +type+ is +nil+, a single quote, or a double quote
306
+ # * +delim+ is the delimiter to look for
307
+ def scan_heredoc(float, type, delim)
308
+ scan_delimited_region(:constant, :string, "", type != "'",
309
+ delim, float ? :float : :flush)
310
+ end
311
+ end
312
+
313
+ SYNTAX["ruby"] = Ruby
314
+ end
315
+ end
316
+ end
@@ -0,0 +1,209 @@
1
+ # syntax highlighting
2
+
3
+ class Shoes
4
+ module Highlighter
5
+ module Markup
6
+ TOKENIZER = Shoes::Highlighter::Syntax.load "ruby"
7
+ COLORS = {
8
+ comment: { stroke: "#887" },
9
+ keyword: { stroke: "#111" },
10
+ method: { stroke: "#C09", weight: "bold" },
11
+ # :class => {:stroke => "#0c4", :weight => "bold"},
12
+ # :module => {:stroke => "#050"},
13
+ # :punct => {:stroke => "#668", :weight => "bold"},
14
+ symbol: { stroke: "#C30" },
15
+ string: { stroke: "#C90" },
16
+ number: { stroke: "#396" },
17
+ regex: { stroke: "#000", fill: "#FFC" },
18
+ # :char => {:stroke => "#f07"},
19
+ attribute: { stroke: "#369" },
20
+ # :global => {:stroke => "#7FB" },
21
+ expr: { stroke: "#722" },
22
+ # :escape => {:stroke => "#277" }
23
+ ident: { stroke: "#994c99" },
24
+ constant: { stroke: "#630", weight: "bold" },
25
+ class: { stroke: "#630", weight: "bold" },
26
+ matching: { stroke: "#ff0", weight: "bold" },
27
+ }
28
+
29
+ def highlight(str, pos = nil, colors = COLORS)
30
+ tokens = []
31
+ TOKENIZER.tokenize(str) do |t|
32
+ if t.group == :punct
33
+ # split punctuation into single characters tokens
34
+ # TODO: to it in the parser
35
+ tokens += t.split('').map { |s| Shoes::Highlighter::Syntax::Token.new(s, :punct) }
36
+ else
37
+ # add token as is
38
+ tokens << t
39
+ end
40
+ end
41
+
42
+ res = []
43
+ tokens.each do |token|
44
+ res <<
45
+ if colors[token.group]
46
+ # span(token, colors[token.group])
47
+ tmp = fg(token, colors[token.group][:stroke])
48
+ colors[token.group][:fill] ? bg(tmp, colors[token.group][:fill]) : tmp
49
+ elsif colors[:any]
50
+ # span(token, colors[:any])
51
+ tmp = fg(token, colors[:any][:stroke])
52
+ colors[:any][:fill] ? bg(tmp, colors[:any][:fill]) : tmp
53
+ else
54
+ token
55
+ end
56
+ end
57
+
58
+ if pos.nil? || pos < 0
59
+ return res
60
+ end
61
+
62
+ token_index, matching_index = matching_token(tokens, pos)
63
+
64
+ if token_index
65
+ # res[token_index] = span(tokens[token_index], colors[:matching])
66
+ tmp = fg(tokens[token_index], colors[:matching][:stroke])
67
+ res[token_index] = colors[:matching][:fill] ? bg(tmp, colors[:matching][:fill]) : tmp
68
+ if matching_index
69
+ # res[matching_index] = span(tokens[matching_index], colors[:matching])
70
+ tmp = fg(tokens[matching_index], colors[:matching][:stroke])
71
+ res[matching_index] = colors[:matching][:fill] ? bg(tmp, colors[:matching][:fill]) : tmp
72
+ end
73
+ end
74
+
75
+ res
76
+ end
77
+
78
+ private
79
+
80
+ def matching_token(tokens, pos)
81
+ curr_pos = 0
82
+ token_index = nil
83
+ tokens.each_with_index do |t, i|
84
+ curr_pos += t.size
85
+ if token_index.nil? && curr_pos >= pos
86
+ token_index = i
87
+ break
88
+ end
89
+ end
90
+ if token_index.nil? then return nil end
91
+
92
+ match = matching_token_at_index(tokens, token_index)
93
+ if match.nil? && curr_pos == pos && token_index < tokens.size - 1
94
+ # try the token before the cursor, instead of the one after
95
+ token_index += 1
96
+ match = matching_token_at_index(tokens, token_index)
97
+ end
98
+
99
+ if match
100
+ [token_index, match]
101
+ else
102
+ nil
103
+ end
104
+ end
105
+
106
+ def matching_token_at_index(tokens, index)
107
+ starts, ends, direction = matching_tokens(tokens, index)
108
+ if starts.nil?
109
+ return nil
110
+ end
111
+
112
+ stack_level = 1
113
+ index += direction
114
+ while index >= 0 && index < tokens.size
115
+ # TODO separate space in the tokenizer
116
+ t = tokens[index].gsub(/\s/, '')
117
+ if ends.include?(t) && !as_modifier?(tokens, index)
118
+ stack_level -= 1
119
+ return index if stack_level == 0
120
+ elsif starts.include?(t) && !as_modifier?(tokens, index)
121
+ stack_level += 1
122
+ end
123
+ index += direction
124
+ end
125
+ # no matching token found
126
+ nil
127
+ end
128
+
129
+ # returns an array of tokens matching and the direction
130
+ def matching_tokens(tokens, index)
131
+ # TODO separate space in the tokenizer
132
+ token = tokens[index].gsub(/\s/, '')
133
+ starts = [token]
134
+ if OPEN_BRACKETS[token]
135
+ direction = 1
136
+ ends = [OPEN_BRACKETS[token]]
137
+ elsif CLOSE_BRACKETS[token]
138
+ direction = -1
139
+ ends = [CLOSE_BRACKETS[token]]
140
+ elsif OPEN_BLOCK.include?(token)
141
+ if as_modifier?(tokens, index)
142
+ return nil
143
+ end
144
+ direction = 1
145
+ ends = ['end']
146
+ starts = OPEN_BLOCK
147
+ elsif token == 'end'
148
+ direction = -1
149
+ ends = OPEN_BLOCK
150
+ else
151
+ return nil
152
+ end
153
+
154
+ [starts, ends, direction]
155
+ end
156
+
157
+ def as_modifier?(tokens, index)
158
+ unless MODIFIERS.include? tokens[index].gsub(/\s/, '')
159
+ return false
160
+ end
161
+
162
+ index -= 1
163
+ # find last index before the token that is no space
164
+ index -= 1 while index >= 0 && tokens[index] =~ /\A[ \t]*\z/
165
+
166
+ if index < 0
167
+ # first character of the string
168
+ false
169
+ elsif tokens[index] =~ /\n[ \t]*\Z/
170
+ # first token of the line
171
+ false
172
+ elsif tokens[index].group == :punct
173
+ # preceded by a punctuation token on the same line
174
+ i = tokens[index].rindex(/\S/)
175
+ punc = tokens[index][i, 1]
176
+ # true if the preceeding statement was terminating
177
+ !NON_TERMINATING.include?(punc)
178
+ else
179
+ # preceded by a non punctuation token on the same line
180
+ true
181
+ end
182
+ end
183
+
184
+ OPEN_BRACKETS = {
185
+ '{' => '}',
186
+ '(' => ')',
187
+ '[' => ']',
188
+ }
189
+
190
+ # close_bracket = {}
191
+ # OPEN_BRACKETS.each{|open, close| opens_bracket[close] = open}
192
+ # CLOSE_BRACKETS = opens_bracket
193
+ # the following is more readable :)
194
+ CLOSE_BRACKETS = {
195
+ '}' => '{',
196
+ ')' => '(',
197
+ ']' => '[',
198
+ }
199
+
200
+ BRACKETS = CLOSE_BRACKETS.keys + OPEN_BRACKETS.keys
201
+
202
+ OPEN_BLOCK = %w(def class module do if unless while until begin for)
203
+
204
+ MODIFIERS = %w(if unless while until)
205
+
206
+ NON_TERMINATING = %w{+ - * / , . = ~ < > ( [}
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,5 @@
1
+ class Shoes
2
+ module Highlighter
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require 'shoes/highlighter/version'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "shoes-highlighter"
9
+ s.version = Shoes::Highlighter::VERSION
10
+ s.platform = Gem::Platform::RUBY
11
+ s.authors = ["Team Shoes"]
12
+ s.email = ["shoes@librelist.com"]
13
+ s.homepage = "https://github.com/shoes/shoes4"
14
+ s.summary = 'A syntax highlighting library used by Shoes'
15
+ s.description = 'A syntax highlighting library used by Shoes. Originally extracted from Hackety-Hack.'
16
+ s.license = 'MIT'
17
+
18
+ s.files = `git ls-files`.split($/)
19
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_development_dependency "bundler", "~> 1.7"
23
+ s.add_development_dependency "rake", "~> 10.0"
24
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shoes-highlighter
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Team Shoes
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ description: A syntax highlighting library used by Shoes. Originally extracted from
42
+ Hackety-Hack.
43
+ email:
44
+ - shoes@librelist.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".gitignore"
50
+ - Gemfile
51
+ - LICENSE
52
+ - README.md
53
+ - Rakefile
54
+ - lib/shoes/highlighter.rb
55
+ - lib/shoes/highlighter/common.rb
56
+ - lib/shoes/highlighter/lang/ruby.rb
57
+ - lib/shoes/highlighter/markup.rb
58
+ - lib/shoes/highlighter/version.rb
59
+ - shoes-highlighter.gemspec
60
+ homepage: https://github.com/shoes/shoes4
61
+ licenses:
62
+ - MIT
63
+ metadata: {}
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubyforge_project:
80
+ rubygems_version: 2.4.2
81
+ signing_key:
82
+ specification_version: 4
83
+ summary: A syntax highlighting library used by Shoes
84
+ test_files: []