@nkootstra/block-kit 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+
2
+ import { C as UserMention, S as Text, _ as MrkdwnNode, a as FormatDateOptions, b as Root, c as Bold, d as Code, f as DateNode, g as Link, h as Italic, i as parse, l as Broadcast, m as InlineNode, n as ParseOptions, o as formatSlackDate, p as Emoji, r as decodeEntities, s as BlockNode, t as parsePlainTextEmoji, u as ChannelMention, v as Preformatted, w as UsergroupMention, x as Strike, y as Quote } from "./index-gBduJdQh.js";
3
+ export { type BlockNode, type Bold, type Broadcast, type ChannelMention, type Code, type DateNode, type Emoji, type FormatDateOptions, type InlineNode, type Italic, type Link, type MrkdwnNode, type ParseOptions, type Preformatted, type Quote, type Root, type Strike, type Text, type UserMention, type UsergroupMention, decodeEntities, formatSlackDate, parse, parsePlainTextEmoji };
package/dist/mrkdwn.js ADDED
@@ -0,0 +1,3 @@
1
+
2
+ import { i as formatSlackDate, n as decodeEntities, r as parse, t as parsePlainTextEmoji } from "./parser-GbXBz4_f.js";
3
+ export { decodeEntities, formatSlackDate, parse, parsePlainTextEmoji };
@@ -0,0 +1,441 @@
1
+
2
+ //#region src/parser/date.ts
3
+ const TOKEN_RE = /\{(date_num|date_slash|date_long_full|date_long_pretty|date_long|date_pretty|date_short_pretty|date_short|date|time_secs|time|ago)\}/g;
4
+ /**
5
+ * Formats a `<!date^...>` token string the way Slack does, e.g.
6
+ * `formatSlackDate(1392734382, "{date_short} at {time}")` → `Feb 18, 2014 at 6:39 AM`.
7
+ * Unknown `{tokens}` are left as they are.
8
+ */
9
+ function formatSlackDate(timestamp, format, options = {}) {
10
+ const locale = options.locale ?? "en-US";
11
+ const timeZone = options.timeZone;
12
+ const date = /* @__PURE__ */ new Date(timestamp * 1e3);
13
+ const now = options.now ?? Date.now();
14
+ const fmt = (opts) => new Intl.DateTimeFormat(locale, {
15
+ timeZone,
16
+ ...opts
17
+ }).format(date);
18
+ const parts = (opts) => Object.fromEntries(new Intl.DateTimeFormat(locale, {
19
+ timeZone,
20
+ ...opts
21
+ }).formatToParts(date).map((p) => [p.type, p.value]));
22
+ const ordinalDay = () => {
23
+ const day = Number(parts({ day: "numeric" }).day);
24
+ return locale.startsWith("en") ? `${day}${ordinalSuffix(day)}` : String(day);
25
+ };
26
+ const monthName = (month) => parts({ month }).month ?? "";
27
+ const year = () => parts({ year: "numeric" }).year ?? "";
28
+ const relativeDay = () => relativeDayName(date, now, locale, timeZone);
29
+ const date_ = () => `${monthName("long")} ${ordinalDay()}, ${year()}`;
30
+ const dateShort = () => fmt({
31
+ month: "short",
32
+ day: "numeric",
33
+ year: "numeric"
34
+ });
35
+ const dateLong = () => `${parts({ weekday: "long" }).weekday}, ${date_()}`;
36
+ const tokens = {
37
+ date_num: () => {
38
+ const p = parts({
39
+ year: "numeric",
40
+ month: "2-digit",
41
+ day: "2-digit"
42
+ });
43
+ return `${p.year}-${p.month}-${p.day}`;
44
+ },
45
+ date_slash: () => fmt({
46
+ year: "numeric",
47
+ month: "2-digit",
48
+ day: "2-digit"
49
+ }),
50
+ date: date_,
51
+ date_pretty: () => relativeDay() ?? date_(),
52
+ date_short: dateShort,
53
+ date_short_pretty: () => relativeDay() ?? dateShort(),
54
+ date_long: dateLong,
55
+ date_long_full: dateLong,
56
+ date_long_pretty: () => relativeDay() ?? dateLong(),
57
+ time: () => fmt({
58
+ hour: "numeric",
59
+ minute: "2-digit"
60
+ }),
61
+ time_secs: () => fmt({
62
+ hour: "numeric",
63
+ minute: "2-digit",
64
+ second: "2-digit"
65
+ }),
66
+ ago: () => ago(date.getTime(), now, locale)
67
+ };
68
+ return format.replace(TOKEN_RE, (_, token) => tokens[token]?.() ?? `{${token}}`);
69
+ }
70
+ function ordinalSuffix(day) {
71
+ if (day % 100 >= 11 && day % 100 <= 13) return "th";
72
+ return [
73
+ "th",
74
+ "st",
75
+ "nd",
76
+ "rd"
77
+ ][day % 10] ?? "th";
78
+ }
79
+ function relativeDayName(date, now, locale, timeZone) {
80
+ const dayKey = (d) => new Intl.DateTimeFormat("en-CA", {
81
+ timeZone,
82
+ year: "numeric",
83
+ month: "2-digit",
84
+ day: "2-digit"
85
+ }).format(d);
86
+ const target = Date.parse(dayKey(date));
87
+ const today = Date.parse(dayKey(new Date(now)));
88
+ const diff = Math.round((target - today) / 864e5);
89
+ if (diff < -1 || diff > 1) return null;
90
+ const text = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(diff, "day");
91
+ return text.charAt(0).toUpperCase() + text.slice(1);
92
+ }
93
+ function ago(time, now, locale) {
94
+ const seconds = Math.round((time - now) / 1e3);
95
+ const abs = Math.abs(seconds);
96
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "always" });
97
+ if (abs < 60) return rtf.format(seconds, "second");
98
+ if (abs < 3600) return rtf.format(Math.round(seconds / 60), "minute");
99
+ if (abs < 86400) return rtf.format(Math.round(seconds / 3600), "hour");
100
+ if (abs < 2592e3) return rtf.format(Math.round(seconds / 86400), "day");
101
+ if (abs < 31536e3) return rtf.format(Math.round(seconds / 2592e3), "month");
102
+ return rtf.format(Math.round(seconds / 31536e3), "year");
103
+ }
104
+ //#endregion
105
+ //#region src/parser/parse.ts
106
+ const MARKER_TYPES = {
107
+ "*": "bold",
108
+ _: "italic",
109
+ "~": "strike"
110
+ };
111
+ const EMOJI_RE$1 = /^:([a-z0-9_+'-]+):(?::skin-tone-([2-6]):)?/i;
112
+ const BARE_URL_RE = /^(?:https?:\/\/|www\.)[^\s<>]+/i;
113
+ const TRAILING_PUNCTUATION_RE = /[.,;:!?'")\]]+$/;
114
+ /** Parses Slack mrkdwn into an AST. Never throws: malformed markup is kept as text. */
115
+ function parse(input, options = {}) {
116
+ return {
117
+ type: "root",
118
+ children: parseBlocks(input, options)
119
+ };
120
+ }
121
+ function decodeEntities(text) {
122
+ return text.replace(/&(amp|lt|gt);/g, (_, name) => name === "amp" ? "&" : name === "lt" ? "<" : ">");
123
+ }
124
+ function parseBlocks(input, options) {
125
+ const nodes = [];
126
+ let rest = input;
127
+ while (rest.length > 0) {
128
+ const open = rest.indexOf("```");
129
+ const close = open === -1 ? -1 : rest.indexOf("```", open + 3);
130
+ if (open === -1 || close === -1) {
131
+ nodes.push(...parseQuotes(rest, options));
132
+ break;
133
+ }
134
+ if (open > 0) nodes.push(...parseQuotes(stripTrailingNewline(rest.slice(0, open)), options));
135
+ const value = rest.slice(open + 3, close);
136
+ nodes.push({
137
+ type: "preformatted",
138
+ value: decodeEntities(trimFenceNewlines(value))
139
+ });
140
+ rest = stripLeadingNewline(rest.slice(close + 3));
141
+ }
142
+ return nodes;
143
+ }
144
+ /** Splits text into runs of quoted and unquoted lines. */
145
+ function parseQuotes(text, options) {
146
+ const nodes = [];
147
+ const lines = text.split("\n");
148
+ let plain = [];
149
+ let quoted = [];
150
+ const flushPlain = () => {
151
+ if (plain.length === 0) return;
152
+ nodes.push(...parseInline(plain.join("\n"), options));
153
+ plain = [];
154
+ };
155
+ const flushQuoted = () => {
156
+ if (quoted.length === 0) return;
157
+ nodes.push({
158
+ type: "quote",
159
+ children: parseInline(quoted.join("\n"), options)
160
+ });
161
+ quoted = [];
162
+ };
163
+ for (let i = 0; i < lines.length; i++) {
164
+ const line = lines[i];
165
+ const multi = matchQuotePrefix(line, true);
166
+ if (multi !== null) {
167
+ flushPlain();
168
+ quoted.push(multi, ...lines.slice(i + 1));
169
+ break;
170
+ }
171
+ const single = matchQuotePrefix(line, false);
172
+ if (single !== null) {
173
+ flushPlain();
174
+ quoted.push(single);
175
+ } else {
176
+ flushQuoted();
177
+ plain.push(line);
178
+ }
179
+ }
180
+ flushPlain();
181
+ flushQuoted();
182
+ return nodes;
183
+ }
184
+ /** Returns the line without its quote prefix, or null when it isn't a quote line. */
185
+ function matchQuotePrefix(line, multi) {
186
+ const gt = "(?:>|&gt;)";
187
+ const match = (multi ? new RegExp(`^${gt}{3} ?`) : new RegExp(`^${gt} ?`)).exec(line);
188
+ return match ? line.slice(match[0].length) : null;
189
+ }
190
+ function parseInline(text, options) {
191
+ const nodes = [];
192
+ let buffer = "";
193
+ let i = 0;
194
+ const flush = () => {
195
+ if (buffer) pushText(nodes, decodeEntities(buffer));
196
+ buffer = "";
197
+ };
198
+ while (i < text.length) {
199
+ const ch = text[i];
200
+ const prev = text[i - 1];
201
+ if (ch === "`") {
202
+ const end = text.indexOf("`", i + 1);
203
+ if (end > i + 1) {
204
+ flush();
205
+ nodes.push({
206
+ type: "code",
207
+ value: decodeEntities(text.slice(i + 1, end))
208
+ });
209
+ i = end + 1;
210
+ continue;
211
+ }
212
+ }
213
+ if (ch === "<") {
214
+ const end = text.indexOf(">", i + 1);
215
+ if (end !== -1) {
216
+ const node = parseAngle(text.slice(i + 1, end), options);
217
+ if (node) {
218
+ flush();
219
+ nodes.push(node);
220
+ i = end + 1;
221
+ continue;
222
+ }
223
+ }
224
+ }
225
+ if (ch === "*" || ch === "_" || ch === "~") {
226
+ if (isBoundary(prev)) {
227
+ const end = findCloser(text, i, ch);
228
+ if (end !== -1) {
229
+ flush();
230
+ nodes.push({
231
+ type: MARKER_TYPES[ch],
232
+ children: parseInline(text.slice(i + 1, end), options)
233
+ });
234
+ i = end + 1;
235
+ continue;
236
+ }
237
+ }
238
+ }
239
+ if (ch === ":") {
240
+ const match = EMOJI_RE$1.exec(text.slice(i));
241
+ if (match) {
242
+ flush();
243
+ const skinTone = match[2] ? Number(match[2]) : void 0;
244
+ nodes.push({
245
+ type: "emoji",
246
+ name: match[1].toLowerCase(),
247
+ ...skinTone ? { skinTone } : {}
248
+ });
249
+ i += match[0].length;
250
+ continue;
251
+ }
252
+ }
253
+ if (!options.verbatim && (ch === "h" || ch === "w" || ch === "H" || ch === "W")) {
254
+ if (isBoundary(prev)) {
255
+ const match = BARE_URL_RE.exec(text.slice(i));
256
+ if (match) {
257
+ const raw = match[0].replace(TRAILING_PUNCTUATION_RE, "");
258
+ if (raw.length > 0 && !/^www\.$/i.test(raw)) {
259
+ flush();
260
+ const url = decodeEntities(raw);
261
+ nodes.push({
262
+ type: "link",
263
+ url: /^www\./i.test(url) ? `http://${url}` : url,
264
+ children: [{
265
+ type: "text",
266
+ value: url
267
+ }]
268
+ });
269
+ i += raw.length;
270
+ continue;
271
+ }
272
+ }
273
+ }
274
+ }
275
+ buffer += ch;
276
+ i++;
277
+ }
278
+ flush();
279
+ return nodes;
280
+ }
281
+ /** Parses the inside of `<...>`. Returns null when it isn't recognized markup. */
282
+ function parseAngle(inner, options) {
283
+ if (inner.length === 0) return null;
284
+ const pipe = inner.indexOf("|");
285
+ const target = pipe === -1 ? inner : inner.slice(0, pipe);
286
+ const label = pipe === -1 ? void 0 : inner.slice(pipe + 1);
287
+ const withLabel = label !== void 0 && label !== "" ? { label: decodeEntities(label) } : {};
288
+ if (target.startsWith("@")) {
289
+ const id = target.slice(1);
290
+ return id ? {
291
+ type: "user",
292
+ id,
293
+ ...withLabel
294
+ } : null;
295
+ }
296
+ if (target.startsWith("#")) {
297
+ const id = target.slice(1);
298
+ return id ? {
299
+ type: "channel",
300
+ id,
301
+ ...withLabel
302
+ } : null;
303
+ }
304
+ if (target.startsWith("!")) return parseSpecial(target.slice(1), label);
305
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target)) {
306
+ const url = decodeEntities(target);
307
+ return label ? {
308
+ type: "link",
309
+ url,
310
+ children: parseInline(label, {
311
+ ...options,
312
+ verbatim: true
313
+ })
314
+ } : {
315
+ type: "link",
316
+ url
317
+ };
318
+ }
319
+ return null;
320
+ }
321
+ function parseSpecial(command, label) {
322
+ const withLabel = label ? { label: decodeEntities(label) } : {};
323
+ if (command === "here" || command === "channel" || command === "everyone") return {
324
+ type: "broadcast",
325
+ range: command,
326
+ ...withLabel
327
+ };
328
+ if (command.startsWith("subteam^")) {
329
+ const id = command.slice(8);
330
+ return id ? {
331
+ type: "usergroup",
332
+ id,
333
+ ...withLabel
334
+ } : null;
335
+ }
336
+ if (command.startsWith("date^")) {
337
+ const [, ts, format, url] = command.split("^");
338
+ const timestamp = Number(ts);
339
+ if (!ts || !Number.isFinite(timestamp) || format === void 0) return null;
340
+ return {
341
+ type: "date",
342
+ timestamp,
343
+ format: decodeEntities(format),
344
+ ...url ? { url: decodeEntities(url) } : {},
345
+ fallback: decodeEntities(label ?? "")
346
+ };
347
+ }
348
+ return null;
349
+ }
350
+ /** Word boundary check for the character before an opening marker or after a closing one. */
351
+ function isBoundary(ch) {
352
+ return ch === void 0 || !/[\p{L}\p{N}]/u.test(ch);
353
+ }
354
+ /**
355
+ * Finds the closing marker for a `*`, `_` or `~` span opening at `start`. The span must stay on
356
+ * one line, have non-empty content that doesn't start or end with whitespace, and be followed by a
357
+ * word boundary. Code spans and `<...>` markup are skipped so markers inside them don't count.
358
+ */
359
+ function findCloser(text, start, marker) {
360
+ const first = text[start + 1];
361
+ if (first === void 0 || first === marker || /\s/.test(first)) return -1;
362
+ for (let j = start + 1; j < text.length; j++) {
363
+ const ch = text[j];
364
+ if (ch === "\n") return -1;
365
+ if (ch === "`") {
366
+ const end = text.indexOf("`", j + 1);
367
+ if (end !== -1) j = end;
368
+ continue;
369
+ }
370
+ if (ch === "<") {
371
+ const end = text.indexOf(">", j + 1);
372
+ if (end !== -1) j = end;
373
+ continue;
374
+ }
375
+ if (ch === marker && j > start + 1 && !/\s/.test(text[j - 1]) && isBoundary(text[j + 1])) return j;
376
+ }
377
+ return -1;
378
+ }
379
+ function pushText(nodes, value) {
380
+ const last = nodes[nodes.length - 1];
381
+ if (last?.type === "text") last.value += value;
382
+ else nodes.push({
383
+ type: "text",
384
+ value
385
+ });
386
+ }
387
+ function trimFenceNewlines(value) {
388
+ return stripTrailingNewline(stripLeadingNewline(value));
389
+ }
390
+ function stripLeadingNewline(value) {
391
+ return value.startsWith("\n") ? value.slice(1) : value;
392
+ }
393
+ function stripTrailingNewline(value) {
394
+ return value.endsWith("\n") ? value.slice(0, -1) : value;
395
+ }
396
+ //#endregion
397
+ //#region src/parser/plainText.ts
398
+ const EMOJI_RE = /^:([a-z0-9_+'-]+):(?::skin-tone-([2-6]):)?/i;
399
+ /**
400
+ * Converts `:name:` shortcodes in a `plain_text` object's text into emoji nodes. Plain text has no
401
+ * other mrkdwn formatting (no bold/links/mentions), so this only ever produces `text` and `emoji`.
402
+ */
403
+ function parsePlainTextEmoji(input) {
404
+ const nodes = [];
405
+ let buffer = "";
406
+ let i = 0;
407
+ const flush = () => {
408
+ if (!buffer) return;
409
+ const last = nodes[nodes.length - 1];
410
+ if (last?.type === "text") last.value += buffer;
411
+ else nodes.push({
412
+ type: "text",
413
+ value: buffer
414
+ });
415
+ buffer = "";
416
+ };
417
+ while (i < input.length) {
418
+ if (input[i] === ":") {
419
+ const match = EMOJI_RE.exec(input.slice(i));
420
+ if (match) {
421
+ flush();
422
+ const skinTone = match[2] ? Number(match[2]) : void 0;
423
+ nodes.push({
424
+ type: "emoji",
425
+ name: match[1].toLowerCase(),
426
+ ...skinTone ? { skinTone } : {}
427
+ });
428
+ i += match[0].length;
429
+ continue;
430
+ }
431
+ }
432
+ buffer += input[i];
433
+ i++;
434
+ }
435
+ flush();
436
+ return nodes;
437
+ }
438
+ //#endregion
439
+ export { formatSlackDate as i, decodeEntities as n, parse as r, parsePlainTextEmoji as t };
440
+
441
+ //# sourceMappingURL=parser-GbXBz4_f.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser-GbXBz4_f.js","names":["EMOJI_RE"],"sources":["../src/parser/date.ts","../src/parser/parse.ts","../src/parser/plainText.ts"],"sourcesContent":["export interface FormatDateOptions {\n /** BCP 47 locale. Slack formats dates in the viewer's locale. Defaults to `en-US`. */\n locale?: string;\n /** IANA time zone. Defaults to the runtime's zone. */\n timeZone?: string;\n /** Reference time for `{ago}` and the `_pretty` variants. Defaults to `Date.now()`. */\n now?: number;\n}\n\nconst TOKEN_RE =\n /\\{(date_num|date_slash|date_long_full|date_long_pretty|date_long|date_pretty|date_short_pretty|date_short|date|time_secs|time|ago)\\}/g;\n\n/**\n * Formats a `<!date^...>` token string the way Slack does, e.g.\n * `formatSlackDate(1392734382, \"{date_short} at {time}\")` → `Feb 18, 2014 at 6:39 AM`.\n * Unknown `{tokens}` are left as they are.\n */\nexport function formatSlackDate(\n timestamp: number,\n format: string,\n options: FormatDateOptions = {},\n): string {\n const locale = options.locale ?? \"en-US\";\n const timeZone = options.timeZone;\n const date = new Date(timestamp * 1000);\n const now = options.now ?? Date.now();\n\n const fmt = (opts: Intl.DateTimeFormatOptions) =>\n new Intl.DateTimeFormat(locale, { timeZone, ...opts }).format(date);\n const parts = (opts: Intl.DateTimeFormatOptions) =>\n Object.fromEntries(\n new Intl.DateTimeFormat(locale, { timeZone, ...opts })\n .formatToParts(date)\n .map((p) => [p.type, p.value]),\n ) as Record<string, string>;\n\n const ordinalDay = () => {\n const day = Number(parts({ day: \"numeric\" }).day);\n return locale.startsWith(\"en\") ? `${day}${ordinalSuffix(day)}` : String(day);\n };\n const monthName = (month: \"long\" | \"short\") => parts({ month }).month ?? \"\";\n const year = () => parts({ year: \"numeric\" }).year ?? \"\";\n const relativeDay = () => relativeDayName(date, now, locale, timeZone);\n\n const date_ = () => `${monthName(\"long\")} ${ordinalDay()}, ${year()}`;\n const dateShort = () => fmt({ month: \"short\", day: \"numeric\", year: \"numeric\" });\n const dateLong = () => `${parts({ weekday: \"long\" }).weekday}, ${date_()}`;\n\n const tokens: Record<string, () => string> = {\n date_num: () => {\n const p = parts({ year: \"numeric\", month: \"2-digit\", day: \"2-digit\" });\n return `${p.year}-${p.month}-${p.day}`;\n },\n date_slash: () => fmt({ year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }),\n date: date_,\n date_pretty: () => relativeDay() ?? date_(),\n date_short: dateShort,\n date_short_pretty: () => relativeDay() ?? dateShort(),\n date_long: dateLong,\n date_long_full: dateLong,\n date_long_pretty: () => relativeDay() ?? dateLong(),\n time: () => fmt({ hour: \"numeric\", minute: \"2-digit\" }),\n time_secs: () => fmt({ hour: \"numeric\", minute: \"2-digit\", second: \"2-digit\" }),\n ago: () => ago(date.getTime(), now, locale),\n };\n\n return format.replace(TOKEN_RE, (_, token: string) => tokens[token]?.() ?? `{${token}}`);\n}\n\nfunction ordinalSuffix(day: number): string {\n if (day % 100 >= 11 && day % 100 <= 13) return \"th\";\n return [\"th\", \"st\", \"nd\", \"rd\"][day % 10] ?? \"th\";\n}\n\nfunction relativeDayName(\n date: Date,\n now: number,\n locale: string,\n timeZone: string | undefined,\n): string | null {\n const dayKey = (d: Date) =>\n new Intl.DateTimeFormat(\"en-CA\", {\n timeZone,\n year: \"numeric\",\n month: \"2-digit\",\n day: \"2-digit\",\n }).format(d);\n const target = Date.parse(dayKey(date));\n const today = Date.parse(dayKey(new Date(now)));\n const diff = Math.round((target - today) / 86_400_000);\n if (diff < -1 || diff > 1) return null;\n const text = new Intl.RelativeTimeFormat(locale, { numeric: \"auto\" }).format(diff, \"day\");\n return text.charAt(0).toUpperCase() + text.slice(1);\n}\n\nfunction ago(time: number, now: number, locale: string): string {\n const seconds = Math.round((time - now) / 1000);\n const abs = Math.abs(seconds);\n const rtf = new Intl.RelativeTimeFormat(locale, { numeric: \"always\" });\n if (abs < 60) return rtf.format(seconds, \"second\");\n if (abs < 3600) return rtf.format(Math.round(seconds / 60), \"minute\");\n if (abs < 86_400) return rtf.format(Math.round(seconds / 3600), \"hour\");\n if (abs < 2_592_000) return rtf.format(Math.round(seconds / 86_400), \"day\");\n if (abs < 31_536_000) return rtf.format(Math.round(seconds / 2_592_000), \"month\");\n return rtf.format(Math.round(seconds / 31_536_000), \"year\");\n}\n","import type { InlineNode, MrkdwnNode, Root } from \"./ast\";\n\nexport interface ParseOptions {\n /**\n * Mirrors the `verbatim` flag on Slack text objects. When `false` (Slack's default),\n * bare URLs are turned into links. When `true`, only explicit `<...>` markup is linked.\n */\n verbatim?: boolean;\n}\n\ntype Marker = \"*\" | \"_\" | \"~\";\n\nconst MARKER_TYPES = { \"*\": \"bold\", _: \"italic\", \"~\": \"strike\" } as const;\n\nconst EMOJI_RE = /^:([a-z0-9_+'-]+):(?::skin-tone-([2-6]):)?/i;\nconst BARE_URL_RE = /^(?:https?:\\/\\/|www\\.)[^\\s<>]+/i;\nconst TRAILING_PUNCTUATION_RE = /[.,;:!?'\")\\]]+$/;\n\n/** Parses Slack mrkdwn into an AST. Never throws: malformed markup is kept as text. */\nexport function parse(input: string, options: ParseOptions = {}): Root {\n return { type: \"root\", children: parseBlocks(input, options) };\n}\n\nexport function decodeEntities(text: string): string {\n return text.replace(/&(amp|lt|gt);/g, (_, name: string) =>\n name === \"amp\" ? \"&\" : name === \"lt\" ? \"<\" : \">\",\n );\n}\n\nfunction parseBlocks(input: string, options: ParseOptions): MrkdwnNode[] {\n const nodes: MrkdwnNode[] = [];\n let rest = input;\n\n while (rest.length > 0) {\n const open = rest.indexOf(\"```\");\n const close = open === -1 ? -1 : rest.indexOf(\"```\", open + 3);\n if (open === -1 || close === -1) {\n nodes.push(...parseQuotes(rest, options));\n break;\n }\n if (open > 0) nodes.push(...parseQuotes(stripTrailingNewline(rest.slice(0, open)), options));\n const value = rest.slice(open + 3, close);\n // Slack drops a single newline directly after the opening and before the closing fence.\n nodes.push({ type: \"preformatted\", value: decodeEntities(trimFenceNewlines(value)) });\n rest = stripLeadingNewline(rest.slice(close + 3));\n }\n\n return nodes;\n}\n\n/** Splits text into runs of quoted and unquoted lines. */\nfunction parseQuotes(text: string, options: ParseOptions): MrkdwnNode[] {\n const nodes: MrkdwnNode[] = [];\n const lines = text.split(\"\\n\");\n let plain: string[] = [];\n let quoted: string[] = [];\n\n const flushPlain = () => {\n if (plain.length === 0) return;\n nodes.push(...parseInline(plain.join(\"\\n\"), options));\n plain = [];\n };\n const flushQuoted = () => {\n if (quoted.length === 0) return;\n nodes.push({ type: \"quote\", children: parseInline(quoted.join(\"\\n\"), options) });\n quoted = [];\n };\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i] as string;\n const multi = matchQuotePrefix(line, true);\n if (multi !== null) {\n flushPlain();\n quoted.push(multi, ...lines.slice(i + 1));\n break;\n }\n const single = matchQuotePrefix(line, false);\n if (single !== null) {\n flushPlain();\n quoted.push(single);\n } else {\n flushQuoted();\n plain.push(line);\n }\n }\n flushPlain();\n flushQuoted();\n return nodes;\n}\n\n/** Returns the line without its quote prefix, or null when it isn't a quote line. */\nfunction matchQuotePrefix(line: string, multi: boolean): string | null {\n const gt = \"(?:>|&gt;)\";\n const re = multi ? new RegExp(`^${gt}{3} ?`) : new RegExp(`^${gt} ?`);\n const match = re.exec(line);\n return match ? line.slice(match[0].length) : null;\n}\n\nfunction parseInline(text: string, options: ParseOptions): InlineNode[] {\n const nodes: InlineNode[] = [];\n let buffer = \"\";\n let i = 0;\n\n const flush = () => {\n if (buffer) pushText(nodes, decodeEntities(buffer));\n buffer = \"\";\n };\n\n while (i < text.length) {\n const ch = text[i] as string;\n const prev = text[i - 1];\n\n if (ch === \"`\") {\n const end = text.indexOf(\"`\", i + 1);\n if (end > i + 1) {\n flush();\n nodes.push({ type: \"code\", value: decodeEntities(text.slice(i + 1, end)) });\n i = end + 1;\n continue;\n }\n }\n\n if (ch === \"<\") {\n const end = text.indexOf(\">\", i + 1);\n if (end !== -1) {\n const node = parseAngle(text.slice(i + 1, end), options);\n if (node) {\n flush();\n nodes.push(node);\n i = end + 1;\n continue;\n }\n }\n }\n\n if (ch === \"*\" || ch === \"_\" || ch === \"~\") {\n if (isBoundary(prev)) {\n const end = findCloser(text, i, ch);\n if (end !== -1) {\n flush();\n nodes.push({\n type: MARKER_TYPES[ch],\n children: parseInline(text.slice(i + 1, end), options),\n });\n i = end + 1;\n continue;\n }\n }\n }\n\n if (ch === \":\") {\n const match = EMOJI_RE.exec(text.slice(i));\n if (match) {\n flush();\n const skinTone = match[2] ? Number(match[2]) : undefined;\n nodes.push({\n type: \"emoji\",\n name: (match[1] as string).toLowerCase(),\n ...(skinTone ? { skinTone } : {}),\n });\n i += match[0].length;\n continue;\n }\n }\n\n if (!options.verbatim && (ch === \"h\" || ch === \"w\" || ch === \"H\" || ch === \"W\")) {\n if (isBoundary(prev)) {\n const match = BARE_URL_RE.exec(text.slice(i));\n if (match) {\n const raw = match[0].replace(TRAILING_PUNCTUATION_RE, \"\");\n if (raw.length > 0 && !/^www\\.$/i.test(raw)) {\n flush();\n const url = decodeEntities(raw);\n nodes.push({\n type: \"link\",\n url: /^www\\./i.test(url) ? `http://${url}` : url,\n children: [{ type: \"text\", value: url }],\n });\n i += raw.length;\n continue;\n }\n }\n }\n }\n\n buffer += ch;\n i++;\n }\n\n flush();\n return nodes;\n}\n\n/** Parses the inside of `<...>`. Returns null when it isn't recognized markup. */\nfunction parseAngle(inner: string, options: ParseOptions): InlineNode | null {\n if (inner.length === 0) return null;\n const pipe = inner.indexOf(\"|\");\n const target = pipe === -1 ? inner : inner.slice(0, pipe);\n const label = pipe === -1 ? undefined : inner.slice(pipe + 1);\n const withLabel = label !== undefined && label !== \"\" ? { label: decodeEntities(label) } : {};\n\n if (target.startsWith(\"@\")) {\n const id = target.slice(1);\n return id ? { type: \"user\", id, ...withLabel } : null;\n }\n if (target.startsWith(\"#\")) {\n const id = target.slice(1);\n return id ? { type: \"channel\", id, ...withLabel } : null;\n }\n if (target.startsWith(\"!\")) {\n return parseSpecial(target.slice(1), label);\n }\n if (/^[a-z][a-z0-9+.-]*:/i.test(target)) {\n const url = decodeEntities(target);\n return label\n ? { type: \"link\", url, children: parseInline(label, { ...options, verbatim: true }) }\n : { type: \"link\", url };\n }\n return null;\n}\n\nfunction parseSpecial(command: string, label: string | undefined): InlineNode | null {\n const withLabel = label ? { label: decodeEntities(label) } : {};\n\n if (command === \"here\" || command === \"channel\" || command === \"everyone\") {\n return { type: \"broadcast\", range: command, ...withLabel };\n }\n if (command.startsWith(\"subteam^\")) {\n const id = command.slice(\"subteam^\".length);\n return id ? { type: \"usergroup\", id, ...withLabel } : null;\n }\n if (command.startsWith(\"date^\")) {\n const [, ts, format, url] = command.split(\"^\");\n const timestamp = Number(ts);\n if (!ts || !Number.isFinite(timestamp) || format === undefined) return null;\n return {\n type: \"date\",\n timestamp,\n format: decodeEntities(format),\n ...(url ? { url: decodeEntities(url) } : {}),\n fallback: decodeEntities(label ?? \"\"),\n };\n }\n return null;\n}\n\n/** Word boundary check for the character before an opening marker or after a closing one. */\nfunction isBoundary(ch: string | undefined): boolean {\n return ch === undefined || !/[\\p{L}\\p{N}]/u.test(ch);\n}\n\n/**\n * Finds the closing marker for a `*`, `_` or `~` span opening at `start`. The span must stay on\n * one line, have non-empty content that doesn't start or end with whitespace, and be followed by a\n * word boundary. Code spans and `<...>` markup are skipped so markers inside them don't count.\n */\nfunction findCloser(text: string, start: number, marker: Marker): number {\n const first = text[start + 1];\n if (first === undefined || first === marker || /\\s/.test(first)) return -1;\n\n for (let j = start + 1; j < text.length; j++) {\n const ch = text[j];\n if (ch === \"\\n\") return -1;\n if (ch === \"`\") {\n const end = text.indexOf(\"`\", j + 1);\n if (end !== -1) j = end;\n continue;\n }\n if (ch === \"<\") {\n const end = text.indexOf(\">\", j + 1);\n if (end !== -1) j = end;\n continue;\n }\n if (\n ch === marker &&\n j > start + 1 &&\n !/\\s/.test(text[j - 1] as string) &&\n isBoundary(text[j + 1])\n ) {\n return j;\n }\n }\n return -1;\n}\n\nfunction pushText(nodes: InlineNode[], value: string): void {\n const last = nodes[nodes.length - 1];\n if (last?.type === \"text\") last.value += value;\n else nodes.push({ type: \"text\", value });\n}\n\nfunction trimFenceNewlines(value: string): string {\n return stripTrailingNewline(stripLeadingNewline(value));\n}\n\nfunction stripLeadingNewline(value: string): string {\n return value.startsWith(\"\\n\") ? value.slice(1) : value;\n}\n\nfunction stripTrailingNewline(value: string): string {\n return value.endsWith(\"\\n\") ? value.slice(0, -1) : value;\n}\n","import type { Emoji, Text } from \"./ast\";\n\nconst EMOJI_RE = /^:([a-z0-9_+'-]+):(?::skin-tone-([2-6]):)?/i;\n\n/**\n * Converts `:name:` shortcodes in a `plain_text` object's text into emoji nodes. Plain text has no\n * other mrkdwn formatting (no bold/links/mentions), so this only ever produces `text` and `emoji`.\n */\nexport function parsePlainTextEmoji(input: string): Array<Text | Emoji> {\n const nodes: Array<Text | Emoji> = [];\n let buffer = \"\";\n let i = 0;\n\n const flush = () => {\n if (!buffer) return;\n const last = nodes[nodes.length - 1];\n if (last?.type === \"text\") last.value += buffer;\n else nodes.push({ type: \"text\", value: buffer });\n buffer = \"\";\n };\n\n while (i < input.length) {\n if (input[i] === \":\") {\n const match = EMOJI_RE.exec(input.slice(i));\n if (match) {\n flush();\n const skinTone = match[2] ? Number(match[2]) : undefined;\n nodes.push({\n type: \"emoji\",\n name: (match[1] as string).toLowerCase(),\n ...(skinTone ? { skinTone } : {}),\n });\n i += match[0].length;\n continue;\n }\n }\n buffer += input[i];\n i++;\n }\n flush();\n return nodes;\n}\n"],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+
2
+ import { HttpTransportOptions, InteractionError, InteractionResult, InteractionTransport, encodeInteraction, httpTransport, readInteractionResponse } from "./transport.js";
3
+ //#region src/interactivity/client.d.ts
4
+ interface SendInteractionOptions {
5
+ /** The app's Slack request URL (an Events API / interactivity endpoint). */
6
+ requestUrl: string;
7
+ /** Same signing secret the receiving app was configured with. */
8
+ signingSecret: string;
9
+ /** A payload built with `@nkootstra/block-kit`'s payload builders. */
10
+ payload: unknown;
11
+ /** Defaults to the current time; override for signature tests. */
12
+ timestamp?: number;
13
+ /** Injectable for tests; defaults to the global `fetch`. */
14
+ fetch?: typeof fetch;
15
+ }
16
+ type SendInteractionResult = InteractionResult;
17
+ /**
18
+ * POSTs a Slack-format interaction payload to an app's request URL exactly as Slack does:
19
+ * `application/x-www-form-urlencoded` body `payload=<json>`, signed with `X-Slack-Signature` /
20
+ * `X-Slack-Request-Timestamp`. If the app acks a `view_submission` with a `response_action`
21
+ * (errors/update/push/clear), it is parsed and returned in `responseAction`.
22
+ */
23
+ export declare function sendInteraction(opts: SendInteractionOptions): Promise<SendInteractionResult>;
24
+ /** POSTs an already-encoded body, signed with the app's secret, as Slack would. */
25
+ export declare function postSigned(opts: {
26
+ requestUrl: string;
27
+ signingSecret: string;
28
+ body: string;
29
+ timestamp?: number;
30
+ fetch?: typeof fetch;
31
+ }): Promise<Response>;
32
+ //#endregion
33
+ //#region src/interactivity/relay.d.ts
34
+ interface InteractionRelayOptions {
35
+ /** The app's Slack request URL (an Events API / interactivity endpoint). */
36
+ requestUrl: string;
37
+ /** Same signing secret the receiving app was configured with. Stays on the server. */
38
+ signingSecret: string;
39
+ /** Injectable for tests; defaults to the global `fetch`. */
40
+ fetch?: typeof fetch;
41
+ }
42
+ /**
43
+ * A server-side `(Request) => Response` handler that signs the unsigned interactions an
44
+ * `httpTransport` POSTs from the browser and forwards them to the app, passing its reply back.
45
+ * The signing secret never reaches the page, and the browser only talks to its own origin.
46
+ *
47
+ * Mount it anywhere that speaks the Fetch API: a Next.js route (`export const POST = relay`),
48
+ * `Bun.serve`, Hono, or a Vite dev-server middleware.
49
+ */
50
+ export declare function createInteractionRelay(options: InteractionRelayOptions): (request: Request) => Promise<Response>;
51
+ //#endregion
52
+ //#region src/interactivity/sign.d.ts
53
+ /**
54
+ * Slack's request-signing scheme (v0): `v0=` + HMAC-SHA256(signing secret, `v0:{ts}:{body}`),
55
+ * hex-encoded. See https://api.slack.com/authentication/verifying-requests-from-slack.
56
+ */
57
+ export declare function signSlackRequest(signingSecret: string, timestamp: number, body: string): string;
58
+ //#endregion
59
+ export { type HttpTransportOptions, InteractionError, type InteractionRelayOptions, type InteractionResult, type InteractionTransport, type SendInteractionOptions, type SendInteractionResult, encodeInteraction, httpTransport, readInteractionResponse };
60
+ //# sourceMappingURL=server.d.ts.map
package/dist/server.js ADDED
@@ -0,0 +1,83 @@
1
+
2
+ import { InteractionError, encodeInteraction, httpTransport, readInteractionResponse } from "./transport.js";
3
+ import { createHmac } from "node:crypto";
4
+ //#region src/interactivity/sign.ts
5
+ /**
6
+ * Slack's request-signing scheme (v0): `v0=` + HMAC-SHA256(signing secret, `v0:{ts}:{body}`),
7
+ * hex-encoded. See https://api.slack.com/authentication/verifying-requests-from-slack.
8
+ */
9
+ function signSlackRequest(signingSecret, timestamp, body) {
10
+ const hmac = createHmac("sha256", signingSecret);
11
+ hmac.update(`v0:${timestamp}:${body}`);
12
+ return `v0=${hmac.digest("hex")}`;
13
+ }
14
+ //#endregion
15
+ //#region src/interactivity/client.ts
16
+ /**
17
+ * POSTs a Slack-format interaction payload to an app's request URL exactly as Slack does:
18
+ * `application/x-www-form-urlencoded` body `payload=<json>`, signed with `X-Slack-Signature` /
19
+ * `X-Slack-Request-Timestamp`. If the app acks a `view_submission` with a `response_action`
20
+ * (errors/update/push/clear), it is parsed and returned in `responseAction`.
21
+ */
22
+ async function sendInteraction(opts) {
23
+ const res = await postSigned({
24
+ ...opts,
25
+ body: encodeInteraction(opts.payload)
26
+ });
27
+ return readInteractionResponse(res);
28
+ }
29
+ /** POSTs an already-encoded body, signed with the app's secret, as Slack would. */
30
+ function postSigned(opts) {
31
+ const timestamp = opts.timestamp ?? Math.floor(Date.now() / 1e3);
32
+ const signature = signSlackRequest(opts.signingSecret, timestamp, opts.body);
33
+ return (opts.fetch ?? fetch)(opts.requestUrl, {
34
+ method: "POST",
35
+ headers: {
36
+ "content-type": "application/x-www-form-urlencoded",
37
+ "x-slack-signature": signature,
38
+ "x-slack-request-timestamp": String(timestamp)
39
+ },
40
+ body: opts.body
41
+ });
42
+ }
43
+ //#endregion
44
+ //#region src/interactivity/relay.ts
45
+ /**
46
+ * A server-side `(Request) => Response` handler that signs the unsigned interactions an
47
+ * `httpTransport` POSTs from the browser and forwards them to the app, passing its reply back.
48
+ * The signing secret never reaches the page, and the browser only talks to its own origin.
49
+ *
50
+ * Mount it anywhere that speaks the Fetch API: a Next.js route (`export const POST = relay`),
51
+ * `Bun.serve`, Hono, or a Vite dev-server middleware.
52
+ */
53
+ function createInteractionRelay(options) {
54
+ return async (request) => {
55
+ if (request.method !== "POST") return new Response("Method Not Allowed", {
56
+ status: 405,
57
+ headers: { allow: "POST" }
58
+ });
59
+ const body = await request.text();
60
+ if (!new URLSearchParams(body).has("payload")) return new Response("Expected a form-encoded `payload`", { status: 400 });
61
+ let res;
62
+ try {
63
+ res = await postSigned({
64
+ ...options,
65
+ body
66
+ });
67
+ } catch (cause) {
68
+ const reason = cause instanceof Error ? cause.message : String(cause);
69
+ return new Response(`Couldn't reach ${options.requestUrl}: ${reason}`, { status: 502 });
70
+ }
71
+ const headers = new Headers();
72
+ const type = res.headers.get("content-type");
73
+ if (type) headers.set("content-type", type);
74
+ return new Response(await res.text(), {
75
+ status: res.status,
76
+ headers
77
+ });
78
+ };
79
+ }
80
+ //#endregion
81
+ export { InteractionError, createInteractionRelay, encodeInteraction, httpTransport, postSigned, readInteractionResponse, sendInteraction, signSlackRequest };
82
+
83
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","names":[],"sources":["../src/interactivity/sign.ts","../src/interactivity/client.ts","../src/interactivity/relay.ts"],"sourcesContent":["import { createHmac } from \"node:crypto\";\n\n/**\n * Slack's request-signing scheme (v0): `v0=` + HMAC-SHA256(signing secret, `v0:{ts}:{body}`),\n * hex-encoded. See https://api.slack.com/authentication/verifying-requests-from-slack.\n */\nexport function signSlackRequest(signingSecret: string, timestamp: number, body: string): string {\n const hmac = createHmac(\"sha256\", signingSecret);\n hmac.update(`v0:${timestamp}:${body}`);\n return `v0=${hmac.digest(\"hex\")}`;\n}\n","import { signSlackRequest } from \"./sign\";\nimport { encodeInteraction, type InteractionResult, readInteractionResponse } from \"./transport\";\n\nexport interface SendInteractionOptions {\n /** The app's Slack request URL (an Events API / interactivity endpoint). */\n requestUrl: string;\n /** Same signing secret the receiving app was configured with. */\n signingSecret: string;\n /** A payload built with `@nkootstra/block-kit`'s payload builders. */\n payload: unknown;\n /** Defaults to the current time; override for signature tests. */\n timestamp?: number;\n /** Injectable for tests; defaults to the global `fetch`. */\n fetch?: typeof fetch;\n}\n\nexport type SendInteractionResult = InteractionResult;\n\n/**\n * POSTs a Slack-format interaction payload to an app's request URL exactly as Slack does:\n * `application/x-www-form-urlencoded` body `payload=<json>`, signed with `X-Slack-Signature` /\n * `X-Slack-Request-Timestamp`. If the app acks a `view_submission` with a `response_action`\n * (errors/update/push/clear), it is parsed and returned in `responseAction`.\n */\nexport async function sendInteraction(\n opts: SendInteractionOptions,\n): Promise<SendInteractionResult> {\n const res = await postSigned({ ...opts, body: encodeInteraction(opts.payload) });\n return readInteractionResponse(res);\n}\n\n/** POSTs an already-encoded body, signed with the app's secret, as Slack would. */\nexport function postSigned(opts: {\n requestUrl: string;\n signingSecret: string;\n body: string;\n timestamp?: number;\n fetch?: typeof fetch;\n}): Promise<Response> {\n const timestamp = opts.timestamp ?? Math.floor(Date.now() / 1000);\n const signature = signSlackRequest(opts.signingSecret, timestamp, opts.body);\n const doFetch = opts.fetch ?? fetch;\n return doFetch(opts.requestUrl, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n \"x-slack-signature\": signature,\n \"x-slack-request-timestamp\": String(timestamp),\n },\n body: opts.body,\n });\n}\n","import { postSigned } from \"./client\";\n\nexport interface InteractionRelayOptions {\n /** The app's Slack request URL (an Events API / interactivity endpoint). */\n requestUrl: string;\n /** Same signing secret the receiving app was configured with. Stays on the server. */\n signingSecret: string;\n /** Injectable for tests; defaults to the global `fetch`. */\n fetch?: typeof fetch;\n}\n\n/**\n * A server-side `(Request) => Response` handler that signs the unsigned interactions an\n * `httpTransport` POSTs from the browser and forwards them to the app, passing its reply back.\n * The signing secret never reaches the page, and the browser only talks to its own origin.\n *\n * Mount it anywhere that speaks the Fetch API: a Next.js route (`export const POST = relay`),\n * `Bun.serve`, Hono, or a Vite dev-server middleware.\n */\nexport function createInteractionRelay(\n options: InteractionRelayOptions,\n): (request: Request) => Promise<Response> {\n return async (request) => {\n if (request.method !== \"POST\") {\n return new Response(\"Method Not Allowed\", { status: 405, headers: { allow: \"POST\" } });\n }\n const body = await request.text();\n if (!new URLSearchParams(body).has(\"payload\")) {\n return new Response(\"Expected a form-encoded `payload`\", { status: 400 });\n }\n let res: Response;\n try {\n res = await postSigned({ ...options, body });\n } catch (cause) {\n const reason = cause instanceof Error ? cause.message : String(cause);\n return new Response(`Couldn't reach ${options.requestUrl}: ${reason}`, { status: 502 });\n }\n const headers = new Headers();\n const type = res.headers.get(\"content-type\");\n if (type) headers.set(\"content-type\", type);\n return new Response(await res.text(), { status: res.status, headers });\n };\n}\n"],"mappings":";;;;;;;;AAMA,SAAgB,iBAAiB,eAAuB,WAAmB,MAAsB;CAC/F,MAAM,OAAO,WAAW,UAAU,aAAa;CAC/C,KAAK,OAAO,MAAM,UAAU,GAAG,MAAM;CACrC,OAAO,MAAM,KAAK,OAAO,KAAK;AAChC;;;;;;;;;ACcA,eAAsB,gBACpB,MACgC;CAChC,MAAM,MAAM,MAAM,WAAW;EAAE,GAAG;EAAM,MAAM,kBAAkB,KAAK,OAAO;CAAE,CAAC;CAC/E,OAAO,wBAAwB,GAAG;AACpC;;AAGA,SAAgB,WAAW,MAML;CACpB,MAAM,YAAY,KAAK,aAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;CAChE,MAAM,YAAY,iBAAiB,KAAK,eAAe,WAAW,KAAK,IAAI;CAE3E,QADgB,KAAK,SAAS,MAAA,CACf,KAAK,YAAY;EAC9B,QAAQ;EACR,SAAS;GACP,gBAAgB;GAChB,qBAAqB;GACrB,6BAA6B,OAAO,SAAS;EAC/C;EACA,MAAM,KAAK;CACb,CAAC;AACH;;;;;;;;;;;AChCA,SAAgB,uBACd,SACyC;CACzC,OAAO,OAAO,YAAY;EACxB,IAAI,QAAQ,WAAW,QACrB,OAAO,IAAI,SAAS,sBAAsB;GAAE,QAAQ;GAAK,SAAS,EAAE,OAAO,OAAO;EAAE,CAAC;EAEvF,MAAM,OAAO,MAAM,QAAQ,KAAK;EAChC,IAAI,CAAC,IAAI,gBAAgB,IAAI,CAAC,CAAC,IAAI,SAAS,GAC1C,OAAO,IAAI,SAAS,qCAAqC,EAAE,QAAQ,IAAI,CAAC;EAE1E,IAAI;EACJ,IAAI;GACF,MAAM,MAAM,WAAW;IAAE,GAAG;IAAS;GAAK,CAAC;EAC7C,SAAS,OAAO;GACd,MAAM,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GACpE,OAAO,IAAI,SAAS,kBAAkB,QAAQ,WAAW,IAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;EACxF;EACA,MAAM,UAAU,IAAI,QAAQ;EAC5B,MAAM,OAAO,IAAI,QAAQ,IAAI,cAAc;EAC3C,IAAI,MAAM,QAAQ,IAAI,gBAAgB,IAAI;EAC1C,OAAO,IAAI,SAAS,MAAM,IAAI,KAAK,GAAG;GAAE,QAAQ,IAAI;GAAQ;EAAQ,CAAC;CACvE;AACF"}