@shurizma/discord-message-contract 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShuriZma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # @shurizma/discord-message-contract
2
+
3
+ The template engine shared by the [shuri.gg](https://shuri.gg) Discord Message Builder and the bots
4
+ that implement its compatibility contract.
5
+
6
+ The builder lets you compose a Discord [Components V2](https://discord.com/developers/docs/components/reference)
7
+ message in the browser and hand it to a bot to send. Templates let that message carry placeholders
8
+ (`{{guild.name}}`) that the bot fills in at send time, so a stored message can re-render itself
9
+ against fresh data.
10
+
11
+ That only works if the website's live preview and the bot's actual send agree on the grammar, down
12
+ to the character. This package is that agreement, extracted so there is exactly one implementation
13
+ instead of one per participant.
14
+
15
+ ## Install
16
+
17
+ ```
18
+ pnpm add @shurizma/discord-message-contract
19
+ ```
20
+
21
+ Zero runtime dependencies. Works in Node and in a browser bundle.
22
+
23
+ ## API
24
+
25
+ ### `renderTemplate(value, data, placeholders)`
26
+
27
+ Walks any JSON-shaped value and substitutes every template tag found in its strings. Arrays and
28
+ objects recurse; non-strings pass through untouched.
29
+
30
+ ```ts
31
+ import { renderTemplate } from '@shurizma/discord-message-contract';
32
+
33
+ const placeholders = [
34
+ { name: 'guild.name', description: 'Server name', kind: 'scalar' },
35
+ { name: 'roles', description: 'Server roles', kind: 'list', itemAlias: 'role' },
36
+ ];
37
+
38
+ const data = {
39
+ guild: { name: 'Skadi' },
40
+ roles: [{ name: 'Admin' }, { name: 'Mod' }],
41
+ };
42
+
43
+ renderTemplate(
44
+ { components: [{ type: 10, content: '# {{guild.name}}\n{{#foreach:roles:, }}{{role.name}}{{/foreach}}' }] },
45
+ data,
46
+ placeholders,
47
+ );
48
+ // { components: [{ type: 10, content: '# Skadi\nAdmin, Mod' }] }
49
+ ```
50
+
51
+ ### `applyConditionalButtons(components, rules, data)`
52
+
53
+ Removes buttons whose declared condition does not hold, and drops any action row left empty. A
54
+ button matches a rule when its `custom_id` starts with the rule's `customIdPrefix`. Buttons matching
55
+ no rule are always kept.
56
+
57
+ Link buttons with an empty `url` are always removed, because Discord rejects them outright.
58
+
59
+ ```ts
60
+ import { applyConditionalButtons } from '@shurizma/discord-message-contract';
61
+
62
+ const rules = [{ customIdPrefix: 'boost:', condition: 'guild.boostCount', negate: true }];
63
+
64
+ applyConditionalButtons(components, rules, { guild: { boostCount: 0 } });
65
+ // the boost: button survives, because boostCount is falsy and the rule is negated
66
+ ```
67
+
68
+ A bot must apply these rules on **every** render that reaches Discord, not only on scheduled
69
+ refreshes. If it does not, the website's preview and the message people actually see will disagree.
70
+
71
+ ### `resolveNowTimestamps(value)`
72
+
73
+ Replaces `<t:now>` and `<t:now:R>` with a real Unix timestamp, recursing the same way
74
+ `renderTemplate` does. Call it last, immediately before handing the payload to Discord, and keep the
75
+ literal `<t:now>` in storage so it re-resolves on every future send or edit rather than freezing to
76
+ whenever the message was composed.
77
+
78
+ Supported format suffixes are Discord's own: `t`, `T`, `d`, `D`, `f`, `F`, `R`. Anything else is
79
+ left alone.
80
+
81
+ ### Order of operations
82
+
83
+ ```ts
84
+ const rendered = renderTemplate(stored.payload, data, definition.placeholders);
85
+ const filtered = { ...rendered, components: applyConditionalButtons(rendered.components, definition.conditionalButtons, data) };
86
+ const payload = resolveNowTimestamps(filtered);
87
+ ```
88
+
89
+ Substitute, then filter, then resolve timestamps. Filtering before substitution would test
90
+ conditions against unrendered placeholder text; resolving timestamps early would freeze them.
91
+
92
+ ## Grammar
93
+
94
+ | Tag | Meaning |
95
+ | --- | --- |
96
+ | `{{path}}` | Insert a value. Dotted paths walk objects. A missing or null value renders as the empty string. |
97
+ | `{{#if:path}}` | Truthy test. An empty array is falsy; every other value follows JavaScript truthiness. |
98
+ | `{{#if:empty:path}}` | True when the value is `null`, `undefined`, `0` or `""`. |
99
+ | `{{#if:isset:path}}` | True when the value is neither `null` nor `undefined`. `0` and `""` count as set. |
100
+ | `{{#if:eq:path:value}}` | String comparison against a literal. `{{#if:eq:count:0}}` matches the number `0`. |
101
+ | `{{else}}` | Optional alternate branch. |
102
+ | `{{/if}}` | Closes the nearest open `if`. |
103
+ | `{{#foreach:list}}` | Iterate a list placeholder. |
104
+ | `{{#foreach:list:separator}}` | Same, joining iterations with the literal separator. `{{#foreach:roles:, }}` gives `Admin, Mod`. |
105
+ | `{{/foreach}}` | Closes the nearest open `foreach`. |
106
+
107
+ Inside a `foreach` body, the current item is bound to the list placeholder's `itemAlias`, so a
108
+ placeholder declared as `{ name: 'roles', kind: 'list', itemAlias: 'role' }` exposes `{{role.name}}`.
109
+ The enclosing scope stays visible, so `{{guild.name}}` still resolves inside the loop.
110
+
111
+ `if` blocks nest. `foreach` blocks do not: a `foreach` inside another `foreach` is emitted as
112
+ literal text rather than iterated. Iterating a list with no registered `itemAlias` throws.
113
+
114
+ Unrecognised tags pass through as literal text.
115
+
116
+ ### Known malformed-input behaviour
117
+
118
+ Malformed blocks fail quietly rather than raising:
119
+
120
+ - An unclosed `{{#if:x}}` renders its body when the condition holds.
121
+ - Text after an unmatched `{{/if}}` is discarded. `a{{/if}}b` renders as `a`.
122
+
123
+ These are frozen as-is because three live implementations already agreed on them. Treat them as
124
+ behaviour to preserve, not as intent.
125
+
126
+ ## Conforming in another language
127
+
128
+ `fixtures/grammar.json` is the language-agnostic definition of the grammar. Passing every fixture is
129
+ what conformance means. Each entry is:
130
+
131
+ ```json
132
+ {
133
+ "name": "foreach-separator",
134
+ "template": "{{#foreach:roles:, }}{{role.name}}{{/foreach}}",
135
+ "placeholders": [{ "name": "roles", "description": "roles", "kind": "list", "itemAlias": "role" }],
136
+ "data": { "roles": [{ "name": "Admin" }, { "name": "Mod" }] },
137
+ "expected": "Admin, Mod"
138
+ }
139
+ ```
140
+
141
+ `template` is any JSON value, not only a string, since substitution recurses through objects and
142
+ arrays. An entry carrying `throws` instead of `expected` must raise an error containing that text.
143
+
144
+ The fixtures were generated by running a shared corpus through the two implementations that existed
145
+ before this package (the shuri.gg frontend and the dallas bot) and recording only the cases where
146
+ both agreed, so they describe behaviour that was already in production rather than a fresh
147
+ specification.
148
+
149
+ ## License
150
+
151
+ MIT
@@ -0,0 +1,3 @@
1
+ import type { ConditionalButtonRule } from './types.js';
2
+ export declare function applyConditionalButtons(components: unknown, rules: ConditionalButtonRule[], data: Record<string, unknown>): unknown;
3
+ //# sourceMappingURL=conditional-buttons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conditional-buttons.d.ts","sourceRoot":"","sources":["../src/conditional-buttons.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AA8CxD,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,qBAAqB,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAUnI"}
@@ -0,0 +1,55 @@
1
+ const ACTION_ROW = 1;
2
+ const CONTAINER = 17;
3
+ function resolvePath(data, path) {
4
+ return path.split('.').reduce((value, key) => {
5
+ if (value === null || value === undefined)
6
+ return undefined;
7
+ return value[key];
8
+ }, data);
9
+ }
10
+ function isTruthy(value) {
11
+ if (Array.isArray(value))
12
+ return value.length > 0;
13
+ if (typeof value === 'boolean')
14
+ return value;
15
+ return Boolean(value);
16
+ }
17
+ function isEmptyLinkButton(button) {
18
+ return typeof button.url === 'string' && button.custom_id === undefined && button.url === '';
19
+ }
20
+ function keepButton(button, rules, data) {
21
+ if (isEmptyLinkButton(button))
22
+ return false;
23
+ const customId = typeof button.custom_id === 'string' ? button.custom_id : undefined;
24
+ const rule = customId !== undefined ? rules.find((r) => customId.startsWith(r.customIdPrefix)) : undefined;
25
+ if (!rule)
26
+ return true;
27
+ const raw = isTruthy(resolvePath(data, rule.condition));
28
+ return rule.negate ? !raw : raw;
29
+ }
30
+ function filterContainerChildren(children, rules, data) {
31
+ const result = [];
32
+ for (const child of children) {
33
+ const row = child;
34
+ if (row?.type === ACTION_ROW && Array.isArray(row.components)) {
35
+ const kept = row.components.filter((b) => keepButton(b, rules, data));
36
+ if (kept.length > 0)
37
+ result.push({ ...row, components: kept });
38
+ continue;
39
+ }
40
+ result.push(child);
41
+ }
42
+ return result;
43
+ }
44
+ export function applyConditionalButtons(components, rules, data) {
45
+ if (!Array.isArray(components))
46
+ return components;
47
+ return components.map((component) => {
48
+ const container = component;
49
+ if (container?.type === CONTAINER && Array.isArray(container.components)) {
50
+ return { ...container, components: filterContainerChildren(container.components, rules, data) };
51
+ }
52
+ return component;
53
+ });
54
+ }
55
+ //# sourceMappingURL=conditional-buttons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conditional-buttons.js","sourceRoot":"","sources":["../src/conditional-buttons.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { TemplatePlaceholder } from './types.js';
2
+ export declare function renderTemplate(value: unknown, data: Record<string, unknown>, placeholders: TemplatePlaceholder[]): unknown;
3
+ //# sourceMappingURL=grammar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grammar.d.ts","sourceRoot":"","sources":["../src/grammar.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAsNtD,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,EAAE,mBAAmB,EAAE,GAAG,OAAO,CAe1H"}
@@ -0,0 +1,219 @@
1
+ const TAG_PATTERN = /\{\{(?:#if:(?:(empty|isset):([\w.]+)|eq:([\w.]+):([^}]*)|([\w.]+))|(else)|(\/if)|#foreach:([\w.]+)(?::([^}]*))?|(\/foreach)|([\w.]+))\}\}/g;
2
+ function tokenize(template) {
3
+ const tokens = [];
4
+ let lastIndex = 0;
5
+ let match;
6
+ TAG_PATTERN.lastIndex = 0;
7
+ while ((match = TAG_PATTERN.exec(template)) !== null) {
8
+ if (match.index > lastIndex) {
9
+ tokens.push({ kind: 'text', value: template.slice(lastIndex, match.index) });
10
+ }
11
+ const [, ifModeRaw, ifPath, eqPath, eqValue, ifPathPlain, elseTag, endifTag, foreachName, foreachSeparator, endforeachTag, scalarPath] = match;
12
+ if (ifModeRaw !== undefined) {
13
+ tokens.push({ kind: 'if', mode: ifModeRaw, path: ifPath });
14
+ }
15
+ else if (eqPath !== undefined) {
16
+ tokens.push({ kind: 'if', mode: 'eq', path: eqPath, value: eqValue });
17
+ }
18
+ else if (ifPathPlain !== undefined) {
19
+ tokens.push({ kind: 'if', mode: 'plain', path: ifPathPlain });
20
+ }
21
+ else if (elseTag !== undefined) {
22
+ tokens.push({ kind: 'else' });
23
+ }
24
+ else if (endifTag !== undefined) {
25
+ tokens.push({ kind: 'endif' });
26
+ }
27
+ else if (foreachName !== undefined) {
28
+ tokens.push({ kind: 'foreach', name: foreachName, separator: foreachSeparator });
29
+ }
30
+ else if (endforeachTag !== undefined) {
31
+ tokens.push({ kind: 'endforeach' });
32
+ }
33
+ else if (scalarPath !== undefined) {
34
+ tokens.push({ kind: 'scalar', path: scalarPath });
35
+ }
36
+ lastIndex = TAG_PATTERN.lastIndex;
37
+ }
38
+ if (lastIndex < template.length) {
39
+ tokens.push({ kind: 'text', value: template.slice(lastIndex) });
40
+ }
41
+ return tokens;
42
+ }
43
+ function tokenToLiteral(token) {
44
+ switch (token.kind) {
45
+ case 'text':
46
+ return token.value;
47
+ case 'scalar':
48
+ return `{{${token.path}}}`;
49
+ case 'if':
50
+ return token.mode === 'eq'
51
+ ? `{{#if:eq:${token.path}:${token.value}}}`
52
+ : `{{#if:${token.mode === 'plain' ? '' : `${token.mode}:`}${token.path}}}`;
53
+ case 'else':
54
+ return '{{else}}';
55
+ case 'endif':
56
+ return '{{/if}}';
57
+ case 'foreach':
58
+ return `{{#foreach:${token.name}${token.separator !== undefined ? `:${token.separator}` : ''}}}`;
59
+ case 'endforeach':
60
+ return '{{/foreach}}';
61
+ }
62
+ }
63
+ function consumeNestedForeachAsLiteral(tokens, pos, openToken) {
64
+ let buffer = tokenToLiteral(openToken);
65
+ let depth = 1;
66
+ while (pos.i < tokens.length && depth > 0) {
67
+ const token = tokens[pos.i++];
68
+ buffer += tokenToLiteral(token);
69
+ if (token.kind === 'foreach')
70
+ depth++;
71
+ if (token.kind === 'endforeach')
72
+ depth--;
73
+ }
74
+ return buffer;
75
+ }
76
+ function parseNodes(tokens, pos, insideForeach) {
77
+ const nodes = [];
78
+ while (pos.i < tokens.length) {
79
+ const token = tokens[pos.i];
80
+ if (token.kind === 'endif' || token.kind === 'else' || token.kind === 'endforeach')
81
+ break;
82
+ pos.i++;
83
+ if (token.kind === 'text') {
84
+ nodes.push({ type: 'text', value: token.value });
85
+ }
86
+ else if (token.kind === 'scalar') {
87
+ nodes.push({ type: 'scalar', path: token.path });
88
+ }
89
+ else if (token.kind === 'if') {
90
+ const thenNodes = parseNodes(tokens, pos, insideForeach);
91
+ let elseNodes = [];
92
+ if (tokens[pos.i]?.kind === 'else') {
93
+ pos.i++;
94
+ elseNodes = parseNodes(tokens, pos, insideForeach);
95
+ }
96
+ if (tokens[pos.i]?.kind === 'endif')
97
+ pos.i++;
98
+ nodes.push({ type: 'if', mode: token.mode, path: token.path, value: token.value, thenNodes, elseNodes });
99
+ }
100
+ else if (token.kind === 'foreach') {
101
+ if (insideForeach) {
102
+ nodes.push({ type: 'text', value: consumeNestedForeachAsLiteral(tokens, pos, token) });
103
+ continue;
104
+ }
105
+ const body = parseNodes(tokens, pos, true);
106
+ if (tokens[pos.i]?.kind === 'endforeach')
107
+ pos.i++;
108
+ nodes.push({ type: 'foreach', name: token.name, separator: token.separator, body });
109
+ }
110
+ }
111
+ return nodes;
112
+ }
113
+ function parseTemplate(template) {
114
+ return parseNodes(tokenize(template), { i: 0 }, false);
115
+ }
116
+ class Scope {
117
+ bindings;
118
+ parent;
119
+ constructor(bindings, parent) {
120
+ this.bindings = bindings;
121
+ this.parent = parent;
122
+ }
123
+ resolve(path) {
124
+ const [head, ...rest] = path.split('.');
125
+ let value;
126
+ if (Object.prototype.hasOwnProperty.call(this.bindings, head)) {
127
+ value = this.bindings[head];
128
+ }
129
+ else if (this.parent) {
130
+ return this.parent.resolve(path);
131
+ }
132
+ else {
133
+ return undefined;
134
+ }
135
+ for (const key of rest) {
136
+ if (value === null || value === undefined)
137
+ return undefined;
138
+ value = value[key];
139
+ }
140
+ return value;
141
+ }
142
+ }
143
+ function isTruthy(value) {
144
+ if (Array.isArray(value))
145
+ return value.length > 0;
146
+ if (typeof value === 'boolean')
147
+ return value;
148
+ return Boolean(value);
149
+ }
150
+ function isEmpty(value) {
151
+ return value === null || value === undefined || value === 0 || value === '';
152
+ }
153
+ function isSet(value) {
154
+ return value !== null && value !== undefined;
155
+ }
156
+ function renderNodes(nodes, scope, itemAliasByListName) {
157
+ let out = '';
158
+ for (const node of nodes) {
159
+ if (node.type === 'text') {
160
+ out += node.value;
161
+ }
162
+ else if (node.type === 'scalar') {
163
+ const value = scope.resolve(node.path);
164
+ out += value === null || value === undefined ? '' : String(value);
165
+ }
166
+ else if (node.type === 'if') {
167
+ const value = scope.resolve(node.path);
168
+ const condition = node.mode === 'empty'
169
+ ? isEmpty(value)
170
+ : node.mode === 'isset'
171
+ ? isSet(value)
172
+ : node.mode === 'eq'
173
+ ? String(value) === node.value
174
+ : isTruthy(value);
175
+ out += renderNodes(condition ? node.thenNodes : node.elseNodes, scope, itemAliasByListName);
176
+ }
177
+ else if (node.type === 'foreach') {
178
+ const list = scope.resolve(node.name);
179
+ if (Array.isArray(list)) {
180
+ const itemAlias = itemAliasByListName.get(node.name);
181
+ if (!itemAlias) {
182
+ throw new Error(`Template references {{#foreach:${node.name}}} but no list placeholder "${node.name}" with an itemAlias is registered`);
183
+ }
184
+ const rendered = list.map((item) => {
185
+ const childScope = new Scope({ [itemAlias]: item }, scope);
186
+ return renderNodes(node.body, childScope, itemAliasByListName);
187
+ });
188
+ out += rendered.join(node.separator ?? '');
189
+ }
190
+ }
191
+ }
192
+ return out;
193
+ }
194
+ function renderTemplateString(template, data, placeholders) {
195
+ const itemAliasByListName = new Map();
196
+ for (const def of placeholders) {
197
+ if (def.kind === 'list' && def.itemAlias)
198
+ itemAliasByListName.set(def.name, def.itemAlias);
199
+ }
200
+ const nodes = parseTemplate(template);
201
+ return renderNodes(nodes, new Scope(data), itemAliasByListName);
202
+ }
203
+ export function renderTemplate(value, data, placeholders) {
204
+ if (typeof value === 'string') {
205
+ return renderTemplateString(value, data, placeholders);
206
+ }
207
+ if (Array.isArray(value)) {
208
+ return value.map((v) => renderTemplate(v, data, placeholders));
209
+ }
210
+ if (value !== null && typeof value === 'object') {
211
+ const result = {};
212
+ for (const [key, v] of Object.entries(value)) {
213
+ result[key] = renderTemplate(v, data, placeholders);
214
+ }
215
+ return result;
216
+ }
217
+ return value;
218
+ }
219
+ //# sourceMappingURL=grammar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grammar.js","sourceRoot":"","sources":["../src/grammar.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,GACb,4IAA4I,CAAC;AAEjJ,SAAS,QAAQ,CAAC,QAAgB;IAC9B,MAAM,MAAM,GAAY,EAAE,CAAC;IAC3B,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,KAA6B,CAAC;IAClC,WAAW,CAAC,SAAS,GAAG,CAAC,CAAC;IAC1B,OAAO,CAAC,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACnD,IAAI,KAAK,CAAC,KAAK,GAAG,SAAS,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACjF,CAAC;QACD,MAAM,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,gBAAgB,EAAE,aAAa,EAAE,UAAU,CAAC,GAClI,KAAK,CAAC;QACV,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAmB,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QACzE,CAAC;aAAM,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAC9B,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;QAC1E,CAAC;aAAM,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YACnC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;QAClE,CAAC;aAAM,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QAClC,CAAC;aAAM,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAChC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;QACnC,CAAC;aAAM,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YACnC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,gBAAgB,EAAE,CAAC,CAAC;QACrF,CAAC;aAAM,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;YACrC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;QACxC,CAAC;aAAM,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAClC,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;QACtD,CAAC;QACD,SAAS,GAAG,WAAW,CAAC,SAAS,CAAC;IACtC,CAAC;IACD,IAAI,SAAS,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACpE,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,SAAS,cAAc,CAAC,KAAY;IAChC,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;QACjB,KAAK,MAAM;YACP,OAAO,KAAK,CAAC,KAAK,CAAC;QACvB,KAAK,QAAQ;YACT,OAAO,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC;QAC/B,KAAK,IAAI;YACL,OAAO,KAAK,CAAC,IAAI,KAAK,IAAI;gBACtB,CAAC,CAAC,YAAY,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,KAAK,IAAI;gBAC3C,CAAC,CAAC,SAAS,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,GAAG,GAAG,KAAK,CAAC,IAAI,IAAI,CAAC;QACnF,KAAK,MAAM;YACP,OAAO,UAAU,CAAC;QACtB,KAAK,OAAO;YACR,OAAO,SAAS,CAAC;QACrB,KAAK,SAAS;YACV,OAAO,cAAc,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC;QACrG,KAAK,YAAY;YACb,OAAO,cAAc,CAAC;IAC9B,CAAC;AACL,CAAC;AAED,SAAS,6BAA6B,CAAC,MAAe,EAAE,GAAkB,EAAE,SAA8C;IACtH,IAAI,MAAM,GAAG,cAAc,CAAC,SAAS,CAAC,CAAC;IACvC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,OAAO,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;QAC9B,MAAM,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS;YAAE,KAAK,EAAE,CAAC;QACtC,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY;YAAE,KAAK,EAAE,CAAC;IAC7C,CAAC;IACD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED,SAAS,UAAU,CAAC,MAAe,EAAE,GAAkB,EAAE,aAAsB;IAC3E,MAAM,KAAK,GAAW,EAAE,CAAC;IACzB,OAAO,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY;YAAE,MAAM;QAC1F,GAAG,CAAC,CAAC,EAAE,CAAC;QAER,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;QACrD,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACjC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QACrD,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;YAC7B,MAAM,SAAS,GAAG,UAAU,CAAC,MAAM,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC;YACzD,IAAI,SAAS,GAAW,EAAE,CAAC;YAC3B,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,MAAM,EAAE,CAAC;gBACjC,GAAG,CAAC,CAAC,EAAE,CAAC;gBACR,SAAS,GAAG,UAAU,CAAC,MAAM,EAAE,GAAG,EAAE,aAAa,CAAC,CAAC;YACvD,CAAC;YACD,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,OAAO;gBAAE,GAAG,CAAC,CAAC,EAAE,CAAC;YAC7C,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC;QAC7G,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAClC,IAAI,aAAa,EAAE,CAAC;gBAChB,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,6BAA6B,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;gBACvF,SAAS;YACb,CAAC;YACD,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;YAC3C,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,YAAY;gBAAE,GAAG,CAAC,CAAC,EAAE,CAAC;YAClD,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACxF,CAAC;IACL,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC;AAED,SAAS,aAAa,CAAC,QAAgB;IACnC,OAAO,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;AAC3D,CAAC;AAED,MAAM,KAAK;IAEK;IACA;IAFZ,YACY,QAAiC,EACjC,MAAc;QADd,aAAQ,GAAR,QAAQ,CAAyB;QACjC,WAAM,GAAN,MAAM,CAAQ;IACvB,CAAC;IAEJ,OAAO,CAAC,IAAY;QAChB,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,KAAc,CAAC;QACnB,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC;YAC5D,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACrB,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACrC,CAAC;aAAM,CAAC;YACJ,OAAO,SAAS,CAAC;QACrB,CAAC;QACD,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACrB,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO,SAAS,CAAC;YAC5D,KAAK,GAAI,KAAiC,CAAC,GAAG,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;CACJ;AAED,SAAS,QAAQ,CAAC,KAAc;IAC5B,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;IAClD,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IAC7C,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,OAAO,CAAC,KAAc;IAC3B,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;AAChF,CAAC;AAED,SAAS,KAAK,CAAC,KAAc;IACzB,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC;AACjD,CAAC;AAED,SAAS,WAAW,CAAC,KAAa,EAAE,KAAY,EAAE,mBAAwC;IACtF,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACvB,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC;QACtB,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAChC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvC,GAAG,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;YAC5B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACvC,MAAM,SAAS,GACX,IAAI,CAAC,IAAI,KAAK,OAAO;gBACjB,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;gBAChB,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,OAAO;oBACrB,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC;oBACd,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI;wBAClB,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,KAAK;wBAC9B,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;YAC9B,GAAG,IAAI,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,mBAAmB,CAAC,CAAC;QAChG,CAAC;aAAM,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YACjC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACtC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBACtB,MAAM,SAAS,GAAG,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACrD,IAAI,CAAC,SAAS,EAAE,CAAC;oBACb,MAAM,IAAI,KAAK,CACX,kCAAkC,IAAI,CAAC,IAAI,+BAA+B,IAAI,CAAC,IAAI,mCAAmC,CACzH,CAAC;gBACN,CAAC;gBACD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;oBAC/B,MAAM,UAAU,GAAG,IAAI,KAAK,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,EAAE,KAAK,CAAC,CAAC;oBAC3D,OAAO,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,mBAAmB,CAAC,CAAC;gBACnE,CAAC,CAAC,CAAC;gBACH,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;YAC/C,CAAC;QACL,CAAC;IACL,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC;AAED,SAAS,oBAAoB,CAAC,QAAgB,EAAE,IAA6B,EAAE,YAAmC;IAC9G,MAAM,mBAAmB,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtD,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC7B,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,IAAI,GAAG,CAAC,SAAS;YAAE,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;IAC/F,CAAC;IACD,MAAM,KAAK,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACtC,OAAO,WAAW,CAAC,KAAK,EAAE,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,mBAAmB,CAAC,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAc,EAAE,IAA6B,EAAE,YAAmC;IAC7G,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC5B,OAAO,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;IAC3D,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC;IACnE,CAAC;IACD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9C,MAAM,MAAM,GAA4B,EAAE,CAAC;QAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3C,MAAM,CAAC,GAAG,CAAC,GAAG,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;QACxD,CAAC;QACD,OAAO,MAAM,CAAC;IAClB,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { applyConditionalButtons } from './conditional-buttons.js';
2
+ export { renderTemplate } from './grammar.js';
3
+ export { resolveNowTimestamps } from './timestamps.js';
4
+ export type { ConditionalButtonRule, TemplateDefinition, TemplatePlaceholder, TemplatePlaceholderKind, } from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,YAAY,EACR,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,EACnB,uBAAuB,GAC1B,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { applyConditionalButtons } from './conditional-buttons.js';
2
+ export { renderTemplate } from './grammar.js';
3
+ export { resolveNowTimestamps } from './timestamps.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare function resolveNowTimestamps(value: unknown): unknown;
2
+ //# sourceMappingURL=timestamps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timestamps.d.ts","sourceRoot":"","sources":["../src/timestamps.ts"],"names":[],"mappings":"AAEA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAc5D"}
@@ -0,0 +1,18 @@
1
+ const NOW_TIMESTAMP = /<t:now(?::([tTdDfFR]))?>/g;
2
+ export function resolveNowTimestamps(value) {
3
+ if (typeof value === 'string') {
4
+ const nowUnix = Math.floor(Date.now() / 1000);
5
+ return value.replace(NOW_TIMESTAMP, (_match, format) => (format ? `<t:${nowUnix}:${format}>` : `<t:${nowUnix}>`));
6
+ }
7
+ if (Array.isArray(value))
8
+ return value.map(resolveNowTimestamps);
9
+ if (value !== null && typeof value === 'object') {
10
+ const cleaned = {};
11
+ for (const [key, v] of Object.entries(value)) {
12
+ cleaned[key] = resolveNowTimestamps(v);
13
+ }
14
+ return cleaned;
15
+ }
16
+ return value;
17
+ }
18
+ //# sourceMappingURL=timestamps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timestamps.js","sourceRoot":"","sources":["../src/timestamps.ts"],"names":[],"mappings":"AAAA,MAAM,aAAa,GAAG,2BAA2B,CAAC;AAElD,MAAM,UAAU,oBAAoB,CAAC,KAAc;IAC/C,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAC9C,OAAO,KAAK,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,OAAO,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC;IACtH,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IACjE,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9C,MAAM,OAAO,GAA4B,EAAE,CAAC;QAC5C,KAAK,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3C,OAAO,CAAC,GAAG,CAAC,GAAG,oBAAoB,CAAC,CAAC,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,OAAO,CAAC;IACnB,CAAC;IACD,OAAO,KAAK,CAAC;AACjB,CAAC"}
@@ -0,0 +1,24 @@
1
+ export type TemplatePlaceholderKind = 'scalar' | 'list';
2
+ export interface TemplatePlaceholder {
3
+ name: string;
4
+ description: string;
5
+ kind: TemplatePlaceholderKind;
6
+ itemAlias?: string;
7
+ example?: unknown;
8
+ current?: unknown;
9
+ }
10
+ export interface ConditionalButtonRule {
11
+ customIdPrefix: string;
12
+ condition: string;
13
+ negate?: boolean;
14
+ }
15
+ export interface TemplateDefinition {
16
+ templateId: string;
17
+ description: string;
18
+ needsChannel: boolean;
19
+ autoRefreshesLive: boolean;
20
+ supportsGamePreview: boolean;
21
+ conditionalButtons: ConditionalButtonRule[];
22
+ placeholders: TemplatePlaceholder[];
23
+ }
24
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,uBAAuB,GAAG,QAAQ,GAAG,MAAM,CAAC;AAExD,MAAM,WAAW,mBAAmB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,uBAAuB,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,kBAAkB,EAAE,qBAAqB,EAAE,CAAC;IAC5C,YAAY,EAAE,mBAAmB,EAAE,CAAC;CACvC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,648 @@
1
+ [
2
+ {
3
+ "name": "plain-scalar",
4
+ "template": "Hello {{name}}",
5
+ "placeholders": [
6
+ {
7
+ "name": "name",
8
+ "description": "name",
9
+ "kind": "scalar"
10
+ }
11
+ ],
12
+ "data": {
13
+ "name": "World"
14
+ },
15
+ "expected": "Hello World"
16
+ },
17
+ {
18
+ "name": "dotted-path",
19
+ "template": "{{guild.name}}",
20
+ "placeholders": [
21
+ {
22
+ "name": "guild.name",
23
+ "description": "guild.name",
24
+ "kind": "scalar"
25
+ }
26
+ ],
27
+ "data": {
28
+ "guild": {
29
+ "name": "Skadi"
30
+ }
31
+ },
32
+ "expected": "Skadi"
33
+ },
34
+ {
35
+ "name": "missing-path",
36
+ "template": "[{{nope.deep.deeper}}]",
37
+ "placeholders": [
38
+ {
39
+ "name": "nope",
40
+ "description": "nope",
41
+ "kind": "scalar"
42
+ }
43
+ ],
44
+ "data": {},
45
+ "expected": "[]"
46
+ },
47
+ {
48
+ "name": "scalar-null",
49
+ "template": "[{{v}}]",
50
+ "placeholders": [
51
+ {
52
+ "name": "v",
53
+ "description": "v",
54
+ "kind": "scalar"
55
+ }
56
+ ],
57
+ "data": {
58
+ "v": null
59
+ },
60
+ "expected": "[]"
61
+ },
62
+ {
63
+ "name": "scalar-zero",
64
+ "template": "[{{v}}]",
65
+ "placeholders": [
66
+ {
67
+ "name": "v",
68
+ "description": "v",
69
+ "kind": "scalar"
70
+ }
71
+ ],
72
+ "data": {
73
+ "v": 0
74
+ },
75
+ "expected": "[0]"
76
+ },
77
+ {
78
+ "name": "scalar-false",
79
+ "template": "[{{v}}]",
80
+ "placeholders": [
81
+ {
82
+ "name": "v",
83
+ "description": "v",
84
+ "kind": "scalar"
85
+ }
86
+ ],
87
+ "data": {
88
+ "v": false
89
+ },
90
+ "expected": "[false]"
91
+ },
92
+ {
93
+ "name": "scalar-array",
94
+ "template": "[{{v}}]",
95
+ "placeholders": [
96
+ {
97
+ "name": "v",
98
+ "description": "v",
99
+ "kind": "scalar"
100
+ }
101
+ ],
102
+ "data": {
103
+ "v": [
104
+ 1,
105
+ 2
106
+ ]
107
+ },
108
+ "expected": "[1,2]"
109
+ },
110
+ {
111
+ "name": "if-plain-true",
112
+ "template": "{{#if:v}}yes{{/if}}",
113
+ "placeholders": [
114
+ {
115
+ "name": "v",
116
+ "description": "v",
117
+ "kind": "scalar"
118
+ }
119
+ ],
120
+ "data": {
121
+ "v": "x"
122
+ },
123
+ "expected": "yes"
124
+ },
125
+ {
126
+ "name": "if-plain-false",
127
+ "template": "{{#if:v}}yes{{/if}}",
128
+ "placeholders": [
129
+ {
130
+ "name": "v",
131
+ "description": "v",
132
+ "kind": "scalar"
133
+ }
134
+ ],
135
+ "data": {
136
+ "v": ""
137
+ },
138
+ "expected": ""
139
+ },
140
+ {
141
+ "name": "if-plain-empty-array",
142
+ "template": "{{#if:v}}yes{{/if}}",
143
+ "placeholders": [
144
+ {
145
+ "name": "v",
146
+ "description": "v",
147
+ "kind": "scalar"
148
+ }
149
+ ],
150
+ "data": {
151
+ "v": []
152
+ },
153
+ "expected": ""
154
+ },
155
+ {
156
+ "name": "if-plain-nonempty-array",
157
+ "template": "{{#if:v}}yes{{/if}}",
158
+ "placeholders": [
159
+ {
160
+ "name": "v",
161
+ "description": "v",
162
+ "kind": "scalar"
163
+ }
164
+ ],
165
+ "data": {
166
+ "v": [
167
+ 1
168
+ ]
169
+ },
170
+ "expected": "yes"
171
+ },
172
+ {
173
+ "name": "if-empty-true",
174
+ "template": "{{#if:empty:v}}empty{{/if}}",
175
+ "placeholders": [
176
+ {
177
+ "name": "v",
178
+ "description": "v",
179
+ "kind": "scalar"
180
+ }
181
+ ],
182
+ "data": {
183
+ "v": 0
184
+ },
185
+ "expected": "empty"
186
+ },
187
+ {
188
+ "name": "if-empty-false",
189
+ "template": "{{#if:empty:v}}empty{{/if}}",
190
+ "placeholders": [
191
+ {
192
+ "name": "v",
193
+ "description": "v",
194
+ "kind": "scalar"
195
+ }
196
+ ],
197
+ "data": {
198
+ "v": "x"
199
+ },
200
+ "expected": ""
201
+ },
202
+ {
203
+ "name": "if-empty-undefined",
204
+ "template": "{{#if:empty:v}}empty{{/if}}",
205
+ "placeholders": [
206
+ {
207
+ "name": "v",
208
+ "description": "v",
209
+ "kind": "scalar"
210
+ }
211
+ ],
212
+ "data": {},
213
+ "expected": "empty"
214
+ },
215
+ {
216
+ "name": "if-isset-true",
217
+ "template": "{{#if:isset:v}}set{{/if}}",
218
+ "placeholders": [
219
+ {
220
+ "name": "v",
221
+ "description": "v",
222
+ "kind": "scalar"
223
+ }
224
+ ],
225
+ "data": {
226
+ "v": 0
227
+ },
228
+ "expected": "set"
229
+ },
230
+ {
231
+ "name": "if-isset-false",
232
+ "template": "{{#if:isset:v}}set{{/if}}",
233
+ "placeholders": [
234
+ {
235
+ "name": "v",
236
+ "description": "v",
237
+ "kind": "scalar"
238
+ }
239
+ ],
240
+ "data": {
241
+ "v": null
242
+ },
243
+ "expected": ""
244
+ },
245
+ {
246
+ "name": "if-eq-true",
247
+ "template": "{{#if:eq:v:42}}match{{/if}}",
248
+ "placeholders": [
249
+ {
250
+ "name": "v",
251
+ "description": "v",
252
+ "kind": "scalar"
253
+ }
254
+ ],
255
+ "data": {
256
+ "v": 42
257
+ },
258
+ "expected": "match"
259
+ },
260
+ {
261
+ "name": "if-eq-false",
262
+ "template": "{{#if:eq:v:42}}match{{/if}}",
263
+ "placeholders": [
264
+ {
265
+ "name": "v",
266
+ "description": "v",
267
+ "kind": "scalar"
268
+ }
269
+ ],
270
+ "data": {
271
+ "v": 7
272
+ },
273
+ "expected": ""
274
+ },
275
+ {
276
+ "name": "if-eq-empty-value",
277
+ "template": "{{#if:eq:v:}}blank{{/if}}",
278
+ "placeholders": [
279
+ {
280
+ "name": "v",
281
+ "description": "v",
282
+ "kind": "scalar"
283
+ }
284
+ ],
285
+ "data": {
286
+ "v": ""
287
+ },
288
+ "expected": "blank"
289
+ },
290
+ {
291
+ "name": "if-else-then",
292
+ "template": "{{#if:v}}A{{else}}B{{/if}}",
293
+ "placeholders": [
294
+ {
295
+ "name": "v",
296
+ "description": "v",
297
+ "kind": "scalar"
298
+ }
299
+ ],
300
+ "data": {
301
+ "v": true
302
+ },
303
+ "expected": "A"
304
+ },
305
+ {
306
+ "name": "if-else-otherwise",
307
+ "template": "{{#if:v}}A{{else}}B{{/if}}",
308
+ "placeholders": [
309
+ {
310
+ "name": "v",
311
+ "description": "v",
312
+ "kind": "scalar"
313
+ }
314
+ ],
315
+ "data": {
316
+ "v": false
317
+ },
318
+ "expected": "B"
319
+ },
320
+ {
321
+ "name": "if-nested",
322
+ "template": "{{#if:a}}[{{#if:b}}both{{else}}only-a{{/if}}]{{else}}none{{/if}}",
323
+ "placeholders": [
324
+ {
325
+ "name": "a",
326
+ "description": "a",
327
+ "kind": "scalar"
328
+ },
329
+ {
330
+ "name": "b",
331
+ "description": "b",
332
+ "kind": "scalar"
333
+ }
334
+ ],
335
+ "data": {
336
+ "a": true,
337
+ "b": false
338
+ },
339
+ "expected": "[only-a]"
340
+ },
341
+ {
342
+ "name": "foreach-no-separator",
343
+ "template": "{{#foreach:roles}}{{role.name}}{{/foreach}}",
344
+ "placeholders": [
345
+ {
346
+ "name": "roles",
347
+ "description": "roles",
348
+ "kind": "list",
349
+ "itemAlias": "role"
350
+ }
351
+ ],
352
+ "data": {
353
+ "roles": [
354
+ {
355
+ "name": "Admin",
356
+ "id": "1",
357
+ "memberCount": 2
358
+ },
359
+ {
360
+ "name": "Mod",
361
+ "id": "2",
362
+ "memberCount": 5
363
+ },
364
+ {
365
+ "name": "Member",
366
+ "id": "3",
367
+ "memberCount": 40
368
+ }
369
+ ]
370
+ },
371
+ "expected": "AdminModMember"
372
+ },
373
+ {
374
+ "name": "foreach-separator",
375
+ "template": "{{#foreach:roles:, }}{{role.name}}{{/foreach}}",
376
+ "placeholders": [
377
+ {
378
+ "name": "roles",
379
+ "description": "roles",
380
+ "kind": "list",
381
+ "itemAlias": "role"
382
+ }
383
+ ],
384
+ "data": {
385
+ "roles": [
386
+ {
387
+ "name": "Admin",
388
+ "id": "1",
389
+ "memberCount": 2
390
+ },
391
+ {
392
+ "name": "Mod",
393
+ "id": "2",
394
+ "memberCount": 5
395
+ },
396
+ {
397
+ "name": "Member",
398
+ "id": "3",
399
+ "memberCount": 40
400
+ }
401
+ ]
402
+ },
403
+ "expected": "Admin, Mod, Member"
404
+ },
405
+ {
406
+ "name": "foreach-empty-list",
407
+ "template": "[{{#foreach:roles:, }}{{role.name}}{{/foreach}}]",
408
+ "placeholders": [
409
+ {
410
+ "name": "roles",
411
+ "description": "roles",
412
+ "kind": "list",
413
+ "itemAlias": "role"
414
+ }
415
+ ],
416
+ "data": {
417
+ "roles": []
418
+ },
419
+ "expected": "[]"
420
+ },
421
+ {
422
+ "name": "foreach-missing-list",
423
+ "template": "[{{#foreach:roles}}{{role.name}}{{/foreach}}]",
424
+ "placeholders": [
425
+ {
426
+ "name": "roles",
427
+ "description": "roles",
428
+ "kind": "list",
429
+ "itemAlias": "role"
430
+ }
431
+ ],
432
+ "data": {},
433
+ "expected": "[]"
434
+ },
435
+ {
436
+ "name": "foreach-outer-scope-access",
437
+ "template": "{{#foreach:roles:, }}{{guild.name}}/{{role.name}}{{/foreach}}",
438
+ "placeholders": [
439
+ {
440
+ "name": "roles",
441
+ "description": "roles",
442
+ "kind": "list",
443
+ "itemAlias": "role"
444
+ },
445
+ {
446
+ "name": "guild.name",
447
+ "description": "guild.name",
448
+ "kind": "scalar"
449
+ }
450
+ ],
451
+ "data": {
452
+ "roles": [
453
+ {
454
+ "name": "Admin",
455
+ "id": "1",
456
+ "memberCount": 2
457
+ },
458
+ {
459
+ "name": "Mod",
460
+ "id": "2",
461
+ "memberCount": 5
462
+ },
463
+ {
464
+ "name": "Member",
465
+ "id": "3",
466
+ "memberCount": 40
467
+ }
468
+ ],
469
+ "guild": {
470
+ "name": "Skadi"
471
+ }
472
+ },
473
+ "expected": "Skadi/Admin, Skadi/Mod, Skadi/Member"
474
+ },
475
+ {
476
+ "name": "foreach-if-inside",
477
+ "template": "{{#foreach:roles:, }}{{#if:eq:role.memberCount:5}}!{{/if}}{{role.name}}{{/foreach}}",
478
+ "placeholders": [
479
+ {
480
+ "name": "roles",
481
+ "description": "roles",
482
+ "kind": "list",
483
+ "itemAlias": "role"
484
+ }
485
+ ],
486
+ "data": {
487
+ "roles": [
488
+ {
489
+ "name": "Admin",
490
+ "id": "1",
491
+ "memberCount": 2
492
+ },
493
+ {
494
+ "name": "Mod",
495
+ "id": "2",
496
+ "memberCount": 5
497
+ },
498
+ {
499
+ "name": "Member",
500
+ "id": "3",
501
+ "memberCount": 40
502
+ }
503
+ ]
504
+ },
505
+ "expected": "Admin, !Mod, Member"
506
+ },
507
+ {
508
+ "name": "foreach-nested-becomes-literal",
509
+ "template": "{{#foreach:roles}}{{#foreach:roles}}{{role.name}}{{/foreach}}{{/foreach}}",
510
+ "placeholders": [
511
+ {
512
+ "name": "roles",
513
+ "description": "roles",
514
+ "kind": "list",
515
+ "itemAlias": "role"
516
+ }
517
+ ],
518
+ "data": {
519
+ "roles": [
520
+ {
521
+ "name": "Admin",
522
+ "id": "1",
523
+ "memberCount": 2
524
+ },
525
+ {
526
+ "name": "Mod",
527
+ "id": "2",
528
+ "memberCount": 5
529
+ },
530
+ {
531
+ "name": "Member",
532
+ "id": "3",
533
+ "memberCount": 40
534
+ }
535
+ ]
536
+ },
537
+ "expected": "{{#foreach:roles}}{{role.name}}{{/foreach}}{{#foreach:roles}}{{role.name}}{{/foreach}}{{#foreach:roles}}{{role.name}}{{/foreach}}"
538
+ },
539
+ {
540
+ "name": "foreach-without-item-alias-throws",
541
+ "template": "{{#foreach:roles}}x{{/foreach}}",
542
+ "placeholders": [
543
+ {
544
+ "name": "roles",
545
+ "description": "roles",
546
+ "kind": "scalar"
547
+ }
548
+ ],
549
+ "data": {
550
+ "roles": [
551
+ {
552
+ "name": "Admin",
553
+ "id": "1",
554
+ "memberCount": 2
555
+ },
556
+ {
557
+ "name": "Mod",
558
+ "id": "2",
559
+ "memberCount": 5
560
+ },
561
+ {
562
+ "name": "Member",
563
+ "id": "3",
564
+ "memberCount": 40
565
+ }
566
+ ]
567
+ },
568
+ "throws": "Template references {{#foreach:roles}} but no list placeholder \"roles\" with an itemAlias is registered"
569
+ },
570
+ {
571
+ "name": "unclosed-if",
572
+ "template": "{{#if:v}}dangling",
573
+ "placeholders": [
574
+ {
575
+ "name": "v",
576
+ "description": "v",
577
+ "kind": "scalar"
578
+ }
579
+ ],
580
+ "data": {
581
+ "v": true
582
+ },
583
+ "expected": "dangling"
584
+ },
585
+ {
586
+ "name": "stray-endif",
587
+ "template": "a{{/if}}b",
588
+ "placeholders": [],
589
+ "data": {},
590
+ "expected": "a"
591
+ },
592
+ {
593
+ "name": "unknown-tag-passthrough",
594
+ "template": "a{{!weird}}b",
595
+ "placeholders": [],
596
+ "data": {},
597
+ "expected": "a{{!weird}}b"
598
+ },
599
+ {
600
+ "name": "object-recursion",
601
+ "template": {
602
+ "a": "Hi {{name}}",
603
+ "b": [
604
+ 1,
605
+ "and {{name}}",
606
+ null
607
+ ],
608
+ "c": {
609
+ "d": "{{name}}"
610
+ }
611
+ },
612
+ "placeholders": [
613
+ {
614
+ "name": "name",
615
+ "description": "name",
616
+ "kind": "scalar"
617
+ }
618
+ ],
619
+ "data": {
620
+ "name": "Z"
621
+ },
622
+ "expected": {
623
+ "a": "Hi Z",
624
+ "b": [
625
+ 1,
626
+ "and Z",
627
+ null
628
+ ],
629
+ "c": {
630
+ "d": "Z"
631
+ }
632
+ }
633
+ },
634
+ {
635
+ "name": "non-string-passthrough",
636
+ "template": 42,
637
+ "placeholders": [],
638
+ "data": {},
639
+ "expected": 42
640
+ },
641
+ {
642
+ "name": "null-passthrough",
643
+ "template": null,
644
+ "placeholders": [],
645
+ "data": {},
646
+ "expected": null
647
+ }
648
+ ]
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@shurizma/discord-message-contract",
3
+ "version": "0.1.0",
4
+ "description": "Shared template grammar and types for the shuri.gg Discord message builder bot contract",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./fixtures/grammar.json": "./fixtures/grammar.json"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "fixtures",
19
+ "README.md"
20
+ ],
21
+ "sideEffects": false,
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://git.shuri.gg/ShuriZma/discord-message-contract.git"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "capture": "tsx scripts/capture-fixtures.ts",
31
+ "prepublishOnly": "pnpm run build && pnpm run test"
32
+ },
33
+ "devDependencies": {
34
+ "tsx": "^4.20.3",
35
+ "typescript": "^5.8.3",
36
+ "vitest": "^3.2.4"
37
+ }
38
+ }