@zhin.js/command 1.0.2 → 1.0.3
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/README.md +46 -8
- package/lib/command-index.d.ts +3 -2
- package/lib/command-index.js +147 -56
- package/lib/definition.d.ts +87 -8
- package/lib/definition.js +161 -2
- package/lib/provider.d.ts +1 -1
- package/lib/provider.js +47 -8
- package/package.json +4 -3
- package/src/command-index.ts +189 -66
- package/src/definition.ts +302 -9
- package/src/provider.ts +60 -11
package/lib/provider.d.ts
CHANGED
|
@@ -3,5 +3,5 @@ export declare const commandFeatureId: import("@zhin.js/plugin-runtime").Feature
|
|
|
3
3
|
export declare class CommandPathSyntaxError extends TypeError {
|
|
4
4
|
constructor(file: string, detail?: string);
|
|
5
5
|
}
|
|
6
|
-
declare const commandFeature: Readonly<import("@zhin.js/feature-kit").FeatureProvider<import("./definition.js").CommandDefinition<unknown, unknown,
|
|
6
|
+
declare const commandFeature: Readonly<import("@zhin.js/feature-kit").FeatureProvider<import("./definition.js").CommandDefinition<unknown, unknown, import("./definition.js").CommandMessage>, CommandIndex>>;
|
|
7
7
|
export default commandFeature;
|
package/lib/provider.js
CHANGED
|
@@ -40,14 +40,34 @@ async function* discoverCommandDirectory(context, directory, ancestors) {
|
|
|
40
40
|
function isCommandSegment(value) {
|
|
41
41
|
return /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
42
42
|
}
|
|
43
|
-
const dynamicCommandFilePattern = /^\[([a-z][a-zA-Z0-9]*):(
|
|
43
|
+
const dynamicCommandFilePattern = /^\[([a-z][a-zA-Z0-9]*):([a-z][a-z0-9-]*)(?:=([^\]]*))?\]\.tsx?$/;
|
|
44
|
+
const commandParameterTypes = new Set([
|
|
45
|
+
'string',
|
|
46
|
+
'number',
|
|
47
|
+
'integer',
|
|
48
|
+
'float',
|
|
49
|
+
'boolean',
|
|
50
|
+
'word',
|
|
51
|
+
'text',
|
|
52
|
+
'mention',
|
|
53
|
+
'image',
|
|
54
|
+
'face',
|
|
55
|
+
'reply',
|
|
56
|
+
'forward',
|
|
57
|
+
'dice',
|
|
58
|
+
'rps',
|
|
59
|
+
]);
|
|
44
60
|
function parseCommandFile(value) {
|
|
45
61
|
if (/^[a-z0-9][a-z0-9-]*\.tsx?$/.test(value)) {
|
|
46
62
|
return { localSegment: parse(value).name };
|
|
47
63
|
}
|
|
48
64
|
const match = dynamicCommandFilePattern.exec(value);
|
|
49
65
|
if (match) {
|
|
50
|
-
const [, name,
|
|
66
|
+
const [, name, rawType, rawDefault] = match;
|
|
67
|
+
if (!name || !rawType || !commandParameterTypes.has(rawType)) {
|
|
68
|
+
throw new CommandPathSyntaxError(value, `unsupported parameter type: ${rawType ?? ''}`);
|
|
69
|
+
}
|
|
70
|
+
const type = rawType;
|
|
51
71
|
// Metadata can change during HMR while $name keeps the Capability identity stable.
|
|
52
72
|
const parameter = rawDefault === undefined
|
|
53
73
|
? { name, type }
|
|
@@ -60,20 +80,38 @@ function parseCommandFile(value) {
|
|
|
60
80
|
return undefined;
|
|
61
81
|
}
|
|
62
82
|
function parseParameterValue(name, type, value, source) {
|
|
63
|
-
if (type === 'string')
|
|
83
|
+
if (type === 'string' || type === 'word' || type === 'text')
|
|
64
84
|
return value;
|
|
65
|
-
if (type === 'number') {
|
|
85
|
+
if (type === 'number' || type === 'integer' || type === 'float') {
|
|
66
86
|
const number = Number(value);
|
|
67
|
-
if (value.trim().length > 0
|
|
87
|
+
if (value.trim().length > 0
|
|
88
|
+
&& Number.isFinite(number)
|
|
89
|
+
&& (type !== 'integer' || Number.isInteger(number))
|
|
90
|
+
&& (type !== 'float' || value.includes('.')))
|
|
68
91
|
return number;
|
|
92
|
+
throw new CommandPathSyntaxError(source, `default for ${name}:${type} is invalid`);
|
|
93
|
+
}
|
|
94
|
+
if (type === 'boolean') {
|
|
95
|
+
if (value === 'true' || value === 'false')
|
|
96
|
+
return value === 'true';
|
|
97
|
+
throw new CommandPathSyntaxError(source, `default for ${name}:${type} is invalid`);
|
|
69
98
|
}
|
|
70
|
-
|
|
71
|
-
|
|
99
|
+
if (isStructuredParameter(type)) {
|
|
100
|
+
throw new CommandPathSyntaxError(source, `default for structured parameter ${name}:${type} is not supported`);
|
|
72
101
|
}
|
|
73
102
|
throw new CommandPathSyntaxError(source, `default for ${name}:${type} is invalid`);
|
|
74
103
|
}
|
|
104
|
+
function isStructuredParameter(type) {
|
|
105
|
+
return type === 'mention'
|
|
106
|
+
|| type === 'image'
|
|
107
|
+
|| type === 'face'
|
|
108
|
+
|| type === 'reply'
|
|
109
|
+
|| type === 'forward'
|
|
110
|
+
|| type === 'dice'
|
|
111
|
+
|| type === 'rps';
|
|
112
|
+
}
|
|
75
113
|
export class CommandPathSyntaxError extends TypeError {
|
|
76
|
-
constructor(file, detail = 'expected [name:
|
|
114
|
+
constructor(file, detail = 'expected [name:type=default].ts(x)') {
|
|
77
115
|
super(`Invalid Command path ${file}: ${detail}`);
|
|
78
116
|
this.name = 'CommandPathSyntaxError';
|
|
79
117
|
}
|
|
@@ -82,6 +120,7 @@ const commandFeature = defineFeatureProvider({
|
|
|
82
120
|
protocol: 1,
|
|
83
121
|
id: commandFeatureId,
|
|
84
122
|
authoring: {
|
|
123
|
+
setupMethod: 'addCommand',
|
|
85
124
|
conventions: [commandFiles],
|
|
86
125
|
validate: parseCommandDefinition,
|
|
87
126
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/command",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Convention-based Command Feature for Zhin Plugin Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
"src"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"
|
|
21
|
-
"@zhin.js/
|
|
20
|
+
"segment-matcher": "^1.0.5",
|
|
21
|
+
"@zhin.js/feature-kit": "1.0.3",
|
|
22
|
+
"@zhin.js/plugin-runtime": "1.1.1"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@types/node": "^26.1.0",
|
package/src/command-index.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SegmentMatcher,
|
|
3
|
+
TypeMatcherRegistry,
|
|
4
|
+
type MessageSegment,
|
|
5
|
+
} from 'segment-matcher';
|
|
1
6
|
import type {
|
|
2
7
|
CapabilitySlot,
|
|
3
8
|
PluginId,
|
|
@@ -9,6 +14,7 @@ import {
|
|
|
9
14
|
type CommandParameterDefinition,
|
|
10
15
|
type CommandParameterType,
|
|
11
16
|
type CommandParameterValue,
|
|
17
|
+
type CommandSegment,
|
|
12
18
|
} from './definition.js';
|
|
13
19
|
|
|
14
20
|
export interface CommandParameterDescriptor extends CommandParameterDefinition {
|
|
@@ -33,30 +39,47 @@ interface CommandRecord extends CommandDescriptor {
|
|
|
33
39
|
readonly slot: Readonly<CapabilitySlot<CommandDefinition>>;
|
|
34
40
|
readonly segments: readonly string[];
|
|
35
41
|
readonly parameter?: CommandParameterDefinition;
|
|
42
|
+
readonly matcher: SegmentMatcher;
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
interface CommandMatch {
|
|
39
46
|
readonly command: CommandRecord;
|
|
40
47
|
readonly params: Readonly<Record<string, CommandParameterValue>>;
|
|
48
|
+
readonly remaining: readonly Readonly<CommandSegment>[];
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
export type CommandMatchInput = string | readonly Readonly<CommandSegment>[];
|
|
52
|
+
|
|
53
|
+
const segmentFields = {
|
|
54
|
+
text: 'text',
|
|
55
|
+
mention: 'target',
|
|
56
|
+
at: ['target', 'user_id', 'qq'],
|
|
57
|
+
face: 'id',
|
|
58
|
+
image: (segment: MessageSegment) =>
|
|
59
|
+
segment.data.media ?? segment.data.file ?? segment.data.url ?? segment.data.src,
|
|
60
|
+
reply: ['message_id', 'reply_id', 'id'],
|
|
61
|
+
forward: ['forward_id', 'res_id', 'message_id', 'id'],
|
|
62
|
+
dice: 'result',
|
|
63
|
+
rps: 'result',
|
|
64
|
+
};
|
|
65
|
+
|
|
43
66
|
export class CommandIndex {
|
|
44
67
|
readonly $projection = 'zhin.command-index/1' as const;
|
|
45
68
|
readonly #commands: readonly CommandRecord[];
|
|
46
|
-
readonly #staticCommands = new Map<string, CommandRecord>();
|
|
47
|
-
readonly #dynamicCommands = new Map<string, CommandRecord>();
|
|
48
69
|
|
|
49
70
|
constructor(
|
|
50
71
|
slots: readonly Readonly<CapabilitySlot<CommandDefinition>>[],
|
|
51
72
|
private readonly snapshot: RuntimeSnapshot,
|
|
52
73
|
) {
|
|
53
74
|
const commands: CommandRecord[] = [];
|
|
75
|
+
const staticCommands = new Map<string, CommandRecord>();
|
|
76
|
+
const dynamicCommands = new Map<string, CommandRecord>();
|
|
54
77
|
for (const slot of slots) {
|
|
55
78
|
const segments = runtimeSegments(slot.owner, slot.localName);
|
|
56
79
|
const parameter = slot.definition.$parameter;
|
|
57
80
|
assertParameterSegment(segments, parameter, slot.source);
|
|
58
81
|
const name = displayName(segments, parameter);
|
|
59
|
-
const record = Object.freeze({
|
|
82
|
+
const record: CommandRecord = Object.freeze({
|
|
60
83
|
name,
|
|
61
84
|
description: slot.definition.description,
|
|
62
85
|
source: slot.source,
|
|
@@ -67,19 +90,20 @@ export class CommandIndex {
|
|
|
67
90
|
slot,
|
|
68
91
|
segments: Object.freeze(segments),
|
|
69
92
|
parameter,
|
|
93
|
+
matcher: new SegmentMatcher(matcherPattern(segments, parameter), segmentFields),
|
|
70
94
|
});
|
|
71
95
|
if (!parameter) {
|
|
72
96
|
const key = segments.join(' ');
|
|
73
|
-
if (
|
|
74
|
-
|
|
97
|
+
if (staticCommands.has(key)) throw duplicateCommand(key);
|
|
98
|
+
staticCommands.set(key, record);
|
|
75
99
|
} else {
|
|
76
100
|
const shape = routeShape(segments);
|
|
77
|
-
if (
|
|
78
|
-
|
|
101
|
+
if (dynamicCommands.has(shape)) throw duplicateCommand(name);
|
|
102
|
+
dynamicCommands.set(shape, record);
|
|
79
103
|
}
|
|
80
104
|
commands.push(record);
|
|
81
105
|
}
|
|
82
|
-
this.#commands = Object.freeze(commands);
|
|
106
|
+
this.#commands = Object.freeze(commands.sort(compareCommands));
|
|
83
107
|
}
|
|
84
108
|
|
|
85
109
|
list(): readonly CommandDescriptor[] {
|
|
@@ -88,7 +112,7 @@ export class CommandIndex {
|
|
|
88
112
|
|
|
89
113
|
has(name: string): boolean {
|
|
90
114
|
try {
|
|
91
|
-
return this.#match(name) !== undefined;
|
|
115
|
+
return this.#match(name, true) !== undefined;
|
|
92
116
|
} catch (error) {
|
|
93
117
|
if (error instanceof CommandParameterValueError) return false;
|
|
94
118
|
throw error;
|
|
@@ -96,8 +120,11 @@ export class CommandIndex {
|
|
|
96
120
|
}
|
|
97
121
|
|
|
98
122
|
async execute(name: string, args: readonly string[] = []): Promise<unknown> {
|
|
99
|
-
const match = this.#match(name);
|
|
100
|
-
if (!match)
|
|
123
|
+
const match = this.#match(name, true);
|
|
124
|
+
if (!match) {
|
|
125
|
+
this.#diagnoseParameter(name);
|
|
126
|
+
throw new Error(`Unknown Command: ${name}`);
|
|
127
|
+
}
|
|
101
128
|
return match.command.slot.definition.execute(
|
|
102
129
|
createCommandContext(
|
|
103
130
|
this.snapshot,
|
|
@@ -108,55 +135,70 @@ export class CommandIndex {
|
|
|
108
135
|
);
|
|
109
136
|
}
|
|
110
137
|
|
|
111
|
-
async dispatch(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
async dispatch(
|
|
139
|
+
input: CommandMatchInput,
|
|
140
|
+
source: unknown = undefined,
|
|
141
|
+
): Promise<CommandDispatchResult> {
|
|
142
|
+
const match = this.#match(input, false);
|
|
143
|
+
if (!match) return Object.freeze({ matched: false });
|
|
144
|
+
const args = textArgs(match.remaining);
|
|
145
|
+
const value = await match.command.slot.definition.execute(
|
|
146
|
+
createCommandContext(
|
|
147
|
+
this.snapshot,
|
|
148
|
+
match.command.slot.owner,
|
|
149
|
+
args,
|
|
150
|
+
match.params,
|
|
151
|
+
source,
|
|
152
|
+
match.remaining,
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
return Object.freeze({
|
|
156
|
+
matched: true,
|
|
157
|
+
command: match.command.name,
|
|
158
|
+
owner: match.command.slot.owner,
|
|
159
|
+
value,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#match(input: CommandMatchInput, exact: boolean): CommandMatch | undefined {
|
|
164
|
+
const segments = normalizeSegments(
|
|
165
|
+
typeof input === 'string'
|
|
166
|
+
? input.trim()
|
|
167
|
+
? [{ type: 'text', data: { text: input.trim() } }]
|
|
168
|
+
: []
|
|
169
|
+
: input,
|
|
170
|
+
);
|
|
171
|
+
if (segments.length === 0) return undefined;
|
|
172
|
+
|
|
173
|
+
for (const command of this.#commands) {
|
|
174
|
+
const result = command.matcher.match(asMatcherSegments(segments));
|
|
175
|
+
if (!result || !hasCommandBoundary(result.remaining)) continue;
|
|
176
|
+
const remaining = normalizeSegments(result.remaining);
|
|
177
|
+
if (exact && remaining.length > 0) continue;
|
|
178
|
+
return {
|
|
179
|
+
command,
|
|
180
|
+
params: Object.freeze({ ...result.params }) as Readonly<
|
|
181
|
+
Record<string, CommandParameterValue>
|
|
182
|
+
>,
|
|
183
|
+
remaining,
|
|
184
|
+
};
|
|
132
185
|
}
|
|
133
|
-
return
|
|
186
|
+
return undefined;
|
|
134
187
|
}
|
|
135
188
|
|
|
136
|
-
#
|
|
189
|
+
#diagnoseParameter(name: string): void {
|
|
137
190
|
const words = splitCommand(name);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
for (const command of this.#dynamicCommands.values()) {
|
|
143
|
-
const parameter = command.parameter as CommandParameterDefinition;
|
|
144
|
-
const optional = parameter.defaultValue !== undefined;
|
|
145
|
-
if (words.length !== command.segments.length &&
|
|
146
|
-
!(optional && words.length === command.segments.length - 1)) continue;
|
|
191
|
+
for (const command of this.#commands) {
|
|
192
|
+
const parameter = command.parameter;
|
|
193
|
+
if (!parameter) continue;
|
|
147
194
|
const parameterIndex = command.segments.findIndex((segment) => segment.startsWith('$'));
|
|
195
|
+
if (words.length !== command.segments.length) continue;
|
|
148
196
|
if (!command.segments.every((segment, index) =>
|
|
149
197
|
index === parameterIndex || segment === words[index])) continue;
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
: parseRuntimeValue(parameter, rawValue);
|
|
154
|
-
return {
|
|
155
|
-
command,
|
|
156
|
-
params: Object.freeze({ [parameter.name]: value }),
|
|
157
|
-
};
|
|
198
|
+
const value = words[parameterIndex];
|
|
199
|
+
if (value === undefined || matchesParameter(parameter.type, value)) continue;
|
|
200
|
+
throw new CommandParameterValueError(parameter.name, parameter.type, value);
|
|
158
201
|
}
|
|
159
|
-
return undefined;
|
|
160
202
|
}
|
|
161
203
|
}
|
|
162
204
|
|
|
@@ -179,9 +221,9 @@ function assertParameterSegment(
|
|
|
179
221
|
): void {
|
|
180
222
|
const dynamicSegments = segments.filter((segment) => segment.startsWith('$'));
|
|
181
223
|
if (!parameter && dynamicSegments.length === 0) return;
|
|
182
|
-
if (parameter && dynamicSegments.length === 1
|
|
183
|
-
dynamicSegments[0] === `$${parameter.name}`
|
|
184
|
-
segments.at(-1) === dynamicSegments[0]) return;
|
|
224
|
+
if (parameter && dynamicSegments.length === 1
|
|
225
|
+
&& dynamicSegments[0] === `$${parameter.name}`
|
|
226
|
+
&& segments.at(-1) === dynamicSegments[0]) return;
|
|
185
227
|
throw new Error(`Broken dynamic Command identity for ${source}`);
|
|
186
228
|
}
|
|
187
229
|
|
|
@@ -197,33 +239,114 @@ function displayName(
|
|
|
197
239
|
}).join(' ');
|
|
198
240
|
}
|
|
199
241
|
|
|
242
|
+
function matcherPattern(
|
|
243
|
+
segments: readonly string[],
|
|
244
|
+
parameter: CommandParameterDefinition | undefined,
|
|
245
|
+
): string {
|
|
246
|
+
return segments.map((segment) => {
|
|
247
|
+
if (!segment.startsWith('$')) return segment;
|
|
248
|
+
if (!parameter) throw new Error(`Missing Command parameter metadata: ${segment}`);
|
|
249
|
+
const type = matcherType(parameter.type);
|
|
250
|
+
return parameter.defaultValue === undefined
|
|
251
|
+
? `<${parameter.name}:${type}>`
|
|
252
|
+
: `[${parameter.name}:${type}=${String(parameter.defaultValue)}]`;
|
|
253
|
+
}).join(' ');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function matcherType(type: CommandParameterType): string {
|
|
257
|
+
return type === 'string' ? 'word' : type;
|
|
258
|
+
}
|
|
259
|
+
|
|
200
260
|
function routeShape(segments: readonly string[]): string {
|
|
201
261
|
return segments.map((segment) => segment.startsWith('$') ? '$' : segment).join(' ');
|
|
202
262
|
}
|
|
203
263
|
|
|
264
|
+
function compareCommands(left: CommandRecord, right: CommandRecord): number {
|
|
265
|
+
const leftDynamic = left.parameter ? 1 : 0;
|
|
266
|
+
const rightDynamic = right.parameter ? 1 : 0;
|
|
267
|
+
return leftDynamic - rightDynamic
|
|
268
|
+
|| staticSegmentCount(right.segments) - staticSegmentCount(left.segments)
|
|
269
|
+
|| right.segments.length - left.segments.length
|
|
270
|
+
|| right.name.length - left.name.length
|
|
271
|
+
|| left.name.localeCompare(right.name);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function staticSegmentCount(segments: readonly string[]): number {
|
|
275
|
+
return segments.filter((segment) => !segment.startsWith('$')).length;
|
|
276
|
+
}
|
|
277
|
+
|
|
204
278
|
function splitCommand(value: string): readonly string[] {
|
|
205
279
|
const normalized = value.trim();
|
|
206
280
|
return normalized ? normalized.split(/\s+/) : [];
|
|
207
281
|
}
|
|
208
282
|
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
value:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
283
|
+
function matchesParameter(type: CommandParameterType, value: string): boolean {
|
|
284
|
+
const matcher = TypeMatcherRegistry.getMatcher(matcherType(type));
|
|
285
|
+
return matcher ? matcher.match(value).success : false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function asMatcherSegments(
|
|
289
|
+
segments: readonly Readonly<CommandSegment>[],
|
|
290
|
+
): MessageSegment[] {
|
|
291
|
+
return segments.map((segment) => ({
|
|
292
|
+
type: typeof segment.type === 'string' ? segment.type : { name: segment.type.name },
|
|
293
|
+
data: { ...segment.data },
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function hasCommandBoundary(segments: readonly MessageSegment[]): boolean {
|
|
298
|
+
const first = segments[0];
|
|
299
|
+
if (!first || first.type !== 'text') return true;
|
|
300
|
+
const text = first.data.text;
|
|
301
|
+
return typeof text !== 'string' || text.length === 0 || /^\s/u.test(text);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function normalizeSegments(
|
|
305
|
+
input: readonly Readonly<CommandSegment>[],
|
|
306
|
+
): readonly Readonly<CommandSegment>[] {
|
|
307
|
+
const segments = input.map((segment) => ({
|
|
308
|
+
type: typeof segment.type === 'string' ? segment.type : { name: segment.type.name },
|
|
309
|
+
data: { ...segment.data },
|
|
310
|
+
}));
|
|
311
|
+
trimBoundary(segments, 'start');
|
|
312
|
+
trimBoundary(segments, 'end');
|
|
313
|
+
return Object.freeze(segments.map((segment) => Object.freeze({
|
|
314
|
+
type: typeof segment.type === 'string'
|
|
315
|
+
? segment.type
|
|
316
|
+
: Object.freeze({ name: segment.type.name }),
|
|
317
|
+
data: Object.freeze(segment.data),
|
|
318
|
+
})));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function trimBoundary(
|
|
322
|
+
segments: Array<{ type: string | { name: string }; data: Record<string, unknown> }>,
|
|
323
|
+
side: 'start' | 'end',
|
|
324
|
+
): void {
|
|
325
|
+
while (segments.length > 0) {
|
|
326
|
+
const index = side === 'start' ? 0 : segments.length - 1;
|
|
327
|
+
const segment = segments[index];
|
|
328
|
+
if (!segment || segment.type !== 'text' || typeof segment.data.text !== 'string') return;
|
|
329
|
+
const text = side === 'start' ? segment.data.text.trimStart() : segment.data.text.trimEnd();
|
|
330
|
+
if (text) {
|
|
331
|
+
segment.data.text = text;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
segments.splice(index, 1);
|
|
219
335
|
}
|
|
220
|
-
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function textArgs(segments: readonly Readonly<CommandSegment>[]): readonly string[] {
|
|
339
|
+
return Object.freeze(segments.flatMap((segment) => {
|
|
340
|
+
if (segment.type !== 'text' || typeof segment.data.text !== 'string') return [];
|
|
341
|
+
return splitCommand(segment.data.text);
|
|
342
|
+
}));
|
|
221
343
|
}
|
|
222
344
|
|
|
223
345
|
function toDescriptor({
|
|
224
346
|
slot: _slot,
|
|
225
347
|
segments: _segments,
|
|
226
348
|
parameter: _parameter,
|
|
349
|
+
matcher: _matcher,
|
|
227
350
|
...descriptor
|
|
228
351
|
}: CommandRecord): CommandDescriptor {
|
|
229
352
|
return descriptor;
|