@trustquery/browser 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.
@@ -0,0 +1,285 @@
1
+ // CommandScanner - Scans text for word matches based on command map
2
+ // Uses simple string matching (not regex) for simplicity and performance
3
+
4
+ export default class CommandScanner {
5
+ constructor() {
6
+ this.commandMap = null;
7
+ this.commands = [];
8
+ console.log('[CommandScanner] Initialized');
9
+ }
10
+
11
+ /**
12
+ * Set command map
13
+ * @param {Object} commandMap - Command map object
14
+ */
15
+ setCommandMap(commandMap) {
16
+ this.commandMap = commandMap;
17
+ this.commands = this.parseCommandMap(commandMap);
18
+ console.log('[CommandScanner] Command map set with', this.commands.length, 'commands');
19
+ }
20
+
21
+ /**
22
+ * Parse simplified TQL triggers format
23
+ * Format: { "tql-triggers": { "error": [...], "warning": [...], "info": [...] } }
24
+ * @param {Object} triggerMap - Trigger map from tql-triggers.json
25
+ * @returns {Array} Array of command objects
26
+ */
27
+ parseCommandMap(triggerMap) {
28
+ const commands = [];
29
+
30
+ // Extract tql-triggers
31
+ const triggers = triggerMap['tql-triggers'] || triggerMap;
32
+
33
+ if (!triggers || typeof triggers !== 'object') {
34
+ console.warn('[CommandScanner] Invalid trigger map structure');
35
+ return commands;
36
+ }
37
+
38
+ // Iterate through each message state (error, warning, info)
39
+ Object.keys(triggers).forEach(messageState => {
40
+ // Skip metadata fields
41
+ if (messageState.startsWith('$')) {
42
+ return;
43
+ }
44
+
45
+ const triggerList = triggers[messageState];
46
+
47
+ if (!Array.isArray(triggerList)) {
48
+ return;
49
+ }
50
+
51
+ // Process each trigger in this state
52
+ triggerList.forEach((trigger, index) => {
53
+ const handler = trigger.handler || {};
54
+ const description = trigger.description || handler.message || '';
55
+ const category = trigger.category || 'general';
56
+
57
+ // Create intent object compatible with handlers
58
+ const intent = {
59
+ description: description,
60
+ handler: {
61
+ ...handler,
62
+ 'message-state': messageState // Add message-state for styling
63
+ },
64
+ category: category
65
+ };
66
+
67
+ // Handle regex patterns
68
+ if (trigger.type === 'regex' && trigger.regex) {
69
+ trigger.regex.forEach(pattern => {
70
+ commands.push({
71
+ id: `${messageState}-${category}-${index}`,
72
+ match: pattern,
73
+ matchType: 'regex',
74
+ messageState: messageState,
75
+ category: category,
76
+ intent: intent,
77
+ handler: intent.handler,
78
+ caseSensitive: true,
79
+ wholeWord: false
80
+ });
81
+ });
82
+ }
83
+
84
+ // Handle string matches
85
+ if (trigger.type === 'match' && trigger.match) {
86
+ trigger.match.forEach(matchStr => {
87
+ commands.push({
88
+ id: `${messageState}-${category}-${index}`,
89
+ match: matchStr,
90
+ matchType: 'string',
91
+ messageState: messageState,
92
+ category: category,
93
+ intent: intent,
94
+ handler: intent.handler,
95
+ caseSensitive: false, // Case insensitive for natural language
96
+ wholeWord: true // Whole word matching
97
+ });
98
+ });
99
+ }
100
+ });
101
+ });
102
+
103
+ // Sort by length (longest first) to match longer patterns first
104
+ commands.sort((a, b) => b.match.length - a.match.length);
105
+
106
+ console.log('[CommandScanner] Parsed commands:', commands.length, commands);
107
+
108
+ return commands;
109
+ }
110
+
111
+ /**
112
+ * Scan text for all matches
113
+ * @param {string} text - Text to scan
114
+ * @returns {Array} Array of match objects with position info
115
+ */
116
+ scan(text) {
117
+ if (!this.commands || this.commands.length === 0) {
118
+ return [];
119
+ }
120
+
121
+ const matches = [];
122
+ const lines = text.split('\n');
123
+
124
+ // Scan each line
125
+ lines.forEach((line, lineIndex) => {
126
+ const lineMatches = this.scanLine(line, lineIndex);
127
+ matches.push(...lineMatches);
128
+ });
129
+
130
+ console.log('[CommandScanner] Found', matches.length, 'matches');
131
+ return matches;
132
+ }
133
+
134
+ /**
135
+ * Scan a single line for matches
136
+ * @param {string} line - Line text
137
+ * @param {number} lineIndex - Line number (0-indexed)
138
+ * @returns {Array} Matches on this line
139
+ */
140
+ scanLine(line, lineIndex) {
141
+ const matches = [];
142
+ const matchedRanges = []; // Track matched positions to avoid overlaps
143
+
144
+ for (const command of this.commands) {
145
+ const commandMatches = this.findMatches(line, command, lineIndex);
146
+
147
+ // Filter out overlapping matches
148
+ for (const match of commandMatches) {
149
+ if (!this.overlapsExisting(match, matchedRanges)) {
150
+ matches.push(match);
151
+ matchedRanges.push({ start: match.col, end: match.col + match.length });
152
+ }
153
+ }
154
+ }
155
+
156
+ // Sort matches by column position
157
+ matches.sort((a, b) => a.col - b.col);
158
+
159
+ return matches;
160
+ }
161
+
162
+ /**
163
+ * Find all matches of a command in a line
164
+ * @param {string} line - Line text
165
+ * @param {Object} command - Command to search for
166
+ * @param {number} lineIndex - Line number
167
+ * @returns {Array} Matches
168
+ */
169
+ findMatches(line, command, lineIndex) {
170
+ const matches = [];
171
+
172
+ // Handle regex patterns
173
+ if (command.matchType === 'regex') {
174
+ try {
175
+ const regex = new RegExp(command.match, 'g');
176
+ let match;
177
+
178
+ while ((match = regex.exec(line)) !== null) {
179
+ matches.push({
180
+ text: match[0],
181
+ line: lineIndex,
182
+ col: match.index,
183
+ length: match[0].length,
184
+ command: command
185
+ });
186
+ }
187
+ } catch (e) {
188
+ console.warn('[CommandScanner] Invalid regex pattern:', command.match, e);
189
+ }
190
+
191
+ return matches;
192
+ }
193
+
194
+ // Handle string patterns (original logic)
195
+ const searchText = command.caseSensitive ? line : line.toLowerCase();
196
+ const pattern = command.caseSensitive ? command.match : command.match.toLowerCase();
197
+
198
+ let startIndex = 0;
199
+
200
+ while (true) {
201
+ const index = searchText.indexOf(pattern, startIndex);
202
+
203
+ if (index === -1) {
204
+ break; // No more matches
205
+ }
206
+
207
+ // Check if this is a whole word match (if required)
208
+ if (command.wholeWord && !this.isWholeWordMatch(line, index, pattern.length)) {
209
+ startIndex = index + 1;
210
+ continue;
211
+ }
212
+
213
+ // Create match object
214
+ matches.push({
215
+ text: line.substring(index, index + pattern.length),
216
+ line: lineIndex,
217
+ col: index,
218
+ length: pattern.length,
219
+ command: command
220
+ });
221
+
222
+ startIndex = index + pattern.length;
223
+ }
224
+
225
+ return matches;
226
+ }
227
+
228
+ /**
229
+ * Check if match is a whole word (not part of a larger word)
230
+ * @param {string} text - Text to check
231
+ * @param {number} start - Start index of match
232
+ * @param {number} length - Length of match
233
+ * @returns {boolean} True if whole word
234
+ */
235
+ isWholeWordMatch(text, start, length) {
236
+ const end = start + length;
237
+
238
+ // Check character before
239
+ if (start > 0) {
240
+ const before = text[start - 1];
241
+ if (this.isWordChar(before)) {
242
+ return false;
243
+ }
244
+ }
245
+
246
+ // Check character after - treat "/" as a word boundary (resolved trigger)
247
+ if (end < text.length) {
248
+ const after = text[end];
249
+ if (this.isWordChar(after) || after === '/') {
250
+ return false;
251
+ }
252
+ }
253
+
254
+ return true;
255
+ }
256
+
257
+ /**
258
+ * Check if character is a word character (alphanumeric or underscore)
259
+ * @param {string} char - Character to check
260
+ * @returns {boolean} True if word character
261
+ */
262
+ isWordChar(char) {
263
+ return /[a-zA-Z0-9_]/.test(char);
264
+ }
265
+
266
+ /**
267
+ * Check if a match overlaps with existing matches
268
+ * @param {Object} match - New match to check
269
+ * @param {Array} existingRanges - Array of {start, end} ranges
270
+ * @returns {boolean} True if overlaps
271
+ */
272
+ overlapsExisting(match, existingRanges) {
273
+ const matchStart = match.col;
274
+ const matchEnd = match.col + match.length;
275
+
276
+ for (const range of existingRanges) {
277
+ // Check for overlap
278
+ if (matchStart < range.end && matchEnd > range.start) {
279
+ return true;
280
+ }
281
+ }
282
+
283
+ return false;
284
+ }
285
+ }