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
data/README.md
ADDED
@@ -0,0 +1,492 @@
|
|
1
|
+
# Cecil
|
2
|
+
|
3
|
+
An experimental templating library for generating source code.
|
4
|
+
|
5
|
+
Cecil templates look like the source code you want to generate thanks to Ruby's flexible syntax.
|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
### Write templates in plain Ruby
|
10
|
+
|
11
|
+
Call `Cecil::Code.generate_string` and pass it a block. Inside the block, add lines of code via backticks (or use `src` if you prefer). Cecil returns your generated source code as a string.
|
12
|
+
|
13
|
+
#### Example
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
model_code = Cecil::Code.generate_string do
|
17
|
+
# Use backticks to add lines of code
|
18
|
+
`import Model from '../model'`
|
19
|
+
|
20
|
+
# Multi-line strings work, too.
|
21
|
+
# Cecil preserves indentation.
|
22
|
+
`class User extends Model {
|
23
|
+
id: number
|
24
|
+
name: string
|
25
|
+
companyId: number | undefined
|
26
|
+
}`
|
27
|
+
|
28
|
+
# use #src if you prefer to avoid backticks
|
29
|
+
src "export type Username = User['name']"
|
30
|
+
end
|
31
|
+
|
32
|
+
puts model_code
|
33
|
+
```
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
|
37
|
+
```typescript
|
38
|
+
import Model from '../model'
|
39
|
+
class User extends Model {
|
40
|
+
id: number
|
41
|
+
name: string
|
42
|
+
companyId: number | undefined
|
43
|
+
}
|
44
|
+
export type Username = User['name']
|
45
|
+
```
|
46
|
+
|
47
|
+
### Interpolate values with Cecil's low-noise syntax
|
48
|
+
|
49
|
+
Use `#[]` on the backticks to replace placeholders with actual values.
|
50
|
+
|
51
|
+
By default, placeholders start with `$` and are followed by an identifier.
|
52
|
+
|
53
|
+
Positional arguments match up with placeholders in order. Named arguments match placeholders by name.
|
54
|
+
|
55
|
+
#### Example
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
field = "user"
|
59
|
+
types = ["string", "string[]"]
|
60
|
+
default_value = ["SilentHaiku", "DriftingSnowfall"]
|
61
|
+
field_class = "Model"
|
62
|
+
|
63
|
+
Cecil::Code.generate_string do
|
64
|
+
# positional arguments match placeholders by position
|
65
|
+
`let $field: $FieldType = $default`[field, types.join('|'), default_value.sort.to_json]
|
66
|
+
|
67
|
+
# named arguments match placeholders by name
|
68
|
+
`let $field: $FieldClass<$Types> = new $FieldClass($default)`[
|
69
|
+
field: field,
|
70
|
+
FieldClass: field_class,
|
71
|
+
Types: types.join('|'),
|
72
|
+
default: default_value.sort.to_json
|
73
|
+
]
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
|
79
|
+
```typescript
|
80
|
+
let user: string|string[] = ["DriftingSnowfall","SilentHaiku"]
|
81
|
+
let user: Model<string|string[]> = new Model(["DriftingSnowfall","SilentHaiku"])
|
82
|
+
```
|
83
|
+
|
84
|
+
|
85
|
+
#### "Doesn't Ruby already have string interpolation?"
|
86
|
+
|
87
|
+
Yes, but compare the readability of these two approaches:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
`let $field: $FieldClass<$Types> = new $FieldClass($default)`[
|
91
|
+
field: field,
|
92
|
+
FieldClass: field_class,
|
93
|
+
Types: types.join('|'),
|
94
|
+
default: default_value.sort.to_json
|
95
|
+
]
|
96
|
+
|
97
|
+
# vs
|
98
|
+
|
99
|
+
field_types = types.join('|'),
|
100
|
+
default_json = default_value.sort.to_json
|
101
|
+
"let #{field}: #{field_class}<#{field_types}> = new #{field_class}(#{default_json})"
|
102
|
+
```
|
103
|
+
|
104
|
+
### Indents code blocks & closes brackets automatically
|
105
|
+
|
106
|
+
Pass a block to `#[]` gets indented and open brackets get closed automatically.
|
107
|
+
|
108
|
+
#### Example
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
model = "User"
|
112
|
+
field_name = "name"
|
113
|
+
field_default = "Unnamed"
|
114
|
+
|
115
|
+
Cecil::Code.generate_string do
|
116
|
+
`class $Class extends Model {`[model] do
|
117
|
+
# indentation is preserved
|
118
|
+
`id: number`
|
119
|
+
|
120
|
+
`override get $field() {`[field_name] do
|
121
|
+
`return super.$field ?? $defaultValue`[field_name, field_default.to_json]
|
122
|
+
end
|
123
|
+
end # the open bracket from `... Model {` gets closed with "}"
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
Returns:
|
128
|
+
|
129
|
+
```typescript
|
130
|
+
class User extends Model {
|
131
|
+
id: number
|
132
|
+
override get name() {
|
133
|
+
return super.name ?? "Unnamed"
|
134
|
+
}
|
135
|
+
}
|
136
|
+
```
|
137
|
+
|
138
|
+
### Emit source code to other locations
|
139
|
+
|
140
|
+
When generating source code, things like functions, parameters, classes, etc, often need to be declared, imported, or otherwise setup or before being used.
|
141
|
+
|
142
|
+
`content_for` can be used to add content to a different location of your file.
|
143
|
+
|
144
|
+
Call `content_for(some_key) { ... }` with key and a block to store content under the key you provide. Call `content_for(some_key)` with the key and *no* block to insert your stored content at that location.
|
145
|
+
|
146
|
+
#### Example
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
models = [
|
150
|
+
{ name: 'User', inherits: 'AuthModel' },
|
151
|
+
{ name: 'Company', inherits: 'Model' },
|
152
|
+
]
|
153
|
+
|
154
|
+
Cecil::Code.generate_string do
|
155
|
+
# insert content collected for :imports
|
156
|
+
content_for :imports
|
157
|
+
|
158
|
+
models.each do |model|
|
159
|
+
``
|
160
|
+
`class $Class extends $SuperClass {`[model[:name], model[:inherits]] do
|
161
|
+
`id: number`
|
162
|
+
end
|
163
|
+
|
164
|
+
content_for :imports do
|
165
|
+
# this gets inserted above
|
166
|
+
`import $SuperClass from '../models/$SuperClass'`[SuperClass: model[:inherits]]
|
167
|
+
end
|
168
|
+
|
169
|
+
content_for :registrations do
|
170
|
+
# this gets inserted below
|
171
|
+
`$SuperClass.registerAncestor($Class)`[model[:inherits], model[:name]]
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
``
|
176
|
+
# insert content collected for :registrations
|
177
|
+
content_for :registrations
|
178
|
+
end
|
179
|
+
```
|
180
|
+
|
181
|
+
Returns:
|
182
|
+
|
183
|
+
```typescript
|
184
|
+
import AuthModel from '../models/AuthModel'
|
185
|
+
import Model from '../models/Model'
|
186
|
+
|
187
|
+
class User extends AuthModel {
|
188
|
+
id: number
|
189
|
+
}
|
190
|
+
|
191
|
+
class Company extends Model {
|
192
|
+
id: number
|
193
|
+
}
|
194
|
+
|
195
|
+
AuthModel.registerAncestor(User)
|
196
|
+
Model.registerAncestor(Company)
|
197
|
+
```
|
198
|
+
|
199
|
+
### Collect data as you go then use it earlier in the document
|
200
|
+
|
201
|
+
The `#defer` method takes a block and waits to call it until the rest of the template is evaluated. The block's result is inserted at the location where `#defer` was called.
|
202
|
+
|
203
|
+
This gives a similar ability to `#content_for`, but is more flexible because you can collect any kind of data, not just source code.
|
204
|
+
|
205
|
+
#### Example
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
models = [
|
209
|
+
{ name: 'User', inherits: 'AuthModel' },
|
210
|
+
{ name: 'Company', inherits: 'Model' },
|
211
|
+
{ name: 'Candidate', inherits: 'AuthModel' },
|
212
|
+
]
|
213
|
+
|
214
|
+
Cecil::Code.generate_string do
|
215
|
+
superclasses = []
|
216
|
+
|
217
|
+
defer do
|
218
|
+
# This block gets called after the rest of the parent block is finished.
|
219
|
+
#
|
220
|
+
# By the time this block is called, the `superclasses` array is full of data
|
221
|
+
#
|
222
|
+
# Even though this block is called later, the output is added at the location where `defer` was called
|
223
|
+
`import { $SuperClasses } from '../models'`[superclasses.uniq.sort.join(', ')]
|
224
|
+
``
|
225
|
+
end
|
226
|
+
|
227
|
+
models.each do |model|
|
228
|
+
superclasses << model[:inherits] # add more strings to `superclasses`, which is used in the block above
|
229
|
+
|
230
|
+
`class $Class extends $SuperClass {}`[model[:name], model[:inherits]]
|
231
|
+
end
|
232
|
+
end
|
233
|
+
```
|
234
|
+
|
235
|
+
Returns:
|
236
|
+
|
237
|
+
```typescript
|
238
|
+
import { AuthModel, Model } from '../models'
|
239
|
+
|
240
|
+
class User extends AuthModel {}
|
241
|
+
class Company extends Model {}
|
242
|
+
class Candidate extends AuthModel {}
|
243
|
+
```
|
244
|
+
|
245
|
+
### Customizable syntax and behaviors
|
246
|
+
|
247
|
+
Easily customize the following features to make Cecil suit your needs/preferences:
|
248
|
+
|
249
|
+
- placeholder syntax
|
250
|
+
- auto-closing brackets
|
251
|
+
- indentation
|
252
|
+
|
253
|
+
Customizations are performed by subclassing [`Cecil::Code`][{Code}] and overriding the relevant methods.
|
254
|
+
|
255
|
+
For example, Cecil comes with [`Cecil::Lang::TypeScript`][{Lang::TypeScript}] that you can use instead of of `Cecil::Code`. It has a few JavaScript/TypeScript-specific customizations. It's a subclass of `Cecil::Code` so it can be used the same way:
|
256
|
+
|
257
|
+
```ruby
|
258
|
+
Cecil::Lang::TypeScript.generate_string do
|
259
|
+
# ...
|
260
|
+
end
|
261
|
+
```
|
262
|
+
|
263
|
+
## Use cases
|
264
|
+
|
265
|
+
Things I've personally used Cecil to generate:
|
266
|
+
|
267
|
+
- **serialization/deserialization code** generated from from specs (e.g. OpenAPI)
|
268
|
+
- **diagrams** (e.g. Mermaid, PlantUML, Dot/Graphviz)
|
269
|
+
- ERDs/schemas
|
270
|
+
- state machine diagrams
|
271
|
+
- graphs
|
272
|
+
- data visualizations
|
273
|
+
- **state machines** generated from a list of states and transitions
|
274
|
+
- **test cases** generated from data that describes inputs/setup and expected outputs; because parameterized tests can be very hard to debug
|
275
|
+
- **complex types** because meta-programming in TypeScript can get complex quickly
|
276
|
+
|
277
|
+
## Quick Reference
|
278
|
+
|
279
|
+
Reference documentation is on RubyDoc.info:
|
280
|
+
[gem](https://www.rubydoc.info/gems/cecil)
|
281
|
+
|
|
282
|
+
[repo](https://www.rubydoc.info/github/nicholaides/cecil/main)
|
283
|
+
|
284
|
+
### Calling Cecil
|
285
|
+
|
286
|
+
Call
|
287
|
+
[`Cecil::Code.generate`][{Code.generate}] /
|
288
|
+
[`generate_string`][{Code.generate_string}]
|
289
|
+
with a block and inside the block, use backticks or `#src` to emit lines of source code.
|
290
|
+
E.g.
|
291
|
+
|
292
|
+
```ruby
|
293
|
+
# returns a string
|
294
|
+
Cecil::Code.generate_string do
|
295
|
+
`function greet() {}`
|
296
|
+
`function respond() {}`
|
297
|
+
end
|
298
|
+
|
299
|
+
# outputs to $stdout
|
300
|
+
Cecil::Code.generate do
|
301
|
+
`function greet() {}`
|
302
|
+
`function respond() {}`
|
303
|
+
end
|
304
|
+
```
|
305
|
+
|
306
|
+
See: [Methods available inside a Cecil block][{BlockContext}]
|
307
|
+
|
308
|
+
### Emitting source code
|
309
|
+
|
310
|
+
- [backticks/``` #`` ```/`#src`][{BlockContext#src}] emit source code.
|
311
|
+
E.g.:
|
312
|
+
```ruby
|
313
|
+
Cecil::Code.generate_string do
|
314
|
+
`function greet() {}`
|
315
|
+
`function respond() {}`
|
316
|
+
src "function ask() {}"
|
317
|
+
end
|
318
|
+
# outputs:
|
319
|
+
# function greet() {}
|
320
|
+
# function respond() {}
|
321
|
+
# function ask() {}
|
322
|
+
```
|
323
|
+
|
324
|
+
- [`#[]`][{Node#with}] interpolates data into placeholders. E.g.
|
325
|
+
```ruby
|
326
|
+
Cecil::Code.generate_string do
|
327
|
+
`function $fn() {}`["greet"]
|
328
|
+
`function $fn() {}`[fn: "respond"]
|
329
|
+
end
|
330
|
+
# outputs:
|
331
|
+
# function greet() {}
|
332
|
+
# function respond() {}
|
333
|
+
```
|
334
|
+
- [`#[]`][{Node#with}]`{ ... }` given a block, interpolates and indents the code emitted in its block.
|
335
|
+
E.g.
|
336
|
+
```ruby
|
337
|
+
Cecil::Code.generate_string do
|
338
|
+
`function $fn() {`["greet"] do
|
339
|
+
`console.log("hello")`
|
340
|
+
end
|
341
|
+
end
|
342
|
+
# outputs:
|
343
|
+
# function greet() {
|
344
|
+
# console.log("hello")
|
345
|
+
# }
|
346
|
+
```
|
347
|
+
- [`#<<`][{Node#<<}] adds code the last line of the block.
|
348
|
+
E.g.
|
349
|
+
```ruby
|
350
|
+
Cecil::Code.generate_string do
|
351
|
+
`(function ${fn}Now() {`["greet"] do
|
352
|
+
`console.log("hello")`
|
353
|
+
end << ')()'
|
354
|
+
end
|
355
|
+
# outputs:
|
356
|
+
# (function greetNow() {
|
357
|
+
# console.log("hello")
|
358
|
+
# })()
|
359
|
+
```
|
360
|
+
- [`#content_for`][{BlockContext#content_for}] emits source code to different locations
|
361
|
+
- [`#defer`][{BlockContext#defer}] for waits to emit the given source until after data has been gathered
|
362
|
+
|
363
|
+
### Customizing behavior for the language of the source code you're generating
|
364
|
+
|
365
|
+
Many of Cecil's defaults can be customized by creating a subclass of [`Cecil::Code`][{Code}] and overriding methods to customize syntax and behavior of:
|
366
|
+
- placeholder syntax
|
367
|
+
- indentation
|
368
|
+
- auto-closing brackets
|
369
|
+
|
370
|
+
Currently, Cecil comes with:
|
371
|
+
- [`Cecil::Code`][{Code}] for generic code
|
372
|
+
- [`Cecil::Lang::TypeScript`][{Lang::TypeScript}] for JavaScript and TypeScript
|
373
|
+
|
374
|
+
|
375
|
+
### Auto-closing brackets
|
376
|
+
|
377
|
+
> Customize which opening brackets are auto-closed by overriding [`Cecil::Code#block_ending_pairs`][{Code#block_ending_pairs}] in a subclass.
|
378
|
+
|
379
|
+
When nesting code blocks with `#[] { ... }`, open brackets at the end of the string get closed automatically.
|
380
|
+
|
381
|
+
For example, notice how we don't have to manually provide a closing `}` in the following:
|
382
|
+
|
383
|
+
```ruby
|
384
|
+
`$var = {`[var: "user"] do
|
385
|
+
`id: 42`
|
386
|
+
end
|
387
|
+
```
|
388
|
+
becomes
|
389
|
+
```javascript
|
390
|
+
user = {
|
391
|
+
id: 42
|
392
|
+
}
|
393
|
+
```
|
394
|
+
|
395
|
+
#### Multiple brackets
|
396
|
+
|
397
|
+
Every consecutive closing bracket at the end of the string gets closed. E.g.
|
398
|
+
|
399
|
+
```ruby
|
400
|
+
`$var = [{(`[var: "user"] do
|
401
|
+
`id: 42`
|
402
|
+
end
|
403
|
+
```
|
404
|
+
|
405
|
+
becomes
|
406
|
+
|
407
|
+
```javascript
|
408
|
+
user = ([{
|
409
|
+
id: 42
|
410
|
+
)}]
|
411
|
+
```
|
412
|
+
|
413
|
+
Currently, the algorithm is simplistic, so open brackets that aren't at the end of the string will *not* get closed.
|
414
|
+
|
415
|
+
In this example, the `(` in `test(` needs to be closed manually:
|
416
|
+
|
417
|
+
```ruby
|
418
|
+
`test("getter $fn", () => {`[fn: 'getUsername'] do
|
419
|
+
`assert(false)`
|
420
|
+
end << `)'
|
421
|
+
```
|
422
|
+
|
423
|
+
```javascript
|
424
|
+
test("getter getUsername", () => {
|
425
|
+
assert(false)
|
426
|
+
})
|
427
|
+
```
|
428
|
+
|
429
|
+
### Placeholder syntax
|
430
|
+
|
431
|
+
Default placeholder rules:
|
432
|
+
- start with `$`-- e.g. `$foo`
|
433
|
+
- named with alpha-numeric and underscore -- e.g. `$foo_bar123`
|
434
|
+
- names can optionally be surrounded by optional brackets -- e.g `${my_placeholder}`
|
435
|
+
|
436
|
+
Surrounding with brackets can be useful to separate a placeholder from subsequent characters that would otherwise get parsed as a placeholder.
|
437
|
+
|
438
|
+
E.g. `function ${fn}Sync()`-- without curly brackets, the placeholder would be parsed as `fnSync`.
|
439
|
+
|
440
|
+
Customize placeholder syntax by subclassing [`Cecil::Code`][{Code}]
|
441
|
+
and overriding [placeholder-related methods][{Code}].
|
442
|
+
|
443
|
+
|
444
|
+
### Helper methods
|
445
|
+
|
446
|
+
If you use your generator frequently it can be helpful to define reusable helper methods on a subclass of [`Cecil::Code`][{Code}].
|
447
|
+
|
448
|
+
For example, the [`Cecil::Lang::TypeScript`][{Lang::TypeScript}] subclass defines several [helper methods][{Lang::TypeScript::Helpers}] for generating TypeScript code.
|
449
|
+
|
450
|
+
[{BlockContext#content_for}]: https://www.rubydoc.info/github/nicholaides/cecil/main/Cecil/BlockContext#content_for-instance_method
|
451
|
+
[{BlockContext#defer}]: https://www.rubydoc.info/github/nicholaides/cecil/main/Cecil/BlockContext#defer-instance_method
|
452
|
+
[{BlockContext#src}]: https://www.rubydoc.info/github/nicholaides/cecil/main/Cecil/BlockContext#src-instance_method
|
453
|
+
[{BlockContext}]: https://www.rubydoc.info/github/nicholaides/cecil/main/Cecil/BlockContext
|
454
|
+
[{Code.generate_string}]: https://www.rubydoc.info/github/nicholaides/cecil/main/Cecil/Code#generate_string-class_method
|
455
|
+
[{Code.generate}]: https://www.rubydoc.info/github/nicholaides/cecil/main/Cecil/Code#generate-class_method
|
456
|
+
[{Code}]: https://www.rubydoc.info/github/nicholaides/cecil/main/Cecil/Code
|
457
|
+
[{Code#block_ending_pairs}]: https://www.rubydoc.info/github/nicholaides/cecil/main/Cecil/Code#block_ending_pairs-instance_method
|
458
|
+
[{Lang::TypeScript}]: https://www.rubydoc.info/github/nicholaides/cecil/main/Cecil/Lang/TypeScript
|
459
|
+
[{Lang::TypeScript::Helpers}]: https://www.rubydoc.info/github/nicholaides/cecil/main/Cecil/Lang/TypeScript/Helpers
|
460
|
+
[{Lang::TypeScript.generate_string}]: https://www.rubydoc.info/github/nicholaides/cecil/main/Cecil/Lang/TypeScript#generate_string-class_method
|
461
|
+
[{Lang::TypeScript.generate}]: https://www.rubydoc.info/github/nicholaides/cecil/main/Cecil/Code#generate-class_method
|
462
|
+
[{Node#<<}]: https://www.rubydoc.info/github/nicholaides/cecil/main/Cecil/Node#<<-instance_method
|
463
|
+
[{Node#with}]: https://www.rubydoc.info/github/nicholaides/cecil/main/Cecil/Node#with-instance_method
|
464
|
+
|
465
|
+
## Installation
|
466
|
+
|
467
|
+
Gem can be installed from github. Once I'm ready to bother with version numbers and releases and such, then I'll publish to Rubygems.
|
468
|
+
|
469
|
+
From your shell:
|
470
|
+
|
471
|
+
```sh
|
472
|
+
bundle add cecil --github=nicholaides/cecil
|
473
|
+
```
|
474
|
+
|
475
|
+
Add it to your Gemfile like:
|
476
|
+
|
477
|
+
```ruby
|
478
|
+
gem 'cecil', github: 'nicholaides/cecil'
|
479
|
+
```
|
480
|
+
## Development
|
481
|
+
|
482
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
483
|
+
|
484
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
485
|
+
|
486
|
+
## Contributing
|
487
|
+
|
488
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/nicholaides/cecil.
|
489
|
+
|
490
|
+
## License
|
491
|
+
|
492
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
|
8
|
+
require "rubocop/rake_task"
|
9
|
+
|
10
|
+
RuboCop::RakeTask.new
|
11
|
+
|
12
|
+
require "rake/clean"
|
13
|
+
CLEAN.include ".yardoc/"
|
14
|
+
|
15
|
+
require "yard"
|
16
|
+
YARD::Rake::YardocTask.new do |t|
|
17
|
+
t.options = %w[--no-cache --fail-on-warning]
|
18
|
+
end
|
19
|
+
|
20
|
+
directory ".yard"
|
21
|
+
CLEAN.include ".yard/"
|
22
|
+
|
23
|
+
def normalize_yard_ref(str)
|
24
|
+
if str.start_with?("Cecil::")
|
25
|
+
str
|
26
|
+
else
|
27
|
+
"Cecil::#{str}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def convert_markdown_yardoc_links_to_yardoc(str)
|
32
|
+
str.gsub(/\[(.+)\]\[\{([^\}\]]+)\}\]/) { "{#{normalize_yard_ref(Regexp.last_match(2))} #{Regexp.last_match(1)}}" }
|
33
|
+
end
|
34
|
+
|
35
|
+
file ".yard/README.md" => ["README.md", ".yard"] do |t|
|
36
|
+
File.write t.name, convert_markdown_yardoc_links_to_yardoc(File.read("README.md"))
|
37
|
+
end
|
38
|
+
task yard: ".yard/README.md"
|
39
|
+
|
40
|
+
task :ensure_yard_readme_is_up_to_date do
|
41
|
+
if File.read(".yard/README.md") != convert_markdown_yardoc_links_to_yardoc(File.read("README.md"))
|
42
|
+
raise ".yard/README.md is not up-to-date. Run `rake` before committing."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
task default: %i[spec yard rubocop]
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require "forwardable"
|
2
|
+
require "delegate"
|
3
|
+
|
4
|
+
module Cecil
|
5
|
+
# The {BlockContext} contains methods available to you inside a Cecil block.
|
6
|
+
#
|
7
|
+
# Methods available in the scope of a Cecil block are:
|
8
|
+
#
|
9
|
+
# - **Methods & variables from local scope**
|
10
|
+
# - **{BlockContext} instance methods** for emitting code (listed below)
|
11
|
+
# - **Helper methods** in your {Code} subclass' `Helpers`. See {Code} for defining your own helper methods.
|
12
|
+
#
|
13
|
+
# @example Methods available in a Cecil block's scope
|
14
|
+
# def has_data?(first_name) = File.exist?("data/#{first_name}.json")
|
15
|
+
#
|
16
|
+
# name = "Bob"
|
17
|
+
# last_names = ["McTesterson", "Rickenbacker"]
|
18
|
+
#
|
19
|
+
# Cecil::Lang::TypeScript.generate_string do
|
20
|
+
# content_for :imports # `content_for` is a BlockContext instance method
|
21
|
+
#
|
22
|
+
# `let firstName = "$username"`[name] # `name` is a local variable
|
23
|
+
#
|
24
|
+
# `let lastNames = $lastNames`[j last_names] # `j` helper via Cecil::Lang::TypeScript::Helpers
|
25
|
+
#
|
26
|
+
# if has_data?(name) # has_data? comes from local scope
|
27
|
+
# content_for :imports do
|
28
|
+
# `import userData from './data/$first_name.json`[s name]
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# end
|
33
|
+
class BlockContext < SimpleDelegator
|
34
|
+
# @!visibility private
|
35
|
+
def initialize(receiver, builder, helpers)
|
36
|
+
super(receiver)
|
37
|
+
@builder = builder
|
38
|
+
extend helpers
|
39
|
+
end
|
40
|
+
|
41
|
+
# @!visibility private
|
42
|
+
# Override from Delegator. This allows methods in the global scope to be accessed, b/c otherwise they are private
|
43
|
+
# and Delegator won't pick them up.
|
44
|
+
def target_respond_to?(target, method_name, _include_private) = super(target, method_name, true)
|
45
|
+
|
46
|
+
extend Forwardable
|
47
|
+
|
48
|
+
# @!method src(source_string)
|
49
|
+
# Inserts a node with the given source string.
|
50
|
+
#
|
51
|
+
# The inserted node can be modified by calling {Node#with}/{Node#[]}
|
52
|
+
#
|
53
|
+
# @return [Node] the inserted node
|
54
|
+
#
|
55
|
+
# @overload src(source_string)
|
56
|
+
# @overload `(source_string)
|
57
|
+
def_delegator :@builder, :src
|
58
|
+
|
59
|
+
# Alias for {#src}
|
60
|
+
def `(source_string) = @builder.src(source_string)
|
61
|
+
|
62
|
+
# @!method defer(&)
|
63
|
+
# Defer execution of the the given block until the rest of the document is evaluated and insert any content in the
|
64
|
+
# document where this method was called.
|
65
|
+
#
|
66
|
+
# @return [Node::Deferred]
|
67
|
+
def_delegator :@builder, :defer
|
68
|
+
|
69
|
+
# @!method content_for(key, &)
|
70
|
+
# Stores content for the given key to be insert at a different location in the document.
|
71
|
+
#
|
72
|
+
# If a block is passed, it will be executed and the result stored. If no block is passed but the key already has
|
73
|
+
# content, it will be retrieved. Otherwise, content rendering will be deferred until later.
|
74
|
+
#
|
75
|
+
# @param [#hash] key Any hashable object to identify the content but can
|
76
|
+
# be anything that works as a hash key
|
77
|
+
#
|
78
|
+
# @return [nil]
|
79
|
+
#
|
80
|
+
# @example Storing content for earlier insertion
|
81
|
+
# content_for :imports # inserts `import { Component } from 'react'` here
|
82
|
+
# # ...
|
83
|
+
# content_for :imports do # store
|
84
|
+
# `import { Component } from 'react'`
|
85
|
+
# end
|
86
|
+
#
|
87
|
+
# @example Storing content for later insertion
|
88
|
+
# `job1 = new Job()`
|
89
|
+
# content_for :run_jobs do # store
|
90
|
+
# `job1.run()`
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# `job2 = new Job()`
|
94
|
+
# content_for :run_jobs do # store
|
95
|
+
# `job2.run()`
|
96
|
+
# end
|
97
|
+
# # ...
|
98
|
+
# content_for :run_jobs # adds `job1.run()` and `job2.run()`
|
99
|
+
#
|
100
|
+
# @example Storing multiple lines
|
101
|
+
# content_for :functions
|
102
|
+
#
|
103
|
+
# content_for :functions do
|
104
|
+
# `function $fnName() {`[fn_name] do
|
105
|
+
# `api.fetch('$fnName', $fn_arg)`[fn_name, fn_arg.to_json]
|
106
|
+
# end
|
107
|
+
# `function undo$fnName() {`[fn_name] do
|
108
|
+
# `api.fetch('undo$fnName', $fn_arg)`[fn_name, fn_arg.to_json]
|
109
|
+
# end
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# @example Using different types for keys
|
113
|
+
# content_for :imports
|
114
|
+
# content_for "imports"
|
115
|
+
# content_for ["imports", :secion1]
|
116
|
+
#
|
117
|
+
# user = User.find(1)
|
118
|
+
# content_for user
|
119
|
+
#
|
120
|
+
# @overload content_for(key)
|
121
|
+
# Insert the stored content for the given key
|
122
|
+
# @return [nil] A node of stored content for the given key
|
123
|
+
#
|
124
|
+
# @overload content_for(key, &)
|
125
|
+
# Store content to be be inserted at a different position in the file
|
126
|
+
# @yield The content in the block is evaluated immediately and stored for later insertion
|
127
|
+
# @return [nil]
|
128
|
+
def_delegator :@builder, :content_for
|
129
|
+
|
130
|
+
# @!method content_for?(key)
|
131
|
+
# Returns whether there is any content stored for the given key.
|
132
|
+
#
|
133
|
+
# This method returns immediately and will return
|
134
|
+
# false even if `#content_for(key) { ... }` is called later.
|
135
|
+
#
|
136
|
+
# @param [#hash] key Any hashable object to identify the content
|
137
|
+
# @return [Boolean] whether any content is stored for the given key
|
138
|
+
def_delegator :@builder, :content_for?
|
139
|
+
|
140
|
+
# @!method content_for!(key)
|
141
|
+
# Returns the content stored for the given key, and raises an exception if there is no content stored. Calling
|
142
|
+
# {#content_for!} is evaluated immeditately and will raise an exception even if `#content_for(key) { ... }` is
|
143
|
+
# called later.
|
144
|
+
#
|
145
|
+
# @param [#hash] key Any hashable object to identify the content
|
146
|
+
# @return [Array<Node::Detached>] A node of stored content for the given key
|
147
|
+
# @raise [Exception] Throws an execption if there is no content stored at the given key
|
148
|
+
def_delegator :@builder, :content_for!
|
149
|
+
end
|
150
|
+
end
|