mayu-live 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +661 -0
  3. data/README.md +598 -0
  4. data/exe/mayu +33 -0
  5. data/lib/mayu/app_metrics.rb +93 -0
  6. data/lib/mayu/banner.rb +12 -0
  7. data/lib/mayu/client/README.md +17 -0
  8. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js +1 -0
  9. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.br +0 -0
  10. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.map +1 -0
  11. data/lib/mayu/client/dist/DecompressionStreamPolyfill-3ceba43e.js.map.br +0 -0
  12. data/lib/mayu/client/dist/custom-elements/mayu-alert-cd7ad2a4.js +1 -0
  13. data/lib/mayu/client/dist/custom-elements/mayu-alert-cd7ad2a4.js.map +1 -0
  14. data/lib/mayu/client/dist/custom-elements/mayu-disconnected-9f349f46.js +1 -0
  15. data/lib/mayu/client/dist/custom-elements/mayu-disconnected-9f349f46.js.map +1 -0
  16. data/lib/mayu/client/dist/custom-elements/mayu-exception-63df4e8c.js +1 -0
  17. data/lib/mayu/client/dist/custom-elements/mayu-exception-63df4e8c.js.map +1 -0
  18. data/lib/mayu/client/dist/custom-elements/mayu-ping-c498c2a6.js +1 -0
  19. data/lib/mayu/client/dist/custom-elements/mayu-ping-c498c2a6.js.map +1 -0
  20. data/lib/mayu/client/dist/custom-elements/mayu-progress-bar-eb3e1ac8.js +1 -0
  21. data/lib/mayu/client/dist/custom-elements/mayu-progress-bar-eb3e1ac8.js.map +1 -0
  22. data/lib/mayu/client/dist/entries.json +3 -0
  23. data/lib/mayu/client/dist/main-4b49dbc4.js +1 -0
  24. data/lib/mayu/client/dist/main-4b49dbc4.js.br +0 -0
  25. data/lib/mayu/client/dist/main-4b49dbc4.js.map +1 -0
  26. data/lib/mayu/client/dist/main-4b49dbc4.js.map.br +0 -0
  27. data/lib/mayu/client/package.json +39 -0
  28. data/lib/mayu/client/rollup.config.js +81 -0
  29. data/lib/mayu/client/src/DecompressionStream.ts +15 -0
  30. data/lib/mayu/client/src/DecompressionStreamPolyfill.ts +43 -0
  31. data/lib/mayu/client/src/MimeTypes.ts +4 -0
  32. data/lib/mayu/client/src/NodeTree.ts +445 -0
  33. data/lib/mayu/client/src/custom-elements/mayu-alert.html +137 -0
  34. data/lib/mayu/client/src/custom-elements/mayu-alert.ts +62 -0
  35. data/lib/mayu/client/src/custom-elements/mayu-disconnected.html +134 -0
  36. data/lib/mayu/client/src/custom-elements/mayu-disconnected.ts +51 -0
  37. data/lib/mayu/client/src/custom-elements/mayu-exception.html +79 -0
  38. data/lib/mayu/client/src/custom-elements/mayu-exception.ts +28 -0
  39. data/lib/mayu/client/src/custom-elements/mayu-log.html +70 -0
  40. data/lib/mayu/client/src/custom-elements/mayu-log.ts +42 -0
  41. data/lib/mayu/client/src/custom-elements/mayu-ping.html +36 -0
  42. data/lib/mayu/client/src/custom-elements/mayu-ping.ts +53 -0
  43. data/lib/mayu/client/src/custom-elements/mayu-progress-bar.html +44 -0
  44. data/lib/mayu/client/src/custom-elements/mayu-progress-bar.ts +40 -0
  45. data/lib/mayu/client/src/custom-elements/types.d.ts +4 -0
  46. data/lib/mayu/client/src/global.d.ts +26 -0
  47. data/lib/mayu/client/src/h.ts +27 -0
  48. data/lib/mayu/client/src/logger.ts +56 -0
  49. data/lib/mayu/client/src/main.ts +271 -0
  50. data/lib/mayu/client/src/serializeEvent.ts +90 -0
  51. data/lib/mayu/client/src/stream.ts +175 -0
  52. data/lib/mayu/client/src/types.ts +1 -0
  53. data/lib/mayu/client/src/utils.ts +71 -0
  54. data/lib/mayu/client/tsconfig.json +18 -0
  55. data/lib/mayu/colors.rb +34 -0
  56. data/lib/mayu/commands/base.rb +22 -0
  57. data/lib/mayu/commands/build.rb +82 -0
  58. data/lib/mayu/commands.rb +53 -0
  59. data/lib/mayu/component/base.rb +177 -0
  60. data/lib/mayu/component/handler_ref.rb +99 -0
  61. data/lib/mayu/component/helpers.rb +93 -0
  62. data/lib/mayu/component/interface.rb +18 -0
  63. data/lib/mayu/component/wrapper.rb +165 -0
  64. data/lib/mayu/component.rb +54 -0
  65. data/lib/mayu/configuration.rb +195 -0
  66. data/lib/mayu/disable_sorbet.rb +23 -0
  67. data/lib/mayu/environment.rb +151 -0
  68. data/lib/mayu/event_stream.rb +158 -0
  69. data/lib/mayu/fetch.rb +88 -0
  70. data/lib/mayu/html.rb +53 -0
  71. data/lib/mayu/html.yaml +767 -0
  72. data/lib/mayu/message_cipher.rb +172 -0
  73. data/lib/mayu/message_cipher.test.rb +16 -0
  74. data/lib/mayu/metrics/collector.rb +161 -0
  75. data/lib/mayu/metrics/exporter.rb +47 -0
  76. data/lib/mayu/metrics/reporter.rb +187 -0
  77. data/lib/mayu/metrics.rb +82 -0
  78. data/lib/mayu/ref_counter.rb +57 -0
  79. data/lib/mayu/resources/README.md +14 -0
  80. data/lib/mayu/resources/asset.rb +71 -0
  81. data/lib/mayu/resources/assets.rb +76 -0
  82. data/lib/mayu/resources/dependency_graph.rb +306 -0
  83. data/lib/mayu/resources/dot_exporter.rb +167 -0
  84. data/lib/mayu/resources/generators/base.rb +18 -0
  85. data/lib/mayu/resources/generators/copy_file.rb +26 -0
  86. data/lib/mayu/resources/generators/image.rb +106 -0
  87. data/lib/mayu/resources/generators/write_file.rb +39 -0
  88. data/lib/mayu/resources/hot_swap/file_watcher.rb +69 -0
  89. data/lib/mayu/resources/hot_swap.rb +46 -0
  90. data/lib/mayu/resources/mermaid_exporter.rb +210 -0
  91. data/lib/mayu/resources/registry.rb +190 -0
  92. data/lib/mayu/resources/resolver/base.rb +32 -0
  93. data/lib/mayu/resources/resolver/filesystem.rb +94 -0
  94. data/lib/mayu/resources/resolver/static.rb +27 -0
  95. data/lib/mayu/resources/resolver.rb +13 -0
  96. data/lib/mayu/resources/resource.rb +150 -0
  97. data/lib/mayu/resources/transformers/__test__/css/adjacent_selectors.in.css +3 -0
  98. data/lib/mayu/resources/transformers/__test__/css/adjacent_selectors.out.css +6 -0
  99. data/lib/mayu/resources/transformers/__test__/css/attributes.in.css +3 -0
  100. data/lib/mayu/resources/transformers/__test__/css/attributes.out.css +6 -0
  101. data/lib/mayu/resources/transformers/__test__/css/composes.in.css +6 -0
  102. data/lib/mayu/resources/transformers/__test__/css/composes.out.css +10 -0
  103. data/lib/mayu/resources/transformers/__test__/css/element_selectors.in.css +3 -0
  104. data/lib/mayu/resources/transformers/__test__/css/element_selectors.out.css +6 -0
  105. data/lib/mayu/resources/transformers/__test__/css/has.in.css +7 -0
  106. data/lib/mayu/resources/transformers/__test__/css/has.out.css +10 -0
  107. data/lib/mayu/resources/transformers/__test__/css/media_queries.in.css +8 -0
  108. data/lib/mayu/resources/transformers/__test__/css/media_queries.out.css +12 -0
  109. data/lib/mayu/resources/transformers/__test__/css/pseudo_classes.in.css +5 -0
  110. data/lib/mayu/resources/transformers/__test__/css/pseudo_classes.out.css +6 -0
  111. data/lib/mayu/resources/transformers/__test__/haml/README.md +10 -0
  112. data/lib/mayu/resources/transformers/__test__/haml/case.haml +8 -0
  113. data/lib/mayu/resources/transformers/__test__/haml/case.rb +15 -0
  114. data/lib/mayu/resources/transformers/__test__/haml/class_names.haml +13 -0
  115. data/lib/mayu/resources/transformers/__test__/haml/class_names.rb +26 -0
  116. data/lib/mayu/resources/transformers/__test__/haml/comments.haml +5 -0
  117. data/lib/mayu/resources/transformers/__test__/haml/comments.rb +5 -0
  118. data/lib/mayu/resources/transformers/__test__/haml/css.haml +3 -0
  119. data/lib/mayu/resources/transformers/__test__/haml/css.rb +11 -0
  120. data/lib/mayu/resources/transformers/__test__/haml/dashes.haml +3 -0
  121. data/lib/mayu/resources/transformers/__test__/haml/dashes.rb +11 -0
  122. data/lib/mayu/resources/transformers/__test__/haml/early_return.haml +4 -0
  123. data/lib/mayu/resources/transformers/__test__/haml/early_return.rb +9 -0
  124. data/lib/mayu/resources/transformers/__test__/haml/early_return2.haml +3 -0
  125. data/lib/mayu/resources/transformers/__test__/haml/early_return2.rb +6 -0
  126. data/lib/mayu/resources/transformers/__test__/haml/handlers.haml +6 -0
  127. data/lib/mayu/resources/transformers/__test__/haml/handlers.rb +12 -0
  128. data/lib/mayu/resources/transformers/__test__/haml/if_else.haml +6 -0
  129. data/lib/mayu/resources/transformers/__test__/haml/if_else.rb +12 -0
  130. data/lib/mayu/resources/transformers/__test__/haml/interpolation.haml +8 -0
  131. data/lib/mayu/resources/transformers/__test__/haml/interpolation.rb +11 -0
  132. data/lib/mayu/resources/transformers/__test__/haml/object_ref_as_key.haml +1 -0
  133. data/lib/mayu/resources/transformers/__test__/haml/object_ref_as_key.rb +5 -0
  134. data/lib/mayu/resources/transformers/__test__/haml/props.haml +4 -0
  135. data/lib/mayu/resources/transformers/__test__/haml/props.rb +11 -0
  136. data/lib/mayu/resources/transformers/__test__/haml/slots.haml +5 -0
  137. data/lib/mayu/resources/transformers/__test__/haml/slots.rb +9 -0
  138. data/lib/mayu/resources/transformers/__test__/haml/slots_dynamic.haml +3 -0
  139. data/lib/mayu/resources/transformers/__test__/haml/slots_dynamic.rb +9 -0
  140. data/lib/mayu/resources/transformers/__test__/haml/slots_fallback.haml +3 -0
  141. data/lib/mayu/resources/transformers/__test__/haml/slots_fallback.rb +5 -0
  142. data/lib/mayu/resources/transformers/__test__/haml/spacing.haml +5 -0
  143. data/lib/mayu/resources/transformers/__test__/haml/spacing.rb +14 -0
  144. data/lib/mayu/resources/transformers/__test__/haml/spacing2.haml +10 -0
  145. data/lib/mayu/resources/transformers/__test__/haml/spacing2.rb +11 -0
  146. data/lib/mayu/resources/transformers/__test__/haml/spacing3.haml +3 -0
  147. data/lib/mayu/resources/transformers/__test__/haml/spacing3.rb +10 -0
  148. data/lib/mayu/resources/transformers/css/rouge_lexer.rb +841 -0
  149. data/lib/mayu/resources/transformers/css.rb +100 -0
  150. data/lib/mayu/resources/transformers/css.test.rb +87 -0
  151. data/lib/mayu/resources/transformers/haml.rb +984 -0
  152. data/lib/mayu/resources/transformers/haml.test.rb +114 -0
  153. data/lib/mayu/resources/types/README.md +36 -0
  154. data/lib/mayu/resources/types/base.rb +35 -0
  155. data/lib/mayu/resources/types/component.rb +198 -0
  156. data/lib/mayu/resources/types/image.rb +169 -0
  157. data/lib/mayu/resources/types/javascript.rb +50 -0
  158. data/lib/mayu/resources/types/nil.rb +23 -0
  159. data/lib/mayu/resources/types/stylesheet.rb +119 -0
  160. data/lib/mayu/resources/types/svg.rb +69 -0
  161. data/lib/mayu/resources/types.rb +37 -0
  162. data/lib/mayu/routes.rb +170 -0
  163. data/lib/mayu/routing/builder.rb +108 -0
  164. data/lib/mayu/routing/matcher.rb +58 -0
  165. data/lib/mayu/routing/routes.rb +85 -0
  166. data/lib/mayu/routing.rb +17 -0
  167. data/lib/mayu/server/app.rb +494 -0
  168. data/lib/mayu/server/controller.rb +152 -0
  169. data/lib/mayu/server/errors.rb +110 -0
  170. data/lib/mayu/server/file_server.rb +140 -0
  171. data/lib/mayu/server.rb +63 -0
  172. data/lib/mayu/session.rb +358 -0
  173. data/lib/mayu/state/README.md +6 -0
  174. data/lib/mayu/state/action_creator.rb +191 -0
  175. data/lib/mayu/state/action_wrapper.rb +30 -0
  176. data/lib/mayu/state/loader.rb +220 -0
  177. data/lib/mayu/state/store.rb +82 -0
  178. data/lib/mayu/state.rb +8 -0
  179. data/lib/mayu/state.test.rb +97 -0
  180. data/lib/mayu/utils.rb +114 -0
  181. data/lib/mayu/vdom/children.rb +117 -0
  182. data/lib/mayu/vdom/component_marshaler.rb +53 -0
  183. data/lib/mayu/vdom/css_attributes.rb +131 -0
  184. data/lib/mayu/vdom/descriptor.rb +151 -0
  185. data/lib/mayu/vdom/descriptor.test.rb +26 -0
  186. data/lib/mayu/vdom/dom.rb +239 -0
  187. data/lib/mayu/vdom/h.rb +22 -0
  188. data/lib/mayu/vdom/id_generator.rb +55 -0
  189. data/lib/mayu/vdom/interfaces.rb +186 -0
  190. data/lib/mayu/vdom/marshalling.rb +78 -0
  191. data/lib/mayu/vdom/reconciliation.rb +205 -0
  192. data/lib/mayu/vdom/reconciliation.test.rb +56 -0
  193. data/lib/mayu/vdom/special_elements.rb +108 -0
  194. data/lib/mayu/vdom/update_context.rb +180 -0
  195. data/lib/mayu/vdom/vdom.perf.test.rb +146 -0
  196. data/lib/mayu/vdom/vnode.rb +266 -0
  197. data/lib/mayu/vdom/vtree.rb +672 -0
  198. data/lib/mayu/vdom/vtree.test.rb +68 -0
  199. data/lib/mayu/vdom.rb +8 -0
  200. data/lib/mayu/vdom.test.rb +73 -0
  201. data/lib/mayu/version.rb +6 -0
  202. data/lib/mayu.rb +8 -0
  203. data/mayu-live.gemspec +70 -0
  204. metadata +612 -0
@@ -0,0 +1,984 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "ripper"
5
+ require "syntax_suggest"
6
+ require "syntax_suggest/code_line"
7
+ require "syntax_suggest/explain_syntax"
8
+ require "syntax_suggest/lex_all"
9
+ require "syntax_suggest/ripper_errors"
10
+ require "syntax_tree"
11
+ require "syntax_tree/haml"
12
+ require_relative "css"
13
+
14
+ module Mayu
15
+ module Resources
16
+ module Transformers
17
+ module Haml
18
+ class MutationVisitor < SyntaxTree::Visitor::MutationVisitor
19
+ # This class visits more nodes than the parent class.
20
+ # This should probably be fixed in the syntax_tree gem,
21
+ # but I don't know what other nodes need to be fixed.
22
+
23
+ def self.build(&block)
24
+ new.tap { yield _1 }
25
+ end
26
+
27
+ def visit_assign(node)
28
+ node.copy(target: visit(node.target), value: visit(node.value))
29
+ end
30
+
31
+ def visit_assoc_splat(node)
32
+ node.copy(value: visit(node.value))
33
+ end
34
+
35
+ def visit_assoc(node)
36
+ node.copy(key: visit(node.key), value: visit(node.value))
37
+ end
38
+
39
+ def visit_aref(node)
40
+ node.copy(
41
+ collection: visit(node.collection),
42
+ index: visit(node.index)
43
+ )
44
+ end
45
+
46
+ def visit_opassign(node)
47
+ node.copy(target: visit(node.target), value: visit(node.value))
48
+ end
49
+
50
+ def visit_binary(node)
51
+ node.copy(left: visit(node.left), right: visit(node.right))
52
+ end
53
+
54
+ def visit_if_op(node)
55
+ node.copy(
56
+ predicate: visit(node.predicate),
57
+ truthy: visit(node.truthy),
58
+ falsy: visit(node.falsy)
59
+ )
60
+ end
61
+ end
62
+
63
+ class TransformResult < T::Struct
64
+ const :filename, String
65
+ const :output, String
66
+ const :content_hash, String
67
+ const :css, T.nilable(CSS::TransformResult)
68
+ const :source_map, T::Hash[String, T.untyped]
69
+ end
70
+
71
+ class TransformOptions < T::Struct
72
+ const :source, String
73
+ const :source_path, String
74
+ const :source_line, Integer
75
+
76
+ # TODO: Remove content_hash, it does not seem to be used?
77
+ const :content_hash, T.nilable(String)
78
+
79
+ const :transform_elements_to_classes, T::Boolean, default: true
80
+ const :enable_new_helper_ident, T::Boolean, default: false
81
+
82
+ def source_path_without_extension
83
+ File.join(
84
+ File.dirname(source_path),
85
+ File.basename(source_path, ".*")
86
+ ).delete_prefix("./")
87
+ end
88
+ end
89
+
90
+ extend T::Sig
91
+
92
+ sig { params(options: TransformOptions).returns(TransformResult) }
93
+ def self.transform(options)
94
+ result =
95
+ SyntaxTree::Haml.parse(options.source).accept(
96
+ Transformer.new(options)
97
+ )
98
+
99
+ TransformResult.new(
100
+ filename: options.source_path,
101
+ output: result.source,
102
+ content_hash: Digest::SHA256.digest(result.source),
103
+ css: result.styles.first,
104
+ source_map: {
105
+ }
106
+ )
107
+ end
108
+
109
+ class RubyBuilder
110
+ include SyntaxTree::DSL
111
+
112
+ def initialize(options)
113
+ @options = options
114
+ end
115
+
116
+ def assign_const(name, value) = Assign(VarField(Const(name)), value)
117
+ def self_var_ref = VarRef(Kw("self"))
118
+
119
+ def create_program(setup, styles, render)
120
+ Program(
121
+ Statements(
122
+ [
123
+ Comment("# frozen_string_literal: true", false),
124
+ assign_const("Self", setup_component(styles)),
125
+ *setup,
126
+ create_render(render)
127
+ ]
128
+ )
129
+ ).accept(StateAndPropsTransformer.new.visitor)
130
+ end
131
+
132
+ def const_path(*names)
133
+ names.reduce(nil) do |parent, name|
134
+ const = Const(name)
135
+
136
+ if T.cast(parent, T.untyped)
137
+ ConstPathRef(parent, const)
138
+ else
139
+ TopConstRef(const)
140
+ end
141
+ end
142
+ end
143
+
144
+ def setup_component(styles)
145
+ CallNode(
146
+ nil,
147
+ nil,
148
+ Ident("setup_component"),
149
+ ArgParen(
150
+ Args(
151
+ [
152
+ BareAssocHash(
153
+ assocs(
154
+ assets:
155
+ array(styles.map { string_literal(_1.filename) }),
156
+ styles: props_hash(CSS.merge_classnames(styles))
157
+ )
158
+ )
159
+ ]
160
+ )
161
+ )
162
+ )
163
+ end
164
+
165
+ def assocs(**kwargs)
166
+ kwargs.map { |key, value| Assoc(Label("#{key}:"), value) }
167
+ end
168
+
169
+ def array(elems)
170
+ ArrayLiteral(LBracket("["), Args(elems))
171
+ end
172
+
173
+ def create_render(statements)
174
+ Command(
175
+ Ident("public"),
176
+ Args(
177
+ [
178
+ DefNode(
179
+ nil,
180
+ nil,
181
+ Ident("render"),
182
+ nil,
183
+ BodyStmt(
184
+ Statements(
185
+ [
186
+ # set_fiber_local("current_component", VarRef(Kw("self"))),
187
+ *statements
188
+ ]
189
+ ),
190
+ nil,
191
+ nil,
192
+ nil,
193
+ nil
194
+ )
195
+ )
196
+ ]
197
+ ),
198
+ nil
199
+ )
200
+ end
201
+
202
+ def set_fiber_local(ident, value)
203
+ Assign(
204
+ ARefField(
205
+ VarRef(Const("Fiber")),
206
+ Args([SymbolLiteral(Ident(ident))])
207
+ ),
208
+ value
209
+ )
210
+ end
211
+
212
+ def slot(name = nil, fallback: nil)
213
+ if fallback in [_, *]
214
+ return(
215
+ MethodAddBlock(
216
+ slot(name, fallback: nil),
217
+ BlockNode(
218
+ Kw("do"),
219
+ nil,
220
+ BodyStmt(Statements(Array(fallback)), nil, nil, nil, nil)
221
+ )
222
+ )
223
+ )
224
+ end
225
+
226
+ # call_helpers(:slot, [Ident("children"), name].compact)
227
+ call_helpers(:slot, [name].compact)
228
+ end
229
+
230
+ def tag(name, children, attrs_to_merge)
231
+ ARef(
232
+ ConstPathRef(
233
+ ConstPathRef(VarRef(Const("Mayu")), Const("VDOM")),
234
+ Const("H")
235
+ ),
236
+ Args(
237
+ [
238
+ tag_name_or_class(name),
239
+ *children,
240
+ merge_props(attrs_to_merge)
241
+ ].flatten.compact
242
+ )
243
+ )
244
+ end
245
+
246
+ def tag_name_or_class(name)
247
+ case name
248
+ in /\A[A-Z]/
249
+ Ident(name)
250
+ else
251
+ SymbolLiteral(Ident(name))
252
+ end
253
+ end
254
+
255
+ def splat_hash(node)
256
+ BareAssocHash([AssocSplat(node)])
257
+ end
258
+
259
+ def merge_props(attrs_to_merge)
260
+ return if attrs_to_merge.empty?
261
+
262
+ splat_hash(call_helpers(:merge_props, attrs_to_merge))
263
+ end
264
+
265
+ def first_or_array(nodes)
266
+ case nodes
267
+ in [node]
268
+ node
269
+ else
270
+ ArrayLiteral(LBracket("["), Args(nodes))
271
+ end
272
+ end
273
+
274
+ def sym(str)
275
+ if str.match(/\A[\w_]+\z/)
276
+ SymbolLiteral(Ident(str))
277
+ else
278
+ DynaSymbol([TStringContent(str)], '"')
279
+ end
280
+ end
281
+
282
+ def props_hash(attrs)
283
+ HashLiteral(
284
+ LBrace("{"),
285
+ attrs.map do |key, value|
286
+ if key.to_s == "class" && value in String
287
+ Assoc(
288
+ SymbolLiteral(Ident(key.to_s)),
289
+ first_or_array(value.split.map { sym(_1) })
290
+ )
291
+ else
292
+ Assoc(
293
+ sym(key.to_s),
294
+ case value
295
+ in Symbol
296
+ SymbolLiteral(Ident(value.to_s))
297
+ in String
298
+ StringLiteral([TStringContent(value.to_s)], :'"')
299
+ in SyntaxTree::ArrayLiteral
300
+ value
301
+ in TrueClass | FalseClass | NilClass
302
+ VarRef(Kw(value.to_s))
303
+ end
304
+ )
305
+ end
306
+ end
307
+ )
308
+ end
309
+
310
+ def try_split_string_literal(node)
311
+ case node
312
+ in SyntaxTree::StringLiteral
313
+ split_string_literal(node)
314
+ in [SyntaxTree::StringLiteral => node]
315
+ split_string_literal(node)
316
+ else
317
+ node
318
+ end
319
+ end
320
+
321
+ def split_string_literal(string_literal)
322
+ string_literal
323
+ # string_literal
324
+ # .parts
325
+ # .map do |part|
326
+ # case part
327
+ # in SyntaxTree::TStringContent
328
+ # string_literal(part.value)
329
+ # in SyntaxTree::StringEmbExpr
330
+ # part.statements
331
+ # end
332
+ # end
333
+ # .flatten
334
+ end
335
+
336
+ def ruby_script(statements)
337
+ case statements
338
+ in []
339
+ nil
340
+ in [SyntaxTree::StringLiteral => string_literal]
341
+ split_string_literal(string_literal)
342
+ in [statement]
343
+ statement
344
+ else
345
+ Begin(BodyStmt(Statements(statements), nil, nil, nil, nil))
346
+ end
347
+ end
348
+
349
+ def silent(node)
350
+ case node
351
+ in SyntaxTree::ReturnNode
352
+ node
353
+ else
354
+ Begin(
355
+ BodyStmt(
356
+ Statements([node, VarRef(Kw("nil"))]),
357
+ nil,
358
+ nil,
359
+ nil,
360
+ nil
361
+ )
362
+ )
363
+ end
364
+ end
365
+
366
+ def compute(node)
367
+ # MethodAddBlock(
368
+ # call_helpers(:compute),
369
+ # BlockNode(
370
+ # Kw("do"),
371
+ # nil,
372
+ # BodyStmt(Statements([node]), nil, nil, nil, nil)
373
+ # )
374
+ # )
375
+ node
376
+ end
377
+
378
+ def mayu_const_path
379
+ ConstPathRef(VarRef(Const("Mayu")), Const("VDOM"))
380
+ # Const("Mayu")
381
+ end
382
+
383
+ def call_helpers(method, *args)
384
+ CallNode(
385
+ Ident("mayu"),
386
+ Period("."),
387
+ Ident(method.to_s),
388
+ wrap_args(args.flatten.compact)
389
+ )
390
+ end
391
+
392
+ def helper_ident
393
+ if @options.enable_new_helper_ident
394
+ CallNode(VarRef(Kw("self")), Period("."), Ident("Mayu"), nil)
395
+ else
396
+ Ident("mayu")
397
+ end
398
+ end
399
+
400
+ def wrap_args(args)
401
+ args.empty? ? nil : ArgParen(Args(args))
402
+ end
403
+
404
+ def string_literal(value) =
405
+ StringLiteral([TStringContent(value.to_s)], '"')
406
+ def call_freeze(node) =
407
+ CallNode(node, Period("."), Ident("freeze"), nil)
408
+ end
409
+
410
+ class ParseError < StandardError
411
+ end
412
+
413
+ class Transformer < SyntaxTree::Haml::Visitor
414
+ class Result < T::Struct
415
+ const :program, SyntaxTree::Program
416
+ const :styles, T::Array[CSS::TransformResult]
417
+
418
+ def source
419
+ SyntaxTree::Formatter.format("", program)
420
+ end
421
+ end
422
+
423
+ def initialize(options)
424
+ @options = options
425
+ @builder = RubyBuilder.new(options)
426
+ @state = {}
427
+ end
428
+
429
+ def visit_root(node)
430
+ setup = []
431
+ styles = []
432
+ render = []
433
+
434
+ node.children.each do |child|
435
+ case child
436
+ in { type: :filter, value: { name: "ruby" } }
437
+ if setup.empty? && styles.empty?
438
+ setup.push(child)
439
+ else
440
+ render.push(child)
441
+ end
442
+ in type: :script | :silent_script
443
+ render.push(child)
444
+ in { type: :filter, value: { name: "css" } }
445
+ styles.push(child.accept(self))
446
+ in type: :tag
447
+ render.push(child)
448
+ end
449
+ end
450
+
451
+ Result.new(
452
+ program:
453
+ @builder.create_program(
454
+ group_control_statements(setup),
455
+ styles,
456
+ group_control_statements(render)
457
+ ),
458
+ styles:
459
+ )
460
+ end
461
+
462
+ def visit_haml_comment(node)
463
+ nil
464
+ end
465
+
466
+ def visit_slot_tag(node)
467
+ node.value => { attributes:, dynamic_attributes: }
468
+
469
+ name = nil
470
+
471
+ if new = dynamic_attributes.new
472
+ parse_ruby(dynamic_attributes.new) => [parsed_attributes]
473
+ hash = parsed_attributes.accept(HashKeyExtractorVisitor.new)
474
+
475
+ name = hash[:name] || hash["name"]
476
+ end
477
+
478
+ if attr = attributes["name"]
479
+ name ||= @builder.string_literal(attr)
480
+ end
481
+
482
+ return(
483
+ @builder.slot(
484
+ name,
485
+ fallback: node.children.map { _1.accept(self) }
486
+ )
487
+ )
488
+ end
489
+
490
+ def visit_tag(node)
491
+ node.value => {
492
+ name:, attributes:, dynamic_attributes:, self_closing:, value:
493
+ }
494
+
495
+ return visit_slot_tag(node) if name == "slot"
496
+
497
+ attrs = []
498
+
499
+ if @options.transform_elements_to_classes
500
+ attrs.push(@builder.props_hash(class: :"__#{name}"))
501
+ end
502
+
503
+ attrs.push(@builder.props_hash(attributes)) unless attributes.empty?
504
+
505
+ if old = dynamic_attributes.old
506
+ attrs.push(*parse_ruby(old))
507
+ end
508
+
509
+ if new = dynamic_attributes.new
510
+ attrs.push(
511
+ *parse_ruby(new)
512
+ .map { _1.accept(string_keys_to_labels_mutation_visitor) }
513
+ .map { _1.accept(wrap_handler_mutation_visitor) }
514
+ )
515
+ end
516
+
517
+ if object_ref = node.value[:object_ref]
518
+ unless object_ref == :nil
519
+ parse_ruby(object_ref) => [key]
520
+ attrs.push(@builder.props_hash(key:))
521
+ end
522
+ end
523
+
524
+ @builder.tag(
525
+ name,
526
+ if value
527
+ if node.value[:parse]
528
+ parse_ruby(value, fix: false) => statements
529
+ @builder.ruby_script(statements)
530
+ elsif !value.empty?
531
+ @builder.string_literal(value.to_s)
532
+ end
533
+ else
534
+ visit_tag_children(node.children)
535
+ end,
536
+ attrs
537
+ )
538
+ end
539
+
540
+ def visit_tag_children(children)
541
+ children
542
+ .reject { _1 in { type: :plain, value: { text: "" } } }
543
+ .then { join_plain_nodes(_1) }
544
+ .then { prepend_whitespace(_1) }
545
+ .then { append_whitespace(_1) }
546
+ .then { group_control_statements(_1) }
547
+ .flatten
548
+ end
549
+
550
+ def join_plain_nodes(children)
551
+ children
552
+ .chunk_while do |prev, curr|
553
+ (
554
+ (prev in { type: :plain, value: { text: prev_text } }) &&
555
+ (curr in { type: :plain, value: { text: new_text } })
556
+ )
557
+ end
558
+ .map do |chunk|
559
+ case chunk
560
+ in [{ type: :plain } => first, *]
561
+ text = chunk.map { _1.value[:text].to_s.strip }.join(" ")
562
+ first.value[:text] = text
563
+ first
564
+ else
565
+ chunk
566
+ end
567
+ end
568
+ .flatten
569
+ .compact
570
+ end
571
+
572
+ IN_RE = /\A\s*in\s+/
573
+
574
+ def group_control_statements(children)
575
+ children
576
+ .chunk_while do |a, b|
577
+ case [a, b]
578
+ in [
579
+ { type: :script, value: { keyword: "if" | "elsif" } },
580
+ { type: :script, value: { keyword: "elsif" | "else" } }
581
+ ]
582
+ true
583
+ in [
584
+ { type: :script, value: { keyword: "case" | "when" } },
585
+ { type: :script, value: { keyword: "when" | "else" } }
586
+ ]
587
+ true
588
+ in [
589
+ {
590
+ type: :script,
591
+ value: { keyword: "case" } | { text: IN_RE }
592
+ },
593
+ {
594
+ type: :script,
595
+ value: { keyword: "else" } | { text: IN_RE }
596
+ }
597
+ ]
598
+ true
599
+ in [
600
+ { type: :script, value: { keyword: "begin" } },
601
+ {
602
+ type: :script,
603
+ value: { keyword: "rescue" | "else" | "ensure" }
604
+ }
605
+ ]
606
+ true
607
+ in [
608
+ { type: :script, value: { keyword: "rescue" } },
609
+ { type: :script, value: { keyword: "else" | "ensure" } }
610
+ ]
611
+ true
612
+ in [
613
+ { type: :script, value: { keyword: "else" } },
614
+ { type: :script, value: { keyword: "ensure" } }
615
+ ]
616
+ true
617
+ else
618
+ false
619
+ end
620
+ end
621
+ .map do |chunk|
622
+ case chunk
623
+ in [{ type: :script, value: { keyword: "if" } }, *]
624
+ @builder.compute(group_condition(:if, chunk))
625
+ in [{ type: :script, value: { keyword: "case" } }, *]
626
+ @builder.compute(group_condition(:case, chunk))
627
+ in [{ type: :script, value: { keyword: "begin" } }, *]
628
+ @builder.compute(group_condition(:begin, chunk))
629
+ else
630
+ chunk.map { _1.accept(self) }
631
+ end
632
+ end
633
+ .flatten
634
+ .compact
635
+ end
636
+
637
+ def group_condition(type, chunk)
638
+ parse_ruby(join_ruby_script_nodes(chunk), fix: true) => [statement]
639
+
640
+ visitor = SyntaxTree::Visitor::MutationVisitor.new
641
+
642
+ chunk.shift if type == :case
643
+
644
+ visitor.mutate("Statements") do |node|
645
+ top = chunk.shift
646
+
647
+ if node.child_nodes in [SyntaxTree::VoidStmt]
648
+ @builder.Statements(visit_tag_children(top.children))
649
+ else
650
+ unless top.children.empty?
651
+ raise "Line #{top.line} should not have children."
652
+ end
653
+
654
+ node
655
+ end
656
+ end
657
+
658
+ @builder.ruby_script([statement.accept(visitor)])
659
+ end
660
+
661
+ def join_ruby_script_nodes(nodes)
662
+ nodes.map { _1.value[:text] }.join("\n")
663
+ end
664
+
665
+ def prepend_whitespace(children)
666
+ [nil, *children].each_cons(2)
667
+ .map do |prev, curr|
668
+ if prev in {
669
+ type: :tag, value: { nuke_outer_whitespace: true }
670
+ }
671
+ if curr in { type: :plain, value: { text: } }
672
+ curr.value = { text: " #{text}" }
673
+ else
674
+ next make_space(curr), curr
675
+ end
676
+ end
677
+
678
+ curr
679
+ end
680
+ end
681
+
682
+ def append_whitespace(children)
683
+ [*children, nil].each_cons(2)
684
+ .flat_map do |curr, succ|
685
+ if succ in {
686
+ type: :tag, value: { nuke_inner_whitespace: true }
687
+ }
688
+ if curr in { type: :plain, value: { text: } }
689
+ curr.value = { text: "#{text} " }
690
+ else
691
+ next curr, make_space(curr)
692
+ end
693
+ end
694
+
695
+ curr
696
+ end
697
+ end
698
+
699
+ def make_space(ref_node)
700
+ ::Haml::Parser::ParseNode.new(
701
+ :plain,
702
+ ref_node.line,
703
+ { text: " " },
704
+ ref_node.parent,
705
+ []
706
+ )
707
+ end
708
+
709
+ def visit_filter(node)
710
+ case node.value
711
+ in { name: "ruby", text: }
712
+ @builder.ruby_script(parse_ruby(text)) if text
713
+ in { name: "css", text: }
714
+ CSS.transform(
715
+ source: text,
716
+ source_path:
717
+ @options.source_path_without_extension + ".haml (inline css)",
718
+ source_line: node.line
719
+ )
720
+ in { name: "plain", text: }
721
+ case text.inspect.each_line.to_a
722
+ in []
723
+ # noop
724
+ in [line]
725
+ @builder.string_literal(text)
726
+ in [*lines]
727
+ @builder.Heredoc(lines.map { @builder.TStringContent(_1) })
728
+ end
729
+ end
730
+ end
731
+
732
+ def visit_plain(node)
733
+ node.value => { text: }
734
+ @builder.string_literal(CGI.escape_html(text))
735
+ end
736
+
737
+ def visit_script(node)
738
+ case node.value[:text].strip
739
+ when /\Areturn\s+(?<type>if|unless)\s+(?<condition_source>.+)/
740
+ $~ => { type:, condition_source: }
741
+
742
+ parse_ruby(condition_source, fix: true) => [condition]
743
+
744
+ statements =
745
+ @builder.Statements(
746
+ [
747
+ @builder.ReturnNode(
748
+ @builder.Args(visit_tag_children(node.children))
749
+ )
750
+ ]
751
+ )
752
+
753
+ case type
754
+ in "if"
755
+ @builder.IfNode(condition, statements, nil)
756
+ in "unless"
757
+ @builder.UnlessNode(condition, statements, nil)
758
+ end
759
+ when /\Areturn/
760
+ @builder.ReturnNode(
761
+ @builder.Args(visit_tag_children(node.children))
762
+ )
763
+ else
764
+ @builder.compute(transform_script_node(node))
765
+ end
766
+ end
767
+
768
+ def with_state(name, value, &block)
769
+ @state[name], prev = value, @state[name]
770
+ yield prev
771
+ ensure
772
+ @state[name] = prev
773
+ end
774
+
775
+ def visit_silent_script(node)
776
+ with_state(:is_silent, true) do |was_silent|
777
+ if was_silent
778
+ visit_script(node)
779
+ else
780
+ @builder.silent(visit_script(node))
781
+ end
782
+ end
783
+ end
784
+
785
+ def transform_script_node(node)
786
+ source = node.value.fetch(:text).strip
787
+
788
+ if node.children.empty?
789
+ parse_ruby(source, fix: false) => statements
790
+ return @builder.ruby_script(statements)
791
+ end
792
+
793
+ parse_ruby(source, fix: true) => [statement]
794
+
795
+ visitor = SyntaxTree::Visitor::MutationVisitor.new
796
+
797
+ visitor.mutate("Statements[body: [VoidStmt]]") do
798
+ @builder.Statements(visit_tag_children(node.children))
799
+ end
800
+
801
+ @builder.ruby_script([statement.accept(visitor)])
802
+ end
803
+
804
+ def parse_ruby(source, fix: false)
805
+ source = fix_syntax_by_adding_missing_pairs(source) if fix
806
+
807
+ SyntaxTree.parse(source).statements.body
808
+ rescue SyntaxTree::Parser::ParseError => e
809
+ explain =
810
+ SyntaxSuggest::ExplainSyntax.new(
811
+ code_lines: SyntaxSuggest::CodeLine.from_source(source)
812
+ ).call
813
+
814
+ msg = ["Failed parsing Ruby: #{source}"]
815
+
816
+ msg.push <<~MSG unless explain.errors.empty?
817
+ Errors:
818
+ #{explain.errors.join(" \n")}
819
+ MSG
820
+
821
+ msg.push <<~MSG unless explain.missing.empty?
822
+ Missing:
823
+ #{explain.missing.map { explain.why(_1) }.join(" \n")}
824
+ MSG
825
+
826
+ raise ParseError, "\n#{msg.join("\n")}"
827
+ end
828
+
829
+ def fix_syntax_by_adding_missing_pairs(source)
830
+ left_right = SyntaxSuggest::LeftRightLexCount.new
831
+ SyntaxSuggest::LexAll.new(source:).each { left_right.count_lex(_1) }
832
+ left_right.missing
833
+ [source, *left_right.missing].join("\n")
834
+ end
835
+
836
+ def wrap_handler_mutation_visitor
837
+ visitor = SyntaxTree::Visitor::MutationVisitor.new
838
+
839
+ visitor.mutate(
840
+ "Assoc[key: Label, value: VCall[value: Ident]]"
841
+ ) do |assoc|
842
+ if assoc.key.value.start_with?("on")
843
+ handler_name = @builder.SymbolLiteral(assoc.value.value)
844
+
845
+ @builder.Assoc(
846
+ assoc.key,
847
+ @builder.call_helpers(:handler, [handler_name])
848
+ )
849
+ else
850
+ assoc
851
+ end
852
+ end
853
+
854
+ visitor
855
+ end
856
+
857
+ def string_keys_to_labels_mutation_visitor
858
+ visitor = SyntaxTree::Visitor::MutationVisitor.new
859
+
860
+ visitor.mutate("Assoc[key: StringLiteral]") do |assoc|
861
+ @builder.Assoc(
862
+ @builder.Label(
863
+ assoc.key.parts.map(&:value).join.gsub("-", "_") + ":"
864
+ ),
865
+ assoc.value
866
+ )
867
+ end
868
+
869
+ visitor
870
+ end
871
+ end
872
+
873
+ class StateAndPropsTransformer
874
+ include SyntaxTree::DSL
875
+
876
+ COLLECTIONS = {
877
+ SyntaxTree::IVar => "state",
878
+ SyntaxTree::GVar => "props"
879
+ }
880
+
881
+ def visitor
882
+ MutationVisitor.build do |visitor|
883
+ visitor.mutate(
884
+ "VarRef[value: GVar[value: /\\A\\$\\w+/]]"
885
+ ) { |var_ref| aref(var_ref.value) }
886
+
887
+ visitor.mutate(
888
+ "Assign[target: VarField[value: GVar]]"
889
+ ) do |assign|
890
+ assign => { target: { target: { value: var_name } } }
891
+ loc = assign.target.location
892
+ raise "Can not write to prop #{var_name} on line #{loc.start_line} col #{loc.start_column}"
893
+ end
894
+
895
+ visitor.mutate("VarRef[value: IVar]") do |var_ref|
896
+ aref(var_ref.value)
897
+ end
898
+
899
+ # visitor.mutate(
900
+ # "OpAssign[target: VarField[value: IVar]]"
901
+ # ) do |assign|
902
+ # assign.copy(target: aref_field(assign.target.value))
903
+ # end
904
+ #
905
+ # visitor.mutate(
906
+ # "Assign[target: VarField[value: IVar]]"
907
+ # ) do |assign|
908
+ # assign.copy(target: aref_field(assign.target.value))
909
+ # end
910
+ end
911
+ end
912
+
913
+ private
914
+
915
+ def aref(node)
916
+ ARef(
917
+ call_self(COLLECTIONS.fetch(node.class)),
918
+ Args([var_to_symbol(node)])
919
+ )
920
+ end
921
+
922
+ def aref_field(node)
923
+ ARefField(
924
+ call_self(COLLECTIONS.fetch(node.class)),
925
+ Args([var_to_symbol(node)])
926
+ )
927
+ end
928
+
929
+ def call_self(method)
930
+ CallNode(VarRef(Kw("self")), Period("."), Ident(method), nil)
931
+ end
932
+
933
+ def var_to_symbol(node)
934
+ SymbolLiteral(Ident(strip_var_prefix(node.value)))
935
+ end
936
+
937
+ def strip_var_prefix(str)
938
+ str[/\A[@$]?(.*)/, 1]
939
+ end
940
+ end
941
+ class HashKeyExtractorVisitor
942
+ extend T::Sig
943
+
944
+ sig do
945
+ params(node: SyntaxTree::HashLiteral).returns(
946
+ T::Hash[T.untyped, T.untyped]
947
+ )
948
+ end
949
+ def visit_hash(node)
950
+ hash = {}
951
+
952
+ node.assocs.each do |child|
953
+ if extract_key(child.key) in key
954
+ hash[key] = extract_value(child.value)
955
+ end
956
+ end
957
+
958
+ hash
959
+ end
960
+
961
+ sig do
962
+ params(node: SyntaxTree::Node).returns(
963
+ T.nilable(T.any(String, Symbol))
964
+ )
965
+ end
966
+ def extract_key(node)
967
+ case node
968
+ when SyntaxTree::StringLiteral
969
+ node.parts => [{ value: }]
970
+ value
971
+ when SyntaxTree::Label
972
+ node.value
973
+ end
974
+ end
975
+
976
+ sig { params(node: SyntaxTree::Node).returns(T.untyped) }
977
+ def extract_value(node)
978
+ node
979
+ end
980
+ end
981
+ end
982
+ end
983
+ end
984
+ end