@@ -165,21 +165,40 @@ impl QueryParams {
165
165
}
166
166
167
167
/// Helper function to serialize classes in the format expected by the categorize function
168
+ /// This version builds the query string directly without JSON serialization to avoid double-escaping
168
169
fn serialize_classes ( classes : & [ ClassRule ] ) -> String {
169
- // Convert Vec<(CategoryId, CategorySpec)> to the JSON format expected by categorize
170
- let serialized_classes: Vec < ( Vec < String > , serde_json:: Value ) > = classes
171
- . iter ( )
172
- . map ( |( category_id, category_spec) | {
173
- let spec_json = serde_json:: json!( {
174
- "type" : category_spec. spec_type,
175
- "regex" : category_spec. regex,
176
- "ignore_case" : category_spec. ignore_case
177
- } ) ;
178
- ( category_id. clone ( ) , spec_json)
179
- } )
180
- . collect ( ) ;
181
-
182
- serde_json:: to_string ( & serialized_classes) . unwrap_or_else ( |_| "[]" . to_string ( ) )
170
+ let mut parts = Vec :: new ( ) ;
171
+
172
+ for ( category_id, category_spec) in classes {
173
+ // Build category array string manually: ["Work", "Programming"]
174
+ let category_str = format ! (
175
+ "[{}]" ,
176
+ category_id
177
+ . iter( )
178
+ . map( |s| format!( "\" {}\" " , s) )
179
+ . collect:: <Vec <_>>( )
180
+ . join( ", " )
181
+ ) ;
182
+
183
+ // Build spec object manually to avoid JSON escaping regex patterns
184
+ let mut spec_parts = Vec :: new ( ) ;
185
+ spec_parts. push ( format ! ( "\" type\" : \" {}\" " , category_spec. spec_type) ) ;
186
+
187
+ // Only include regex for non-"none" types, and use raw pattern without escaping
188
+ if category_spec. spec_type != "none" {
189
+ spec_parts. push ( format ! ( "\" regex\" : \" {}\" " , category_spec. regex) ) ;
190
+ }
191
+
192
+ // Always include ignore_case field
193
+ spec_parts. push ( format ! ( "\" ignore_case\" : {}" , category_spec. ignore_case) ) ;
194
+
195
+ let spec_str = format ! ( "{{{}}}" , spec_parts. join( ", " ) ) ;
196
+
197
+ // Build the tuple [category, spec]
198
+ parts. push ( format ! ( "[{}, {}]" , category_str, spec_str) ) ;
199
+ }
200
+
201
+ format ! ( "[{}]" , parts. join( ", " ) )
183
202
}
184
203
185
204
fn build_desktop_canonical_events ( params : & DesktopQueryParams ) -> String {
@@ -195,7 +214,7 @@ fn build_desktop_canonical_events(params: &DesktopQueryParams) -> String {
195
214
if params. base . filter_afk {
196
215
query. push ( format ! (
197
216
"not_afk = flood(query_bucket(find_bucket(\" {}\" )));
198
- not_afk = filter_keyvals(not_afk, \" status\" , [\" not-afk\" ])",
217
+ not_afk = filter_keyvals(not_afk, \" status\" , [\" not-afk\" ])" ,
199
218
escape_doublequote( & params. bid_afk)
200
219
) ) ;
201
220
}
@@ -207,7 +226,7 @@ fn build_desktop_canonical_events(params: &DesktopQueryParams) -> String {
207
226
if params. base . include_audible {
208
227
query. push (
209
228
"audible_events = filter_keyvals(browser_events, \" audible\" , [true]);
210
- not_afk = period_union(not_afk, audible_events)"
229
+ not_afk = period_union(not_afk, audible_events)"
211
230
. to_string ( ) ,
212
231
) ;
213
232
}
@@ -221,7 +240,7 @@ fn build_desktop_canonical_events(params: &DesktopQueryParams) -> String {
221
240
// Add categorization if classes specified
222
241
if !params. base . classes . is_empty ( ) {
223
242
query. push ( format ! (
224
- "events = categorize(events, {})" ,
243
+ "events = categorize(events, {}); " ,
225
244
serialize_classes( & params. base. classes)
226
245
) ) ;
227
246
}
@@ -252,7 +271,7 @@ fn build_android_canonical_events(params: &AndroidQueryParams) -> String {
252
271
// Add categorization if classes specified
253
272
if !params. base . classes . is_empty ( ) {
254
273
query. push ( format ! (
255
- "events = categorize(events, {})" ,
274
+ "events = categorize(events, {}); " ,
256
275
serialize_classes( & params. base. classes)
257
276
) ) ;
258
277
}
@@ -269,26 +288,26 @@ fn build_android_canonical_events(params: &AndroidQueryParams) -> String {
269
288
}
270
289
271
290
fn build_browser_events ( params : & DesktopQueryParams ) -> String {
272
- let mut query = String :: from ( "browser_events = [];\n " ) ;
291
+ let mut query = String :: from ( "browser_events = [];" ) ;
273
292
274
293
for browser_bucket in & params. base . bid_browsers {
275
294
for ( browser_name, app_names) in BROWSER_APPNAMES . entries ( ) {
276
295
if browser_bucket. contains ( browser_name) {
277
296
query. push_str ( & format ! (
278
- "events_{0} = flood(query_bucket(\" {1}\" ));
279
- window_{0} = filter_keyvals(events, \" app\" , {2});
280
- events_{0} = filter_period_intersect(events_{0}, window_{0});
281
- events_{0} = split_url_events(events_{0});
282
- browser_events = concat(browser_events, events_{0});
283
- browser_events = sort_by_timestamp(browser_events);\n " ,
297
+ "
298
+ events_{0} = flood(query_bucket(\" {1}\" ));
299
+ window_{0} = filter_keyvals(events, \" app\" , {2});
300
+ events_{0} = filter_period_intersect(events_{0}, window_{0});
301
+ events_{0} = split_url_events(events_{0});
302
+ browser_events = concat(browser_events, events_{0});
303
+ browser_events = sort_by_timestamp(browser_events)" ,
284
304
browser_name,
285
305
escape_doublequote( browser_bucket) ,
286
306
serde_json:: to_string( app_names) . unwrap( )
287
307
) ) ;
288
308
}
289
309
}
290
310
}
291
-
292
311
query
293
312
}
294
313
@@ -414,9 +433,9 @@ mod tests {
414
433
assert ! ( serialized. contains( "Programming" ) ) ;
415
434
assert ! ( serialized. contains( "Google Docs" ) ) ;
416
435
assert ! ( serialized. contains( "GitHub|vim" ) ) ;
417
- assert ! ( serialized. contains( "\" type\" :\" regex\" " ) ) ;
418
- assert ! ( serialized. contains( "\" ignore_case\" :false" ) ) ;
419
- assert ! ( serialized. contains( "\" ignore_case\" :true" ) ) ;
436
+ assert ! ( serialized. contains( "\" type\" : \" regex\" " ) ) ;
437
+ assert ! ( serialized. contains( "\" ignore_case\" : false" ) ) ;
438
+ assert ! ( serialized. contains( "\" ignore_case\" : true" ) ) ;
420
439
}
421
440
422
441
#[ test]
0 commit comments