@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.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/dist/trustquery.js +2904 -0
- package/dist/trustquery.js.map +1 -0
- package/package.json +49 -0
- package/src/AutoGrow.js +66 -0
- package/src/BubbleManager.js +219 -0
- package/src/CommandHandlers.js +350 -0
- package/src/CommandScanner.js +285 -0
- package/src/DropdownManager.js +592 -0
- package/src/InteractionHandler.js +225 -0
- package/src/OverlayRenderer.js +241 -0
- package/src/StyleManager.js +523 -0
- package/src/TrustQuery.js +402 -0
|
@@ -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
|
+
}
|