shoes-highlighter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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: []