@typed-sql/compiler 1.0.0-beta.1 → 1.0.0-rc.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.
@@ -1,32 +1,185 @@
1
- const importPattern = /import\s*\{([\s\S]*?)\}\s*from\s*["']([^"']+)["']/g;
1
+ function isIdentifierStart(char) {
2
+ if (char === undefined)
3
+ return false;
4
+ const code = char.charCodeAt(0);
5
+ return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || char === "_" || char === "$";
6
+ }
7
+ function isIdentifierPart(char) {
8
+ if (isIdentifierStart(char))
9
+ return true;
10
+ if (char === undefined)
11
+ return false;
12
+ const code = char.charCodeAt(0);
13
+ return code >= 48 && code <= 57;
14
+ }
15
+ function isWhitespace(char) {
16
+ return char === " " || char === "\t" || char === "\n" || char === "\r" || char === "\f" || char === "\v";
17
+ }
18
+ function identifierAt(source, start) {
19
+ if (!isIdentifierStart(source[start]))
20
+ return undefined;
21
+ let end = start + 1;
22
+ while (isIdentifierPart(source[end]))
23
+ end += 1;
24
+ return { value: source.slice(start, end), end };
25
+ }
26
+ function skipTrivia(source, start) {
27
+ let index = start;
28
+ while (index < source.length) {
29
+ if (isWhitespace(source[index]))
30
+ index += 1;
31
+ else if (source[index] === "/" && source[index + 1] === "/")
32
+ index = skipLineComment(source, index);
33
+ else if (source[index] === "/" && source[index + 1] === "*")
34
+ index = skipBlockComment(source, index);
35
+ else
36
+ break;
37
+ }
38
+ return index;
39
+ }
40
+ function closingImportBrace(source, start) {
41
+ let depth = 1;
42
+ let index = start + 1;
43
+ while (index < source.length) {
44
+ const char = source[index];
45
+ if (char === '"' || char === "'")
46
+ index = skipQuoted(source, index, char);
47
+ else if (char === "/" && source[index + 1] === "/")
48
+ index = skipLineComment(source, index);
49
+ else if (char === "/" && source[index + 1] === "*")
50
+ index = skipBlockComment(source, index);
51
+ else if (char === "{") {
52
+ depth += 1;
53
+ index += 1;
54
+ }
55
+ else if (char === "}") {
56
+ depth -= 1;
57
+ if (depth === 0)
58
+ return index;
59
+ index += 1;
60
+ }
61
+ else
62
+ index += 1;
63
+ }
64
+ return undefined;
65
+ }
66
+ function importedSqlName(specifier) {
67
+ const tokens = [];
68
+ let index = 0;
69
+ while (index < specifier.length) {
70
+ index = skipTrivia(specifier, index);
71
+ const token = identifierAt(specifier, index);
72
+ if (token === undefined) {
73
+ index += 1;
74
+ continue;
75
+ }
76
+ tokens.push(token.value);
77
+ index = token.end;
78
+ }
79
+ if (tokens[0] !== "sql")
80
+ return undefined;
81
+ if (tokens.length === 1)
82
+ return "sql";
83
+ return tokens[1] === "as" && tokens[2] !== undefined ? tokens[2] : undefined;
84
+ }
2
85
  function importedSqlNames(source, sqlModules) {
3
86
  const names = new Set();
4
- for (const match of source.matchAll(importPattern)) {
5
- const specifiers = match[1];
6
- const moduleName = match[2];
7
- if (specifiers === undefined || moduleName === undefined)
87
+ let index = 0;
88
+ while (index < source.length) {
89
+ const char = source[index];
90
+ if (char === '"' || char === "'" || char === "`") {
91
+ index = skipQuoted(source, index, char);
92
+ continue;
93
+ }
94
+ if (char === "/" && source[index + 1] === "/") {
95
+ index = skipLineComment(source, index);
8
96
  continue;
9
- if (!sqlModules.has(moduleName))
97
+ }
98
+ if (char === "/" && source[index + 1] === "*") {
99
+ index = skipBlockComment(source, index);
10
100
  continue;
11
- for (const specifier of specifiers.split(",")) {
12
- const parts = specifier.trim().split(/\s+as\s+/);
13
- if (parts[0] === "sql")
14
- names.add(parts[1] ?? "sql");
15
101
  }
102
+ const keyword = identifierAt(source, index);
103
+ if (keyword === undefined) {
104
+ index += 1;
105
+ continue;
106
+ }
107
+ index = keyword.end;
108
+ if (keyword.value !== "import")
109
+ continue;
110
+ let cursor = skipTrivia(source, index);
111
+ if (source[cursor] !== "{")
112
+ continue;
113
+ const close = closingImportBrace(source, cursor);
114
+ if (close === undefined)
115
+ break;
116
+ const specifiers = source.slice(cursor + 1, close);
117
+ cursor = skipTrivia(source, close + 1);
118
+ const from = identifierAt(source, cursor);
119
+ if (from?.value !== "from")
120
+ continue;
121
+ cursor = skipTrivia(source, from.end);
122
+ const quote = source[cursor];
123
+ if (quote !== '"' && quote !== "'")
124
+ continue;
125
+ const moduleEnd = skipQuoted(source, cursor, quote);
126
+ if (moduleEnd > source.length || source[moduleEnd - 1] !== quote)
127
+ continue;
128
+ const moduleName = source.slice(cursor + 1, moduleEnd - 1);
129
+ if (sqlModules.has(moduleName)) {
130
+ for (const specifier of specifiers.split(",")) {
131
+ const localName = importedSqlName(specifier);
132
+ if (localName !== undefined)
133
+ names.add(localName);
134
+ }
135
+ }
136
+ index = moduleEnd;
16
137
  }
17
138
  return names;
18
139
  }
140
+ const MAX_LINE_START_CACHE_ENTRIES = 8;
141
+ const lineStartCache = new Map();
142
+ function lineStarts(source) {
143
+ const cached = lineStartCache.get(source);
144
+ if (cached !== undefined) {
145
+ lineStartCache.delete(source);
146
+ lineStartCache.set(source, cached);
147
+ return cached;
148
+ }
149
+ const starts = [0];
150
+ for (let index = 0; index < source.length; index += 1) {
151
+ if (source[index] === "\n")
152
+ starts.push(index + 1);
153
+ }
154
+ lineStartCache.set(source, starts);
155
+ while (lineStartCache.size > MAX_LINE_START_CACHE_ENTRIES) {
156
+ lineStartCache.delete(lineStartCache.keys().next().value);
157
+ }
158
+ return starts;
159
+ }
19
160
  function positionRange(source, start, end) {
20
- const before = source.slice(0, start);
21
- const lastNewline = before.lastIndexOf("\n");
161
+ const starts = lineStarts(source);
162
+ let low = 0;
163
+ let high = starts.length;
164
+ while (low < high) {
165
+ const middle = (low + high) >>> 1;
166
+ if ((starts[middle] ?? 0) <= start)
167
+ low = middle + 1;
168
+ else
169
+ high = middle;
170
+ }
171
+ const lineIndex = Math.max(0, low - 1);
172
+ const lineStart = starts[lineIndex] ?? 0;
22
173
  return {
23
174
  start,
24
175
  end,
25
- line: before.split("\n").length,
26
- column: start - lastNewline,
176
+ line: lineIndex + 1,
177
+ column: start - lineStart + 1,
27
178
  };
28
179
  }
29
180
  function skipQuoted(source, start, quote) {
181
+ if (quote === "`")
182
+ return skipTemplate(source, start);
30
183
  let index = start + 1;
31
184
  while (index < source.length) {
32
185
  if (source[index] === "\\")
@@ -38,6 +191,20 @@ function skipQuoted(source, start, quote) {
38
191
  }
39
192
  return source.length;
40
193
  }
194
+ function skipTemplate(source, start) {
195
+ let index = start + 1;
196
+ while (index < source.length) {
197
+ if (source[index] === "\\")
198
+ index += 2;
199
+ else if (source[index] === "`")
200
+ return index + 1;
201
+ else if (source[index] === "$" && source[index + 1] === "{")
202
+ index = interpolationEnd(source, index + 2);
203
+ else
204
+ index += 1;
205
+ }
206
+ return source.length;
207
+ }
41
208
  function skipLineComment(source, start) {
42
209
  const end = source.indexOf("\n", start + 2);
43
210
  return end === -1 ? source.length : end + 1;
@@ -74,11 +241,60 @@ function interpolationEnd(source, start) {
74
241
  }
75
242
  return source.length;
76
243
  }
77
- function extractTemplate(source, tagName, tagStart, backtick, placeholderFor) {
244
+ function hexadecimalValue(source, start, length) {
245
+ const value = source.slice(start, start + length);
246
+ return value.length === length && /^[\dA-Fa-f]+$/u.test(value) ? Number.parseInt(value, 16) : undefined;
247
+ }
248
+ function cookedEscape(source, start) {
249
+ const escaped = source[start + 1];
250
+ if (escaped === undefined)
251
+ return undefined;
252
+ const simple = {
253
+ "0": "\0",
254
+ b: "\b",
255
+ f: "\f",
256
+ n: "\n",
257
+ r: "\r",
258
+ t: "\t",
259
+ v: "\v",
260
+ };
261
+ if (escaped === "\n")
262
+ return { text: "", end: start + 2 };
263
+ if (escaped === "\r") {
264
+ return { text: "", end: source[start + 2] === "\n" ? start + 3 : start + 2 };
265
+ }
266
+ if (escaped === "0" && /\d/u.test(source[start + 2] ?? ""))
267
+ return undefined;
268
+ if (escaped in simple)
269
+ return { text: simple[escaped], end: start + 2 };
270
+ if (escaped === "x") {
271
+ const value = hexadecimalValue(source, start + 2, 2);
272
+ return value === undefined ? undefined : { text: String.fromCodePoint(value), end: start + 4 };
273
+ }
274
+ if (escaped === "u") {
275
+ if (source[start + 2] === "{") {
276
+ const close = source.indexOf("}", start + 3);
277
+ if (close === -1)
278
+ return undefined;
279
+ const digits = source.slice(start + 3, close);
280
+ if (!/^[\dA-Fa-f]{1,6}$/u.test(digits))
281
+ return undefined;
282
+ const value = Number.parseInt(digits, 16);
283
+ return value > 0x10ffff ? undefined : { text: String.fromCodePoint(value), end: close + 1 };
284
+ }
285
+ const value = hexadecimalValue(source, start + 2, 4);
286
+ return value === undefined ? undefined : { text: String.fromCodePoint(value), end: start + 6 };
287
+ }
288
+ if (/[1-9]/u.test(escaped))
289
+ return undefined;
290
+ return { text: escaped, end: start + 2 };
291
+ }
292
+ function extractTemplate(source, tagName, tagStart, backtick, placeholderFor, parameterOffset = 0) {
78
293
  let index = backtick + 1;
79
294
  let sql = "";
80
295
  let parameterCount = 0;
81
296
  const sqlOffsetMap = [];
297
+ const interpolations = [];
82
298
  while (index < source.length) {
83
299
  const char = source[index];
84
300
  if (char === "`") {
@@ -89,21 +305,39 @@ function extractTemplate(source, tagName, tagStart, backtick, placeholderFor) {
89
305
  insertionPosition: tagStart + tagName.length,
90
306
  range: positionRange(source, tagStart, index + 1),
91
307
  sqlOffsetMap,
308
+ interpolations,
92
309
  };
93
310
  }
94
- if (char === "\\" && index + 1 < source.length) {
95
- sql += source[index + 1];
96
- sqlOffsetMap.push(index);
97
- index += 2;
311
+ if (char === "\\") {
312
+ const cooked = cookedEscape(source, index);
313
+ if (cooked === undefined)
314
+ return undefined;
315
+ sql += cooked.text;
316
+ for (let offset = 0; offset < cooked.text.length; offset += 1)
317
+ sqlOffsetMap.push(index);
318
+ index = cooked.end;
98
319
  continue;
99
320
  }
100
321
  if (char === "$" && source[index + 1] === "{") {
101
322
  parameterCount += 1;
102
- const placeholder = placeholderFor(parameterCount);
323
+ const sourceStart = index;
324
+ const expressionStart = index + 2;
325
+ const sourceEnd = interpolationEnd(source, expressionStart);
326
+ const sqlStart = sql.length;
327
+ const placeholder = placeholderFor(parameterOffset + parameterCount);
103
328
  sql += placeholder;
104
329
  for (let offset = 0; offset < placeholder.length; offset += 1)
105
330
  sqlOffsetMap.push(index);
106
- index = interpolationEnd(source, index + 2);
331
+ interpolations.push({
332
+ index: parameterCount,
333
+ sourceStart,
334
+ expressionStart,
335
+ expressionEnd: Math.max(expressionStart, sourceEnd - 1),
336
+ sourceEnd,
337
+ sqlStart,
338
+ sqlEnd: sql.length,
339
+ });
340
+ index = sourceEnd;
107
341
  continue;
108
342
  }
109
343
  sql += char;
@@ -112,6 +346,181 @@ function extractTemplate(source, tagName, tagStart, backtick, placeholderFor) {
112
346
  }
113
347
  return undefined;
114
348
  }
349
+ function bindingNameBefore(source, start) {
350
+ let cursor = start - 1;
351
+ while (cursor >= 0 && isWhitespace(source[cursor]))
352
+ cursor -= 1;
353
+ if (source[cursor] !== "=")
354
+ return undefined;
355
+ cursor -= 1;
356
+ while (cursor >= 0 && isWhitespace(source[cursor]))
357
+ cursor -= 1;
358
+ const end = cursor + 1;
359
+ while (cursor >= 0 && isIdentifierPart(source[cursor]))
360
+ cursor -= 1;
361
+ const name = source.slice(cursor + 1, end);
362
+ return name.length > 0 && isIdentifierStart(name[0]) ? name : undefined;
363
+ }
364
+ function closingParenthesis(source, start) {
365
+ let depth = 1;
366
+ let index = start + 1;
367
+ while (index < source.length) {
368
+ const char = source[index];
369
+ if (char === '"' || char === "'" || char === "`")
370
+ index = skipQuoted(source, index, char);
371
+ else if (char === "/" && source[index + 1] === "/")
372
+ index = skipLineComment(source, index);
373
+ else if (char === "/" && source[index + 1] === "*")
374
+ index = skipBlockComment(source, index);
375
+ else if (char === "(") {
376
+ depth += 1;
377
+ index += 1;
378
+ }
379
+ else if (char === ")") {
380
+ depth -= 1;
381
+ if (depth === 0)
382
+ return index;
383
+ index += 1;
384
+ }
385
+ else
386
+ index += 1;
387
+ }
388
+ return undefined;
389
+ }
390
+ function trimmedRange(source, start, end) {
391
+ let first = start;
392
+ let last = end;
393
+ while (first < last && isWhitespace(source[first]))
394
+ first += 1;
395
+ while (last > first && isWhitespace(source[last - 1]))
396
+ last -= 1;
397
+ return { start: first, end: last };
398
+ }
399
+ function unwrappedRange(source, start, end) {
400
+ let range = trimmedRange(source, start, end);
401
+ while (source[range.start] === "(" && closingParenthesis(source, range.start) === range.end - 1) {
402
+ range = trimmedRange(source, range.start + 1, range.end - 1);
403
+ }
404
+ return range;
405
+ }
406
+ function structuralOperand(source, start, end, tagName) {
407
+ const range = trimmedRange(source, start, end);
408
+ const empty = `${tagName}.empty`;
409
+ if (source.slice(range.start, range.end) === empty)
410
+ return { kind: "empty", ...range };
411
+ const prefix = `${tagName}.fragment`;
412
+ if (!source.startsWith(prefix, range.start))
413
+ return undefined;
414
+ const backtick = skipTrivia(source, range.start + prefix.length);
415
+ if (source[backtick] !== "`" || skipQuoted(source, backtick, "`") !== range.end)
416
+ return undefined;
417
+ return { kind: "fragment", ...range, backtick };
418
+ }
419
+ function conditionalRanges(source, start, end) {
420
+ let question;
421
+ let colon;
422
+ let round = 0;
423
+ let square = 0;
424
+ let curly = 0;
425
+ let index = start;
426
+ while (index < end) {
427
+ const char = source[index];
428
+ if (char === '"' || char === "'" || char === "`") {
429
+ index = skipQuoted(source, index, char);
430
+ continue;
431
+ }
432
+ if (char === "/" && source[index + 1] === "/") {
433
+ index = skipLineComment(source, index);
434
+ continue;
435
+ }
436
+ if (char === "/" && source[index + 1] === "*") {
437
+ index = skipBlockComment(source, index);
438
+ continue;
439
+ }
440
+ if (char === "(")
441
+ round += 1;
442
+ else if (char === ")")
443
+ round -= 1;
444
+ else if (char === "[")
445
+ square += 1;
446
+ else if (char === "]")
447
+ square -= 1;
448
+ else if (char === "{")
449
+ curly += 1;
450
+ else if (char === "}")
451
+ curly -= 1;
452
+ else if (round === 0 &&
453
+ square === 0 &&
454
+ curly === 0 &&
455
+ char === "?" &&
456
+ source[index + 1] !== "." &&
457
+ source[index + 1] !== "?" &&
458
+ source[index - 1] !== "?" &&
459
+ question === undefined)
460
+ question = index;
461
+ else if (round === 0 && square === 0 && curly === 0 && char === ":" && question !== undefined) {
462
+ colon = index;
463
+ break;
464
+ }
465
+ index += 1;
466
+ }
467
+ if (question === undefined || colon === undefined)
468
+ return undefined;
469
+ return {
470
+ condition: unwrappedRange(source, start, question),
471
+ truthy: unwrappedRange(source, question + 1, colon),
472
+ falsy: unwrappedRange(source, colon + 1, end),
473
+ };
474
+ }
475
+ function bareTemplateRange(source, start, end) {
476
+ const range = unwrappedRange(source, start, end);
477
+ if (source[range.start] !== "`" || skipQuoted(source, range.start, "`") !== range.end)
478
+ return undefined;
479
+ return positionRange(source, range.start, range.end);
480
+ }
481
+ export function findUntaggedStructuralTemplates(source, query) {
482
+ const templates = [];
483
+ for (const interpolation of query.interpolations) {
484
+ const expression = unwrappedRange(source, interpolation.expressionStart, interpolation.expressionEnd);
485
+ const conditional = conditionalRanges(source, expression.start, expression.end);
486
+ if (conditional === undefined)
487
+ continue;
488
+ const truthyStructural = structuralOperand(source, conditional.truthy.start, conditional.truthy.end, query.tagName);
489
+ const falsyStructural = structuralOperand(source, conditional.falsy.start, conditional.falsy.end, query.tagName);
490
+ const truthyTemplate = bareTemplateRange(source, conditional.truthy.start, conditional.truthy.end);
491
+ const falsyTemplate = bareTemplateRange(source, conditional.falsy.start, conditional.falsy.end);
492
+ if (truthyTemplate !== undefined && falsyStructural !== undefined) {
493
+ templates.push({ tagName: query.tagName, range: truthyTemplate });
494
+ }
495
+ if (falsyTemplate !== undefined && truthyStructural !== undefined) {
496
+ templates.push({ tagName: query.tagName, range: falsyTemplate });
497
+ }
498
+ }
499
+ return templates;
500
+ }
501
+ export function parseStructuralInterpolation(source, interpolation, tagName) {
502
+ const expression = unwrappedRange(source, interpolation.expressionStart, interpolation.expressionEnd);
503
+ const { start, end } = expression;
504
+ const conditional = conditionalRanges(source, start, end);
505
+ if (conditional === undefined) {
506
+ const truthy = structuralOperand(source, start, end, tagName);
507
+ return truthy === undefined ? undefined : { truthy };
508
+ }
509
+ const truthy = structuralOperand(source, conditional.truthy.start, conditional.truthy.end, tagName);
510
+ const falsy = structuralOperand(source, conditional.falsy.start, conditional.falsy.end, tagName);
511
+ if (truthy === undefined || falsy === undefined)
512
+ return undefined;
513
+ return {
514
+ condition: source.slice(conditional.condition.start, conditional.condition.end).replace(/\s+/gu, " "),
515
+ truthy,
516
+ falsy,
517
+ };
518
+ }
519
+ export function extractStructuralOperand(source, operand, tagName, placeholderFor, parameterOffset) {
520
+ if (operand.kind === "empty" || operand.backtick === undefined)
521
+ return undefined;
522
+ return extractTemplate(source, `${tagName}.fragment`, operand.start, operand.backtick, placeholderFor, parameterOffset);
523
+ }
115
524
  export function extractStaticQueries(source, placeholderFor, sqlModules = ["@typed-sql/core"]) {
116
525
  const names = importedSqlNames(source, new Set(sqlModules));
117
526
  if (names.size === 0)
@@ -136,10 +545,10 @@ export function extractStaticQueries(source, placeholderFor, sqlModules = ["@typ
136
545
  index = skipBlockComment(source, index);
137
546
  continue;
138
547
  }
139
- if (char !== undefined && /[A-Za-z_$]/.test(char)) {
548
+ if (isIdentifierStart(char)) {
140
549
  const start = index;
141
550
  index += 1;
142
- while (index < source.length && /[A-Za-z0-9_$]/.test(source[index]))
551
+ while (index < source.length && isIdentifierPart(source[index]))
143
552
  index += 1;
144
553
  const name = source.slice(start, index);
145
554
  if (!names.has(name))
@@ -160,6 +569,116 @@ export function extractStaticQueries(source, placeholderFor, sqlModules = ["@typ
160
569
  }
161
570
  return queries;
162
571
  }
572
+ export function extractAppendFragments(source, placeholderFor, sqlModules = ["@typed-sql/core"], queries = extractStaticQueries(source, placeholderFor, sqlModules)) {
573
+ const names = importedSqlNames(source, new Set(sqlModules));
574
+ if (names.size === 0)
575
+ return [];
576
+ const bindings = queries.flatMap((query) => {
577
+ const name = bindingNameBefore(source, query.range.start);
578
+ return name === undefined ? [] : [{ name, query }];
579
+ });
580
+ const fragments = [];
581
+ let index = 0;
582
+ while (index < source.length) {
583
+ const char = source[index];
584
+ if (char === '"' || char === "'" || char === "`") {
585
+ index = skipQuoted(source, index, char);
586
+ continue;
587
+ }
588
+ if (char === "/" && source[index + 1] === "/") {
589
+ index = skipLineComment(source, index);
590
+ continue;
591
+ }
592
+ if (char === "/" && source[index + 1] === "*") {
593
+ index = skipBlockComment(source, index);
594
+ continue;
595
+ }
596
+ const tag = identifierAt(source, index);
597
+ if (tag === undefined) {
598
+ index += 1;
599
+ continue;
600
+ }
601
+ index = tag.end;
602
+ if (!names.has(tag.value))
603
+ continue;
604
+ let cursor = skipTrivia(source, tag.end);
605
+ if (source[cursor] !== ".")
606
+ continue;
607
+ cursor = skipTrivia(source, cursor + 1);
608
+ const method = identifierAt(source, cursor);
609
+ if (method?.value !== "append")
610
+ continue;
611
+ cursor = skipTrivia(source, method.end);
612
+ if (source[cursor] !== "(")
613
+ continue;
614
+ const close = closingParenthesis(source, cursor);
615
+ if (close === undefined)
616
+ continue;
617
+ let argument = skipTrivia(source, cursor + 1);
618
+ const baseName = identifierAt(source, argument);
619
+ if (baseName === undefined) {
620
+ index = close + 1;
621
+ continue;
622
+ }
623
+ const base = bindings
624
+ .filter((binding) => binding.name === baseName.value && binding.query.range.start < index)
625
+ .sort((left, right) => right.query.range.start - left.query.range.start)[0]?.query;
626
+ argument = skipTrivia(source, baseName.end);
627
+ if (base === undefined || source[argument] !== ",") {
628
+ index = close + 1;
629
+ continue;
630
+ }
631
+ let fragmentCursor = argument + 1;
632
+ const prefix = [];
633
+ let parameterOffset = base.parameterCount;
634
+ while (fragmentCursor < close) {
635
+ const fragmentChar = source[fragmentCursor];
636
+ if (fragmentChar === '"' || fragmentChar === "'") {
637
+ fragmentCursor = skipQuoted(source, fragmentCursor, fragmentChar);
638
+ continue;
639
+ }
640
+ if (fragmentChar === "`") {
641
+ fragmentCursor = skipQuoted(source, fragmentCursor, fragmentChar);
642
+ continue;
643
+ }
644
+ if (fragmentChar === "/" && source[fragmentCursor + 1] === "/") {
645
+ fragmentCursor = skipLineComment(source, fragmentCursor);
646
+ continue;
647
+ }
648
+ if (fragmentChar === "/" && source[fragmentCursor + 1] === "*") {
649
+ fragmentCursor = skipBlockComment(source, fragmentCursor);
650
+ continue;
651
+ }
652
+ const fragmentTag = identifierAt(source, fragmentCursor);
653
+ if (fragmentTag === undefined) {
654
+ fragmentCursor += 1;
655
+ continue;
656
+ }
657
+ fragmentCursor = fragmentTag.end;
658
+ if (fragmentTag.value !== tag.value)
659
+ continue;
660
+ let memberCursor = skipTrivia(source, fragmentTag.end);
661
+ if (source[memberCursor] !== ".")
662
+ continue;
663
+ memberCursor = skipTrivia(source, memberCursor + 1);
664
+ const member = identifierAt(source, memberCursor);
665
+ if (member?.value !== "fragment")
666
+ continue;
667
+ const backtick = skipTrivia(source, member.end);
668
+ if (source[backtick] !== "`")
669
+ continue;
670
+ const fragment = extractTemplate(source, `${tag.value}.fragment`, fragmentTag.end - tag.value.length, backtick, placeholderFor, parameterOffset);
671
+ if (fragment !== undefined) {
672
+ fragments.push({ base, prefix: [...prefix], fragment, parameterOffset });
673
+ prefix.push(fragment);
674
+ parameterOffset += fragment.parameterCount;
675
+ fragmentCursor = fragment.range.end;
676
+ }
677
+ }
678
+ index = close + 1;
679
+ }
680
+ return fragments;
681
+ }
163
682
  export function mapSqlRange(source, query, range) {
164
683
  const start = query.sqlOffsetMap[range.start] ?? query.range.start;
165
684
  const last = range.end > range.start ? query.sqlOffsetMap[range.end - 1] : start;