1import { describe, expect, it } from "vitest";
2import { CloverQuestionExtensions, transform } from "./mod.ts";
3
4const cloverExtensions: CloverQuestionExtensions = {
5 question: "q-block",
6 artifactRef: "artifact-ref",
7 questionRef: "question-ref",
8 labelledRedaction: "redacted",
9};
10
11describe("clover extensions", () => {
12 describe("question blocks", () => {
13 it("transforms q: prefix to question element", () => {
14 const result = transform({
15 source: "q: what is your favorite color?",
16 markdownOnly: true,
17 cloverExtensions,
18 });
19 expect(result.success).toBe(true);
20 if (result.success) {
21 expect(result.text).toContain("<q-block>");
22 expect(result.text).toContain("what is your favorite color?");
23 expect(result.text).toContain("</q-block>");
24 }
25 });
26
27 it("adds br between consecutive question lines", () => {
28 const result = transform({
29 source: "q: first question\nq: second question",
30 markdownOnly: true,
31 cloverExtensions,
32 });
33 expect(result.success).toBe(true);
34 if (result.success) {
35 expect(result.text).toContain(
36 "<q-block>first question<br>second question</q-block>",
37 );
38 }
39 });
40
41 it("does not add br for non-consecutive questions", () => {
42 const result = transform({
43 source: "q: first question\n\nsome text\n\nq: second question",
44 markdownOnly: true,
45 cloverExtensions,
46 });
47 expect(result.success).toBe(true);
48 if (result.success) {
49 expect(result.text).not.toContain("<br />");
50 }
51 });
52
53 it("parses inline markdown in questions", () => {
54 const result = transform({
55 source: "q: what about **bold** text?",
56 markdownOnly: true,
57 cloverExtensions,
58 });
59 expect(result.success).toBe(true);
60 if (result.success) {
61 expect(result.text).toContain("<strong>bold</strong>");
62 }
63 });
64 });
65
66 describe("@html blocks", () => {
67 it("passes through raw HTML", () => {
68 const result = transform({
69 source: '@html <custom-element attr="value">content</custom-element>',
70 markdownOnly: true,
71 cloverExtensions,
72 });
73 expect(result.success).toBe(true);
74 if (result.success) {
75 expect(result.text).toContain(
76 '<custom-element attr="value">content</custom-element>',
77 );
78 }
79 });
80
81 it("does not wrap @html in paragraph", () => {
82 const result = transform({
83 source: "@html <div>raw</div>",
84 markdownOnly: true,
85 cloverExtensions,
86 });
87 expect(result.success).toBe(true);
88 if (result.success) {
89 expect(result.text).not.toContain("<p>");
90 expect(result.text).toContain("<div>raw</div>");
91 }
92 });
93 });
94
95 describe("artifact refs", () => {
96 it("transforms @slug to artifact ref element", () => {
97 const result = transform({
98 source: "check out @its-snowing for details",
99 markdownOnly: true,
100 cloverExtensions,
101 });
102 expect(result.success).toBe(true);
103 if (result.success) {
104 expect(result.text).toContain(
105 "<artifact-ref>its-snowing</artifact-ref>",
106 );
107 }
108 });
109
110 it("handles multiple artifact refs", () => {
111 const result = transform({
112 source: "see @first-one and @second-one",
113 markdownOnly: true,
114 cloverExtensions,
115 });
116 expect(result.success).toBe(true);
117 if (result.success) {
118 expect(result.text).toContain("<artifact-ref>first-one</artifact-ref>");
119 expect(result.text).toContain(
120 "<artifact-ref>second-one</artifact-ref>",
121 );
122 }
123 });
124
125 it("only matches lowercase letters, digits, and hyphens", () => {
126 const result = transform({
127 source: "@valid-slug123 but @INVALID stays",
128 markdownOnly: true,
129 cloverExtensions,
130 });
131 expect(result.success).toBe(true);
132 if (result.success) {
133 expect(result.text).toContain(
134 "<artifact-ref>valid-slug123</artifact-ref>",
135 );
136 expect(result.text).toContain("@INVALID");
137 expect(result.text).not.toContain("<artifact-ref>INVALID");
138 }
139 });
140
141 it("does not match @html as artifact ref", () => {
142 const result = transform({
143 source: "@html <div>test</div>",
144 markdownOnly: true,
145 cloverExtensions,
146 });
147 expect(result.success).toBe(true);
148 if (result.success) {
149 expect(result.text).not.toContain("<artifact-ref>html");
150 }
151 });
152 });
153
154 describe("question refs", () => {
155 it("transforms 10-digit refs", () => {
156 const result = transform({
157 source: "see #2602142011 for more",
158 markdownOnly: true,
159 cloverExtensions,
160 });
161 expect(result.success).toBe(true);
162 if (result.success) {
163 expect(result.text).toContain(
164 "<question-ref>2602142011</question-ref>",
165 );
166 }
167 });
168
169 it("transforms 12-digit refs", () => {
170 const result = transform({
171 source: "see #260214201112 for more",
172 markdownOnly: true,
173 cloverExtensions,
174 });
175 expect(result.success).toBe(true);
176 if (result.success) {
177 expect(result.text).toContain(
178 "<question-ref>260214201112</question-ref>",
179 );
180 }
181 });
182
183 it("does not match other digit counts", () => {
184 const result = transform({
185 source: "#123456789 and #1234567890123",
186 markdownOnly: true,
187 cloverExtensions,
188 });
189 expect(result.success).toBe(true);
190 if (result.success) {
191 // 9 digits - not matched
192 expect(result.text).toContain("#123456789");
193 expect(result.text).not.toContain("<question-ref>123456789</");
194 // 13 digits - not matched
195 expect(result.text).toContain("#1234567890123");
196 }
197 });
198
199 it("handles multiple question refs", () => {
200 const result = transform({
201 source: "compare #2602142011 with #2602142012",
202 markdownOnly: true,
203 cloverExtensions,
204 });
205 expect(result.success).toBe(true);
206 if (result.success) {
207 expect(result.text).toContain(
208 "<question-ref>2602142011</question-ref>",
209 );
210 expect(result.text).toContain(
211 "<question-ref>2602142012</question-ref>",
212 );
213 }
214 });
215 });
216
217 describe("labelled redactions", () => {
218 it("transforms #label# to redaction element", () => {
219 const result = transform({
220 source: "the #name# is hidden",
221 markdownOnly: true,
222 cloverExtensions,
223 });
224 expect(result.success).toBe(true);
225 if (result.success) {
226 expect(result.text).toContain("<redacted>name</redacted>");
227 }
228 });
229
230 it("handles hyphens and underscores in labels", () => {
231 const result = transform({
232 source: "#my-secret_label#",
233 markdownOnly: true,
234 cloverExtensions,
235 });
236 expect(result.success).toBe(true);
237 if (result.success) {
238 expect(result.text).toContain("<redacted>my-secret_label</redacted>");
239 }
240 });
241
242 it("does not match empty labels", () => {
243 const result = transform({
244 source: "text ## ### #### more",
245 markdownOnly: true,
246 cloverExtensions,
247 });
248 expect(result.success).toBe(true);
249 if (result.success) {
250 expect(result.text).not.toContain("<redacted>");
251 // ## in the middle of text stays as-is
252 expect(result.text).toContain("##");
253 }
254 });
255
256 it("handles multiple redactions", () => {
257 const result = transform({
258 source: "#first# and #second#",
259 markdownOnly: true,
260 cloverExtensions,
261 });
262 expect(result.success).toBe(true);
263 if (result.success) {
264 expect(result.text).toContain("<redacted>first</redacted>");
265 expect(result.text).toContain("<redacted>second</redacted>");
266 }
267 });
268 });
269
270 describe("combined usage", () => {
271 it("handles all extensions in one document", () => {
272 const result = transform({
273 source: `q: what is @some-artifact about?
274q: also see #2602142011
275
276The #secret# is revealed.
277
278@html <hr class="divider" />`,
279 markdownOnly: true,
280 cloverExtensions,
281 });
282 expect(result.success).toBe(true);
283 if (result.success) {
284 expect(result.text).toBe(
285 `<q-block>what is <artifact-ref>some-artifact</artifact-ref> about?<br>also see <question-ref>2602142011</question-ref></q-block>
286<p>The <redacted>secret</redacted> is revealed.</p>
287<hr class="divider" />
288`,
289 );
290 }
291 });
292
293 it("works with markdownOnly: false (marko mode)", () => {
294 const result = transform({
295 source: "q: question with @artifact",
296 cloverExtensions,
297 });
298 expect(result.success).toBe(true);
299 if (result.success) {
300 expect(result.text).toContain("<q-block>");
301 expect(result.text).toContain("<artifact-ref>artifact</artifact-ref>");
302 }
303 });
304 });
305
306 describe("without cloverExtensions", () => {
307 it("does not parse clover syntax when extensions not provided", () => {
308 const result = transform({
309 source:
310 "q: not a question\n@not-artifact\n#2602142011\n##not-redacted##",
311 markdownOnly: true,
312 });
313 expect(result.success).toBe(true);
314 if (result.success) {
315 expect(result.text).toContain("q: not a question");
316 expect(result.text).not.toContain("<q-block>");
317 expect(result.text).not.toContain("<artifact-ref>");
318 expect(result.text).not.toContain("<question-ref>");
319 expect(result.text).not.toContain("<redacted>");
320 }
321 });
322 });
323});
324
325it("sample cases", () => {
326 expect(
327 transform({
328 source: `q: how is/was your valentine's day?
329
330it was overall good but i don't understand why i'm loved as much as i am. but i appreciate her in every way. also every time i look away, my [frieren collection](/file/q+a/frieren.jpeg) is slowly growing.
331
332@html <p style="color:#81D8D0;padding-left:30px">[clo] is rly cutee and nice to me so i love her alot and pat her head a lot and i miss [clo] meow hug her shes rly cool and does rly cool things and shes cute and likes frieren and meow</p>
333
334oh oops i left my answering machine out.`,
335 markdownOnly: true,
336 cloverExtensions,
337 }).text,
338 ).toBe(`<q-block>how is/was your valentine’s day?</q-block>
339<p>it was overall good but i don’t understand why i’m loved as much as i am. but i appreciate her in every way. also every time i look away, my <a href="/file/q+a/frieren.jpeg">frieren collection</a> is slowly growing.</p>
340<p style="color:#81D8D0;padding-left:30px">[clo] is rly cutee and nice to me so i love her alot and pat her head a lot and i miss [clo] meow hug her shes rly cool and does rly cool things and shes cute and likes frieren and meow</p>
341<p>oh oops i left my answering machine out.</p>
342`);
343});