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.
- checksums.yaml +7 -0
- data/.rdoc_options +30 -0
- data/CHANGELOG.md +38 -0
- data/LICENSE.txt +21 -0
- data/README.md +163 -0
- data/app/assets/stylesheets/maquina_stream/components/attachment.css +35 -0
- data/app/assets/stylesheets/maquina_stream/components/code_block.css +30 -0
- data/app/assets/stylesheets/maquina_stream/components/shimmer.css +31 -0
- data/app/assets/stylesheets/maquina_stream/components/snippet.css +22 -0
- data/app/assets/stylesheets/maquina_stream/components/source_citation.css +14 -0
- data/app/assets/stylesheets/maquina_stream/components/suggestion.css +32 -0
- data/app/assets/stylesheets/maquina_stream/reveal.css +48 -0
- data/app/assets/stylesheets/maquina_stream/themes/dark.css +235 -0
- data/app/assets/stylesheets/maquina_stream/themes/light.css +117 -0
- data/app/controllers/maquina_stream/application_controller.rb +21 -0
- data/app/controllers/maquina_stream/blocks_controller.rb +41 -0
- data/app/controllers/maquina_stream/manifests_controller.rb +15 -0
- data/app/helpers/maquina_stream/components_helper.rb +80 -0
- data/app/javascript/maquina_stream/controllers/application_controller.js +169 -0
- data/app/javascript/maquina_stream/controllers/ms_autoscroll_controller.js +110 -0
- data/app/javascript/maquina_stream/controllers/ms_code_controller.js +96 -0
- data/app/javascript/maquina_stream/controllers/ms_deferred_controller.js +223 -0
- data/app/javascript/maquina_stream/controllers/ms_diagram_controller.js +40 -0
- data/app/javascript/maquina_stream/controllers/ms_link_safety_controller.js +196 -0
- data/app/javascript/maquina_stream/controllers/ms_math_controller.js +32 -0
- data/app/javascript/maquina_stream/controllers/ms_repair_controller.js +167 -0
- data/app/javascript/maquina_stream/controllers/ms_reveal_controller.js +320 -0
- data/app/javascript/maquina_stream/controllers/ms_table_controller.js +183 -0
- data/app/javascript/maquina_stream/index.js +59 -0
- data/app/views/maquina_stream/components/_attachment.html.erb +139 -0
- data/app/views/maquina_stream/components/_code_block.html.erb +74 -0
- data/app/views/maquina_stream/components/_shimmer.html.erb +36 -0
- data/app/views/maquina_stream/components/_snippet.html.erb +50 -0
- data/app/views/maquina_stream/components/_source_citation.html.erb +42 -0
- data/app/views/maquina_stream/components/_suggestion.html.erb +73 -0
- data/config/importmap.rb +10 -0
- data/config/locales/en.yml +79 -0
- data/config/locales/es.yml +82 -0
- data/config/routes.rb +11 -0
- data/docs/configuration.md +219 -0
- data/docs/deferred-renderers.md +184 -0
- data/docs/getting-started.md +356 -0
- data/docs/javascript.md +298 -0
- data/docs/registries.md +283 -0
- data/docs/repair.md +162 -0
- data/docs/security.md +247 -0
- data/docs/streaming.md +308 -0
- data/lib/generators/maquina_stream/install/USAGE +26 -0
- data/lib/generators/maquina_stream/install/install_generator.rb +199 -0
- data/lib/generators/maquina_stream/install/templates/initializer.rb.tt +121 -0
- data/lib/generators/maquina_stream/streamable/USAGE +28 -0
- data/lib/generators/maquina_stream/streamable/streamable_generator.rb +187 -0
- data/lib/generators/maquina_stream/streamable/templates/migration.rb.tt +21 -0
- data/lib/generators/maquina_stream/streamable/templates/model.rb.tt +4 -0
- data/lib/maquina_stream/block.rb +99 -0
- data/lib/maquina_stream/broadcaster.rb +233 -0
- data/lib/maquina_stream/component_cache.rb +0 -0
- data/lib/maquina_stream/components/contract.rb +184 -0
- data/lib/maquina_stream/components.rb +135 -0
- data/lib/maquina_stream/configuration.rb +240 -0
- data/lib/maquina_stream/document.rb +296 -0
- data/lib/maquina_stream/engine.rb +46 -0
- data/lib/maquina_stream/errors.rb +17 -0
- data/lib/maquina_stream/export.rb +66 -0
- data/lib/maquina_stream/frame.rb +73 -0
- data/lib/maquina_stream/manifest.rb +137 -0
- data/lib/maquina_stream/registries.rb +116 -0
- data/lib/maquina_stream/renderer/fence.rb +115 -0
- data/lib/maquina_stream/renderer/post_pass.rb +363 -0
- data/lib/maquina_stream/renderer/tag_blocks.rb +276 -0
- data/lib/maquina_stream/renderer/view_context.rb +72 -0
- data/lib/maquina_stream/renderer.rb +128 -0
- data/lib/maquina_stream/sanitizer.rb +392 -0
- data/lib/maquina_stream/streamable.rb +281 -0
- data/lib/maquina_stream/text_direction.rb +56 -0
- data/lib/maquina_stream/themes.rb +84 -0
- data/lib/maquina_stream/version.rb +5 -0
- data/lib/maquina_stream.rb +175 -0
- 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
|
+
}
|