Merge pull request #2415 from LenAnderson/parser-followup-2 Parser followup 2

11608e0cb80c8bd17b30d6054e4fe8f9428b49fb

Wolfsblvt <wolfsblvt@gmail.com>

Signed
50 files changed, +6136 -695Ignore whitespace
public/img/step-into.svg+149 -0
@@ -0,0 +1,149 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 width="48"
6 height="48"
7 viewBox="0 0 48 48"
8 version="1.1"
9 id="svg2120"
10 xml:space="preserve"
11 inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12 sodipodi:docname="step-into.svg"
13 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15 xmlns:xlink="http://www.w3.org/1999/xlink"
16 xmlns="http://www.w3.org/2000/svg"
17 xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18 id="namedview2122"
19 pagecolor="#ffffff"
20 bordercolor="#000000"
21 borderopacity="0.25"
22 inkscape:showpageshadow="2"
23 inkscape:pageopacity="0.0"
24 inkscape:pagecheckerboard="0"
25 inkscape:deskcolor="#d1d1d1"
26 inkscape:document-units="px"
27 showgrid="false"
28 inkscape:zoom="11.313708"
29 inkscape:cx="58.910834"
30 inkscape:cy="25.323262"
31 inkscape:window-width="1920"
32 inkscape:window-height="992"
33 inkscape:window-x="-8"
34 inkscape:window-y="-8"
35 inkscape:window-maximized="1"
36 inkscape:current-layer="g2714" /><defs
37 id="defs2117"><inkscape:path-effect
38 effect="spiro"
39 id="path-effect2144"
40 is_visible="true"
41 lpeversion="1" /><inkscape:path-effect
42 effect="bspline"
43 id="path-effect2138"
44 is_visible="true"
45 lpeversion="1"
46 weight="33.333333"
47 steps="2"
48 helper_size="0"
49 apply_no_weight="true"
50 apply_with_weight="true"
51 only_selected="false" /></defs><g
52 inkscape:groupmode="layer"
53 id="layer4"
54 inkscape:label="img"
55 style="display:none"><image
56 width="305.68866"
57 height="70.374367"
58 preserveAspectRatio="none"
59 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
634zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
750nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79+vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87"
88 id="image2132"
89 x="-82"
90 y="-11.9" /></g><g
91 inkscape:groupmode="layer"
92 id="layer5"
93 inkscape:label="dot" /><g
94 inkscape:label="over"
95 inkscape:groupmode="layer"
96 id="layer1"
97 style="display:none"><ellipse
98 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
99 id="path2637"
100 cx="24"
101 cy="33"
102 rx="4.3600898"
103 ry="4.3600893" /><path
104 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
105 d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
106 id="path2142"
107 inkscape:path-effect="#path-effect2144"
108 inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
109 sodipodi:nodetypes="csc" /><path
110 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
111 d="M 26,22 H 39 V 9"
112 id="path2626"
113 sodipodi:nodetypes="ccc" /></g><g
114 inkscape:groupmode="layer"
115 id="layer6"
116 inkscape:label="into"
117 style="display:inline"><ellipse
118 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
119 id="path2637-3"
120 cx="24"
121 cy="38.5"
122 rx="4.3600898"
123 ry="4.3600893" /><path
124 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
125 d="M 24,2 V 24"
126 id="path2668"
127 sodipodi:nodetypes="cc" /><path
128 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
129 d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
130 id="path2626-8"
131 sodipodi:nodetypes="ccc" /></g><g
132 inkscape:groupmode="layer"
133 id="g2714"
134 inkscape:label="out"
135 style="display:none"><ellipse
136 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
137 id="ellipse2708"
138 cx="24"
139 cy="38.5"
140 rx="4.3600898"
141 ry="4.3600893" /><path
142 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
143 d="M 24,29.722858 V 7.7228579"
144 id="path2710"
145 sodipodi:nodetypes="cc" /><path
146 style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
147 d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
148 id="path2712"
149 sodipodi:nodetypes="ccc" /></g></svg>
public/img/step-out.svg+149 -0
@@ -0,0 +1,149 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 width="48"
6 height="48"
7 viewBox="0 0 48 48"
8 version="1.1"
9 id="svg2120"
10 xml:space="preserve"
11 inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12 sodipodi:docname="step-out.svg"
13 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15 xmlns:xlink="http://www.w3.org/1999/xlink"
16 xmlns="http://www.w3.org/2000/svg"
17 xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18 id="namedview2122"
19 pagecolor="#ffffff"
20 bordercolor="#000000"
21 borderopacity="0.25"
22 inkscape:showpageshadow="2"
23 inkscape:pageopacity="0.0"
24 inkscape:pagecheckerboard="0"
25 inkscape:deskcolor="#d1d1d1"
26 inkscape:document-units="px"
27 showgrid="false"
28 inkscape:zoom="11.313708"
29 inkscape:cx="58.910834"
30 inkscape:cy="25.323262"
31 inkscape:window-width="1920"
32 inkscape:window-height="992"
33 inkscape:window-x="-8"
34 inkscape:window-y="-8"
35 inkscape:window-maximized="1"
36 inkscape:current-layer="g2714" /><defs
37 id="defs2117"><inkscape:path-effect
38 effect="spiro"
39 id="path-effect2144"
40 is_visible="true"
41 lpeversion="1" /><inkscape:path-effect
42 effect="bspline"
43 id="path-effect2138"
44 is_visible="true"
45 lpeversion="1"
46 weight="33.333333"
47 steps="2"
48 helper_size="0"
49 apply_no_weight="true"
50 apply_with_weight="true"
51 only_selected="false" /></defs><g
52 inkscape:groupmode="layer"
53 id="layer4"
54 inkscape:label="img"
55 style="display:none"><image
56 width="305.68866"
57 height="70.374367"
58 preserveAspectRatio="none"
59 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
634zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
750nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79+vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87"
88 id="image2132"
89 x="-82"
90 y="-11.9" /></g><g
91 inkscape:groupmode="layer"
92 id="layer5"
93 inkscape:label="dot" /><g
94 inkscape:label="over"
95 inkscape:groupmode="layer"
96 id="layer1"
97 style="display:none"><ellipse
98 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
99 id="path2637"
100 cx="24"
101 cy="33"
102 rx="4.3600898"
103 ry="4.3600893" /><path
104 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
105 d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
106 id="path2142"
107 inkscape:path-effect="#path-effect2144"
108 inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
109 sodipodi:nodetypes="csc" /><path
110 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
111 d="M 26,22 H 39 V 9"
112 id="path2626"
113 sodipodi:nodetypes="ccc" /></g><g
114 inkscape:groupmode="layer"
115 id="layer6"
116 inkscape:label="into"
117 style="display:none"><ellipse
118 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
119 id="path2637-3"
120 cx="24"
121 cy="38.5"
122 rx="4.3600898"
123 ry="4.3600893" /><path
124 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
125 d="M 24,2 V 24"
126 id="path2668"
127 sodipodi:nodetypes="cc" /><path
128 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
129 d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
130 id="path2626-8"
131 sodipodi:nodetypes="ccc" /></g><g
132 inkscape:groupmode="layer"
133 id="g2714"
134 inkscape:label="out"
135 style="display:inline"><ellipse
136 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
137 id="ellipse2708"
138 cx="24"
139 cy="38.5"
140 rx="4.3600898"
141 ry="4.3600893" /><path
142 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
143 d="M 24,29.722858 V 7.7228579"
144 id="path2710"
145 sodipodi:nodetypes="cc" /><path
146 style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
147 d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
148 id="path2712"
149 sodipodi:nodetypes="ccc" /></g></svg>
public/img/step-over.svg+149 -0
@@ -0,0 +1,149 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 width="48"
6 height="48"
7 viewBox="0 0 48 48"
8 version="1.1"
9 id="svg2120"
10 xml:space="preserve"
11 inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12 sodipodi:docname="step-over.svg"
13 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15 xmlns:xlink="http://www.w3.org/1999/xlink"
16 xmlns="http://www.w3.org/2000/svg"
17 xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18 id="namedview2122"
19 pagecolor="#ffffff"
20 bordercolor="#000000"
21 borderopacity="0.25"
22 inkscape:showpageshadow="2"
23 inkscape:pageopacity="0.0"
24 inkscape:pagecheckerboard="0"
25 inkscape:deskcolor="#d1d1d1"
26 inkscape:document-units="px"
27 showgrid="false"
28 inkscape:zoom="11.313708"
29 inkscape:cx="58.910834"
30 inkscape:cy="25.323262"
31 inkscape:window-width="1920"
32 inkscape:window-height="992"
33 inkscape:window-x="-8"
34 inkscape:window-y="-8"
35 inkscape:window-maximized="1"
36 inkscape:current-layer="g2714" /><defs
37 id="defs2117"><inkscape:path-effect
38 effect="spiro"
39 id="path-effect2144"
40 is_visible="true"
41 lpeversion="1" /><inkscape:path-effect
42 effect="bspline"
43 id="path-effect2138"
44 is_visible="true"
45 lpeversion="1"
46 weight="33.333333"
47 steps="2"
48 helper_size="0"
49 apply_no_weight="true"
50 apply_with_weight="true"
51 only_selected="false" /></defs><g
52 inkscape:groupmode="layer"
53 id="layer4"
54 inkscape:label="img"
55 style="display:none"><image
56 width="305.68866"
57 height="70.374367"
58 preserveAspectRatio="none"
59 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
634zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
750nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79+vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87"
88 id="image2132"
89 x="-82"
90 y="-11.9" /></g><g
91 inkscape:groupmode="layer"
92 id="layer5"
93 inkscape:label="dot" /><g
94 inkscape:label="over"
95 inkscape:groupmode="layer"
96 id="layer1"
97 style="display:inline"><ellipse
98 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
99 id="path2637"
100 cx="24"
101 cy="33"
102 rx="4.3600898"
103 ry="4.3600893" /><path
104 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
105 d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
106 id="path2142"
107 inkscape:path-effect="#path-effect2144"
108 inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
109 sodipodi:nodetypes="csc" /><path
110 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
111 d="M 26,22 H 39 V 9"
112 id="path2626"
113 sodipodi:nodetypes="ccc" /></g><g
114 inkscape:groupmode="layer"
115 id="layer6"
116 inkscape:label="into"
117 style="display:none"><ellipse
118 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
119 id="path2637-3"
120 cx="24"
121 cy="38.5"
122 rx="4.3600898"
123 ry="4.3600893" /><path
124 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
125 d="M 24,2 V 24"
126 id="path2668"
127 sodipodi:nodetypes="cc" /><path
128 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
129 d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
130 id="path2626-8"
131 sodipodi:nodetypes="ccc" /></g><g
132 inkscape:groupmode="layer"
133 id="g2714"
134 inkscape:label="out"
135 style="display:none"><ellipse
136 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
137 id="ellipse2708"
138 cx="24"
139 cy="38.5"
140 rx="4.3600898"
141 ry="4.3600893" /><path
142 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
143 d="M 24,29.722858 V 7.7228579"
144 id="path2710"
145 sodipodi:nodetypes="cc" /><path
146 style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
147 d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
148 id="path2712"
149 sodipodi:nodetypes="ccc" /></g></svg>
public/img/step-resume.svg+218 -0
@@ -0,0 +1,218 @@
1<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4<svg
5 width="48"
6 height="48"
7 viewBox="0 0 48 48"
8 version="1.1"
9 id="svg2120"
10 xml:space="preserve"
11 inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
12 sodipodi:docname="step-resume.svg"
13 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15 xmlns:xlink="http://www.w3.org/1999/xlink"
16 xmlns="http://www.w3.org/2000/svg"
17 xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
18 id="namedview2122"
19 pagecolor="#ffffff"
20 bordercolor="#000000"
21 borderopacity="0.25"
22 inkscape:showpageshadow="2"
23 inkscape:pageopacity="0.0"
24 inkscape:pagecheckerboard="0"
25 inkscape:deskcolor="#d1d1d1"
26 inkscape:document-units="px"
27 showgrid="false"
28 inkscape:zoom="11.315"
29 inkscape:cx="4.7282369"
30 inkscape:cy="24.259832"
31 inkscape:window-width="1920"
32 inkscape:window-height="992"
33 inkscape:window-x="-8"
34 inkscape:window-y="-8"
35 inkscape:window-maximized="1"
36 inkscape:current-layer="layer8" /><defs
37 id="defs2117"><inkscape:path-effect
38 effect="spiro"
39 id="path-effect2144"
40 is_visible="true"
41 lpeversion="1" /><inkscape:path-effect
42 effect="bspline"
43 id="path-effect2138"
44 is_visible="true"
45 lpeversion="1"
46 weight="33.333333"
47 steps="2"
48 helper_size="0"
49 apply_no_weight="true"
50 apply_with_weight="true"
51 only_selected="false" /></defs><g
52 inkscape:groupmode="layer"
53 id="layer4"
54 inkscape:label="img"
55 style="display:none"><image
56 width="305.68866"
57 height="70.374367"
58 preserveAspectRatio="none"
59 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARYAAABACAYAAADf7VgRAAAABHNCSVQICAgIfAhkiAAABdxJREFU
60eJzt3T9vo0gYBvB3by+SsZRiKCI5SCkyJS6dzny3+26UpoSSFJFMOaTyUETaK3zDAQYbzGBgeH7S
61SutN1kKj8cM7fxj/2u/3fwgAQKO/xr4AADAPggUAtEOwAIB2CBYA0A7BAgDaIVgAQDsECwBoh2AB
62AO0QLACgHYIFALRDsACAdn+PfQFw5nkeWZZFRERSSvJ9f+Qrmg7XdSnLMorjeOxLgZYQLDBpruuS
634zj5a4TLPGAoBJNVDRXOOXHOR7wiaAvBApNUDRUF4TIPCBaYnKZQURAu04dggUm5FSoKwmXaECww
64GW1DRUG4TNfDV4VUx1mtViSEICKiLMtIStn4f4qdJ8syOh6Pw14kPFxdqAghyLKs0jI8EeWvif7v
65G1gtmpbBg8V1XbJtm4jKHYKoHBhCCIrjuDZkqr+HYDFLU6gEQUCe55X+3ff90p4fIoTLFA0WLI7j
66kOu6rX/ftm2ybZuEEBRF0dUKBsxxLVSaIFymT/scC2OMdrtdp1Apsm2bPM8jzvlFhQPmybKs9PpW
67qCi+71/cfKrvZQrXdYkxNvZldKK1YmGM0cfHR+3PhBAkpaTv728iIjqdTmTbNq1WK7Jtu3aYxBij
68KIp0XuLoXNelz8/PzhWZZVnkuq5x1ZyqMDjnrUNFKVYuURQZOUTe7XZ5NR+GIaVpOvYltaItWCzL
69qg0VNXdS1yDFf2OM0Xa7LQWMbdt3Vz5TVOwkQRC0DgjLsvK5BlPD5d5Jed/3yXEcI0OlOj+53W5n
70Ey5ahkLFjq9IKelwOFAQBK0aIk1T8n3/ooOohp27aifZ7XathnrVtjUtbJU+wWBiqBDRRWWrwqXv
71sKi40jYULRVLtaP3eTo3iiLKssy4/QlJkpSGfCpcrlUudYFNhAnKpZBSUhAEpZvQvZWLGkoXb9RS
72SkqSZJD+1Lti4ZxfVBVhGPZ6zziOjfvwpGlKYRhe3IGaKpemUDkcDrMohUEPFS59KhfOOXmed/E5
73tSwr/5nuCqZ3xfL6+lp63bXjq0naJVDhUpxLqgsXhAoU9alcGGM3q3/VB3WeAdSrYqmO1aSUnTs+
74Yyyf0Kz+MVFT5XILQmXZ7q1cmlZpqyzL6vQ4xS29KpZqtZIkSa+LWYq6yuUahIo56irRLqr95Vrl
750nUksNlstE2EawsWKaVx8yJDahsuCBWzDLEa0xQu6/W68/vo0itYdIzJumyIMs2tcEGoQFt1/We1
76WvV+j3vhzNuRNYULQsVMfTc2Nn346/qLEKLTtg2d+4FGOzbhdDrlz3aYtIv0HtVwQaiYq0+V33W1
77UJ0U0LYS0fms1cOCpelQHjzNfKbCRf0dzqSUed9Ych+5ZwuClJLCMGy1MqQevdHl136//6Pt3Rqo
78Z2SuMfUhMoC++u5runXSngognTe0wSuWup25dd7f3/MnoAHgTMdmyTiOKUmSh27pHzRY1Jbhtr/7
79+vqKJWuA/+jcga022FWP+RzKpFaFlrK1vw5jjNbrdX4W8K1zgJdiye1S9xR734n9R7XdoMFS3Zl7
80i6nb+K+pOxyLcz7IuHdO0C7necfi8GVOq4W/397e/hnqzdfrNb28vLT+fSklfX19DXU5k8M5p+12
81W/uzp6enfGl+Lp1JF7TL2c/PD6VpSs/Pz7ML00G/V0h9vUdbSylxidrPPy3t7F+0S5maG5lTqBAN
82HCzFPQhtLGnitsspcCaeGNcE7WKGwb8Jse3ZrkKI2aVyH13mk+oOGzcV2sUMgweLmmy7Fi5dT2ef
83O3wY6qFdzPGQ5WZ1ULY6LU4dEHU8HilJkkVVKvda0vxTF2iXaXroPpYlzaFco+ae2t6hl/LhQbuY
84Y/ChENTrctrekk7mQ7uYAcEykjiOW91xl3YyH9rFDAiWEd1aMRNC9P4qlTlCu8zfQ45NgOscx6HN
85ZlOa1M6ybPF3ZLTLfCFYAEA7DIUAQDsECwBoh2ABAO0QLACgHYIFALRDsACAdggWANAOwQIA2iFY
86AEA7BAsAaIdgAQDt/gUoDXNStc/rMQAAAABJRU5ErkJggg==
87"
88 id="image2132"
89 x="-82"
90 y="-11.9" /><image
91 width="460"
92 height="71"
93 preserveAspectRatio="none"
94 xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAcwAAABHCAYAAACK23cpAAAABHNCSVQICAgIfAhkiAAACuZJREFU
95eJzt3T9o3FgeB/Cv77aZBHInYZPD+JLF4iDxDl44ZNiAvVfNhC2vCEqxWQJpJn3MwlRXTeP0drNF
96NtfIKa5cYnVxkQMLDoztbU6GgBkwDiPOEHuOK/aK7FM0Gs3M01gzkp6+HzBknPFEkSV93z/9NLO6
97uvoLiIiIaKjfZL0BRERERcDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIiksDAJCIi
98ksDAJCIiksDAJCIiksDAJCIiksDAJCIikvBZ1htAlIZmswlN0wAAvu+j1WplvEX5YVkWOp0OHMfJ
99elMoBYuLi8Gfj4+PM9yS8mFgEinMsiyYphm8ZmgSjY9DskSKioZlvV5HrVbLcIuoyEzTDEZxyoqB
100SaSgaFgKDE0aR6PRCI6pMocmA5NIMYPCUmBoUhKNRgOGYQD4eOyUOTQZmEQKGRWWAkOTZPm+3/O6
101zKHJwCRShGxYCgxNkmHbNlzX7fleWUNzqqtk5xaWMbuwHLx+d+Tg4vw0eH3txs2e19RPXBA1TQuW
102lHc6nb5WYFj4ouj7ft/BT8UXF5ae50HX9Z7bbQD0XOTq9ToArp6l4WzbBoC+RWQA4Lru0OuPSqYa
103mLMLy1i69yh4/f5kvycgv3nyI47evuwL0jKzLCuYPxjWmvM8D47jxIanOLDF+xiYahkUlltbW2g2
104mz3fb7VaPfesAgxNksPQzOGQ7NK9R/jLgw3MhXqiZWSaJjY2NoJhj1FDH4ZhBCvZyjZMUmbDwnKQ
105VqsV26ji8CyNUvbh2VwWLrh24ya+frCBs5N9uK+fl6q3aRgGarVa0Ksc5+ebzSZ2dnZK0+ors06n
1060/N6VFgKcT1NVY8Vy7Lgui48z8t6Uwpr1HVpUE9T0zQ8ffp0rDAVo2F5GhHLXQ8zbG5hGd88+RG3
107l2q4duNm1pszcaKXGHdQioPHtm3Yto2tra2hoViv15XsbY77f9I0DY1GQ7n94TgOdnZ2AMiHpRDu
108acb1HFTQaDRgmmbP1AYlI86dUfsvrqfp+z42NzfHaowZhgHLsnI18pHLHmaUef+Z8r1NcVBGibnJ
109uNZx+Hvi4AofrOJ7qhAnrWEYiU5CTdOCuTzLsmDbtlK9Kcdxxl7M1Wq1YJqmkmEZnf8Xv3v2NJMJ
110r4GQfW+4IS9Cc9yeZp7m2H9769atv03rH5tbWMbcH78MXkcX94QXBEVdv3ETf/rzXwEAF+en+N9/
111P0xuQ6csfEEXfN/HixcvgovhKL7vY3d3F7quY35+Pvi+rut97yvixdGyLFSrVQBApVJBtVrFwcEB
112ut0uAGBtbQ2VSgUA0O12sbu7C6B/34r9U8R9MEy73Y79/qD9IvOzRddut1GtVoP/f6VSgWEYaLfb
113V2owaZqGSqUSHHvTNu1h9MePH/e89jwPvu/3fIWvM4Zh4PLyEr7vB/uo2+3i4OAA8/PzfT876Cv6
114mb7vZ36s5raHeXF+ig/np32Lf5buPcLnX9Thvn6Os5P9jLYuXdFe4FWetmHbNjqdTqJWYRG4rgvD
115MIKLhZgbGdbTjGuIAPloqdLkxfVsxu1pip8LD0v6vo+9vb1SHU+Dhv3j6hYD/T3NJFMG0Tl2y7KC
116sM5KbucwP5yf4s2r9dhhWLEo6OsHG4Wf24ybSBfLt8cVntdShed5fUOpwxYUDArLra0tDsmVSNwc
117Wlz4DVOr1dBsNvver2ka6vV634W9jCaxejauMTzusG5acjske3F+indHDv5zdox//+sfuP67P+D3
118c70HrBimFUO0RRymffjwYTBkBCS/oNdqteDADH/put43HAsUd0gWQDAkYxhGzzBbtVrtOYkqlQrW
1191tb6fr6MYSkzJKs6MRw4zvCszDoAcQxOa99Oe0g2PFo17PpxeHjYNyUUNzwra9DvLTodM0257WFG
120ua+f482r9dhFP+b9ZzDvPytcbzN6f6Xv+4kv6GIRTNyXigb1NEcpY1jSJ+P2NOMW4sXRNC1RWUJV
121pd3THPZ7y6Knmds5zDhnJ/v46YfvcPerb/H5F/WegBS3oBSpUlD0BNvb28toS4pFhKbsScOwVEfc
122MHsS0eNl2Jxm0kanqquNk0q7IpCYK15ZWQl+f4ZhBPt7mnOahelhhv38z78PXPRTpEpBKysrwZ99
1233y/V4oGriutpxmFYqkWMyoz7Negz43qaSXswcVMgZZV2T9NxnL4Ohfi8aSpUDzPsQwF6kKOMuxI2
124LMmqM9WM6mkyLEnWoIVjV/2MMptETzNq2o2UQgbm3a++HXjP5rsjB+7r51PeIsrKoNBkWKrpqsNv
125g0It7ngRTwOSxeHYfmmFZtziK3HuT1OhAnNuYRl37z2KHW49O9nHz29fKnNv5iDiwPN9P6gjqlLV
126mnFEQ5Nhqa6rjMokvdVIPPlHtucYretLH101NEXJ0LCkZSDTUpjANO8/w+2l+JqC7uvneHek9vyf
127uH0kSnYuT3Xh1ibD8pNOp8OGFca7L9f3fdi2LbVSVpSwpHjjhmZcydCkBRDSlPvAvL1Ug3n/Wezf
128qV5fVhhW+Fg8nUTV4tlJMCj7lXmOW7hKEQvP87CzszO0chYX7MlJGppiMVaYaMRkJbeBef3Xaj5x
129w68X56dKlcYbRvZRX/V6PfOyUUR5k0bFJ8dx4LouS+OlIGloRhf1ZF08P7eBee3GzdhCBGVa1CNK
130b8m+1zRNnrxEv0qzPKIYBhTzmWVqmIbncXVdly7mkITMU07ysDYht4EZVZZFPVdhGEZpA1MUZtc0
131DcfHx8GCjbIr836JK2l31YtuWfZdWKfT6SleP6nbZ4aFpmEYmYclUJDALMOinjhJb8pVtRzeMHEr
1326IBPcx15OMmywP2CYOW0OC/y0EMpItu2p1b0fFBo5mV9Rm6LrwMfe5VvXq3j/QR6lbOzs7i4uEj9
133c9Ok63rwDEgZ4pmYZVGr1QYWxq5UKkGDI+n9dEXH/fJRt9uF53mYn5/H9va2MmE57eLr3W4X7XYb
134mqZhZmam52ERk3CVgu2Tlsse5qQX9czOzuL777/H+vr6RD4/LUlP8DLdByY7v1uv16debzJL3C+9
135srwFQSWe5020wSHzPM08yF0t2XdHDn764buJhuWTJ08m8tlpE08el1Wm+ctRj1wa971Fx/1CRTSJ
13652lOwlR7mO9P9nH09mXwOloP9s2r9Yku6hFhOTs7O7F/I23RJ8YPMukWYN4kma8VC1/y1FKdFO4X
137Kqq0a89OwlQD8+xkf2ggMiz7iUUawx5llVWZqKzkqcWZJ9wvVHR5D81czmGmrahhKXieh1arFRQx
1380HUdmqbBdV24rluqnuW4sj7R8or7hfImz6GpfGAWPSzDHMcp1TzlIGJuV7ZHVZZQ4H4hVeQ1NHO3
1396CdNKoUl9Yo+TDat9xYd9wupIo8LgZQNTIal2hzHkWpllq0wNvcLqWRQaGZVpEXJwGRYlsPm5ubQ
140cMjiAbN5wP1CKomGpud5mVX+mWqln2lIEpZsYRdbt9vF7u4ufN9HpVIJqpC4rouDgwNsb2+Xcp6O
141+0Vt0670kweHh4fQdR2Xl5eZ3hEws7q6+ktm/3rKkvYs817ph4iI8kOZVbKi3B0REdEkKNXDJCIi
142mhQlF/0QERGljYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQkgYFJREQk
1434bPFxcWst4GIiCj32MMkIiKSMHPnzh3WkiUiIhqBPUwiIiIJDEwiIiIJDEwiIiIJ/weH8+Ed/xCz
144fAAAAABJRU5ErkJggg==
145"
146 id="image2845"
147 x="-15.489594"
148 y="-8.2945251" /></g><g
149 inkscape:groupmode="layer"
150 id="layer5"
151 inkscape:label="dot" /><g
152 inkscape:label="over"
153 inkscape:groupmode="layer"
154 id="layer1"
155 style="display:none"><ellipse
156 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
157 id="path2637"
158 cx="24"
159 cy="33"
160 rx="4.3600898"
161 ry="4.3600893" /><path
162 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
163 d="M 9,24 C 9.7590836,20.626295 11.695032,17.529367 14.395314,15.369142 17.095595,13.208917 20.541953,12 24,12 c 3.458047,0 6.904405,1.208917 9.604686,3.369142 C 36.304968,17.529367 38.240916,20.626295 39,24"
164 id="path2142"
165 inkscape:path-effect="#path-effect2144"
166 inkscape:original-d="m 9,24 c 4.959859,-3.824406 10.021901,-8.173595 15,-12 4.978099,-3.8264055 10.285024,8.398237 15,12"
167 sodipodi:nodetypes="csc" /><path
168 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
169 d="M 26,22 H 39 V 9"
170 id="path2626"
171 sodipodi:nodetypes="ccc" /></g><g
172 inkscape:groupmode="layer"
173 id="layer6"
174 inkscape:label="into"
175 style="display:none"><ellipse
176 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
177 id="path2637-3"
178 cx="24"
179 cy="38.5"
180 rx="4.3600898"
181 ry="4.3600893" /><path
182 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
183 d="M 24,2 V 24"
184 id="path2668"
185 sodipodi:nodetypes="cc" /><path
186 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
187 d="M 14.807612,16.994936 24,26.187324 33.192388,16.994936"
188 id="path2626-8"
189 sodipodi:nodetypes="ccc" /></g><g
190 inkscape:groupmode="layer"
191 id="g2714"
192 inkscape:label="out"
193 style="display:none"><ellipse
194 style="fill:#000000;stroke:#000000;stroke-width:5.27982;stroke-dasharray:none"
195 id="ellipse2708"
196 cx="24"
197 cy="38.5"
198 rx="4.3600898"
199 ry="4.3600893" /><path
200 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
201 d="M 24,29.722858 V 7.7228579"
202 id="path2710"
203 sodipodi:nodetypes="cc" /><path
204 style="display:inline;fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
205 d="M 33.192388,14.727922 24,5.5355339 14.807612,14.727922"
206 id="path2712"
207 sodipodi:nodetypes="ccc" /></g><g
208 inkscape:groupmode="layer"
209 id="layer8"
210 inkscape:label="resume"><path
211 style="fill:#000000;stroke:#000000;stroke-width:5;stroke-dasharray:none"
212 d="M 13,12 V 38"
213 id="path2850"
214 sodipodi:nodetypes="cc" /><path
215 style="fill:none;stroke:#000000;stroke-width:5;stroke-dasharray:none"
216 d="M 21,16 V 34 L 36.5,25 Z"
217 id="path2852"
218 sodipodi:nodetypes="cccc" /></g></svg>
public/index.html+10 -0
@@ -4268,6 +4268,16 @@
4268 </div>4268 </div>
4269 </div>4269 </div>
4270 </div>4270 </div>
4271 <div title="Determines which keys select an item from the AutoComplete suggestions">
4272 <label data-i18n="Keyboard">
4273 <small>Keyboard:</small>
4274 </label>
4275 <select id="stscript_autocomplete_select">
4276 <option value="3" data-i18n="Select with Tab or Enter">Select with Tab or Enter</option>
4277 <option value="1" data-i18n="Select with Tab">Select with Tab</option>
4278 <option value="2" data-i18n="Select with Enter">Select with Enter</option>
4279 </select>
4280 </div>
4271 <div class="flex-container flexFlowColumn gap0" title="Sets the font size of the autocomplete." data-i18n="[title]Sets the font size of the autocomplete.">4281 <div class="flex-container flexFlowColumn gap0" title="Sets the font size of the autocomplete." data-i18n="[title]Sets the font size of the autocomplete.">
4272 <label for="stscript_autocomplete_font_scale"><small>Font Scale</small></label>4282 <label for="stscript_autocomplete_font_scale"><small>Font Scale</small></label>
4273 <input class="neo-range-slider" type="range" id="stscript_autocomplete_font_scale" min="0.5" max="2" step="0.01">4283 <input class="neo-range-slider" type="range" id="stscript_autocomplete_font_scale" min="0.5" max="2" step="0.01">
public/scripts/autocomplete/AutoComplete.js+71 -48
@@ -16,8 +16,15 @@ export const AUTOCOMPLETE_WIDTH = {
16 'FULL': 2,16 'FULL': 2,
17};17};
1818
19/**@readonly*/
20/**@enum {Number}*/
21export const AUTOCOMPLETE_SELECT_KEY = {
22 'TAB': 1, // 2^0
23 'ENTER': 2, // 2^1
24};
25
19export class AutoComplete {26export class AutoComplete {
20 /**@type {HTMLTextAreaElement}*/ textarea;27 /**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea;
21 /**@type {boolean}*/ isFloating = false;28 /**@type {boolean}*/ isFloating = false;
22 /**@type {()=>boolean}*/ checkIfActivate;29 /**@type {()=>boolean}*/ checkIfActivate;
23 /**@type {(text:string, index:number) => Promise<AutoCompleteNameResult>}*/ getNameAt;30 /**@type {(text:string, index:number) => Promise<AutoCompleteNameResult>}*/ getNameAt;
@@ -56,6 +63,8 @@ export class AutoComplete {
56 /**@type {function}*/ updateDetailsPositionDebounced;63 /**@type {function}*/ updateDetailsPositionDebounced;
57 /**@type {function}*/ updateFloatingPositionDebounced;64 /**@type {function}*/ updateFloatingPositionDebounced;
5865
66 /**@type {(item:AutoCompleteOption)=>any}*/ onSelect;
67
59 get matchType() {68 get matchType() {
60 return power_user.stscript.matching ?? 'fuzzy';69 return power_user.stscript.matching ?? 'fuzzy';
61 }70 }
@@ -68,7 +77,7 @@ export class AutoComplete {
6877
6978
70 /**79 /**
71 * @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete.80 * @param {HTMLTextAreaElement|HTMLInputElement} textarea The textarea to receive autocomplete.
72 * @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/')81 * @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/')
73 * @param {(text: string, index: number) => Promise<AutoCompleteNameResult>} getNameAt Function should return (unfiltered, matching against input is done in AutoComplete) information about name options at index in text.82 * @param {(text: string, index: number) => Promise<AutoCompleteNameResult>} getNameAt Function should return (unfiltered, matching against input is done in AutoComplete) information about name options at index in text.
74 * @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor.83 * @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor.
@@ -102,10 +111,15 @@ export class AutoComplete {
102 this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);111 this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);
103 this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);112 this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);
104113
105 textarea.addEventListener('input', ()=>this.text != this.textarea.value && this.show(true, this.wasForced));114 textarea.addEventListener('input', ()=>{
115 this.selectionStart = this.textarea.selectionStart;
116 if (this.text != this.textarea.value) this.show(true, this.wasForced);
117 });
106 textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));118 textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));
107 textarea.addEventListener('click', ()=>this.isActive ? this.show() : null);119 textarea.addEventListener('click', ()=>{
108 textarea.addEventListener('selectionchange', ()=>this.show());120 this.selectionStart = this.textarea.selectionStart;
121 if (this.isActive) this.show();
122 });
109 textarea.addEventListener('blur', ()=>this.hide());123 textarea.addEventListener('blur', ()=>this.hide());
110 if (isFloating) {124 if (isFloating) {
111 textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());125 textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());
@@ -189,6 +203,11 @@ export class AutoComplete {
189 * @returns The option.203 * @returns The option.
190 */204 */
191 fuzzyScore(option) {205 fuzzyScore(option) {
206 // might have been matched by the options matchProvider function instead
207 if (!this.fuzzyRegex.test(option.name)) {
208 option.score = new AutoCompleteFuzzyScore(Number.MAX_SAFE_INTEGER, -1);
209 return option;
210 }
192 const parts = this.fuzzyRegex.exec(option.name).slice(1, -1);211 const parts = this.fuzzyRegex.exec(option.name).slice(1, -1);
193 let start = null;212 let start = null;
194 let consecutive = [];213 let consecutive = [];
@@ -339,7 +358,7 @@ export class AutoComplete {
339358
340 this.result = this.effectiveParserResult.optionList359 this.result = this.effectiveParserResult.optionList
341 // filter the list of options by the partial name according to the matching type360 // filter the list of options by the partial name according to the matching type
342 .filter(it => this.isReplaceable || it.name == '' ? matchers[this.matchType](it.name) : it.name.toLowerCase() == this.name)361 .filter(it => this.isReplaceable || it.name == '' ? (it.matchProvider ? it.matchProvider(this.name) : matchers[this.matchType](it.name)) : it.name.toLowerCase() == this.name)
343 // remove aliases362 // remove aliases
344 .filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);363 .filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);
345364
@@ -357,10 +376,11 @@ export class AutoComplete {
357 // build element376 // build element
358 option.dom = this.makeItem(option);377 option.dom = this.makeItem(option);
359 // update replacer and add quotes if necessary378 // update replacer and add quotes if necessary
379 const optionName = option.valueProvider ? option.valueProvider(this.name) : option.name;
360 if (this.effectiveParserResult.canBeQuoted) {380 if (this.effectiveParserResult.canBeQuoted) {
361 option.replacer = option.name.includes(' ') || this.startQuote || this.endQuote ? `"${option.name}"` : `${option.name}`;381 option.replacer = optionName.includes(' ') || this.startQuote || this.endQuote ? `"${optionName.replace(/"/g, '\\"')}"` : `${optionName}`;
362 } else {382 } else {
363 option.replacer = option.name;383 option.replacer = optionName;
364 }384 }
365 // calculate fuzzy score if matching is fuzzy385 // calculate fuzzy score if matching is fuzzy
366 if (this.matchType == 'fuzzy') this.fuzzyScore(option);386 if (this.matchType == 'fuzzy') this.fuzzyScore(option);
@@ -399,7 +419,7 @@ export class AutoComplete {
399 ,419 ,
400 );420 );
401 this.result.push(option);421 this.result.push(option);
402 } else if (this.result.length == 1 && this.effectiveParserResult && this.result[0].name == this.effectiveParserResult.name) {422 } else if (this.result.length == 1 && this.effectiveParserResult && this.effectiveParserResult != this.secondaryParserResult && this.result[0].name == this.effectiveParserResult.name) {
403 // only one result that is exactly the current value? just show hint, no autocomplete423 // only one result that is exactly the current value? just show hint, no autocomplete
404 this.isReplaceable = false;424 this.isReplaceable = false;
405 this.isShowingDetails = false;425 this.isShowingDetails = false;
@@ -439,11 +459,14 @@ export class AutoComplete {
439 } else {459 } else {
440 item.dom.classList.remove('selected');460 item.dom.classList.remove('selected');
441 }461 }
462 if (!item.isSelectable) {
463 item.dom.classList.add('not-selectable');
464 }
442 frag.append(item.dom);465 frag.append(item.dom);
443 }466 }
444 this.dom.append(frag);467 this.dom.append(frag);
445 this.updatePosition();468 this.updatePosition();
446 getTopmostModalLayer().append(this.domWrap);469 this.getLayer().append(this.domWrap);
447 } else {470 } else {
448 this.domWrap.remove();471 this.domWrap.remove();
449 }472 }
@@ -458,10 +481,17 @@ export class AutoComplete {
458 if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();481 if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();
459 this.detailsDom.innerHTML = '';482 this.detailsDom.innerHTML = '';
460 this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');483 this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');
461 getTopmostModalLayer().append(this.detailsWrap);484 this.getLayer().append(this.detailsWrap);
462 this.updateDetailsPositionDebounced();485 this.updateDetailsPositionDebounced();
463 }486 }
464487
488 /**
489 * @returns {HTMLElement} closest ancestor dialog or body
490 */
491 getLayer() {
492 return this.textarea.closest('dialog, body');
493 }
494
465495
466496
467 /**497 /**
@@ -474,7 +504,7 @@ export class AutoComplete {
474 const rect = {};504 const rect = {};
475 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();505 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
476 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();506 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
477 rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayer().getBoundingClientRect();507 rect[AUTOCOMPLETE_WIDTH.FULL] = this.getLayer().getBoundingClientRect();
478 this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);508 this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
479 this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);509 this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
480 this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;510 this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;
@@ -501,7 +531,7 @@ export class AutoComplete {
501 const rect = {};531 const rect = {};
502 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();532 rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
503 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();533 rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
504 rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayer().getBoundingClientRect();534 rect[AUTOCOMPLETE_WIDTH.FULL] = this.getLayer().getBoundingClientRect();
505 if (this.isReplaceable) {535 if (this.isReplaceable) {
506 this.detailsWrap.classList.remove('full');536 this.detailsWrap.classList.remove('full');
507 const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();537 const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();
@@ -527,32 +557,34 @@ export class AutoComplete {
527 updateFloatingPosition() {557 updateFloatingPosition() {
528 const location = this.getCursorPosition();558 const location = this.getCursorPosition();
529 const rect = this.textarea.getBoundingClientRect();559 const rect = this.textarea.getBoundingClientRect();
560 const layerRect = this.getLayer().getBoundingClientRect();
530 // cursor is out of view -> hide561 // cursor is out of view -> hide
531 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {562 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
532 return this.hide();563 return this.hide();
533 }564 }
534 const left = Math.max(rect.left, location.left);565 const left = Math.max(rect.left, location.left) - layerRect.left;
535 this.domWrap.style.setProperty('--targetOffset', `${left}`);566 this.domWrap.style.setProperty('--targetOffset', `${left}`);
536 if (location.top <= window.innerHeight / 2) {567 if (location.top <= window.innerHeight / 2) {
537 // if cursor is in lower half of window, show list above line568 // if cursor is in lower half of window, show list above line
538 this.domWrap.style.top = `${location.bottom}px`;569 this.domWrap.style.top = `${location.bottom - layerRect.top}px`;
539 this.domWrap.style.bottom = 'auto';570 this.domWrap.style.bottom = 'auto';
540 this.domWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`;571 this.domWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
541 } else {572 } else {
542 // if cursor is in upper half of window, show list below line573 // if cursor is in upper half of window, show list below line
543 this.domWrap.style.top = 'auto';574 this.domWrap.style.top = 'auto';
544 this.domWrap.style.bottom = `calc(100vh - ${location.top}px)`;575 this.domWrap.style.bottom = `calc(${layerRect.height}px - ${location.top - layerRect.top}px)`;
545 this.domWrap.style.maxHeight = `calc(${location.top}px - 1vh)`;576 this.domWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
546 }577 }
547 }578 }
548579
549 updateFloatingDetailsPosition(location = null) {580 updateFloatingDetailsPosition(location = null) {
550 if (!location) location = this.getCursorPosition();581 if (!location) location = this.getCursorPosition();
551 const rect = this.textarea.getBoundingClientRect();582 const rect = this.textarea.getBoundingClientRect();
583 const layerRect = this.getLayer().getBoundingClientRect();
552 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {584 if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
553 return this.hide();585 return this.hide();
554 }586 }
555 const left = Math.max(rect.left, location.left);587 const left = Math.max(rect.left, location.left) - layerRect.left;
556 this.detailsWrap.style.setProperty('--targetOffset', `${left}`);588 this.detailsWrap.style.setProperty('--targetOffset', `${left}`);
557 if (this.isReplaceable) {589 if (this.isReplaceable) {
558 this.detailsWrap.classList.remove('full');590 this.detailsWrap.classList.remove('full');
@@ -572,14 +604,14 @@ export class AutoComplete {
572 }604 }
573 if (location.top <= window.innerHeight / 2) {605 if (location.top <= window.innerHeight / 2) {
574 // if cursor is in lower half of window, show list above line606 // if cursor is in lower half of window, show list above line
575 this.detailsWrap.style.top = `${location.bottom}px`;607 this.detailsWrap.style.top = `${location.bottom - layerRect.top}px`;
576 this.detailsWrap.style.bottom = 'auto';608 this.detailsWrap.style.bottom = 'auto';
577 this.detailsWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`;609 this.detailsWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
578 } else {610 } else {
579 // if cursor is in upper half of window, show list below line611 // if cursor is in upper half of window, show list below line
580 this.detailsWrap.style.top = 'auto';612 this.detailsWrap.style.top = 'auto';
581 this.detailsWrap.style.bottom = `calc(100vh - ${location.top}px)`;613 this.detailsWrap.style.bottom = `calc(${layerRect.height}px - ${location.top - layerRect.top}px)`;
582 this.detailsWrap.style.maxHeight = `calc(${location.top}px - 1vh)`;614 this.detailsWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
583 }615 }
584 }616 }
585617
@@ -597,7 +629,7 @@ export class AutoComplete {
597 }629 }
598 this.clone.style.position = 'fixed';630 this.clone.style.position = 'fixed';
599 this.clone.style.visibility = 'hidden';631 this.clone.style.visibility = 'hidden';
600 getTopmostModalLayer().append(this.clone);632 document.body.append(this.clone);
601 const mo = new MutationObserver(muts=>{633 const mo = new MutationObserver(muts=>{
602 if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {634 if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
603 this.clone.remove();635 this.clone.remove();
@@ -656,6 +688,7 @@ export class AutoComplete {
656 }688 }
657 this.wasForced = false;689 this.wasForced = false;
658 this.textarea.dispatchEvent(new Event('input', { bubbles:true }));690 this.textarea.dispatchEvent(new Event('input', { bubbles:true }));
691 this.onSelect?.(this.selectedItem);
659 }692 }
660693
661694
@@ -708,8 +741,10 @@ export class AutoComplete {
708 }741 }
709 case 'Enter': {742 case 'Enter': {
710 // pick the selected item to autocomplete743 // pick the selected item to autocomplete
744 if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.ENTER) != AUTOCOMPLETE_SELECT_KEY.ENTER) break;
711 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;745 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
712 if (this.selectedItem.name == this.name) break;746 if (this.selectedItem.name == this.name) break;
747 if (!this.selectedItem.isSelectable) break;
713 evt.preventDefault();748 evt.preventDefault();
714 evt.stopImmediatePropagation();749 evt.stopImmediatePropagation();
715 this.select();750 this.select();
@@ -717,9 +752,11 @@ export class AutoComplete {
717 }752 }
718 case 'Tab': {753 case 'Tab': {
719 // pick the selected item to autocomplete754 // pick the selected item to autocomplete
755 if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.TAB) != AUTOCOMPLETE_SELECT_KEY.TAB) break;
720 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;756 if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
721 evt.preventDefault();757 evt.preventDefault();
722 evt.stopImmediatePropagation();758 evt.stopImmediatePropagation();
759 if (!this.selectedItem.isSelectable) break;
723 this.select();760 this.select();
724 return;761 return;
725 }762 }
@@ -772,30 +809,16 @@ export class AutoComplete {
772 // ignore keydown on modifier keys809 // ignore keydown on modifier keys
773 return;810 return;
774 }811 }
775 switch (evt.key) {812 // await keyup to see if cursor position or text has changed
776 case 'ArrowUp':813 const oldText = this.textarea.value;
777 case 'ArrowDown':814 await new Promise(resolve=>{
778 case 'ArrowRight':815 window.addEventListener('keyup', resolve, { once:true });
779 case 'ArrowLeft': {816 });
780 if (this.isActive) {817 if (this.selectionStart != this.textarea.selectionStart) {
781 // keyboard navigation, wait for keyup to complete cursor move818 this.selectionStart = this.textarea.selectionStart;
782 const oldText = this.textarea.value;819 this.show(this.isReplaceable || oldText != this.textarea.value);
783 await new Promise(resolve=>{820 } else if (this.isActive) {
784 window.addEventListener('keyup', resolve, { once:true });821 this.text != this.textarea.value && this.show(this.isReplaceable);
785 });
786 if (this.selectionStart != this.textarea.selectionStart) {
787 this.selectionStart = this.textarea.selectionStart;
788 this.show(this.isReplaceable || oldText != this.textarea.value);
789 }
790 }
791 break;
792 }
793 default: {
794 if (this.isActive) {
795 this.text != this.textarea.value && this.show(this.isReplaceable);
796 }
797 break;
798 }
799 }822 }
800 }823 }
801}824}
public/scripts/autocomplete/AutoCompleteNameResult.js+3 -30
@@ -1,36 +1,9 @@
1import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';1import { AutoCompleteNameResultBase } from './AutoCompleteNameResultBase.js';
2import { AutoCompleteOption } from './AutoCompleteOption.js';2import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
3// import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
43
54
65
7export class AutoCompleteNameResult {6export class AutoCompleteNameResult extends AutoCompleteNameResultBase {
8 /**@type {string} */ name;
9 /**@type {number} */ start;
10 /**@type {AutoCompleteOption[]} */ optionList = [];
11 /**@type {boolean} */ canBeQuoted = false;
12 /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
13 /**@type {()=>string} */ makeNoOptionsText = ()=>'No options';
14
15
16 /**
17 * @param {string} name Name (potentially partial) of the name at the requested index.
18 * @param {number} start Index where the name starts.
19 * @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope.
20 * @param {boolean} canBeQuoted Whether the name can be inside quotes.
21 * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found.
22 * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against.
23 */
24 constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) {
25 this.name = name;
26 this.start = start;
27 this.optionList = optionList;
28 this.canBeQuoted = canBeQuoted;
29 this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
30 this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText;
31 }
32
33
34 /**7 /**
35 *8 *
36 * @param {string} text The whole text9 * @param {string} text The whole text
public/scripts/autocomplete/AutoCompleteNameResultBase.js+31 -0
@@ -0,0 +1,31 @@
1import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';
2import { AutoCompleteOption } from './AutoCompleteOption.js';
3
4
5
6export class AutoCompleteNameResultBase {
7 /**@type {string} */ name;
8 /**@type {number} */ start;
9 /**@type {AutoCompleteOption[]} */ optionList = [];
10 /**@type {boolean} */ canBeQuoted = false;
11 /**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
12 /**@type {()=>string} */ makeNoOptionsText = ()=>'No options';
13
14
15 /**
16 * @param {string} name Name (potentially partial) of the name at the requested index.
17 * @param {number} start Index where the name starts.
18 * @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope.
19 * @param {boolean} canBeQuoted Whether the name can be inside quotes.
20 * @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found.
21 * @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against.
22 */
23 constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) {
24 this.name = name;
25 this.start = start;
26 this.optionList = optionList;
27 this.canBeQuoted = canBeQuoted;
28 this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
29 this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText;
30 }
31}
public/scripts/autocomplete/AutoCompleteOption.js+11 -1
@@ -11,6 +11,9 @@ export class AutoCompleteOption {
11 /**@type {AutoCompleteFuzzyScore}*/ score;11 /**@type {AutoCompleteFuzzyScore}*/ score;
12 /**@type {string}*/ replacer;12 /**@type {string}*/ replacer;
13 /**@type {HTMLElement}*/ dom;13 /**@type {HTMLElement}*/ dom;
14 /**@type {(input:string)=>boolean}*/ matchProvider;
15 /**@type {(input:string)=>string}*/ valueProvider;
16 /**@type {boolean}*/ makeSelectable = false;
1417
1518
16 /**19 /**
@@ -21,14 +24,21 @@ export class AutoCompleteOption {
21 return this.name;24 return this.name;
22 }25 }
2326
27 get isSelectable() {
28 return this.makeSelectable || !this.valueProvider;
29 }
30
2431
25 /**32 /**
26 * @param {string} name33 * @param {string} name
27 */34 */
28 constructor(name, typeIcon = ' ', type = '') {35 constructor(name, typeIcon = ' ', type = '', matchProvider = null, valueProvider = null, makeSelectable = false) {
29 this.name = name;36 this.name = name;
30 this.typeIcon = typeIcon;37 this.typeIcon = typeIcon;
31 this.type = type;38 this.type = type;
39 this.matchProvider = matchProvider;
40 this.valueProvider = valueProvider;
41 this.makeSelectable = makeSelectable;
32 }42 }
3343
3444
public/scripts/autocomplete/AutoCompleteSecondaryNameResult.js+2 -2
@@ -1,6 +1,6 @@
1import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';1import { AutoCompleteNameResultBase } from './AutoCompleteNameResultBase.js';
22
3export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResult {3export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResultBase {
4 /**@type {boolean}*/ isRequired = false;4 /**@type {boolean}*/ isRequired = false;
5 /**@type {boolean}*/ forceMatch = true;5 /**@type {boolean}*/ forceMatch = true;
6}6}
public/scripts/extensions/quick-reply/api/QuickReplyApi.js+88 -66
@@ -24,9 +24,17 @@ export class QuickReplyApi {
2424
2525
26 /**26 /**
27 * @param {QuickReply} qr
28 * @returns {QuickReplySet}
29 */
30 getSetByQr(qr) {
31 return QuickReplySet.list.find(it=>it.qrList.includes(qr));
32 }
33
34 /**
27 * Finds and returns an existing Quick Reply Set by its name.35 * Finds and returns an existing Quick Reply Set by its name.
28 *36 *
29 * @param {String} name name of the quick reply set37 * @param {string} name name of the quick reply set
30 * @returns the quick reply set, or undefined if not found38 * @returns the quick reply set, or undefined if not found
31 */39 */
32 getSetByName(name) {40 getSetByName(name) {
@@ -36,13 +44,14 @@ export class QuickReplyApi {
36 /**44 /**
37 * Finds and returns an existing Quick Reply by its set's name and its label.45 * Finds and returns an existing Quick Reply by its set's name and its label.
38 *46 *
39 * @param {String} setName name of the quick reply set47 * @param {string} setName name of the quick reply set
40 * @param {String} label label of the quick reply48 * @param {string|number} label label or numeric ID of the quick reply
41 * @returns the quick reply, or undefined if not found49 * @returns the quick reply, or undefined if not found
42 */50 */
43 getQrByLabel(setName, label) {51 getQrByLabel(setName, label) {
44 const set = this.getSetByName(setName);52 const set = this.getSetByName(setName);
45 if (!set) return;53 if (!set) return;
54 if (Number.isInteger(label)) return set.qrList.find(it=>it.id == label);
46 return set.qrList.find(it=>it.label == label);55 return set.qrList.find(it=>it.label == label);
47 }56 }
4857
@@ -70,24 +79,25 @@ export class QuickReplyApi {
70 /**79 /**
71 * Executes an existing quick reply.80 * Executes an existing quick reply.
72 *81 *
73 * @param {String} setName name of the existing quick reply set82 * @param {string} setName name of the existing quick reply set
74 * @param {String} label label of the existing quick reply (text on the button)83 * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
75 * @param {Object} [args] optional arguments84 * @param {object} [args] optional arguments
85 * @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options] optional execution options
76 */86 */
77 async executeQuickReply(setName, label, args = {}) {87 async executeQuickReply(setName, label, args = {}, options = {}) {
78 const qr = this.getQrByLabel(setName, label);88 const qr = this.getQrByLabel(setName, label);
79 if (!qr) {89 if (!qr) {
80 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);90 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
81 }91 }
82 return await qr.execute(args);92 return await qr.execute(args, false, false, options);
83 }93 }
8494
8595
86 /**96 /**
87 * Adds or removes a quick reply set to the list of globally active quick reply sets.97 * Adds or removes a quick reply set to the list of globally active quick reply sets.
88 *98 *
89 * @param {String} name the name of the set99 * @param {string} name the name of the set
90 * @param {Boolean} isVisible whether to show the set's buttons or not100 * @param {boolean} isVisible whether to show the set's buttons or not
91 */101 */
92 toggleGlobalSet(name, isVisible = true) {102 toggleGlobalSet(name, isVisible = true) {
93 const set = this.getSetByName(name);103 const set = this.getSetByName(name);
@@ -104,8 +114,8 @@ export class QuickReplyApi {
104 /**114 /**
105 * Adds a quick reply set to the list of globally active quick reply sets.115 * Adds a quick reply set to the list of globally active quick reply sets.
106 *116 *
107 * @param {String} name the name of the set117 * @param {string} name the name of the set
108 * @param {Boolean} isVisible whether to show the set's buttons or not118 * @param {boolean} isVisible whether to show the set's buttons or not
109 */119 */
110 addGlobalSet(name, isVisible = true) {120 addGlobalSet(name, isVisible = true) {
111 const set = this.getSetByName(name);121 const set = this.getSetByName(name);
@@ -118,7 +128,7 @@ export class QuickReplyApi {
118 /**128 /**
119 * Removes a quick reply set from the list of globally active quick reply sets.129 * Removes a quick reply set from the list of globally active quick reply sets.
120 *130 *
121 * @param {String} name the name of the set131 * @param {string} name the name of the set
122 */132 */
123 removeGlobalSet(name) {133 removeGlobalSet(name) {
124 const set = this.getSetByName(name);134 const set = this.getSetByName(name);
@@ -132,8 +142,8 @@ export class QuickReplyApi {
132 /**142 /**
133 * Adds or removes a quick reply set to the list of the current chat's active quick reply sets.143 * Adds or removes a quick reply set to the list of the current chat's active quick reply sets.
134 *144 *
135 * @param {String} name the name of the set145 * @param {string} name the name of the set
136 * @param {Boolean} isVisible whether to show the set's buttons or not146 * @param {boolean} isVisible whether to show the set's buttons or not
137 */147 */
138 toggleChatSet(name, isVisible = true) {148 toggleChatSet(name, isVisible = true) {
139 if (!this.settings.chatConfig) return;149 if (!this.settings.chatConfig) return;
@@ -151,8 +161,8 @@ export class QuickReplyApi {
151 /**161 /**
152 * Adds a quick reply set to the list of the current chat's active quick reply sets.162 * Adds a quick reply set to the list of the current chat's active quick reply sets.
153 *163 *
154 * @param {String} name the name of the set164 * @param {string} name the name of the set
155 * @param {Boolean} isVisible whether to show the set's buttons or not165 * @param {boolean} isVisible whether to show the set's buttons or not
156 */166 */
157 addChatSet(name, isVisible = true) {167 addChatSet(name, isVisible = true) {
158 if (!this.settings.chatConfig) return;168 if (!this.settings.chatConfig) return;
@@ -166,7 +176,7 @@ export class QuickReplyApi {
166 /**176 /**
167 * Removes a quick reply set from the list of the current chat's active quick reply sets.177 * Removes a quick reply set from the list of the current chat's active quick reply sets.
168 *178 *
169 * @param {String} name the name of the set179 * @param {string} name the name of the set
170 */180 */
171 removeChatSet(name) {181 removeChatSet(name) {
172 if (!this.settings.chatConfig) return;182 if (!this.settings.chatConfig) return;
@@ -181,21 +191,25 @@ export class QuickReplyApi {
181 /**191 /**
182 * Creates a new quick reply in an existing quick reply set.192 * Creates a new quick reply in an existing quick reply set.
183 *193 *
184 * @param {String} setName name of the quick reply set to insert the new quick reply into194 * @param {string} setName name of the quick reply set to insert the new quick reply into
185 * @param {String} label label for the new quick reply (text on the button)195 * @param {string} label label for the new quick reply (text on the button)
186 * @param {Object} [props]196 * @param {object} [props]
187 * @param {String} [props.message] the message to be sent or slash command to be executed by the new quick reply197 * @param {string} [props.icon] the icon to show on the QR button
188 * @param {String} [props.title] the title / tooltip to be shown on the quick reply button198 * @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned
189 * @param {Boolean} [props.isHidden] whether to hide or show the button199 * @param {string} [props.message] the message to be sent or slash command to be executed by the new quick reply
190 * @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts200 * @param {string} [props.title] the title / tooltip to be shown on the quick reply button
191 * @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message201 * @param {boolean} [props.isHidden] whether to hide or show the button
192 * @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message202 * @param {boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
193 * @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded203 * @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
194 * @param {Boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected204 * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
195 * @param {String} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated205 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
206 * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
207 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
196 * @returns {QuickReply} the new quick reply208 * @returns {QuickReply} the new quick reply
197 */209 */
198 createQuickReply(setName, label, {210 createQuickReply(setName, label, {
211 icon,
212 showLabel,
199 message,213 message,
200 title,214 title,
201 isHidden,215 isHidden,
@@ -212,6 +226,8 @@ export class QuickReplyApi {
212 }226 }
213 const qr = set.addQuickReply();227 const qr = set.addQuickReply();
214 qr.label = label ?? '';228 qr.label = label ?? '';
229 qr.icon = icon ?? '';
230 qr.showLabel = showLabel ?? false;
215 qr.message = message ?? '';231 qr.message = message ?? '';
216 qr.title = title ?? '';232 qr.title = title ?? '';
217 qr.isHidden = isHidden ?? false;233 qr.isHidden = isHidden ?? false;
@@ -228,22 +244,26 @@ export class QuickReplyApi {
228 /**244 /**
229 * Updates an existing quick reply.245 * Updates an existing quick reply.
230 *246 *
231 * @param {String} setName name of the existing quick reply set247 * @param {string} setName name of the existing quick reply set
232 * @param {String} label label of the existing quick reply (text on the button)248 * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
233 * @param {Object} [props]249 * @param {object} [props]
234 * @param {String} [props.newLabel] new label for quick reply (text on the button)250 * @param {string} [props.icon] the icon to show on the QR button
235 * @param {String} [props.message] the message to be sent or slash command to be executed by the quick reply251 * @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned
236 * @param {String} [props.title] the title / tooltip to be shown on the quick reply button252 * @param {string} [props.newLabel] new label for quick reply (text on the button)
237 * @param {Boolean} [props.isHidden] whether to hide or show the button253 * @param {string} [props.message] the message to be sent or slash command to be executed by the quick reply
238 * @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts254 * @param {string} [props.title] the title / tooltip to be shown on the quick reply button
239 * @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message255 * @param {boolean} [props.isHidden] whether to hide or show the button
240 * @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message256 * @param {boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
241 * @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded257 * @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
242 * @param {Boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected258 * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
243 * @param {String} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated259 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
260 * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
261 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
244 * @returns {QuickReply} the altered quick reply262 * @returns {QuickReply} the altered quick reply
245 */263 */
246 updateQuickReply(setName, label, {264 updateQuickReply(setName, label, {
265 icon,
266 showLabel,
247 newLabel,267 newLabel,
248 message,268 message,
249 title,269 title,
@@ -259,6 +279,8 @@ export class QuickReplyApi {
259 if (!qr) {279 if (!qr) {
260 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);280 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
261 }281 }
282 qr.updateIcon(icon ?? qr.icon);
283 qr.updateShowLabel(showLabel ?? qr.showLabel);
262 qr.updateLabel(newLabel ?? qr.label);284 qr.updateLabel(newLabel ?? qr.label);
263 qr.updateMessage(message ?? qr.message);285 qr.updateMessage(message ?? qr.message);
264 qr.updateTitle(title ?? qr.title);286 qr.updateTitle(title ?? qr.title);
@@ -276,8 +298,8 @@ export class QuickReplyApi {
276 /**298 /**
277 * Deletes an existing quick reply.299 * Deletes an existing quick reply.
278 *300 *
279 * @param {String} setName name of the existing quick reply set301 * @param {string} setName name of the existing quick reply set
280 * @param {String} label label of the existing quick reply (text on the button)302 * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
281 */303 */
282 deleteQuickReply(setName, label) {304 deleteQuickReply(setName, label) {
283 const qr = this.getQrByLabel(setName, label);305 const qr = this.getQrByLabel(setName, label);
@@ -291,10 +313,10 @@ export class QuickReplyApi {
291 /**313 /**
292 * Adds an existing quick reply set as a context menu to an existing quick reply.314 * Adds an existing quick reply set as a context menu to an existing quick reply.
293 *315 *
294 * @param {String} setName name of the existing quick reply set containing the quick reply316 * @param {string} setName name of the existing quick reply set containing the quick reply
295 * @param {String} label label of the existing quick reply317 * @param {string|number} label label of the existing quick reply or its numeric ID
296 * @param {String} contextSetName name of the existing quick reply set to be used as a context menu318 * @param {string} contextSetName name of the existing quick reply set to be used as a context menu
297 * @param {Boolean} isChained whether or not to chain the context menu quick replies319 * @param {boolean} isChained whether or not to chain the context menu quick replies
298 */320 */
299 createContextItem(setName, label, contextSetName, isChained = false) {321 createContextItem(setName, label, contextSetName, isChained = false) {
300 const qr = this.getQrByLabel(setName, label);322 const qr = this.getQrByLabel(setName, label);
@@ -314,9 +336,9 @@ export class QuickReplyApi {
314 /**336 /**
315 * Removes a quick reply set from a quick reply's context menu.337 * Removes a quick reply set from a quick reply's context menu.
316 *338 *
317 * @param {String} setName name of the existing quick reply set containing the quick reply339 * @param {string} setName name of the existing quick reply set containing the quick reply
318 * @param {String} label label of the existing quick reply340 * @param {string|number} label label of the existing quick reply or its numeric ID
319 * @param {String} contextSetName name of the existing quick reply set to be used as a context menu341 * @param {string} contextSetName name of the existing quick reply set to be used as a context menu
320 */342 */
321 deleteContextItem(setName, label, contextSetName) {343 deleteContextItem(setName, label, contextSetName) {
322 const qr = this.getQrByLabel(setName, label);344 const qr = this.getQrByLabel(setName, label);
@@ -333,8 +355,8 @@ export class QuickReplyApi {
333 /**355 /**
334 * Removes all entries from a quick reply's context menu.356 * Removes all entries from a quick reply's context menu.
335 *357 *
336 * @param {String} setName name of the existing quick reply set containing the quick reply358 * @param {string} setName name of the existing quick reply set containing the quick reply
337 * @param {String} label label of the existing quick reply359 * @param {string|number} label label of the existing quick reply or its numeric ID
338 */360 */
339 clearContextMenu(setName, label) {361 clearContextMenu(setName, label) {
340 const qr = this.getQrByLabel(setName, label);362 const qr = this.getQrByLabel(setName, label);
@@ -348,11 +370,11 @@ export class QuickReplyApi {
348 /**370 /**
349 * Create a new quick reply set.371 * Create a new quick reply set.
350 *372 *
351 * @param {String} name name of the new quick reply set373 * @param {string} name name of the new quick reply set
352 * @param {Object} [props]374 * @param {object} [props]
353 * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box375 * @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
354 * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input376 * @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
355 * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply377 * @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
356 * @returns {Promise<QuickReplySet>} the new quick reply set378 * @returns {Promise<QuickReplySet>} the new quick reply set
357 */379 */
358 async createSet(name, {380 async createSet(name, {
@@ -384,11 +406,11 @@ export class QuickReplyApi {
384 /**406 /**
385 * Update an existing quick reply set.407 * Update an existing quick reply set.
386 *408 *
387 * @param {String} name name of the existing quick reply set409 * @param {string} name name of the existing quick reply set
388 * @param {Object} [props]410 * @param {object} [props]
389 * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box411 * @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
390 * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input412 * @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
391 * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply413 * @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
392 * @returns {Promise<QuickReplySet>} the altered quick reply set414 * @returns {Promise<QuickReplySet>} the altered quick reply set
393 */415 */
394 async updateSet(name, {416 async updateSet(name, {
@@ -411,7 +433,7 @@ export class QuickReplyApi {
411 /**433 /**
412 * Delete an existing quick reply set.434 * Delete an existing quick reply set.
413 *435 *
414 * @param {String} name name of the existing quick reply set436 * @param {string} name name of the existing quick reply set
415 */437 */
416 async deleteSet(name) {438 async deleteSet(name) {
417 const set = this.getSetByName(name);439 const set = this.getSetByName(name);
@@ -451,7 +473,7 @@ export class QuickReplyApi {
451 /**473 /**
452 * Gets a list of all quick replies in the quick reply set.474 * Gets a list of all quick replies in the quick reply set.
453 *475 *
454 * @param {String} setName name of the existing quick reply set476 * @param {string} setName name of the existing quick reply set
455 * @returns array with the labels of this set's quick replies477 * @returns array with the labels of this set's quick replies
456 */478 */
457 listQuickReplies(setName) {479 listQuickReplies(setName) {
public/scripts/extensions/quick-reply/html/qrEditor.html+34 -8
@@ -2,10 +2,23 @@
2 <div id="qr--main">2 <div id="qr--main">
3 <h3 data-i18n="Labels and Message">Labels and Message</h3>3 <h3 data-i18n="Labels and Message">Labels and Message</h3>
4 <div class="qr--labels">4 <div class="qr--labels">
5 <label>5 <label class="qr--fit">
6 <span class="qr--labelText" data-i18n="Label">Label</span>6 <span class="qr--labelText" data-i18n="Label">Icon</span>
7 <input type="text" class="text_pole" id="qr--modal-label">7 <small class="qr--labelHint">&nbsp;</small>
8 <div class="menu_button fa-fw" id="qr--modal-icon" title="Click to change icon"></div>
8 </label>9 </label>
10 <div class="label">
11 <span class="qr--labelText" data-i18n="Label">Label</span>
12 <small class="qr--labelHint" data-i18n="(label of the button, if no icon is chosen) ">(label of the button, if no icon is chosen)</small>
13 <div class="qr--inputGroup">
14 <label class="checkbox_label" title="Show label even if an icon is assigned">
15 <input type="checkbox" id="qr--modal-showLabel">
16 Show
17 </label>
18 <input type="text" class="text_pole" id="qr--modal-label">
19 <div class="menu_button fa-fw fa-solid fa-chevron-down" id="qr--modal-switcher" title="Switch to another QR"></div>
20 </div>
21 </div>
9 <label>22 <label>
10 <span class="qr--labelText" data-i18n="Title">Title</span>23 <span class="qr--labelText" data-i18n="Title">Title</span>
11 <small class="qr--labelHint" data-i18n="(tooltip, leave empty to show message or /command)">(tooltip, leave empty to show message or /command)</small>24 <small class="qr--labelHint" data-i18n="(tooltip, leave empty to show message or /command)">(tooltip, leave empty to show message or /command)</small>
@@ -33,6 +46,8 @@
33 <input type="checkbox" id="qr--modal-syntax">46 <input type="checkbox" id="qr--modal-syntax">
34 <span>Syntax highlight</span>47 <span>Syntax highlight</span>
35 </label>48 </label>
49 <small>Ctrl+Alt+Click (or F9) to set / remove breakpoints</small>
50 <small>Ctrl+<span id="qr--modal-commentKey"></span> to toggle block comments</small>
36 </div>51 </div>
37 <div id="qr--modal-messageHolder">52 <div id="qr--modal-messageHolder">
38 <pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre>53 <pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre>
@@ -43,6 +58,10 @@
4358
4459
4560
61 <div id="qr--resizeHandle"></div>
62
63
64
46 <div id="qr--qrOptions">65 <div id="qr--qrOptions">
47 <h3 data-i18n="Context Menu">Context Menu</h3>66 <h3 data-i18n="Context Menu">Context Menu</h3>
48 <div id="qr--ctxEditor">67 <div id="qr--ctxEditor">
@@ -64,7 +83,7 @@
6483
6584
66 <h3 data-i18n="Auto-Execute">Auto-Execute</h3>85 <h3 data-i18n="Auto-Execute">Auto-Execute</h3>
67 <div class="flex-container flexFlowColumn">86 <div id="qr--autoExec" class="flex-container flexFlowColumn">
68 <label class="checkbox_label" title="Prevent this quick reply from triggering other auto-executed quick replies while auto-executing (i.e., prevent recursive auto-execution)">87 <label class="checkbox_label" title="Prevent this quick reply from triggering other auto-executed quick replies while auto-executing (i.e., prevent recursive auto-execution)">
69 <input type="checkbox" id="qr--preventAutoExecute" >88 <input type="checkbox" id="qr--preventAutoExecute" >
70 <span><i class="fa-solid fa-fw fa-plane-slash"></i><span data-i18n="Don't trigger auto-execute">Don't trigger auto-execute</span></span>89 <span><i class="fa-solid fa-fw fa-plane-slash"></i><span data-i18n="Don't trigger auto-execute">Don't trigger auto-execute</span></span>
@@ -117,11 +136,18 @@
117 </div>136 </div>
118 </div>137 </div>
119 <div id="qr--modal-executeProgress"></div>138 <div id="qr--modal-executeProgress"></div>
120 <label class="checkbox_label">
121 <input type="checkbox" id="qr--modal-executeHide">
122 <span title="Hide editor while executing"> Hide editor while executing</span>
123 </label>
124 <div id="qr--modal-executeErrors"></div>139 <div id="qr--modal-executeErrors"></div>
125 <div id="qr--modal-executeResult"></div>140 <div id="qr--modal-executeResult"></div>
141
142 <div id="qr--modal-debugButtons">
143 <div title="Resume" id="qr--modal-resume" class="qr--modal-debugButton menu_button"></div>
144 <div title="Step Over" id="qr--modal-step" class="qr--modal-debugButton menu_button"></div>
145 <div title="Step Into" id="qr--modal-stepInto" class="qr--modal-debugButton menu_button"></div>
146 <div title="Step Out" id="qr--modal-stepOut" class="qr--modal-debugButton menu_button"></div>
147 <div title="Minimize" id="qr--modal-minimize" class="qr--modal-debugButton menu_button fa-solid fa-minimize"></div>
148 <div title="Maximize" id="qr--modal-maximize" class="qr--modal-debugButton menu_button fa-solid fa-maximize"></div>
149 </div>
150 <textarea rows="1" id="qr--modal-send_textarea" placeholder="Chat input for use with {{input}}" title="Chat input for use with {{input}}"></textarea>
151 <div id="qr--modal-debugState"></div>
126 </div>152 </div>
127</div>153</div>
public/scripts/extensions/quick-reply/html/settings.html+10 -0
@@ -60,10 +60,20 @@
60 <label class="flex-container" id="qr--injectInputContainer">60 <label class="flex-container" id="qr--injectInputContainer">
61 <input type="checkbox" id="qr--injectInput"> <span><span data-i18n="Inject user input automatically">Inject user input automatically</span> <small><span data-i18n="(if disabled, use ">(if disabled, use</span><code>{{input}}</code> <span data-i18n="macro for manual injection)">macro for manual injection)</span></small></span>61 <input type="checkbox" id="qr--injectInput"> <span><span data-i18n="Inject user input automatically">Inject user input automatically</span> <small><span data-i18n="(if disabled, use ">(if disabled, use</span><code>{{input}}</code> <span data-i18n="macro for manual injection)">macro for manual injection)</span></small></span>
62 </label>62 </label>
63 <div class="flex-container alignItemsCenter">
64 <toolcool-color-picker id="qr--color"></toolcool-color-picker>
65 <div class="menu_button" id="qr--colorClear">Clear</div>
66 <span data-i18n="Color">Color</span>
67 </div>
68 <label class="flex-container" id="qr--onlyBorderColorContainer">
69 <input type="checkbox" id="qr--onlyBorderColor"> <span data-i18n="Only apply color as accent">Only apply color as accent</span>
70 </label>
63 </div>71 </div>
64 <div id="qr--set-qrList" class="qr--qrList"></div>72 <div id="qr--set-qrList" class="qr--qrList"></div>
65 <div class="qr--set-qrListActions">73 <div class="qr--set-qrListActions">
66 <div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-add" title="Add quick reply"></div>74 <div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-add" title="Add quick reply"></div>
75 <div class="qr--paste menu_button menu_button_icon fa-solid fa-paste" id="qr--set-paste" title="Paste quick reply from clipboard"></div>
76 <div class="qr--import menu_button menu_button_icon fa-solid fa-file-import" id="qr--set-importQr" title="Import quick reply from file"></div>
67 </div>77 </div>
68 </div>78 </div>
69 </div>79 </div>
public/scripts/extensions/quick-reply/index.js+2 -2
@@ -176,7 +176,7 @@ const init = async () => {
176 buttons.show();176 buttons.show();
177 settings.onSave = ()=>buttons.refresh();177 settings.onSave = ()=>buttons.refresh();
178178
179 window['executeQuickReplyByName'] = async(name, args = {}) => {179 window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => {
180 let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]180 let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]
181 .map(it=>it.set.qrList)181 .map(it=>it.set.qrList)
182 .flat()182 .flat()
@@ -191,7 +191,7 @@ const init = async () => {
191 }191 }
192 }192 }
193 if (qr && qr.onExecute) {193 if (qr && qr.onExecute) {
194 return await qr.execute(args, false, true);194 return await qr.execute(args, false, true, options);
195 } else {195 } else {
196 throw new Error(`No Quick Reply found for "${name}".`);196 throw new Error(`No Quick Reply found for "${name}".`);
197 }197 }
public/scripts/extensions/quick-reply/lib/morphdom-esm.js+769 -0
@@ -0,0 +1,769 @@
1var DOCUMENT_FRAGMENT_NODE = 11;
2
3function morphAttrs(fromNode, toNode) {
4 var toNodeAttrs = toNode.attributes;
5 var attr;
6 var attrName;
7 var attrNamespaceURI;
8 var attrValue;
9 var fromValue;
10
11 // document-fragments dont have attributes so lets not do anything
12 if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) {
13 return;
14 }
15
16 // update attributes on original DOM element
17 for (var i = toNodeAttrs.length - 1; i >= 0; i--) {
18 attr = toNodeAttrs[i];
19 attrName = attr.name;
20 attrNamespaceURI = attr.namespaceURI;
21 attrValue = attr.value;
22
23 if (attrNamespaceURI) {
24 attrName = attr.localName || attrName;
25 fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);
26
27 if (fromValue !== attrValue) {
28 if (attr.prefix === 'xmlns'){
29 attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix
30 }
31 fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);
32 }
33 } else {
34 fromValue = fromNode.getAttribute(attrName);
35
36 if (fromValue !== attrValue) {
37 fromNode.setAttribute(attrName, attrValue);
38 }
39 }
40 }
41
42 // Remove any extra attributes found on the original DOM element that
43 // weren't found on the target element.
44 var fromNodeAttrs = fromNode.attributes;
45
46 for (var d = fromNodeAttrs.length - 1; d >= 0; d--) {
47 attr = fromNodeAttrs[d];
48 attrName = attr.name;
49 attrNamespaceURI = attr.namespaceURI;
50
51 if (attrNamespaceURI) {
52 attrName = attr.localName || attrName;
53
54 if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) {
55 fromNode.removeAttributeNS(attrNamespaceURI, attrName);
56 }
57 } else {
58 if (!toNode.hasAttribute(attrName)) {
59 fromNode.removeAttribute(attrName);
60 }
61 }
62 }
63}
64
65var range; // Create a range object for efficently rendering strings to elements.
66var NS_XHTML = 'http://www.w3.org/1999/xhtml';
67
68var doc = typeof document === 'undefined' ? undefined : document;
69var HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template');
70var HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange();
71
72function createFragmentFromTemplate(str) {
73 var template = doc.createElement('template');
74 template.innerHTML = str;
75 return template.content.childNodes[0];
76}
77
78function createFragmentFromRange(str) {
79 if (!range) {
80 range = doc.createRange();
81 range.selectNode(doc.body);
82 }
83
84 var fragment = range.createContextualFragment(str);
85 return fragment.childNodes[0];
86}
87
88function createFragmentFromWrap(str) {
89 var fragment = doc.createElement('body');
90 fragment.innerHTML = str;
91 return fragment.childNodes[0];
92}
93
94/**
95 * This is about the same
96 * var html = new DOMParser().parseFromString(str, 'text/html');
97 * return html.body.firstChild;
98 *
99 * @method toElement
100 * @param {String} str
101 */
102function toElement(str) {
103 str = str.trim();
104 if (HAS_TEMPLATE_SUPPORT) {
105 // avoid restrictions on content for things like `<tr><th>Hi</th></tr>` which
106 // createContextualFragment doesn't support
107 // <template> support not available in IE
108 return createFragmentFromTemplate(str);
109 } else if (HAS_RANGE_SUPPORT) {
110 return createFragmentFromRange(str);
111 }
112
113 return createFragmentFromWrap(str);
114}
115
116/**
117 * Returns true if two node's names are the same.
118 *
119 * NOTE: We don't bother checking `namespaceURI` because you will never find two HTML elements with the same
120 * nodeName and different namespace URIs.
121 *
122 * @param {Element} a
123 * @param {Element} b The target element
124 * @return {boolean}
125 */
126function compareNodeNames(fromEl, toEl) {
127 var fromNodeName = fromEl.nodeName;
128 var toNodeName = toEl.nodeName;
129 var fromCodeStart, toCodeStart;
130
131 if (fromNodeName === toNodeName) {
132 return true;
133 }
134
135 fromCodeStart = fromNodeName.charCodeAt(0);
136 toCodeStart = toNodeName.charCodeAt(0);
137
138 // If the target element is a virtual DOM node or SVG node then we may
139 // need to normalize the tag name before comparing. Normal HTML elements that are
140 // in the "http://www.w3.org/1999/xhtml"
141 // are converted to upper case
142 if (fromCodeStart <= 90 && toCodeStart >= 97) { // from is upper and to is lower
143 return fromNodeName === toNodeName.toUpperCase();
144 } else if (toCodeStart <= 90 && fromCodeStart >= 97) { // to is upper and from is lower
145 return toNodeName === fromNodeName.toUpperCase();
146 } else {
147 return false;
148 }
149}
150
151/**
152 * Create an element, optionally with a known namespace URI.
153 *
154 * @param {string} name the element name, e.g. 'div' or 'svg'
155 * @param {string} [namespaceURI] the element's namespace URI, i.e. the value of
156 * its `xmlns` attribute or its inferred namespace.
157 *
158 * @return {Element}
159 */
160function createElementNS(name, namespaceURI) {
161 return !namespaceURI || namespaceURI === NS_XHTML ?
162 doc.createElement(name) :
163 doc.createElementNS(namespaceURI, name);
164}
165
166/**
167 * Copies the children of one DOM element to another DOM element
168 */
169function moveChildren(fromEl, toEl) {
170 var curChild = fromEl.firstChild;
171 while (curChild) {
172 var nextChild = curChild.nextSibling;
173 toEl.appendChild(curChild);
174 curChild = nextChild;
175 }
176 return toEl;
177}
178
179function syncBooleanAttrProp(fromEl, toEl, name) {
180 if (fromEl[name] !== toEl[name]) {
181 fromEl[name] = toEl[name];
182 if (fromEl[name]) {
183 fromEl.setAttribute(name, '');
184 } else {
185 fromEl.removeAttribute(name);
186 }
187 }
188}
189
190var specialElHandlers = {
191 OPTION: function(fromEl, toEl) {
192 var parentNode = fromEl.parentNode;
193 if (parentNode) {
194 var parentName = parentNode.nodeName.toUpperCase();
195 if (parentName === 'OPTGROUP') {
196 parentNode = parentNode.parentNode;
197 parentName = parentNode && parentNode.nodeName.toUpperCase();
198 }
199 if (parentName === 'SELECT' && !parentNode.hasAttribute('multiple')) {
200 if (fromEl.hasAttribute('selected') && !toEl.selected) {
201 // Workaround for MS Edge bug where the 'selected' attribute can only be
202 // removed if set to a non-empty value:
203 // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12087679/
204 fromEl.setAttribute('selected', 'selected');
205 fromEl.removeAttribute('selected');
206 }
207 // We have to reset select element's selectedIndex to -1, otherwise setting
208 // fromEl.selected using the syncBooleanAttrProp below has no effect.
209 // The correct selectedIndex will be set in the SELECT special handler below.
210 parentNode.selectedIndex = -1;
211 }
212 }
213 syncBooleanAttrProp(fromEl, toEl, 'selected');
214 },
215 /**
216 * The "value" attribute is special for the <input> element since it sets
217 * the initial value. Changing the "value" attribute without changing the
218 * "value" property will have no effect since it is only used to the set the
219 * initial value. Similar for the "checked" attribute, and "disabled".
220 */
221 INPUT: function(fromEl, toEl) {
222 syncBooleanAttrProp(fromEl, toEl, 'checked');
223 syncBooleanAttrProp(fromEl, toEl, 'disabled');
224
225 if (fromEl.value !== toEl.value) {
226 fromEl.value = toEl.value;
227 }
228
229 if (!toEl.hasAttribute('value')) {
230 fromEl.removeAttribute('value');
231 }
232 },
233
234 TEXTAREA: function(fromEl, toEl) {
235 var newValue = toEl.value;
236 if (fromEl.value !== newValue) {
237 fromEl.value = newValue;
238 }
239
240 var firstChild = fromEl.firstChild;
241 if (firstChild) {
242 // Needed for IE. Apparently IE sets the placeholder as the
243 // node value and vise versa. This ignores an empty update.
244 var oldValue = firstChild.nodeValue;
245
246 if (oldValue == newValue || (!newValue && oldValue == fromEl.placeholder)) {
247 return;
248 }
249
250 firstChild.nodeValue = newValue;
251 }
252 },
253 SELECT: function(fromEl, toEl) {
254 if (!toEl.hasAttribute('multiple')) {
255 var selectedIndex = -1;
256 var i = 0;
257 // We have to loop through children of fromEl, not toEl since nodes can be moved
258 // from toEl to fromEl directly when morphing.
259 // At the time this special handler is invoked, all children have already been morphed
260 // and appended to / removed from fromEl, so using fromEl here is safe and correct.
261 var curChild = fromEl.firstChild;
262 var optgroup;
263 var nodeName;
264 while(curChild) {
265 nodeName = curChild.nodeName && curChild.nodeName.toUpperCase();
266 if (nodeName === 'OPTGROUP') {
267 optgroup = curChild;
268 curChild = optgroup.firstChild;
269 } else {
270 if (nodeName === 'OPTION') {
271 if (curChild.hasAttribute('selected')) {
272 selectedIndex = i;
273 break;
274 }
275 i++;
276 }
277 curChild = curChild.nextSibling;
278 if (!curChild && optgroup) {
279 curChild = optgroup.nextSibling;
280 optgroup = null;
281 }
282 }
283 }
284
285 fromEl.selectedIndex = selectedIndex;
286 }
287 }
288};
289
290var ELEMENT_NODE = 1;
291var DOCUMENT_FRAGMENT_NODE$1 = 11;
292var TEXT_NODE = 3;
293var COMMENT_NODE = 8;
294
295function noop() {}
296
297function defaultGetNodeKey(node) {
298 if (node) {
299 return (node.getAttribute && node.getAttribute('id')) || node.id;
300 }
301}
302
303function morphdomFactory(morphAttrs) {
304
305 return function morphdom(fromNode, toNode, options) {
306 if (!options) {
307 options = {};
308 }
309
310 if (typeof toNode === 'string') {
311 if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML' || fromNode.nodeName === 'BODY') {
312 var toNodeHtml = toNode;
313 toNode = doc.createElement('html');
314 toNode.innerHTML = toNodeHtml;
315 } else {
316 toNode = toElement(toNode);
317 }
318 } else if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
319 toNode = toNode.firstElementChild;
320 }
321
322 var getNodeKey = options.getNodeKey || defaultGetNodeKey;
323 var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;
324 var onNodeAdded = options.onNodeAdded || noop;
325 var onBeforeElUpdated = options.onBeforeElUpdated || noop;
326 var onElUpdated = options.onElUpdated || noop;
327 var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;
328 var onNodeDiscarded = options.onNodeDiscarded || noop;
329 var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;
330 var skipFromChildren = options.skipFromChildren || noop;
331 var addChild = options.addChild || function(parent, child){ return parent.appendChild(child); };
332 var childrenOnly = options.childrenOnly === true;
333
334 // This object is used as a lookup to quickly find all keyed elements in the original DOM tree.
335 var fromNodesLookup = Object.create(null);
336 var keyedRemovalList = [];
337
338 function addKeyedRemoval(key) {
339 keyedRemovalList.push(key);
340 }
341
342 function walkDiscardedChildNodes(node, skipKeyedNodes) {
343 if (node.nodeType === ELEMENT_NODE) {
344 var curChild = node.firstChild;
345 while (curChild) {
346
347 var key = undefined;
348
349 if (skipKeyedNodes && (key = getNodeKey(curChild))) {
350 // If we are skipping keyed nodes then we add the key
351 // to a list so that it can be handled at the very end.
352 addKeyedRemoval(key);
353 } else {
354 // Only report the node as discarded if it is not keyed. We do this because
355 // at the end we loop through all keyed elements that were unmatched
356 // and then discard them in one final pass.
357 onNodeDiscarded(curChild);
358 if (curChild.firstChild) {
359 walkDiscardedChildNodes(curChild, skipKeyedNodes);
360 }
361 }
362
363 curChild = curChild.nextSibling;
364 }
365 }
366 }
367
368 /**
369 * Removes a DOM node out of the original DOM
370 *
371 * @param {Node} node The node to remove
372 * @param {Node} parentNode The nodes parent
373 * @param {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded.
374 * @return {undefined}
375 */
376 function removeNode(node, parentNode, skipKeyedNodes) {
377 if (onBeforeNodeDiscarded(node) === false) {
378 return;
379 }
380
381 if (parentNode) {
382 parentNode.removeChild(node);
383 }
384
385 onNodeDiscarded(node);
386 walkDiscardedChildNodes(node, skipKeyedNodes);
387 }
388
389 // // TreeWalker implementation is no faster, but keeping this around in case this changes in the future
390 // function indexTree(root) {
391 // var treeWalker = document.createTreeWalker(
392 // root,
393 // NodeFilter.SHOW_ELEMENT);
394 //
395 // var el;
396 // while((el = treeWalker.nextNode())) {
397 // var key = getNodeKey(el);
398 // if (key) {
399 // fromNodesLookup[key] = el;
400 // }
401 // }
402 // }
403
404 // // NodeIterator implementation is no faster, but keeping this around in case this changes in the future
405 //
406 // function indexTree(node) {
407 // var nodeIterator = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT);
408 // var el;
409 // while((el = nodeIterator.nextNode())) {
410 // var key = getNodeKey(el);
411 // if (key) {
412 // fromNodesLookup[key] = el;
413 // }
414 // }
415 // }
416
417 function indexTree(node) {
418 if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
419 var curChild = node.firstChild;
420 while (curChild) {
421 var key = getNodeKey(curChild);
422 if (key) {
423 fromNodesLookup[key] = curChild;
424 }
425
426 // Walk recursively
427 indexTree(curChild);
428
429 curChild = curChild.nextSibling;
430 }
431 }
432 }
433
434 indexTree(fromNode);
435
436 function handleNodeAdded(el) {
437 onNodeAdded(el);
438
439 var curChild = el.firstChild;
440 while (curChild) {
441 var nextSibling = curChild.nextSibling;
442
443 var key = getNodeKey(curChild);
444 if (key) {
445 var unmatchedFromEl = fromNodesLookup[key];
446 // if we find a duplicate #id node in cache, replace `el` with cache value
447 // and morph it to the child node.
448 if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {
449 curChild.parentNode.replaceChild(unmatchedFromEl, curChild);
450 morphEl(unmatchedFromEl, curChild);
451 } else {
452 handleNodeAdded(curChild);
453 }
454 } else {
455 // recursively call for curChild and it's children to see if we find something in
456 // fromNodesLookup
457 handleNodeAdded(curChild);
458 }
459
460 curChild = nextSibling;
461 }
462 }
463
464 function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {
465 // We have processed all of the "to nodes". If curFromNodeChild is
466 // non-null then we still have some from nodes left over that need
467 // to be removed
468 while (curFromNodeChild) {
469 var fromNextSibling = curFromNodeChild.nextSibling;
470 if ((curFromNodeKey = getNodeKey(curFromNodeChild))) {
471 // Since the node is keyed it might be matched up later so we defer
472 // the actual removal to later
473 addKeyedRemoval(curFromNodeKey);
474 } else {
475 // NOTE: we skip nested keyed nodes from being removed since there is
476 // still a chance they will be matched up later
477 removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
478 }
479 curFromNodeChild = fromNextSibling;
480 }
481 }
482
483 function morphEl(fromEl, toEl, childrenOnly) {
484 var toElKey = getNodeKey(toEl);
485
486 if (toElKey) {
487 // If an element with an ID is being morphed then it will be in the final
488 // DOM so clear it out of the saved elements collection
489 delete fromNodesLookup[toElKey];
490 }
491
492 if (!childrenOnly) {
493 // optional
494 var beforeUpdateResult = onBeforeElUpdated(fromEl, toEl);
495 if (beforeUpdateResult === false) {
496 return;
497 } else if (beforeUpdateResult instanceof HTMLElement) {
498 fromEl = beforeUpdateResult;
499 // reindex the new fromEl in case it's not in the same
500 // tree as the original fromEl
501 // (Phoenix LiveView sometimes returns a cloned tree,
502 // but keyed lookups would still point to the original tree)
503 indexTree(fromEl);
504 }
505
506 // update attributes on original DOM element first
507 morphAttrs(fromEl, toEl);
508 // optional
509 onElUpdated(fromEl);
510
511 if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {
512 return;
513 }
514 }
515
516 if (fromEl.nodeName !== 'TEXTAREA') {
517 morphChildren(fromEl, toEl);
518 } else {
519 specialElHandlers.TEXTAREA(fromEl, toEl);
520 }
521 }
522
523 function morphChildren(fromEl, toEl) {
524 var skipFrom = skipFromChildren(fromEl, toEl);
525 var curToNodeChild = toEl.firstChild;
526 var curFromNodeChild = fromEl.firstChild;
527 var curToNodeKey;
528 var curFromNodeKey;
529
530 var fromNextSibling;
531 var toNextSibling;
532 var matchingFromEl;
533
534 // walk the children
535 outer: while (curToNodeChild) {
536 toNextSibling = curToNodeChild.nextSibling;
537 curToNodeKey = getNodeKey(curToNodeChild);
538
539 // walk the fromNode children all the way through
540 while (!skipFrom && curFromNodeChild) {
541 fromNextSibling = curFromNodeChild.nextSibling;
542
543 if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {
544 curToNodeChild = toNextSibling;
545 curFromNodeChild = fromNextSibling;
546 continue outer;
547 }
548
549 curFromNodeKey = getNodeKey(curFromNodeChild);
550
551 var curFromNodeType = curFromNodeChild.nodeType;
552
553 // this means if the curFromNodeChild doesnt have a match with the curToNodeChild
554 var isCompatible = undefined;
555
556 if (curFromNodeType === curToNodeChild.nodeType) {
557 if (curFromNodeType === ELEMENT_NODE) {
558 // Both nodes being compared are Element nodes
559
560 if (curToNodeKey) {
561 // The target node has a key so we want to match it up with the correct element
562 // in the original DOM tree
563 if (curToNodeKey !== curFromNodeKey) {
564 // The current element in the original DOM tree does not have a matching key so
565 // let's check our lookup to see if there is a matching element in the original
566 // DOM tree
567 if ((matchingFromEl = fromNodesLookup[curToNodeKey])) {
568 if (fromNextSibling === matchingFromEl) {
569 // Special case for single element removals. To avoid removing the original
570 // DOM node out of the tree (since that can break CSS transitions, etc.),
571 // we will instead discard the current node and wait until the next
572 // iteration to properly match up the keyed target element with its matching
573 // element in the original tree
574 isCompatible = false;
575 } else {
576 // We found a matching keyed element somewhere in the original DOM tree.
577 // Let's move the original DOM node into the current position and morph
578 // it.
579
580 // NOTE: We use insertBefore instead of replaceChild because we want to go through
581 // the `removeNode()` function for the node that is being discarded so that
582 // all lifecycle hooks are correctly invoked
583 fromEl.insertBefore(matchingFromEl, curFromNodeChild);
584
585 // fromNextSibling = curFromNodeChild.nextSibling;
586
587 if (curFromNodeKey) {
588 // Since the node is keyed it might be matched up later so we defer
589 // the actual removal to later
590 addKeyedRemoval(curFromNodeKey);
591 } else {
592 // NOTE: we skip nested keyed nodes from being removed since there is
593 // still a chance they will be matched up later
594 removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
595 }
596
597 curFromNodeChild = matchingFromEl;
598 curFromNodeKey = getNodeKey(curFromNodeChild);
599 }
600 } else {
601 // The nodes are not compatible since the "to" node has a key and there
602 // is no matching keyed node in the source tree
603 isCompatible = false;
604 }
605 }
606 } else if (curFromNodeKey) {
607 // The original has a key
608 isCompatible = false;
609 }
610
611 isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);
612 if (isCompatible) {
613 // We found compatible DOM elements so transform
614 // the current "from" node to match the current
615 // target DOM node.
616 // MORPH
617 morphEl(curFromNodeChild, curToNodeChild);
618 }
619
620 } else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {
621 // Both nodes being compared are Text or Comment nodes
622 isCompatible = true;
623 // Simply update nodeValue on the original node to
624 // change the text value
625 if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {
626 curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
627 }
628
629 }
630 }
631
632 if (isCompatible) {
633 // Advance both the "to" child and the "from" child since we found a match
634 // Nothing else to do as we already recursively called morphChildren above
635 curToNodeChild = toNextSibling;
636 curFromNodeChild = fromNextSibling;
637 continue outer;
638 }
639
640 // No compatible match so remove the old node from the DOM and continue trying to find a
641 // match in the original DOM. However, we only do this if the from node is not keyed
642 // since it is possible that a keyed node might match up with a node somewhere else in the
643 // target tree and we don't want to discard it just yet since it still might find a
644 // home in the final DOM tree. After everything is done we will remove any keyed nodes
645 // that didn't find a home
646 if (curFromNodeKey) {
647 // Since the node is keyed it might be matched up later so we defer
648 // the actual removal to later
649 addKeyedRemoval(curFromNodeKey);
650 } else {
651 // NOTE: we skip nested keyed nodes from being removed since there is
652 // still a chance they will be matched up later
653 removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
654 }
655
656 curFromNodeChild = fromNextSibling;
657 } // END: while(curFromNodeChild) {}
658
659 // If we got this far then we did not find a candidate match for
660 // our "to node" and we exhausted all of the children "from"
661 // nodes. Therefore, we will just append the current "to" node
662 // to the end
663 if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {
664 // MORPH
665 if(!skipFrom){ addChild(fromEl, matchingFromEl); }
666 morphEl(matchingFromEl, curToNodeChild);
667 } else {
668 var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);
669 if (onBeforeNodeAddedResult !== false) {
670 if (onBeforeNodeAddedResult) {
671 curToNodeChild = onBeforeNodeAddedResult;
672 }
673
674 if (curToNodeChild.actualize) {
675 curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);
676 }
677 addChild(fromEl, curToNodeChild);
678 handleNodeAdded(curToNodeChild);
679 }
680 }
681
682 curToNodeChild = toNextSibling;
683 curFromNodeChild = fromNextSibling;
684 }
685
686 cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);
687
688 var specialElHandler = specialElHandlers[fromEl.nodeName];
689 if (specialElHandler) {
690 specialElHandler(fromEl, toEl);
691 }
692 } // END: morphChildren(...)
693
694 var morphedNode = fromNode;
695 var morphedNodeType = morphedNode.nodeType;
696 var toNodeType = toNode.nodeType;
697
698 if (!childrenOnly) {
699 // Handle the case where we are given two DOM nodes that are not
700 // compatible (e.g. <div> --> <span> or <div> --> TEXT)
701 if (morphedNodeType === ELEMENT_NODE) {
702 if (toNodeType === ELEMENT_NODE) {
703 if (!compareNodeNames(fromNode, toNode)) {
704 onNodeDiscarded(fromNode);
705 morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));
706 }
707 } else {
708 // Going from an element node to a text node
709 morphedNode = toNode;
710 }
711 } else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node
712 if (toNodeType === morphedNodeType) {
713 if (morphedNode.nodeValue !== toNode.nodeValue) {
714 morphedNode.nodeValue = toNode.nodeValue;
715 }
716
717 return morphedNode;
718 } else {
719 // Text node to something else
720 morphedNode = toNode;
721 }
722 }
723 }
724
725 if (morphedNode === toNode) {
726 // The "to node" was not compatible with the "from node" so we had to
727 // toss out the "from node" and use the "to node"
728 onNodeDiscarded(fromNode);
729 } else {
730 if (toNode.isSameNode && toNode.isSameNode(morphedNode)) {
731 return;
732 }
733
734 morphEl(morphedNode, toNode, childrenOnly);
735
736 // We now need to loop over any keyed nodes that might need to be
737 // removed. We only do the removal if we know that the keyed node
738 // never found a match. When a keyed node is matched up we remove
739 // it out of fromNodesLookup and we use fromNodesLookup to determine
740 // if a keyed node has been matched up or not
741 if (keyedRemovalList) {
742 for (var i=0, len=keyedRemovalList.length; i<len; i++) {
743 var elToRemove = fromNodesLookup[keyedRemovalList[i]];
744 if (elToRemove) {
745 removeNode(elToRemove, elToRemove.parentNode, false);
746 }
747 }
748 }
749 }
750
751 if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {
752 if (morphedNode.actualize) {
753 morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);
754 }
755 // If we had to swap out the from node with a new node because the old
756 // node was not compatible with the target node then we need to
757 // replace the old DOM node in the original DOM tree. This is only
758 // possible if the original DOM node was part of a DOM tree which
759 // we know is the case if it has a parent node.
760 fromNode.parentNode.replaceChild(morphedNode, fromNode);
761 }
762
763 return morphedNode;
764 };
765}
766
767var morphdom = morphdomFactory(morphAttrs);
768
769export default morphdom;
public/scripts/extensions/quick-reply/lib/morphdom.LICENSE.txt+21 -0
@@ -0,0 +1,21 @@
1The MIT License (MIT)
2
3Copyright (c) Patrick Steele-Idem <pnidem@gmail.com> (psteeleidem.com)
4
5Permission is hereby granted, free of charge, to any person obtaining a copy
6of this software and associated documentation files (the "Software"), to deal
7in the Software without restriction, including without limitation the rights
8to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9copies of the Software, and to permit persons to whom the Software is
10furnished to do so, subject to the following conditions:
11
12The above copyright notice and this permission notice shall be included in
13all copies or substantial portions of the Software.
14
15THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21THE SOFTWARE.
\ No newline at end of file21 \ No newline at end of file
public/scripts/extensions/quick-reply/src/QuickReply.js+1277 -87
@@ -1,10 +1,17 @@
1import { POPUP_TYPE, Popup } from '../../../popup.js';1import { POPUP_RESULT, POPUP_TYPE, Popup } from '../../../popup.js';
2import { setSlashCommandAutoComplete } from '../../../slash-commands.js';2import { setSlashCommandAutoComplete } from '../../../slash-commands.js';
3import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';3import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
4import { SlashCommandBreakPoint } from '../../../slash-commands/SlashCommandBreakPoint.js';
5import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js';
6import { SlashCommandClosureResult } from '../../../slash-commands/SlashCommandClosureResult.js';
7import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js';
8import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecutor.js';
9import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
4import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';10import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
5import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';11import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
6import { debounce, getSortableDelay } from '../../../utils.js';12import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';
7import { log, warn } from '../index.js';13import { log, quickReplyApi, warn } from '../index.js';
14import morphdom from '../lib/morphdom-esm.js';
8import { QuickReplyContextLink } from './QuickReplyContextLink.js';15import { QuickReplyContextLink } from './QuickReplyContextLink.js';
9import { QuickReplySet } from './QuickReplySet.js';16import { QuickReplySet } from './QuickReplySet.js';
10import { ContextMenu } from './ui/ctx/ContextMenu.js';17import { ContextMenu } from './ui/ctx/ContextMenu.js';
@@ -21,48 +28,61 @@ export class QuickReply {
2128
2229
2330
24 /**@type {Number}*/ id;31 /**@type {number}*/ id;
25 /**@type {String}*/ label = '';32 /**@type {string}*/ icon;
26 /**@type {String}*/ title = '';33 /**@type {string}*/ label = '';
27 /**@type {String}*/ message = '';34 /**@type {boolean}*/ showLabel = false;
35 /**@type {string}*/ title = '';
36 /**@type {string}*/ message = '';
2837
29 /**@type {QuickReplyContextLink[]}*/ contextList;38 /**@type {QuickReplyContextLink[]}*/ contextList;
3039
31 /**@type {Boolean}*/ preventAutoExecute = true;40 /**@type {boolean}*/ preventAutoExecute = true;
32 /**@type {Boolean}*/ isHidden = false;41 /**@type {boolean}*/ isHidden = false;
33 /**@type {Boolean}*/ executeOnStartup = false;42 /**@type {boolean}*/ executeOnStartup = false;
34 /**@type {Boolean}*/ executeOnUser = false;43 /**@type {boolean}*/ executeOnUser = false;
35 /**@type {Boolean}*/ executeOnAi = false;44 /**@type {boolean}*/ executeOnAi = false;
36 /**@type {Boolean}*/ executeOnChatChange = false;45 /**@type {boolean}*/ executeOnChatChange = false;
37 /**@type {Boolean}*/ executeOnGroupMemberDraft = false;46 /**@type {boolean}*/ executeOnGroupMemberDraft = false;
38 /**@type {String}*/ automationId = '';47 /**@type {string}*/ automationId = '';
3948
40 /**@type {Function}*/ onExecute;49 /**@type {function}*/ onExecute;
41 /**@type {Function}*/ onDelete;50 /**@type {(qr:QuickReply)=>AsyncGenerator<SlashCommandClosureResult|{closure:SlashCommandClosure, executor:SlashCommandExecutor|SlashCommandClosureResult}, SlashCommandClosureResult, boolean>}*/ onDebug;
42 /**@type {Function}*/ onUpdate;51 /**@type {function}*/ onDelete;
52 /**@type {function}*/ onUpdate;
53 /**@type {function}*/ onInsertBefore;
54 /**@type {function}*/ onTransfer;
4355
4456
45 /**@type {HTMLElement}*/ dom;57 /**@type {HTMLElement}*/ dom;
58 /**@type {HTMLElement}*/ domIcon;
46 /**@type {HTMLElement}*/ domLabel;59 /**@type {HTMLElement}*/ domLabel;
47 /**@type {HTMLElement}*/ settingsDom;60 /**@type {HTMLElement}*/ settingsDom;
61 /**@type {HTMLElement}*/ settingsDomIcon;
48 /**@type {HTMLInputElement}*/ settingsDomLabel;62 /**@type {HTMLInputElement}*/ settingsDomLabel;
49 /**@type {HTMLTextAreaElement}*/ settingsDomMessage;63 /**@type {HTMLTextAreaElement}*/ settingsDomMessage;
5064
51 /**@type {Popup}*/ editorPopup;65 /**@type {Popup}*/ editorPopup;
66 /**@type {HTMLElement}*/ editorDom;
5267
68 /**@type {HTMLTextAreaElement}*/ editorMessage;
69 /**@type {HTMLTextAreaElement}*/ editorMessageLabel;
70 /**@type {HTMLElement}*/ editorSyntax;
53 /**@type {HTMLElement}*/ editorExecuteBtn;71 /**@type {HTMLElement}*/ editorExecuteBtn;
54 /**@type {HTMLElement}*/ editorExecuteBtnPause;72 /**@type {HTMLElement}*/ editorExecuteBtnPause;
55 /**@type {HTMLElement}*/ editorExecuteBtnStop;73 /**@type {HTMLElement}*/ editorExecuteBtnStop;
56 /**@type {HTMLElement}*/ editorExecuteProgress;74 /**@type {HTMLElement}*/ editorExecuteProgress;
57 /**@type {HTMLElement}*/ editorExecuteErrors;75 /**@type {HTMLElement}*/ editorExecuteErrors;
58 /**@type {HTMLElement}*/ editorExecuteResult;76 /**@type {HTMLElement}*/ editorExecuteResult;
59 /**@type {HTMLInputElement}*/ editorExecuteHide;77 /**@type {HTMLElement}*/ editorDebugState;
60 /**@type {Promise}*/ editorExecutePromise;78 /**@type {Promise}*/ editorExecutePromise;
79 /**@type {boolean}*/ isExecuting;
61 /**@type {SlashCommandAbortController}*/ abortController;80 /**@type {SlashCommandAbortController}*/ abortController;
81 /**@type {SlashCommandDebugController}*/ debugController;
6282
6383
64 get hasContext() {84 get hasContext() {
65 return this.contextList && this.contextList.length > 0;85 return this.contextList && this.contextList.filter(it => it.set).length > 0;
66 }86 }
6787
6888
@@ -75,6 +95,14 @@ export class QuickReply {
75 updateRender() {95 updateRender() {
76 if (!this.dom) return;96 if (!this.dom) return;
77 this.dom.title = this.title || this.message;97 this.dom.title = this.title || this.message;
98 if (this.icon) {
99 this.domIcon.classList.remove('qr--hidden');
100 if (this.showLabel) this.domLabel.classList.remove('qr--hidden');
101 else this.domLabel.classList.add('qr--hidden');
102 } else {
103 this.domIcon.classList.add('qr--hidden');
104 this.domLabel.classList.remove('qr--hidden');
105 }
78 this.domLabel.textContent = this.label;106 this.domLabel.textContent = this.label;
79 this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx');107 this.dom.classList[this.hasContext ? 'add' : 'remove']('qr--hasCtx');
80 }108 }
@@ -105,9 +133,18 @@ export class QuickReply {
105 }133 }
106 this.execute();134 this.execute();
107 });135 });
136 const icon = document.createElement('div'); {
137 this.domIcon = icon;
138 icon.classList.add('qr--button-icon');
139 icon.classList.add('fa-solid');
140 if (!this.icon) icon.classList.add('qr--hidden');
141 else icon.classList.add(this.icon);
142 root.append(icon);
143 }
108 const lbl = document.createElement('div'); {144 const lbl = document.createElement('div'); {
109 this.domLabel = lbl;145 this.domLabel = lbl;
110 lbl.classList.add('qr--button-label');146 lbl.classList.add('qr--button-label');
147 if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden');
111 lbl.textContent = this.label;148 lbl.textContent = this.label;
112 root.append(lbl);149 root.append(lbl);
113 }150 }
@@ -138,36 +175,115 @@ export class QuickReply {
138 item.classList.add('qr--set-item');175 item.classList.add('qr--set-item');
139 item.setAttribute('data-order', String(idx));176 item.setAttribute('data-order', String(idx));
140 item.setAttribute('data-id', String(this.id));177 item.setAttribute('data-id', String(this.id));
141 const drag = document.createElement('div'); {178 const adder = document.createElement('div'); {
142 drag.classList.add('drag-handle');179 adder.classList.add('qr--set-itemAdder');
143 drag.classList.add('ui-sortable-handle');180 const actions = document.createElement('div'); {
144 drag.textContent = '☰';181 actions.classList.add('qr--actions');
145 item.append(drag);182 const addNew = document.createElement('div'); {
146 }183 addNew.classList.add('qr--action');
147 const lblContainer = document.createElement('div'); {184 addNew.classList.add('qr--add');
148 lblContainer.classList.add('qr--set-itemLabelContainer');185 addNew.classList.add('menu_button');
149 const lbl = document.createElement('input'); {186 addNew.classList.add('menu_button_icon');
150 this.settingsDomLabel = lbl;187 addNew.classList.add('fa-solid');
151 lbl.classList.add('qr--set-itemLabel');188 addNew.classList.add('fa-plus');
152 lbl.classList.add('text_pole');189 addNew.title = 'Add quick reply';
153 lbl.value = this.label;190 addNew.addEventListener('click', ()=>this.onInsertBefore());
154 lbl.addEventListener('input', ()=>this.updateLabel(lbl.value));191 actions.append(addNew);
155 lblContainer.append(lbl);192 }
193 const paste = document.createElement('div'); {
194 paste.classList.add('qr--action');
195 paste.classList.add('qr--paste');
196 paste.classList.add('menu_button');
197 paste.classList.add('menu_button_icon');
198 paste.classList.add('fa-solid');
199 paste.classList.add('fa-paste');
200 paste.title = 'Add quick reply from clipboard';
201 paste.addEventListener('click', async()=>{
202 const text = await navigator.clipboard.readText();
203 this.onInsertBefore(text);
204 });
205 actions.append(paste);
206 }
207 const importFile = document.createElement('div'); {
208 importFile.classList.add('qr--action');
209 importFile.classList.add('qr--importFile');
210 importFile.classList.add('menu_button');
211 importFile.classList.add('menu_button_icon');
212 importFile.classList.add('fa-solid');
213 importFile.classList.add('fa-file-import');
214 importFile.title = 'Add quick reply from JSON file';
215 importFile.addEventListener('click', async()=>{
216 const inp = document.createElement('input'); {
217 inp.type = 'file';
218 inp.accept = '.json';
219 inp.addEventListener('change', async()=>{
220 if (inp.files.length > 0) {
221 for (const file of inp.files) {
222 const text = await file.text();
223 this.onInsertBefore(text);
224 }
225 }
226 });
227 inp.click();
228 }
229 });
230 actions.append(importFile);
231 }
232 adder.append(actions);
233 }
234 item.append(adder);
235 }
236 const itemContent = document.createElement('div'); {
237 itemContent.classList.add('qr--content');
238 const drag = document.createElement('div'); {
239 drag.classList.add('drag-handle');
240 drag.classList.add('ui-sortable-handle');
241 drag.textContent = '☰';
242 itemContent.append(drag);
243 }
244 const lblContainer = document.createElement('div'); {
245 lblContainer.classList.add('qr--set-itemLabelContainer');
246 const icon = document.createElement('div'); {
247 this.settingsDomIcon = icon;
248 icon.title = 'Click to change icon';
249 icon.classList.add('qr--set-itemIcon');
250 icon.classList.add('menu_button');
251 icon.classList.add('fa-fw');
252 if (this.icon) {
253 icon.classList.add('fa-solid');
254 icon.classList.add(this.icon);
255 }
256 icon.addEventListener('click', async()=>{
257 let value = await showFontAwesomePicker();
258 this.updateIcon(value);
259 });
260 lblContainer.append(icon);
261 }
262 const lbl = document.createElement('input'); {
263 this.settingsDomLabel = lbl;
264 lbl.classList.add('qr--set-itemLabel');
265 lbl.classList.add('text_pole');
266 lbl.value = this.label;
267 lbl.addEventListener('input', ()=>this.updateLabel(lbl.value));
268 lblContainer.append(lbl);
269 }
270 itemContent.append(lblContainer);
156 }271 }
157 item.append(lblContainer);272 item.append(itemContent);
158 }273 }
159 const optContainer = document.createElement('div'); {274 const optContainer = document.createElement('div'); {
160 optContainer.classList.add('qr--set-optionsContainer');275 optContainer.classList.add('qr--set-optionsContainer');
161 const opt = document.createElement('div'); {276 const opt = document.createElement('div'); {
162 opt.classList.add('qr--action');277 opt.classList.add('qr--action');
163 opt.classList.add('menu_button');278 opt.classList.add('menu_button');
279 opt.classList.add('fa-fw');
164 opt.classList.add('fa-solid');280 opt.classList.add('fa-solid');
165 opt.textContent = '⁝';281 opt.textContent = '⁝';
166 opt.title = 'Additional options:\n - large editor\n - context menu\n - auto-execution\n - tooltip';282 opt.title = 'Additional options:\n - large editor\n - context menu\n - auto-execution\n - tooltip';
167 opt.addEventListener('click', ()=>this.showEditor());283 opt.addEventListener('click', ()=>this.showEditor());
168 optContainer.append(opt);284 optContainer.append(opt);
169 }285 }
170 item.append(optContainer);286 itemContent.append(optContainer);
171 }287 }
172 const mes = document.createElement('textarea'); {288 const mes = document.createElement('textarea'); {
173 this.settingsDomMessage = mes;289 this.settingsDomMessage = mes;
@@ -176,22 +292,89 @@ export class QuickReply {
176 mes.value = this.message;292 mes.value = this.message;
177 //HACK need to use jQuery to catch the triggered event from the expanded editor293 //HACK need to use jQuery to catch the triggered event from the expanded editor
178 $(mes).on('input', ()=>this.updateMessage(mes.value));294 $(mes).on('input', ()=>this.updateMessage(mes.value));
179 item.append(mes);295 itemContent.append(mes);
180 }296 }
181 const actions = document.createElement('div'); {297 const actions = document.createElement('div'); {
182 actions.classList.add('qr--actions');298 actions.classList.add('qr--actions');
299 const move = document.createElement('div'); {
300 move.classList.add('qr--action');
301 move.classList.add('menu_button');
302 move.classList.add('fa-fw');
303 move.classList.add('fa-solid');
304 move.classList.add('fa-truck-arrow-right');
305 move.title = 'Move quick reply to other set';
306 move.addEventListener('click', ()=>this.onTransfer(this));
307 actions.append(move);
308 }
309 const copy = document.createElement('div'); {
310 copy.classList.add('qr--action');
311 copy.classList.add('menu_button');
312 copy.classList.add('fa-fw');
313 copy.classList.add('fa-solid');
314 copy.classList.add('fa-copy');
315 copy.title = 'Copy quick reply to clipboard';
316 copy.addEventListener('click', async()=>{
317 await navigator.clipboard.writeText(JSON.stringify(this));
318 copy.classList.add('qr--success');
319 await delay(3010);
320 copy.classList.remove('qr--success');
321 });
322 actions.append(copy);
323 }
324 const cut = document.createElement('div'); {
325 cut.classList.add('qr--action');
326 cut.classList.add('menu_button');
327 cut.classList.add('fa-fw');
328 cut.classList.add('fa-solid');
329 cut.classList.add('fa-cut');
330 cut.title = 'Cut quick reply to clipboard (copy and remove)';
331 cut.addEventListener('click', async()=>{
332 await navigator.clipboard.writeText(JSON.stringify(this));
333 this.delete();
334 });
335 actions.append(cut);
336 }
337 const exp = document.createElement('div'); {
338 exp.classList.add('qr--action');
339 exp.classList.add('menu_button');
340 exp.classList.add('fa-fw');
341 exp.classList.add('fa-solid');
342 exp.classList.add('fa-file-export');
343 exp.title = 'Export quick reply as file';
344 exp.addEventListener('click', ()=>{
345 const blob = new Blob([JSON.stringify(this)], { type:'text' });
346 const url = URL.createObjectURL(blob);
347 const a = document.createElement('a'); {
348 a.href = url;
349 a.download = `${this.label}.qr.json`;
350 a.click();
351 }
352 });
353 actions.append(exp);
354 }
183 const del = document.createElement('div'); {355 const del = document.createElement('div'); {
184 del.classList.add('qr--action');356 del.classList.add('qr--action');
185 del.classList.add('menu_button');357 del.classList.add('menu_button');
186 del.classList.add('menu_button_icon');358 del.classList.add('fa-fw');
187 del.classList.add('fa-solid');359 del.classList.add('fa-solid');
188 del.classList.add('fa-trash-can');360 del.classList.add('fa-trash-can');
189 del.classList.add('redWarningBG');361 del.classList.add('redWarningBG');
190 del.title = 'Remove quick reply';362 del.title = 'Remove Quick Reply\n---\nShit+Click to skip confirmation';
191 del.addEventListener('click', ()=>this.delete());363 del.addEventListener('click', async(evt)=>{
364 if (!evt.shiftKey) {
365 const result = await Popup.show.confirm(
366 'Remove Quick Reply',
367 'Are you sure you want to remove this Quick Reply?',
368 );
369 if (result != POPUP_RESULT.AFFIRMATIVE) {
370 return;
371 }
372 }
373 this.delete();
374 });
192 actions.append(del);375 actions.append(del);
193 }376 }
194 item.append(actions);377 itemContent.append(actions);
195 }378 }
196 }379 }
197 }380 }
@@ -208,22 +391,156 @@ export class QuickReply {
208 /**@type {HTMLElement} */391 /**@type {HTMLElement} */
209 // @ts-ignore392 // @ts-ignore
210 const dom = this.template.cloneNode(true);393 const dom = this.template.cloneNode(true);
394 this.editorDom = dom;
211 this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 });395 this.editorPopup = new Popup(dom, POPUP_TYPE.TEXT, undefined, { okButton: 'OK', wide: true, large: true, rows: 1 });
212 const popupResult = this.editorPopup.show();396 const popupResult = this.editorPopup.show();
213397
214 // basics398 // basics
399 /**@type {HTMLElement}*/
400 const icon = dom.querySelector('#qr--modal-icon');
401 if (this.icon) {
402 icon.classList.add('fa-solid');
403 icon.classList.add(this.icon);
404 }
405 else {
406 icon.textContent = '…';
407 }
408 icon.addEventListener('click', async()=>{
409 let value = await showFontAwesomePicker();
410 if (value === null) return;
411 if (this.icon) icon.classList.remove(this.icon);
412 if (value == '') {
413 icon.classList.remove('fa-solid');
414 icon.textContent = '…';
415 } else {
416 icon.textContent = '';
417 icon.classList.add('fa-solid');
418 icon.classList.add(value);
419 }
420 this.updateIcon(value);
421 });
422 /**@type {HTMLInputElement}*/
423 const showLabel = dom.querySelector('#qr--modal-showLabel');
424 showLabel.checked = this.showLabel;
425 showLabel.addEventListener('click', ()=>{
426 this.updateShowLabel(showLabel.checked);
427 });
215 /**@type {HTMLInputElement}*/428 /**@type {HTMLInputElement}*/
216 const label = dom.querySelector('#qr--modal-label');429 const label = dom.querySelector('#qr--modal-label');
217 label.value = this.label;430 label.value = this.label;
218 label.addEventListener('input', ()=>{431 label.addEventListener('input', ()=>{
219 this.updateLabel(label.value);432 this.updateLabel(label.value);
220 });433 });
434 let switcherList;
435 dom.querySelector('#qr--modal-switcher').addEventListener('click', (evt)=>{
436 if (switcherList) {
437 switcherList.remove();
438 switcherList = null;
439 return;
440 }
441 const list = document.createElement('ul'); {
442 switcherList = list;
443 list.classList.add('qr--modal-switcherList');
444 const makeList = (qrs)=>{
445 const setItem = document.createElement('li'); {
446 setItem.classList.add('qr--modal-switcherItem');
447 setItem.addEventListener('click', ()=>{
448 list.innerHTML = '';
449 for (const qrs of quickReplyApi.listSets()) {
450 const item = document.createElement('li'); {
451 item.classList.add('qr--modal-switcherItem');
452 item.addEventListener('click', ()=>{
453 list.innerHTML = '';
454 makeList(quickReplyApi.getSetByName(qrs));
455 });
456 const lbl = document.createElement('div'); {
457 lbl.classList.add('qr--label');
458 lbl.textContent = qrs;
459 item.append(lbl);
460 }
461 list.append(item);
462 }
463 }
464 });
465 const lbl = document.createElement('div'); {
466 lbl.classList.add('qr--label');
467 const icon = document.createElement('i'); {
468 icon.classList.add('fa-solid');
469 icon.classList.add('fa-arrow-alt-circle-right');
470 icon.classList.add('menu_button');
471 lbl.append(icon);
472 }
473 const text = document.createElement('span'); {
474 text.textContent = 'Switch QR Sets...';
475 lbl.append(text);
476 }
477 setItem.append(lbl);
478 }
479 list.append(setItem);
480 }
481 const addItem = document.createElement('li'); {
482 addItem.classList.add('qr--modal-switcherItem');
483 addItem.addEventListener('click', ()=>{
484 const qr = quickReplyApi.getSetByQr(this).addQuickReply();
485 this.editorPopup.completeAffirmative();
486 qr.showEditor();
487 });
488 const lbl = document.createElement('div'); {
489 lbl.classList.add('qr--label');
490 const icon = document.createElement('i'); {
491 icon.classList.add('fa-solid');
492 icon.classList.add('fa-plus');
493 icon.classList.add('menu_button');
494 lbl.append(icon);
495 }
496 const text = document.createElement('span'); {
497 text.textContent = 'Add QR';
498 lbl.append(text);
499 }
500 addItem.append(lbl);
501 }
502 list.append(addItem);
503 }
504 for (const qr of qrs.qrList.toSorted((a,b)=>a.label.toLowerCase().localeCompare(b.label.toLowerCase()))) {
505 const item = document.createElement('li'); {
506 item.classList.add('qr--modal-switcherItem');
507 if (qr == this) item.classList.add('qr--current');
508 else item.addEventListener('click', ()=>{
509 this.editorPopup.completeAffirmative();
510 qr.showEditor();
511 });
512 const lbl = document.createElement('div'); {
513 lbl.classList.add('qr--label');
514 lbl.textContent = qr.label;
515 item.append(lbl);
516 }
517 const id = document.createElement('div'); {
518 id.classList.add('qr--id');
519 id.textContent = qr.id.toString();
520 item.append(id);
521 }
522 const mes = document.createElement('div'); {
523 mes.classList.add('qr--message');
524 mes.textContent = qr.message;
525 item.append(mes);
526 }
527 list.append(item);
528 }
529 }
530 };
531 makeList(quickReplyApi.getSetByQr(this));
532 }
533 label.parentElement.append(list);
534 });
221 /**@type {HTMLInputElement}*/535 /**@type {HTMLInputElement}*/
222 const title = dom.querySelector('#qr--modal-title');536 const title = dom.querySelector('#qr--modal-title');
223 title.value = this.title;537 title.value = this.title;
224 title.addEventListener('input', () => {538 title.addEventListener('input', () => {
225 this.updateTitle(title.value);539 this.updateTitle(title.value);
226 });540 });
541 /**@type {HTMLElement}*/
542 const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
543 this.editorSyntax = messageSyntaxInner;
227 /**@type {HTMLInputElement}*/544 /**@type {HTMLInputElement}*/
228 const wrap = dom.querySelector('#qr--modal-wrap');545 const wrap = dom.querySelector('#qr--modal-wrap');
229 wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false');546 wrap.checked = JSON.parse(localStorage.getItem('qr--wrap') ?? 'false');
@@ -235,9 +552,15 @@ export class QuickReply {
235 if (wrap.checked) {552 if (wrap.checked) {
236 message.style.whiteSpace = 'pre-wrap';553 message.style.whiteSpace = 'pre-wrap';
237 messageSyntaxInner.style.whiteSpace = 'pre-wrap';554 messageSyntaxInner.style.whiteSpace = 'pre-wrap';
555 if (this.clone) {
556 this.clone.style.whiteSpace = 'pre-wrap';
557 }
238 } else {558 } else {
239 message.style.whiteSpace = 'pre';559 message.style.whiteSpace = 'pre';
240 messageSyntaxInner.style.whiteSpace = 'pre';560 messageSyntaxInner.style.whiteSpace = 'pre';
561 if (this.clone) {
562 this.clone.style.whiteSpace = 'pre';
563 }
241 }564 }
242 updateScrollDebounced();565 updateScrollDebounced();
243 };566 };
@@ -261,11 +584,8 @@ export class QuickReply {
261 });584 });
262 };585 };
263 const updateScrollDebounced = updateScroll;586 const updateScrollDebounced = updateScroll;
264 const updateSyntax = ()=>{
265 messageSyntaxInner.innerHTML = hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
266 };
267 const updateSyntaxEnabled = ()=>{587 const updateSyntaxEnabled = ()=>{
268 if (JSON.parse(localStorage.getItem('qr--syntax'))) {588 if (syntax.checked) {
269 dom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');589 dom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');
270 } else {590 } else {
271 dom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');591 dom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');
@@ -296,48 +616,108 @@ export class QuickReply {
296 localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));616 localStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
297 updateSyntaxEnabled();617 updateSyntaxEnabled();
298 });618 });
619 if (navigator.keyboard) {
620 navigator.keyboard.getLayoutMap().then(it=>dom.querySelector('#qr--modal-commentKey').textContent = it.get('Backslash'));
621 } else {
622 dom.querySelector('#qr--modal-commentKey').closest('small').remove();
623 }
624 this.editorMessageLabel = dom.querySelector('label[for="qr--modal-message"]');
299 /**@type {HTMLTextAreaElement}*/625 /**@type {HTMLTextAreaElement}*/
300 const message = dom.querySelector('#qr--modal-message');626 const message = dom.querySelector('#qr--modal-message');
627 this.editorMessage = message;
301 message.value = this.message;628 message.value = this.message;
629 const updateMessageDebounced = debounce((value)=>this.updateMessage(value), 10);
302 message.addEventListener('input', () => {630 message.addEventListener('input', () => {
303 updateSyntax();631 updateMessageDebounced(message.value);
304 this.updateMessage(message.value);
305 updateScrollDebounced();632 updateScrollDebounced();
306 });633 }, { passive:true });
307 setSlashCommandAutoComplete(message, true);634 const getLineStart = ()=>{
308 //TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize635 const start = message.selectionStart;
636 const end = message.selectionEnd;
637 let lineStart;
638 if (start == 0 || message.value[start - 1] == '\n') {
639 // cursor is already at beginning of line
640 // -> keep start
641 lineStart = start;
642 } else {
643 // cursor is at end of line or somewhere in the line
644 // -> find last newline before cursor and start after that
645 lineStart = message.value.lastIndexOf('\n', start - 1) + 1;
646 }
647 return lineStart;
648 };
309 message.addEventListener('keydown', async(evt) => {649 message.addEventListener('keydown', async(evt) => {
650 if (this.isExecuting) return;
310 if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {651 if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
652 // increase indent
311 evt.preventDefault();653 evt.preventDefault();
312 const start = message.selectionStart;654 const start = message.selectionStart;
313 const end = message.selectionEnd;655 const end = message.selectionEnd;
314 if (end - start > 0 && message.value.substring(start, end).includes('\n')) {656 if (end - start > 0 && message.value.substring(start, end).includes('\n')) {
315 const lineStart = message.value.lastIndexOf('\n', start);657 evt.stopImmediatePropagation();
316 const count = message.value.substring(lineStart, end).split('\n').length - 1;658 evt.stopPropagation();
317 message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${message.value.substring(end)}`;659 const lineStart = getLineStart();
318 message.selectionStart = start + 1;660 message.selectionStart = lineStart;
319 message.selectionEnd = end + count;661 const affectedLines = message.value.substring(lineStart, end).split('\n');
320 updateSyntax();662 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
321 } else {663 document.execCommand('insertText', false, `\t${affectedLines.join('\n\t')}`);
322 message.value = `${message.value.substring(0, start)}\t${message.value.substring(end)}`;
323 message.selectionStart = start + 1;664 message.selectionStart = start + 1;
324 message.selectionEnd = end + 1;665 message.selectionEnd = end + affectedLines.length;
325 updateSyntax();666 message.dispatchEvent(new Event('input', { bubbles:true }));
667 } else if (!(ac.isReplaceable && ac.isActive)) {
668 evt.stopImmediatePropagation();
669 evt.stopPropagation();
670 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
671 document.execCommand('insertText', false, '\t');
672 message.dispatchEvent(new Event('input', { bubbles:true }));
326 }673 }
327 } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {674 } else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
675 // decrease indent
328 evt.preventDefault();676 evt.preventDefault();
677 evt.stopImmediatePropagation();
678 evt.stopPropagation();
329 const start = message.selectionStart;679 const start = message.selectionStart;
330 const end = message.selectionEnd;680 const end = message.selectionEnd;
331 const lineStart = message.value.lastIndexOf('\n', start);681 const lineStart = getLineStart();
332 const count = message.value.substring(lineStart, end).split('\n\t').length - 1;682 message.selectionStart = lineStart;
333 message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${message.value.substring(end)}`;683 const affectedLines = message.value.substring(lineStart, end).split('\n');
334 message.selectionStart = start - 1;684 const newText = affectedLines.map(it=>it.replace(/^\t/, '')).join('\n');
335 message.selectionEnd = end - count;685 const delta = affectedLines.join('\n').length - newText.length;
336 updateSyntax();686 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
687 if (delta > 0) {
688 if (newText == '') {
689 document.execCommand('delete', false);
690 } else {
691 document.execCommand('insertText', false, newText);
692 }
693 message.selectionStart = start - (affectedLines[0].startsWith('\t') ? 1 : 0);
694 message.selectionEnd = end - delta;
695 message.dispatchEvent(new Event('input', { bubbles:true }));
696 } else {
697 message.selectionStart = start;
698 }
699 } else if (evt.key == 'Enter' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey && !(ac.isReplaceable && ac.isActive)) {
700 // new line, keep indent
701 const start = message.selectionStart;
702 const end = message.selectionEnd;
703 let lineStart = getLineStart();
704 const indent = /^([^\S\n]*)/.exec(message.value.slice(lineStart))[1] ?? '';
705 if (indent.length) {
706 evt.stopImmediatePropagation();
707 evt.stopPropagation();
708 evt.preventDefault();
709 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
710 document.execCommand('insertText', false, `\n${indent}`);
711 message.selectionStart = start + 1 + indent.length;
712 message.selectionEnd = message.selectionStart;
713 message.dispatchEvent(new Event('input', { bubbles:true }));
714 }
337 } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {715 } else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
338 evt.stopPropagation();
339 evt.preventDefault();
340 if (executeShortcut.checked) {716 if (executeShortcut.checked) {
717 // execute QR
718 evt.stopImmediatePropagation();
719 evt.stopPropagation();
720 evt.preventDefault();
341 const selectionStart = message.selectionStart;721 const selectionStart = message.selectionStart;
342 const selectionEnd = message.selectionEnd;722 const selectionEnd = message.selectionEnd;
343 message.blur();723 message.blur();
@@ -348,17 +728,175 @@ export class QuickReply {
348 message.selectionEnd = selectionEnd;728 message.selectionEnd = selectionEnd;
349 }729 }
350 }730 }
731 } else if (evt.key == 'F9' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
732 // toggle breakpoint
733 evt.stopImmediatePropagation();
734 evt.stopPropagation();
735 evt.preventDefault();
736 preBreakPointStart = message.selectionStart;
737 preBreakPointEnd = message.selectionEnd;
738 toggleBreakpoint();
739 } else if (evt.code == 'Backslash' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
740 // toggle block comment
741 // (evt.code will use the same physical key on the keyboard across different keyboard layouts)
742 evt.stopImmediatePropagation();
743 evt.stopPropagation();
744 evt.preventDefault();
745 // check if we are inside a comment -> uncomment
746 const parser = new SlashCommandParser();
747 parser.parse(message.value, false);
748 const start = message.selectionStart;
749 const end = message.selectionEnd;
750 const comment = parser.commandIndex.findLast(it=>it.name == '*' && (it.start <= start && it.end >= start || it.start <= end && it.end >= end));
751 if (comment) {
752 // uncomment
753 let content = message.value.slice(comment.start + 1, comment.end - 1);
754 let len = content.length;
755 content = content.replace(/^ /, '');
756 const offsetStart = len - content.length;
757 len = content.length;
758 content = content.replace(/ $/, '');
759 const offsetEnd = len - content.length;
760 message.selectionStart = comment.start - 1;
761 message.selectionEnd = comment.end + 1;
762 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
763 document.execCommand('insertText', false, content);
764 message.selectionStart = start - (start >= comment.start ? 2 + offsetStart : 0);
765 message.selectionEnd = end - 2 - offsetStart - (end >= comment.end ? 2 + offsetEnd : 0);
766 } else {
767 // comment
768 const lineStart = getLineStart();
769 const lineEnd = message.value.indexOf('\n', end);
770 message.selectionStart = lineStart;
771 message.selectionEnd = lineEnd;
772 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
773 document.execCommand('insertText', false, `/* ${message.value.slice(lineStart, lineEnd)} *|`);
774 message.selectionStart = start + 3;
775 message.selectionEnd = end + 3;
776 }
777 message.dispatchEvent(new Event('input', { bubbles:true }));
351 }778 }
352 });779 });
780 const ac = await setSlashCommandAutoComplete(message, true);
353 message.addEventListener('wheel', (evt)=>{781 message.addEventListener('wheel', (evt)=>{
354 updateScrollDebounced(evt);782 updateScrollDebounced(evt);
355 });783 });
356 message.addEventListener('scroll', (evt)=>{784 message.addEventListener('scroll', (evt)=>{
357 updateScrollDebounced();785 updateScrollDebounced();
358 });786 });
787 let preBreakPointStart;
788 let preBreakPointEnd;
789 /**
790 * @param {SlashCommandBreakPoint} bp
791 */
792 const removeBreakpoint = (bp)=>{
793 // start at -1 because "/" is not included in start-end
794 let start = bp.start - 1;
795 // step left until forward slash "/"
796 while (message.value[start] != '/') start--;
797 // step left while whitespace (except newline) before start
798 while (/[^\S\n]/.test(message.value[start - 1])) start--;
799 // if newline before indent, include the newline for removal
800 if (message.value[start - 1] == '\n') start--;
801 let end = bp.end;
802 // step right while whitespace
803 while (/\s/.test(message.value[end])) end++;
804 // if pipe after whitepace, include pipe for removal
805 if (message.value[end] == '|') end++;
806 message.selectionStart = start;
807 message.selectionEnd = end;
808 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
809 document.execCommand('insertText', false, '');
810 message.dispatchEvent(new Event('input', { bubbles:true }));
811 let postStart = preBreakPointStart;
812 let postEnd = preBreakPointEnd;
813 // set caret back to where it was
814 if (preBreakPointStart <= start) {
815 // selection start was before breakpoint: do nothing
816 } else if (preBreakPointStart > start && preBreakPointEnd < end) {
817 // selection start was inside breakpoint: move to index before breakpoint
818 postStart = start;
819 } else if (preBreakPointStart >= end) {
820 // selection start was behind breakpoint: move back by length of removed string
821 postStart = preBreakPointStart - (end - start);
822 }
823 if (preBreakPointEnd <= start) {
824 // do nothing
825 } else if (preBreakPointEnd > start && preBreakPointEnd < end) {
826 // selection end was inside breakpoint: move to index before breakpoint
827 postEnd = start;
828 } else if (preBreakPointEnd >= end) {
829 // selection end was behind breakpoint: move back by length of removed string
830 postEnd = preBreakPointEnd - (end - start);
831 }
832 return { start:postStart, end:postEnd };
833 };
834 /**
835 * @param {SlashCommandExecutor} cmd
836 */
837 const addBreakpoint = (cmd)=>{
838 // start at -1 because "/" is not included in start-end
839 let start = cmd.start - 1;
840 let indent = '';
841 // step left until forward slash "/"
842 while (message.value[start] != '/') start--;
843 // step left while whitespace (except newline) before start, collect the whitespace to help build indentation
844 while (/[^\S\n]/.test(message.value[start - 1])) {
845 start--;
846 indent += message.value[start];
847 }
848 // if newline before indent, include the newline
849 if (message.value[start - 1] == '\n') {
850 start--;
851 indent = `\n${indent}`;
852 }
853 const breakpointText = `${indent}/breakpoint |`;
854 message.selectionStart = start;
855 message.selectionEnd = start;
856 // document.execCommand is deprecated (and potentially buggy in some browsers) but the only way to retain undo-history
857 document.execCommand('insertText', false, breakpointText);
858 message.dispatchEvent(new Event('input', { bubbles:true }));
859 return breakpointText.length;
860 };
861 const toggleBreakpoint = ()=>{
862 const idx = message.selectionStart;
863 let postStart = preBreakPointStart;
864 let postEnd = preBreakPointEnd;
865 const parser = new SlashCommandParser();
866 parser.parse(message.value, false);
867 const cmdIdx = parser.commandIndex.findLastIndex(it=>it.start <= idx);
868 if (cmdIdx > -1) {
869 const cmd = parser.commandIndex[cmdIdx];
870 if (cmd instanceof SlashCommandBreakPoint) {
871 const bp = cmd;
872 const { start, end } = removeBreakpoint(bp);
873 postStart = start;
874 postEnd = end;
875 } else if (parser.commandIndex[cmdIdx - 1] instanceof SlashCommandBreakPoint) {
876 const bp = parser.commandIndex[cmdIdx - 1];
877 const { start, end } = removeBreakpoint(bp);
878 postStart = start;
879 postEnd = end;
880 } else {
881 const len = addBreakpoint(cmd);
882 postStart += len;
883 postEnd += len;
884 }
885 message.selectionStart = postStart;
886 message.selectionEnd = postEnd;
887 }
888 };
889 message.addEventListener('pointerdown', (evt)=>{
890 if (!evt.ctrlKey || !evt.altKey) return;
891 preBreakPointStart = message.selectionStart;
892 preBreakPointEnd = message.selectionEnd;
893 });
894 message.addEventListener('pointerup', async(evt)=>{
895 if (!evt.ctrlKey || !evt.altKey || message.selectionStart != message.selectionEnd) return;
896 toggleBreakpoint();
897 });
359 /** @type {any} */898 /** @type {any} */
360 const resizeListener = debounce((evt) => {899 const resizeListener = debounce((evt) => {
361 updateSyntax();
362 updateScrollDebounced(evt);900 updateScrollDebounced(evt);
363 if (document.activeElement == message) {901 if (document.activeElement == message) {
364 message.blur();902 message.blur();
@@ -367,10 +905,43 @@ export class QuickReply {
367 });905 });
368 window.addEventListener('resize', resizeListener);906 window.addEventListener('resize', resizeListener);
369 updateSyntaxEnabled();907 updateSyntaxEnabled();
908 const updateSyntax = ()=>{
909 if (messageSyntaxInner && syntax.checked) {
910 morphdom(
911 messageSyntaxInner,
912 `<div>${hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value}</div>`,
913 { childrenOnly: true },
914 );
915 updateScrollDebounced();
916 }
917 };
918 let lastSyntaxUpdate = 0;
919 const fpsTime = 1000 / 30;
920 let lastMessageValue = null;
921 let wasSyntax = null;
922 const updateSyntaxLoop = ()=>{
923 const now = Date.now();
924 // fps limit
925 if (now - lastSyntaxUpdate < fpsTime) return requestAnimationFrame(updateSyntaxLoop);
926 // elements don't exist (yet?)
927 if (!messageSyntaxInner || !message) return requestAnimationFrame(updateSyntaxLoop);
928 // elements no longer part of the document
929 if (!messageSyntaxInner.closest('body')) return;
930 // debugger is running
931 if (this.isExecuting) {
932 lastMessageValue = null;
933 return requestAnimationFrame(updateSyntaxLoop);
934 }
935 // value hasn't changed
936 if (wasSyntax == syntax.checked && lastMessageValue == message.value) return requestAnimationFrame(updateSyntaxLoop);
937 wasSyntax = syntax.checked;
938 lastSyntaxUpdate = now;
939 lastMessageValue = message.value;
940 updateSyntax();
941 requestAnimationFrame(updateSyntaxLoop);
942 };
943 requestAnimationFrame(()=>updateSyntaxLoop());
370 message.style.setProperty('text-shadow', 'none', 'important');944 message.style.setProperty('text-shadow', 'none', 'important');
371 /**@type {HTMLElement}*/
372 const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
373 updateSyntax();
374 updateWrap();945 updateWrap();
375 updateTabSize();946 updateTabSize();
376947
@@ -379,7 +950,7 @@ export class QuickReply {
379 const tpl = dom.querySelector('#qr--ctxItem');950 const tpl = dom.querySelector('#qr--ctxItem');
380 const linkList = dom.querySelector('#qr--ctxEditor');951 const linkList = dom.querySelector('#qr--ctxEditor');
381 const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => {952 const fillQrSetSelect = (/**@type {HTMLSelectElement}*/select, /**@type {QuickReplyContextLink}*/ link) => {
382 [{ name: 'Select a QR set' }, ...QuickReplySet.list].forEach(qrs => {953 [{ name: 'Select a QR set' }, ...QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase()))].forEach(qrs => {
383 const opt = document.createElement('option'); {954 const opt = document.createElement('option'); {
384 opt.value = qrs.name;955 opt.value = qrs.name;
385 opt.textContent = qrs.name;956 opt.textContent = qrs.name;
@@ -388,7 +959,7 @@ export class QuickReply {
388 }959 }
389 });960 });
390 };961 };
391 const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {Number}*/idx) => {962 const addCtxItem = (/**@type {QuickReplyContextLink}*/link, /**@type {number}*/idx) => {
392 /**@type {HTMLElement} */963 /**@type {HTMLElement} */
393 // @ts-ignore964 // @ts-ignore
394 const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); {965 const itemDom = tpl.content.querySelector('.qr--ctxItem').cloneNode(true); {
@@ -506,9 +1077,9 @@ export class QuickReply {
506 /**@type {HTMLElement}*/1077 /**@type {HTMLElement}*/
507 const executeResult = dom.querySelector('#qr--modal-executeResult');1078 const executeResult = dom.querySelector('#qr--modal-executeResult');
508 this.editorExecuteResult = executeResult;1079 this.editorExecuteResult = executeResult;
509 /**@type {HTMLInputElement}*/1080 /**@type {HTMLElement}*/
510 const executeHide = dom.querySelector('#qr--modal-executeHide');1081 const debugState = dom.querySelector('#qr--modal-debugState');
511 this.editorExecuteHide = executeHide;1082 this.editorDebugState = debugState;
512 /**@type {HTMLElement}*/1083 /**@type {HTMLElement}*/
513 const executeBtn = dom.querySelector('#qr--modal-execute');1084 const executeBtn = dom.querySelector('#qr--modal-execute');
514 this.editorExecuteBtn = executeBtn;1085 this.editorExecuteBtn = executeBtn;
@@ -536,6 +1107,76 @@ export class QuickReply {
536 this.abortController?.abort('Stop button clicked');1107 this.abortController?.abort('Stop button clicked');
537 });1108 });
5381109
1110 /**@type {HTMLTextAreaElement} */
1111 const inputOg = document.querySelector('#send_textarea');
1112 const inputMirror = dom.querySelector('#qr--modal-send_textarea');
1113 inputMirror.value = inputOg.value;
1114 const inputOgMo = new MutationObserver(muts=>{
1115 if (muts.find(it=>[...it.removedNodes].includes(inputMirror) || [...it.removedNodes].find(n=>n.contains(inputMirror)))) {
1116 inputOg.removeEventListener('input', inputOgListener);
1117 }
1118 });
1119 inputOgMo.observe(document.body, { childList:true });
1120 const inputOgListener = ()=>{
1121 inputMirror.value = inputOg.value;
1122 };
1123 inputOg.addEventListener('input', inputOgListener);
1124 inputMirror.addEventListener('input', ()=>{
1125 inputOg.value = inputMirror.value;
1126 });
1127
1128 /**@type {HTMLElement}*/
1129 const resumeBtn = dom.querySelector('#qr--modal-resume');
1130 resumeBtn.addEventListener('click', ()=>{
1131 this.debugController?.resume();
1132 });
1133 /**@type {HTMLElement}*/
1134 const stepBtn = dom.querySelector('#qr--modal-step');
1135 stepBtn.addEventListener('click', ()=>{
1136 this.debugController?.step();
1137 });
1138 /**@type {HTMLElement}*/
1139 const stepIntoBtn = dom.querySelector('#qr--modal-stepInto');
1140 stepIntoBtn.addEventListener('click', ()=>{
1141 this.debugController?.stepInto();
1142 });
1143 /**@type {HTMLElement}*/
1144 const stepOutBtn = dom.querySelector('#qr--modal-stepOut');
1145 stepOutBtn.addEventListener('click', ()=>{
1146 this.debugController?.stepOut();
1147 });
1148 /**@type {HTMLElement}*/
1149 const minimizeBtn = dom.querySelector('#qr--modal-minimize');
1150 minimizeBtn.addEventListener('click', ()=>{
1151 this.editorDom.classList.add('qr--minimized');
1152 });
1153 const maximizeBtn = dom.querySelector('#qr--modal-maximize');
1154 maximizeBtn.addEventListener('click', ()=>{
1155 this.editorDom.classList.remove('qr--minimized');
1156 });
1157 /**@type {boolean}*/
1158 let isResizing = false;
1159 let resizeStart;
1160 let wStart;
1161 /**@type {HTMLElement}*/
1162 const resizeHandle = dom.querySelector('#qr--resizeHandle');
1163 resizeHandle.addEventListener('pointerdown', (evt)=>{
1164 if (isResizing) return;
1165 isResizing = true;
1166 evt.preventDefault();
1167 resizeStart = evt.x;
1168 wStart = dom.querySelector('#qr--qrOptions').offsetWidth;
1169 const dragListener = debounce((evt)=>{
1170 const w = wStart + resizeStart - evt.x;
1171 dom.querySelector('#qr--qrOptions').style.setProperty('--width', `${w}px`);
1172 }, 5);
1173 window.addEventListener('pointerup', ()=>{
1174 window.removeEventListener('pointermove', dragListener);
1175 isResizing = false;
1176 }, { once:true });
1177 window.addEventListener('pointermove', dragListener);
1178 });
1179
539 await popupResult;1180 await popupResult;
5401181
541 window.removeEventListener('resize', resizeListener);1182 window.removeEventListener('resize', resizeListener);
@@ -544,8 +1185,62 @@ export class QuickReply {
544 }1185 }
545 }1186 }
5461187
1188 getEditorPosition(start, end, message = null) {
1189 const inputRect = this.editorMessage.getBoundingClientRect();
1190 const style = window.getComputedStyle(this.editorMessage);
1191 if (!this.clone) {
1192 this.clone = document.createElement('div');
1193 for (const key of style) {
1194 this.clone.style[key] = style[key];
1195 }
1196 this.clone.style.position = 'fixed';
1197 this.clone.style.visibility = 'hidden';
1198 const mo = new MutationObserver(muts=>{
1199 if (muts.find(it=>[...it.removedNodes].includes(this.editorMessage) || [...it.removedNodes].find(n=>n.contains(this.editorMessage)))) {
1200 this.clone?.remove();
1201 this.clone = null;
1202 }
1203 });
1204 mo.observe(document.body, { childList:true });
1205 }
1206 document.body.append(this.clone);
1207 this.clone.style.width = `${inputRect.width}px`;
1208 this.clone.style.height = `${inputRect.height}px`;
1209 this.clone.style.left = `${inputRect.left}px`;
1210 this.clone.style.top = `${inputRect.top}px`;
1211 this.clone.style.whiteSpace = style.whiteSpace;
1212 this.clone.style.tabSize = style.tabSize;
1213 const text = message ?? this.editorMessage.value;
1214 const before = text.slice(0, start);
1215 this.clone.textContent = before;
1216 const locator = document.createElement('span');
1217 locator.textContent = text.slice(start, end);
1218 this.clone.append(locator);
1219 this.clone.append(text.slice(end));
1220 this.clone.scrollTop = this.editorSyntax.scrollTop;
1221 this.clone.scrollLeft = this.editorSyntax.scrollLeft;
1222 const locatorRect = locator.getBoundingClientRect();
1223 const bodyRect = document.body.getBoundingClientRect();
1224 const location = {
1225 left: locatorRect.left - bodyRect.left,
1226 right: locatorRect.right - bodyRect.left,
1227 top: locatorRect.top - bodyRect.top,
1228 bottom: locatorRect.bottom - bodyRect.top,
1229 };
1230 // this.clone.remove();
1231 return location;
1232 }
547 async executeFromEditor() {1233 async executeFromEditor() {
548 if (this.editorExecutePromise) return;1234 if (this.isExecuting) return;
1235 this.editorPopup.onClosing = ()=>false;
1236 const uuidCheck = /^[0-9a-z]{8}(-[0-9a-z]{4}){3}-[0-9a-z]{12}$/;
1237 const oText = this.message;
1238 this.isExecuting = true;
1239 this.editorDom.classList.add('qr--isExecuting');
1240 const noSyntax = this.editorDom.querySelector('#qr--modal-messageHolder').classList.contains('qr--noSyntax');
1241 if (noSyntax) {
1242 this.editorDom.querySelector('#qr--modal-messageHolder').classList.remove('qr--noSyntax');
1243 }
549 this.editorExecuteBtn.classList.add('qr--busy');1244 this.editorExecuteBtn.classList.add('qr--busy');
550 this.editorExecuteProgress.style.setProperty('--prog', '0');1245 this.editorExecuteProgress.style.setProperty('--prog', '0');
551 this.editorExecuteErrors.classList.remove('qr--hasErrors');1246 this.editorExecuteErrors.classList.remove('qr--hasErrors');
@@ -556,12 +1251,451 @@ export class QuickReply {
556 this.editorExecuteProgress.classList.remove('qr--aborted');1251 this.editorExecuteProgress.classList.remove('qr--aborted');
557 this.editorExecuteErrors.innerHTML = '';1252 this.editorExecuteErrors.innerHTML = '';
558 this.editorExecuteResult.innerHTML = '';1253 this.editorExecuteResult.innerHTML = '';
559 if (this.editorExecuteHide.checked) {1254 const syntax = this.editorDom.querySelector('#qr--modal-messageSyntaxInner');
560 this.editorPopup.dlg.classList.add('qr--hide');1255 const updateScroll = (evt) => {
561 }1256 let left = syntax.scrollLeft;
1257 let top = syntax.scrollTop;
1258 if (evt) {
1259 evt.preventDefault();
1260 left = syntax.scrollLeft + evt.deltaX;
1261 top = syntax.scrollTop + evt.deltaY;
1262 syntax.scrollTo({
1263 behavior: 'instant',
1264 left,
1265 top,
1266 });
1267 }
1268 this.editorMessage.scrollTo({
1269 behavior: 'instant',
1270 left,
1271 top,
1272 });
1273 };
1274 const updateScrollDebounced = updateScroll;
1275 syntax.addEventListener('wheel', (evt)=>{
1276 updateScrollDebounced(evt);
1277 });
1278 syntax.addEventListener('scroll', (evt)=>{
1279 updateScrollDebounced();
1280 });
562 try {1281 try {
563 this.editorExecutePromise = this.execute({}, true);1282 this.abortController = new SlashCommandAbortController();
564 const result = await this.editorExecutePromise;1283 this.debugController = new SlashCommandDebugController();
1284 this.debugController.onBreakPoint = async(closure, executor)=>{
1285 this.editorDom.classList.add('qr--isPaused');
1286 syntax.innerHTML = hljs.highlight(`${closure.fullText}${closure.fullText.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
1287 this.editorMessageLabel.innerHTML = '';
1288 if (uuidCheck.test(closure.source)) {
1289 const p0 = document.createElement('span'); {
1290 p0.textContent = 'anonymous: ';
1291 this.editorMessageLabel.append(p0);
1292 }
1293 const p1 = document.createElement('strong'); {
1294 p1.textContent = executor.source.slice(0,5);
1295 this.editorMessageLabel.append(p1);
1296 }
1297 const p2 = document.createElement('span'); {
1298 p2.textContent = executor.source.slice(5, -5);
1299 this.editorMessageLabel.append(p2);
1300 }
1301 const p3 = document.createElement('strong'); {
1302 p3.textContent = executor.source.slice(-5);
1303 this.editorMessageLabel.append(p3);
1304 }
1305 } else {
1306 this.editorMessageLabel.textContent = executor.source;
1307 }
1308 const source = closure.source;
1309 this.editorDebugState.innerHTML = '';
1310 let ci = -1;
1311 const varNames = [];
1312 const macroNames = [];
1313 /**
1314 * @param {SlashCommandScope} scope
1315 */
1316 const buildVars = (scope, isCurrent = false)=>{
1317 if (!isCurrent) {
1318 ci--;
1319 }
1320 const c = this.debugController.stack.slice(ci)[0];
1321 const wrap = document.createElement('div'); {
1322 wrap.classList.add('qr--scope');
1323 if (isCurrent) {
1324 const executor = this.debugController.cmdStack.slice(-1)[0];
1325 { // named args
1326 const namedTitle = document.createElement('div'); {
1327 namedTitle.classList.add('qr--title');
1328 namedTitle.textContent = `Named Args - /${executor.name}`;
1329 if (executor.command.name == 'run') {
1330 namedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1331 }
1332 wrap.append(namedTitle);
1333 }
1334 const keys = new Set([...Object.keys(this.debugController.namedArguments ?? {}), ...(executor.namedArgumentList ?? []).map(it=>it.name)]);
1335 for (const key of keys) {
1336 if (key[0] == '_') continue;
1337 const item = document.createElement('div'); {
1338 item.classList.add('qr--var');
1339 const k = document.createElement('div'); {
1340 k.classList.add('qr--key');
1341 k.textContent = key;
1342 item.append(k);
1343 }
1344 const vUnresolved = document.createElement('div'); {
1345 vUnresolved.classList.add('qr--val');
1346 vUnresolved.classList.add('qr--singleCol');
1347 const val = executor.namedArgumentList.find(it=>it.name == key)?.value;
1348 if (val instanceof SlashCommandClosure) {
1349 vUnresolved.classList.add('qr--closure');
1350 vUnresolved.title = val.rawText;
1351 vUnresolved.textContent = val.toString();
1352 } else if (val === undefined) {
1353 vUnresolved.classList.add('qr--undefined');
1354 vUnresolved.textContent = 'undefined';
1355 } else {
1356 let jsonVal;
1357 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1358 if (jsonVal && typeof jsonVal == 'object') {
1359 vUnresolved.textContent = JSON.stringify(jsonVal, null, 2);
1360 } else {
1361 vUnresolved.textContent = val;
1362 vUnresolved.classList.add('qr--simple');
1363 }
1364 }
1365 item.append(vUnresolved);
1366 }
1367 const vResolved = document.createElement('div'); {
1368 vResolved.classList.add('qr--val');
1369 vResolved.classList.add('qr--singleCol');
1370 if (this.debugController.namedArguments === undefined) {
1371 vResolved.classList.add('qr--unresolved');
1372 } else {
1373 const val = this.debugController.namedArguments?.[key];
1374 if (val instanceof SlashCommandClosure) {
1375 vResolved.classList.add('qr--closure');
1376 vResolved.title = val.rawText;
1377 vResolved.textContent = val.toString();
1378 } else if (val === undefined) {
1379 vResolved.classList.add('qr--undefined');
1380 vResolved.textContent = 'undefined';
1381 } else {
1382 let jsonVal;
1383 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1384 if (jsonVal && typeof jsonVal == 'object') {
1385 vResolved.textContent = JSON.stringify(jsonVal, null, 2);
1386 } else {
1387 vResolved.textContent = val;
1388 vResolved.classList.add('qr--simple');
1389 }
1390 }
1391 }
1392 item.append(vResolved);
1393 }
1394 wrap.append(item);
1395 }
1396 }
1397 }
1398 { // unnamed args
1399 const unnamedTitle = document.createElement('div'); {
1400 unnamedTitle.classList.add('qr--title');
1401 unnamedTitle.textContent = `Unnamed Args - /${executor.name}`;
1402 if (executor.command.name == 'run') {
1403 unnamedTitle.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1404 }
1405 wrap.append(unnamedTitle);
1406 }
1407 let i = 0;
1408 let unnamed = this.debugController.unnamedArguments ?? [];
1409 if (!Array.isArray(unnamed)) unnamed = [unnamed];
1410 while (unnamed.length < executor.unnamedArgumentList?.length ?? 0) unnamed.push(undefined);
1411 unnamed = unnamed.map((it,idx)=>[executor.unnamedArgumentList?.[idx], it]);
1412 for (const arg of unnamed) {
1413 i++;
1414 const item = document.createElement('div'); {
1415 item.classList.add('qr--var');
1416 const k = document.createElement('div'); {
1417 k.classList.add('qr--key');
1418 k.textContent = i.toString();
1419 item.append(k);
1420 }
1421 const vUnresolved = document.createElement('div'); {
1422 vUnresolved.classList.add('qr--val');
1423 vUnresolved.classList.add('qr--singleCol');
1424 const val = arg[0]?.value;
1425 if (val instanceof SlashCommandClosure) {
1426 vUnresolved.classList.add('qr--closure');
1427 vUnresolved.title = val.rawText;
1428 vUnresolved.textContent = val.toString();
1429 } else if (val === undefined) {
1430 vUnresolved.classList.add('qr--undefined');
1431 vUnresolved.textContent = 'undefined';
1432 } else {
1433 let jsonVal;
1434 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1435 if (jsonVal && typeof jsonVal == 'object') {
1436 vUnresolved.textContent = JSON.stringify(jsonVal, null, 2);
1437 } else {
1438 vUnresolved.textContent = val;
1439 vUnresolved.classList.add('qr--simple');
1440 }
1441 }
1442 item.append(vUnresolved);
1443 }
1444 const vResolved = document.createElement('div'); {
1445 vResolved.classList.add('qr--val');
1446 vResolved.classList.add('qr--singleCol');
1447 if (this.debugController.unnamedArguments === undefined) {
1448 vResolved.classList.add('qr--unresolved');
1449 } else if ((Array.isArray(this.debugController.unnamedArguments) ? this.debugController.unnamedArguments : [this.debugController.unnamedArguments]).length < i) {
1450 // do nothing
1451 } else {
1452 const val = arg[1];
1453 if (val instanceof SlashCommandClosure) {
1454 vResolved.classList.add('qr--closure');
1455 vResolved.title = val.rawText;
1456 vResolved.textContent = val.toString();
1457 } else if (val === undefined) {
1458 vResolved.classList.add('qr--undefined');
1459 vResolved.textContent = 'undefined';
1460 } else {
1461 let jsonVal;
1462 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1463 if (jsonVal && typeof jsonVal == 'object') {
1464 vResolved.textContent = JSON.stringify(jsonVal, null, 2);
1465 } else {
1466 vResolved.textContent = val;
1467 vResolved.classList.add('qr--simple');
1468 }
1469 }
1470 }
1471 item.append(vResolved);
1472 }
1473 wrap.append(item);
1474 }
1475 }
1476 }
1477 }
1478 // current scope
1479 const title = document.createElement('div'); {
1480 title.classList.add('qr--title');
1481 title.textContent = isCurrent ? 'Current Scope' : 'Parent Scope';
1482 if (c.source == source) {
1483 let hi;
1484 title.addEventListener('pointerenter', ()=>{
1485 const loc = this.getEditorPosition(Math.max(0, c.executorList[0].start - 1), c.executorList.slice(-1)[0].end, c.fullText);
1486 const layer = syntax.getBoundingClientRect();
1487 hi = document.createElement('div');
1488 hi.classList.add('qr--highlight-secondary');
1489 hi.style.left = `${loc.left - layer.left}px`;
1490 hi.style.width = `${loc.right - loc.left}px`;
1491 hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1492 hi.style.height = `${loc.bottom - loc.top}px`;
1493 syntax.append(hi);
1494 });
1495 title.addEventListener('pointerleave', ()=>hi?.remove());
1496 }
1497 wrap.append(title);
1498 }
1499 for (const key of Object.keys(scope.variables)) {
1500 const isHidden = varNames.includes(key);
1501 if (!isHidden) varNames.push(key);
1502 const item = document.createElement('div'); {
1503 item.classList.add('qr--var');
1504 if (isHidden) item.classList.add('qr--isHidden');
1505 const k = document.createElement('div'); {
1506 k.classList.add('qr--key');
1507 k.textContent = key;
1508 item.append(k);
1509 }
1510 const v = document.createElement('div'); {
1511 v.classList.add('qr--val');
1512 const val = scope.variables[key];
1513 if (val instanceof SlashCommandClosure) {
1514 v.classList.add('qr--closure');
1515 v.title = val.rawText;
1516 v.textContent = val.toString();
1517 } else if (val === undefined) {
1518 v.classList.add('qr--undefined');
1519 v.textContent = 'undefined';
1520 } else {
1521 let jsonVal;
1522 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1523 if (jsonVal && typeof jsonVal == 'object') {
1524 v.textContent = JSON.stringify(jsonVal, null, 2);
1525 } else {
1526 v.textContent = val;
1527 v.classList.add('qr--simple');
1528 }
1529 }
1530 item.append(v);
1531 }
1532 wrap.append(item);
1533 }
1534 }
1535 for (const key of Object.keys(scope.macros)) {
1536 const isHidden = macroNames.includes(key);
1537 if (!isHidden) macroNames.push(key);
1538 const item = document.createElement('div'); {
1539 item.classList.add('qr--macro');
1540 if (isHidden) item.classList.add('qr--isHidden');
1541 const k = document.createElement('div'); {
1542 k.classList.add('qr--key');
1543 k.textContent = key;
1544 item.append(k);
1545 }
1546 const v = document.createElement('div'); {
1547 v.classList.add('qr--val');
1548 const val = scope.macros[key];
1549 if (val instanceof SlashCommandClosure) {
1550 v.classList.add('qr--closure');
1551 v.title = val.rawText;
1552 v.textContent = val.toString();
1553 } else if (val === undefined) {
1554 v.classList.add('qr--undefined');
1555 v.textContent = 'undefined';
1556 } else {
1557 let jsonVal;
1558 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1559 if (jsonVal && typeof jsonVal == 'object') {
1560 v.textContent = JSON.stringify(jsonVal, null, 2);
1561 } else {
1562 v.textContent = val;
1563 v.classList.add('qr--simple');
1564 }
1565 }
1566 item.append(v);
1567 }
1568 wrap.append(item);
1569 }
1570 }
1571 const pipeItem = document.createElement('div'); {
1572 pipeItem.classList.add('qr--pipe');
1573 const k = document.createElement('div'); {
1574 k.classList.add('qr--key');
1575 k.textContent = 'pipe';
1576 pipeItem.append(k);
1577 }
1578 const v = document.createElement('div'); {
1579 v.classList.add('qr--val');
1580 const val = scope.pipe;
1581 if (val instanceof SlashCommandClosure) {
1582 v.classList.add('qr--closure');
1583 v.title = val.rawText;
1584 v.textContent = val.toString();
1585 } else if (val === undefined) {
1586 v.classList.add('qr--undefined');
1587 v.textContent = 'undefined';
1588 } else {
1589 let jsonVal;
1590 try { jsonVal = JSON.parse(val); } catch { /* empty */ }
1591 if (jsonVal && typeof jsonVal == 'object') {
1592 v.textContent = JSON.stringify(jsonVal, null, 2);
1593 } else {
1594 v.textContent = val;
1595 v.classList.add('qr--simple');
1596 }
1597 }
1598 pipeItem.append(v);
1599 }
1600 wrap.append(pipeItem);
1601 }
1602 if (scope.parent) {
1603 wrap.append(buildVars(scope.parent));
1604 }
1605 }
1606 return wrap;
1607 };
1608 const buildStack = ()=>{
1609 const wrap = document.createElement('div'); {
1610 wrap.classList.add('qr--stack');
1611 const title = document.createElement('div'); {
1612 title.classList.add('qr--title');
1613 title.textContent = 'Call Stack';
1614 wrap.append(title);
1615 }
1616 let ei = -1;
1617 for (const executor of this.debugController.cmdStack.toReversed()) {
1618 ei++;
1619 const c = this.debugController.stack.toReversed()[ei];
1620 const item = document.createElement('div'); {
1621 item.classList.add('qr--item');
1622 if (executor.source == source) {
1623 let hi;
1624 item.addEventListener('pointerenter', ()=>{
1625 const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, c.fullText);
1626 const layer = syntax.getBoundingClientRect();
1627 hi = document.createElement('div');
1628 hi.classList.add('qr--highlight-secondary');
1629 hi.style.left = `${loc.left - layer.left}px`;
1630 hi.style.width = `${loc.right - loc.left}px`;
1631 hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1632 hi.style.height = `${loc.bottom - loc.top}px`;
1633 syntax.append(hi);
1634 });
1635 item.addEventListener('pointerleave', ()=>hi?.remove());
1636 }
1637 const cmd = document.createElement('div'); {
1638 cmd.classList.add('qr--cmd');
1639 cmd.textContent = `/${executor.name}`;
1640 if (executor.command.name == 'run') {
1641 cmd.textContent += `${(executor.name == ':' ? '' : ' ')}${executor.unnamedArgumentList[0]?.value}`;
1642 }
1643 item.append(cmd);
1644 }
1645 const src = document.createElement('div'); {
1646 src.classList.add('qr--source');
1647 const line = closure.fullText.slice(0, executor.start).split('\n').length;
1648 if (uuidCheck.test(executor.source)) {
1649 const p1 = document.createElement('span'); {
1650 p1.classList.add('qr--fixed');
1651 p1.textContent = executor.source.slice(0,5);
1652 src.append(p1);
1653 }
1654 const p2 = document.createElement('span'); {
1655 p2.classList.add('qr--truncated');
1656 p2.textContent = '…';
1657 src.append(p2);
1658 }
1659 const p3 = document.createElement('span'); {
1660 p3.classList.add('qr--fixed');
1661 p3.textContent = `${executor.source.slice(-5)}:${line}`;
1662 src.append(p3);
1663 }
1664 src.title = `anonymous: ${executor.source}`;
1665 } else {
1666 src.textContent = `${executor.source}:${line}`;
1667 }
1668 item.append(src);
1669 }
1670 wrap.append(item);
1671 }
1672 }
1673 }
1674 return wrap;
1675 };
1676 this.editorDebugState.append(buildVars(closure.scope, true));
1677 this.editorDebugState.append(buildStack());
1678 this.editorDebugState.classList.add('qr--active');
1679 const loc = this.getEditorPosition(Math.max(0, executor.start - 1), executor.end, closure.fullText);
1680 const layer = syntax.getBoundingClientRect();
1681 const hi = document.createElement('div');
1682 hi.classList.add('qr--highlight');
1683 if (this.debugController.namedArguments === undefined) {
1684 hi.classList.add('qr--unresolved');
1685 }
1686 hi.style.left = `${loc.left - layer.left}px`;
1687 hi.style.width = `${loc.right - loc.left}px`;
1688 hi.style.top = `${loc.top - layer.top + syntax.scrollTop}px`;
1689 hi.style.height = `${loc.bottom - loc.top}px`;
1690 syntax.append(hi);
1691 const isStepping = await this.debugController.awaitContinue();
1692 hi.remove();
1693 this.editorDebugState.textContent = '';
1694 this.editorDebugState.classList.remove('qr--active');
1695 this.editorDom.classList.remove('qr--isPaused');
1696 return isStepping;
1697 };
1698 const result = await this.onDebug(this);
565 if (this.abortController?.signal?.aborted) {1699 if (this.abortController?.signal?.aborted) {
566 this.editorExecuteProgress.classList.add('qr--aborted');1700 this.editorExecuteProgress.classList.add('qr--aborted');
567 } else {1701 } else {
@@ -586,9 +1720,18 @@ export class QuickReply {
586 `;1720 `;
587 }1721 }
588 }1722 }
1723 if (noSyntax) {
1724 this.editorDom.querySelector('#qr--modal-messageHolder').classList.add('qr--noSyntax');
1725 }
1726 this.editorMessageLabel.innerHTML = '';
1727 this.editorMessageLabel.textContent = 'Message / Command: ';
1728 this.editorMessage.value = oText;
1729 this.editorMessage.dispatchEvent(new Event('input', { bubbles:true }));
589 this.editorExecutePromise = null;1730 this.editorExecutePromise = null;
590 this.editorExecuteBtn.classList.remove('qr--busy');1731 this.editorExecuteBtn.classList.remove('qr--busy');
591 this.editorPopup.dlg.classList.remove('qr--hide');1732 this.editorDom.classList.remove('qr--isExecuting');
1733 this.isExecuting = false;
1734 this.editorPopup.onClosing = null;
592 }1735 }
5931736
594 updateEditorProgress(done, total) {1737 updateEditorProgress(done, total) {
@@ -623,6 +1766,47 @@ export class QuickReply {
623 /**1766 /**
624 * @param {string} value1767 * @param {string} value
625 */1768 */
1769 updateIcon(value) {
1770 if (this.onUpdate) {
1771 if (value === null) return;
1772 if (this.settingsDomIcon) {
1773 if (this.icon != value) {
1774 if (value == '') {
1775 if (this.icon) {
1776 this.settingsDomIcon.classList.remove(this.icon);
1777 }
1778 this.settingsDomIcon.textContent = '…';
1779 this.settingsDomIcon.classList.remove('fa-solid');
1780 } else {
1781 if (this.icon) {
1782 this.settingsDomIcon.classList.remove(this.icon);
1783 } else {
1784 this.settingsDomIcon.classList.add('fa-solid');
1785 }
1786 this.settingsDomIcon.classList.add(value);
1787 }
1788 }
1789 }
1790 this.icon = value;
1791 this.updateRender();
1792 this.onUpdate(this);
1793 }
1794 }
1795
1796 /**
1797 * @param {boolean} value
1798 */
1799 updateShowLabel(value) {
1800 if (this.onUpdate) {
1801 this.showLabel = value;
1802 this.updateRender();
1803 this.onUpdate(this);
1804 }
1805 }
1806
1807 /**
1808 * @param {string} value
1809 */
626 updateLabel(value) {1810 updateLabel(value) {
627 if (this.onUpdate) {1811 if (this.onUpdate) {
628 if (this.settingsDomLabel && this.settingsDomLabel.value != value) {1812 if (this.settingsDomLabel && this.settingsDomLabel.value != value) {
@@ -670,21 +1854,25 @@ export class QuickReply {
670 }1854 }
6711855
6721856
673 async execute(args = {}, isEditor = false, isRun = false) {1857 async execute(args = {}, isEditor = false, isRun = false, options = {}) {
674 if (this.message?.length > 0 && this.onExecute) {1858 if (this.message?.length > 0 && this.onExecute) {
675 const scope = new SlashCommandScope();1859 const scope = new SlashCommandScope();
676 for (const key of Object.keys(args)) {1860 for (const key of Object.keys(args)) {
1861 if (key[0] == '_') continue;
1862 if (key == 'isAutoExecute') continue;
677 scope.setMacro(`arg::${key}`, args[key]);1863 scope.setMacro(`arg::${key}`, args[key]);
678 }1864 }
1865 scope.setMacro('arg::*', '');
679 if (isEditor) {1866 if (isEditor) {
680 this.abortController = new SlashCommandAbortController();1867 this.abortController = new SlashCommandAbortController();
681 }1868 }
682 return await this.onExecute(this, {1869 return await this.onExecute(this, {
683 message:this.message,1870 message: this.message,
684 isAutoExecute: args.isAutoExecute ?? false,1871 isAutoExecute: args.isAutoExecute ?? false,
685 isEditor,1872 isEditor,
686 isRun,1873 isRun,
687 scope,1874 scope,
1875 executionOptions: options,
688 });1876 });
689 }1877 }
690 }1878 }
@@ -695,6 +1883,8 @@ export class QuickReply {
695 toJSON() {1883 toJSON() {
696 return {1884 return {
697 id: this.id,1885 id: this.id,
1886 icon: this.icon,
1887 showLabel: this.showLabel,
698 label: this.label,1888 label: this.label,
699 title: this.title,1889 title: this.title,
700 message: this.message,1890 message: this.message,
public/scripts/extensions/quick-reply/src/QuickReplyConfig.js+6 -1
@@ -60,7 +60,12 @@ export class QuickReplyConfig {
60 /**@type {HTMLElement}*/60 /**@type {HTMLElement}*/
61 this.setListDom = root.querySelector('.qr--setList');61 this.setListDom = root.querySelector('.qr--setList');
62 root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{62 root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{
63 this.addSet(QuickReplySet.list[0]);63 const newSet = QuickReplySet.list.find(qr=>!this.setList.find(qrl=>qrl.set == qr));
64 if (newSet) {
65 this.addSet(newSet);
66 } else {
67 toastr.warning('All existing QR Sets have already been added.');
68 }
64 });69 });
65 this.updateSetListDom();70 this.updateSetListDom();
66 }71 }
public/scripts/extensions/quick-reply/src/QuickReplySet.js+191 -20
@@ -1,7 +1,9 @@
1import { getRequestHeaders, substituteParams } from '../../../../script.js';1import { getRequestHeaders, substituteParams } from '../../../../script.js';
2import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';
2import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';3import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
4import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
3import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';5import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
4import { debounceAsync, warn } from '../index.js';6import { debounceAsync, log, warn } from '../index.js';
5import { QuickReply } from './QuickReply.js';7import { QuickReply } from './QuickReply.js';
68
7export class QuickReplySet {9export class QuickReplySet {
@@ -16,7 +18,7 @@ export class QuickReplySet {
16 }18 }
1719
18 /**20 /**
19 * @param {String} name - name of the QuickReplySet21 * @param {string} name - name of the QuickReplySet
20 */22 */
21 static get(name) {23 static get(name) {
22 return this.list.find(it=>it.name == name);24 return this.list.find(it=>it.name == name);
@@ -25,17 +27,19 @@ export class QuickReplySet {
2527
2628
2729
28 /**@type {String}*/ name;30 /**@type {string}*/ name;
29 /**@type {Boolean}*/ disableSend = false;31 /**@type {boolean}*/ disableSend = false;
30 /**@type {Boolean}*/ placeBeforeInput = false;32 /**@type {boolean}*/ placeBeforeInput = false;
31 /**@type {Boolean}*/ injectInput = false;33 /**@type {boolean}*/ injectInput = false;
34 /**@type {string}*/ color = 'transparent';
35 /**@type {boolean}*/ onlyBorderColor = false;
32 /**@type {QuickReply[]}*/ qrList = [];36 /**@type {QuickReply[]}*/ qrList = [];
3337
34 /**@type {Number}*/ idIndex = 0;38 /**@type {number}*/ idIndex = 0;
3539
36 /**@type {Boolean}*/ isDeleted = false;40 /**@type {boolean}*/ isDeleted = false;
3741
38 /**@type {Function}*/ save;42 /**@type {function}*/ save;
3943
40 /**@type {HTMLElement}*/ dom;44 /**@type {HTMLElement}*/ dom;
41 /**@type {HTMLElement}*/ settingsDom;45 /**@type {HTMLElement}*/ settingsDom;
@@ -64,6 +68,7 @@ export class QuickReplySet {
64 const root = document.createElement('div'); {68 const root = document.createElement('div'); {
65 this.dom = root;69 this.dom = root;
66 root.classList.add('qr--buttons');70 root.classList.add('qr--buttons');
71 this.updateColor();
67 this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{72 this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{
68 root.append(qr.render());73 root.append(qr.render());
69 });74 });
@@ -78,6 +83,22 @@ export class QuickReplySet {
78 this.dom.append(qr.render());83 this.dom.append(qr.render());
79 });84 });
80 }85 }
86 updateColor() {
87 if (!this.dom) return;
88 if (this.color && this.color != 'transparent') {
89 this.dom.style.setProperty('--qr--color', this.color);
90 this.dom.classList.add('qr--color');
91 if (this.onlyBorderColor) {
92 this.dom.classList.add('qr--borderColor');
93 } else {
94 this.dom.classList.remove('qr--borderColor');
95 }
96 } else {
97 this.dom.style.setProperty('--qr--color', 'transparent');
98 this.dom.classList.remove('qr--color');
99 this.dom.classList.remove('qr--borderColor');
100 }
101 }
81102
82103
83104
@@ -93,6 +114,11 @@ export class QuickReplySet {
93 }114 }
94 return this.settingsDom;115 return this.settingsDom;
95 }116 }
117 /**
118 *
119 * @param {QuickReply} qr
120 * @param {number} idx
121 */
96 renderSettingsItem(qr, idx) {122 renderSettingsItem(qr, idx) {
97 this.settingsDom.append(qr.renderSettings(idx));123 this.settingsDom.append(qr.renderSettings(idx));
98 }124 }
@@ -102,6 +128,18 @@ export class QuickReplySet {
102128
103 /**129 /**
104 *130 *
131 * @param {QuickReply} qr
132 */
133 async debug(qr) {
134 const parser = new SlashCommandParser();
135 const closure = parser.parse(qr.message, true, [], qr.abortController, qr.debugController);
136 closure.source = `${this.name}.${qr.label}`;
137 closure.onProgress = (done, total) => qr.updateEditorProgress(done, total);
138 closure.scope.setMacro('arg::*', '');
139 return (await closure.execute())?.pipe;
140 }
141 /**
142 *
105 * @param {QuickReply} qr The QR to execute.143 * @param {QuickReply} qr The QR to execute.
106 * @param {object} options144 * @param {object} options
107 * @param {string} [options.message] (null) altered message to be used145 * @param {string} [options.message] (null) altered message to be used
@@ -109,6 +147,7 @@ export class QuickReplySet {
109 * @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor147 * @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor
110 * @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)148 * @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)
111 * @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command149 * @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command
150 * @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options.executionOptions] ({}) further execution options
112 * @returns151 * @returns
113 */152 */
114 async executeWithOptions(qr, options = {}) {153 async executeWithOptions(qr, options = {}) {
@@ -118,7 +157,9 @@ export class QuickReplySet {
118 isEditor:false,157 isEditor:false,
119 isRun:false,158 isRun:false,
120 scope:null,159 scope:null,
160 executionOptions:{},
121 }, options);161 }, options);
162 const execOptions = options.executionOptions;
122 /**@type {HTMLTextAreaElement}*/163 /**@type {HTMLTextAreaElement}*/
123 const ta = document.querySelector('#send_textarea');164 const ta = document.querySelector('#send_textarea');
124 const finalMessage = options.message ?? qr.message;165 const finalMessage = options.message ?? qr.message;
@@ -136,21 +177,24 @@ export class QuickReplySet {
136 if (input[0] == '/' && !this.disableSend) {177 if (input[0] == '/' && !this.disableSend) {
137 let result;178 let result;
138 if (options.isAutoExecute || options.isRun) {179 if (options.isAutoExecute || options.isRun) {
139 result = await executeSlashCommandsWithOptions(input, {180 result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
140 handleParserErrors: true,181 handleParserErrors: true,
141 scope: options.scope,182 scope: options.scope,
142 });183 source: `${this.name}.${qr.label}`,
184 }));
143 } else if (options.isEditor) {185 } else if (options.isEditor) {
144 result = await executeSlashCommandsWithOptions(input, {186 result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
145 handleParserErrors: false,187 handleParserErrors: false,
146 scope: options.scope,188 scope: options.scope,
147 abortController: qr.abortController,189 abortController: qr.abortController,
190 source: `${this.name}.${qr.label}`,
148 onProgress: (done, total) => qr.updateEditorProgress(done, total),191 onProgress: (done, total) => qr.updateEditorProgress(done, total),
149 });192 }));
150 } else {193 } else {
151 result = await executeSlashCommandsOnChatInput(input, {194 result = await executeSlashCommandsOnChatInput(input, Object.assign(execOptions, {
152 scope: options.scope,195 scope: options.scope,
153 });196 source: `${this.name}.${qr.label}`,
197 }));
154 }198 }
155 return typeof result === 'object' ? result?.pipe : '';199 return typeof result === 'object' ? result?.pipe : '';
156 }200 }
@@ -165,7 +209,7 @@ export class QuickReplySet {
165 }209 }
166 /**210 /**
167 * @param {QuickReply} qr211 * @param {QuickReply} qr
168 * @param {String} [message] - optional altered message to be used212 * @param {string} [message] - optional altered message to be used
169 * @param {SlashCommandScope} [scope] - optional scope to be used when running the command213 * @param {SlashCommandScope} [scope] - optional scope to be used when running the command
170 */214 */
171 async execute(qr, message = null, isAutoExecute = false, scope = null) {215 async execute(qr, message = null, isAutoExecute = false, scope = null) {
@@ -179,10 +223,11 @@ export class QuickReplySet {
179223
180224
181225
182 addQuickReply() {226 addQuickReply(data = {}) {
183 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;227 const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
228 data.id =
184 this.idIndex = id + 1;229 this.idIndex = id + 1;
185 const qr = QuickReply.from({ id });230 const qr = QuickReply.from(data);
186 this.qrList.push(qr);231 this.qrList.push(qr);
187 this.hookQuickReply(qr);232 this.hookQuickReply(qr);
188 if (this.settingsDom) {233 if (this.settingsDom) {
@@ -194,11 +239,131 @@ export class QuickReplySet {
194 this.save();239 this.save();
195 return qr;240 return qr;
196 }241 }
242 addQuickReplyFromText(qrJson) {
243 let data;
244 if (qrJson) {
245 try {
246 data = JSON.parse(qrJson ?? '{}');
247 delete data.id;
248 } catch {
249 // not JSON data
250 }
251 if (data) {
252 // JSON data
253 if (data.label === undefined || data.message === undefined) {
254 // not a QR
255 toastr.error('Not a QR.');
256 return;
257 }
258 } else {
259 // no JSON, use plaintext as QR message
260 data = { message: qrJson };
261 }
262 } else {
263 data = {};
264 }
265 const newQr = this.addQuickReply(data);
266 return newQr;
267 }
197268
269 /**
270 *
271 * @param {QuickReply} qr
272 */
198 hookQuickReply(qr) {273 hookQuickReply(qr) {
274 qr.onDebug = ()=>this.debug(qr);
199 qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);275 qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
200 qr.onDelete = ()=>this.removeQuickReply(qr);276 qr.onDelete = ()=>this.removeQuickReply(qr);
201 qr.onUpdate = ()=>this.save();277 qr.onUpdate = ()=>this.save();
278 qr.onInsertBefore = (qrJson)=>{
279 this.addQuickReplyFromText(qrJson);
280 const newQr = this.qrList.pop();
281 this.qrList.splice(this.qrList.indexOf(qr), 0, newQr);
282 if (qr.settingsDom) {
283 qr.settingsDom.insertAdjacentElement('beforebegin', newQr.settingsDom);
284 }
285 this.save();
286 };
287 qr.onTransfer = async()=>{
288 /**@type {HTMLSelectElement} */
289 let sel;
290 let isCopy = false;
291 const dom = document.createElement('div'); {
292 dom.classList.add('qr--transferModal');
293 const title = document.createElement('h3'); {
294 title.textContent = 'Transfer Quick Reply';
295 dom.append(title);
296 }
297 const subTitle = document.createElement('h4'); {
298 const entryName = qr.label;
299 const bookName = this.name;
300 subTitle.textContent = `${bookName}: ${entryName}`;
301 dom.append(subTitle);
302 }
303 sel = document.createElement('select'); {
304 sel.classList.add('qr--transferSelect');
305 sel.setAttribute('autofocus', '1');
306 const noOpt = document.createElement('option'); {
307 noOpt.value = '';
308 noOpt.textContent = '-- Select QR Set --';
309 sel.append(noOpt);
310 }
311 for (const qrs of QuickReplySet.list) {
312 const opt = document.createElement('option'); {
313 opt.value = qrs.name;
314 opt.textContent = qrs.name;
315 sel.append(opt);
316 }
317 }
318 sel.addEventListener('keyup', (evt)=>{
319 if (evt.key == 'Shift') {
320 (dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy');
321 return;
322 }
323 });
324 sel.addEventListener('keydown', (evt)=>{
325 if (evt.key == 'Shift') {
326 (dlg.dom ?? dlg.dlg).classList.add('qr--isCopy');
327 return;
328 }
329 if (!evt.ctrlKey && !evt.altKey && evt.key == 'Enter') {
330 evt.preventDefault();
331 if (evt.shiftKey) isCopy = true;
332 dlg.completeAffirmative();
333 }
334 });
335 dom.append(sel);
336 }
337 const hintP = document.createElement('p'); {
338 const hint = document.createElement('small'); {
339 hint.textContent = 'Type or arrows to select QR Set. Enter to transfer. Shift+Enter to copy.';
340 hintP.append(hint);
341 }
342 dom.append(hintP);
343 }
344 }
345 const dlg = new Popup(dom, POPUP_TYPE.CONFIRM, null, { okButton:'Transfer', cancelButton:'Cancel' });
346 const copyBtn = document.createElement('div'); {
347 copyBtn.classList.add('qr--copy');
348 copyBtn.classList.add('menu_button');
349 copyBtn.textContent = 'Copy';
350 copyBtn.addEventListener('click', ()=>{
351 isCopy = true;
352 dlg.completeAffirmative();
353 });
354 (dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn);
355 }
356 const prom = dlg.show();
357 sel.focus();
358 await prom;
359 if (dlg.result == POPUP_RESULT.AFFIRMATIVE) {
360 const qrs = QuickReplySet.list.find(it=>it.name == sel.value);
361 qrs.addQuickReply(qr.toJSON());
362 if (!isCopy) {
363 qr.delete();
364 }
365 }
366 };
202 }367 }
203368
204 removeQuickReply(qr) {369 removeQuickReply(qr) {
@@ -214,6 +379,8 @@ export class QuickReplySet {
214 disableSend: this.disableSend,379 disableSend: this.disableSend,
215 placeBeforeInput: this.placeBeforeInput,380 placeBeforeInput: this.placeBeforeInput,
216 injectInput: this.injectInput,381 injectInput: this.injectInput,
382 color: this.color,
383 onlyBorderColor: this.onlyBorderColor,
217 qrList: this.qrList,384 qrList: this.qrList,
218 idIndex: this.idIndex,385 idIndex: this.idIndex,
219 };386 };
@@ -245,8 +412,12 @@ export class QuickReplySet {
245 if (response.ok) {412 if (response.ok) {
246 this.unrender();413 this.unrender();
247 const idx = QuickReplySet.list.indexOf(this);414 const idx = QuickReplySet.list.indexOf(this);
248 QuickReplySet.list.splice(idx, 1);415 if (idx > -1) {
249 this.isDeleted = true;416 QuickReplySet.list.splice(idx, 1);
417 this.isDeleted = true;
418 } else {
419 warn(`Deleted Quick Reply Set was not found in the list of sets: ${this.name}`);
420 }
250 } else {421 } else {
251 warn(`Failed to delete Quick Reply Set: ${this.name}`);422 warn(`Failed to delete Quick Reply Set: ${this.name}`);
252 }423 }
public/scripts/extensions/quick-reply/src/QuickReplySetLink.js+1 -1
@@ -45,7 +45,7 @@ export class QuickReplySetLink {
45 this.set = QuickReplySet.get(set.value);45 this.set = QuickReplySet.get(set.value);
46 this.update();46 this.update();
47 });47 });
48 QuickReplySet.list.forEach(qrs=>{48 QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs=>{
49 const opt = document.createElement('option'); {49 const opt = document.createElement('option'); {
50 opt.value = qrs.name;50 opt.value = qrs.name;
51 opt.textContent = qrs.name;51 opt.textContent = qrs.name;
public/scripts/extensions/quick-reply/src/SlashCommandHandler.js+259 -18
@@ -1,8 +1,12 @@
1import { SlashCommand } from '../../../slash-commands/SlashCommand.js';1import { SlashCommand } from '../../../slash-commands/SlashCommand.js';
2import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
2import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js';3import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js';
4import { SlashCommandClosure } from '../../../slash-commands/SlashCommandClosure.js';
3import { enumIcons } from '../../../slash-commands/SlashCommandCommonEnumsProvider.js';5import { enumIcons } from '../../../slash-commands/SlashCommandCommonEnumsProvider.js';
6import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js';
4import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashCommandEnumValue.js';7import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashCommandEnumValue.js';
5import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';8import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
9import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
6import { isTrueBoolean } from '../../../utils.js';10import { isTrueBoolean } from '../../../utils.js';
7// eslint-disable-next-line no-unused-vars11// eslint-disable-next-line no-unused-vars
8import { QuickReplyApi } from '../api/QuickReplyApi.js';12import { QuickReplyApi } from '../api/QuickReplyApi.js';
@@ -47,6 +51,13 @@ export class SlashCommandHandler {
47 return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr);51 return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr);
48 }) ?? [],52 }) ?? [],
4953
54 /** All QRs inside a set, utilizing the "set" named argument, returns the QR's ID */
55 qrIds: (executor) => QuickReplySet.get(String(executor.namedArgumentList.find(x => x.name == 'set')?.value))?.qrList.map(qr => {
56 const icons = getExecutionIcons(qr);
57 const message = `${qr.automationId ? `[${qr.automationId}]` : ''}${icons ? `[auto: ${icons}]` : ''} ${qr.title || qr.message}`.trim();
58 return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr, null, ()=>qr.id.toString(), true);
59 }) ?? [],
60
50 /** All QRs as a set.name string, to be able to execute, for example via the /run command */61 /** All QRs as a set.name string, to be able to execute, for example via the /run command */
51 qrExecutables: () => {62 qrExecutables: () => {
52 const globalSetList = this.api.settings.config.setList;63 const globalSetList = this.api.settings.config.setList;
@@ -63,7 +74,7 @@ export class SlashCommandHandler {
63 ...otherQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `${x.qr.title || x.qr.message}`, enumTypes.qr, enumIcons.qr)),74 ...otherQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `${x.qr.title || x.qr.message}`, enumTypes.qr, enumIcons.qr)),
64 ];75 ];
65 },76 },
66 }77 };
6778
68 window['qrEnumProviderExecutables'] = localEnumProviders.qrExecutables;79 window['qrEnumProviderExecutables'] = localEnumProviders.qrExecutables;
6980
@@ -234,8 +245,20 @@ export class SlashCommandHandler {
234 name: 'label',245 name: 'label',
235 description: 'text on the button, e.g., label=MyButton',246 description: 'text on the button, e.g., label=MyButton',
236 typeList: [ARGUMENT_TYPE.STRING],247 typeList: [ARGUMENT_TYPE.STRING],
237 isRequired: true,248 isRequired: false,
238 enumProvider: localEnumProviders.qrLabels,249 enumProvider: localEnumProviders.qrEntries,
250 }),
251 SlashCommandNamedArgument.fromProps({
252 name: 'icon',
253 description: 'icon to show on the button, e.g., icon=fa-pencil',
254 typeList: [ARGUMENT_TYPE.STRING],
255 isRequired: false,
256 }),
257 SlashCommandNamedArgument.fromProps({
258 name: 'showlabel',
259 description: 'whether to show the label even when an icon is assigned, e.g., icon=fa-pencil showlabel=true',
260 typeList: [ARGUMENT_TYPE.BOOLEAN],
261 isRequired: false,
239 }),262 }),
240 new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),263 new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
241 new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),264 new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
@@ -247,6 +270,13 @@ export class SlashCommandHandler {
247 ];270 ];
248 const qrUpdateArgs = [271 const qrUpdateArgs = [
249 new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false),272 new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false),
273 SlashCommandNamedArgument.fromProps({
274 name: 'id',
275 description: 'numeric ID of the QR, e.g., id=42',
276 typeList: [ARGUMENT_TYPE.NUMBER],
277 isRequired: false,
278 enumProvider: localEnumProviders.qrIds,
279 }),
250 ];280 ];
251281
252 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',282 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',
@@ -272,13 +302,61 @@ export class SlashCommandHandler {
272 </div>302 </div>
273 `,303 `,
274 }));304 }));
305 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-get',
306 callback: (args, _) => {
307 return this.getQuickReply(args);
308 },
309 namedArgumentList: [
310 SlashCommandNamedArgument.fromProps({
311 name: 'set',
312 description: 'name of the QR set, e.g., set=PresetName1',
313 typeList: [ARGUMENT_TYPE.STRING],
314 isRequired: true,
315 enumProvider: localEnumProviders.qrSets,
316 }),
317 SlashCommandNamedArgument.fromProps({
318 name: 'label',
319 description: 'text on the button, e.g., label=MyButton',
320 typeList: [ARGUMENT_TYPE.STRING],
321 isRequired: false,
322 enumProvider: localEnumProviders.qrEntries,
323 }),
324 SlashCommandNamedArgument.fromProps({
325 name: 'id',
326 description: 'numeric ID of the QR, e.g., id=42',
327 typeList: [ARGUMENT_TYPE.NUMBER],
328 isRequired: false,
329 enumProvider: localEnumProviders.qrIds,
330 }),
331 ],
332 returns: 'a dictionary with all the QR\'s properties',
333 helpString: `
334 <div>Get a Quick Reply's properties.</div>
335 <div>
336 <strong>Examples:</strong>
337 <ul>
338 <li>
339 <pre><code>/qr-get set=MyPreset label=MyButton | /echo</code></pre>
340 <pre><code>/qr-get set=MyPreset id=42 | /echo</code></pre>
341 </li>
342 </ul>
343 </div>
344 `,
345 }));
275 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',346 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',
276 callback: (args, message) => {347 callback: (args, message) => {
277 this.updateQuickReply(args, message);348 this.updateQuickReply(args, message);
278 return '';349 return '';
279 },350 },
280 returns: 'updated quick reply',351 returns: 'updated quick reply',
281 namedArgumentList: [...qrUpdateArgs, ...qrArgs],352 namedArgumentList: [...qrUpdateArgs, ...qrArgs.map(it=>{
353 if (it.name == 'label') {
354 const clone = SlashCommandNamedArgument.fromProps(it);
355 clone.isRequired = false;
356 return clone;
357 }
358 return it;
359 })],
282 unnamedArgumentList: [360 unnamedArgumentList: [
283 new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING]),361 new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING]),
284 ],362 ],
@@ -315,6 +393,12 @@ export class SlashCommandHandler {
315 typeList: [ARGUMENT_TYPE.STRING],393 typeList: [ARGUMENT_TYPE.STRING],
316 enumProvider: localEnumProviders.qrEntries,394 enumProvider: localEnumProviders.qrEntries,
317 }),395 }),
396 SlashCommandNamedArgument.fromProps({
397 name: 'id',
398 description: 'numeric ID of the QR, e.g., id=42',
399 typeList: [ARGUMENT_TYPE.NUMBER],
400 enumProvider: localEnumProviders.qrIds,
401 }),
318 ],402 ],
319 unnamedArgumentList: [403 unnamedArgumentList: [
320 SlashCommandArgument.fromProps({404 SlashCommandArgument.fromProps({
@@ -344,6 +428,12 @@ export class SlashCommandHandler {
344 typeList: [ARGUMENT_TYPE.STRING],428 typeList: [ARGUMENT_TYPE.STRING],
345 enumProvider: localEnumProviders.qrEntries,429 enumProvider: localEnumProviders.qrEntries,
346 }),430 }),
431 SlashCommandNamedArgument.fromProps({
432 name: 'id',
433 description: 'numeric ID of the QR, e.g., id=42',
434 typeList: [ARGUMENT_TYPE.NUMBER],
435 enumProvider: localEnumProviders.qrIds,
436 }),
347 new SlashCommandNamedArgument(437 new SlashCommandNamedArgument(
348 'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',438 'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
349 ),439 ),
@@ -389,6 +479,12 @@ export class SlashCommandHandler {
389 typeList: [ARGUMENT_TYPE.STRING],479 typeList: [ARGUMENT_TYPE.STRING],
390 enumProvider: localEnumProviders.qrEntries,480 enumProvider: localEnumProviders.qrEntries,
391 }),481 }),
482 SlashCommandNamedArgument.fromProps({
483 name: 'id',
484 description: 'numeric ID of the QR, e.g., id=42',
485 typeList: [ARGUMENT_TYPE.NUMBER],
486 enumProvider: localEnumProviders.qrIds,
487 }),
392 ],488 ],
393 unnamedArgumentList: [489 unnamedArgumentList: [
394 SlashCommandArgument.fromProps({490 SlashCommandArgument.fromProps({
@@ -425,6 +521,12 @@ export class SlashCommandHandler {
425 isRequired: true,521 isRequired: true,
426 enumProvider: localEnumProviders.qrSets,522 enumProvider: localEnumProviders.qrSets,
427 }),523 }),
524 SlashCommandNamedArgument.fromProps({
525 name: 'id',
526 description: 'numeric ID of the QR, e.g., id=42',
527 typeList: [ARGUMENT_TYPE.NUMBER],
528 enumProvider: localEnumProviders.qrIds,
529 }),
428 ],530 ],
429 unnamedArgumentList: [531 unnamedArgumentList: [
430 SlashCommandArgument.fromProps({532 SlashCommandArgument.fromProps({
@@ -454,8 +556,8 @@ export class SlashCommandHandler {
454 new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false),556 new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false),
455 ];557 ];
456 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create',558 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create',
457 callback: (args, name) => {559 callback: async (args, name) => {
458 this.createSet(name, args);560 await this.createSet(name, args);
459 return '';561 return '';
460 },562 },
461 aliases: ['qr-presetadd'],563 aliases: ['qr-presetadd'],
@@ -485,8 +587,8 @@ export class SlashCommandHandler {
485 }));587 }));
486588
487 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',589 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',
488 callback: (args, name) => {590 callback: async (args, name) => {
489 this.updateSet(name, args);591 await this.updateSet(name, args);
490 return '';592 return '';
491 },593 },
492 aliases: ['qr-presetupdate'],594 aliases: ['qr-presetupdate'],
@@ -510,8 +612,8 @@ export class SlashCommandHandler {
510 `,612 `,
511 }));613 }));
512 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',614 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',
513 callback: (_, name) => {615 callback: async (_, name) => {
514 this.deleteSet(name);616 await this.deleteSet(name);
515 return '';617 return '';
516 },618 },
517 aliases: ['qr-presetdelete'],619 aliases: ['qr-presetdelete'],
@@ -533,6 +635,134 @@ export class SlashCommandHandler {
533 </div>635 </div>
534 `,636 `,
535 }));637 }));
638
639 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-arg',
640 callback: ({ _scope }, [key, value]) => {
641 _scope.setMacro(`arg::${key}`, value, key.includes('*'));
642 return '';
643 },
644 unnamedArgumentList: [
645 SlashCommandArgument.fromProps({ description: 'argument name',
646 typeList: ARGUMENT_TYPE.STRING,
647 isRequired: true,
648 }),
649 SlashCommandArgument.fromProps({ description: 'argument value',
650 typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY],
651 isRequired: true,
652 }),
653 ],
654 splitUnnamedArgument: true,
655 splitUnnamedArgumentCount: 2,
656 helpString: `
657 <div>
658 Set a fallback value for a Quick Reply argument.
659 </div>
660 <div>
661 <strong>Example:</strong>
662 <pre><code>/qr-arg x foo |\n/echo {{arg::x}}</code></pre>
663 </div>
664 `,
665 }));
666
667 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'import',
668 /**
669 *
670 * @param {{_scope:SlashCommandScope, _abortController:SlashCommandAbortController, _debugController:SlashCommandDebugController, from:string}} args
671 * @param {string} value
672 */
673 callback: (args, value) => {
674 if (!args.from) throw new Error('/import requires from= to be set.');
675 if (!value) throw new Error('/import requires the unnamed argument to be set.');
676 let qr = [...this.api.listGlobalSets(), ...this.api.listChatSets()]
677 .map(it=>this.api.getSetByName(it)?.qrList ?? [])
678 .flat()
679 .find(it=>it.label == args.from)
680 ;
681 if (!qr) {
682 let [setName, ...qrNameParts] = args.from.split('.');
683 let qrName = qrNameParts.join('.');
684 let qrs = QuickReplySet.get(setName);
685 if (qrs) {
686 qr = qrs.qrList.find(it=>it.label == qrName);
687 }
688 }
689 if (qr) {
690 const parser = new SlashCommandParser();
691 const closure = parser.parse(qr.message, true, [], args._abortController, args._debugController);
692 if (args._debugController) {
693 closure.source = args.from;
694 }
695 const testCandidates = (executor)=>{
696 return (
697 executor.namedArgumentList.find(arg=>arg.name == 'key')
698 && executor.unnamedArgumentList.length > 0
699 && executor.unnamedArgumentList[0].value instanceof SlashCommandClosure
700 ) || (
701 !executor.namedArgumentList.find(arg=>arg.name == 'key')
702 && executor.unnamedArgumentList.length > 1
703 && executor.unnamedArgumentList[1].value instanceof SlashCommandClosure
704 );
705 };
706 const candidates = closure.executorList
707 .filter(executor=>['let', 'var'].includes(executor.command.name))
708 .filter(testCandidates)
709 .map(executor=>({
710 key: executor.namedArgumentList.find(arg=>arg.name == 'key')?.value ?? executor.unnamedArgumentList[0].value,
711 value: executor.unnamedArgumentList[executor.namedArgumentList.find(arg=>arg.name == 'key') ? 0 : 1].value,
712 }))
713 ;
714 for (let i = 0; i < value.length; i++) {
715 const srcName = value[i];
716 let dstName = srcName;
717 if (i + 2 < value.length && value[i + 1] == 'as') {
718 dstName = value[i + 2];
719 i += 2;
720 }
721 const pick = candidates.find(it=>it.key == srcName);
722 if (!pick) throw new Error(`No scoped closure named "${srcName}" found in "${args.from}"`);
723 if (args._scope.existsVariableInScope(dstName)) {
724 args._scope.setVariable(dstName, pick.value);
725 } else {
726 args._scope.letVariable(dstName, pick.value);
727 }
728 }
729 } else {
730 throw new Error(`No Quick Reply found for "${name}".`);
731 }
732 return '';
733 },
734 namedArgumentList: [
735 SlashCommandNamedArgument.fromProps({ name: 'from',
736 description: 'Quick Reply to import from (QRSet.QRLabel)',
737 typeList: ARGUMENT_TYPE.STRING,
738 isRequired: true,
739 }),
740 ],
741 unnamedArgumentList: [
742 SlashCommandArgument.fromProps({ description: 'what to import (x or x as y)',
743 acceptsMultiple: true,
744 typeList: ARGUMENT_TYPE.STRING,
745 isRequired: true,
746 }),
747 ],
748 splitUnnamedArgument: true,
749 helpString: `
750 <div>
751 Import one or more closures from another Quick Reply.
752 </div>
753 <div>
754 Only imports closures that are directly assigned a scoped variable via <code>/let</code> or <code>/var</code>.
755 </div>
756 <div>
757 <strong>Examples:</strong>
758 <ul>
759 <li><pre><code>/import from=LibraryQrSet.FooBar foo |\n/:foo</code></pre></li>
760 <li><pre><code>/import from=LibraryQrSet.FooBar\n\tfoo\n\tbar\n|\n/:foo |\n/:bar</code></pre></li>
761 <li><pre><code>/import from=LibraryQrSet.FooBar\n\tfoo as x\n\tbar as y\n|\n/:x |\n/:y</code></pre></li>
762 </ul>
763 </div>
764 `,
765 }));
536 }766 }
537767
538768
@@ -618,6 +848,8 @@ export class SlashCommandHandler {
618 args.set ?? '',848 args.set ?? '',
619 args.label ?? '',849 args.label ?? '',
620 {850 {
851 icon: args.icon,
852 showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel),
621 message: message ?? '',853 message: message ?? '',
622 title: args.title,854 title: args.title,
623 isHidden: isTrueBoolean(args.hidden),855 isHidden: isTrueBoolean(args.hidden),
@@ -633,12 +865,21 @@ export class SlashCommandHandler {
633 toastr.error(ex.message);865 toastr.error(ex.message);
634 }866 }
635 }867 }
868 getQuickReply(args) {
869 try {
870 return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));
871 } catch (ex) {
872 toastr.error(ex.message);
873 }
874 }
636 updateQuickReply(args, message) {875 updateQuickReply(args, message) {
637 try {876 try {
638 this.api.updateQuickReply(877 this.api.updateQuickReply(
639 args.set ?? '',878 args.set ?? '',
640 args.label ?? '',879 args.id !== undefined ? Number(args.id) : (args.label ?? ''),
641 {880 {
881 icon: args.icon,
882 showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel),
642 newLabel: args.newlabel,883 newLabel: args.newlabel,
643 message: (message ?? '').trim().length > 0 ? message : undefined,884 message: (message ?? '').trim().length > 0 ? message : undefined,
644 title: args.title,885 title: args.title,
@@ -657,7 +898,7 @@ export class SlashCommandHandler {
657 }898 }
658 deleteQuickReply(args, label) {899 deleteQuickReply(args, label) {
659 try {900 try {
660 this.api.deleteQuickReply(args.set, args.label ?? label);901 this.api.deleteQuickReply(args.set, args.id !== undefined ? Number(args.id) : (args.label ?? label));
661 } catch (ex) {902 } catch (ex) {
662 toastr.error(ex.message);903 toastr.error(ex.message);
663 }904 }
@@ -692,9 +933,9 @@ export class SlashCommandHandler {
692 }933 }
693934
694935
695 createSet(name, args) {936 async createSet(name, args) {
696 try {937 try {
697 this.api.createSet(938 await this.api.createSet(
698 args.name ?? name ?? '',939 args.name ?? name ?? '',
699 {940 {
700 disableSend: isTrueBoolean(args.nosend),941 disableSend: isTrueBoolean(args.nosend),
@@ -706,9 +947,9 @@ export class SlashCommandHandler {
706 toastr.error(ex.message);947 toastr.error(ex.message);
707 }948 }
708 }949 }
709 updateSet(name, args) {950 async updateSet(name, args) {
710 try {951 try {
711 this.api.updateSet(952 await this.api.updateSet(
712 args.name ?? name ?? '',953 args.name ?? name ?? '',
713 {954 {
714 disableSend: args.nosend !== undefined ? isTrueBoolean(args.nosend) : undefined,955 disableSend: args.nosend !== undefined ? isTrueBoolean(args.nosend) : undefined,
@@ -720,9 +961,9 @@ export class SlashCommandHandler {
720 toastr.error(ex.message);961 toastr.error(ex.message);
721 }962 }
722 }963 }
723 deleteSet(name) {964 async deleteSet(name) {
724 try {965 try {
725 this.api.deleteSet(name ?? '');966 await this.api.deleteSet(name ?? '');
726 } catch (ex) {967 } catch (ex) {
727 toastr.error(ex.message);968 toastr.error(ex.message);
728 }969 }
public/scripts/extensions/quick-reply/src/ui/SettingsUi.js+54 -3
@@ -23,6 +23,8 @@ export class SettingsUi {
23 /**@type {HTMLInputElement}*/ disableSend;23 /**@type {HTMLInputElement}*/ disableSend;
24 /**@type {HTMLInputElement}*/ placeBeforeInput;24 /**@type {HTMLInputElement}*/ placeBeforeInput;
25 /**@type {HTMLInputElement}*/ injectInput;25 /**@type {HTMLInputElement}*/ injectInput;
26 /**@type {HTMLInputElement}*/ color;
27 /**@type {HTMLInputElement}*/ onlyBorderColor;
26 /**@type {HTMLSelectElement}*/ currentSet;28 /**@type {HTMLSelectElement}*/ currentSet;
2729
2830
@@ -117,10 +119,29 @@ export class SettingsUi {
117 this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{119 this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{
118 this.currentQrSet.addQuickReply();120 this.currentQrSet.addQuickReply();
119 });121 });
122 this.dom.querySelector('#qr--set-paste').addEventListener('click', async()=>{
123 const text = await navigator.clipboard.readText();
124 this.currentQrSet.addQuickReplyFromText(text);
125 });
126 this.dom.querySelector('#qr--set-importQr').addEventListener('click', async()=>{
127 const inp = document.createElement('input'); {
128 inp.type = 'file';
129 inp.accept = '.json';
130 inp.addEventListener('change', async()=>{
131 if (inp.files.length > 0) {
132 for (const file of inp.files) {
133 const text = await file.text();
134 this.currentQrSet.addQuickReply(JSON.parse(text));
135 }
136 }
137 });
138 inp.click();
139 }
140 });
120 this.qrList = this.dom.querySelector('#qr--set-qrList');141 this.qrList = this.dom.querySelector('#qr--set-qrList');
121 this.currentSet = this.dom.querySelector('#qr--set');142 this.currentSet = this.dom.querySelector('#qr--set');
122 this.currentSet.addEventListener('change', ()=>this.onQrSetChange());143 this.currentSet.addEventListener('change', ()=>this.onQrSetChange());
123 QuickReplySet.list.forEach(qrs=>{144 QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs=>{
124 const opt = document.createElement('option'); {145 const opt = document.createElement('option'); {
125 opt.value = qrs.name;146 opt.value = qrs.name;
126 opt.textContent = qrs.name;147 opt.textContent = qrs.name;
@@ -145,6 +166,34 @@ export class SettingsUi {
145 qrs.injectInput = this.injectInput.checked;166 qrs.injectInput = this.injectInput.checked;
146 qrs.save();167 qrs.save();
147 });168 });
169 let initialColorChange = true;
170 this.color = this.dom.querySelector('#qr--color');
171 this.color.color = this.currentQrSet?.color ?? 'transparent';
172 this.color.addEventListener('change', (evt)=>{
173 if (!this.dom.closest('body')) return;
174 const qrs = this.currentQrSet;
175 if (initialColorChange) {
176 initialColorChange = false;
177 this.color.color = qrs.color;
178 return;
179 }
180 qrs.color = evt.detail.rgb;
181 qrs.save();
182 this.currentQrSet.updateColor();
183 });
184 this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt)=>{
185 const qrs = this.currentQrSet;
186 this.color.color = 'transparent';
187 qrs.save();
188 this.currentQrSet.updateColor();
189 });
190 this.onlyBorderColor = this.dom.querySelector('#qr--onlyBorderColor');
191 this.onlyBorderColor.addEventListener('click', ()=>{
192 const qrs = this.currentQrSet;
193 qrs.onlyBorderColor = this.onlyBorderColor.checked;
194 qrs.save();
195 this.currentQrSet.updateColor();
196 });
148 this.onQrSetChange();197 this.onQrSetChange();
149 }198 }
150 onQrSetChange() {199 onQrSetChange() {
@@ -152,6 +201,8 @@ export class SettingsUi {
152 this.disableSend.checked = this.currentQrSet.disableSend;201 this.disableSend.checked = this.currentQrSet.disableSend;
153 this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;202 this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
154 this.injectInput.checked = this.currentQrSet.injectInput;203 this.injectInput.checked = this.currentQrSet.injectInput;
204 this.color.color = this.currentQrSet.color ?? 'transparent';
205 this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor;
155 this.qrList.innerHTML = '';206 this.qrList.innerHTML = '';
156 const qrsDom = this.currentQrSet.renderSettings();207 const qrsDom = this.currentQrSet.renderSettings();
157 this.qrList.append(qrsDom);208 this.qrList.append(qrsDom);
@@ -265,7 +316,7 @@ export class SettingsUi {
265 const qrs = new QuickReplySet();316 const qrs = new QuickReplySet();
266 qrs.name = name;317 qrs.name = name;
267 qrs.addQuickReply();318 qrs.addQuickReply();
268 const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(name) == 1);319 const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(name.toLowerCase()) == 1);
269 if (idx > -1) {320 if (idx > -1) {
270 QuickReplySet.list.splice(idx, 0, qrs);321 QuickReplySet.list.splice(idx, 0, qrs);
271 } else {322 } else {
@@ -321,7 +372,7 @@ export class SettingsUi {
321 this.prepareChatSetList();372 this.prepareChatSetList();
322 }373 }
323 } else {374 } else {
324 const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(qrs.name) == 1);375 const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1);
325 if (idx > -1) {376 if (idx > -1) {
326 QuickReplySet.list.splice(idx, 0, qrs);377 QuickReplySet.list.splice(idx, 0, qrs);
327 } else {378 } else {
public/scripts/extensions/quick-reply/src/ui/ctx/ContextMenu.js+5 -0
@@ -33,11 +33,14 @@ export class ContextMenu {
33 */33 */
34 build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) {34 build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) {
35 const tree = {35 const tree = {
36 icon: qr.icon,
37 showLabel: qr.showLabel,
36 label: qr.label,38 label: qr.label,
37 message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message,39 message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message,
38 children: [],40 children: [],
39 };41 };
40 qr.contextList.forEach((cl) => {42 qr.contextList.forEach((cl) => {
43 if (!cl.set) return;
41 if (!hierarchy.includes(cl.set)) {44 if (!hierarchy.includes(cl.set)) {
42 const nextHierarchy = [...hierarchy, cl.set];45 const nextHierarchy = [...hierarchy, cl.set];
43 const nextLabelHierarchy = [...labelHierarchy, tree.label];46 const nextLabelHierarchy = [...labelHierarchy, tree.label];
@@ -45,6 +48,8 @@ export class ContextMenu {
45 cl.set.qrList.forEach(subQr => {48 cl.set.qrList.forEach(subQr => {
46 const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy);49 const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy);
47 tree.children.push(new MenuItem(50 tree.children.push(new MenuItem(
51 subTree.icon,
52 subTree.showLabel,
48 subTree.label,53 subTree.label,
49 subTree.message,54 subTree.message,
50 (evt) => {55 (evt) => {
public/scripts/extensions/quick-reply/src/ui/ctx/MenuHeader.js+1 -1
@@ -2,7 +2,7 @@ import { MenuItem } from './MenuItem.js';
22
3export class MenuHeader extends MenuItem {3export class MenuHeader extends MenuItem {
4 constructor(/**@type {String}*/label) {4 constructor(/**@type {String}*/label) {
5 super(label, null, null);5 super(null, null, label, null, null);
6 }6 }
77
88
public/scripts/extensions/quick-reply/src/ui/ctx/MenuItem.js+34 -7
@@ -1,21 +1,34 @@
1import { SubMenu } from './SubMenu.js';1import { SubMenu } from './SubMenu.js';
22
3export class MenuItem {3export class MenuItem {
4 /**@type {String}*/ label;4 /**@type {string}*/ icon;
5 /**@type {Object}*/ value;5 /**@type {boolean}*/ showLabel;
6 /**@type {Function}*/ callback;6 /**@type {string}*/ label;
7 /**@type {object}*/ value;
8 /**@type {function}*/ callback;
7 /**@type {MenuItem[]}*/ childList = [];9 /**@type {MenuItem[]}*/ childList = [];
8 /**@type {SubMenu}*/ subMenu;10 /**@type {SubMenu}*/ subMenu;
9 /**@type {Boolean}*/ isForceExpanded = false;11 /**@type {boolean}*/ isForceExpanded = false;
1012
11 /**@type {HTMLElement}*/ root;13 /**@type {HTMLElement}*/ root;
1214
13 /**@type {Function}*/ onExpand;15 /**@type {function}*/ onExpand;
1416
1517
1618
1719
18 constructor(/**@type {String}*/label, /**@type {Object}*/value, /**@type {function}*/callback, /**@type {MenuItem[]}*/children = []) {20 /**
21 *
22 * @param {string} icon
23 * @param {boolean} showLabel
24 * @param {string} label
25 * @param {object} value
26 * @param {function} callback
27 * @param {MenuItem[]} children
28 */
29 constructor(icon, showLabel, label, value, callback, children = []) {
30 this.icon = icon;
31 this.showLabel = showLabel;
19 this.label = label;32 this.label = label;
20 this.value = value;33 this.value = value;
21 this.callback = callback;34 this.callback = callback;
@@ -33,7 +46,21 @@ export class MenuItem {
33 if (this.callback) {46 if (this.callback) {
34 item.addEventListener('click', (evt) => this.callback(evt, this));47 item.addEventListener('click', (evt) => this.callback(evt, this));
35 }48 }
36 item.append(this.label);49 const icon = document.createElement('div'); {
50 this.domIcon = icon;
51 icon.classList.add('qr--button-icon');
52 icon.classList.add('fa-solid');
53 if (!this.icon) icon.classList.add('qr--hidden');
54 else icon.classList.add(this.icon);
55 item.append(icon);
56 }
57 const lbl = document.createElement('div'); {
58 this.domLabel = lbl;
59 lbl.classList.add('qr--button-label');
60 if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden');
61 lbl.textContent = this.label;
62 item.append(lbl);
63 }
37 if (this.childList.length > 0) {64 if (this.childList.length > 0) {
38 item.classList.add('ctx-has-children');65 item.classList.add('ctx-has-children');
39 const sub = new SubMenu(this.childList);66 const sub = new SubMenu(this.childList);
public/scripts/extensions/quick-reply/style.css+601 -19
@@ -1,3 +1,20 @@
1@keyframes qr--success {
2 0%,
3 100% {
4 color: var(--SmartThemeBodyColor);
5 }
6 25%,
7 75% {
8 color: #51a351;
9 }
10}
11.qr--success {
12 animation-name: qr--success;
13 animation-duration: 3s;
14 animation-timing-function: linear;
15 animation-delay: 0s;
16 animation-iteration-count: 1;
17}
1#qr--bar {18#qr--bar {
2 outline: none;19 outline: none;
3 margin: 0;20 margin: 0;
@@ -41,6 +58,7 @@
41}58}
42#qr--bar > .qr--buttons,59#qr--bar > .qr--buttons,
43#qr--popout > .qr--body > .qr--buttons {60#qr--popout > .qr--body > .qr--buttons {
61 --qr--color: transparent;
44 margin: 0;62 margin: 0;
45 padding: 0;63 padding: 0;
46 display: flex;64 display: flex;
@@ -49,10 +67,44 @@
49 gap: 5px;67 gap: 5px;
50 width: 100%;68 width: 100%;
51}69}
70#qr--bar > .qr--buttons.qr--color,
71#qr--popout > .qr--body > .qr--buttons.qr--color {
72 background-color: var(--qr--color);
73}
74#qr--bar > .qr--buttons.qr--borderColor,
75#qr--popout > .qr--body > .qr--buttons.qr--borderColor {
76 background-color: transparent;
77 border-left: 5px solid var(--qr--color);
78 border-right: 5px solid var(--qr--color);
79}
80#qr--bar > .qr--buttons:has(.qr--buttons.qr--color),
81#qr--popout > .qr--body > .qr--buttons:has(.qr--buttons.qr--color) {
82 margin: 5px;
83}
52#qr--bar > .qr--buttons > .qr--buttons,84#qr--bar > .qr--buttons > .qr--buttons,
53#qr--popout > .qr--body > .qr--buttons > .qr--buttons {85#qr--popout > .qr--body > .qr--buttons > .qr--buttons {
54 display: contents;86 display: contents;
55}87}
88#qr--bar > .qr--buttons > .qr--buttons.qr--color .qr--button:before,
89#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color .qr--button:before {
90 content: '';
91 background-color: var(--qr--color);
92 position: absolute;
93 inset: -5px;
94 z-index: -1;
95}
96#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor .qr--button:before,
97#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor .qr--button:before {
98 display: none;
99}
100#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:before,
101#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:before,
102#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:after,
103#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:after {
104 content: '';
105 width: 5px;
106 background-color: var(--qr--color);
107}
56#qr--bar > .qr--buttons .qr--button,108#qr--bar > .qr--buttons .qr--button,
57#qr--popout > .qr--body > .qr--buttons .qr--button {109#qr--popout > .qr--body > .qr--buttons .qr--button {
58 color: var(--SmartThemeBodyColor);110 color: var(--SmartThemeBodyColor);
@@ -66,11 +118,19 @@
66 align-items: center;118 align-items: center;
67 justify-content: center;119 justify-content: center;
68 text-align: center;120 text-align: center;
121 position: relative;
69}122}
70#qr--bar > .qr--buttons .qr--button:hover,123#qr--bar > .qr--buttons .qr--button:hover,
71#qr--popout > .qr--body > .qr--buttons .qr--button:hover {124#qr--popout > .qr--body > .qr--buttons .qr--button:hover {
72 opacity: 1;125 background-color: #4d4d4d;
73 filter: brightness(1.2);126}
127#qr--bar > .qr--buttons .qr--button .qr--hidden,
128#qr--popout > .qr--body > .qr--buttons .qr--button .qr--hidden {
129 display: none;
130}
131#qr--bar > .qr--buttons .qr--button .qr--button-icon,
132#qr--popout > .qr--body > .qr--buttons .qr--button .qr--button-icon {
133 margin: 0 0.5em;
74}134}
75#qr--bar > .qr--buttons .qr--button > .qr--button-expander,135#qr--bar > .qr--buttons .qr--button > .qr--button-expander,
76#qr--popout > .qr--body > .qr--buttons .qr--button > .qr--button-expander {136#qr--popout > .qr--body > .qr--buttons .qr--button > .qr--button-expander {
@@ -170,36 +230,80 @@
170#qr--settings #qr--set-qrList .qr--set-qrListContents {230#qr--settings #qr--set-qrList .qr--set-qrListContents {
171 padding: 0 0.5em;231 padding: 0 0.5em;
172}232}
173#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item {233#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder {
234 display: flex;
235 align-items: center;
236 opacity: 0;
237 transition: 100ms;
238 margin: -2px 0 -11px 0;
239 position: relative;
240}
241#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions {
242 display: flex;
243 gap: 0.25em;
244 flex: 0 0 auto;
245}
246#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions .qr--action {
247 margin: 0;
248}
249#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:before,
250#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:after {
251 content: "";
252 display: block;
253 flex: 1 1 auto;
254 border: 1px solid;
255 margin: 0 1em;
256 height: 0;
257}
258#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:hover,
259#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:focus-within {
260 opacity: 1;
261}
262#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content {
174 display: flex;263 display: flex;
175 flex-direction: row;264 flex-direction: row;
176 gap: 0.5em;265 gap: 0.5em;
177 align-items: baseline;266 align-items: baseline;
178 padding: 0.25em 0;267 padding: 0.25em 0;
179}268}
180#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(1) {269#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) {
181 flex: 0 0 auto;270 flex: 0 0 auto;
182}271}
183#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(2) {272#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) {
184 flex: 1 1 25%;273 flex: 1 1 25%;
185}274}
186#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(3) {275#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(3) {
187 flex: 0 0 auto;276 flex: 0 0 auto;
188}277}
189#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(4) {278#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(4) {
190 flex: 1 1 75%;279 flex: 1 1 75%;
191}280}
192#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(5) {281#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(5) {
193 flex: 0 0 auto;282 flex: 0 1 auto;
283 display: flex;
284 gap: 0.25em;
285 justify-content: flex-end;
286 flex-wrap: wrap;
194}287}
195#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > .drag-handle {288#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > .drag-handle {
196 padding: 0.75em;289 padding: 0.75em;
197}290}
198#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabel,291#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer {
199#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action {292 display: flex;
293 align-items: center;
294 gap: 0.5em;
295}
296#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer .qr--set-itemIcon:not(.fa-solid) {
297 display: none;
298}
299#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer .qr--set-itemLabel {
300 min-width: 24px;
301}
302#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabel,
303#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--action {
200 margin: 0;304 margin: 0;
201}305}
202#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemMessage {306#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemMessage {
203 font-size: smaller;307 font-size: smaller;
204}308}
205#qr--settings .qr--set-qrListActions {309#qr--settings .qr--set-qrListActions {
@@ -212,6 +316,7 @@
212#qr--qrOptions {316#qr--qrOptions {
213 display: flex;317 display: flex;
214 flex-direction: column;318 flex-direction: column;
319 padding-right: 1px;
215}320}
216#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {321#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {
217 display: flex;322 display: flex;
@@ -219,6 +324,12 @@
219 gap: 0.5em;324 gap: 0.5em;
220 align-items: baseline;325 align-items: baseline;
221}326}
327#qr--qrOptions > #qr--autoExec .checkbox_label {
328 text-wrap: nowrap;
329}
330#qr--qrOptions > #qr--autoExec .checkbox_label .fa-fw {
331 margin-right: 2px;
332}
222@media screen and (max-width: 750px) {333@media screen and (max-width: 750px) {
223 body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor {334 body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor {
224 flex-direction: column;335 flex-direction: column;
@@ -238,6 +349,72 @@
238.popup:has(#qr--modalEditor) {349.popup:has(#qr--modalEditor) {
239 aspect-ratio: unset;350 aspect-ratio: unset;
240}351}
352.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) {
353 min-width: unset;
354 min-height: unset;
355 height: auto !important;
356 width: min-content !important;
357 position: absolute;
358 right: 1em;
359 top: 1em;
360 left: unset;
361 bottom: unset;
362 margin: unset;
363 padding: 0;
364}
365.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized)::backdrop {
366 backdrop-filter: unset;
367 background-color: transparent;
368}
369.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-body {
370 flex: 0 0 auto;
371 height: min-content;
372 width: min-content;
373}
374.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content {
375 flex: 0 0 auto;
376 margin-top: 0;
377}
378.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor {
379 max-height: 50vh;
380}
381.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--main,
382.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--resizeHandle,
383.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > h3,
384.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > #qr--modal-executeButtons,
385.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > #qr--modal-executeProgress {
386 display: none;
387}
388.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--qrOptions {
389 width: auto;
390}
391.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
392 display: flex;
393}
394.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-minimize {
395 display: none;
396}
397.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugState {
398 padding-top: 0;
399}
400.popup:has(#qr--modalEditor):has(.qr--isExecuting) .popup-controls {
401 display: none;
402}
403.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight {
404 position: absolute;
405 z-index: 50000;
406 pointer-events: none;
407 background-color: rgba(47, 150, 180, 0.5);
408}
409.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight.qr--unresolved {
410 background-color: rgba(255, 255, 0, 0.5);
411}
412.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight-secondary {
413 position: absolute;
414 z-index: 50000;
415 pointer-events: none;
416 border: 3px solid red;
417}
241.popup:has(#qr--modalEditor) .popup-content {418.popup:has(#qr--modalEditor) .popup-content {
242 display: flex;419 display: flex;
243 flex-direction: column;420 flex-direction: column;
@@ -249,6 +426,67 @@
249 gap: 1em;426 gap: 1em;
250 overflow: hidden;427 overflow: hidden;
251}428}
429.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > h3:first-child,
430.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--labels,
431.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings,
432.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > h3:first-child,
433.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > #qr--ctxEditor,
434.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions,
435.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions + h3,
436.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions + h3 + div {
437 display: none;
438}
439.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
440 visibility: hidden;
441}
442.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--modal-debugButtons {
443 display: flex;
444}
445.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
446 cursor: not-allowed;
447 opacity: 0.5;
448 pointer-events: none;
449 transition: 200ms;
450 border-color: transparent;
451}
452.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
453 cursor: pointer;
454 opacity: 1;
455 pointer-events: all;
456}
457.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-resume {
458 animation-name: qr--debugPulse;
459 animation-duration: 1500ms;
460 animation-timing-function: ease-in-out;
461 animation-delay: 0s;
462 animation-iteration-count: infinite;
463}
464.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-resume {
465 border-color: #51a351;
466}
467.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-step {
468 border-color: var(--SmartThemeQuoteColor);
469}
470.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-stepInto {
471 border-color: var(--SmartThemeQuoteColor);
472}
473.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-stepOut {
474 border-color: var(--SmartThemeQuoteColor);
475}
476.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle {
477 width: 6px;
478 background-color: var(--SmartThemeBorderColor);
479 border: 2px solid var(--SmartThemeBlurTintColor);
480 transition: border-color 200ms, background-color 200ms;
481 cursor: w-resize;
482}
483.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle:hover {
484 background-color: var(--SmartThemeQuoteColor);
485 border-color: var(--SmartThemeQuoteColor);
486}
487.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions {
488 width: var(--width, auto);
489}
252.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main {490.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main {
253 flex: 1 1 auto;491 flex: 1 1 auto;
254 display: flex;492 display: flex;
@@ -260,21 +498,115 @@
260 display: flex;498 display: flex;
261 flex-direction: row;499 flex-direction: row;
262 gap: 0.5em;500 gap: 0.5em;
501 padding: 1px;
263}502}
264.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label {503.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label,
504.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label {
265 flex: 1 1 1px;505 flex: 1 1 1px;
266 display: flex;506 display: flex;
267 flex-direction: column;507 flex-direction: column;
508 position: relative;
509}
510.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label.qr--fit,
511.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label.qr--fit {
512 flex: 0 0 auto;
513 justify-content: center;
514}
515.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--inputGroup,
516.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--inputGroup {
517 display: flex;
518 align-items: baseline;
519 gap: 0.5em;
520}
521.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--inputGroup input,
522.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--inputGroup input {
523 flex: 1 1 auto;
268}524}
269.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText {525.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--labelText,
526.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--labelText {
270 flex: 1 1 auto;527 flex: 1 1 auto;
271}528}
272.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint {529.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--labelHint,
530.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--labelHint {
273 flex: 1 1 auto;531 flex: 1 1 auto;
274}532}
275.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > input {533.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label input,
534.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label input {
276 flex: 0 0 auto;535 flex: 0 0 auto;
277}536}
537.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList,
538.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList {
539 background-color: var(--stcdx--bgColor);
540 border: 1px solid var(--SmartThemeBorderColor);
541 backdrop-filter: blur(var(--SmartThemeBlurStrength));
542 border-radius: 10px;
543 font-size: smaller;
544 position: absolute;
545 top: 100%;
546 left: 0;
547 right: 0;
548 overflow: auto;
549 margin: 0;
550 padding: 0.5em;
551 max-height: 50vh;
552 list-style: none;
553 z-index: 40000;
554 max-width: 100%;
555}
556.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem,
557.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem {
558 display: flex;
559 gap: 1em;
560 text-align: left;
561 opacity: 0.75;
562 transition: 200ms;
563 cursor: pointer;
564}
565.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem:hover,
566.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem:hover {
567 opacity: 1;
568}
569.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current,
570.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current {
571 opacity: 1;
572}
573.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--label,
574.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--label,
575.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--id,
576.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--id {
577 font-weight: bold;
578}
579.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--label,
580.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--label {
581 white-space: nowrap;
582}
583.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--label .menu_button,
584.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--label .menu_button {
585 display: inline-block;
586 height: min-content;
587 width: min-content;
588 margin: 0 0.5em 0 0;
589}
590.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id,
591.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id {
592 opacity: 0.5;
593}
594.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id:before,
595.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id:before {
596 content: "[";
597}
598.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id:after,
599.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id:after {
600 content: "]";
601}
602.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--message,
603.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--message {
604 height: 1lh;
605 overflow: hidden;
606 text-overflow: ellipsis;
607 white-space: nowrap;
608 opacity: 0.5;
609}
278.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {610.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {
279 flex: 1 1 auto;611 flex: 1 1 auto;
280 display: flex;612 display: flex;
@@ -283,8 +615,9 @@
283}615}
284.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {616.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {
285 display: flex;617 display: flex;
618 flex-wrap: wrap;
286 flex-direction: row;619 flex-direction: row;
287 gap: 1em;620 column-gap: 1em;
288 color: var(--grey70);621 color: var(--grey70);
289 font-size: smaller;622 font-size: smaller;
290 align-items: baseline;623 align-items: baseline;
@@ -308,6 +641,11 @@
308 background-color: var(--ac-style-color-background);641 background-color: var(--ac-style-color-background);
309 color: var(--ac-style-color-text);642 color: var(--ac-style-color-text);
310}643}
644.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::-webkit-scrollbar,
645.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::-webkit-scrollbar-thumb {
646 visibility: visible;
647 cursor: unset;
648}
311.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {649.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {
312 color: unset;650 color: unset;
313 background-color: rgba(108 171 251 / 0.25);651 background-color: rgba(108 171 251 / 0.25);
@@ -357,11 +695,15 @@
357 font-family: var(--monoFontFamily);695 font-family: var(--monoFontFamily);
358 padding: 0.75em;696 padding: 0.75em;
359 margin: 0;697 margin: 0;
360 border: none;
361 resize: none;698 resize: none;
362 line-height: 1.2;699 line-height: 1.2;
363 border: 1px solid var(--SmartThemeBorderColor);700 border: 1px solid var(--SmartThemeBorderColor);
364 border-radius: 5px;701 border-radius: 5px;
702 position: relative;
703}
704.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-icon {
705 height: 100%;
706 aspect-ratio: 1 / 1;
365}707}
366.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons {708.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons {
367 display: flex;709 display: flex;
@@ -410,6 +752,46 @@
410.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {752.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
411 border-color: #d78872;753 border-color: #d78872;
412}754}
755.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons {
756 display: none;
757 gap: 1em;
758}
759.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton {
760 aspect-ratio: 1.25 / 1;
761 width: 2.25em;
762 position: relative;
763}
764.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:not(.fa-solid) {
765 border-width: 1px;
766 border-style: solid;
767}
768.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:not(.fa-solid):after {
769 content: '';
770 position: absolute;
771 inset: 3px;
772 background-color: var(--SmartThemeBodyColor);
773 mask-size: contain;
774 mask-position: center;
775 mask-repeat: no-repeat;
776}
777.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-resume:after {
778 mask-image: url('/img/step-resume.svg');
779}
780.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-step:after {
781 mask-image: url('/img/step-over.svg');
782}
783.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepInto:after {
784 mask-image: url('/img/step-into.svg');
785}
786.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepOut:after {
787 mask-image: url('/img/step-out.svg');
788}
789.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
790 display: none;
791}
792.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-send_textarea {
793 flex: 0 0 auto;
794}
413.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress {795.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress {
414 --prog: 0;796 --prog: 0;
415 --progColor: #92befc;797 --progColor: #92befc;
@@ -417,6 +799,7 @@
417 --progSuccessColor: #51a351;799 --progSuccessColor: #51a351;
418 --progErrorColor: #bd362f;800 --progErrorColor: #bd362f;
419 --progAbortedColor: #d78872;801 --progAbortedColor: #d78872;
802 flex: 0 0 auto;
420 height: 0.5em;803 height: 0.5em;
421 background-color: var(--black50a);804 background-color: var(--black50a);
422 position: relative;805 position: relative;
@@ -469,6 +852,7 @@
469 overflow: auto;852 overflow: auto;
470 min-width: 100%;853 min-width: 100%;
471 width: 0;854 width: 0;
855 white-space: pre-wrap;
472}856}
473.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult.qr--hasResult {857.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult.qr--hasResult {
474 display: block;858 display: block;
@@ -476,6 +860,150 @@
476.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult:before {860.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult:before {
477 content: 'Result: ';861 content: 'Result: ';
478}862}
863.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState {
864 display: none;
865 text-align: left;
866 font-size: smaller;
867 font-family: var(--monoFontFamily);
868 color: white;
869 padding: 0.5em 0;
870 overflow: auto;
871 min-width: 100%;
872 width: 0;
873 white-space: pre-wrap;
874}
875.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState.qr--active {
876 display: block;
877}
878.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope {
879 display: grid;
880 grid-template-columns: 0fr 1fr 1fr;
881 column-gap: 0em;
882}
883.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--title {
884 grid-column: 1 / 4;
885 font-weight: bold;
886 font-family: var(--mainFontFamily);
887 background-color: var(--black50a);
888 padding: 0.25em;
889 margin-top: 0.5em;
890}
891.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var,
892.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro,
893.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe {
894 display: contents;
895}
896.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--key,
897.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--key,
898.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--key,
899.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val,
900.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val,
901.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val {
902 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
903}
904.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val:nth-child(2n),
905.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val:nth-child(2n),
906.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val:nth-child(2n) {
907 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.125);
908}
909.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val:hover,
910.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val:hover,
911.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val:hover {
912 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
913}
914.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n) .qr--val:nth-child(2n),
915.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n) .qr--val:nth-child(2n),
916.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n) .qr--val:nth-child(2n) {
917 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.0625);
918}
919.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n) .qr--val:hover,
920.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n) .qr--val:hover,
921.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n) .qr--val:hover {
922 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
923}
924.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--key,
925.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--key,
926.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--key,
927.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--val,
928.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--val,
929.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--val {
930 opacity: 0.5;
931}
932.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val,
933.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val,
934.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val {
935 grid-column: 2 / 4;
936}
937.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--singleCol,
938.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--singleCol,
939.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--singleCol {
940 grid-column: unset;
941}
942.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--simple:before,
943.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--simple:before,
944.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--simple:before,
945.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--simple:after,
946.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--simple:after,
947.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--simple:after {
948 content: '"';
949 color: var(--SmartThemeQuoteColor);
950}
951.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--unresolved:after,
952.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--unresolved:after,
953.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--unresolved:after {
954 content: '-UNRESOLVED-';
955 font-style: italic;
956 color: var(--SmartThemeQuoteColor);
957}
958.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key {
959 margin-left: 0.5em;
960 padding-right: 1em;
961}
962.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key:after {
963 content: ": ";
964}
965.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe > .qr--key:before,
966.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro > .qr--key:before {
967 content: "{{";
968}
969.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe > .qr--key:after,
970.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro > .qr--key:after {
971 content: "}}: ";
972}
973.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope {
974 display: contents;
975}
976.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--key,
977.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--val {
978 opacity: 0.5;
979}
980.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack {
981 display: grid;
982 grid-template-columns: 1fr 0fr;
983}
984.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--title {
985 grid-column: 1 / 3;
986 font-weight: bold;
987 font-family: var(--mainFontFamily);
988 background-color: var(--black50a);
989 padding: 0.25em;
990 margin-top: 1em;
991}
992.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item {
993 display: contents;
994}
995.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) .qr--name,
996.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) .qr--source {
997 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
998}
999.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--name {
1000 margin-left: 0.5em;
1001}
1002.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--source {
1003 opacity: 0.5;
1004 text-align: right;
1005 white-space: nowrap;
1006}
479@keyframes qr--progressPulse {1007@keyframes qr--progressPulse {
480 0%,1008 0%,
481 100% {1009 100% {
@@ -485,9 +1013,63 @@
485 background-color: var(--progFlashColor);1013 background-color: var(--progFlashColor);
486 }1014 }
487}1015}
1016@keyframes qr--debugPulse {
1017 0%,
1018 100% {
1019 border-color: #51a351;
1020 }
1021 50% {
1022 border-color: #92befc;
1023 }
1024}
488.popup.qr--hide {1025.popup.qr--hide {
489 opacity: 0 !important;1026 opacity: 0 !important;
490}1027}
491.popup.qr--hide::backdrop {1028.popup.qr--hide::backdrop {
492 opacity: 0 !important;1029 opacity: 0 !important;
493}1030}
1031.popup.qr--hide::backdrop {
1032 opacity: 0 !important;
1033}
1034.popup:has(.qr--transferModal) .popup-button-ok {
1035 display: flex;
1036 align-items: center;
1037 flex-direction: column;
1038 white-space: pre;
1039 font-weight: normal;
1040 box-shadow: 0 0 0;
1041 transition: 200ms;
1042}
1043.popup:has(.qr--transferModal) .popup-button-ok:after {
1044 content: 'Transfer';
1045 height: 0;
1046 overflow: hidden;
1047 font-weight: bold;
1048}
1049.popup:has(.qr--transferModal) .qr--copy {
1050 display: flex;
1051 align-items: center;
1052 flex-direction: column;
1053 white-space: pre;
1054 font-weight: normal;
1055 box-shadow: 0 0 0;
1056 transition: 200ms;
1057}
1058.popup:has(.qr--transferModal) .qr--copy:after {
1059 content: 'Copy';
1060 height: 0;
1061 overflow: hidden;
1062 font-weight: bold;
1063}
1064.popup:has(.qr--transferModal):has(.qr--transferSelect:focus) .popup-button-ok {
1065 font-weight: bold;
1066 box-shadow: 0 0 10px;
1067}
1068.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .popup-button-ok {
1069 font-weight: normal;
1070 box-shadow: 0 0 0;
1071}
1072.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .qr--copy {
1073 font-weight: bold;
1074 box-shadow: 0 0 10px;
1075}
public/scripts/extensions/quick-reply/style.less+663 -125
@@ -1,3 +1,18 @@
1@keyframes qr--success {
2 0%, 100% {
3 color: var(--SmartThemeBodyColor);
4 }
5 25%, 75% {
6 color: rgb(81, 163, 81);
7 }
8}
9&.qr--success {
10 animation-name: qr--success;
11 animation-duration: 3s;
12 animation-timing-function: linear;
13 animation-delay: 0s;
14 animation-iteration-count: 1;
15}
1#qr--bar {16#qr--bar {
2 outline: none;17 outline: none;
3 margin: 0;18 margin: 0;
@@ -50,6 +65,18 @@
50#qr--bar,65#qr--bar,
51#qr--popout>.qr--body {66#qr--popout>.qr--body {
52 >.qr--buttons {67 >.qr--buttons {
68 --qr--color: transparent;
69 &.qr--color {
70 background-color: var(--qr--color);
71 }
72 &.qr--borderColor {
73 background-color: transparent;
74 border-left: 5px solid var(--qr--color);
75 border-right: 5px solid var(--qr--color);
76 }
77 &:has(.qr--buttons.qr--color) {
78 margin: 5px;
79 }
53 margin: 0;80 margin: 0;
54 padding: 0;81 padding: 0;
55 display: flex;82 display: flex;
@@ -60,6 +87,25 @@
6087
61 >.qr--buttons {88 >.qr--buttons {
62 display: contents;89 display: contents;
90 &.qr--color {
91 .qr--button:before {
92 content: '';
93 background-color: var(--qr--color);
94 position: absolute;
95 inset: -5px;
96 z-index: -1;
97 }
98 &.qr--borderColor {
99 .qr--button:before {
100 display: none;
101 }
102 &:before, &:after {
103 content: '';
104 width: 5px;
105 background-color: var(--qr--color);
106 }
107 }
108 }
63 }109 }
64110
65 .qr--button {111 .qr--button {
@@ -75,10 +121,17 @@
75 align-items: center;121 align-items: center;
76 justify-content: center;122 justify-content: center;
77 text-align: center;123 text-align: center;
124 position: relative;
78125
79 &:hover {126 &:hover {
80 opacity: 1;127 background-color: rgb(30% 30% 30%);
81 filter: brightness(1.2);128 }
129
130 .qr--hidden {
131 display: none;
132 }
133 .qr--button-icon {
134 margin: 0 0.5em;
82 }135 }
83136
84 >.qr--button-expander {137 >.qr--button-expander {
@@ -211,14 +264,41 @@
211 .qr--set-qrListContents> {264 .qr--set-qrListContents> {
212 padding: 0 0.5em;265 padding: 0 0.5em;
213266
214 >.qr--set-item {267 >.qr--set-item .qr--set-itemAdder {
268 display: flex;
269 align-items: center;
270 opacity: 0;
271 transition: 100ms;
272 margin: -2px 0 -11px 0;
273 position: relative;
274 .qr--actions {
275 display: flex;
276 gap: 0.25em;
277 flex: 0 0 auto;
278 .qr--action {
279 margin: 0;
280 }
281 }
282 &:before, &:after {
283 content: "";
284 display: block;
285 flex: 1 1 auto;
286 border: 1px solid;
287 margin: 0 1em;
288 height: 0;
289 }
290 &:hover, &:focus-within {
291 opacity: 1;
292 }
293 }
294 >.qr--set-item .qr--content {
215 display: flex;295 display: flex;
216 flex-direction: row;296 flex-direction: row;
217 gap: 0.5em;297 gap: 0.5em;
218 align-items: baseline;298 align-items: baseline;
219 padding: 0.25em 0;299 padding: 0.25em 0;
220300
221 > :nth-child(1) {301 > :nth-child(2) {
222 flex: 0 0 auto;302 flex: 0 0 auto;
223 }303 }
224304
@@ -235,13 +315,29 @@
235 }315 }
236316
237 > :nth-child(5) {317 > :nth-child(5) {
238 flex: 0 0 auto;318 flex: 0 1 auto;
319 display: flex;
320 gap: 0.25em;
321 justify-content: flex-end;
322 flex-wrap: wrap;
239 }323 }
240324
241 >.drag-handle {325 >.drag-handle {
242 padding: 0.75em;326 padding: 0.75em;
243 }327 }
244328
329 .qr--set-itemLabelContainer {
330 display: flex;
331 align-items: center;
332 gap: 0.5em;
333 .qr--set-itemIcon:not(.fa-solid) {
334 display: none;
335 }
336 .qr--set-itemLabel {
337 min-width: 24px;
338 }
339 }
340
245 .qr--set-itemLabel,341 .qr--set-itemLabel,
246 .qr--action {342 .qr--action {
247 margin: 0;343 margin: 0;
@@ -251,6 +347,8 @@
251 font-size: smaller;347 font-size: smaller;
252 }348 }
253 }349 }
350
351
254 }352 }
255 }353 }
256354
@@ -270,6 +368,7 @@
270#qr--qrOptions {368#qr--qrOptions {
271 display: flex;369 display: flex;
272 flex-direction: column;370 flex-direction: column;
371 padding-right: 1px;
273372
274 >#qr--ctxEditor {373 >#qr--ctxEditor {
275 .qr--ctxItem {374 .qr--ctxItem {
@@ -279,6 +378,15 @@
279 align-items: baseline;378 align-items: baseline;
280 }379 }
281 }380 }
381 >#qr--autoExec {
382 .checkbox_label {
383 text-wrap: nowrap;
384
385 .fa-fw {
386 margin-right: 2px;
387 }
388 }
389 }
282}390}
283391
284392
@@ -306,6 +414,78 @@
306.popup:has(#qr--modalEditor) {414.popup:has(#qr--modalEditor) {
307 aspect-ratio: unset;415 aspect-ratio: unset;
308416
417 &:has(.qr--isExecuting.qr--minimized) {
418 min-width: unset;
419 min-height: unset;
420 height: auto !important;
421 width: min-content !important;
422 position: absolute;
423 right: 1em;
424 top: 1em;
425 left: unset;
426 bottom: unset;
427 margin: unset;
428 padding: 0;
429 &::backdrop {
430 backdrop-filter: unset;
431 background-color: transparent;
432 }
433 .popup-body {
434 flex: 0 0 auto;
435 height: min-content;
436 width: min-content;
437 }
438 .popup-content {
439 flex: 0 0 auto;
440 margin-top: 0;
441
442 > #qr--modalEditor {
443 max-height: 50vh;
444 > #qr--main,
445 > #qr--resizeHandle,
446 > #qr--qrOptions > h3,
447 > #qr--qrOptions > #qr--modal-executeButtons,
448 > #qr--qrOptions > #qr--modal-executeProgress
449 {
450 display: none;
451 }
452 #qr--qrOptions {
453 width: auto;
454 }
455 #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
456 display: flex;
457 }
458 #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-minimize {
459 display: none;
460 }
461 #qr--modal-debugState {
462 padding-top: 0;
463 }
464 }
465 }
466 }
467 &:has(.qr--isExecuting) {
468 .popup-controls {
469 display: none;
470 }
471
472 .qr--highlight {
473 position: absolute;
474 z-index: 50000;
475 pointer-events: none;
476 background-color: rgb(47 150 180 / 0.5);
477 &.qr--unresolved {
478 background-color: rgb(255 255 0 / 0.5);
479 }
480 }
481 .qr--highlight-secondary {
482 position: absolute;
483 z-index: 50000;
484 pointer-events: none;
485 border: 3px solid red;
486 }
487 }
488
309 .popup-content {489 .popup-content {
310 display: flex;490 display: flex;
311 flex-direction: column;491 flex-direction: column;
@@ -317,140 +497,262 @@
317 gap: 1em;497 gap: 1em;
318 overflow: hidden;498 overflow: hidden;
319499
320 >#qr--main {500 &.qr--isExecuting {
321 flex: 1 1 auto;501 #qr--main > h3:first-child,
322 display: flex;502 #qr--main > .qr--labels,
323 flex-direction: column;503 #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings,
324 overflow: hidden;504 #qr--qrOptions > h3:first-child,
325505 #qr--qrOptions > #qr--ctxEditor,
326 >.qr--labels {506 #qr--qrOptions > .qr--ctxEditorActions,
327 flex: 0 0 auto;507 #qr--qrOptions > .qr--ctxEditorActions + h3,
328 display: flex;508 #qr--qrOptions > .qr--ctxEditorActions + h3 + div
329 flex-direction: row;509 {
330 gap: 0.5em;510 display: none;
331511 }
332 >label {512 #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
333 flex: 1 1 1px;513 visibility: hidden;
334 display: flex;514 }
335 flex-direction: column;515 #qr--modal-debugButtons {
336516 display: flex;
337 >.qr--labelText {517 .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
338 flex: 1 1 auto;518 cursor: not-allowed;
519 opacity: 0.5;
520 pointer-events: none;
521 transition: 200ms;
522 border-color: transparent;
523 }
524 }
525 &.qr--isPaused #qr--modal-debugButtons {
526 .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
527 cursor: pointer;
528 opacity: 1;
529 pointer-events: all;
530 &#qr--modal-resume {
531 animation-name: qr--debugPulse;
532 animation-duration: 1500ms;
533 animation-timing-function: ease-in-out;
534 animation-delay: 0s;
535 animation-iteration-count: infinite;
339 }536 }
340537 &#qr--modal-resume {
341 >.qr--labelHint {538 border-color: rgb(81, 163, 81);
342 flex: 1 1 auto;
343 }539 }
344540 &#qr--modal-step {
345 >input {541 border-color: var(--SmartThemeQuoteColor);
346 flex: 0 0 auto;542 }
543 &#qr--modal-stepInto {
544 border-color: var(--SmartThemeQuoteColor);
545 }
546 &#qr--modal-stepOut {
547 border-color: var(--SmartThemeQuoteColor);
347 }548 }
348 }549 }
349 }550 }
350551 #qr--resizeHandle {
351 >.qr--modal-messageContainer {552 width: 6px;
352 flex: 1 1 auto;553 background-color: var(--SmartThemeBorderColor);
353 display: flex;554 border: 2px solid var(--SmartThemeBlurTintColor);
354 flex-direction: column;555 transition: border-color 200ms, background-color 200ms;
355 overflow: hidden;556 cursor: w-resize;
356557 &:hover {
357 >.qr--modal-editorSettings {558 background-color: var(--SmartThemeQuoteColor);
358 display: flex;559 border-color: var(--SmartThemeQuoteColor);
359 flex-direction: row;560 }
360 gap: 1em;561 }
361 color: var(--grey70);562 #qr--qrOptions {
362 font-size: smaller;563 width: var(--width, auto);
363 align-items: baseline;564 }
364565 }
365 >.checkbox_label {566
366 white-space: nowrap;567 > #qr--main {
367568 flex: 1 1 auto;
368 >input {569 display: flex;
369 font-size: inherit;570 flex-direction: column;
370 }571 overflow: hidden;
572 > .qr--labels {
573 flex: 0 0 auto;
574 display: flex;
575 flex-direction: row;
576 gap: 0.5em;
577 padding: 1px;
578 > label, > .label {
579 flex: 1 1 1px;
580 display: flex;
581 flex-direction: column;
582 position: relative;
583 &.qr--fit {
584 flex: 0 0 auto;
585 justify-content: center;
371 }586 }
372 }587 .qr--inputGroup {
373588 display: flex;
374 >#qr--modal-messageHolder {589 align-items: baseline;
375 flex: 1 1 auto;590 gap: 0.5em;
376 display: grid;591 input {
377 text-align: left;592 flex: 1 1 auto;
378 overflow: hidden;
379
380 &.qr--noSyntax {
381 >#qr--modal-messageSyntax {
382 display: none;
383 }593 }
384594 }
385 >#qr--modal-message {595 .qr--labelText {
386 background-color: var(--ac-style-color-background);596 flex: 1 1 auto;
387 color: var(--ac-style-color-text);597 }
388598 .qr--labelHint {
389 &::selection {599 flex: 1 1 auto;
390 color: unset;600 }
391 background-color: rgba(108 171 251 / 0.25);601 input {
392602 flex: 0 0 auto;
393 @supports (color: rgb(from white r g b / 0.25)) {603 }
394 background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);604 .qr--modal-switcherList {
605 background-color: var(--stcdx--bgColor);
606 border: 1px solid var(--SmartThemeBorderColor);
607 backdrop-filter: blur(var(--SmartThemeBlurStrength));
608 border-radius: 10px;
609 font-size: smaller;
610 position: absolute;
611 top: 100%;
612 left: 0;
613 right: 0;
614 overflow: auto;
615 margin: 0;
616 padding: 0.5em;
617 max-height: 50vh;
618 list-style: none;
619 z-index: 40000;
620 max-width: 100%;
621 .qr--modal-switcherItem {
622 display: flex;
623 gap: 1em;
624 text-align: left;
625 opacity: 0.75;
626 transition: 200ms;
627 cursor: pointer;
628 &:hover {
629 opacity: 1;
630 }
631 &.qr--current {
632 opacity: 1;
633 .qr--label, .qr--id {
634 font-weight: bold;
395 }635 }
396 }636 }
397 }637 }
398 }638 .qr--label {
399639 white-space: nowrap;
400 >#qr--modal-messageSyntax {640 .menu_button {
401 grid-column: 1;641 display: inline-block;
402 grid-row: 1;642 height: min-content;
403 padding: 0;643 width: min-content;
404 margin: 0;644 margin: 0 0.5em 0 0;
405 border: none;645 }
406 overflow: hidden;
407 min-width: 100%;
408 width: 0;
409
410 >#qr--modal-messageSyntaxInner {
411 height: 100%;
412 }646 }
413 }647 .qr--id {
414648 opacity: 0.5;
415 >#qr--modal-message {649 &:before { content: "["; }
416 background-color: transparent;650 &:after { content: "]"; }
417 color: transparent;
418 grid-column: 1;
419 grid-row: 1;
420 caret-color: var(--ac-style-color-text);
421 overflow: auto;
422
423 &::-webkit-scrollbar,
424 &::-webkit-scrollbar-thumb {
425 visibility: hidden;
426 cursor: default;
427 }651 }
428652 .qr--message {
429 &::selection {653 height: 1lh;
430 color: transparent;654 overflow: hidden;
431 background-color: rgba(108 171 251 / 0.25);655 text-overflow: ellipsis;
432656 white-space: nowrap;
433 @supports (color: rgb(from white r g b / 0.25)) {657 opacity: 0.5;
434 background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
435 }
436 }658 }
437 }659 }
438660 }
439 #qr--modal-message,661 }
440 #qr--modal-messageSyntaxInner {662 > .qr--modal-messageContainer {
441 font-family: var(--monoFontFamily);663 flex: 1 1 auto;
442 padding: 0.75em;664 display: flex;
443 margin: 0;665 flex-direction: column;
444 border: none;666 overflow: hidden;
445 resize: none;667 > .qr--modal-editorSettings {
446 line-height: 1.2;668 display: flex;
447 border: 1px solid var(--SmartThemeBorderColor);669 flex-wrap: wrap;
448 border-radius: 5px;670 flex-direction: row;
449 }671 column-gap: 1em;
450 }672 color: var(--grey70);
451 }673 font-size: smaller;
674 align-items: baseline;
675 > .checkbox_label {
676 white-space: nowrap;
677 > input {
678 font-size: inherit;
679 }
680 }
681 }
682 > #qr--modal-messageHolder {
683 flex: 1 1 auto;
684 display: grid;
685 text-align: left;
686 overflow: hidden;
687 &.qr--noSyntax {
688 > #qr--modal-messageSyntax {
689 display: none;
690 }
691 > #qr--modal-message {
692 background-color: var(--ac-style-color-background);
693 color: var(--ac-style-color-text);
694 &::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
695 visibility: visible;
696 cursor: unset;
697 }
698 &::selection {
699 color: unset;
700 background-color: rgba(108 171 251 / 0.25);
701 @supports (color: rgb(from white r g b / 0.25)) {
702 background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
703 }
704 }
705 }
706 }
707 > #qr--modal-messageSyntax {
708 grid-column: 1;
709 grid-row: 1;
710 padding: 0;
711 margin: 0;
712 border: none;
713 overflow: hidden;
714 min-width: 100%;
715 width: 0;
716 > #qr--modal-messageSyntaxInner {
717 height: 100%;
718 }
719 }
720 > #qr--modal-message {
721 background-color: transparent;
722 color: transparent;
723 grid-column: 1;
724 grid-row: 1;
725 caret-color: var(--ac-style-color-text);
726 overflow: auto;
727 &::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
728 visibility: hidden;
729 cursor: default;
730 }
731 &::selection {
732 color: transparent;
733 background-color: rgba(108 171 251 / 0.25);
734 @supports (color: rgb(from white r g b / 0.25)) {
735 background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
736 }
737 }
738 }
739 #qr--modal-message, #qr--modal-messageSyntaxInner {
740 font-family: var(--monoFontFamily);
741 padding: 0.75em;
742 margin: 0;
743 resize: none;
744 line-height: 1.2;
745 border: 1px solid var(--SmartThemeBorderColor);
746 border-radius: 5px;
747 position: relative;
748 }
749 }
750 }
751 }
752 #qr--modal-icon {
753 height: 100%;
754 aspect-ratio: 1 / 1;
452 }755 }
453
454 #qr--modal-executeButtons {756 #qr--modal-executeButtons {
455 display: flex;757 display: flex;
456 gap: 1em;758 gap: 1em;
@@ -510,6 +812,47 @@
510 border-color: rgb(215, 136, 114);812 border-color: rgb(215, 136, 114);
511 }813 }
512 }814 }
815 #qr--modal-debugButtons {
816 display: none;
817 gap: 1em;
818 .qr--modal-debugButton {
819 aspect-ratio: 1.25 / 1;
820 width: 2.25em;
821 position: relative;
822 &:not(.fa-solid) {
823 border-width: 1px;
824 border-style: solid;
825 &:after {
826 content: '';
827 position: absolute;
828 inset: 3px;
829 background-color: var(--SmartThemeBodyColor);
830 mask-size: contain;
831 mask-position: center;
832 mask-repeat: no-repeat;
833 }
834 }
835 &#qr--modal-resume:after {
836 mask-image: url('/img/step-resume.svg');
837 }
838 &#qr--modal-step:after {
839 mask-image: url('/img/step-over.svg');
840 }
841 &#qr--modal-stepInto:after {
842 mask-image: url('/img/step-into.svg');
843 }
844 &#qr--modal-stepOut:after {
845 mask-image: url('/img/step-out.svg');
846 }
847 &#qr--modal-maximize {
848 display: none;
849 }
850 }
851 }
852
853 #qr--modal-send_textarea {
854 flex: 0 0 auto;
855 }
513856
514 #qr--modal-executeProgress {857 #qr--modal-executeProgress {
515 --prog: 0;858 --prog: 0;
@@ -518,6 +861,7 @@
518 --progSuccessColor: rgb(81, 163, 81);861 --progSuccessColor: rgb(81, 163, 81);
519 --progErrorColor: rgb(189, 54, 47);862 --progErrorColor: rgb(189, 54, 47);
520 --progAbortedColor: rgb(215, 136, 114);863 --progAbortedColor: rgb(215, 136, 114);
864 flex: 0 0 auto;
521 height: 0.5em;865 height: 0.5em;
522 background-color: var(--black50a);866 background-color: var(--black50a);
523 position: relative;867 position: relative;
@@ -588,6 +932,135 @@
588 overflow: auto;932 overflow: auto;
589 min-width: 100%;933 min-width: 100%;
590 width: 0;934 width: 0;
935 white-space: pre-wrap;
936 }
937 #qr--modal-debugState {
938 display: none;
939 &.qr--active {
940 display: block;
941 }
942 text-align: left;
943 font-size: smaller;
944 font-family: var(--monoFontFamily);
945 // background-color: rgb(146, 190, 252);
946 color: white;
947 padding: 0.5em 0;
948 overflow: auto;
949 min-width: 100%;
950 width: 0;
951 white-space: pre-wrap;
952
953 .qr--scope {
954 display: grid;
955 grid-template-columns: 0fr 1fr 1fr;
956 column-gap: 0em;
957 .qr--title {
958 grid-column: 1 / 4;
959 font-weight: bold;
960 font-family: var(--mainFontFamily);
961 background-color: var(--black50a);
962 padding: 0.25em;
963 margin-top: 0.5em;
964 }
965 .qr--var, .qr--macro, .qr--pipe {
966 display: contents;
967 &:nth-child(2n + 1) {
968 .qr--key, .qr--val {
969 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
970 }
971 .qr--val {
972 &:nth-child(2n) {
973 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.125);
974 }
975 &:hover {
976 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
977 }
978 }
979 }
980 &:nth-child(2n) {
981 .qr--val {
982 &:nth-child(2n) {
983 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.0625);
984 }
985 &:hover {
986 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
987 }
988 }
989 }
990 &.qr--isHidden {
991 .qr--key, .qr--val {
992 opacity: 0.5;
993 }
994 }
995 .qr--val {
996 grid-column: 2 / 4;
997 &.qr--singleCol {
998 grid-column: unset;
999 }
1000 &.qr--simple {
1001 &:before, &:after {
1002 content: '"';
1003 color: var(--SmartThemeQuoteColor);
1004 }
1005 }
1006 &.qr--unresolved {
1007 &:after {
1008 content: '-UNRESOLVED-';
1009 font-style: italic;
1010 color: var(--SmartThemeQuoteColor);
1011 }
1012 }
1013 }
1014 }
1015 .qr--key {
1016 margin-left: 0.5em;
1017 padding-right: 1em;
1018 &:after { content: ": "; }
1019 }
1020 .qr--pipe, .qr--macro {
1021 > .qr--key {
1022 &:before { content: "{{"; }
1023 &:after { content: "}}: "; }
1024 }
1025 }
1026 .qr--scope {
1027 display: contents;
1028 .qr--pipe {
1029 .qr--key, .qr--val {
1030 opacity: 0.5;
1031 }
1032 }
1033 }
1034 }
1035
1036 .qr--stack {
1037 display: grid;
1038 grid-template-columns: 1fr 0fr;
1039 .qr--title {
1040 grid-column: 1 / 3;
1041 font-weight: bold;
1042 font-family: var(--mainFontFamily);
1043 background-color: var(--black50a);
1044 padding: 0.25em;
1045 margin-top: 1em;
1046 }
1047 .qr--item {
1048 display: contents;
1049 &:nth-child(2n + 1) {
1050 .qr--name, .qr--source {
1051 background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
1052 }
1053 }
1054 .qr--name {
1055 margin-left: 0.5em;
1056 }
1057 .qr--source {
1058 opacity: 0.5;
1059 text-align: right;
1060 white-space: nowrap;
1061 }
1062 }
1063 }
591 }1064 }
592 }1065 }
593 }1066 }
@@ -605,10 +1078,75 @@
605 }1078 }
606}1079}
6071080
1081@keyframes qr--debugPulse {
1082 0%,
1083 100% {
1084 border-color: rgb(81, 163, 81);
1085 }
1086
1087 50% {
1088 border-color: rgb(146, 190, 252);
1089 }
1090}
1091
608.popup.qr--hide {1092.popup.qr--hide {
609 opacity: 0 !important;1093 opacity: 0 !important;
1094 &::backdrop {
1095 opacity: 0 !important;
1096 }
610}1097}
6111098
612.popup.qr--hide::backdrop {1099.popup.qr--hide::backdrop {
613 opacity: 0 !important;1100 opacity: 0 !important;
614}1101}
1102
1103
1104
1105.popup:has(.qr--transferModal) {
1106 .popup-button-ok {
1107 &:after {
1108 content: 'Transfer';
1109 height: 0;
1110 overflow: hidden;
1111 font-weight: bold;
1112 }
1113 display: flex;
1114 align-items: center;
1115 flex-direction: column;
1116 white-space: pre;
1117 font-weight: normal;
1118 box-shadow: 0 0 0;
1119 transition: 200ms;
1120 }
1121 .qr--copy {
1122 &:after {
1123 content: 'Copy';
1124 height: 0;
1125 overflow: hidden;
1126 font-weight: bold;
1127 }
1128 display: flex;
1129 align-items: center;
1130 flex-direction: column;
1131 white-space: pre;
1132 font-weight: normal;
1133 box-shadow: 0 0 0;
1134 transition: 200ms;
1135 }
1136 &:has(.qr--transferSelect:focus) {
1137 .popup-button-ok {
1138 font-weight: bold;
1139 box-shadow: 0 0 10px;
1140 }
1141 &.qr--isCopy {
1142 .popup-button-ok {
1143 font-weight: normal;
1144 box-shadow: 0 0 0;
1145 }
1146 .qr--copy {
1147 font-weight: bold;
1148 box-shadow: 0 0 10px;
1149 }
1150 }
1151 }
1152}
public/scripts/popup.js+9 -0
@@ -526,6 +526,15 @@ export class Popup {
526526
527 return this.#promise;527 return this.#promise;
528 }528 }
529 async completeAffirmative() {
530 return await this.complete(POPUP_RESULT.AFFIRMATIVE);
531 }
532 async completeNegative() {
533 return await this.complete(POPUP_RESULT.NEGATIVE);
534 }
535 async completeCancelled() {
536 return await this.complete(POPUP_RESULT.CANCELLED);
537 }
529538
530 /**539 /**
531 * Hides the popup, using the internal resolver to return the value to the original show promise540 * Hides the popup, using the internal resolver to return the value to the original show promise
public/scripts/power-user.js+13 -1
@@ -45,7 +45,7 @@ import { FILTER_TYPES } from './filters.js';
45import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';45import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
46import { SlashCommand } from './slash-commands/SlashCommand.js';46import { SlashCommand } from './slash-commands/SlashCommand.js';
47import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';47import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
48import { AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';48import { AUTOCOMPLETE_SELECT_KEY, AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
49import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';49import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
50import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';50import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
51import { POPUP_TYPE, callGenericPopup } from './popup.js';51import { POPUP_TYPE, callGenericPopup } from './popup.js';
@@ -277,6 +277,7 @@ let power_user = {
277 left: AUTOCOMPLETE_WIDTH.CHAT,277 left: AUTOCOMPLETE_WIDTH.CHAT,
278 right: AUTOCOMPLETE_WIDTH.CHAT,278 right: AUTOCOMPLETE_WIDTH.CHAT,
279 },279 },
280 select: AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER,
280 },281 },
281 parser: {282 parser: {
282 /**@type {Object.<PARSER_FLAG,boolean>} */283 /**@type {Object.<PARSER_FLAG,boolean>} */
@@ -1494,6 +1495,9 @@ function loadPowerUserSettings(settings, data) {
1494 if (power_user.stscript.autocomplete.style === undefined) {1495 if (power_user.stscript.autocomplete.style === undefined) {
1495 power_user.stscript.autocomplete.style = power_user.stscript.autocomplete_style || defaultStscript.autocomplete.style;1496 power_user.stscript.autocomplete.style = power_user.stscript.autocomplete_style || defaultStscript.autocomplete.style;
1496 }1497 }
1498 if (power_user.stscript.autocomplete.select === undefined) {
1499 power_user.stscript.autocomplete.select = defaultStscript.autocomplete.select;
1500 }
1497 }1501 }
1498 if (power_user.stscript.parser === undefined) {1502 if (power_user.stscript.parser === undefined) {
1499 power_user.stscript.parser = defaultStscript.parser;1503 power_user.stscript.parser = defaultStscript.parser;
@@ -1658,6 +1662,7 @@ function loadPowerUserSettings(settings, data) {
1658 $('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');1662 $('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');
1659 $('#stscript_autocomplete_style').val(power_user.stscript.autocomplete.style ?? 'theme');1663 $('#stscript_autocomplete_style').val(power_user.stscript.autocomplete.style ?? 'theme');
1660 document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete.style);1664 document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete.style);
1665 $('#stscript_autocomplete_select').val(power_user.stscript.autocomplete.select ?? (AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER));
1661 $('#stscript_parser_flag_strict_escaping').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] ?? false);1666 $('#stscript_parser_flag_strict_escaping').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] ?? false);
1662 $('#stscript_parser_flag_replace_getvar').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] ?? false);1667 $('#stscript_parser_flag_replace_getvar').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] ?? false);
1663 $('#stscript_autocomplete_font_scale').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale);1668 $('#stscript_autocomplete_font_scale').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale);
@@ -3848,6 +3853,12 @@ $(document).ready(() => {
3848 saveSettingsDebounced();3853 saveSettingsDebounced();
3849 });3854 });
38503855
3856 $('#stscript_autocomplete_select').on('change', function () {
3857 const value = $(this).find(':selected').val();
3858 power_user.stscript.autocomplete.select = parseInt(String(value));
3859 saveSettingsDebounced();
3860 });
3861
3851 $('#stscript_autocomplete_font_scale').on('input', function () {3862 $('#stscript_autocomplete_font_scale').on('input', function () {
3852 const value = $(this).val();3863 const value = $(this).val();
3853 $('#stscript_autocomplete_font_scale_counter').val(value);3864 $('#stscript_autocomplete_font_scale_counter').val(value);
@@ -4016,6 +4027,7 @@ $(document).ready(() => {
4016 ),4027 ),
4017 ],4028 ],
4018 helpString: 'Enter message deletion mode, and auto-deletes last N messages if numeric argument is provided.',4029 helpString: 'Enter message deletion mode, and auto-deletes last N messages if numeric argument is provided.',
4030 returns: 'The text of the deleted messages.',
4019 }));4031 }));
4020 SlashCommandParser.addCommandObject(SlashCommand.fromProps({4032 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
4021 name: 'cut',4033 name: 'cut',
public/scripts/slash-commands.js+92 -9
@@ -51,7 +51,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
51import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';51import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
52import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';52import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
53import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';53import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';
54import { debounce, delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';54import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
55import { registerVariableCommands, resolveVariable } from './variables.js';55import { registerVariableCommands, resolveVariable } from './variables.js';
56import { background_settings } from './backgrounds.js';56import { background_settings } from './backgrounds.js';
57import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';57import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -64,6 +64,9 @@ import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashComma
64import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';64import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
65import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';65import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
66import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';66import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
67import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
68import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
69import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
67export {70export {
68 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,71 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
69};72};
@@ -1120,10 +1123,10 @@ export function initDefaultSlashCommands() {
1120 unnamedArgumentList: [1123 unnamedArgumentList: [
1121 SlashCommandArgument.fromProps({1124 SlashCommandArgument.fromProps({
1122 description: 'scoped variable or qr label',1125 description: 'scoped variable or qr label',
1123 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING],1126 typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.CLOSURE],
1124 isRequired: true,1127 isRequired: true,
1125 enumProvider: () => [1128 enumProvider: (executor, scope) => [
1126 ...commonEnumProviders.variables('scope')(),1129 ...commonEnumProviders.variables('scope')(executor, scope),
1127 ...(typeof window['qrEnumProviderExecutables'] === 'function') ? window['qrEnumProviderExecutables']() : [],1130 ...(typeof window['qrEnumProviderExecutables'] === 'function') ? window['qrEnumProviderExecutables']() : [],
1128 ],1131 ],
1129 }),1132 }),
@@ -1477,6 +1480,21 @@ export function initDefaultSlashCommands() {
1477 ],1480 ],
1478 helpString: 'Sets the specified prompt manager entry/entries on or off.',1481 helpString: 'Sets the specified prompt manager entry/entries on or off.',
1479 }));1482 }));
1483 SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pick-icon',
1484 callback: async()=>((await showFontAwesomePicker()) ?? false).toString(),
1485 returns: 'The chosen icon name or false if cancelled.',
1486 helpString: `
1487 <div>Opens a popup with all the available Font Awesome icons and returns the selected icon's name.</div>
1488 <div>
1489 <strong>Example:</strong>
1490 <ul>
1491 <li>
1492 <pre><code>/pick-icon |\n/if left={{pipe}} rule=eq right=false\n\telse={: /echo chosen icon: "{{pipe}}" :}\n\t{: /echo cancelled icon selection :}\n|</code></pre>
1493 </li>
1494 </ul>
1495 </div>
1496 `,
1497 }));
14801498
1481 registerVariableCommands();1499 registerVariableCommands();
1482}1500}
@@ -1821,6 +1839,11 @@ async function runCallback(args, name) {
1821 throw new Error('No name provided for /run command');1839 throw new Error('No name provided for /run command');
1822 }1840 }
18231841
1842 if (name instanceof SlashCommandClosure) {
1843 name.breakController = new SlashCommandBreakController();
1844 return (await name.execute())?.pipe;
1845 }
1846
1824 /**@type {SlashCommandScope} */1847 /**@type {SlashCommandScope} */
1825 const scope = args._scope;1848 const scope = args._scope;
1826 if (scope.existsVariable(name)) {1849 if (scope.existsVariable(name)) {
@@ -1829,6 +1852,11 @@ async function runCallback(args, name) {
1829 throw new Error(`"${name}" is not callable.`);1852 throw new Error(`"${name}" is not callable.`);
1830 }1853 }
1831 closure.scope.parent = scope;1854 closure.scope.parent = scope;
1855 closure.breakController = new SlashCommandBreakController();
1856 if (args._debugController && !closure.debugController) {
1857 closure.debugController = args._debugController;
1858 }
1859 while (closure.providedArgumentList.pop());
1832 closure.argumentList.forEach(arg => {1860 closure.argumentList.forEach(arg => {
1833 if (Object.keys(args).includes(arg.name)) {1861 if (Object.keys(args).includes(arg.name)) {
1834 const providedArg = new SlashCommandNamedArgumentAssignment();1862 const providedArg = new SlashCommandNamedArgumentAssignment();
@@ -1847,9 +1875,14 @@ async function runCallback(args, name) {
18471875
1848 try {1876 try {
1849 name = name.trim();1877 name = name.trim();
1850 return await window['executeQuickReplyByName'](name, args);1878 /**@type {ExecuteSlashCommandsOptions} */
1879 const options = {
1880 abortController: args._abortController,
1881 debugController: args._debugController,
1882 };
1883 return await window['executeQuickReplyByName'](name, args, options);
1851 } catch (error) {1884 } catch (error) {
1852 throw new Error(`Error running Quick Reply "${name}": ${error.message}`, 'Error');1885 throw new Error(`Error running Quick Reply "${name}": ${error.message}`);
1853 }1886 }
1854}1887}
18551888
@@ -3428,7 +3461,9 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress);
3428 * @prop {boolean} [handleExecutionErrors] (false) Whether to handle execution errors (show toast on error) or throw3461 * @prop {boolean} [handleExecutionErrors] (false) Whether to handle execution errors (show toast on error) or throw
3429 * @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply3462 * @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply
3430 * @prop {SlashCommandAbortController} [abortController] (null) Controller used to abort or pause command execution3463 * @prop {SlashCommandAbortController} [abortController] (null) Controller used to abort or pause command execution
3464 * @prop {SlashCommandDebugController} [debugController] (null) Controller used to control debug execution
3431 * @prop {(done:number, total:number)=>void} [onProgress] (null) Callback to handle progress events3465 * @prop {(done:number, total:number)=>void} [onProgress] (null) Callback to handle progress events
3466 * @prop {string} [source] (null) String indicating where the code come from (e.g., QR name)
3432 */3467 */
34333468
3434/**3469/**
@@ -3436,6 +3471,7 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress);
3436 * @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.3471 * @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
3437 * @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply3472 * @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply
3438 * @prop {boolean} [clearChatInput] (false) Whether to clear the chat input textarea3473 * @prop {boolean} [clearChatInput] (false) Whether to clear the chat input textarea
3474 * @prop {string} [source] (null) String indicating where the code come from (e.g., QR name)
3439 */3475 */
34403476
3441/**3477/**
@@ -3451,6 +3487,7 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) {
3451 scope: null,3487 scope: null,
3452 parserFlags: null,3488 parserFlags: null,
3453 clearChatInput: false,3489 clearChatInput: false,
3490 source: null,
3454 }, options);3491 }, options);
34553492
3456 isExecutingCommandsFromChatInput = true;3493 isExecutingCommandsFromChatInput = true;
@@ -3473,13 +3510,21 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) {
34733510
3474 /**@type {SlashCommandClosureResult} */3511 /**@type {SlashCommandClosureResult} */
3475 let result = null;3512 let result = null;
3513 let currentProgress = 0;
3476 try {3514 try {
3477 commandsFromChatInputAbortController = new SlashCommandAbortController();3515 commandsFromChatInputAbortController = new SlashCommandAbortController();
3478 result = await executeSlashCommandsWithOptions(text, {3516 result = await executeSlashCommandsWithOptions(text, {
3479 abortController: commandsFromChatInputAbortController,3517 abortController: commandsFromChatInputAbortController,
3480 onProgress: (done, total) => ta.style.setProperty('--prog', `${done / total * 100}%`),3518 onProgress: (done, total) => {
3519 const newProgress = done / total;
3520 if (newProgress > currentProgress) {
3521 currentProgress = newProgress;
3522 ta.style.setProperty('--prog', `${newProgress * 100}%`);
3523 }
3524 },
3481 parserFlags: options.parserFlags,3525 parserFlags: options.parserFlags,
3482 scope: options.scope,3526 scope: options.scope,
3527 source: options.source,
3483 });3528 });
3484 if (commandsFromChatInputAbortController.signal.aborted) {3529 if (commandsFromChatInputAbortController.signal.aborted) {
3485 document.querySelector('#form_sheld').classList.add('script_aborted');3530 document.querySelector('#form_sheld').classList.add('script_aborted');
@@ -3492,7 +3537,23 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) {
3492 result.isError = true;3537 result.isError = true;
3493 result.errorMessage = e.message || 'An unknown error occurred';3538 result.errorMessage = e.message || 'An unknown error occurred';
3494 if (e.cause !== 'abort') {3539 if (e.cause !== 'abort') {
3495 toastr.error(result.errorMessage);3540 if (e instanceof SlashCommandExecutionError) {
3541 /**@type {SlashCommandExecutionError}*/
3542 const ex = e;
3543 const toast = `
3544 <div>${ex.message}</div>
3545 <div>Line: ${ex.line} Column: ${ex.column}</div>
3546 <pre style="text-align:left;">${ex.hint}</pre>
3547 `;
3548 const clickHint = '<p>Click to see details</p>';
3549 toastr.error(
3550 `${toast}${clickHint}`,
3551 'SlashCommandExecutionError',
3552 { escapeHtml: false, timeOut: 10000, onclick: () => callPopup(toast, 'text') },
3553 );
3554 } else {
3555 toastr.error(result.errorMessage);
3556 }
3496 }3557 }
3497 } finally {3558 } finally {
3498 delay(1000).then(() => clearCommandProgressDebounced());3559 delay(1000).then(() => clearCommandProgressDebounced());
@@ -3520,7 +3581,9 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
3520 handleExecutionErrors: false,3581 handleExecutionErrors: false,
3521 parserFlags: null,3582 parserFlags: null,
3522 abortController: null,3583 abortController: null,
3584 debugController: null,
3523 onProgress: null,3585 onProgress: null,
3586 source: null,
3524 }, options);3587 }, options);
35253588
3526 let closure;3589 let closure;
@@ -3528,6 +3591,8 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
3528 closure = parser.parse(text, true, options.parserFlags, options.abortController ?? new SlashCommandAbortController());3591 closure = parser.parse(text, true, options.parserFlags, options.abortController ?? new SlashCommandAbortController());
3529 closure.scope.parent = options.scope;3592 closure.scope.parent = options.scope;
3530 closure.onProgress = options.onProgress;3593 closure.onProgress = options.onProgress;
3594 closure.debugController = options.debugController;
3595 closure.source = options.source;
3531 } catch (e) {3596 } catch (e) {
3532 if (options.handleParserErrors && e instanceof SlashCommandParserError) {3597 if (options.handleParserErrors && e instanceof SlashCommandParserError) {
3533 /**@type {SlashCommandParserError}*/3598 /**@type {SlashCommandParserError}*/
@@ -3559,7 +3624,23 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
3559 return result;3624 return result;
3560 } catch (e) {3625 } catch (e) {
3561 if (options.handleExecutionErrors) {3626 if (options.handleExecutionErrors) {
3562 toastr.error(e.message);3627 if (e instanceof SlashCommandExecutionError) {
3628 /**@type {SlashCommandExecutionError}*/
3629 const ex = e;
3630 const toast = `
3631 <div>${ex.message}</div>
3632 <div>Line: ${ex.line} Column: ${ex.column}</div>
3633 <pre style="text-align:left;">${ex.hint}</pre>
3634 `;
3635 const clickHint = '<p>Click to see details</p>';
3636 toastr.error(
3637 `${toast}${clickHint}`,
3638 'SlashCommandExecutionError',
3639 { escapeHtml: false, timeOut: 10000, onclick: () => callPopup(toast, 'text') },
3640 );
3641 } else {
3642 toastr.error(e.message);
3643 }
3563 const result = new SlashCommandClosureResult();3644 const result = new SlashCommandClosureResult();
3564 result.isError = true;3645 result.isError = true;
3565 result.errorMessage = e.message;3646 result.errorMessage = e.message;
@@ -3596,6 +3677,7 @@ async function executeSlashCommands(text, handleParserErrors = true, scope = nul
3596 *3677 *
3597 * @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete3678 * @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete
3598 * @param {Boolean} isFloating Whether to show the auto complete as a floating window (e.g., large QR editor)3679 * @param {Boolean} isFloating Whether to show the auto complete as a floating window (e.g., large QR editor)
3680 * @returns {Promise<AutoComplete>}
3599 */3681 */
3600export async function setSlashCommandAutoComplete(textarea, isFloating = false) {3682export async function setSlashCommandAutoComplete(textarea, isFloating = false) {
3601 function canUseNegativeLookbehind() {3683 function canUseNegativeLookbehind() {
@@ -3619,6 +3701,7 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
3619 async (text, index) => await parser.getNameAt(text, index),3701 async (text, index) => await parser.getNameAt(text, index),
3620 isFloating,3702 isFloating,
3621 );3703 );
3704 return ac;
3622}3705}
3623/**@type {HTMLTextAreaElement} */3706/**@type {HTMLTextAreaElement} */
3624const sendTextarea = document.querySelector('#send_textarea');3707const sendTextarea = document.querySelector('#send_textarea');
public/scripts/slash-commands/SlashCommand.js+78 -35
@@ -1,6 +1,7 @@
1import { SlashCommandAbortController } from './SlashCommandAbortController.js';1import { SlashCommandAbortController } from './SlashCommandAbortController.js';
2import { SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js';2import { SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js';
3import { SlashCommandClosure } from './SlashCommandClosure.js';3import { SlashCommandClosure } from './SlashCommandClosure.js';
4import { SlashCommandDebugController } from './SlashCommandDebugController.js';
4import { PARSER_FLAG } from './SlashCommandParser.js';5import { PARSER_FLAG } from './SlashCommandParser.js';
5import { SlashCommandScope } from './SlashCommandScope.js';6import { SlashCommandScope } from './SlashCommandScope.js';
67
@@ -12,6 +13,7 @@ import { SlashCommandScope } from './SlashCommandScope.js';
12 * _scope:SlashCommandScope,13 * _scope:SlashCommandScope,
13 * _parserFlags:{[id:PARSER_FLAG]:boolean},14 * _parserFlags:{[id:PARSER_FLAG]:boolean},
14 * _abortController:SlashCommandAbortController,15 * _abortController:SlashCommandAbortController,
16 * _debugController:SlashCommandDebugController,
15 * _hasUnnamedArgument:boolean,17 * _hasUnnamedArgument:boolean,
16 * [id:string]:string|SlashCommandClosure,18 * [id:string]:string|SlashCommandClosure,
17 * }} NamedArguments19 * }} NamedArguments
@@ -36,6 +38,7 @@ export class SlashCommand {
36 * @param {(namedArguments:NamedArguments|NamedArgumentsCapture, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>} [props.callback]38 * @param {(namedArguments:NamedArguments|NamedArgumentsCapture, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>} [props.callback]
37 * @param {string} [props.helpString]39 * @param {string} [props.helpString]
38 * @param {boolean} [props.splitUnnamedArgument]40 * @param {boolean} [props.splitUnnamedArgument]
41 * @param {Number} [props.splitUnnamedArgumentCount]
39 * @param {string[]} [props.aliases]42 * @param {string[]} [props.aliases]
40 * @param {string} [props.returns]43 * @param {string} [props.returns]
41 * @param {SlashCommandNamedArgument[]} [props.namedArgumentList]44 * @param {SlashCommandNamedArgument[]} [props.namedArgumentList]
@@ -50,9 +53,10 @@ export class SlashCommand {
5053
5154
52 /**@type {string}*/ name;55 /**@type {string}*/ name;
53 /**@type {(namedArguments:{_pipe:string|SlashCommandClosure, _scope:SlashCommandScope, _abortController:SlashCommandAbortController, [id:string]:string|SlashCommandClosure}, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>}*/ callback;56 /**@type {(namedArguments:{_scope:SlashCommandScope, _abortController:SlashCommandAbortController, [id:string]:string|SlashCommandClosure}, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>}*/ callback;
54 /**@type {string}*/ helpString;57 /**@type {string}*/ helpString;
55 /**@type {boolean}*/ splitUnnamedArgument = false;58 /**@type {boolean}*/ splitUnnamedArgument = false;
59 /**@type {Number}*/ splitUnnamedArgumentCount;
56 /**@type {string[]}*/ aliases = [];60 /**@type {string[]}*/ aliases = [];
57 /**@type {string}*/ returns;61 /**@type {string}*/ returns;
58 /**@type {SlashCommandNamedArgument[]}*/ namedArgumentList = [];62 /**@type {SlashCommandNamedArgument[]}*/ namedArgumentList = [];
@@ -61,6 +65,10 @@ export class SlashCommand {
61 /**@type {Object.<string, HTMLElement>}*/ helpCache = {};65 /**@type {Object.<string, HTMLElement>}*/ helpCache = {};
62 /**@type {Object.<string, DocumentFragment>}*/ helpDetailsCache = {};66 /**@type {Object.<string, DocumentFragment>}*/ helpDetailsCache = {};
6367
68 /**@type {boolean}*/ isExtension = false;
69 /**@type {boolean}*/ isThirdParty = false;
70 /**@type {string}*/ source;
71
64 renderHelpItem(key = null) {72 renderHelpItem(key = null) {
65 key = key ?? this.name;73 key = key ?? this.name;
66 if (!this.helpCache[key]) {74 if (!this.helpCache[key]) {
@@ -225,12 +233,35 @@ export class SlashCommand {
225 const aliasList = [cmd.name, ...(cmd.aliases ?? [])].filter(it=>it != key);233 const aliasList = [cmd.name, ...(cmd.aliases ?? [])].filter(it=>it != key);
226 const specs = document.createElement('div'); {234 const specs = document.createElement('div'); {
227 specs.classList.add('specs');235 specs.classList.add('specs');
228 const name = document.createElement('div'); {236 const head = document.createElement('div'); {
229 name.classList.add('name');237 head.classList.add('head');
230 name.classList.add('monospace');238 const name = document.createElement('div'); {
231 name.title = 'command name';239 name.classList.add('name');
232 name.textContent = `/${key}`;240 name.classList.add('monospace');
233 specs.append(name);241 name.title = 'command name';
242 name.textContent = `/${key}`;
243 head.append(name);
244 }
245 const src = document.createElement('div'); {
246 src.classList.add('source');
247 src.classList.add('fa-solid');
248 if (this.isExtension) {
249 src.classList.add('isExtension');
250 src.classList.add('fa-cubes');
251 if (this.isThirdParty) src.classList.add('isThirdParty');
252 else src.classList.add('isCore');
253 } else {
254 src.classList.add('isCore');
255 src.classList.add('fa-star-of-life');
256 }
257 src.title = [
258 this.isExtension ? 'Extension' : 'Core',
259 this.isThirdParty ? 'Third Party' : (this.isExtension ? 'Core' : null),
260 this.source,
261 ].filter(it=>it).join('\n');
262 head.append(src);
263 }
264 specs.append(head);
234 }265 }
235 const body = document.createElement('div'); {266 const body = document.createElement('div'); {
236 body.classList.add('body');267 body.classList.add('body');
@@ -303,40 +334,52 @@ export class SlashCommand {
303 for (const arg of unnamedArguments) {334 for (const arg of unnamedArguments) {
304 const listItem = document.createElement('li'); {335 const listItem = document.createElement('li'); {
305 listItem.classList.add('argumentItem');336 listItem.classList.add('argumentItem');
306 const argItem = document.createElement('div'); {337 const argSpec = document.createElement('div'); {
307 argItem.classList.add('argument');338 argSpec.classList.add('argumentSpec');
308 argItem.classList.add('unnamedArgument');339 const argItem = document.createElement('div'); {
309 argItem.title = `${arg.isRequired ? '' : 'optional '}unnamed argument`;340 argItem.classList.add('argument');
310 if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');341 argItem.classList.add('unnamedArgument');
311 if (arg.acceptsMultiple) argItem.classList.add('multiple');342 argItem.title = `${arg.isRequired ? '' : 'optional '}unnamed argument`;
312 if (arg.enumList.length > 0) {343 if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
313 const enums = document.createElement('span'); {344 if (arg.acceptsMultiple) argItem.classList.add('multiple');
314 enums.classList.add('argument-enums');345 if (arg.enumList.length > 0) {
315 enums.title = `${argItem.title} - accepted values`;346 const enums = document.createElement('span'); {
316 for (const e of arg.enumList) {347 enums.classList.add('argument-enums');
317 const enumItem = document.createElement('span'); {348 enums.title = `${argItem.title} - accepted values`;
318 enumItem.classList.add('argument-enum');349 for (const e of arg.enumList) {
319 enumItem.textContent = e.value;350 const enumItem = document.createElement('span'); {
320 enums.append(enumItem);351 enumItem.classList.add('argument-enum');
352 enumItem.textContent = e.value;
353 enums.append(enumItem);
354 }
321 }355 }
356 argItem.append(enums);
322 }357 }
323 argItem.append(enums);358 } else {
324 }359 const types = document.createElement('span'); {
325 } else {360 types.classList.add('argument-types');
326 const types = document.createElement('span'); {361 types.title = `${argItem.title} - accepted types`;
327 types.classList.add('argument-types');362 for (const t of arg.typeList) {
328 types.title = `${argItem.title} - accepted types`;363 const type = document.createElement('span'); {
329 for (const t of arg.typeList) {364 type.classList.add('argument-type');
330 const type = document.createElement('span'); {365 type.textContent = t;
331 type.classList.add('argument-type');366 types.append(type);
332 type.textContent = t;367 }
333 types.append(type);
334 }368 }
369 argItem.append(types);
335 }370 }
336 argItem.append(types);371 }
372 argSpec.append(argItem);
373 }
374 if (arg.defaultValue !== null) {
375 const argDefault = document.createElement('div'); {
376 argDefault.classList.add('argument-default');
377 argDefault.title = 'default value';
378 argDefault.textContent = arg.defaultValue.toString();
379 argSpec.append(argDefault);
337 }380 }
338 }381 }
339 listItem.append(argItem);382 listItem.append(argSpec);
340 }383 }
341 const desc = document.createElement('div'); {384 const desc = document.createElement('div'); {
342 desc.classList.add('argument-description');385 desc.classList.add('argument-description');
public/scripts/slash-commands/SlashCommandArgument.js+8 -5
@@ -2,6 +2,9 @@ import { SlashCommandClosure } from './SlashCommandClosure.js';
2import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js';2import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js';
3import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';3import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
4import { SlashCommandExecutor } from './SlashCommandExecutor.js';4import { SlashCommandExecutor } from './SlashCommandExecutor.js';
5import { SlashCommandScope } from './SlashCommandScope.js';
6
7
58
6/**@readonly*/9/**@readonly*/
7/**@enum {string}*/10/**@enum {string}*/
@@ -27,7 +30,7 @@ export class SlashCommandArgument {
27 * @param {boolean} [props.acceptsMultiple=false] default: false - whether argument accepts multiple values30 * @param {boolean} [props.acceptsMultiple=false] default: false - whether argument accepts multiple values
28 * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided31 * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided
29 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values32 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values
30 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options33 * @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options
31 * @param {boolean} [props.forceEnum=false] default: false - whether the input must match one of the enum values34 * @param {boolean} [props.forceEnum=false] default: false - whether the input must match one of the enum values
32 */35 */
33 static fromProps(props) {36 static fromProps(props) {
@@ -49,7 +52,7 @@ export class SlashCommandArgument {
49 /**@type {boolean}*/ acceptsMultiple = false;52 /**@type {boolean}*/ acceptsMultiple = false;
50 /**@type {string|SlashCommandClosure}*/ defaultValue;53 /**@type {string|SlashCommandClosure}*/ defaultValue;
51 /**@type {SlashCommandEnumValue[]}*/ enumList = [];54 /**@type {SlashCommandEnumValue[]}*/ enumList = [];
52 /**@type {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]}*/ enumProvider = null;55 /**@type {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]}*/ enumProvider = null;
53 /**@type {boolean}*/ forceEnum = false;56 /**@type {boolean}*/ forceEnum = false;
5457
55 /**58 /**
@@ -57,7 +60,7 @@ export class SlashCommandArgument {
57 * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types60 * @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types
58 * @param {string|SlashCommandClosure} defaultValue61 * @param {string|SlashCommandClosure} defaultValue
59 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums62 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums
60 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options63 * @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options
61 */64 */
62 constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = false) {65 constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = false) {
63 this.description = description;66 this.description = description;
@@ -89,7 +92,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
89 * @param {boolean} [props.acceptsMultiple=false] default: false - whether argument accepts multiple values92 * @param {boolean} [props.acceptsMultiple=false] default: false - whether argument accepts multiple values
90 * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided93 * @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided
91 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values94 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values
92 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options95 * @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options
93 * @param {boolean} [props.forceEnum=false] default: false - whether the input must match one of the enum values96 * @param {boolean} [props.forceEnum=false] default: false - whether the input must match one of the enum values
94 */97 */
95 static fromProps(props) {98 static fromProps(props) {
@@ -119,7 +122,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
119 * @param {string|SlashCommandClosure} [defaultValue=null]122 * @param {string|SlashCommandClosure} [defaultValue=null]
120 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [enums=[]]123 * @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [enums=[]]
121 * @param {string[]} [aliases=[]]124 * @param {string[]} [aliases=[]]
122 * @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [enumProvider=null] function that returns auto complete options125 * @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [enumProvider=null] function that returns auto complete options
123 * @param {boolean} [forceEnum=false]126 * @param {boolean} [forceEnum=false]
124 */127 */
125 constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = false) {128 constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = false) {
public/scripts/slash-commands/SlashCommandAutoCompleteNameResult.js+19 -9
@@ -8,15 +8,18 @@ import { SlashCommandCommandAutoCompleteOption } from './SlashCommandCommandAuto
8import { SlashCommandEnumAutoCompleteOption } from './SlashCommandEnumAutoCompleteOption.js';8import { SlashCommandEnumAutoCompleteOption } from './SlashCommandEnumAutoCompleteOption.js';
9import { SlashCommandExecutor } from './SlashCommandExecutor.js';9import { SlashCommandExecutor } from './SlashCommandExecutor.js';
10import { SlashCommandNamedArgumentAutoCompleteOption } from './SlashCommandNamedArgumentAutoCompleteOption.js';10import { SlashCommandNamedArgumentAutoCompleteOption } from './SlashCommandNamedArgumentAutoCompleteOption.js';
11import { SlashCommandScope } from './SlashCommandScope.js';
1112
12export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {13export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
13 /**@type {SlashCommandExecutor}*/ executor;14 /**@type {SlashCommandExecutor}*/ executor;
15 /**@type {SlashCommandScope}*/ scope;
1416
15 /**17 /**
16 * @param {SlashCommandExecutor} executor18 * @param {SlashCommandExecutor} executor
19 * @param {SlashCommandScope} scope
17 * @param {Object.<string,SlashCommand>} commands20 * @param {Object.<string,SlashCommand>} commands
18 */21 */
19 constructor(executor, commands) {22 constructor(executor, scope, commands) {
20 super(23 super(
21 executor.name,24 executor.name,
22 executor.start,25 executor.start,
@@ -29,6 +32,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
29 ()=>'No slash commands found!',32 ()=>'No slash commands found!',
30 );33 );
31 this.executor = executor;34 this.executor = executor;
35 this.scope = scope;
32 }36 }
3337
34 getSecondaryNameAt(text, index, isSelect) {38 getSecondaryNameAt(text, index, isSelect) {
@@ -86,8 +90,8 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
86 }90 }
87 } else if (unamedArgLength > 0 && index >= this.executor.startUnnamedArgs && index <= this.executor.endUnnamedArgs) {91 } else if (unamedArgLength > 0 && index >= this.executor.startUnnamedArgs && index <= this.executor.endUnnamedArgs) {
88 // cursor is somewhere within the unnamed arguments92 // cursor is somewhere within the unnamed arguments
89 //TODO if index is in first array item and that is a string, treat it as an unfinished named arg93 // if index is in first array item and that is a string, treat it as an unfinished named arg
90 if (typeof this.executor.unnamedArgumentList[0].value == 'string') {94 if (typeof this.executor.unnamedArgumentList[0]?.value == 'string') {
91 if (index <= this.executor.startUnnamedArgs + this.executor.unnamedArgumentList[0].value.length) {95 if (index <= this.executor.startUnnamedArgs + this.executor.unnamedArgumentList[0].value.length) {
92 name = this.executor.unnamedArgumentList[0].value.slice(0, index - this.executor.startUnnamedArgs);96 name = this.executor.unnamedArgumentList[0].value.slice(0, index - this.executor.startUnnamedArgs);
93 start = this.executor.startUnnamedArgs;97 start = this.executor.startUnnamedArgs;
@@ -103,7 +107,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
103107
104 if (name.includes('=') && cmdArg) {108 if (name.includes('=') && cmdArg) {
105 // if cursor is already behind "=" check for enums109 // if cursor is already behind "=" check for enums
106 const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList;110 const enumList = cmdArg?.enumProvider?.(this.executor, this.scope) ?? cmdArg?.enumList;
107 if (cmdArg && enumList?.length) {111 if (cmdArg && enumList?.length) {
108 if (isSelect && enumList.find(it=>it.value == value) && argAssign && argAssign.end == index) {112 if (isSelect && enumList.find(it=>it.value == value) && argAssign && argAssign.end == index) {
109 return null;113 return null;
@@ -111,7 +115,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
111 const result = new AutoCompleteSecondaryNameResult(115 const result = new AutoCompleteSecondaryNameResult(
112 value,116 value,
113 start + name.length,117 start + name.length,
114 enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),118 enumList.map(it=>SlashCommandEnumAutoCompleteOption.from(this.executor.command, it)),
115 true,119 true,
116 );120 );
117 result.isRequired = true;121 result.isRequired = true;
@@ -150,7 +154,10 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
150 if (idx > -1) {154 if (idx > -1) {
151 argAssign = this.executor.unnamedArgumentList[idx];155 argAssign = this.executor.unnamedArgumentList[idx];
152 cmdArg = this.executor.command.unnamedArgumentList[idx];156 cmdArg = this.executor.command.unnamedArgumentList[idx];
153 const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList;157 if (cmdArg === undefined && this.executor.command.unnamedArgumentList.slice(-1)[0]?.acceptsMultiple) {
158 cmdArg = this.executor.command.unnamedArgumentList.slice(-1)[0];
159 }
160 const enumList = cmdArg?.enumProvider?.(this.executor, this.scope) ?? cmdArg?.enumList;
154 if (cmdArg && enumList.length > 0) {161 if (cmdArg && enumList.length > 0) {
155 value = argAssign.value.toString().slice(0, index - argAssign.start);162 value = argAssign.value.toString().slice(0, index - argAssign.start);
156 start = argAssign.start;163 start = argAssign.start;
@@ -161,23 +168,26 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
161 value = '';168 value = '';
162 start = index;169 start = index;
163 cmdArg = notProvidedArguments[0];170 cmdArg = notProvidedArguments[0];
171 if (cmdArg === undefined && this.executor.command.unnamedArgumentList.slice(-1)[0]?.acceptsMultiple) {
172 cmdArg = this.executor.command.unnamedArgumentList.slice(-1)[0];
173 }
164 }174 }
165 } else {175 } else {
166 return null;176 return null;
167 }177 }
168178
169 const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList;179 const enumList = cmdArg?.enumProvider?.(this.executor, this.scope) ?? cmdArg?.enumList;
170 if (cmdArg == null || enumList.length == 0) return null;180 if (cmdArg == null || enumList.length == 0) return null;
171181
172 const result = new AutoCompleteSecondaryNameResult(182 const result = new AutoCompleteSecondaryNameResult(
173 value,183 value,
174 start,184 start,
175 enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),185 enumList.map(it=>SlashCommandEnumAutoCompleteOption.from(this.executor.command, it)),
176 false,186 false,
177 );187 );
178 const isCompleteValue = enumList.find(it=>it.value == value);188 const isCompleteValue = enumList.find(it=>it.value == value);
179 const isSelectedValue = isSelect && isCompleteValue;189 const isSelectedValue = isSelect && isCompleteValue;
180 result.isRequired = cmdArg.isRequired && !isSelectedValue && !isCompleteValue;190 result.isRequired = cmdArg.isRequired && !isSelectedValue;
181 result.forceMatch = cmdArg.forceEnum;191 result.forceMatch = cmdArg.forceEnum;
182 return result;192 return result;
183 }193 }
public/scripts/slash-commands/SlashCommandBreak.js+7 -0
@@ -0,0 +1,7 @@
1import { SlashCommandExecutor } from './SlashCommandExecutor.js';
2
3export class SlashCommandBreak extends SlashCommandExecutor {
4 get value() {
5 return this.unnamedArgumentList[0]?.value;
6 }
7}
public/scripts/slash-commands/SlashCommandBreakController.js+7 -0
@@ -0,0 +1,7 @@
1export class SlashCommandBreakController {
2 /**@type {boolean} */ isBreak = false;
3
4 break() {
5 this.isBreak = true;
6 }
7}
public/scripts/slash-commands/SlashCommandBreakPoint.js+3 -0
@@ -0,0 +1,3 @@
1import { SlashCommandExecutor } from './SlashCommandExecutor.js';
2
3export class SlashCommandBreakPoint extends SlashCommandExecutor {}
public/scripts/slash-commands/SlashCommandClosure.js+292 -98
@@ -1,9 +1,13 @@
1import { substituteParams } from '../../script.js';1import { substituteParams } from '../../script.js';
2import { delay, escapeRegex } from '../utils.js';2import { delay, escapeRegex, uuidv4 } from '../utils.js';
3import { SlashCommand } from './SlashCommand.js';3import { SlashCommand } from './SlashCommand.js';
4import { SlashCommandAbortController } from './SlashCommandAbortController.js';4import { SlashCommandAbortController } from './SlashCommandAbortController.js';
5import { SlashCommandClosureExecutor } from './SlashCommandClosureExecutor.js';5import { SlashCommandBreak } from './SlashCommandBreak.js';
6import { SlashCommandBreakController } from './SlashCommandBreakController.js';
7import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js';
6import { SlashCommandClosureResult } from './SlashCommandClosureResult.js';8import { SlashCommandClosureResult } from './SlashCommandClosureResult.js';
9import { SlashCommandDebugController } from './SlashCommandDebugController.js';
10import { SlashCommandExecutionError } from './SlashCommandExecutionError.js';
7import { SlashCommandExecutor } from './SlashCommandExecutor.js';11import { SlashCommandExecutor } from './SlashCommandExecutor.js';
8import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';12import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
9import { SlashCommandScope } from './SlashCommandScope.js';13import { SlashCommandScope } from './SlashCommandScope.js';
@@ -17,8 +21,20 @@ export class SlashCommandClosure {
17 /**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = [];21 /**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = [];
18 /**@type {SlashCommandExecutor[]}*/ executorList = [];22 /**@type {SlashCommandExecutor[]}*/ executorList = [];
19 /**@type {SlashCommandAbortController}*/ abortController;23 /**@type {SlashCommandAbortController}*/ abortController;
24 /**@type {SlashCommandBreakController}*/ breakController;
25 /**@type {SlashCommandDebugController}*/ debugController;
20 /**@type {(done:number, total:number)=>void}*/ onProgress;26 /**@type {(done:number, total:number)=>void}*/ onProgress;
21 /**@type {string}*/ rawText;27 /**@type {string}*/ rawText;
28 /**@type {string}*/ fullText;
29 /**@type {string}*/ parserContext;
30 /**@type {string}*/ #source = uuidv4();
31 get source() { return this.#source; }
32 set source(value) {
33 this.#source = value;
34 for (const executor of this.executorList) {
35 executor.source = value;
36 }
37 }
2238
23 /**@type {number}*/39 /**@type {number}*/
24 get commandCount() {40 get commandCount() {
@@ -30,7 +46,7 @@ export class SlashCommandClosure {
30 }46 }
3147
32 toString() {48 toString() {
33 return '[Closure]';49 return `[Closure]${this.executeNow ? '()' : ''}`;
34 }50 }
3551
36 /**52 /**
@@ -43,16 +59,37 @@ export class SlashCommandClosure {
43 let isList = false;59 let isList = false;
44 let listValues = [];60 let listValues = [];
45 scope = scope ?? this.scope;61 scope = scope ?? this.scope;
46 const macros = scope.macroList.map(it=>escapeRegex(it.key)).join('|');62 const escapeMacro = (it, isAnchored = false)=>{
47 const re = new RegExp(`({{pipe}})|(?:{{var::([^\\s]+?)(?:::((?!}}).+))?}})|(?:{{(${macros})}})`);63 const regexText = escapeRegex(it.key.replace(/\*/g, '~~~WILDCARD~~~'))
64 .replaceAll('~~~WILDCARD~~~', '(?:(?:(?!(?:::|}})).)*)')
65 ;
66 if (isAnchored) {
67 return `^${regexText}$`;
68 }
69 return regexText;
70 };
71 const macroList = scope.macroList.toSorted((a,b)=>{
72 if (a.key.includes('*') && !b.key.includes('*')) return 1;
73 if (!a.key.includes('*') && b.key.includes('*')) return -1;
74 if (a.key.includes('*') && b.key.includes('*')) return b.key.indexOf('*') - a.key.indexOf('*');
75 return 0;
76 });
77 const macros = macroList.map(it=>escapeMacro(it)).join('|');
78 const re = new RegExp(`(?<pipe>{{pipe}})|(?:{{var::(?<var>[^\\s]+?)(?:::(?<varIndex>(?!}}).+))?}})|(?:{{(?<macro>${macros})}})`);
48 let done = '';79 let done = '';
49 let remaining = text;80 let remaining = text;
50 while (re.test(remaining)) {81 while (re.test(remaining)) {
51 const match = re.exec(remaining);82 const match = re.exec(remaining);
52 const before = substituteParams(remaining.slice(0, match.index));83 const before = substituteParams(remaining.slice(0, match.index));
53 const after = remaining.slice(match.index + match[0].length);84 const after = remaining.slice(match.index + match[0].length);
54 const replacer = match[1] ? scope.pipe : match[2] ? scope.getVariable(match[2], match[3]) : scope.macroList.find(it=>it.key == match[4])?.value;85 const replacer = match.groups.pipe ? scope.pipe : match.groups.var ? scope.getVariable(match.groups.var, match.groups.index) : macroList.find(it=>it.key == match.groups.macro || new RegExp(escapeMacro(it, true)).test(match.groups.macro))?.value;
55 if (replacer instanceof SlashCommandClosure) {86 if (replacer instanceof SlashCommandClosure) {
87 replacer.abortController = this.abortController;
88 replacer.breakController = this.breakController;
89 replacer.scope.parent = this.scope;
90 if (this.debugController && !replacer.debugController) {
91 replacer.debugController = this.debugController;
92 }
56 isList = true;93 isList = true;
57 if (match.index > 0) {94 if (match.index > 0) {
58 listValues.push(before);95 listValues.push(before);
@@ -87,6 +124,12 @@ export class SlashCommandClosure {
87 closure.providedArgumentList = this.providedArgumentList;124 closure.providedArgumentList = this.providedArgumentList;
88 closure.executorList = this.executorList;125 closure.executorList = this.executorList;
89 closure.abortController = this.abortController;126 closure.abortController = this.abortController;
127 closure.breakController = this.breakController;
128 closure.debugController = this.debugController;
129 closure.rawText = this.rawText;
130 closure.fullText = this.fullText;
131 closure.parserContext = this.parserContext;
132 closure.source = this.source;
90 closure.onProgress = this.onProgress;133 closure.onProgress = this.onProgress;
91 return closure;134 return closure;
92 }135 }
@@ -96,11 +139,22 @@ export class SlashCommandClosure {
96 * @returns {Promise<SlashCommandClosureResult>}139 * @returns {Promise<SlashCommandClosureResult>}
97 */140 */
98 async execute() {141 async execute() {
142 // execute a copy of the closure to no taint it and its scope with the effects of its execution
143 // as this would affect the closure being called a second time (e.g., loop, multiple /run calls)
99 const closure = this.getCopy();144 const closure = this.getCopy();
100 return await closure.executeDirect();145 const gen = closure.executeDirect();
146 let step;
147 while (!step?.done) {
148 step = await gen.next(this.debugController?.testStepping(this) ?? false);
149 if (!(step.value instanceof SlashCommandClosureResult) && this.debugController) {
150 this.debugController.isStepping = await this.debugController.awaitBreakPoint(step.value.closure, step.value.executor);
151 }
152 }
153 return step.value;
101 }154 }
102155
103 async executeDirect() {156 async * executeDirect() {
157 this.debugController?.down(this);
104 // closure arguments158 // closure arguments
105 for (const arg of this.argumentList) {159 for (const arg of this.argumentList) {
106 let v = arg.value;160 let v = arg.value;
@@ -108,6 +162,7 @@ export class SlashCommandClosure {
108 /**@type {SlashCommandClosure}*/162 /**@type {SlashCommandClosure}*/
109 const closure = v;163 const closure = v;
110 closure.scope.parent = this.scope;164 closure.scope.parent = this.scope;
165 closure.breakController = this.breakController;
111 if (closure.executeNow) {166 if (closure.executeNow) {
112 v = (await closure.execute())?.pipe;167 v = (await closure.execute())?.pipe;
113 } else {168 } else {
@@ -131,6 +186,7 @@ export class SlashCommandClosure {
131 /**@type {SlashCommandClosure}*/186 /**@type {SlashCommandClosure}*/
132 const closure = v;187 const closure = v;
133 closure.scope.parent = this.scope;188 closure.scope.parent = this.scope;
189 closure.breakController = this.breakController;
134 if (closure.executeNow) {190 if (closure.executeNow) {
135 v = (await closure.execute())?.pipe;191 v = (await closure.execute())?.pipe;
136 } else {192 } else {
@@ -149,106 +205,145 @@ export class SlashCommandClosure {
149 this.scope.setVariable(arg.name, v);205 this.scope.setVariable(arg.name, v);
150 }206 }
151207
152 let done = 0;
153 if (this.executorList.length == 0) {208 if (this.executorList.length == 0) {
154 this.scope.pipe = '';209 this.scope.pipe = '';
155 }210 }
156 for (const executor of this.executorList) {211 const stepper = this.executeStep();
157 this.onProgress?.(done, this.commandCount);212 let step;
158 if (executor instanceof SlashCommandClosureExecutor) {213 while (!step?.done && !this.breakController?.isBreak) {
159 const closure = this.scope.getVariable(executor.name);214 // get executor before execution
160 if (!closure || !(closure instanceof SlashCommandClosure)) throw new Error(`${executor.name} is not a closure.`);215 step = await stepper.next();
161 closure.scope.parent = this.scope;216 if (step.value instanceof SlashCommandBreakPoint) {
162 closure.providedArgumentList = executor.providedArgumentList;217 console.log('encountered SlashCommandBreakPoint');
163 const result = await closure.execute();218 if (this.debugController) {
164 this.scope.pipe = result.pipe;219 // resolve args
165 } else {220 step = await stepper.next();
166 /**@type {import('./SlashCommand.js').NamedArguments} */221 // "execute" breakpoint
167 let args = {222 step = await stepper.next();
168 _scope: this.scope,223 // get next executor
169 _parserFlags: executor.parserFlags,224 step = await stepper.next();
170 _abortController: this.abortController,225 // breakpoint has to yield before arguments are resolved if one of the
171 _hasUnnamedArgument: executor.unnamedArgumentList.length > 0,226 // arguments is an immediate closure, otherwise you cannot step into the
172 };227 // immediate closure
173 let value;228 const hasImmediateClosureInNamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.namedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow);
174 // substitute named arguments229 const hasImmediateClosureInUnnamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.unnamedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow);
175 for (const arg of executor.namedArgumentList) {230 if (hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs) {
176 if (arg.value instanceof SlashCommandClosure) {231 this.debugController.isStepping = yield { closure:this, executor:step.value };
177 /**@type {SlashCommandClosure}*/
178 const closure = arg.value;
179 closure.scope.parent = this.scope;
180 if (closure.executeNow) {
181 args[arg.name] = (await closure.execute())?.pipe;
182 } else {
183 args[arg.name] = closure;
184 }
185 } else {232 } else {
186 args[arg.name] = this.substituteParams(arg.value);233 this.debugController.isStepping = true;
187 }234 this.debugController.stepStack[this.debugController.stepStack.length - 1] = true;
188 // unescape named argument
189 if (typeof args[arg.name] == 'string') {
190 args[arg.name] = args[arg.name]
191 ?.replace(/\\\{/g, '{')
192 ?.replace(/\\\}/g, '}')
193 ;
194 }235 }
195 }236 }
196237 } else if (!step.done && this.debugController?.testStepping(this)) {
197 // substitute unnamed argument238 this.debugController.isSteppingInto = false;
198 if (executor.unnamedArgumentList.length == 0) {239 // if stepping, have to yield before arguments are resolved if one of the arguments
199 if (executor.injectPipe) {240 // is an immediate closure, otherwise you cannot step into the immediate closure
200 value = this.scope.pipe;241 const hasImmediateClosureInNamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.namedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow);
201 args._hasUnnamedArgument = this.scope.pipe !== null && this.scope.pipe !== undefined;242 const hasImmediateClosureInUnnamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.unnamedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow);
202 }243 if (hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs) {
203 } else {244 this.debugController.isStepping = yield { closure:this, executor:step.value };
204 value = [];
205 for (let i = 0; i < executor.unnamedArgumentList.length; i++) {
206 let v = executor.unnamedArgumentList[i].value;
207 if (v instanceof SlashCommandClosure) {
208 /**@type {SlashCommandClosure}*/
209 const closure = v;
210 closure.scope.parent = this.scope;
211 if (closure.executeNow) {
212 v = (await closure.execute())?.pipe;
213 } else {
214 v = closure;
215 }
216 } else {
217 v = this.substituteParams(v);
218 }
219 value[i] = v;
220 }
221 if (!executor.command.splitUnnamedArgument) {
222 if (value.length == 1) {
223 value = value[0];
224 } else if (!value.find(it=>it instanceof SlashCommandClosure)) {
225 value = value.join('');
226 }
227 }
228 }245 }
229 // unescape unnamed argument246 }
230 if (typeof value == 'string') {247 // resolve args
231 value = value248 step = await stepper.next();
232 ?.replace(/\\\{/g, '{')249 if (step.value instanceof SlashCommandBreak) {
233 ?.replace(/\\\}/g, '}')250 console.log('encountered SlashCommandBreak');
234 ;251 if (this.breakController) {
235 } else if (Array.isArray(value)) {252 this.breakController?.break();
236 value = value.map(v=>{253 break;
237 if (typeof v == 'string') {
238 return v
239 ?.replace(/\\\{/g, '{')
240 ?.replace(/\\\}/g, '}');
241 }
242 return v;
243 });
244 }254 }
255 } else if (!step.done && this.debugController?.testStepping(this)) {
256 this.debugController.isSteppingInto = false;
257 this.debugController.isStepping = yield { closure:this, executor:step.value };
258 }
259 // execute executor
260 step = await stepper.next();
261 }
262
263 // if execution has returned a closure result, return that (should only happen on abort)
264 if (step.value instanceof SlashCommandClosureResult) {
265 this.debugController?.up();
266 return step.value;
267 }
268 /**@type {SlashCommandClosureResult} */
269 const result = Object.assign(new SlashCommandClosureResult(), { pipe: this.scope.pipe, isBreak: this.breakController?.isBreak ?? false });
270 this.debugController?.up();
271 return result;
272 }
273 /**
274 * Generator that steps through the executor list.
275 * Every executor is split into three steps:
276 * - before arguments are resolved
277 * - after arguments are resolved
278 * - after execution
279 */
280 async * executeStep() {
281 let done = 0;
282 let isFirst = true;
283 for (const executor of this.executorList) {
284 this.onProgress?.(done, this.commandCount);
285 if (this.debugController) {
286 this.debugController.setExecutor(executor);
287 this.debugController.namedArguments = undefined;
288 this.debugController.unnamedArguments = undefined;
289 }
290 // yield before doing anything with this executor, the debugger might want to do
291 // something with it (e.g., breakpoint, immediate closures that need resolving
292 // or stepping into)
293 yield executor;
294 /**@type {import('./SlashCommand.js').NamedArguments} */
295 // @ts-ignore
296 let args = {
297 _scope: this.scope,
298 _parserFlags: executor.parserFlags,
299 _abortController: this.abortController,
300 _debugController: this.debugController,
301 _hasUnnamedArgument: executor.unnamedArgumentList.length > 0,
302 };
303 if (executor instanceof SlashCommandBreakPoint) {
304 // nothing to do for breakpoints, just raise counter and yield for "before exec"
305 done++;
306 yield executor;
307 isFirst = false;
308 } else if (executor instanceof SlashCommandBreak) {
309 // /break need to resolve the unnamed arg and put it into pipe, then yield
310 // for "before exec"
311 const value = await this.substituteUnnamedArgument(executor, isFirst, args);
312 done += this.executorList.length - this.executorList.indexOf(executor);
313 this.scope.pipe = value ?? this.scope.pipe;
314 yield executor;
315 isFirst = false;
316 } else {
317 // regular commands do all the argument resolving logic...
318 await this.substituteNamedArguments(executor, args);
319 let value = await this.substituteUnnamedArgument(executor, isFirst, args);
245320
246 let abortResult = await this.testAbortController();321 let abortResult = await this.testAbortController();
247 if (abortResult) {322 if (abortResult) {
248 return abortResult;323 return abortResult;
249 }324 }
325 if (this.debugController) {
326 this.debugController.namedArguments = args;
327 this.debugController.unnamedArguments = value ?? '';
328 }
329 // then yield for "before exec"
330 yield executor;
331 // followed by command execution
250 executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount);332 executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount);
251 this.scope.pipe = await executor.command.callback(args, value ?? '');333 const isStepping = this.debugController?.testStepping(this);
334 if (this.debugController) {
335 this.debugController.isStepping = false || this.debugController.isSteppingInto;
336 }
337 try {
338 this.scope.pipe = await executor.command.callback(args, value ?? '');
339 } catch (ex) {
340 throw new SlashCommandExecutionError(ex, ex.message, executor.name, executor.start, executor.end, this.fullText.slice(executor.start, executor.end), this.fullText);
341 }
342 if (this.debugController) {
343 this.debugController.namedArguments = undefined;
344 this.debugController.unnamedArguments = undefined;
345 this.debugController.isStepping = isStepping;
346 }
252 this.#lintPipe(executor.command);347 this.#lintPipe(executor.command);
253 done += executor.commandCount;348 done += executor.commandCount;
254 this.onProgress?.(done, this.commandCount);349 this.onProgress?.(done, this.commandCount);
@@ -257,10 +352,10 @@ export class SlashCommandClosure {
257 return abortResult;352 return abortResult;
258 }353 }
259 }354 }
355 // finally, yield for "after exec"
356 yield executor;
357 isFirst = false;
260 }358 }
261 /**@type {SlashCommandClosureResult} */
262 const result = Object.assign(new SlashCommandClosureResult(), { pipe: this.scope.pipe });
263 return result;
264 }359 }
265360
266 async testPaused() {361 async testPaused() {
@@ -280,13 +375,112 @@ export class SlashCommandClosure {
280 }375 }
281376
282 /**377 /**
378 * @param {SlashCommandExecutor} executor
379 * @param {import('./SlashCommand.js').NamedArguments} args
380 */
381 async substituteNamedArguments(executor, args) {
382 // substitute named arguments
383 for (const arg of executor.namedArgumentList) {
384 if (arg.value instanceof SlashCommandClosure) {
385 /**@type {SlashCommandClosure}*/
386 const closure = arg.value;
387 closure.scope.parent = this.scope;
388 closure.breakController = this.breakController;
389 if (this.debugController && !closure.debugController) {
390 closure.debugController = this.debugController;
391 }
392 if (closure.executeNow) {
393 args[arg.name] = (await closure.execute())?.pipe;
394 } else {
395 args[arg.name] = closure;
396 }
397 } else {
398 args[arg.name] = this.substituteParams(arg.value);
399 }
400 // unescape named argument
401 if (typeof args[arg.name] == 'string') {
402 args[arg.name] = args[arg.name]
403 ?.replace(/\\\{/g, '{')
404 ?.replace(/\\\}/g, '}')
405 ;
406 }
407 }
408 }
409
410 /**
411 * @param {SlashCommandExecutor} executor
412 * @param {boolean} isFirst
413 * @param {import('./SlashCommand.js').NamedArguments} args
414 * @returns {Promise<string|SlashCommandClosure|(string|SlashCommandClosure)[]>}
415 */
416 async substituteUnnamedArgument(executor, isFirst, args) {
417 let value;
418 // substitute unnamed argument
419 if (executor.unnamedArgumentList.length == 0) {
420 if (!isFirst && executor.injectPipe) {
421 value = this.scope.pipe;
422 args._hasUnnamedArgument = this.scope.pipe !== null && this.scope.pipe !== undefined;
423 }
424 } else {
425 value = [];
426 for (let i = 0; i < executor.unnamedArgumentList.length; i++) {
427 let v = executor.unnamedArgumentList[i].value;
428 if (v instanceof SlashCommandClosure) {
429 /**@type {SlashCommandClosure}*/
430 const closure = v;
431 closure.scope.parent = this.scope;
432 closure.breakController = this.breakController;
433 if (this.debugController && !closure.debugController) {
434 closure.debugController = this.debugController;
435 }
436 if (closure.executeNow) {
437 v = (await closure.execute())?.pipe;
438 } else {
439 v = closure;
440 }
441 } else {
442 v = this.substituteParams(v);
443 }
444 value[i] = v;
445 }
446 if (!executor.command.splitUnnamedArgument) {
447 if (value.length == 1) {
448 value = value[0];
449 } else if (!value.find(it=>it instanceof SlashCommandClosure)) {
450 value = value.join('');
451 }
452 }
453 }
454 // unescape unnamed argument
455 if (typeof value == 'string') {
456 value = value
457 ?.replace(/\\\{/g, '{')
458 ?.replace(/\\\}/g, '}')
459 ;
460 } else if (Array.isArray(value)) {
461 value = value.map(v=>{
462 if (typeof v == 'string') {
463 return v
464 ?.replace(/\\\{/g, '{')
465 ?.replace(/\\\}/g, '}');
466 }
467 return v;
468 });
469 }
470 return value;
471 }
472
473 /**
283 * Auto-fixes the pipe if it is not a valid result for STscript.474 * Auto-fixes the pipe if it is not a valid result for STscript.
284 * @param {SlashCommand} command Command being executed475 * @param {SlashCommand} command Command being executed
285 */476 */
286 #lintPipe(command) {477 #lintPipe(command) {
287 if (this.scope.pipe === undefined || this.scope.pipe === null) {478 if (this.scope.pipe === undefined || this.scope.pipe === null) {
288 console.warn(`${command.name} returned undefined or null. Auto-fixing to empty string.`);479 console.warn(`/${command.name} returned undefined or null. Auto-fixing to empty string.`);
289 this.scope.pipe = '';480 this.scope.pipe = '';
481 } else if (!(typeof this.scope.pipe == 'string' || this.scope.pipe instanceof SlashCommandClosure)) {
482 console.warn(`/${command.name} returned illegal type (${typeof this.scope.pipe} - ${this.scope.pipe.constructor?.name ?? ''}). Auto-fixing to stringified JSON.`);
483 this.scope.pipe = JSON.stringify(this.scope.pipe) ?? '';
290 }484 }
291 }485 }
292}486}
public/scripts/slash-commands/SlashCommandClosureExecutor.js+0 -7
@@ -1,7 +0,0 @@
1import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
2
3export class SlashCommandClosureExecutor {
4 /**@type {String}*/ name = '';
5 // @ts-ignore
6 /**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = [];
7}
public/scripts/slash-commands/SlashCommandClosureResult.js+1 -0
@@ -1,6 +1,7 @@
1export class SlashCommandClosureResult {1export class SlashCommandClosureResult {
2 /**@type {boolean}*/ interrupt = false;2 /**@type {boolean}*/ interrupt = false;
3 /**@type {string}*/ pipe;3 /**@type {string}*/ pipe;
4 /**@type {boolean}*/ isBreak = false;
4 /**@type {boolean}*/ isAborted = false;5 /**@type {boolean}*/ isAborted = false;
5 /**@type {boolean}*/ isQuietlyAborted = false;6 /**@type {boolean}*/ isQuietlyAborted = false;
6 /**@type {string}*/ abortReason;7 /**@type {string}*/ abortReason;
public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js+6 -5
@@ -6,6 +6,7 @@ import { searchCharByName, getTagsList, tags } from '../tags.js';
6import { world_names } from '../world-info.js';6import { world_names } from '../world-info.js';
7import { SlashCommandClosure } from './SlashCommandClosure.js';7import { SlashCommandClosure } from './SlashCommandClosure.js';
8import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';8import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';
9import { SlashCommandScope } from "./SlashCommandScope.js";
910
10/**11/**
11 * A collection of regularly used enum icons12 * A collection of regularly used enum icons
@@ -134,16 +135,16 @@ export const commonEnumProviders = {
134 * Can be filtered by `type` to only show global or local variables135 * Can be filtered by `type` to only show global or local variables
135 *136 *
136 * @param {...('global'|'local'|'scope'|'all')} type - The type of variables to include in the array. Can be 'all', 'global', or 'local'.137 * @param {...('global'|'local'|'scope'|'all')} type - The type of variables to include in the array. Can be 'all', 'global', or 'local'.
137 * @returns {() => SlashCommandEnumValue[]}138 * @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]}
138 */139 */
139 variables: (...type) => () => {140 variables: (...type) => (executor, scope) => {
140 const types = type.flat();141 const types = type.flat();
141 const isAll = types.includes('all');142 const isAll = types.includes('all');
142 return [143 return [
143 ...isAll || types.includes('global') ? Object.keys(extension_settings.variables.global ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.macro, enumIcons.globalVariable)) : [],144 ...isAll || types.includes('scope') ? scope.allVariableNames.map(name => new SlashCommandEnumValue(name, null, enumTypes.variable, enumIcons.scopeVariable)) : [],
144 ...isAll || types.includes('local') ? Object.keys(chat_metadata.variables ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.localVariable)) : [],145 ...isAll || types.includes('local') ? Object.keys(chat_metadata.variables ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.localVariable)) : [],
145 ...isAll || types.includes('scope') ? [].map(name => new SlashCommandEnumValue(name, null, enumTypes.variable, enumIcons.scopeVariable)) : [], // TODO: Add scoped variables here, Lenny146 ...isAll || types.includes('global') ? Object.keys(extension_settings.variables.global ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.macro, enumIcons.globalVariable)) : [],
146 ];147 ].filter((item, idx, list)=>idx == list.findIndex(it=>it.value == item.value));
147 },148 },
148149
149 /**150 /**
public/scripts/slash-commands/SlashCommandDebugController.js+83 -0
@@ -0,0 +1,83 @@
1import { SlashCommandClosure } from './SlashCommandClosure.js';
2import { SlashCommandExecutor } from './SlashCommandExecutor.js';
3
4export class SlashCommandDebugController {
5 /**@type {SlashCommandClosure[]} */ stack = [];
6 /**@type {SlashCommandExecutor[]} */ cmdStack = [];
7 /**@type {boolean[]} */ stepStack = [];
8 /**@type {boolean} */ isStepping = false;
9 /**@type {boolean} */ isSteppingInto = false;
10 /**@type {boolean} */ isSteppingOut = false;
11
12 /**@type {object} */ namedArguments;
13 /**@type {string|SlashCommandClosure|(string|SlashCommandClosure)[]} */ unnamedArguments;
14
15 /**@type {Promise<boolean>} */ continuePromise;
16 /**@type {(boolean)=>void} */ continueResolver;
17
18 /**@type {(closure:SlashCommandClosure, executor:SlashCommandExecutor)=>Promise<boolean>} */ onBreakPoint;
19
20
21
22
23 testStepping(closure) {
24 return this.stepStack[this.stack.indexOf(closure)];
25 }
26
27
28
29
30 down(closure) {
31 this.stack.push(closure);
32 if (this.stepStack.length < this.stack.length) {
33 this.stepStack.push(this.isSteppingInto);
34 }
35 }
36 up() {
37 this.stack.pop();
38 while (this.cmdStack.length > this.stack.length) this.cmdStack.pop();
39 this.stepStack.pop();
40 }
41
42 setExecutor(executor) {
43 this.cmdStack[this.stack.length - 1] = executor;
44 }
45
46
47
48 resume() {
49 this.continueResolver?.(false);
50 this.continuePromise = null;
51 this.stepStack.forEach((_,idx)=>this.stepStack[idx] = false);
52 }
53 step() {
54 this.stepStack.forEach((_,idx)=>this.stepStack[idx] = true);
55 this.continueResolver?.(true);
56 this.continuePromise = null;
57 }
58 stepInto() {
59 this.isSteppingInto = true;
60 this.stepStack.forEach((_,idx)=>this.stepStack[idx] = true);
61 this.continueResolver?.(true);
62 this.continuePromise = null;
63 }
64 stepOut() {
65 this.isSteppingOut = true;
66 this.stepStack[this.stepStack.length - 1] = false;
67 this.continueResolver?.(false);
68 this.continuePromise = null;
69 }
70
71 async awaitContinue() {
72 this.continuePromise ??= new Promise(resolve=>{
73 this.continueResolver = resolve;
74 });
75 this.isStepping = await this.continuePromise;
76 return this.isStepping;
77 }
78
79 async awaitBreakPoint(closure, executor) {
80 this.isStepping = await this.onBreakPoint(closure, executor);
81 return this.isStepping;
82 }
83}
public/scripts/slash-commands/SlashCommandEnumAutoCompleteOption.js+12 -1
@@ -3,6 +3,17 @@ import { SlashCommand } from './SlashCommand.js';
3import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';3import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
44
5export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {5export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
6 /**
7 * @param {SlashCommand} cmd
8 * @param {SlashCommandEnumValue} enumValue
9 * @returns {SlashCommandEnumAutoCompleteOption}
10 */
11 static from(cmd, enumValue) {
12 const mapped = this.valueToOptionMap.find(it=>enumValue instanceof it.value)?.option ?? this;
13 return new mapped(cmd, enumValue);
14 }
15 /**@type {{value:(typeof SlashCommandEnumValue), option:(typeof SlashCommandEnumAutoCompleteOption)}[]} */
16 static valueToOptionMap = [];
6 /**@type {SlashCommand}*/ cmd;17 /**@type {SlashCommand}*/ cmd;
7 /**@type {SlashCommandEnumValue}*/ enumValue;18 /**@type {SlashCommandEnumValue}*/ enumValue;
819
@@ -13,7 +24,7 @@ export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
13 * @param {SlashCommandEnumValue} enumValue24 * @param {SlashCommandEnumValue} enumValue
14 */25 */
15 constructor(cmd, enumValue) {26 constructor(cmd, enumValue) {
16 super(enumValue.value, enumValue.typeIcon, enumValue.type);27 super(enumValue.value, enumValue.typeIcon, enumValue.type, enumValue.matchProvider, enumValue.valueProvider, enumValue.makeSelectable);
17 this.cmd = cmd;28 this.cmd = cmd;
18 this.enumValue = enumValue;29 this.enumValue = enumValue;
19 }30 }
public/scripts/slash-commands/SlashCommandEnumValue.js+16 -4
@@ -1,3 +1,6 @@
1import { SlashCommandExecutor } from './SlashCommandExecutor.js';
2import { SlashCommandScope } from './SlashCommandScope.js';
3
14
2/**5/**
3 * @typedef {'enum' | 'command' | 'namedArgument' | 'variable' | 'qr' | 'macro' | 'number' | 'name'} EnumType6 * @typedef {'enum' | 'command' | 'namedArgument' | 'variable' | 'qr' | 'macro' | 'number' | 'name'} EnumType
@@ -37,14 +40,17 @@ export const enumTypes = {
37 getBasedOnIndex(index) {40 getBasedOnIndex(index) {
38 const keys = Object.keys(this);41 const keys = Object.keys(this);
39 return this[keys[(index ?? 0) % keys.length]];42 return this[keys[(index ?? 0) % keys.length]];
40 }43 },
41}44};
4245
43export class SlashCommandEnumValue {46export class SlashCommandEnumValue {
44 /**@type {string}*/ value;47 /**@type {string}*/ value;
45 /**@type {string}*/ description;48 /**@type {string}*/ description;
46 /**@type {EnumType}*/ type = 'enum';49 /**@type {EnumType}*/ type = 'enum';
47 /**@type {string}*/ typeIcon = '◊';50 /**@type {string}*/ typeIcon = '◊';
51 /**@type {(input:string)=>boolean}*/ matchProvider;
52 /**@type {(input:string)=>string}*/ valueProvider;
53 /**@type {boolean}*/ makeSelectable = false;
4854
49 /**55 /**
50 * A constructor for creating a SlashCommandEnumValue instance.56 * A constructor for creating a SlashCommandEnumValue instance.
@@ -52,13 +58,19 @@ export class SlashCommandEnumValue {
52 * @param {string} value - The value58 * @param {string} value - The value
53 * @param {string?} description - Optional description, displayed in a second line59 * @param {string?} description - Optional description, displayed in a second line
54 * @param {EnumType?} type - type of the enum (defining its color)60 * @param {EnumType?} type - type of the enum (defining its color)
55 * @param {string} typeIcon - The icon to display (Can be pulled from `enumIcons` for common ones)61 * @param {string?} typeIcon - The icon to display (Can be pulled from `enumIcons` for common ones)
62 * @param {(input:string)=>boolean?} matchProvider - A custom function to match autocomplete input instead of startsWith/includes/fuzzy. Should only be used for generic options like "any number" or "any string". "input" is the part of the text that is getting auto completed.
63 * @param {(input:string)=>string?} valueProvider - A function returning a value to be used in autocomplete instead of the enum value. "input" is the part of the text that is getting auto completed. By default, values with a valueProvider will not be selectable in the autocomplete (with tab/enter).
64 * @param {boolean?} makeSelectable - Set to true to make the value selectable (through tab/enter) even though a valueProvider exists.
56 */65 */
57 constructor(value, description = null, type = 'enum', typeIcon = '◊') {66 constructor(value, description = null, type = 'enum', typeIcon = '◊', matchProvider = null, valueProvider = null, makeSelectable = false) {
58 this.value = value;67 this.value = value;
59 this.description = description;68 this.description = description;
60 this.type = type ?? 'enum';69 this.type = type ?? 'enum';
61 this.typeIcon = typeIcon;70 this.typeIcon = typeIcon;
71 this.matchProvider = matchProvider;
72 this.valueProvider = valueProvider;
73 this.makeSelectable = makeSelectable;
62 }74 }
6375
64 toString() {76 toString() {
public/scripts/slash-commands/SlashCommandExecutionError.js+60 -0
@@ -0,0 +1,60 @@
1export class SlashCommandExecutionError extends Error {
2 /**@type {string} */ commandName;
3 /**@type {number} */ start;
4 /**@type {number} */ end;
5 /**@type {string} */ commandText;
6
7 /**@type {string} */ text;
8 get index() { return this.start; }
9
10 get line() {
11 return this.text.slice(0, this.index).replace(/[^\n]/g, '').length;
12 }
13 get column() {
14 return this.text.slice(0, this.index).split('\n').pop().length;
15 }
16 get hint() {
17 let lineOffset = this.line.toString().length;
18 let lineStart = this.index;
19 let start = this.index;
20 let end = this.index;
21 let offset = 0;
22 let lineCount = 0;
23 while (offset < 10000 && lineCount < 3 && start >= 0) {
24 if (this.text[start] == '\n') lineCount++;
25 if (lineCount == 0) lineStart--;
26 offset++;
27 start--;
28 }
29 if (this.text[start + 1] == '\n') start++;
30 offset = 0;
31 while (offset < 10000 && this.text[end] != '\n') {
32 offset++;
33 end++;
34 }
35 let hint = [];
36 let lines = this.text.slice(start + 1, end - 1).split('\n');
37 let lineNum = this.line - lines.length + 1;
38 let tabOffset = 0;
39 for (const line of lines) {
40 const num = `${' '.repeat(lineOffset - lineNum.toString().length)}${lineNum}`;
41 lineNum++;
42 const untabbedLine = line.replace(/\t/g, ' '.repeat(4));
43 tabOffset = untabbedLine.length - line.length;
44 hint.push(`${num}: ${untabbedLine}`);
45 }
46 hint.push(`${' '.repeat(this.index - lineStart + lineOffset + 1 + tabOffset)}^^^^^`);
47 return hint.join('\n');
48 }
49
50
51
52 constructor(cause, message, commandName, start, end, commandText, fullText) {
53 super(message, { cause });
54 this.commandName = commandName;
55 this.start = start;
56 this.end = end;
57 this.commandText = commandText;
58 this.text = fullText;
59 }
60}
public/scripts/slash-commands/SlashCommandExecutor.js+12 -0
@@ -1,4 +1,5 @@
1// eslint-disable-next-line no-unused-vars1// eslint-disable-next-line no-unused-vars
2import { uuidv4 } from '../utils.js';
2import { SlashCommand } from './SlashCommand.js';3import { SlashCommand } from './SlashCommand.js';
3// eslint-disable-next-line no-unused-vars4// eslint-disable-next-line no-unused-vars
4import { SlashCommandClosure } from './SlashCommandClosure.js';5import { SlashCommandClosure } from './SlashCommandClosure.js';
@@ -16,6 +17,17 @@ export class SlashCommandExecutor {
16 /**@type {Number}*/ startUnnamedArgs;17 /**@type {Number}*/ startUnnamedArgs;
17 /**@type {Number}*/ endUnnamedArgs;18 /**@type {Number}*/ endUnnamedArgs;
18 /**@type {String}*/ name = '';19 /**@type {String}*/ name = '';
20 /**@type {String}*/ #source = uuidv4();
21 get source() { return this.#source; }
22 set source(value) {
23 this.#source = value;
24 for (const arg of this.namedArgumentList.filter(it=>it.value instanceof SlashCommandClosure)) {
25 arg.value.source = value;
26 }
27 for (const arg of this.unnamedArgumentList.filter(it=>it.value instanceof SlashCommandClosure)) {
28 arg.value.source = value;
29 }
30 }
19 /**@type {SlashCommand}*/ command;31 /**@type {SlashCommand}*/ command;
20 // @ts-ignore32 // @ts-ignore
21 /**@type {SlashCommandNamedArgumentAssignment[]}*/ namedArgumentList = [];33 /**@type {SlashCommandNamedArgumentAssignment[]}*/ namedArgumentList = [];
public/scripts/slash-commands/SlashCommandParser.js+321 -26
@@ -17,7 +17,10 @@ import { SlashCommandAutoCompleteNameResult } from './SlashCommandAutoCompleteNa
17import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js';17import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js';
18import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';18import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
19import { MacroAutoCompleteOption } from '../autocomplete/MacroAutoCompleteOption.js';19import { MacroAutoCompleteOption } from '../autocomplete/MacroAutoCompleteOption.js';
20import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js';
21import { SlashCommandDebugController } from './SlashCommandDebugController.js';
20import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js';22import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js';
23import { SlashCommandBreak } from './SlashCommandBreak.js';
2124
22/** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */25/** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */
23/** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */26/** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */
@@ -53,7 +56,7 @@ export class SlashCommandParser {
53 * @param {SlashCommand} command56 * @param {SlashCommand} command
54 */57 */
55 static addCommandObject(command) {58 static addCommandObject(command) {
56 const reserved = ['/', '#', ':', 'parser-flag'];59 const reserved = ['/', '#', ':', 'parser-flag', 'breakpoint'];
57 for (const start of reserved) {60 for (const start of reserved) {
58 if (command.name.toLowerCase().startsWith(start) || (command.aliases ?? []).find(a=>a.toLowerCase().startsWith(start))) {61 if (command.name.toLowerCase().startsWith(start) || (command.aliases ?? []).find(a=>a.toLowerCase().startsWith(start))) {
59 throw new Error(`Illegal Name. Slash command name cannot begin with "${start}".`);62 throw new Error(`Illegal Name. Slash command name cannot begin with "${start}".`);
@@ -70,6 +73,18 @@ export class SlashCommandParser {
70 console.trace('WARN: Duplicate slash command registered!', [command.name, ...command.aliases]);73 console.trace('WARN: Duplicate slash command registered!', [command.name, ...command.aliases]);
71 }74 }
7275
76 const stack = new Error().stack.split('\n').map(it=>it.trim());
77 command.isExtension = stack.find(it=>it.includes('/scripts/extensions/')) != null;
78 command.isThirdParty = stack.find(it=>it.includes('/scripts/extensions/third-party/')) != null;
79 if (command.isThirdParty) {
80 command.source = stack.find(it=>it.includes('/scripts/extensions/third-party/')).replace(/^.*?\/scripts\/extensions\/third-party\/([^/]+)\/.*$/, '$1');
81 } else if (command.isExtension) {
82 command.source = stack.find(it=>it.includes('/scripts/extensions/')).replace(/^.*?\/scripts\/extensions\/([^/]+)\/.*$/, '$1');
83 } else {
84 const idx = stack.findLastIndex(it=>it.includes('at SlashCommandParser.')) + 1;
85 command.source = stack[idx].replace(/^.*?\/((?:scripts\/)?(?:[^/]+)\.js).*$/, '$1');
86 }
87
73 this.commands[command.name] = command;88 this.commands[command.name] = command;
7489
75 if (Array.isArray(command.aliases)) {90 if (Array.isArray(command.aliases)) {
@@ -89,6 +104,7 @@ export class SlashCommandParser {
89 /**@type {string}*/ text;104 /**@type {string}*/ text;
90 /**@type {number}*/ index;105 /**@type {number}*/ index;
91 /**@type {SlashCommandAbortController}*/ abortController;106 /**@type {SlashCommandAbortController}*/ abortController;
107 /**@type {SlashCommandDebugController}*/ debugController;
92 /**@type {SlashCommandScope}*/ scope;108 /**@type {SlashCommandScope}*/ scope;
93 /**@type {SlashCommandClosure}*/ closure;109 /**@type {SlashCommandClosure}*/ closure;
94110
@@ -101,6 +117,8 @@ export class SlashCommandParser {
101 /**@type {SlashCommandExecutor[]}*/ commandIndex;117 /**@type {SlashCommandExecutor[]}*/ commandIndex;
102 /**@type {SlashCommandScope[]}*/ scopeIndex;118 /**@type {SlashCommandScope[]}*/ scopeIndex;
103119
120 /**@type {string}*/ parserContext;
121
104 get userIndex() { return this.index; }122 get userIndex() { return this.index; }
105123
106 get ahead() {124 get ahead() {
@@ -154,6 +172,21 @@ export class SlashCommandParser {
154 helpString: 'Write a comment.',172 helpString: 'Write a comment.',
155 }));173 }));
156 }174 }
175 if (!Object.keys(this.commands).includes('breakpoint')) {
176 SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: 'breakpoint',
177 helpString: 'Set a breakpoint for debugging in the QR Editor.',
178 }));
179 }
180 if (!Object.keys(this.commands).includes('break')) {
181 SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: 'break',
182 helpString: 'Break out of a loop or closure executed through /run or /:',
183 unnamedArgumentList: [
184 SlashCommandArgument.fromProps({ description: 'value to pass down the pipe instead of the current pipe value',
185 typeList: Object.values(ARGUMENT_TYPE),
186 }),
187 ],
188 }));
189 }
157190
158 //TODO should not be re-registered from every instance191 //TODO should not be re-registered from every instance
159 this.registerLanguage();192 this.registerLanguage();
@@ -191,13 +224,19 @@ export class SlashCommandParser {
191224
192 function getQuotedRunRegex() {225 function getQuotedRunRegex() {
193 try {226 try {
194 return new RegExp('(".+?(?<!\\\\)")|(\\S+?)');227 return new RegExp('(".+?(?<!\\\\)")|(\\S+?)(\\||$|\\s)');
195 } catch {228 } catch {
196 // fallback for browsers that don't support lookbehind229 // fallback for browsers that don't support lookbehind
197 return /(".+?")|(\S+?)/;230 return /(".+?")|(\S+?)(\||$|\s)/;
198 }231 }
199 }232 }
200233
234 const BLOCK_COMMENT = {
235 scope: 'comment',
236 begin: /\/\*/,
237 end: /\*\|/,
238 contains: [],
239 };
201 const COMMENT = {240 const COMMENT = {
202 scope: 'comment',241 scope: 'comment',
203 begin: /\/[/#]/,242 begin: /\/[/#]/,
@@ -205,9 +244,29 @@ export class SlashCommandParser {
205 contains: [],244 contains: [],
206 };245 };
207 const ABORT = {246 const ABORT = {
208 scope: 'abort',247 begin: /\/(abort|breakpoint)/,
209 begin: /\/abort/,248 beginScope: 'abort',
210 end: /\||$|:}/,249 end: /\||$|(?=:})/,
250 excludeEnd: false,
251 returnEnd: true,
252 contains: [],
253 };
254 const IMPORT = {
255 scope: 'command',
256 begin: /\/(import)/,
257 beginScope: 'keyword',
258 end: /\||$|(?=:})/,
259 excludeEnd: false,
260 returnEnd: true,
261 contains: [],
262 };
263 const BREAK = {
264 scope: 'command',
265 begin: /\/(break)/,
266 beginScope: 'keyword',
267 end: /\||$|(?=:})/,
268 excludeEnd: false,
269 returnEnd: true,
211 contains: [],270 contains: [],
212 };271 };
213 const LET = {272 const LET = {
@@ -218,26 +277,31 @@ export class SlashCommandParser {
218 1: 'variable',277 1: 'variable',
219 },278 },
220 end: /\||$|:}/,279 end: /\||$|:}/,
280 excludeEnd: false,
281 returnEnd: true,
221 contains: [],282 contains: [],
222 };283 };
223 const SETVAR = {284 const SETVAR = {
224 begin: /\/(setvar|setglobalvar)\s+/,285 begin: /\/(setvar|setglobalvar)\s+/,
225 beginScope: 'variable',286 beginScope: 'variable',
226 end: /\||$|:}/,287 end: /\||$|:}/,
227 excludeEnd: true,288 excludeEnd: false,
289 returnEnd: true,
228 contains: [],290 contains: [],
229 };291 };
230 const GETVAR = {292 const GETVAR = {
231 begin: /\/(getvar|getglobalvar)\s+/,293 begin: /\/(getvar|getglobalvar)\s+/,
232 beginScope: 'variable',294 beginScope: 'variable',
233 end: /\||$|:}/,295 end: /\||$|:}/,
234 excludeEnd: true,296 excludeEnd: false,
297 returnEnd: true,
235 contains: [],298 contains: [],
236 };299 };
237 const RUN = {300 const RUN = {
238 match: [301 match: [
239 /\/:/,302 /\/:/,
240 getQuotedRunRegex(),303 getQuotedRunRegex(),
304 /\||$|(?=:})/,
241 ],305 ],
242 className: {306 className: {
243 1: 'variable.language',307 1: 'variable.language',
@@ -250,7 +314,8 @@ export class SlashCommandParser {
250 begin: /\/\S+/,314 begin: /\/\S+/,
251 beginScope: 'title.function',315 beginScope: 'title.function',
252 end: /\||$|(?=:})/,316 end: /\||$|(?=:})/,
253 excludeEnd: true,317 excludeEnd: false,
318 returnEnd: true,
254 contains: [], // defined later319 contains: [], // defined later
255 };320 };
256 const CLOSURE = {321 const CLOSURE = {
@@ -271,6 +336,19 @@ export class SlashCommandParser {
271 begin: /{{/,336 begin: /{{/,
272 end: /}}/,337 end: /}}/,
273 };338 };
339 const PIPEBREAK = {
340 beginScope: 'pipebreak',
341 begin: /\|\|/,
342 end: '',
343 };
344 const PIPE = {
345 beginScope: 'pipe',
346 begin: /\|/,
347 end: '',
348 };
349 BLOCK_COMMENT.contains.push(
350 BLOCK_COMMENT,
351 );
274 RUN.contains.push(352 RUN.contains.push(
275 hljs.BACKSLASH_ESCAPE,353 hljs.BACKSLASH_ESCAPE,
276 NAMED_ARG,354 NAMED_ARG,
@@ -279,6 +357,22 @@ export class SlashCommandParser {
279 MACRO,357 MACRO,
280 CLOSURE,358 CLOSURE,
281 );359 );
360 IMPORT.contains.push(
361 hljs.BACKSLASH_ESCAPE,
362 NAMED_ARG,
363 NUMBER,
364 MACRO,
365 CLOSURE,
366 hljs.QUOTE_STRING_MODE,
367 );
368 BREAK.contains.push(
369 hljs.BACKSLASH_ESCAPE,
370 NAMED_ARG,
371 NUMBER,
372 MACRO,
373 CLOSURE,
374 hljs.QUOTE_STRING_MODE,
375 );
282 LET.contains.push(376 LET.contains.push(
283 hljs.BACKSLASH_ESCAPE,377 hljs.BACKSLASH_ESCAPE,
284 NAMED_ARG,378 NAMED_ARG,
@@ -303,6 +397,14 @@ export class SlashCommandParser {
303 MACRO,397 MACRO,
304 CLOSURE,398 CLOSURE,
305 );399 );
400 ABORT.contains.push(
401 hljs.BACKSLASH_ESCAPE,
402 NAMED_ARG,
403 NUMBER,
404 MACRO,
405 CLOSURE,
406 hljs.QUOTE_STRING_MODE,
407 );
306 COMMAND.contains.push(408 COMMAND.contains.push(
307 hljs.BACKSLASH_ESCAPE,409 hljs.BACKSLASH_ESCAPE,
308 NAMED_ARG,410 NAMED_ARG,
@@ -313,8 +415,11 @@ export class SlashCommandParser {
313 );415 );
314 CLOSURE.contains.push(416 CLOSURE.contains.push(
315 hljs.BACKSLASH_ESCAPE,417 hljs.BACKSLASH_ESCAPE,
418 BLOCK_COMMENT,
316 COMMENT,419 COMMENT,
317 ABORT,420 ABORT,
421 IMPORT,
422 BREAK,
318 NAMED_ARG,423 NAMED_ARG,
319 NUMBER,424 NUMBER,
320 MACRO,425 MACRO,
@@ -325,20 +430,27 @@ export class SlashCommandParser {
325 COMMAND,430 COMMAND,
326 'self',431 'self',
327 hljs.QUOTE_STRING_MODE,432 hljs.QUOTE_STRING_MODE,
433 PIPEBREAK,
434 PIPE,
328 );435 );
329 hljs.registerLanguage('stscript', ()=>({436 hljs.registerLanguage('stscript', ()=>({
330 case_insensitive: false,437 case_insensitive: false,
331 keywords: ['|'],438 keywords: [],
332 contains: [439 contains: [
333 hljs.BACKSLASH_ESCAPE,440 hljs.BACKSLASH_ESCAPE,
441 BLOCK_COMMENT,
334 COMMENT,442 COMMENT,
335 ABORT,443 ABORT,
444 IMPORT,
445 BREAK,
336 RUN,446 RUN,
337 LET,447 LET,
338 GETVAR,448 GETVAR,
339 SETVAR,449 SETVAR,
340 COMMAND,450 COMMAND,
341 CLOSURE,451 CLOSURE,
452 PIPEBREAK,
453 PIPE,
342 ],454 ],
343 }));455 }));
344 }456 }
@@ -415,7 +527,7 @@ export class SlashCommandParser {
415 );527 );
416 return result;528 return result;
417 }529 }
418 const result = new SlashCommandAutoCompleteNameResult(executor, this.commands);530 const result = new SlashCommandAutoCompleteNameResult(executor, this.scopeIndex[this.commandIndex.indexOf(executor)], this.commands);
419 return result;531 return result;
420 }532 }
421 return null;533 return null;
@@ -515,11 +627,14 @@ export class SlashCommandParser {
515 }627 }
516628
517 replaceGetvar(value) {629 replaceGetvar(value) {
518 return value.replace(/{{(get(?:global)?var)::([^}]+)}}/gi, (_, cmd, name) => {630 return value.replace(/{{(get(?:global)?var)::([^}]+)}}/gi, (match, cmd, name, idx) => {
519 name = name.trim();631 name = name.trim();
632 const startIdx = this.index - value.length + idx;
633 const endIdx = this.index - value.length + idx + match.length;
520 // store pipe634 // store pipe
521 const pipeName = `_PARSER_${uuidv4()}`;635 const pipeName = `_PARSER_PIPE_${uuidv4()}`;
522 const storePipe = new SlashCommandExecutor(null); {636 const storePipe = new SlashCommandExecutor(startIdx); {
637 storePipe.end = endIdx;
523 storePipe.command = this.commands['let'];638 storePipe.command = this.commands['let'];
524 storePipe.name = 'let';639 storePipe.name = 'let';
525 const nameAss = new SlashCommandUnnamedArgumentAssignment();640 const nameAss = new SlashCommandUnnamedArgumentAssignment();
@@ -530,17 +645,19 @@ export class SlashCommandParser {
530 this.closure.executorList.push(storePipe);645 this.closure.executorList.push(storePipe);
531 }646 }
532 // getvar / getglobalvar647 // getvar / getglobalvar
533 const getvar = new SlashCommandExecutor(null); {648 const getvar = new SlashCommandExecutor(startIdx); {
649 getvar.end = endIdx;
534 getvar.command = this.commands[cmd];650 getvar.command = this.commands[cmd];
535 getvar.name = 'cmd';651 getvar.name = cmd;
536 const nameAss = new SlashCommandUnnamedArgumentAssignment();652 const nameAss = new SlashCommandUnnamedArgumentAssignment();
537 nameAss.value = name;653 nameAss.value = name;
538 getvar.unnamedArgumentList = [nameAss];654 getvar.unnamedArgumentList = [nameAss];
539 this.closure.executorList.push(getvar);655 this.closure.executorList.push(getvar);
540 }656 }
541 // set to temp scoped var657 // set to temp scoped var
542 const varName = `_PARSER_${uuidv4()}`;658 const varName = `_PARSER_VAR_${uuidv4()}`;
543 const setvar = new SlashCommandExecutor(null); {659 const setvar = new SlashCommandExecutor(startIdx); {
660 setvar.end = endIdx;
544 setvar.command = this.commands['let'];661 setvar.command = this.commands['let'];
545 setvar.name = 'let';662 setvar.name = 'let';
546 const nameAss = new SlashCommandUnnamedArgumentAssignment();663 const nameAss = new SlashCommandUnnamedArgumentAssignment();
@@ -551,7 +668,8 @@ export class SlashCommandParser {
551 this.closure.executorList.push(setvar);668 this.closure.executorList.push(setvar);
552 }669 }
553 // return pipe670 // return pipe
554 const returnPipe = new SlashCommandExecutor(null); {671 const returnPipe = new SlashCommandExecutor(startIdx); {
672 returnPipe.end = endIdx;
555 returnPipe.command = this.commands['return'];673 returnPipe.command = this.commands['return'];
556 returnPipe.name = 'return';674 returnPipe.name = 'return';
557 const varAss = new SlashCommandUnnamedArgumentAssignment();675 const varAss = new SlashCommandUnnamedArgumentAssignment();
@@ -564,12 +682,13 @@ export class SlashCommandParser {
564 }682 }
565683
566684
567 parse(text, verifyCommandNames = true, flags = null, abortController = null) {685 parse(text, verifyCommandNames = true, flags = null, abortController = null, debugController = null) {
568 this.verifyCommandNames = verifyCommandNames;686 this.verifyCommandNames = verifyCommandNames;
569 for (const key of Object.keys(PARSER_FLAG)) {687 for (const key of Object.keys(PARSER_FLAG)) {
570 this.flags[PARSER_FLAG[key]] = flags?.[PARSER_FLAG[key]] ?? power_user.stscript.parser.flags[PARSER_FLAG[key]] ?? false;688 this.flags[PARSER_FLAG[key]] = flags?.[PARSER_FLAG[key]] ?? power_user.stscript.parser.flags[PARSER_FLAG[key]] ?? false;
571 }689 }
572 this.abortController = abortController;690 this.abortController = abortController;
691 this.debugController = debugController;
573 this.text = text;692 this.text = text;
574 this.index = 0;693 this.index = 0;
575 this.scope = null;694 this.scope = null;
@@ -577,6 +696,7 @@ export class SlashCommandParser {
577 this.commandIndex = [];696 this.commandIndex = [];
578 this.scopeIndex = [];697 this.scopeIndex = [];
579 this.macroIndex = [];698 this.macroIndex = [];
699 this.parserContext = uuidv4();
580 const closure = this.parseClosure(true);700 const closure = this.parseClosure(true);
581 return closure;701 return closure;
582 }702 }
@@ -604,8 +724,12 @@ export class SlashCommandParser {
604 if (!isRoot) this.take(2); // discard opening {:724 if (!isRoot) this.take(2); // discard opening {:
605 const textStart = this.index;725 const textStart = this.index;
606 let closure = new SlashCommandClosure(this.scope);726 let closure = new SlashCommandClosure(this.scope);
727 closure.parserContext = this.parserContext;
728 closure.fullText = this.text;
607 closure.abortController = this.abortController;729 closure.abortController = this.abortController;
730 closure.debugController = this.debugController;
608 this.scope = closure.scope;731 this.scope = closure.scope;
732 const oldClosure = this.closure;
609 this.closure = closure;733 this.closure = closure;
610 this.discardWhitespace();734 this.discardWhitespace();
611 while (this.testNamedArgument()) {735 while (this.testNamedArgument()) {
@@ -615,7 +739,9 @@ export class SlashCommandParser {
615 this.discardWhitespace();739 this.discardWhitespace();
616 }740 }
617 while (!this.testClosureEnd()) {741 while (!this.testClosureEnd()) {
618 if (this.testComment()) {742 if (this.testBlockComment()) {
743 this.parseBlockComment();
744 } else if (this.testComment()) {
619 this.parseComment();745 this.parseComment();
620 } else if (this.testParserFlag()) {746 } else if (this.testParserFlag()) {
621 this.parseParserFlag();747 this.parseParserFlag();
@@ -623,6 +749,14 @@ export class SlashCommandParser {
623 const cmd = this.parseRunShorthand();749 const cmd = this.parseRunShorthand();
624 closure.executorList.push(cmd);750 closure.executorList.push(cmd);
625 injectPipe = true;751 injectPipe = true;
752 } else if (this.testBreakPoint()) {
753 const bp = this.parseBreakPoint();
754 if (this.debugController) {
755 closure.executorList.push(bp);
756 }
757 } else if (this.testBreak()) {
758 const b = this.parseBreak();
759 closure.executorList.push(b);
626 } else if (this.testCommand()) {760 } else if (this.testCommand()) {
627 const cmd = this.parseCommand();761 const cmd = this.parseCommand();
628 cmd.injectPipe = injectPipe;762 cmd.injectPipe = injectPipe;
@@ -651,14 +785,83 @@ export class SlashCommandParser {
651 }785 }
652 closureIndexEntry.end = this.index - 1;786 closureIndexEntry.end = this.index - 1;
653 this.scope = closure.scope.parent;787 this.scope = closure.scope.parent;
788 this.closure = oldClosure ?? closure;
654 return closure;789 return closure;
655 }790 }
656791
792 testBreakPoint() {
793 return this.testSymbol(/\/breakpoint\s*\|/);
794 }
795 parseBreakPoint() {
796 const cmd = new SlashCommandBreakPoint();
797 cmd.name = 'breakpoint';
798 cmd.command = this.commands['breakpoint'];
799 cmd.start = this.index + 1;
800 this.take('/breakpoint'.length);
801 cmd.end = this.index;
802 this.commandIndex.push(cmd);
803 this.scopeIndex.push(this.scope.getCopy());
804 return cmd;
805 }
806
807 testBreak() {
808 return this.testSymbol(/\/break(\s|\||$)/);
809 }
810 parseBreak() {
811 const cmd = new SlashCommandBreak();
812 cmd.name = 'break';
813 cmd.command = this.commands['break'];
814 cmd.start = this.index + 1;
815 this.take('/break'.length);
816 this.discardWhitespace();
817 if (this.testUnnamedArgument()) {
818 cmd.unnamedArgumentList.push(...this.parseUnnamedArgument());
819 }
820 cmd.end = this.index;
821 this.commandIndex.push(cmd);
822 this.scopeIndex.push(this.scope.getCopy());
823 return cmd;
824 }
825
826 testBlockComment() {
827 return this.testSymbol('/*');
828 }
829 testBlockCommentEnd() {
830 if (!this.verifyCommandNames) {
831 if (this.index >= this.text.length) return true;
832 } else {
833 if (this.ahead.length < 1) throw new SlashCommandParserError(`Unclosed block comment at position ${this.userIndex}`, this.text, this.index);
834 }
835 return this.testSymbol('*|');
836 }
837 parseBlockComment() {
838 const start = this.index + 1;
839 const cmd = new SlashCommandExecutor(start);
840 cmd.command = this.commands['*'];
841 this.commandIndex.push(cmd);
842 this.scopeIndex.push(this.scope.getCopy());
843 this.take(); // discard "/"
844 cmd.name = this.take(); //set "*" as name
845 while (!this.testBlockCommentEnd()) {
846 if (this.testBlockComment()) {
847 this.parseBlockComment();
848 }
849 this.take();
850 }
851 this.take(2); // take closing "*|"
852 cmd.end = this.index - 1;
853 }
854
657 testComment() {855 testComment() {
658 return this.testSymbol(/\/[/#]/);856 return this.testSymbol(/\/[/#]/);
659 }857 }
660 testCommentEnd() {858 testCommentEnd() {
661 return this.testCommandEnd();859 if (!this.verifyCommandNames) {
860 if (this.index >= this.text.length) return true;
861 } else {
862 if (this.endOfText) throw new SlashCommandParserError(`Unclosed comment at position ${this.userIndex}`, this.text, this.index);
863 }
864 return this.testSymbol('|');
662 }865 }
663 parseComment() {866 parseComment() {
664 const start = this.index + 1;867 const start = this.index + 1;
@@ -719,11 +922,13 @@ export class SlashCommandParser {
719 else assignment.value = this.parseValue();922 else assignment.value = this.parseValue();
720 cmd.unnamedArgumentList = [assignment];923 cmd.unnamedArgumentList = [assignment];
721 this.discardWhitespace();924 this.discardWhitespace();
925 cmd.startNamedArgs = this.index;
722 while (this.testNamedArgument()) {926 while (this.testNamedArgument()) {
723 const arg = this.parseNamedArgument();927 const arg = this.parseNamedArgument();
724 cmd.namedArgumentList.push(arg);928 cmd.namedArgumentList.push(arg);
725 this.discardWhitespace();929 this.discardWhitespace();
726 }930 }
931 cmd.endNamedArgs = this.index;
727 this.discardWhitespace();932 this.discardWhitespace();
728 // /run shorthand does not take unnamed arguments (the command name practically *is* the unnamed argument)933 // /run shorthand does not take unnamed arguments (the command name practically *is* the unnamed argument)
729 if (this.testRunShorthandEnd()) {934 if (this.testRunShorthandEnd()) {
@@ -761,10 +966,10 @@ export class SlashCommandParser {
761 this.discardWhitespace();966 this.discardWhitespace();
762 }967 }
763 this.discardWhitespace();968 this.discardWhitespace();
764 cmd.startUnnamedArgs = this.index;969 cmd.startUnnamedArgs = this.index - (/\s(\s*)$/s.exec(this.behind)?.[1]?.length ?? 0);
765 cmd.endUnnamedArgs = this.index;970 cmd.endUnnamedArgs = this.index;
766 if (this.testUnnamedArgument()) {971 if (this.testUnnamedArgument()) {
767 cmd.unnamedArgumentList = this.parseUnnamedArgument(cmd.command?.unnamedArgumentList?.length && cmd?.command?.splitUnnamedArgument);972 cmd.unnamedArgumentList = this.parseUnnamedArgument(cmd.command?.unnamedArgumentList?.length && cmd?.command?.splitUnnamedArgument, cmd?.command?.splitUnnamedArgumentCount);
768 cmd.endUnnamedArgs = this.index;973 cmd.endUnnamedArgs = this.index;
769 if (cmd.name == 'let') {974 if (cmd.name == 'let') {
770 const keyArg = cmd.namedArgumentList.find(it=>it.name == 'key');975 const keyArg = cmd.namedArgumentList.find(it=>it.name == 'key');
@@ -773,6 +978,17 @@ export class SlashCommandParser {
773 } else if (typeof cmd.unnamedArgumentList[0]?.value == 'string') {978 } else if (typeof cmd.unnamedArgumentList[0]?.value == 'string') {
774 this.scope.variableNames.push(cmd.unnamedArgumentList[0].value);979 this.scope.variableNames.push(cmd.unnamedArgumentList[0].value);
775 }980 }
981 } else if (cmd.name == 'import') {
982 const value = /**@type {string[]}*/(cmd.unnamedArgumentList.map(it=>it.value));
983 for (let i = 0; i < value.length; i++) {
984 const srcName = value[i];
985 let dstName = srcName;
986 if (i + 2 < value.length && value[i + 1] == 'as') {
987 dstName = value[i + 2];
988 i += 2;
989 }
990 this.scope.variableNames.push(dstName);
991 }
776 }992 }
777 }993 }
778 if (this.testCommandEnd()) {994 if (this.testCommandEnd()) {
@@ -813,24 +1029,60 @@ export class SlashCommandParser {
813 testUnnamedArgumentEnd() {1029 testUnnamedArgumentEnd() {
814 return this.testCommandEnd();1030 return this.testCommandEnd();
815 }1031 }
816 parseUnnamedArgument(split) {1032 parseUnnamedArgument(split, splitCount = null) {
1033 const wasSplit = split;
817 /**@type {SlashCommandClosure|String}*/1034 /**@type {SlashCommandClosure|String}*/
818 let value = this.jumpedEscapeSequence ? this.take() : ''; // take the first, already tested, char if it is an escaped one1035 let value = this.jumpedEscapeSequence ? this.take() : ''; // take the first, already tested, char if it is an escaped one
819 let isList = split;1036 let isList = split;
820 let listValues = [];1037 let listValues = [];
1038 let listQuoted = []; // keep track of which listValues were quoted
821 /**@type {SlashCommandUnnamedArgumentAssignment}*/1039 /**@type {SlashCommandUnnamedArgumentAssignment}*/
822 let assignment = new SlashCommandUnnamedArgumentAssignment();1040 let assignment = new SlashCommandUnnamedArgumentAssignment();
823 assignment.start = this.index;1041 assignment.start = this.index;
1042 if (!split && this.testQuotedValue()) {
1043 // if the next bit is a quoted value, take the whole value and gather contents as a list
1044 assignment.value = this.parseQuotedValue();
1045 assignment.end = this.index;
1046 isList = true;
1047 listValues.push(assignment);
1048 listQuoted.push(true);
1049 assignment = new SlashCommandUnnamedArgumentAssignment();
1050 assignment.start = this.index;
1051 }
824 while (!this.testUnnamedArgumentEnd()) {1052 while (!this.testUnnamedArgumentEnd()) {
1053 if (split && splitCount && listValues.length >= splitCount) {
1054 // the split count has just been reached: stop splitting, the rest is one singular value
1055 split = false;
1056 if (this.testQuotedValue()) {
1057 // if the next bit is a quoted value, take the whole value
1058 assignment.value = this.parseQuotedValue();
1059 assignment.end = this.index;
1060 listValues.push(assignment);
1061 listQuoted.push(true);
1062 assignment = new SlashCommandUnnamedArgumentAssignment();
1063 assignment.start = this.index;
1064 }
1065 }
825 if (this.testClosure()) {1066 if (this.testClosure()) {
826 isList = true;1067 isList = true;
827 if (value.length > 0) {1068 if (value.length > 0) {
828 this.indexMacros(this.index - value.length, value);1069 this.indexMacros(this.index - value.length, value);
829 assignment.value = value;1070 assignment.value = value;
830 listValues.push(assignment);1071 listValues.push(assignment);
1072 listQuoted.push(false);
831 assignment = new SlashCommandUnnamedArgumentAssignment();1073 assignment = new SlashCommandUnnamedArgumentAssignment();
832 assignment.start = this.index;1074 assignment.start = this.index;
833 value = '';1075 if (!split && this.testQuotedValue()) {
1076 // if where currently not splitting and the next bit is a quoted value, take the whole value
1077 assignment.value = this.parseQuotedValue();
1078 assignment.end = this.index;
1079 listValues.push(assignment);
1080 listQuoted.push(true);
1081 assignment = new SlashCommandUnnamedArgumentAssignment();
1082 assignment.start = this.index;
1083 } else {
1084 value = '';
1085 }
834 }1086 }
835 assignment.start = this.index;1087 assignment.start = this.index;
836 assignment.value = this.parseClosure();1088 assignment.value = this.parseClosure();
@@ -845,18 +1097,21 @@ export class SlashCommandParser {
845 assignment.value = this.parseQuotedValue();1097 assignment.value = this.parseQuotedValue();
846 assignment.end = this.index;1098 assignment.end = this.index;
847 listValues.push(assignment);1099 listValues.push(assignment);
1100 listQuoted.push(true);
848 assignment = new SlashCommandUnnamedArgumentAssignment();1101 assignment = new SlashCommandUnnamedArgumentAssignment();
849 } else if (this.testListValue()) {1102 } else if (this.testListValue()) {
850 assignment.start = this.index;1103 assignment.start = this.index;
851 assignment.value = this.parseListValue();1104 assignment.value = this.parseListValue();
852 assignment.end = this.index;1105 assignment.end = this.index;
853 listValues.push(assignment);1106 listValues.push(assignment);
1107 listQuoted.push(false);
854 assignment = new SlashCommandUnnamedArgumentAssignment();1108 assignment = new SlashCommandUnnamedArgumentAssignment();
855 } else if (this.testValue()) {1109 } else if (this.testValue()) {
856 assignment.start = this.index;1110 assignment.start = this.index;
857 assignment.value = this.parseValue();1111 assignment.value = this.parseValue();
858 assignment.end = this.index;1112 assignment.end = this.index;
859 listValues.push(assignment);1113 listValues.push(assignment);
1114 listQuoted.push(false);
860 assignment = new SlashCommandUnnamedArgumentAssignment();1115 assignment = new SlashCommandUnnamedArgumentAssignment();
861 } else {1116 } else {
862 throw new SlashCommandParserError(`Unexpected end of unnamed argument at index ${this.userIndex}.`);1117 throw new SlashCommandParserError(`Unexpected end of unnamed argument at index ${this.userIndex}.`);
@@ -870,8 +1125,48 @@ export class SlashCommandParser {
870 if (isList && value.length > 0) {1125 if (isList && value.length > 0) {
871 assignment.value = value;1126 assignment.value = value;
872 listValues.push(assignment);1127 listValues.push(assignment);
1128 listQuoted.push(false);
873 }1129 }
874 if (isList) {1130 if (isList) {
1131 const firstVal = listValues[0];
1132 if (typeof firstVal?.value == 'string') {
1133 if (!listQuoted[0]) {
1134 // only trim the first part if it wasn't quoted
1135 firstVal.value = firstVal.value.trimStart();
1136 }
1137 if (firstVal.value.length == 0) {
1138 listValues.shift();
1139 listQuoted.shift();
1140 }
1141 }
1142 const lastVal = listValues.slice(-1)[0];
1143 if (typeof lastVal?.value == 'string') {
1144 if (!listQuoted.slice(-1)[0]) {
1145 // only trim the last part if it wasn't quoted
1146 lastVal.value = lastVal.value.trimEnd();
1147 }
1148 if (lastVal.value.length == 0) {
1149 listValues.pop();
1150 listQuoted.pop();
1151 }
1152 }
1153 if (wasSplit && splitCount && splitCount + 1 < listValues.length) {
1154 // if split with a split count and there are more values than expected
1155 // -> should be result of quoting + additional (non-whitespace) text
1156 // -> join the parts into one and restore quotes
1157 const joined = new SlashCommandUnnamedArgumentAssignment();
1158 joined.start = listValues[splitCount].start;
1159 joined.end = listValues.slice(-1)[0].end;
1160 joined.value = '';
1161 for (let i = splitCount; i < listValues.length; i++) {
1162 if (listQuoted[i]) joined.value += `"${listValues[i].value}"`;
1163 else joined.value += listValues[i].value;
1164 }
1165 listValues = [
1166 ...listValues.slice(0, splitCount),
1167 joined,
1168 ];
1169 }
875 return listValues;1170 return listValues;
876 }1171 }
877 this.indexMacros(this.index - value.length, value);1172 this.indexMacros(this.index - value.length, value);
public/scripts/slash-commands/SlashCommandScope.js+5 -3
@@ -38,8 +38,10 @@ export class SlashCommandScope {
38 }38 }
3939
4040
41 setMacro(key, value) {41 setMacro(key, value, overwrite = true) {
42 this.macros[key] = value;42 if (overwrite || !this.macroList.find(it=>it.key == key)) {
43 this.macros[key] = value;
44 }
43 }45 }
4446
4547
@@ -95,7 +97,7 @@ export class SlashCommandScope {
95 return v ?? '';97 return v ?? '';
96 } else {98 } else {
97 const value = this.variables[key];99 const value = this.variables[key];
98 return (value === '' || isNaN(Number(value))) ? (value || '') : Number(value);100 return (value?.trim?.() === '' || isNaN(Number(value))) ? (value || '') : Number(value);
99 }101 }
100 }102 }
101 if (this.parent) {103 if (this.parent) {
public/scripts/utils.js+73 -1
@@ -3,7 +3,7 @@ import { getRequestHeaders } from '../script.js';
3import { isMobile } from './RossAscends-mods.js';3import { isMobile } from './RossAscends-mods.js';
4import { collapseNewlines } from './power-user.js';4import { collapseNewlines } from './power-user.js';
5import { debounce_timeout } from './constants.js';5import { debounce_timeout } from './constants.js';
6import { Popup } from './popup.js';6import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
77
8/**8/**
9 * Pagination status string template.9 * Pagination status string template.
@@ -1929,3 +1929,75 @@ export function getFreeName(name, list, numberFormatter = (n) => ` #${n}`) {
1929 }1929 }
1930 return `${name}${numberFormatter(counter)}`;1930 return `${name}${numberFormatter(counter)}`;
1931}1931}
1932
1933export async function fetchFaFile(name) {
1934 const style = document.createElement('style');
1935 style.innerHTML = await (await fetch(`/css/${name}`)).text();
1936 document.head.append(style);
1937 const sheet = style.sheet;
1938 style.remove();
1939 return [...sheet.cssRules].filter(it=>it.style?.content).map(it=>it.selectorText.split('::').shift().slice(1));
1940}
1941export async function fetchFa() {
1942 return [...new Set((await Promise.all([
1943 fetchFaFile('fontawesome.min.css'),
1944 ])).flat())];
1945}
1946/**
1947 * Opens a popup with all the available Font Awesome icons and returns the selected icon's name.
1948 * @prop {string[]} customList A custom list of Font Awesome icons to use instead of all available icons.
1949 * @returns {Promise<string>} The icon name (fa-pencil) or null if cancelled.
1950 */
1951export async function showFontAwesomePicker(customList = null) {
1952 const faList = customList ?? await fetchFa();
1953 const fas = {};
1954 const dom = document.createElement('div'); {
1955 dom.classList.add('faPicker-container');
1956 const search = document.createElement('div'); {
1957 search.classList.add('faQuery-container');
1958 const qry = document.createElement('input'); {
1959 qry.classList.add('text_pole');
1960 qry.classList.add('faQuery');
1961 qry.type = 'search';
1962 qry.placeholder = 'Filter icons';
1963 qry.autofocus = true;
1964 const qryDebounced = debounce(()=>{
1965 const result = faList.filter(it=>it.includes(qry.value));
1966 for (const fa of faList) {
1967 if (!result.includes(fa)) {
1968 fas[fa].classList.add('hidden');
1969 } else {
1970 fas[fa].classList.remove('hidden');
1971 }
1972 }
1973 });
1974 qry.addEventListener('input', () => qryDebounced());
1975 search.append(qry);
1976 }
1977 dom.append(search);
1978 }
1979 const grid = document.createElement('div'); {
1980 grid.classList.add('faPicker');
1981 for (const fa of faList) {
1982 const opt = document.createElement('div'); {
1983 fas[fa] = opt;
1984 opt.classList.add('menu_button');
1985 opt.classList.add('fa-solid');
1986 opt.classList.add(fa);
1987 opt.title = fa.slice(3);
1988 opt.dataset.result = POPUP_RESULT.AFFIRMATIVE.toString();
1989 opt.addEventListener('click', () => value = fa);
1990 grid.append(opt);
1991 }
1992 }
1993 dom.append(grid);
1994 }
1995 }
1996 let value = '';
1997 const picker = new Popup(dom, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, okButton: 'No Icon', cancelButton: 'Cancel' });
1998 await picker.show();
1999 if (picker.result == POPUP_RESULT.AFFIRMATIVE) {
2000 return value;
2001 }
2002 return null;
2003}
public/scripts/variables.js+93 -46
@@ -4,10 +4,11 @@ import { executeSlashCommandsWithOptions } from './slash-commands.js';
4import { SlashCommand } from './slash-commands/SlashCommand.js';4import { SlashCommand } from './slash-commands/SlashCommand.js';
5import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';5import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
6import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';6import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
7import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
7import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';8import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
8import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';9import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
9import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';10import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
10import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';11import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
11import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';12import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
12import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';13import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
13import { isFalseBoolean } from './utils.js';14import { isFalseBoolean } from './utils.js';
@@ -40,7 +41,7 @@ function getLocalVariable(name, args = {}) {
40 }41 }
41 }42 }
4243
43 return (localVariable === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);44 return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);
44}45}
4546
46function setLocalVariable(name, value, args = {}) {47function setLocalVariable(name, value, args = {}) {
@@ -93,7 +94,7 @@ function getGlobalVariable(name, args = {}) {
93 }94 }
94 }95 }
9596
96 return (globalVariable === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);97 return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);
97}98}
9899
99function setGlobalVariable(name, value, args = {}) {100function setGlobalVariable(name, value, args = {}) {
@@ -348,11 +349,13 @@ async function whileCallback(args, value) {
348349
349 if (result && command) {350 if (result && command) {
350 if (command instanceof SlashCommandClosure) {351 if (command instanceof SlashCommandClosure) {
352 command.breakController = new SlashCommandBreakController();
351 commandResult = await command.execute();353 commandResult = await command.execute();
352 } else {354 } else {
353 commandResult = await executeSubCommands(command, args._scope, args._parserFlags, args._abortController);355 commandResult = await executeSubCommands(command, args._scope, args._parserFlags, args._abortController);
354 }356 }
355 if (commandResult.isAborted) break;357 if (commandResult.isAborted) break;
358 if (commandResult.isBreak) break;
356 } else {359 } else {
357 break;360 break;
358 }361 }
@@ -390,8 +393,8 @@ async function timesCallback(args, value) {
390 const iterations = Math.min(Number(repeats), isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS);393 const iterations = Math.min(Number(repeats), isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS);
391 let result;394 let result;
392 for (let i = 0; i < iterations; i++) {395 for (let i = 0; i < iterations; i++) {
393 /**@type {SlashCommandClosureResult}*/
394 if (command instanceof SlashCommandClosure) {396 if (command instanceof SlashCommandClosure) {
397 command.breakController = new SlashCommandBreakController();
395 command.scope.setMacro('timesIndex', i);398 command.scope.setMacro('timesIndex', i);
396 result = await command.execute();399 result = await command.execute();
397 }400 }
@@ -399,6 +402,7 @@ async function timesCallback(args, value) {
399 result = await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i.toString()), args._scope, args._parserFlags, args._abortController);402 result = await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i.toString()), args._scope, args._parserFlags, args._abortController);
400 }403 }
401 if (result.isAborted) break;404 if (result.isAborted) break;
405 if (result.isBreak) break;
402 }406 }
403407
404 return result?.pipe ?? '';408 return result?.pipe ?? '';
@@ -461,7 +465,7 @@ function existsGlobalVariable(name) {
461 * @param {object} args Command arguments465 * @param {object} args Command arguments
462 * @returns {{a: string | number, b: string | number, rule: string}} Boolean operands466 * @returns {{a: string | number, b: string | number, rule: string}} Boolean operands
463 */467 */
464function parseBooleanOperands(args) {468export function parseBooleanOperands(args) {
465 // Resolution order: numeric literal, local variable, global variable, string literal469 // Resolution order: numeric literal, local variable, global variable, string literal
466 /**470 /**
467 * @param {string} operand Boolean operand candidate471 * @param {string} operand Boolean operand candidate
@@ -510,36 +514,15 @@ function parseBooleanOperands(args) {
510 * @param {string|number} b The right operand514 * @param {string|number} b The right operand
511 * @returns {boolean} True if the rule yields true, false otherwise515 * @returns {boolean} True if the rule yields true, false otherwise
512 */516 */
513function evalBoolean(rule, a, b) {517export function evalBoolean(rule, a, b) {
514 if (!rule) {518 if (!rule) {
515 toastr.warning('The rule must be specified for the boolean comparison.', 'Invalid command');519 toastr.warning('The rule must be specified for the boolean comparison.', 'Invalid command');
516 throw new Error('Invalid command.');520 throw new Error('Invalid command.');
517 }521 }
518522
519 let result = false;523 let result = false;
520524 if (typeof a === 'number' && typeof b === 'number') {
521 if (typeof a === 'string' && typeof b !== 'number') {525 // only do numeric comparison if both operands are numbers
522 const aString = String(a).toLowerCase();
523 const bString = String(b).toLowerCase();
524
525 switch (rule) {
526 case 'in':
527 result = aString.includes(bString);
528 break;
529 case 'nin':
530 result = !aString.includes(bString);
531 break;
532 case 'eq':
533 result = aString === bString;
534 break;
535 case 'neq':
536 result = aString !== bString;
537 break;
538 default:
539 toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command');
540 throw new Error('Invalid command.');
541 }
542 } else if (typeof a === 'number') {
543 const aNumber = Number(a);526 const aNumber = Number(a);
544 const bNumber = Number(b);527 const bNumber = Number(b);
545528
@@ -569,6 +552,38 @@ function evalBoolean(rule, a, b) {
569 toastr.error('Unknown boolean comparison rule for type number.', 'Invalid command');552 toastr.error('Unknown boolean comparison rule for type number.', 'Invalid command');
570 throw new Error('Invalid command.');553 throw new Error('Invalid command.');
571 }554 }
555 } else {
556 // otherwise do case-insensitive string comparsion, stringify non-strings
557 let aString;
558 let bString;
559 if (typeof a == 'string') {
560 aString = a.toLowerCase();
561 } else {
562 aString = JSON.stringify(a).toLowerCase();
563 }
564 if (typeof b == 'string') {
565 bString = b.toLowerCase();
566 } else {
567 bString = JSON.stringify(b).toLowerCase();
568 }
569
570 switch (rule) {
571 case 'in':
572 result = aString.includes(bString);
573 break;
574 case 'nin':
575 result = !aString.includes(bString);
576 break;
577 case 'eq':
578 result = aString === bString;
579 break;
580 case 'neq':
581 result = aString !== bString;
582 break;
583 default:
584 toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command');
585 throw new Error('Invalid command.');
586 }
572 }587 }
573588
574 return result;589 return result;
@@ -783,24 +798,29 @@ function randValuesCallback(from, to, args) {
783 * @returns The variable's value798 * @returns The variable's value
784 */799 */
785function letCallback(args, value) {800function letCallback(args, value) {
786 if (Array.isArray(value)) {801 if (!Array.isArray(value)) value = [value];
787 args._scope.letVariable(value[0], typeof value[1] == 'string' ? value.slice(1).join(' ') : value[1]);
788 return value[1];
789 }
790 if (args.key !== undefined) {802 if (args.key !== undefined) {
791 const key = args.key;803 const key = args.key;
792 const val = value;804 if (typeof key != 'string') throw new Error('Key must be a string');
793 args._scope.letVariable(key, val);805 if (args._hasUnnamedArgument) {
794 return val;806 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
807 args._scope.letVariable(key, val);
808 return val;
809 } else {
810 args._scope.letVariable(key);
811 return '';
812 }
795 }813 }
796 if (value instanceof SlashCommandClosure) throw new Error('/let unnamed argument does not support closures if no key is provided');814 const key = value.shift();
797 if (value.includes(' ')) {815 if (typeof key != 'string') throw new Error('Key must be a string');
798 const key = value.split(' ')[0];816 if (value.length > 0) {
799 const val = value.split(' ').slice(1).join(' ');817 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
800 args._scope.letVariable(key, val);818 args._scope.letVariable(key, val);
801 return val;819 return val;
820 } else {
821 args._scope.letVariable(key);
822 return '';
802 }823 }
803 args._scope.letVariable(value);
804}824}
805825
806/**826/**
@@ -813,8 +833,9 @@ function varCallback(args, value) {
813 if (!Array.isArray(value)) value = [value];833 if (!Array.isArray(value)) value = [value];
814 if (args.key !== undefined) {834 if (args.key !== undefined) {
815 const key = args.key;835 const key = args.key;
836 if (typeof key != 'string') throw new Error('Key must be a string');
816 if (args._hasUnnamedArgument) {837 if (args._hasUnnamedArgument) {
817 const val = value.join(' ');838 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
818 args._scope.setVariable(key, val, args.index);839 args._scope.setVariable(key, val, args.index);
819 return val;840 return val;
820 } else {841 } else {
@@ -822,8 +843,9 @@ function varCallback(args, value) {
822 }843 }
823 }844 }
824 const key = value.shift();845 const key = value.shift();
846 if (typeof key != 'string') throw new Error('Key must be a string');
825 if (value.length > 0) {847 if (value.length > 0) {
826 const val = value.join(' ');848 const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
827 args._scope.setVariable(key, val, args.index);849 args._scope.setVariable(key, val, args.index);
828 return val;850 return val;
829 } else {851 } else {
@@ -1382,6 +1404,7 @@ export function registerVariableCommands() {
1382 ),1404 ),
1383 ],1405 ],
1384 splitUnnamedArgument: true,1406 splitUnnamedArgument: true,
1407 splitUnnamedArgumentCount: 1,
1385 helpString: `1408 helpString: `
1386 <div>1409 <div>
1387 Execute any valid slash command enclosed in quotes <code>repeats</code> number of times.1410 Execute any valid slash command enclosed in quotes <code>repeats</code> number of times.
@@ -1459,7 +1482,7 @@ export function registerVariableCommands() {
1459 }));1482 }));
1460 SlashCommandParser.addCommandObject(SlashCommand.fromProps({1483 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
1461 name: 'add',1484 name: 'add',
1462 callback: addValuesCallback,1485 callback: (args, /**@type {string[]}*/value) => addValuesCallback(args, value.join(' ')),
1463 returns: 'sum of the provided values',1486 returns: 'sum of the provided values',
1464 unnamedArgumentList: [1487 unnamedArgumentList: [
1465 SlashCommandArgument.fromProps({1488 SlashCommandArgument.fromProps({
@@ -1467,10 +1490,32 @@ export function registerVariableCommands() {
1467 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],1490 typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
1468 isRequired: true,1491 isRequired: true,
1469 acceptsMultiple: true,1492 acceptsMultiple: true,
1470 enumProvider: commonEnumProviders.variables('all'),1493 enumProvider: (executor, scope)=>{
1494 const vars = commonEnumProviders.variables('all')(executor, scope);
1495 vars.push(
1496 new SlashCommandEnumValue(
1497 'any variable name',
1498 null,
1499 enumTypes.variable,
1500 enumIcons.variable,
1501 (input)=>/^\w*$/.test(input),
1502 (input)=>input,
1503 ),
1504 new SlashCommandEnumValue(
1505 'any number',
1506 null,
1507 enumTypes.number,
1508 enumIcons.number,
1509 (input)=>input == '' || !Number.isNaN(Number(input)),
1510 (input)=>input,
1511 ),
1512 );
1513 return vars;
1514 },
1471 forceEnum: false,1515 forceEnum: false,
1472 }),1516 }),
1473 ],1517 ],
1518 splitUnnamedArgument: true,
1474 helpString: `1519 helpString: `
1475 <div>1520 <div>
1476 Performs an addition of the set of values and passes the result down the pipe.1521 Performs an addition of the set of values and passes the result down the pipe.
@@ -2001,6 +2046,7 @@ export function registerVariableCommands() {
2001 ),2046 ),
2002 ],2047 ],
2003 splitUnnamedArgument: true,2048 splitUnnamedArgument: true,
2049 splitUnnamedArgumentCount: 1,
2004 helpString: `2050 helpString: `
2005 <div>2051 <div>
2006 Get or set a variable.2052 Get or set a variable.
@@ -2043,6 +2089,7 @@ export function registerVariableCommands() {
2043 ),2089 ),
2044 ],2090 ],
2045 splitUnnamedArgument: true,2091 splitUnnamedArgument: true,
2092 splitUnnamedArgumentCount: 1,
2046 helpString: `2093 helpString: `
2047 <div>2094 <div>
2048 Declares a new variable in the current scope.2095 Declares a new variable in the current scope.
public/style.css+117 -6
@@ -1383,6 +1383,8 @@ body[data-stscript-style="dark"] {
1383 --ac-style-color-matchedText: rgba(108 171 251 / 1);1383 --ac-style-color-matchedText: rgba(108 171 251 / 1);
1384 --ac-style-color-selectedBackground: rgba(32 57 92 / 1);1384 --ac-style-color-selectedBackground: rgba(32 57 92 / 1);
1385 --ac-style-color-selectedText: rgba(255 255 255 / 1);1385 --ac-style-color-selectedText: rgba(255 255 255 / 1);
1386 --ac-style-color-notSelectableBackground: rgb(65, 78, 95);
1387 --ac-style-color-notSelectableText: rgba(255 255 255 / 1);
1386 --ac-style-color-hoveredBackground: rgba(43 45 46 / 1);1388 --ac-style-color-hoveredBackground: rgba(43 45 46 / 1);
1387 --ac-style-color-hoveredText: rgba(204 204 204 / 1);1389 --ac-style-color-hoveredText: rgba(204 204 204 / 1);
1388 --ac-style-color-argName: rgba(171 209 239 / 1);1390 --ac-style-color-argName: rgba(171 209 239 / 1);
@@ -1398,6 +1400,7 @@ body[data-stscript-style="dark"] {
1398 --ac-style-color-punctuationL2: rgba(98 160 251 / 1);1400 --ac-style-color-punctuationL2: rgba(98 160 251 / 1);
1399 --ac-style-color-currentParenthesis: rgba(195 118 210 / 1);1401 --ac-style-color-currentParenthesis: rgba(195 118 210 / 1);
1400 --ac-style-color-comment: rgba(122 151 90 / 1);1402 --ac-style-color-comment: rgba(122 151 90 / 1);
1403 --ac-style-color-keyword: rgba(182 137 190 / 1);
1401}1404}
14021405
1403body[data-stscript-style="light"] {1406body[data-stscript-style="light"] {
@@ -1408,6 +1411,8 @@ body[data-stscript-style="light"] {
1408 --ac-style-color-matchedText: rgba(61 104 188 / 1);1411 --ac-style-color-matchedText: rgba(61 104 188 / 1);
1409 --ac-style-color-selectedBackground: rgba(232 232 232 / 1);1412 --ac-style-color-selectedBackground: rgba(232 232 232 / 1);
1410 --ac-style-color-selectedText: rgba(0 0 0 / 1);1413 --ac-style-color-selectedText: rgba(0 0 0 / 1);
1414 --ac-style-color-notSelectableBackground: rgba(232 232 232 / 1);
1415 --ac-style-color-notSelectableText: rgb(83, 83, 83);
1411 --ac-style-color-hoveredBackground: rgba(242 242 242 / 1);1416 --ac-style-color-hoveredBackground: rgba(242 242 242 / 1);
1412 --ac-style-color-hoveredText: rgba(59 59 59 / 1);1417 --ac-style-color-hoveredText: rgba(59 59 59 / 1);
1413 --ac-style-color-argName: rgba(16 24 125 / 1);1418 --ac-style-color-argName: rgba(16 24 125 / 1);
@@ -1419,6 +1424,7 @@ body[data-stscript-style="light"] {
1419 --ac-style-color-variable: rgba(16 24 125 / 1);1424 --ac-style-color-variable: rgba(16 24 125 / 1);
1420 --ac-style-color-currentParenthesis: rgba(195 118 210 / 1);1425 --ac-style-color-currentParenthesis: rgba(195 118 210 / 1);
1421 --ac-style-color-comment: rgba(70 126 26 / 1);1426 --ac-style-color-comment: rgba(70 126 26 / 1);
1427 --ac-style-color-keyword: rgba(182 137 190 / 1);
1422}1428}
14231429
1424body[data-stscript-style="theme"] {1430body[data-stscript-style="theme"] {
@@ -1429,6 +1435,8 @@ body[data-stscript-style="theme"] {
1429 --ac-style-color-matchedText: var(--SmartThemeQuoteColor);1435 --ac-style-color-matchedText: var(--SmartThemeQuoteColor);
1430 --ac-style-color-selectedBackground: color-mix(in srgb, rgb(128 128 128) 75%, var(--SmartThemeChatTintColor));1436 --ac-style-color-selectedBackground: color-mix(in srgb, rgb(128 128 128) 75%, var(--SmartThemeChatTintColor));
1431 --ac-style-color-selectedText: var(--SmartThemeBodyColor);1437 --ac-style-color-selectedText: var(--SmartThemeBodyColor);
1438 --ac-style-color-notSelectableBackground: color-mix(in srgb, rgb(128 128 128) 50%, var(--SmartThemeChatTintColor));
1439 --ac-style-color-notSelectableText: var(--SmartThemeBodyColor);
1432 --ac-style-color-hoveredBackground: color-mix(in srgb, rgb(128 128 128) 30%, var(--SmartThemeChatTintColor));1440 --ac-style-color-hoveredBackground: color-mix(in srgb, rgb(128 128 128) 30%, var(--SmartThemeChatTintColor));
1433 --ac-style-color-hoveredText: var(--SmartThemeEmColor);1441 --ac-style-color-hoveredText: var(--SmartThemeEmColor);
1434 --ac-style-color-argName: rgba(171 209 239 / 1);1442 --ac-style-color-argName: rgba(171 209 239 / 1);
@@ -1439,6 +1447,7 @@ body[data-stscript-style="theme"] {
1439 --ac-style-color-variable: rgba(131 193 252 / 1);1447 --ac-style-color-variable: rgba(131 193 252 / 1);
1440 --ac-style-color-currentParenthesis: rgba(195 118 210 / 1);1448 --ac-style-color-currentParenthesis: rgba(195 118 210 / 1);
1441 --ac-style-color-comment: rgba(122 151 90 / 1);1449 --ac-style-color-comment: rgba(122 151 90 / 1);
1450 --ac-style-color-keyword: rgba(182 137 190 / 1);
1442}1451}
14431452
1444body {1453body {
@@ -1454,6 +1463,8 @@ body {
1454 --ac-color-matchedText: var(--ac-style-color-matchedText, rgba(108 171 251 / 1));1463 --ac-color-matchedText: var(--ac-style-color-matchedText, rgba(108 171 251 / 1));
1455 --ac-color-selectedBackground: var(--ac-style-color-selectedBackground, rgba(32 57 92 / 1));1464 --ac-color-selectedBackground: var(--ac-style-color-selectedBackground, rgba(32 57 92 / 1));
1456 --ac-color-selectedText: var(--ac-style-color-selectedText, rgba(255 255 255 / 1));1465 --ac-color-selectedText: var(--ac-style-color-selectedText, rgba(255 255 255 / 1));
1466 --ac-color-notSelectableBackground: var(--ac-style-color-notSelectableBackground, rgb(60, 73, 92));
1467 --ac-color-notSelectableText: var(--ac-style-color-notSelectableText, rgba(255 255 255 / 1));
1457 --ac-color-hoveredBackground: var(--ac-style-color-hoveredBackground, rgba(43 45 46 / 1));1468 --ac-color-hoveredBackground: var(--ac-style-color-hoveredBackground, rgba(43 45 46 / 1));
1458 --ac-color-hoveredText: var(--ac-style-color-hoveredText, rgba(204 204 204 / 1));1469 --ac-color-hoveredText: var(--ac-style-color-hoveredText, rgba(204 204 204 / 1));
1459 --ac-color-argName: var(--ac-style-color-argName, rgba(171 209 239 / 1));1470 --ac-color-argName: var(--ac-style-color-argName, rgba(171 209 239 / 1));
@@ -1469,6 +1480,7 @@ body {
1469 --ac-color-punctuationL2: var(--ac-style-color-punctuationL2, rgba(98 160 251 / 1));1480 --ac-color-punctuationL2: var(--ac-style-color-punctuationL2, rgba(98 160 251 / 1));
1470 --ac-color-currentParenthesis: var(--ac-style-color-currentParenthesis, rgba(195 118 210 / 1));1481 --ac-color-currentParenthesis: var(--ac-style-color-currentParenthesis, rgba(195 118 210 / 1));
1471 --ac-color-comment: var(--ac-style-color-comment, rgba(122 151 90 / 1));1482 --ac-color-comment: var(--ac-style-color-comment, rgba(122 151 90 / 1));
1483 --ac-color-keyword: var(--ac-style-color-keyword, rgba(182 137 190 / 1));
14721484
1473 font-size: calc(var(--ac-font-scale) * 1em);1485 font-size: calc(var(--ac-font-scale) * 1em);
14741486
@@ -1569,16 +1581,25 @@ body[data-stscript-style] .hljs.language-stscript {
1569 color: var(--ac-style-color-punctuation);1581 color: var(--ac-style-color-punctuation);
1570 }1582 }
15711583
1572 .hljs-keyword {
1573 color: var(--ac-style-color-variableLanguage);
1574 }
1575
1576 .hljs-comment {1584 .hljs-comment {
1577 color: var(--ac-style-color-comment);1585 color: var(--ac-style-color-comment);
1578 }1586 }
15791587
1580 .hljs-abort {1588 .hljs-abort {
1581 color: var(--ac-style-color-abort, #e38e23);1589 color: var(--ac-style-color-abort, #e38e23);
1590 font-weight: bold;
1591 }
1592
1593 .hljs-keyword {
1594 color: var(--ac-style-color-keyword);
1595 font-weight: bold;
1596 }
1597
1598 .hljs-pipe {
1599 color: var(--ac-style-color-punctuation);
1600 }
1601 .hljs-pipebreak {
1602 color: var(--ac-style-color-type);
1582 }1603 }
15831604
1584 .hljs-closure {1605 .hljs-closure {
@@ -1670,6 +1691,10 @@ body[data-stscript-style] .hljs.language-stscript {
1670 background-color: var(--ac-color-selectedBackground);1691 background-color: var(--ac-color-selectedBackground);
1671 color: var(--ac-color-selectedText);1692 color: var(--ac-color-selectedText);
1672 }1693 }
1694 &.selected.not-selectable>* {
1695 background-color: var(--ac-color-notSelectableBackground);
1696 color: var(--ac-color-notSelectableText);
1697 }
16731698
1674 >* {1699 >* {
1675 height: 100%;1700 height: 100%;
@@ -1747,7 +1772,12 @@ body[data-stscript-style] .hljs.language-stscript {
1747 padding: 0.25em 0.25em 0.5em 0.25em;1772 padding: 0.25em 0.25em 0.5em 0.25em;
1748 border-bottom: 1px solid var(--ac-color-border);1773 border-bottom: 1px solid var(--ac-color-border);
17491774
1750 >.name {1775 > .head {
1776 display: flex;
1777 gap: 0.5em;
1778 }
1779 > .head > .name, > .name {
1780 flex: 1 1 auto;
1751 font-weight: bold;1781 font-weight: bold;
1752 color: var(--ac-color-text);1782 color: var(--ac-color-text);
1753 cursor: help;1783 cursor: help;
@@ -1756,6 +1786,35 @@ body[data-stscript-style] .hljs.language-stscript {
1756 text-decoration: 1px dotted underline;1786 text-decoration: 1px dotted underline;
1757 }1787 }
1758 }1788 }
1789 > .head > .source {
1790 padding: 0 0.5em;
1791 cursor: help;
1792 display: flex;
1793 align-items: center;
1794 gap: 0.5em;
1795 &.isThirdParty.isExtension {
1796 color: #F89406;
1797 }
1798 &.isCore {
1799 color: transparent;
1800 &.isExtension {
1801 color: #51A351;
1802 }
1803 &:after {
1804 content: '';
1805 order: -1;
1806 height: 14px;
1807 aspect-ratio: 1 / 1;
1808 background-image: url('/favicon.ico');
1809 background-size: contain;
1810 background-repeat: no-repeat;
1811 }
1812 }
1813
1814 &:hover {
1815 text-decoration: 1px dotted underline;
1816 }
1817 }
17591818
1760 >.body {1819 >.body {
1761 flex-direction: column;1820 flex-direction: column;
@@ -1843,7 +1902,8 @@ body[data-stscript-style] .hljs.language-stscript {
18431902
1844 >code {1903 >code {
1845 display: block;1904 display: block;
1846 padding: 0;1905 padding: 1px;
1906 tab-size: 4;
1847 }1907 }
1848 }1908 }
1849 }1909 }
@@ -3291,6 +3351,11 @@ grammarly-extension {
3291 text-align: center;3351 text-align: center;
3292}3352}
32933353
3354.menu_button.fa-fw {
3355 /* Font Awesome icons that are a menu button and should be fixed width need a slight fix. 1.25em is their default, but we need to account for button spacing. */
3356 min-width: calc(1.25em + 12px);
3357}
3358
3294.avatar_div .menu_button,3359.avatar_div .menu_button,
3295.form_create_bottom_buttons_block .menu_button {3360.form_create_bottom_buttons_block .menu_button {
3296 padding: 5px;3361 padding: 5px;
@@ -5300,3 +5365,49 @@ body:not(.movingUI) .drawer-content.maximized {
5300.regex-highlight {5365.regex-highlight {
5301 color: #FAF8F6;5366 color: #FAF8F6;
5302}5367}
5368
5369.popup:has(.faPicker) {
5370 /* Fix height for fa picker popup, otherwise search is making it resize weirdly */
5371 height: 70%;
5372 .popup-content {
5373 display: flex;
5374 flex-direction: column;
5375 }
5376}
5377
5378.faPicker-container {
5379 display: flex;
5380 flex-direction: column;
5381 overflow: hidden;;
5382}
5383
5384.faQuery-container {
5385 flex: 0 0 auto;
5386}
5387
5388.faPicker {
5389 flex: 1 1 auto;
5390 overflow: auto;
5391 gap: 1em;
5392 display: grid;
5393 grid-template-columns: repeat(auto-fill, minmax(3.5em, 1fr));
5394
5395 .menu_button {
5396 aspect-ratio: 1 / 1;
5397 font-size: 2em;
5398 height: 1lh;
5399 line-height: 1.2;
5400 padding: 0.25em;
5401 width: unset;
5402 box-sizing: content-box;
5403 &.hidden {
5404 display: none;
5405 }
5406 }
5407}
5408
5409.faPicker:not(:has(:not(.hidden)))::after {
5410 content: 'No icons found';
5411 color: var(--SmartThemeBodyColor);
5412 opacity: 0.7;
5413}