cecil 0.1.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.
- 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
|