cecil 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +25 -0
- data/.tool-versions +1 -0
- data/.yard/README.md +492 -0
- data/.yardopts +2 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +81 -0
- data/LICENSE.txt +21 -0
- data/README.md +492 -0
- data/Rakefile +46 -0
- data/lib/cecil/block_context.rb +150 -0
- data/lib/cecil/builder.rb +66 -0
- data/lib/cecil/code.rb +335 -0
- data/lib/cecil/content_for.rb +27 -0
- data/lib/cecil/indentation.rb +131 -0
- data/lib/cecil/lang/typescript.rb +95 -0
- data/lib/cecil/node.rb +397 -0
- data/lib/cecil/placeholder.rb +31 -0
- data/lib/cecil/text.rb +112 -0
- data/lib/cecil/version.rb +5 -0
- data/lib/cecil.rb +11 -0
- data/sig/cecil.rbs +4 -0
- metadata +70 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
require_relative "content_for"
|
2
|
+
require_relative "node"
|
3
|
+
|
4
|
+
require "forwardable"
|
5
|
+
|
6
|
+
module Cecil
|
7
|
+
# @!visibility private
|
8
|
+
class Builder
|
9
|
+
attr_accessor :root, :syntax
|
10
|
+
|
11
|
+
extend Forwardable
|
12
|
+
def_delegators :@content_for, :content_for, :content_for!, :content_for?
|
13
|
+
|
14
|
+
def initialize(syntax_class)
|
15
|
+
@syntax = syntax_class.new
|
16
|
+
@helpers = syntax_class::Helpers
|
17
|
+
|
18
|
+
@root = Node::RootNode.new(self)
|
19
|
+
@active_nodes = [@root]
|
20
|
+
|
21
|
+
@content_for = ContentFor.new(
|
22
|
+
store: method(:detached_node),
|
23
|
+
place: method(:reattach_nodes),
|
24
|
+
defer: method(:defer)
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def build(&block)
|
29
|
+
@block_context = BlockContext.new(block.binding.receiver, self, @helpers)
|
30
|
+
@block_context.instance_exec(&block)
|
31
|
+
|
32
|
+
root
|
33
|
+
.evaluate!
|
34
|
+
.stringify
|
35
|
+
.lstrip
|
36
|
+
end
|
37
|
+
|
38
|
+
def detached_node(&) = Node::Detached.new(root, &)
|
39
|
+
|
40
|
+
def reattach_nodes(detached_nodes)
|
41
|
+
container = Node::Container.new(parent: current_node) {} # rubocop:disable Lint/EmptyBlock
|
42
|
+
detached_nodes.each { _1.attach_to container }
|
43
|
+
add_node container
|
44
|
+
container
|
45
|
+
end
|
46
|
+
|
47
|
+
def current_node = @active_nodes.last || raise("Not inside a Cecil block")
|
48
|
+
def replace_node(...) = current_node.replace_child(...)
|
49
|
+
|
50
|
+
def build_node(node)
|
51
|
+
@active_nodes.push node
|
52
|
+
yield
|
53
|
+
ensure
|
54
|
+
@active_nodes.pop
|
55
|
+
end
|
56
|
+
|
57
|
+
def src(src) = add_node current_node.build_child(src:)
|
58
|
+
|
59
|
+
def defer(&) = add_node Node::Deferred.new(parent: current_node, &)
|
60
|
+
|
61
|
+
def add_node(child)
|
62
|
+
current_node.add_child child
|
63
|
+
child
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/cecil/code.rb
ADDED
@@ -0,0 +1,335 @@
|
|
1
|
+
require_relative "placeholder"
|
2
|
+
require_relative "indentation"
|
3
|
+
|
4
|
+
module Cecil
|
5
|
+
# {Code} serves as the base class for generating source code using Cecil.
|
6
|
+
# Subclassing {Code} allows customizing the behavior (indentation, auto-closing brackets, etc) and providing helpers.
|
7
|
+
#
|
8
|
+
# - Override {Code} instance methods to change behavior.
|
9
|
+
# - Defined a module named `Helpers` in your subclass to add methods available in the Cecil block.
|
10
|
+
#
|
11
|
+
# Check out classes in the {Lang} module for examples of customizing {Code}.
|
12
|
+
#
|
13
|
+
# @example Creating a custom syntax
|
14
|
+
# class CSS < Cecil::Code
|
15
|
+
# # Override instance methods to customize behavior
|
16
|
+
#
|
17
|
+
# def indent_chars = " " # use 2 spaces for indentation
|
18
|
+
#
|
19
|
+
# # methods in this module will be available in a Cecil block
|
20
|
+
# module Helpers
|
21
|
+
#
|
22
|
+
# # if we want to inherit other helpers, include the module
|
23
|
+
# include Cecil::Code::Helpers
|
24
|
+
#
|
25
|
+
# def data_uri(file) = DataURI.from_file(file) # fake code
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# background_types = {
|
30
|
+
# star: "star-@100x100.png",
|
31
|
+
# dots: "polka-dots@50x50.png",
|
32
|
+
# }
|
33
|
+
#
|
34
|
+
# CSS.generate_string do
|
35
|
+
# background_types.each do |bg_name, image_file|
|
36
|
+
# `.bg-$class {`[bg_name] do
|
37
|
+
#
|
38
|
+
# # #data_uri is available because it was defined in CSS::Helpers
|
39
|
+
# `background-image: url($img);`[data_uri(image_file)]
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# # outputs:
|
45
|
+
# # .bg-star {
|
46
|
+
# # background-image: url(data:image/png;base64,iRxVB0…);
|
47
|
+
# # }
|
48
|
+
# # .bg-dots {
|
49
|
+
# # background-image: url(data:image/png;base64,iRxVB0…);
|
50
|
+
# # }
|
51
|
+
|
52
|
+
class Code
|
53
|
+
class << self
|
54
|
+
# Generates output by executing the given block and writing its return value to the provided output buffer/stream.
|
55
|
+
#
|
56
|
+
# The stream is written to by calling `#<<` with the generated source code.
|
57
|
+
#
|
58
|
+
# @param [#<<] out The output buffer/stream to write to
|
59
|
+
# @yield The given block can use backticks (i.e. {BlockContext#src `` #`(code_str) ``} ) to add lines of code to
|
60
|
+
# the buffer/stream.
|
61
|
+
# @return The returned value of `out <<`
|
62
|
+
#
|
63
|
+
# @example Outputing to stdout
|
64
|
+
# Cecil.generate do
|
65
|
+
# `function helloWorld() {}`
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# @example Outputing to a file
|
69
|
+
# File.open "output.js", "w" do |file|
|
70
|
+
# Cecil.generate file do
|
71
|
+
# `function helloWorld() {}`
|
72
|
+
# end
|
73
|
+
# end
|
74
|
+
def generate(out = $stdout, &) = Cecil.generate(syntax_class: self, out:, &)
|
75
|
+
|
76
|
+
# Generates output and returns it as a string
|
77
|
+
#
|
78
|
+
# @yield (see .generate)
|
79
|
+
# @return [String] The generated source code
|
80
|
+
# @see .generate
|
81
|
+
# @example
|
82
|
+
# my_code = Cecil.generate_string do
|
83
|
+
# `function helloWorld() {}`
|
84
|
+
# end
|
85
|
+
# puts my_code
|
86
|
+
def generate_string(&) = generate("", &)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Subclasses of {Code} can define a module named `Helpers` and add methods to it that will be available inside a
|
90
|
+
# Cecil block for that subclass.
|
91
|
+
#
|
92
|
+
# When defining your own `Helpers` module, if you want your parent class' helper methods, then `include` your parent
|
93
|
+
# class' `Helpers` module in yours, like this:
|
94
|
+
#
|
95
|
+
# class CSS < Code::Syntax
|
96
|
+
# module Helpers
|
97
|
+
# include Code::Syntax::Helpers
|
98
|
+
#
|
99
|
+
# def data_uri(file) = DataURI.from_file(file) # this is made up, not working code
|
100
|
+
#
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# class SCSS < CSS
|
105
|
+
# module Helpers
|
106
|
+
# include CSS::Helpers # now `data_uri` will be an available helper
|
107
|
+
# end
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
# Subclasses that don't define a `Helpers` module inherit the one from their parent class.
|
111
|
+
module Helpers
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns the string to use for each level of indentation. Default is 4 spaces.
|
115
|
+
#
|
116
|
+
# To turn off indentation, override this method to return an empty string.
|
117
|
+
#
|
118
|
+
# @return [String] the string to use for each level of indentation. Default is 4 spaces.
|
119
|
+
#
|
120
|
+
# @example Use tab for indentation
|
121
|
+
# class MySyntax < Cecil::Code
|
122
|
+
# def indent_chars = "\t"
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# @example Use 2 spaces for indentation
|
126
|
+
# class MySyntax < Cecil::Code
|
127
|
+
# def indent_chars = " "
|
128
|
+
# end
|
129
|
+
def indent_chars = " "
|
130
|
+
|
131
|
+
# When indenting with a code block, the end of the code string is searched for consecutive opening brackets, each of
|
132
|
+
# which gets closed with a matching closing bracket.
|
133
|
+
#
|
134
|
+
# E.g.
|
135
|
+
#
|
136
|
+
# `my_func( (<`[] do
|
137
|
+
# `more code`
|
138
|
+
# end
|
139
|
+
# # outputs:
|
140
|
+
# # my_func((<
|
141
|
+
# # more code
|
142
|
+
# # >) )
|
143
|
+
#
|
144
|
+
# @return [Hash{String => String}] Pairs of opening/closing strings
|
145
|
+
#
|
146
|
+
# @example Override to close only `{` and `[` brackets.
|
147
|
+
# class MySyntax < Cecil::Code
|
148
|
+
# def block_ending_pairs
|
149
|
+
# {
|
150
|
+
# "{" => "}",
|
151
|
+
# "[" => "]",
|
152
|
+
#
|
153
|
+
# " " => " ", # allows for "my_func { [ " to be closed with " ] } "
|
154
|
+
# "\t" => "\t" # allows for "my_func\t{[\t" to be closed with "\t]}\t"
|
155
|
+
# }
|
156
|
+
# end
|
157
|
+
# end
|
158
|
+
#
|
159
|
+
# @example Override to also close `/*` with `*/`
|
160
|
+
# class MySyntax < Cecil::Code
|
161
|
+
# def block_ending_pairs = super.merge({ '/*' => '*/' })
|
162
|
+
# end
|
163
|
+
#
|
164
|
+
# @example Override to turn this feature off, and don't close open brackets
|
165
|
+
# class MySyntax < Cecil::Code
|
166
|
+
# def block_ending_pairs = {}
|
167
|
+
# end
|
168
|
+
def block_ending_pairs
|
169
|
+
{
|
170
|
+
"{" => "}",
|
171
|
+
"[" => "]",
|
172
|
+
"<" => ">",
|
173
|
+
"(" => ")",
|
174
|
+
|
175
|
+
" " => " ", # allows for "my_func ( [ " to be closed with " ] ) "
|
176
|
+
"\t" => "\t" # allows for "my_func\t([\t" to be closed with "\t])\t"
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
180
|
+
# Pairs that can be used to surround placeholder names. The pairs that are used do not change the placeholder's
|
181
|
+
# name.
|
182
|
+
#
|
183
|
+
# E.g., these all produce the same result:
|
184
|
+
#
|
185
|
+
# `const $field`[field: 'username']
|
186
|
+
# `const ${field}`[field: 'username']
|
187
|
+
# `const $[field]`[field: 'username']
|
188
|
+
# `const $<field>`[field: 'username']
|
189
|
+
# `const $(field)`[field: 'username']
|
190
|
+
#
|
191
|
+
# By default, `"" => ""` is one of the pairs, meaning you don't need to surround placeholder names.
|
192
|
+
#
|
193
|
+
# @return [Regexp]
|
194
|
+
#
|
195
|
+
# @example Override to allow `$/my_field/` syntax for placeholders, in addition to the default options
|
196
|
+
# class MySyntax < Cecil::Code
|
197
|
+
# def placeholder_delimiting_pairs = super.merge("/" => "/")
|
198
|
+
# end
|
199
|
+
#
|
200
|
+
# @example Override to turn off placeholder delimiting pairs (i.e. only allow `$my_field` syntax)
|
201
|
+
# class MySyntax < Cecil::Code
|
202
|
+
# def placeholder_delimiting_pairs = { "" => "" }
|
203
|
+
# # or
|
204
|
+
# def placeholder_delimiting_pairs = Cecil::Code::PLACEHOLDER_NO_BRACKETS_PAIR
|
205
|
+
# end
|
206
|
+
def placeholder_delimiting_pairs
|
207
|
+
{
|
208
|
+
"{" => "}",
|
209
|
+
"[" => "]",
|
210
|
+
"<" => ">",
|
211
|
+
"(" => ")",
|
212
|
+
**PLACEHOLDER_NO_BRACKETS_PAIR # this needs to be last
|
213
|
+
}
|
214
|
+
end
|
215
|
+
PLACEHOLDER_NO_BRACKETS_PAIR = { "" => "" }.freeze
|
216
|
+
|
217
|
+
# Regexp to use to match a placeholder's name.
|
218
|
+
#
|
219
|
+
# @return [Regexp]
|
220
|
+
#
|
221
|
+
# @example Override to only allow all-caps placeholders (e.g. `$MY_FIELD`)
|
222
|
+
# class MySyntax < Cecil::Code
|
223
|
+
# def placeholder_ident_re = /[A-Z_]+/
|
224
|
+
# end
|
225
|
+
#
|
226
|
+
# @example Override to allow any characters placeholders, and require brackets (e.g. `${ my field ??! :) }`)
|
227
|
+
# class MySyntax < Cecil::Code
|
228
|
+
# # override `#placeholder_delimiting_pairs` to allow the default
|
229
|
+
# # brackets but not allow no brackets
|
230
|
+
# def placeholder_delimiting_pairs = super.except("")
|
231
|
+
# def placeholder_ident_re = /.+/
|
232
|
+
# end
|
233
|
+
def placeholder_ident_re = /[[:alnum:]_]+/
|
234
|
+
|
235
|
+
# Regexp to match a placeholder's starting character(s).
|
236
|
+
#
|
237
|
+
# @return [Regexp]
|
238
|
+
#
|
239
|
+
# @example Override to make placeholders start with `%`, e.g. `%myField`
|
240
|
+
# class MySyntax < Cecil::Code
|
241
|
+
# def placeholder_start_re = /%/
|
242
|
+
# end
|
243
|
+
#
|
244
|
+
# @example Override to make placeholders be all-caps without starting characters (e.g. `MY_FIELD`)
|
245
|
+
# class MySyntax < Cecil::Code
|
246
|
+
# def placeholder_start_re = //
|
247
|
+
# end
|
248
|
+
def placeholder_start_re = /\$/
|
249
|
+
|
250
|
+
# Regexp to match placeholders. By default, this constructs a Regexp from the pieces defined in:
|
251
|
+
#
|
252
|
+
# - {#placeholder_delimiting_pairs}
|
253
|
+
# - {#placeholder_ident_re}
|
254
|
+
# - {#placeholder_start_re}
|
255
|
+
#
|
256
|
+
# If you override this method, make sure it returns a Regexp that has a capture group named "placeholder".
|
257
|
+
#
|
258
|
+
# @return [Regexp] A regexp with a capture group named "placeholder"
|
259
|
+
def placeholder_re
|
260
|
+
/
|
261
|
+
#{placeholder_start_re}
|
262
|
+
#{Regexp.union(
|
263
|
+
placeholder_delimiting_pairs.map do |pstart, pend|
|
264
|
+
/
|
265
|
+
#{Regexp.quote pstart}
|
266
|
+
(?<placeholder>#{placeholder_ident_re})
|
267
|
+
#{Regexp.quote pend}
|
268
|
+
/x
|
269
|
+
end
|
270
|
+
)}
|
271
|
+
/x
|
272
|
+
end
|
273
|
+
|
274
|
+
# Returns a list of {Placeholder} objects representing placeholders found in the given string. The default
|
275
|
+
# implementation scans the string for matches of {#placeholder_re}.
|
276
|
+
#
|
277
|
+
# This method can be overriden to change the way placeholders are parsed, or to omit, add, or modify placeholders.
|
278
|
+
#
|
279
|
+
# @return [Array<Placeholder>]
|
280
|
+
#
|
281
|
+
# @example Override to transform placeholder names to lowercase
|
282
|
+
# class MySyntax < Cecil::Code
|
283
|
+
# super.map do |placeholder|
|
284
|
+
# placeholder.transform_key(:ident, &:downcase)
|
285
|
+
# end
|
286
|
+
# end
|
287
|
+
#
|
288
|
+
# MySyntax.generate_string do
|
289
|
+
# `const $VAR = $VALUE`[var: 'id', value: '42']
|
290
|
+
# end
|
291
|
+
# # outputs:
|
292
|
+
# # const id = 42
|
293
|
+
def scan_for_placeholders(src)
|
294
|
+
Text.scan_for_re_matches(src, placeholder_re)
|
295
|
+
.map do |match|
|
296
|
+
Placeholder.new(match[:placeholder], *match.offset(0))
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# What do to in case of ambiguous indentation.
|
301
|
+
#
|
302
|
+
# 2 examples of ambiguous indentation:
|
303
|
+
#
|
304
|
+
# `def python_fn():
|
305
|
+
# pass`
|
306
|
+
#
|
307
|
+
# `def ruby_method
|
308
|
+
# end`
|
309
|
+
#
|
310
|
+
# Because only the second line strings have leading indentation, we don't know how `pass` or `end` should be
|
311
|
+
# indented.
|
312
|
+
#
|
313
|
+
# In the future we could use `caller` to identify the source location of that line and read the ruby file to figure
|
314
|
+
# out the indentation.
|
315
|
+
#
|
316
|
+
# For now, though, you can return:
|
317
|
+
#
|
318
|
+
# - {Indentation::Ambiguity.raise_error}
|
319
|
+
# - {Indentation::Ambiguity.ignore} (works for the Ruby example)
|
320
|
+
# - {Indentation::Ambiguity.adjust_by} (works for the Python example)
|
321
|
+
#
|
322
|
+
# @example Override to ignore ambiguous indentation
|
323
|
+
# class MyRubySyntax < Cecil::Code
|
324
|
+
# def handle_ambiguous_indentation = Indentation::Ambiguity.ignore
|
325
|
+
# end
|
326
|
+
#
|
327
|
+
# @example Override to adjust indentation
|
328
|
+
# class MyRubySyntax < Cecil::Code
|
329
|
+
# def handle_ambiguous_indentation
|
330
|
+
# Indentation::Ambiguity.adjust_by(2)
|
331
|
+
# end
|
332
|
+
# end
|
333
|
+
def handle_ambiguous_indentation = Indentation::Ambiguity.raise_error
|
334
|
+
end
|
335
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Cecil
|
2
|
+
# @!visibility private
|
3
|
+
class ContentFor
|
4
|
+
def initialize(store:, place:, defer:)
|
5
|
+
@store = store
|
6
|
+
@place = place
|
7
|
+
@defer = defer
|
8
|
+
|
9
|
+
@content = Hash.new { |hash, key| hash[key] = [] }
|
10
|
+
end
|
11
|
+
|
12
|
+
def content_for(key, &)
|
13
|
+
if block_given?
|
14
|
+
@content[key] << @store.call(&)
|
15
|
+
nil # so that users don't get access to the array of content
|
16
|
+
elsif content_for?(key)
|
17
|
+
content_for!(key)
|
18
|
+
else
|
19
|
+
@defer.call { content_for!(key) }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def content_for?(key) = @content.key?(key)
|
24
|
+
|
25
|
+
def content_for!(key) = @place.call(@content.fetch(key))
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
module Cecil
|
2
|
+
module Indentation
|
3
|
+
module_function
|
4
|
+
|
5
|
+
# @!visibility private
|
6
|
+
def line_level(str) = str.index(/[^\t ]/) || str.length
|
7
|
+
|
8
|
+
# @!visibility private
|
9
|
+
def levels(lines) = lines.map { line_level(_1) }
|
10
|
+
|
11
|
+
# @!visibility private
|
12
|
+
def level__basic(src) = levels(src.lines.grep(/\S/)).min
|
13
|
+
|
14
|
+
module Ambiguity
|
15
|
+
module_function
|
16
|
+
|
17
|
+
# When given an ambiguously indented string, it assumes that first line is `adjustment` characters less than the
|
18
|
+
# least indented of the other lines.
|
19
|
+
#
|
20
|
+
# Useful for this situation. Setting to `adjust_by(4)` will behave
|
21
|
+
# according to what's intended.
|
22
|
+
#
|
23
|
+
# `def python_fn():
|
24
|
+
# pass`
|
25
|
+
def adjust_by(adjustment) = ->(min_level:, **) { min_level - adjustment }
|
26
|
+
|
27
|
+
# When given an ambiguously indented string, assume that first line is the same as the least indented of the other
|
28
|
+
# lines.
|
29
|
+
#
|
30
|
+
# Useful for this situation:
|
31
|
+
#
|
32
|
+
# `def ruby_method
|
33
|
+
# end`
|
34
|
+
def ignore = adjust_by(0)
|
35
|
+
|
36
|
+
# When given an ambiguously indented string, raise an exception
|
37
|
+
def raise_error
|
38
|
+
lambda do |src:, **|
|
39
|
+
raise <<~MSG
|
40
|
+
Indentation is ambiguous, cannot reindent. Try adding a blank
|
41
|
+
line at the beginning or end of this fragment. Fragment:
|
42
|
+
|
43
|
+
#{src}
|
44
|
+
MSG
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# @!visibility private
|
50
|
+
def level__starts_and_stops_with_content(src, handle_ambiguity:)
|
51
|
+
levels = levels(src.lines.drop(1).grep(/\S/))
|
52
|
+
|
53
|
+
min_level = levels.min
|
54
|
+
|
55
|
+
if levels.last == levels.max && ambiguous_level = handle_ambiguity.call(src:, min_level:)
|
56
|
+
return ambiguous_level
|
57
|
+
end
|
58
|
+
|
59
|
+
min_level
|
60
|
+
end
|
61
|
+
|
62
|
+
# @!visibility private
|
63
|
+
def level__starts_with_content(src)
|
64
|
+
src.lines => _first, *middle, last
|
65
|
+
|
66
|
+
levels([*middle.grep(/\S/), last]).min
|
67
|
+
end
|
68
|
+
|
69
|
+
# @!visibility private
|
70
|
+
SINGLE_LINE = /\A.*\n?\z/
|
71
|
+
|
72
|
+
# @!visibility private
|
73
|
+
STARTS_WITH_CONTENT = /\A\S/ # e.g. `content ...
|
74
|
+
|
75
|
+
# @!visibility private
|
76
|
+
ENDS_WITH_CONTENT = /.*\S.*\z/ # e.g. "..\n content "
|
77
|
+
|
78
|
+
# @!visibility private
|
79
|
+
def level(src, handle_ambiguity:)
|
80
|
+
case src
|
81
|
+
when SINGLE_LINE
|
82
|
+
0
|
83
|
+
when STARTS_WITH_CONTENT
|
84
|
+
if src =~ ENDS_WITH_CONTENT
|
85
|
+
level__starts_and_stops_with_content(src, handle_ambiguity:)
|
86
|
+
else
|
87
|
+
level__starts_with_content(src)
|
88
|
+
end
|
89
|
+
else
|
90
|
+
level__basic(src)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Reindent `src` string to the level specified by `depth`. `indent_chars` is used only the current level of
|
95
|
+
# indentation as well as add more indentation.
|
96
|
+
#
|
97
|
+
# Reindents the given source code string to the specified depth.
|
98
|
+
#
|
99
|
+
# @param src [String] The source code to reindent
|
100
|
+
# @param depth [Integer] The indentation level to reindent to
|
101
|
+
# @param indent_chars [String] The indentation characters to use
|
102
|
+
# @param handle_ambiguity [Proc] How to handle ambiguous indentation cases.
|
103
|
+
#
|
104
|
+
# - defaults to {Ambiguity.raise_error}
|
105
|
+
# - use {Ambiguity.ignore} if your syntax doesn't have signigicant whitespace
|
106
|
+
# - use {Ambiguity.adjust_by} if your syntax has significant whitespace
|
107
|
+
def reindent(src, depth, indent_chars, handle_ambiguity: Ambiguity.raise_error)
|
108
|
+
# Turn
|
109
|
+
# "\n" +
|
110
|
+
# " line 1\n" +
|
111
|
+
# " line 2\n"
|
112
|
+
# into
|
113
|
+
# " line 1\n" +
|
114
|
+
# " line 2\n"
|
115
|
+
src = src.sub(/\A\R/m, "")
|
116
|
+
|
117
|
+
new_indentation = indent_chars * depth
|
118
|
+
reindent_line_re = /^[ \t]{0,#{level(src, handle_ambiguity:)}}/
|
119
|
+
|
120
|
+
lines = src.lines.map do |line|
|
121
|
+
if line =~ /\S/
|
122
|
+
line.sub(reindent_line_re, new_indentation)
|
123
|
+
else
|
124
|
+
line.sub(/^[ \t]*/, "")
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
lines.join
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require_relative "../../cecil"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Cecil
|
5
|
+
module Lang
|
6
|
+
class TypeScript < Code
|
7
|
+
# Overrides to use 2 spaces for indentation
|
8
|
+
def indent_chars = " "
|
9
|
+
|
10
|
+
# Overrides to ignore ambiguous indentation
|
11
|
+
def handle_ambiguous_indentation = Indentation::Ambiguity.ignore
|
12
|
+
|
13
|
+
# Overrides to add support for closing multi-line comments (e.g. /* ... */)
|
14
|
+
def block_ending_pairs = super.merge({ "/*" => "*/" })
|
15
|
+
|
16
|
+
module Helpers
|
17
|
+
include Code::Helpers
|
18
|
+
|
19
|
+
# Short for "types"; Accepts one or a list of types and returns their union.
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# the_types = ["Websocket", "undefined", "null"]
|
23
|
+
# `function register<$types>() {}`[t the_types]
|
24
|
+
#
|
25
|
+
# # outputs:
|
26
|
+
# # function register<Websocket | undefined | null>() {}
|
27
|
+
#
|
28
|
+
# @param items [Array[#to_s], #to_s] One or a list of objects that respond to `#to_s`
|
29
|
+
# @return [String] The stringified inputs concatenated with `" | "`
|
30
|
+
def t(items) = Array(items).compact.join(" | ")
|
31
|
+
|
32
|
+
# Short for "list"; Accepts one or a list of strings and returns them joined with `", "`
|
33
|
+
#
|
34
|
+
# Useful for:
|
35
|
+
# - arrays
|
36
|
+
# - objects
|
37
|
+
# - function arguments
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# the_classes = ["Websocket", "Array", "Function"]
|
41
|
+
# `register($args)`[l the_classes]
|
42
|
+
#
|
43
|
+
# # outputs:
|
44
|
+
# # register(Websocket, Array, Function)
|
45
|
+
#
|
46
|
+
# @param items [Array[#to_s], #to_s] One or a list of objects that respond to `#to_s`
|
47
|
+
# @return [String] The stringified inputs concatenated with `", "`
|
48
|
+
def l(items) = Array(items).compact.join(", ")
|
49
|
+
|
50
|
+
# Short for "json"; returns the JSON representation of the input.
|
51
|
+
#
|
52
|
+
# Useful for when you have a value in Ruby and you want it as a literal
|
53
|
+
# value in the JavaScript/TypeScript source code.
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# current_user = { name: "Bob" }
|
57
|
+
# `const user = $user_obj`[j current_user]
|
58
|
+
#
|
59
|
+
# # outputs:
|
60
|
+
# # const user = {"name":"Bob"}
|
61
|
+
#
|
62
|
+
# @param item [#to_json] Any object that responds to `#to_json`
|
63
|
+
# @return [String] JSON representation of the input
|
64
|
+
def j(item) = item.to_json
|
65
|
+
|
66
|
+
# Short for "string content"; returns escaped version of the string that can be inserted into a JavaScript
|
67
|
+
# string literal or template literal.
|
68
|
+
#
|
69
|
+
# Useful for inserting data into a string or for outputting a string but using quotes to make it clear to the
|
70
|
+
# reader what the intended output will be.
|
71
|
+
#
|
72
|
+
# It also escapes single quotes and backticks so that it can be inserted into single-quoted strings and string
|
73
|
+
# templates.
|
74
|
+
#
|
75
|
+
# @example Inserting into a string literal
|
76
|
+
# name = %q{Bob "the Machine" O'Brian}
|
77
|
+
# `const admin = "$name (Admin)"`[s name]
|
78
|
+
#
|
79
|
+
# # outputs:
|
80
|
+
# # const admin = "Bob \"the Machine\" O\'Brian (Admin)"
|
81
|
+
#
|
82
|
+
# @example Make your code communicate that a value will be a string
|
83
|
+
# name = %q{Bob "the Machine" O'Brian}
|
84
|
+
# `const admin = "$name"`[s name]
|
85
|
+
#
|
86
|
+
# # We could use the `#j` helper, too, but `#s` and quotes makes it clearer that the value will be a string
|
87
|
+
# `const admin = $name`[j name]
|
88
|
+
#
|
89
|
+
# @param item [#to_s] A string or any object that responds to `#to_s`
|
90
|
+
# @return [String] A JSON string without quotes
|
91
|
+
def s(item) = item.to_s.to_json[1...-1].gsub("'", "\\\\'").gsub("`", "\\\\`")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|