maquina_stream 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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.rdoc_options +30 -0
  3. data/CHANGELOG.md +38 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +163 -0
  6. data/app/assets/stylesheets/maquina_stream/components/attachment.css +35 -0
  7. data/app/assets/stylesheets/maquina_stream/components/code_block.css +30 -0
  8. data/app/assets/stylesheets/maquina_stream/components/shimmer.css +31 -0
  9. data/app/assets/stylesheets/maquina_stream/components/snippet.css +22 -0
  10. data/app/assets/stylesheets/maquina_stream/components/source_citation.css +14 -0
  11. data/app/assets/stylesheets/maquina_stream/components/suggestion.css +32 -0
  12. data/app/assets/stylesheets/maquina_stream/reveal.css +48 -0
  13. data/app/assets/stylesheets/maquina_stream/themes/dark.css +235 -0
  14. data/app/assets/stylesheets/maquina_stream/themes/light.css +117 -0
  15. data/app/controllers/maquina_stream/application_controller.rb +21 -0
  16. data/app/controllers/maquina_stream/blocks_controller.rb +41 -0
  17. data/app/controllers/maquina_stream/manifests_controller.rb +15 -0
  18. data/app/helpers/maquina_stream/components_helper.rb +80 -0
  19. data/app/javascript/maquina_stream/controllers/application_controller.js +169 -0
  20. data/app/javascript/maquina_stream/controllers/ms_autoscroll_controller.js +110 -0
  21. data/app/javascript/maquina_stream/controllers/ms_code_controller.js +96 -0
  22. data/app/javascript/maquina_stream/controllers/ms_deferred_controller.js +223 -0
  23. data/app/javascript/maquina_stream/controllers/ms_diagram_controller.js +40 -0
  24. data/app/javascript/maquina_stream/controllers/ms_link_safety_controller.js +196 -0
  25. data/app/javascript/maquina_stream/controllers/ms_math_controller.js +32 -0
  26. data/app/javascript/maquina_stream/controllers/ms_repair_controller.js +167 -0
  27. data/app/javascript/maquina_stream/controllers/ms_reveal_controller.js +320 -0
  28. data/app/javascript/maquina_stream/controllers/ms_table_controller.js +183 -0
  29. data/app/javascript/maquina_stream/index.js +59 -0
  30. data/app/views/maquina_stream/components/_attachment.html.erb +139 -0
  31. data/app/views/maquina_stream/components/_code_block.html.erb +74 -0
  32. data/app/views/maquina_stream/components/_shimmer.html.erb +36 -0
  33. data/app/views/maquina_stream/components/_snippet.html.erb +50 -0
  34. data/app/views/maquina_stream/components/_source_citation.html.erb +42 -0
  35. data/app/views/maquina_stream/components/_suggestion.html.erb +73 -0
  36. data/config/importmap.rb +10 -0
  37. data/config/locales/en.yml +79 -0
  38. data/config/locales/es.yml +82 -0
  39. data/config/routes.rb +11 -0
  40. data/docs/configuration.md +219 -0
  41. data/docs/deferred-renderers.md +184 -0
  42. data/docs/getting-started.md +356 -0
  43. data/docs/javascript.md +298 -0
  44. data/docs/registries.md +283 -0
  45. data/docs/repair.md +162 -0
  46. data/docs/security.md +247 -0
  47. data/docs/streaming.md +308 -0
  48. data/lib/generators/maquina_stream/install/USAGE +26 -0
  49. data/lib/generators/maquina_stream/install/install_generator.rb +199 -0
  50. data/lib/generators/maquina_stream/install/templates/initializer.rb.tt +121 -0
  51. data/lib/generators/maquina_stream/streamable/USAGE +28 -0
  52. data/lib/generators/maquina_stream/streamable/streamable_generator.rb +187 -0
  53. data/lib/generators/maquina_stream/streamable/templates/migration.rb.tt +21 -0
  54. data/lib/generators/maquina_stream/streamable/templates/model.rb.tt +4 -0
  55. data/lib/maquina_stream/block.rb +99 -0
  56. data/lib/maquina_stream/broadcaster.rb +233 -0
  57. data/lib/maquina_stream/component_cache.rb +0 -0
  58. data/lib/maquina_stream/components/contract.rb +184 -0
  59. data/lib/maquina_stream/components.rb +135 -0
  60. data/lib/maquina_stream/configuration.rb +240 -0
  61. data/lib/maquina_stream/document.rb +296 -0
  62. data/lib/maquina_stream/engine.rb +46 -0
  63. data/lib/maquina_stream/errors.rb +17 -0
  64. data/lib/maquina_stream/export.rb +66 -0
  65. data/lib/maquina_stream/frame.rb +73 -0
  66. data/lib/maquina_stream/manifest.rb +137 -0
  67. data/lib/maquina_stream/registries.rb +116 -0
  68. data/lib/maquina_stream/renderer/fence.rb +115 -0
  69. data/lib/maquina_stream/renderer/post_pass.rb +363 -0
  70. data/lib/maquina_stream/renderer/tag_blocks.rb +276 -0
  71. data/lib/maquina_stream/renderer/view_context.rb +72 -0
  72. data/lib/maquina_stream/renderer.rb +128 -0
  73. data/lib/maquina_stream/sanitizer.rb +392 -0
  74. data/lib/maquina_stream/streamable.rb +281 -0
  75. data/lib/maquina_stream/text_direction.rb +56 -0
  76. data/lib/maquina_stream/themes.rb +84 -0
  77. data/lib/maquina_stream/version.rb +5 -0
  78. data/lib/maquina_stream.rb +175 -0
  79. metadata +204 -0
@@ -0,0 +1,235 @@
1
+ /* Generated by `rake maquina_stream:themes`. Do not edit by hand. */
2
+ [data-theme="dark"] [data-ms-code] table td { padding: 5px; }
3
+ [data-theme="dark"] [data-ms-code] table pre { margin: 0; }
4
+ [data-theme="dark"] [data-ms-code], [data-theme="dark"] [data-ms-code] .w {
5
+ color: #c9d1d9;
6
+ background-color: #161b22;
7
+ }
8
+ [data-theme="dark"] [data-ms-code] .k, [data-theme="dark"] [data-ms-code] .kd, [data-theme="dark"] [data-ms-code] .kn, [data-theme="dark"] [data-ms-code] .kp, [data-theme="dark"] [data-ms-code] .kr, [data-theme="dark"] [data-ms-code] .kt, [data-theme="dark"] [data-ms-code] .kv {
9
+ color: #ff7b72;
10
+ }
11
+ [data-theme="dark"] [data-ms-code] .gr {
12
+ color: #f0f6fc;
13
+ }
14
+ [data-theme="dark"] [data-ms-code] .gd {
15
+ color: #ffdcd7;
16
+ background-color: #67060c;
17
+ }
18
+ [data-theme="dark"] [data-ms-code] .nb {
19
+ color: #ffa657;
20
+ }
21
+ [data-theme="dark"] [data-ms-code] .nc {
22
+ color: #ffa657;
23
+ }
24
+ [data-theme="dark"] [data-ms-code] .no {
25
+ color: #ffa657;
26
+ }
27
+ [data-theme="dark"] [data-ms-code] .nn {
28
+ color: #ffa657;
29
+ }
30
+ [data-theme="dark"] [data-ms-code] .sr {
31
+ color: #7ee787;
32
+ }
33
+ [data-theme="dark"] [data-ms-code] .na {
34
+ color: #7ee787;
35
+ }
36
+ [data-theme="dark"] [data-ms-code] .nt {
37
+ color: #7ee787;
38
+ }
39
+ [data-theme="dark"] [data-ms-code] .gi {
40
+ color: #aff5b4;
41
+ background-color: #033a16;
42
+ }
43
+ [data-theme="dark"] [data-ms-code] .ges {
44
+ font-weight: bold;
45
+ font-style: italic;
46
+ }
47
+ [data-theme="dark"] [data-ms-code] .kc {
48
+ color: #79c0ff;
49
+ }
50
+ [data-theme="dark"] [data-ms-code] .l, [data-theme="dark"] [data-ms-code] .ld, [data-theme="dark"] [data-ms-code] .m, [data-theme="dark"] [data-ms-code] .mb, [data-theme="dark"] [data-ms-code] .mf, [data-theme="dark"] [data-ms-code] .mh, [data-theme="dark"] [data-ms-code] .mi, [data-theme="dark"] [data-ms-code] .il, [data-theme="dark"] [data-ms-code] .mo, [data-theme="dark"] [data-ms-code] .mx {
51
+ color: #79c0ff;
52
+ }
53
+ [data-theme="dark"] [data-ms-code] .sb {
54
+ color: #79c0ff;
55
+ }
56
+ [data-theme="dark"] [data-ms-code] .bp {
57
+ color: #79c0ff;
58
+ }
59
+ [data-theme="dark"] [data-ms-code] .ne {
60
+ color: #79c0ff;
61
+ }
62
+ [data-theme="dark"] [data-ms-code] .nl {
63
+ color: #79c0ff;
64
+ }
65
+ [data-theme="dark"] [data-ms-code] .py {
66
+ color: #79c0ff;
67
+ }
68
+ [data-theme="dark"] [data-ms-code] .nv, [data-theme="dark"] [data-ms-code] .vc, [data-theme="dark"] [data-ms-code] .vg, [data-theme="dark"] [data-ms-code] .vi, [data-theme="dark"] [data-ms-code] .vm {
69
+ color: #79c0ff;
70
+ }
71
+ [data-theme="dark"] [data-ms-code] .o, [data-theme="dark"] [data-ms-code] .ow {
72
+ color: #79c0ff;
73
+ }
74
+ [data-theme="dark"] [data-ms-code] .gh {
75
+ color: #1f6feb;
76
+ font-weight: bold;
77
+ }
78
+ [data-theme="dark"] [data-ms-code] .gu {
79
+ color: #1f6feb;
80
+ font-weight: bold;
81
+ }
82
+ [data-theme="dark"] [data-ms-code] .s, [data-theme="dark"] [data-ms-code] .sa, [data-theme="dark"] [data-ms-code] .sc, [data-theme="dark"] [data-ms-code] .dl, [data-theme="dark"] [data-ms-code] .sd, [data-theme="dark"] [data-ms-code] .s2, [data-theme="dark"] [data-ms-code] .se, [data-theme="dark"] [data-ms-code] .sh, [data-theme="dark"] [data-ms-code] .sx, [data-theme="dark"] [data-ms-code] .s1, [data-theme="dark"] [data-ms-code] .ss {
83
+ color: #a5d6ff;
84
+ }
85
+ [data-theme="dark"] [data-ms-code] .nd {
86
+ color: #d2a8ff;
87
+ }
88
+ [data-theme="dark"] [data-ms-code] .nf, [data-theme="dark"] [data-ms-code] .fm {
89
+ color: #d2a8ff;
90
+ }
91
+ [data-theme="dark"] [data-ms-code] .err {
92
+ color: #f0f6fc;
93
+ background-color: #8e1519;
94
+ }
95
+ [data-theme="dark"] [data-ms-code] .c, [data-theme="dark"] [data-ms-code] .ch, [data-theme="dark"] [data-ms-code] .cd, [data-theme="dark"] [data-ms-code] .cm, [data-theme="dark"] [data-ms-code] .cp, [data-theme="dark"] [data-ms-code] .cpf, [data-theme="dark"] [data-ms-code] .c1, [data-theme="dark"] [data-ms-code] .cs {
96
+ color: #8b949e;
97
+ }
98
+ [data-theme="dark"] [data-ms-code] .gl {
99
+ color: #8b949e;
100
+ }
101
+ [data-theme="dark"] [data-ms-code] .gt {
102
+ color: #8b949e;
103
+ }
104
+ [data-theme="dark"] [data-ms-code] .ni {
105
+ color: #c9d1d9;
106
+ }
107
+ [data-theme="dark"] [data-ms-code] .si {
108
+ color: #c9d1d9;
109
+ }
110
+ [data-theme="dark"] [data-ms-code] .ge {
111
+ color: #c9d1d9;
112
+ font-style: italic;
113
+ }
114
+ [data-theme="dark"] [data-ms-code] .gs {
115
+ color: #c9d1d9;
116
+ font-weight: bold;
117
+ }
118
+ @media (prefers-color-scheme: dark) {
119
+ :root:not([data-theme="light"]) [data-ms-code] table td { padding: 5px; }
120
+ :root:not([data-theme="light"]) [data-ms-code] table pre { margin: 0; }
121
+ :root:not([data-theme="light"]) [data-ms-code], :root:not([data-theme="light"]) [data-ms-code] .w {
122
+ color: #c9d1d9;
123
+ background-color: #161b22;
124
+ }
125
+ :root:not([data-theme="light"]) [data-ms-code] .k, :root:not([data-theme="light"]) [data-ms-code] .kd, :root:not([data-theme="light"]) [data-ms-code] .kn, :root:not([data-theme="light"]) [data-ms-code] .kp, :root:not([data-theme="light"]) [data-ms-code] .kr, :root:not([data-theme="light"]) [data-ms-code] .kt, :root:not([data-theme="light"]) [data-ms-code] .kv {
126
+ color: #ff7b72;
127
+ }
128
+ :root:not([data-theme="light"]) [data-ms-code] .gr {
129
+ color: #f0f6fc;
130
+ }
131
+ :root:not([data-theme="light"]) [data-ms-code] .gd {
132
+ color: #ffdcd7;
133
+ background-color: #67060c;
134
+ }
135
+ :root:not([data-theme="light"]) [data-ms-code] .nb {
136
+ color: #ffa657;
137
+ }
138
+ :root:not([data-theme="light"]) [data-ms-code] .nc {
139
+ color: #ffa657;
140
+ }
141
+ :root:not([data-theme="light"]) [data-ms-code] .no {
142
+ color: #ffa657;
143
+ }
144
+ :root:not([data-theme="light"]) [data-ms-code] .nn {
145
+ color: #ffa657;
146
+ }
147
+ :root:not([data-theme="light"]) [data-ms-code] .sr {
148
+ color: #7ee787;
149
+ }
150
+ :root:not([data-theme="light"]) [data-ms-code] .na {
151
+ color: #7ee787;
152
+ }
153
+ :root:not([data-theme="light"]) [data-ms-code] .nt {
154
+ color: #7ee787;
155
+ }
156
+ :root:not([data-theme="light"]) [data-ms-code] .gi {
157
+ color: #aff5b4;
158
+ background-color: #033a16;
159
+ }
160
+ :root:not([data-theme="light"]) [data-ms-code] .ges {
161
+ font-weight: bold;
162
+ font-style: italic;
163
+ }
164
+ :root:not([data-theme="light"]) [data-ms-code] .kc {
165
+ color: #79c0ff;
166
+ }
167
+ :root:not([data-theme="light"]) [data-ms-code] .l, :root:not([data-theme="light"]) [data-ms-code] .ld, :root:not([data-theme="light"]) [data-ms-code] .m, :root:not([data-theme="light"]) [data-ms-code] .mb, :root:not([data-theme="light"]) [data-ms-code] .mf, :root:not([data-theme="light"]) [data-ms-code] .mh, :root:not([data-theme="light"]) [data-ms-code] .mi, :root:not([data-theme="light"]) [data-ms-code] .il, :root:not([data-theme="light"]) [data-ms-code] .mo, :root:not([data-theme="light"]) [data-ms-code] .mx {
168
+ color: #79c0ff;
169
+ }
170
+ :root:not([data-theme="light"]) [data-ms-code] .sb {
171
+ color: #79c0ff;
172
+ }
173
+ :root:not([data-theme="light"]) [data-ms-code] .bp {
174
+ color: #79c0ff;
175
+ }
176
+ :root:not([data-theme="light"]) [data-ms-code] .ne {
177
+ color: #79c0ff;
178
+ }
179
+ :root:not([data-theme="light"]) [data-ms-code] .nl {
180
+ color: #79c0ff;
181
+ }
182
+ :root:not([data-theme="light"]) [data-ms-code] .py {
183
+ color: #79c0ff;
184
+ }
185
+ :root:not([data-theme="light"]) [data-ms-code] .nv, :root:not([data-theme="light"]) [data-ms-code] .vc, :root:not([data-theme="light"]) [data-ms-code] .vg, :root:not([data-theme="light"]) [data-ms-code] .vi, :root:not([data-theme="light"]) [data-ms-code] .vm {
186
+ color: #79c0ff;
187
+ }
188
+ :root:not([data-theme="light"]) [data-ms-code] .o, :root:not([data-theme="light"]) [data-ms-code] .ow {
189
+ color: #79c0ff;
190
+ }
191
+ :root:not([data-theme="light"]) [data-ms-code] .gh {
192
+ color: #1f6feb;
193
+ font-weight: bold;
194
+ }
195
+ :root:not([data-theme="light"]) [data-ms-code] .gu {
196
+ color: #1f6feb;
197
+ font-weight: bold;
198
+ }
199
+ :root:not([data-theme="light"]) [data-ms-code] .s, :root:not([data-theme="light"]) [data-ms-code] .sa, :root:not([data-theme="light"]) [data-ms-code] .sc, :root:not([data-theme="light"]) [data-ms-code] .dl, :root:not([data-theme="light"]) [data-ms-code] .sd, :root:not([data-theme="light"]) [data-ms-code] .s2, :root:not([data-theme="light"]) [data-ms-code] .se, :root:not([data-theme="light"]) [data-ms-code] .sh, :root:not([data-theme="light"]) [data-ms-code] .sx, :root:not([data-theme="light"]) [data-ms-code] .s1, :root:not([data-theme="light"]) [data-ms-code] .ss {
200
+ color: #a5d6ff;
201
+ }
202
+ :root:not([data-theme="light"]) [data-ms-code] .nd {
203
+ color: #d2a8ff;
204
+ }
205
+ :root:not([data-theme="light"]) [data-ms-code] .nf, :root:not([data-theme="light"]) [data-ms-code] .fm {
206
+ color: #d2a8ff;
207
+ }
208
+ :root:not([data-theme="light"]) [data-ms-code] .err {
209
+ color: #f0f6fc;
210
+ background-color: #8e1519;
211
+ }
212
+ :root:not([data-theme="light"]) [data-ms-code] .c, :root:not([data-theme="light"]) [data-ms-code] .ch, :root:not([data-theme="light"]) [data-ms-code] .cd, :root:not([data-theme="light"]) [data-ms-code] .cm, :root:not([data-theme="light"]) [data-ms-code] .cp, :root:not([data-theme="light"]) [data-ms-code] .cpf, :root:not([data-theme="light"]) [data-ms-code] .c1, :root:not([data-theme="light"]) [data-ms-code] .cs {
213
+ color: #8b949e;
214
+ }
215
+ :root:not([data-theme="light"]) [data-ms-code] .gl {
216
+ color: #8b949e;
217
+ }
218
+ :root:not([data-theme="light"]) [data-ms-code] .gt {
219
+ color: #8b949e;
220
+ }
221
+ :root:not([data-theme="light"]) [data-ms-code] .ni {
222
+ color: #c9d1d9;
223
+ }
224
+ :root:not([data-theme="light"]) [data-ms-code] .si {
225
+ color: #c9d1d9;
226
+ }
227
+ :root:not([data-theme="light"]) [data-ms-code] .ge {
228
+ color: #c9d1d9;
229
+ font-style: italic;
230
+ }
231
+ :root:not([data-theme="light"]) [data-ms-code] .gs {
232
+ color: #c9d1d9;
233
+ font-weight: bold;
234
+ }
235
+ }
@@ -0,0 +1,117 @@
1
+ /* Generated by `rake maquina_stream:themes`. Do not edit by hand. */
2
+ :root:not([data-theme="dark"]) [data-ms-code] table td { padding: 5px; }
3
+ :root:not([data-theme="dark"]) [data-ms-code] table pre { margin: 0; }
4
+ :root:not([data-theme="dark"]) [data-ms-code], :root:not([data-theme="dark"]) [data-ms-code] .w {
5
+ color: #24292f;
6
+ background-color: #f6f8fa;
7
+ }
8
+ :root:not([data-theme="dark"]) [data-ms-code] .k, :root:not([data-theme="dark"]) [data-ms-code] .kd, :root:not([data-theme="dark"]) [data-ms-code] .kn, :root:not([data-theme="dark"]) [data-ms-code] .kp, :root:not([data-theme="dark"]) [data-ms-code] .kr, :root:not([data-theme="dark"]) [data-ms-code] .kt, :root:not([data-theme="dark"]) [data-ms-code] .kv {
9
+ color: #cf222e;
10
+ }
11
+ :root:not([data-theme="dark"]) [data-ms-code] .gr {
12
+ color: #f6f8fa;
13
+ }
14
+ :root:not([data-theme="dark"]) [data-ms-code] .gd {
15
+ color: #82071e;
16
+ background-color: #ffebe9;
17
+ }
18
+ :root:not([data-theme="dark"]) [data-ms-code] .nb {
19
+ color: #953800;
20
+ }
21
+ :root:not([data-theme="dark"]) [data-ms-code] .nc {
22
+ color: #953800;
23
+ }
24
+ :root:not([data-theme="dark"]) [data-ms-code] .no {
25
+ color: #953800;
26
+ }
27
+ :root:not([data-theme="dark"]) [data-ms-code] .nn {
28
+ color: #953800;
29
+ }
30
+ :root:not([data-theme="dark"]) [data-ms-code] .sr {
31
+ color: #116329;
32
+ }
33
+ :root:not([data-theme="dark"]) [data-ms-code] .na {
34
+ color: #116329;
35
+ }
36
+ :root:not([data-theme="dark"]) [data-ms-code] .nt {
37
+ color: #116329;
38
+ }
39
+ :root:not([data-theme="dark"]) [data-ms-code] .gi {
40
+ color: #116329;
41
+ background-color: #dafbe1;
42
+ }
43
+ :root:not([data-theme="dark"]) [data-ms-code] .ges {
44
+ font-weight: bold;
45
+ font-style: italic;
46
+ }
47
+ :root:not([data-theme="dark"]) [data-ms-code] .kc {
48
+ color: #0550ae;
49
+ }
50
+ :root:not([data-theme="dark"]) [data-ms-code] .l, :root:not([data-theme="dark"]) [data-ms-code] .ld, :root:not([data-theme="dark"]) [data-ms-code] .m, :root:not([data-theme="dark"]) [data-ms-code] .mb, :root:not([data-theme="dark"]) [data-ms-code] .mf, :root:not([data-theme="dark"]) [data-ms-code] .mh, :root:not([data-theme="dark"]) [data-ms-code] .mi, :root:not([data-theme="dark"]) [data-ms-code] .il, :root:not([data-theme="dark"]) [data-ms-code] .mo, :root:not([data-theme="dark"]) [data-ms-code] .mx {
51
+ color: #0550ae;
52
+ }
53
+ :root:not([data-theme="dark"]) [data-ms-code] .sb {
54
+ color: #0550ae;
55
+ }
56
+ :root:not([data-theme="dark"]) [data-ms-code] .bp {
57
+ color: #0550ae;
58
+ }
59
+ :root:not([data-theme="dark"]) [data-ms-code] .ne {
60
+ color: #0550ae;
61
+ }
62
+ :root:not([data-theme="dark"]) [data-ms-code] .nl {
63
+ color: #0550ae;
64
+ }
65
+ :root:not([data-theme="dark"]) [data-ms-code] .py {
66
+ color: #0550ae;
67
+ }
68
+ :root:not([data-theme="dark"]) [data-ms-code] .nv, :root:not([data-theme="dark"]) [data-ms-code] .vc, :root:not([data-theme="dark"]) [data-ms-code] .vg, :root:not([data-theme="dark"]) [data-ms-code] .vi, :root:not([data-theme="dark"]) [data-ms-code] .vm {
69
+ color: #0550ae;
70
+ }
71
+ :root:not([data-theme="dark"]) [data-ms-code] .o, :root:not([data-theme="dark"]) [data-ms-code] .ow {
72
+ color: #0550ae;
73
+ }
74
+ :root:not([data-theme="dark"]) [data-ms-code] .gh {
75
+ color: #0550ae;
76
+ font-weight: bold;
77
+ }
78
+ :root:not([data-theme="dark"]) [data-ms-code] .gu {
79
+ color: #0550ae;
80
+ font-weight: bold;
81
+ }
82
+ :root:not([data-theme="dark"]) [data-ms-code] .s, :root:not([data-theme="dark"]) [data-ms-code] .sa, :root:not([data-theme="dark"]) [data-ms-code] .sc, :root:not([data-theme="dark"]) [data-ms-code] .dl, :root:not([data-theme="dark"]) [data-ms-code] .sd, :root:not([data-theme="dark"]) [data-ms-code] .s2, :root:not([data-theme="dark"]) [data-ms-code] .se, :root:not([data-theme="dark"]) [data-ms-code] .sh, :root:not([data-theme="dark"]) [data-ms-code] .sx, :root:not([data-theme="dark"]) [data-ms-code] .s1, :root:not([data-theme="dark"]) [data-ms-code] .ss {
83
+ color: #0a3069;
84
+ }
85
+ :root:not([data-theme="dark"]) [data-ms-code] .nd {
86
+ color: #8250df;
87
+ }
88
+ :root:not([data-theme="dark"]) [data-ms-code] .nf, :root:not([data-theme="dark"]) [data-ms-code] .fm {
89
+ color: #8250df;
90
+ }
91
+ :root:not([data-theme="dark"]) [data-ms-code] .err {
92
+ color: #f6f8fa;
93
+ background-color: #82071e;
94
+ }
95
+ :root:not([data-theme="dark"]) [data-ms-code] .c, :root:not([data-theme="dark"]) [data-ms-code] .ch, :root:not([data-theme="dark"]) [data-ms-code] .cd, :root:not([data-theme="dark"]) [data-ms-code] .cm, :root:not([data-theme="dark"]) [data-ms-code] .cp, :root:not([data-theme="dark"]) [data-ms-code] .cpf, :root:not([data-theme="dark"]) [data-ms-code] .c1, :root:not([data-theme="dark"]) [data-ms-code] .cs {
96
+ color: #6e7781;
97
+ }
98
+ :root:not([data-theme="dark"]) [data-ms-code] .gl {
99
+ color: #6e7781;
100
+ }
101
+ :root:not([data-theme="dark"]) [data-ms-code] .gt {
102
+ color: #6e7781;
103
+ }
104
+ :root:not([data-theme="dark"]) [data-ms-code] .ni {
105
+ color: #24292f;
106
+ }
107
+ :root:not([data-theme="dark"]) [data-ms-code] .si {
108
+ color: #24292f;
109
+ }
110
+ :root:not([data-theme="dark"]) [data-ms-code] .ge {
111
+ color: #24292f;
112
+ font-style: italic;
113
+ }
114
+ :root:not([data-theme="dark"]) [data-ms-code] .gs {
115
+ color: #24292f;
116
+ font-weight: bold;
117
+ }
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MaquinaStream
4
+ # Every engine request resolves the record through the host's `find_stream`
5
+ # callable and is authorized by the host's `authorize` callable. With neither
6
+ # configured the engine refuses: it never assumes it may serve a message.
7
+ class ApplicationController < ActionController::Base
8
+ before_action :set_stream_record
9
+ before_action :authorize_stream_record
10
+
11
+ private
12
+ def set_stream_record
13
+ @record = MaquinaStream.config.find_stream!(params[:sid])
14
+ head :not_found unless @record
15
+ end
16
+
17
+ def authorize_stream_record
18
+ head :forbidden unless MaquinaStream.config.authorized?(@record, request)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MaquinaStream
4
+ # The blocks the client asked for, as morphing Turbo Stream actions.
5
+ #
6
+ # Only ids the message actually has are served, and only the ones requested:
7
+ # `ids` comes from the client, so it is filtered against the document rather
8
+ # than trusted. A request for every block is a legitimate cold repair, so the
9
+ # count is not capped — but each id is matched, not interpolated.
10
+ class BlocksController < ApplicationController
11
+ # `GET /maquina_stream/:sid/blocks?ids[]=…` → a Turbo Stream carrying one
12
+ # morphing replace per requested block.
13
+ def index
14
+ requested = Array(params[:ids]).map(&:to_s).to_set
15
+ blocks = document.blocks.select { |block| requested.include?(block.id) }
16
+
17
+ # Rendered explicitly rather than via `render turbo_stream:`, which does
18
+ # not count as a render when the list is empty and then looks for a
19
+ # template that does not exist. Asking to repair nothing is a normal
20
+ # answer to a manifest that already agreed.
21
+ render plain: blocks.map { |block| morph(block) }.join, content_type: Mime[:turbo_stream]
22
+ end
23
+
24
+ private
25
+ def document
26
+ Document.new(
27
+ @record.maquina_stream_buffer,
28
+ config: MaquinaStream.config,
29
+ sid: @record.maquina_stream_id,
30
+ mode: @record.maquina_stream_open? ? :streaming : :static
31
+ )
32
+ end
33
+
34
+ # `method: :morph` is what makes the repair silent: idiomorph patches the
35
+ # existing node in place, so client state inside it survives and no
36
+ # animation is triggered by a replacement that never happens.
37
+ def morph(block)
38
+ turbo_stream.replace(block.id, block.html, method: :morph)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MaquinaStream
4
+ # The snapshot the repair path diffs against: sequence plus one digest per
5
+ # sealed block. A few hundred bytes whatever the message weighs.
6
+ class ManifestsController < ApplicationController
7
+ # `GET /maquina_stream/:sid/manifest` →
8
+ # `{ seq:, cutoff:, rollup:, blocks: [[id, digest], …] }`.
9
+ def show
10
+ # `full=1` is the cold path: a client whose rollup disagrees with ours
11
+ # cannot repair from the window alone and asks for everything.
12
+ render json: Manifest.for(@record, full: params[:full].present?).to_h
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MaquinaStream
4
+ # The view side of the component seam. Engine views and the render pipeline
5
+ # call `component`; nothing renders a vendored partial by path.
6
+ module ComponentsHelper
7
+ # Renders one component through the seam.
8
+ #
9
+ # ```erb
10
+ # <%= component(:code_block, lang: "ruby", source: raw, css_classes: "…") %>
11
+ # ```
12
+ #
13
+ # `name` resolves to a `maquina_components` partial when that gem defines
14
+ # it, and to the vendored fallback otherwise. Locals go through
15
+ # MaquinaStream::Components::Contract on the way in.
16
+ #
17
+ # The partial is generic; the engine's DOM contract and labels are added
18
+ # here, by MaquinaStream::Components::Contract. That is what lets a vendored
19
+ # partial leave for maquina_components without carrying `data-ms-*` or the
20
+ # `maquina_stream.*` locale namespace with it.
21
+ def component(name, **locals, &block)
22
+ config = maquina_stream_config
23
+ partial = MaquinaStream::Components.partial_for(name, config: config)
24
+ locals = MaquinaStream::Components::Contract.apply(name, locals, config: config)
25
+
26
+ if block
27
+ render(layout: partial, locals: locals, &block)
28
+ else
29
+ render(partial: partial, locals: locals)
30
+ end
31
+ end
32
+
33
+ # Merges caller-supplied data attributes with the component's own.
34
+ #
35
+ # The component wins its identity keys — `component`, `variant`, `size` and
36
+ # anything ending in `_part` — because those are what the CSS selects on.
37
+ # `controller` and `action` concatenate, component tokens first. Every other
38
+ # key belongs to the caller.
39
+ def component_data(own, provided = nil)
40
+ own = own.compact
41
+ provided = (provided || {}).transform_keys { |key| key.to_s.tr("-", "_").to_sym }
42
+
43
+ merged = provided.merge(own) do |key, caller_value, own_value|
44
+ if %i[controller action].include?(key)
45
+ [own_value, caller_value].compact.join(" ").strip
46
+ else
47
+ own_value
48
+ end
49
+ end
50
+
51
+ merged.reject { |_, value| value.nil? }
52
+ end
53
+
54
+ # Same-shaped merge for the non-data attributes a caller passes through
55
+ # `**html_options`, with `css_classes` folded into `class`.
56
+ def component_html_options(html_options, own_data: {}, css_classes: nil, base_classes: nil)
57
+ options = html_options.dup
58
+ provided_data = options.delete(:data) || options.delete("data")
59
+
60
+ options[:data] = component_data(own_data, provided_data)
61
+ classes = component_classes(base_classes, css_classes)
62
+ options[:class] = classes if classes.present?
63
+ options
64
+ end
65
+
66
+ def component_classes(*tokens)
67
+ tokens.flatten.compact.map(&:to_s).reject(&:empty?).join(" ").presence
68
+ end
69
+
70
+ # Stylesheet paths for the components currently rendering from our own
71
+ # app/views. Empty once maquina_components serves all of them.
72
+ def component_stylesheets
73
+ MaquinaStream::Components.stylesheets(config: maquina_stream_config)
74
+ end
75
+
76
+ def maquina_stream_config
77
+ MaquinaStream.config
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,169 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Base class for every `ms-` controller.
4
+ //
5
+ // Two things live here because every controller needs them and neither belongs
6
+ // in a single one:
7
+ //
8
+ // 1. `streaming` — the one signal that makes controls inert while a message
9
+ // is still being written. See docs/javascript.md.
10
+ // 2. `copyText` / `downloadText` — clipboard and download plumbing, including
11
+ // the insecure-context clipboard fallback.
12
+ //
13
+ // Nothing here ever assigns a DOM-derived or payload-derived string as HTML.
14
+ // Per docs/security.md the sanitizer cannot tell our attributes from injected
15
+ // ones, so every value a controller reads is untrusted input.
16
+ export default class ApplicationController extends Controller {
17
+ // ---------------------------------------------------------------- streaming
18
+
19
+ // The message element. The DOM contract fixes `#ms-msg-<sid>`; hosts that
20
+ // wrap rendered output themselves can mark it with `data-ms-message`.
21
+ get messageElement() {
22
+ return this.element.closest("[data-ms-message]") ||
23
+ this.element.closest("[data-ms-streaming]") ||
24
+ this.element.closest("[id^='ms-msg-']")
25
+ }
26
+
27
+ // A message is open exactly while its element carries `data-ms-streaming`.
28
+ //
29
+ // This used to read `data-ms-caret` off the tail block. Blocks no longer
30
+ // carry chrome at all — it made a block's bytes disagree with its digest, so
31
+ // repair could not correct it and two tabs could disagree — and the caret
32
+ // moved to the message element, derived in CSS. Reading the caret here
33
+ // therefore reported "not streaming" for every message, and controls were
34
+ // never inert. See docs/javascript.md.
35
+ //
36
+ // Still one source of truth, and still the server's: the host stamps the
37
+ // attribute from `maquina_stream_open?`.
38
+ get streaming() {
39
+ const message = this.messageElement
40
+ if (!message) return false
41
+ return message.hasAttribute("data-ms-streaming")
42
+ }
43
+
44
+ // Controls opt in by carrying `data-ms-control`. They are disabled while the
45
+ // message streams so the UI never offers half a code block to copy.
46
+ startStreamGuard() {
47
+ this.syncStreamGuard()
48
+ if (!this.messageElement) return
49
+
50
+ this.streamGuardObserver = new MutationObserver(() => this.syncStreamGuard())
51
+ this.streamGuardObserver.observe(this.messageElement, {
52
+ subtree: true,
53
+ childList: true,
54
+ attributes: true,
55
+ // The seal removes data-ms-streaming from the message element, which is
56
+ // what re-enables the controls.
57
+ attributeFilter: ["data-ms-streaming"]
58
+ })
59
+ }
60
+
61
+ stopStreamGuard() {
62
+ this.streamGuardObserver?.disconnect()
63
+ this.streamGuardObserver = null
64
+ }
65
+
66
+ syncStreamGuard() {
67
+ const streaming = this.streaming
68
+ this.element.querySelectorAll("[data-ms-control]").forEach((control) => {
69
+ control.disabled = streaming
70
+ control.setAttribute("aria-disabled", String(streaming))
71
+ })
72
+ }
73
+
74
+ // Every public action that touches the clipboard, the filesystem or the
75
+ // layout starts with this. The observer above is a convenience; this is the
76
+ // guarantee.
77
+ refuseWhileStreaming(event) {
78
+ if (!this.streaming) return false
79
+ event?.preventDefault()
80
+ this.notify("refused", { reason: "streaming" })
81
+ return true
82
+ }
83
+
84
+ // ---------------------------------------------------------------- clipboard
85
+
86
+ // `navigator.clipboard` is undefined in an insecure context — plain HTTP on
87
+ // anything but localhost — which is exactly where this engine gets demoed.
88
+ // The fallback is a throwaway textarea plus `document.execCommand("copy")`.
89
+ // When both fail, a `ms:copy-failed` event is dispatched carrying the text so
90
+ // the host can offer it another way; nothing is silently lost.
91
+ async copyText(text) {
92
+ if (window.isSecureContext && navigator.clipboard) {
93
+ try {
94
+ await navigator.clipboard.writeText(text)
95
+ this.notify("copied", { length: text.length })
96
+ return true
97
+ } catch {
98
+ // Permission denied, or the document is not focused. Fall through.
99
+ }
100
+ }
101
+
102
+ if (this.copyWithSelection(text)) {
103
+ this.notify("copied", { length: text.length, fallback: true })
104
+ return true
105
+ }
106
+
107
+ this.notify("copy-failed", { text })
108
+ return false
109
+ }
110
+
111
+ copyWithSelection(text) {
112
+ const carrier = document.createElement("textarea")
113
+ carrier.value = text
114
+ carrier.setAttribute("readonly", "")
115
+ carrier.setAttribute("aria-hidden", "true")
116
+ carrier.style.position = "fixed"
117
+ carrier.style.top = "0"
118
+ carrier.style.left = "-9999px"
119
+ document.body.appendChild(carrier)
120
+
121
+ const previous = document.activeElement
122
+ let copied = false
123
+ try {
124
+ carrier.select()
125
+ carrier.setSelectionRange(0, carrier.value.length)
126
+ copied = document.execCommand("copy")
127
+ } catch {
128
+ copied = false
129
+ } finally {
130
+ carrier.remove()
131
+ if (previous instanceof HTMLElement) previous.focus()
132
+ }
133
+ return copied
134
+ }
135
+
136
+ // ---------------------------------------------------------------- downloads
137
+
138
+ downloadText(text, filename, mime = "text/plain") {
139
+ const blob = new Blob([text], { type: `${mime};charset=utf-8` })
140
+ const url = URL.createObjectURL(blob)
141
+ const link = document.createElement("a")
142
+ link.href = url
143
+ link.download = this.safeFilename(filename)
144
+ link.rel = "noopener"
145
+ document.body.appendChild(link)
146
+ link.click()
147
+ link.remove()
148
+ // Revoking synchronously races the download in Safari.
149
+ setTimeout(() => URL.revokeObjectURL(url), 1000)
150
+ this.notify("downloaded", { filename: link.download })
151
+ }
152
+
153
+ // A filename is built from a DOM-derived language string. Path separators,
154
+ // control characters and leading dots are stripped rather than trusted.
155
+ safeFilename(name) {
156
+ const cleaned = String(name)
157
+ .replace(/[\u0000-\u001f\u007f]/g, "")
158
+ .replace(/[\\/:*?"<>|]/g, "-")
159
+ .replace(/^\.+/, "")
160
+ .trim()
161
+ return cleaned.length ? cleaned.slice(0, 120) : "download.txt"
162
+ }
163
+
164
+ // ------------------------------------------------------------------- events
165
+
166
+ notify(name, detail = {}) {
167
+ this.dispatch(name, { detail, prefix: "ms" })
168
+ }
169
+ }