@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 +21 -0
- package/README.md +151 -0
- package/dist/conditional-buttons.d.ts +3 -0
- package/dist/conditional-buttons.d.ts.map +1 -0
- package/dist/conditional-buttons.js +55 -0
- package/dist/conditional-buttons.js.map +1 -0
- package/dist/grammar.d.ts +3 -0
- package/dist/grammar.d.ts.map +1 -0
- package/dist/grammar.js +219 -0
- package/dist/grammar.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/timestamps.d.ts +2 -0
- package/dist/timestamps.d.ts.map +1 -0
- package/dist/timestamps.js +18 -0
- package/dist/timestamps.js.map +1 -0
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/fixtures/grammar.json +648 -0
- package/package.json +38 -0
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 @@
|
|
|
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 @@
|
|
|
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"}
|
package/dist/grammar.js
ADDED
|
@@ -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":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|