quince 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2cb965782c941f40df31a058388f1c7e30864c5a1cf708f2840340bf014a3b16
4
+ data.tar.gz: 235e3aa8b1a759799f996f2d2170582f628ac32a80a69bd4fab2838d91dd3a03
5
+ SHA512:
6
+ metadata.gz: 8de7ca420355f54e90e5dc16517f670a6009ebf0d02f6fa45bbeddf2b92fb8eb9e05f8b1e1f9eec584aa6e283b9063146b8980b4b6c2b1463b4b104f6c967865
7
+ data.tar.gz: 0ac305e40cab122d9865e030572c704897a8daa5f2fa1dbb6222976834690be58efe1c4a5e1049b3f1e86846af584795ff41c2eb547490783d626544db677100
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in quince.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
data/Gemfile.lock ADDED
@@ -0,0 +1,40 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ quince (0.1.0)
5
+ oj (~> 3.13)
6
+ typed_struct (>= 0.1.4)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ diff-lcs (1.4.4)
12
+ oj (3.13.5)
13
+ rake (13.0.6)
14
+ rbs (1.6.2)
15
+ rspec (3.10.0)
16
+ rspec-core (~> 3.10.0)
17
+ rspec-expectations (~> 3.10.0)
18
+ rspec-mocks (~> 3.10.0)
19
+ rspec-core (3.10.1)
20
+ rspec-support (~> 3.10.0)
21
+ rspec-expectations (3.10.1)
22
+ diff-lcs (>= 1.2.0, < 2.0)
23
+ rspec-support (~> 3.10.0)
24
+ rspec-mocks (3.10.2)
25
+ diff-lcs (>= 1.2.0, < 2.0)
26
+ rspec-support (~> 3.10.0)
27
+ rspec-support (3.10.2)
28
+ typed_struct (0.1.4)
29
+ rbs (~> 1.0)
30
+
31
+ PLATFORMS
32
+ ruby
33
+
34
+ DEPENDENCIES
35
+ quince!
36
+ rake (~> 13.0)
37
+ rspec (~> 3.0)
38
+
39
+ BUNDLED WITH
40
+ 2.1.4
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Joseph Johansen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Quince
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/quince`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'quince'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install quince
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ 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.
30
+
31
+ 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).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/quince.
36
+
37
+ ## License
38
+
39
+ 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,8 @@
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
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "quince"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,476 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "types"
4
+
5
+ module Quince
6
+ module HtmlTagComponents
7
+ referrer_policy = Rbs("'' | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url' | Quince::Types::Undefined")
8
+ form_method = Rbs(
9
+ '"get" | "post" | "GET" | "POST" | :GET | :POST | :get | :post | Quince::Types::Undefined'
10
+ )
11
+ t = Quince::Types
12
+ opt_string_sym = Rbs("#{t::OptionalString} | Symbol")
13
+ opt_bool = t::OptionalBoolean
14
+ opt_method = Rbs("Method | Quince::Types::Undefined")
15
+ value = opt_string_sym # for now
16
+
17
+ ATTRIBUTES_BY_ELEMENT = {
18
+ "A" => {
19
+ href: opt_string_sym,
20
+ hreflang: opt_string_sym,
21
+ media: opt_string_sym,
22
+ ping: opt_string_sym,
23
+ rel: opt_string_sym,
24
+ target: opt_string_sym,
25
+ type: opt_string_sym,
26
+ referrerpolicy: referrer_policy,
27
+ }.freeze,
28
+ "Abbr" => {}.freeze,
29
+ "Address" => {}.freeze,
30
+ "Article" => {}.freeze,
31
+ "Aside" => {}.freeze,
32
+ "Audio" => {
33
+ autoplay: opt_bool,
34
+ controls: opt_bool,
35
+ controlslist: opt_string_sym,
36
+ crossorigin: opt_string_sym,
37
+ loop: opt_bool,
38
+ mediagroup: opt_string_sym,
39
+ muted: opt_bool,
40
+ playsinline: opt_bool,
41
+ preload: opt_string_sym,
42
+ src: opt_string_sym,
43
+ }.freeze,
44
+ "B" => {}.freeze,
45
+ "Bdo" => {}.freeze,
46
+ "Blockquote" => {
47
+ cite: opt_string_sym,
48
+ }.freeze,
49
+ "Body" => {}.freeze,
50
+ "Button" => {
51
+ autofocus: opt_bool,
52
+ disabled: opt_bool,
53
+ form: opt_string_sym,
54
+ formaction: opt_string_sym,
55
+ formenctype: opt_string_sym,
56
+ formmethod: form_method,
57
+ formnovalidate: opt_bool,
58
+ formtarget: opt_string_sym,
59
+ name: opt_string_sym,
60
+ type: Rbs("'submit' | 'reset' | 'button' | Quince::Types::Undefined"),
61
+ value: value,
62
+ }.freeze,
63
+ "Canvas" => {
64
+ height: opt_string_sym,
65
+ width: opt_string_sym,
66
+ }.freeze,
67
+ "Caption" => {}.freeze,
68
+ "Cite" => {}.freeze,
69
+ "Code" => {}.freeze,
70
+ "Command" => {}.freeze,
71
+ "Colgroup" => {
72
+ span: opt_string_sym,
73
+ }.freeze,
74
+ "Data" => {
75
+ value: value,
76
+ }.freeze,
77
+ "Datalist" => {}.freeze,
78
+ "Dd" => {}.freeze,
79
+ "Del" => {
80
+ cite: opt_string_sym,
81
+ datetime: opt_string_sym,
82
+ }.freeze,
83
+ "Details" => {
84
+ open: opt_bool,
85
+ # ontoggle: Quince::Types::OptionalMethod;
86
+ }.freeze,
87
+ "Dfn" => {}.freeze,
88
+ "Dialog" => {
89
+ open: opt_bool,
90
+ }.freeze,
91
+ "Div" => {}.freeze,
92
+ "Dl" => {}.freeze,
93
+ "Dt" => {}.freeze,
94
+ "Em" => {}.freeze,
95
+ "Fieldset" => {
96
+ disabled: opt_bool,
97
+ form: opt_string_sym,
98
+ name: opt_string_sym,
99
+ }.freeze,
100
+ "Figcaption" => {}.freeze,
101
+ "Figure" => {}.freeze,
102
+ "Form" => {
103
+ "accept-charset": opt_string_sym,
104
+ action: opt_string_sym,
105
+ autocomplete: opt_string_sym,
106
+ enctype: opt_string_sym,
107
+ Method: form_method,
108
+ name: opt_string_sym,
109
+ novalidate: opt_bool,
110
+ target: opt_string_sym,
111
+ }.freeze,
112
+ "Footer" => {}.freeze,
113
+ "H1" => {}.freeze,
114
+ "H2" => {}.freeze,
115
+ "H3" => {}.freeze,
116
+ "H4" => {}.freeze,
117
+ "H5" => {}.freeze,
118
+ "H6" => {}.freeze,
119
+ "Head" => {}.freeze,
120
+ "Header" => {}.freeze,
121
+ "Html" => {
122
+ manifest: opt_string_sym,
123
+ }.freeze,
124
+ "I" => {}.freeze,
125
+ "Iframe" => {
126
+ allow: opt_string_sym,
127
+ allowfullscreen: opt_bool,
128
+ allowtransparency: opt_bool,
129
+ height: opt_string_sym,
130
+ loading: Rbs('"eager" | "lazy" | Quince::Types::Undefined'),
131
+ name: opt_string_sym,
132
+ referrerpolicy: referrer_policy,
133
+ sandbox: opt_string_sym,
134
+ seamless: opt_bool,
135
+ src: opt_string_sym,
136
+ srcdoc: opt_string_sym,
137
+ width: opt_string_sym,
138
+ }.freeze,
139
+ "Ins" => {
140
+ cite: opt_string_sym,
141
+ datetime: opt_string_sym,
142
+ }.freeze,
143
+ "Kbd" => {}.freeze,
144
+ "Keygen" => {
145
+ autofocus: opt_bool,
146
+ challenge: opt_string_sym,
147
+ disabled: opt_bool,
148
+ form: opt_string_sym,
149
+ keytype: opt_string_sym,
150
+ keyparams: opt_string_sym,
151
+ name: opt_string_sym,
152
+ }.freeze,
153
+ "Label" => {
154
+ form: opt_string_sym,
155
+ for: opt_string_sym,
156
+ }.freeze,
157
+ "Legend" => {}.freeze,
158
+ "Li" => {
159
+ value: value,
160
+ }.freeze,
161
+ "Main" => {},
162
+ "Map" => {
163
+ name: opt_string_sym,
164
+ }.freeze,
165
+ "Mark" => {}.freeze,
166
+ "Menu" => {
167
+ type: opt_string_sym,
168
+ }.freeze,
169
+ "Meter" => {
170
+ form: opt_string_sym,
171
+ high: opt_string_sym,
172
+ low: opt_string_sym,
173
+ Max: opt_string_sym,
174
+ Min: opt_string_sym,
175
+ optimum: opt_string_sym,
176
+ value: value,
177
+ }.freeze,
178
+ "Nav" => {}.freeze,
179
+ "Noscript" => {}.freeze,
180
+ "Object" => {
181
+ classid: opt_string_sym,
182
+ data: opt_string_sym,
183
+ form: opt_string_sym,
184
+ height: opt_string_sym,
185
+ name: opt_string_sym,
186
+ type: opt_string_sym,
187
+ usemap: opt_string_sym,
188
+ width: opt_string_sym,
189
+ wmode: opt_string_sym,
190
+ }.freeze,
191
+ "Ol" => {
192
+ reversed: opt_bool,
193
+ start: opt_string_sym,
194
+ type: Rbs("'1' | 'a' | 'A' | 'i' | 'I' | Quince::Types::Undefined"),
195
+ }.freeze,
196
+ "Optgroup" => {
197
+ disabled: opt_bool,
198
+ label: opt_string_sym,
199
+ }.freeze,
200
+ "Option" => {
201
+ disabled: opt_bool,
202
+ label: opt_string_sym,
203
+ selected: opt_bool,
204
+ value: value,
205
+ }.freeze,
206
+ "Output" => {
207
+ form: opt_string_sym,
208
+ for: opt_string_sym,
209
+ name: opt_string_sym,
210
+ }.freeze,
211
+ "Para" => {}, # for "p" element, in order not to clash with Ruby's common `p` method
212
+ "Pre" => {}.freeze,
213
+ "Progress" => {
214
+ Max: opt_string_sym,
215
+ value: value,
216
+ }.freeze,
217
+ "Q" => {}.freeze,
218
+ "Quote" => {
219
+ cite: opt_string_sym,
220
+ }.freeze,
221
+ "Rp" => {}.freeze,
222
+ "Rt" => {}.freeze,
223
+ "Ruby" => {}.freeze,
224
+ "S" => {}.freeze,
225
+ "Samp" => {}.freeze,
226
+ "Script" => {
227
+ async: opt_bool,
228
+ crossorigin: opt_string_sym,
229
+ defer: opt_bool,
230
+ integrity: opt_string_sym,
231
+ nomodule: opt_bool,
232
+ nonce: opt_string_sym,
233
+ referrerpolicy: referrer_policy,
234
+ src: opt_string_sym,
235
+ type: opt_string_sym,
236
+ }.freeze,
237
+ "Section" => {}.freeze,
238
+ "Select" => {
239
+ autocomplete: opt_string_sym,
240
+ autofocus: opt_bool,
241
+ disabled: opt_bool,
242
+ form: opt_string_sym,
243
+ multiple: opt_bool,
244
+ name: opt_string_sym,
245
+ required: opt_bool,
246
+ Size: opt_string_sym,
247
+ value: value,
248
+ # onchange: Rbs::Types::OptionalMethod,
249
+ }.freeze,
250
+ "Small" => {}.freeze,
251
+ "Span" => {}.freeze,
252
+ "Strong" => {}.freeze,
253
+ "Slot" => {
254
+ name: opt_string_sym,
255
+ }.freeze,
256
+ "Style" => {
257
+ media: opt_string_sym,
258
+ nonce: opt_string_sym,
259
+ scoped: opt_bool,
260
+ type: opt_string_sym,
261
+ }.freeze,
262
+ "Sub" => {}.freeze,
263
+ "Sup" => {}.freeze,
264
+ "Table" => {
265
+ cellpadding: opt_string_sym,
266
+ cellspacing: opt_string_sym,
267
+ summary: opt_string_sym,
268
+ width: opt_string_sym,
269
+ }.freeze,
270
+ "Tbody" => {}.freeze,
271
+ "Td" => {
272
+ align: Rbs('"left" | "center" | "right" | "justify" | "char" | Quince::Types::Undefined'),
273
+ colspan: opt_string_sym,
274
+ headers: opt_string_sym,
275
+ rowspan: opt_string_sym,
276
+ scope: opt_string_sym,
277
+ abbr: opt_string_sym,
278
+ height: opt_string_sym,
279
+ width: opt_string_sym,
280
+ valign: Rbs('"top" | "middle" | "bottom" | "baseline" | Quince::Types::Undefined'),
281
+ }.freeze,
282
+ "Textarea" => {
283
+ autocomplete: opt_string_sym,
284
+ autofocus: opt_bool,
285
+ cols: opt_string_sym,
286
+ dirname: opt_string_sym,
287
+ disabled: opt_bool,
288
+ form: opt_string_sym,
289
+ maxlength: opt_string_sym,
290
+ minlength: opt_string_sym,
291
+ name: opt_string_sym,
292
+ placeholder: opt_string_sym,
293
+ readonly: opt_bool,
294
+ required: opt_bool,
295
+ rows: opt_string_sym,
296
+ value: value,
297
+ wrap: opt_string_sym,
298
+ # onchange: Rbs::Types::OptionalMethod,
299
+ }.freeze,
300
+ "Tfoot" => {}.freeze,
301
+ "Th" => {
302
+ align: Rbs('"left" | "center" | "right" | "justify" | "char" | Quince::Types::Undefined'),
303
+ colspan: opt_string_sym,
304
+ headers: opt_string_sym,
305
+ rowspan: opt_string_sym,
306
+ scope: opt_string_sym,
307
+ abbr: opt_string_sym,
308
+ }.freeze,
309
+ "Thead" => {}.freeze,
310
+ "Title" => {}.freeze,
311
+ "Time" => {
312
+ datetime: opt_string_sym,
313
+ }.freeze,
314
+ "Tr" => {}.freeze,
315
+ "U" => {}.freeze,
316
+ "Ul" => {}.freeze,
317
+ "Var" => {}.freeze,
318
+ "Video" => {
319
+ autoplay: opt_bool,
320
+ controls: opt_bool,
321
+ controlslist: opt_string_sym,
322
+ crossorigin: opt_string_sym,
323
+ height: opt_string_sym,
324
+ loop: opt_bool,
325
+ mediagroup: opt_string_sym,
326
+ muted: opt_bool,
327
+ playsinline: opt_bool,
328
+ poster: opt_string_sym,
329
+ preload: opt_string_sym,
330
+ src: opt_string_sym,
331
+ width: opt_string_sym,
332
+ }.freeze,
333
+ }.freeze
334
+
335
+ SELF_CLOSING_TAGS = {
336
+ "Area" => {
337
+ alt: opt_string_sym,
338
+ coords: opt_string_sym,
339
+ download: t::Any,
340
+ href: opt_string_sym,
341
+ hreflang: opt_string_sym,
342
+ media: opt_string_sym,
343
+ referrerpolicy: referrer_policy,
344
+ rel: opt_string_sym,
345
+ shape: opt_string_sym,
346
+ target: opt_string_sym,
347
+ }.freeze,
348
+ "Base" => {
349
+ href: opt_string_sym,
350
+ target: opt_string_sym,
351
+ }.freeze,
352
+ "Br" => {}.freeze,
353
+ "Col" => {
354
+ span: opt_string_sym,
355
+ width: opt_string_sym,
356
+ }.freeze,
357
+ "Embed" => {
358
+ height: opt_string_sym,
359
+ src: opt_string_sym,
360
+ type: opt_string_sym,
361
+ width: opt_string_sym,
362
+ }.freeze,
363
+ "Hr" => {}.freeze,
364
+ "Img" => {
365
+ alt: opt_string_sym,
366
+ crossorigin: Rbs('"anonymous" | "use-credentials" | "" | Quince::Types::Undefined'),
367
+ decoding: Rbs('"async" | "auto" | "sync" | Quince::Types::Undefined'),
368
+ height: Rbs("#{opt_string_sym} | Integer"),
369
+ loading: Rbs('"eager" | "lazy" | Quince::Types::Undefined'),
370
+ referrerpolicy: referrer_policy,
371
+ sizes: opt_string_sym,
372
+ src: opt_string_sym,
373
+ srcSet: opt_string_sym,
374
+ useMap: opt_string_sym,
375
+ width: Rbs("#{opt_string_sym} | Integer"),
376
+ }.freeze,
377
+ "Input" => {
378
+ accept: opt_string_sym,
379
+ alt: opt_string_sym,
380
+ autocomplete: opt_string_sym,
381
+ autofocus: opt_bool,
382
+ capture: Rbs("String | #{opt_bool}"),
383
+ checked: opt_bool,
384
+ crossorigin: opt_string_sym,
385
+ disabled: opt_bool,
386
+ form: opt_string_sym,
387
+ formaction: opt_string_sym,
388
+ formenctype: opt_string_sym,
389
+ formmethod: form_method,
390
+ formnovalidate: opt_bool,
391
+ formtarget: opt_string_sym,
392
+ height: opt_string_sym,
393
+ list: opt_string_sym,
394
+ Max: opt_string_sym,
395
+ maxlength: opt_string_sym,
396
+ Min: opt_string_sym,
397
+ minlength: opt_string_sym,
398
+ multiple: opt_bool,
399
+ name: opt_string_sym,
400
+ pattern: opt_string_sym,
401
+ placeholder: opt_string_sym,
402
+ readonly: opt_bool,
403
+ required: opt_bool,
404
+ Size: opt_string_sym,
405
+ src: opt_string_sym,
406
+ step: opt_string_sym,
407
+ type: opt_string_sym,
408
+ value: value,
409
+ width: opt_string_sym,
410
+ # onchange: Rbs::Types::OptionalMethod,
411
+ }.freeze,
412
+ "Link" => {
413
+ as: opt_string_sym,
414
+ crossorigin: opt_string_sym,
415
+ href: opt_string_sym,
416
+ hreflang: opt_string_sym,
417
+ integrity: opt_string_sym,
418
+ media: opt_string_sym,
419
+ referrerpolicy: referrer_policy,
420
+ rel: opt_string_sym,
421
+ sizes: opt_string_sym,
422
+ type: opt_string_sym,
423
+ charset: opt_string_sym,
424
+ }.freeze,
425
+ "Meta" => {
426
+ charSet: opt_string_sym,
427
+ content: opt_string_sym,
428
+ "http-equiv": opt_string_sym,
429
+ name: opt_string_sym,
430
+ }.freeze,
431
+ "Param" => {
432
+ name: opt_string_sym,
433
+ value: value,
434
+ }.freeze,
435
+ "Source" => {
436
+ media: opt_string_sym,
437
+ sizes: opt_string_sym,
438
+ src: opt_string_sym,
439
+ srcset: opt_string_sym,
440
+ type: opt_string_sym,
441
+ }.freeze,
442
+ "Track" => {
443
+ default: opt_bool,
444
+ kind: opt_string_sym,
445
+ label: opt_string_sym,
446
+ src: opt_string_sym,
447
+ srclang: opt_string_sym,
448
+ }.freeze,
449
+ "Wbr" => {}.freeze,
450
+ }.freeze
451
+
452
+ GLOBAL_HTML_ATTRS = {
453
+ accesskey: opt_string_sym,
454
+ Class: opt_string_sym,
455
+ contenteditable: opt_string_sym,
456
+ # data-*: opt_string_sym,
457
+ dir: opt_string_sym,
458
+ draggable: opt_string_sym,
459
+ hidden: opt_string_sym,
460
+ id: opt_string_sym,
461
+ lang: opt_string_sym,
462
+ spellcheck: opt_string_sym,
463
+ style: opt_string_sym,
464
+ tabindex: opt_string_sym,
465
+ title: opt_string_sym,
466
+ translate: opt_string_sym,
467
+ }.freeze
468
+
469
+ DOM_EVENTS = {
470
+ onclick: opt_method,
471
+ onsubmit: opt_method,
472
+ }.freeze
473
+ end
474
+ end
475
+
476
+ Undefined = Quince::Types::Undefined
@@ -0,0 +1,90 @@
1
+ module Quince
2
+ class Component
3
+ class << self
4
+ def inherited(subclass)
5
+ Quince.define_constructor(subclass)
6
+ end
7
+
8
+ def Props(**kw)
9
+ self.const_set "Props", TypedStruct.new(
10
+ { default: Quince::Types::Undefined },
11
+ Quince::Component::HTML_SELECTOR_ATTR => String,
12
+ **kw,
13
+ )
14
+ end
15
+
16
+ def State(**kw)
17
+ st = kw.empty? ? nil : TypedStruct.new(
18
+ { default: Quince::Types::Undefined },
19
+ **kw,
20
+ )
21
+ self.const_set "State", st
22
+ end
23
+
24
+ def exposed(action, meth0d: :POST)
25
+ @exposed_actions ||= Set.new
26
+ @exposed_actions.add action
27
+ route = "/api/#{self.name}/#{action}"
28
+ Quince.middleware.create_route_handler(
29
+ verb: meth0d,
30
+ route: route,
31
+ ) do |params|
32
+ instance = Quince::Serialiser.deserialise params[:component]
33
+ if @exposed_actions.member? action
34
+ if instance.method(action).arity.zero?
35
+ instance.send action
36
+ else
37
+ instance.send action, params[:params]
38
+ end
39
+ instance
40
+ else
41
+ raise "The action you called is not exposed"
42
+ end
43
+ end
44
+
45
+ route
46
+ end
47
+
48
+ def initial_state=(attrs)
49
+ @initial_state ||= self::State.new(**attrs)
50
+ end
51
+
52
+ attr_reader :initial_state
53
+
54
+ private
55
+
56
+ def initialize_props(const, id, **props)
57
+ const::Props.new(HTML_SELECTOR_ATTR => id, **props) if const.const_defined?("Props")
58
+ end
59
+ end
60
+
61
+ attr_reader :props, :state, :children
62
+
63
+ def initialize(*children, **props, &block_children)
64
+ @__id = SecureRandom.alphanumeric(6)
65
+ @props = self.class.send :initialize_props, self.class, @__id, **props
66
+ @state = self.class.initial_state
67
+ @children = block_children || children
68
+ end
69
+
70
+ def render
71
+ raise "not implemented"
72
+ end
73
+
74
+ protected
75
+
76
+ def to(route, via: :POST)
77
+ self.class.exposed route, meth0d: via
78
+ end
79
+
80
+ private
81
+
82
+ attr_reader :__id
83
+
84
+ HTML_SELECTOR_ATTR = :"data-respid"
85
+
86
+ def html_element_selector
87
+ "[#{HTML_SELECTOR_ATTR}='#{__id}']".freeze
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,102 @@
1
+ require "oj"
2
+ require_relative "attributes_by_element"
3
+ require_relative "serialiser"
4
+
5
+ module Quince
6
+ module HtmlTagComponents
7
+ def self.define_html_tag_component(const_name, attrs, self_closing: false)
8
+ klass = Class.new(Quince::Component) do
9
+ Props(
10
+ **GLOBAL_HTML_ATTRS,
11
+ **DOM_EVENTS,
12
+ **attrs,
13
+ )
14
+
15
+ def render
16
+ attrs = if props
17
+ props.each_pair.map { |k, v| to_html_attr(k, v) }.compact.join(" ")
18
+ end
19
+ result = "<#{tag_name}"
20
+ result << " #{attrs}>" unless attrs.empty?
21
+
22
+ return result if self_closing?
23
+
24
+ result << Quince.to_html(children)
25
+ result << "</#{tag_name}>"
26
+ result
27
+ end
28
+
29
+ def to_html_attr(key, value)
30
+ # for attribute names which clash with standard ruby method
31
+ # eg :class, the first letter is capitalised
32
+ key[0].downcase!
33
+ attrib = case value
34
+ when String, Integer, Float, Symbol
35
+ value.to_s
36
+ when Method
37
+ owner = value.owner
38
+ receiver = value.receiver
39
+ name = value.name
40
+ selector = receiver.send :html_element_selector
41
+ internal = Quince::Serialiser.serialise receiver
42
+ payload = { component: internal }.to_json
43
+ case key
44
+ when :onclick
45
+ CGI.escape_html(
46
+ "callRemoteEndpoint(`/api/#{owner}/#{name}`,`#{payload}`,`#{selector}`)"
47
+ )
48
+ when :onsubmit
49
+ CGI.escape_html(
50
+ "const p = #{payload}; callRemoteEndpoint( `/api/#{owner}/#{name}`, JSON.stringify({...p, params: getFormValues(this)}), `#{selector}`); return false"
51
+ )
52
+ end
53
+ when true
54
+ return key
55
+ when false, nil, Quince::Types::Undefined
56
+ return ""
57
+ else
58
+ raise "prop type not yet implemented #{value}"
59
+ end
60
+
61
+ %Q{#{key}="#{attrib}"}
62
+ end
63
+
64
+ def tag_name
65
+ @tag_name ||= self.class::TAG_NAME
66
+ end
67
+
68
+ def self_closing?
69
+ @self_closing ||= self.class::SELF_CLOSING
70
+ end
71
+ end
72
+
73
+ lower_tag = const_name.downcase
74
+ klass.const_set("TAG_NAME", lower_tag == "para" ? "p".freeze : lower_tag.freeze)
75
+ klass.const_set "SELF_CLOSING", self_closing
76
+
77
+ HtmlTagComponents.const_set const_name, klass
78
+
79
+ Quince.define_constructor HtmlTagComponents.const_get(const_name), lower_tag
80
+ end
81
+ private_class_method :define_html_tag_component
82
+
83
+ ATTRIBUTES_BY_ELEMENT.each { |t, attrs| define_html_tag_component(t, attrs) }
84
+ SELF_CLOSING_TAGS.each { |t, attrs| define_html_tag_component(t, attrs, self_closing: true) }
85
+
86
+ def internal_scripts
87
+ contents = File.read(File.join(__dir__, "..", "..", "scripts.js"))
88
+ script {
89
+ contents
90
+ }
91
+ end
92
+ end
93
+
94
+ Quince::Component.include HtmlTagComponents
95
+ end
96
+
97
+ # tmp hack
98
+ class TypedStruct < Struct
99
+ def to_json(*args)
100
+ to_h.to_json(*args)
101
+ end
102
+ end
@@ -0,0 +1,102 @@
1
+ module Quince
2
+ class Serialiser
3
+ class << self
4
+ def serialise(obj)
5
+ val = case obj
6
+ when Quince::Component
7
+ {
8
+ id: serialise(obj.send(:__id)),
9
+ props: serialise(obj.props),
10
+ state: serialise(obj.state),
11
+ children: serialise(obj.children),
12
+ html_element_selector: serialise(obj.send(:html_element_selector)),
13
+ }
14
+ when Array
15
+ obj.map { |e| serialise e }
16
+ when TypedStruct, Struct, OpenStruct, Hash
17
+ result = obj.each_pair.each_with_object({}) do |(k, v), ob|
18
+ case v
19
+ when Undefined
20
+ next
21
+ else
22
+ ob[k] = serialise(v)
23
+ end
24
+ end
25
+ if result.empty? && !obj.is_a?(Hash)
26
+ obj = nil
27
+ nil
28
+ else
29
+ result
30
+ end
31
+ when Proc
32
+ obj = obj.call # is there a more efficient way of doing this?
33
+ serialise(obj)
34
+ when String
35
+ res = obj.gsub "
36
+ ", "
37
+ "
38
+ res.gsub! ?", '\"'
39
+ res
40
+ else
41
+ obj
42
+ end
43
+
44
+ { t: obj.class&.name, v: val }
45
+ end
46
+
47
+ def deserialise(json)
48
+ case json[:t]
49
+ when "String", "Integer", "Float", "NilClass", "TrueClass", "FalseClass"
50
+ json[:v]
51
+ when "Symbol"
52
+ json[:v].to_sym
53
+ when "Array"
54
+ json[:v].map { |e| deserialise e }
55
+ when "Hash"
56
+ transform_hash json[:v]
57
+ when "OpenStruct"
58
+ OpenStruct.new(**transform_hash(props))
59
+ when nil
60
+ nil
61
+ else
62
+ klass = Object.const_get(json[:t])
63
+ if klass < TypedStruct
64
+ transform_hash_for_struct(json[:v]) || {}
65
+ elsif klass < Quince::Component
66
+ instance = klass.allocate
67
+ val = json[:v]
68
+ id = deserialise val[:id]
69
+
70
+ instance.instance_variable_set :@__id, id
71
+ instance.instance_variable_set(
72
+ :@props,
73
+ klass.send(
74
+ :initialize_props,
75
+ klass,
76
+ id,
77
+ **(deserialise(val[:props]) || {}),
78
+ ),
79
+ )
80
+ st = deserialise(val[:state])
81
+ instance.instance_variable_set :@state, klass::State.new(**st) if st
82
+ instance.instance_variable_set :@children, deserialise(val[:children])
83
+ instance
84
+ else
85
+ klass = Object.const_get(json[:t])
86
+ klass.new(deserialise(json[:v]))
87
+ end
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def transform_hash(hsh)
94
+ hsh.transform_values! { |v| deserialise v }
95
+ end
96
+
97
+ def transform_hash_for_struct(hsh)
98
+ hsh.to_h { |k, v| [k.to_sym, deserialise(v)] }
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,57 @@
1
+ module Quince
2
+ class << self
3
+ attr_reader :middleware
4
+ attr_accessor :underlying_app
5
+
6
+ def optional_string
7
+ @optional_string ||= Rbs("String?")
8
+ end
9
+
10
+ def middleware=(middleware)
11
+ @middleware = middleware
12
+ Object.define_method(:expose) do |component, at:|
13
+ component = component.new if component.instance_of? Class
14
+
15
+ Quince.middleware.create_route_handler(
16
+ verb: :GET,
17
+ route: at,
18
+ component: component,
19
+ )
20
+ end
21
+ end
22
+
23
+ def define_constructor(const, constructor_name = const.to_s)
24
+ HtmlTagComponents.instance_eval do
25
+ define_method(constructor_name) do |*children, **props, &block_children|
26
+ new_props = { **props, Quince::Component::HTML_SELECTOR_ATTR => __id }
27
+ const.new(*children, **new_props, &block_children)
28
+ end
29
+ end
30
+ end
31
+
32
+ def to_html(component)
33
+ output = component
34
+
35
+ until output.is_a? String
36
+ case output
37
+ when Array
38
+ output = output.map { |c| to_html(c) }.join
39
+ when String
40
+ break
41
+ when Proc
42
+ output = to_html(output.call)
43
+ when NilClass
44
+ output = ""
45
+ else
46
+ tmp = output
47
+ output = output.render
48
+ if output.is_a?(Array)
49
+ raise "#render in #{tmp.class} should not return multiple elements. Consider wrapping it in a div"
50
+ end
51
+ end
52
+ end
53
+
54
+ output
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,13 @@
1
+ module Quince
2
+ module Types
3
+ class Base; end
4
+
5
+ # precompiled helper types
6
+ OptionalString = Rbs("String | Quince::Types::Undefined").freeze
7
+ OptionalBoolean = Rbs("true | false | Quince::Types::Undefined").freeze
8
+
9
+ # no functional value for now, other than constants
10
+ Undefined = Class.new(Base).new.freeze
11
+ Any = Class.new(Base).new.freeze
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Quince
4
+ VERSION = "0.1.0"
5
+ end
data/lib/quince.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "securerandom"
2
+ require "typed_struct"
3
+ require "cgi"
4
+ require_relative "quince/singleton_methods"
5
+ require_relative "quince/component"
6
+ require_relative "quince/html_tag_components"
data/quince.gemspec ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/quince/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "quince"
7
+ spec.version = Quince::VERSION
8
+ spec.authors = ["Joseph Johansen"]
9
+ spec.email = ["joe@stotles.com"]
10
+
11
+ spec.summary = "The ruby framework for building dynamic & stateful user experiences"
12
+ spec.description = "Quince is an opinionated framework for building dynamic yet fully server-rendered web apps, with little to no JavaScript"
13
+ spec.homepage = "https://github.com/johansenja/quince"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
16
+
17
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = spec.homepage
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ # Uncomment to register a new dependency of your gem
32
+ spec.add_dependency "typed_struct", ">= 0.1.4"
33
+ spec.add_dependency "oj", "~> 3.13"
34
+
35
+ # For more information and examples about making a new gem, checkout our
36
+ # guide at: https://bundler.io/guides/creating_gem.html
37
+ end
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: quince
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joseph Johansen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2021-09-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: typed_struct
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: oj
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.13'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.13'
41
+ description: Quince is an opinionated framework for building dynamic yet fully server-rendered
42
+ web apps, with little to no JavaScript
43
+ email:
44
+ - joe@stotles.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - ".gitignore"
50
+ - ".rspec"
51
+ - Gemfile
52
+ - Gemfile.lock
53
+ - LICENSE.txt
54
+ - README.md
55
+ - Rakefile
56
+ - bin/console
57
+ - bin/setup
58
+ - lib/quince.rb
59
+ - lib/quince/attributes_by_element.rb
60
+ - lib/quince/component.rb
61
+ - lib/quince/html_tag_components.rb
62
+ - lib/quince/serialiser.rb
63
+ - lib/quince/singleton_methods.rb
64
+ - lib/quince/types.rb
65
+ - lib/quince/version.rb
66
+ - quince.gemspec
67
+ homepage: https://github.com/johansenja/quince
68
+ licenses:
69
+ - MIT
70
+ metadata:
71
+ allowed_push_host: https://rubygems.org
72
+ homepage_uri: https://github.com/johansenja/quince
73
+ source_code_uri: https://github.com/johansenja/quince
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 2.7.0
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubygems_version: 3.1.2
90
+ signing_key:
91
+ specification_version: 4
92
+ summary: The ruby framework for building dynamic & stateful user experiences
93
+ test_files: []