@faubulous/mentor-rdf-serializers 1.0.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.
Files changed (82) hide show
  1. package/README.md +582 -0
  2. package/dist/base-serializer.d.ts +63 -0
  3. package/dist/base-serializer.d.ts.map +1 -0
  4. package/dist/base-serializer.js +144 -0
  5. package/dist/base-serializer.js.map +1 -0
  6. package/dist/base-token-formatter.d.ts +225 -0
  7. package/dist/base-token-formatter.d.ts.map +1 -0
  8. package/dist/base-token-formatter.js +521 -0
  9. package/dist/base-token-formatter.js.map +1 -0
  10. package/dist/index.d.ts +20 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +36 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/jsonld/serializer.d.ts +99 -0
  15. package/dist/jsonld/serializer.d.ts.map +1 -0
  16. package/dist/jsonld/serializer.js +282 -0
  17. package/dist/jsonld/serializer.js.map +1 -0
  18. package/dist/n3/formatter.d.ts +105 -0
  19. package/dist/n3/formatter.d.ts.map +1 -0
  20. package/dist/n3/formatter.js +444 -0
  21. package/dist/n3/formatter.js.map +1 -0
  22. package/dist/n3/serializer.d.ts +103 -0
  23. package/dist/n3/serializer.d.ts.map +1 -0
  24. package/dist/n3/serializer.js +134 -0
  25. package/dist/n3/serializer.js.map +1 -0
  26. package/dist/nquads/formatter.d.ts +44 -0
  27. package/dist/nquads/formatter.d.ts.map +1 -0
  28. package/dist/nquads/formatter.js +116 -0
  29. package/dist/nquads/formatter.js.map +1 -0
  30. package/dist/nquads/serializer.d.ts +38 -0
  31. package/dist/nquads/serializer.d.ts.map +1 -0
  32. package/dist/nquads/serializer.js +70 -0
  33. package/dist/nquads/serializer.js.map +1 -0
  34. package/dist/ntriples/formatter.d.ts +44 -0
  35. package/dist/ntriples/formatter.d.ts.map +1 -0
  36. package/dist/ntriples/formatter.js +116 -0
  37. package/dist/ntriples/formatter.js.map +1 -0
  38. package/dist/ntriples/serializer.d.ts +38 -0
  39. package/dist/ntriples/serializer.d.ts.map +1 -0
  40. package/dist/ntriples/serializer.js +79 -0
  41. package/dist/ntriples/serializer.js.map +1 -0
  42. package/dist/sorting/index.d.ts +5 -0
  43. package/dist/sorting/index.d.ts.map +1 -0
  44. package/dist/sorting/index.js +5 -0
  45. package/dist/sorting/index.js.map +1 -0
  46. package/dist/sorting/strategies.d.ts +46 -0
  47. package/dist/sorting/strategies.d.ts.map +1 -0
  48. package/dist/sorting/strategies.js +259 -0
  49. package/dist/sorting/strategies.js.map +1 -0
  50. package/dist/sparql/formatter.d.ts +103 -0
  51. package/dist/sparql/formatter.d.ts.map +1 -0
  52. package/dist/sparql/formatter.js +764 -0
  53. package/dist/sparql/formatter.js.map +1 -0
  54. package/dist/token-serializer.d.ts +96 -0
  55. package/dist/token-serializer.d.ts.map +1 -0
  56. package/dist/token-serializer.js +456 -0
  57. package/dist/token-serializer.js.map +1 -0
  58. package/dist/trig/formatter.d.ts +99 -0
  59. package/dist/trig/formatter.d.ts.map +1 -0
  60. package/dist/trig/formatter.js +418 -0
  61. package/dist/trig/formatter.js.map +1 -0
  62. package/dist/trig/serializer.d.ts +39 -0
  63. package/dist/trig/serializer.d.ts.map +1 -0
  64. package/dist/trig/serializer.js +170 -0
  65. package/dist/trig/serializer.js.map +1 -0
  66. package/dist/turtle/formatter.d.ts +56 -0
  67. package/dist/turtle/formatter.d.ts.map +1 -0
  68. package/dist/turtle/formatter.js +325 -0
  69. package/dist/turtle/formatter.js.map +1 -0
  70. package/dist/turtle/serializer.d.ts +87 -0
  71. package/dist/turtle/serializer.d.ts.map +1 -0
  72. package/dist/turtle/serializer.js +343 -0
  73. package/dist/turtle/serializer.js.map +1 -0
  74. package/dist/types.d.ts +323 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +14 -0
  77. package/dist/types.js.map +1 -0
  78. package/dist/utils.d.ts +157 -0
  79. package/dist/utils.d.ts.map +1 -0
  80. package/dist/utils.js +446 -0
  81. package/dist/utils.js.map +1 -0
  82. package/package.json +73 -0
@@ -0,0 +1,764 @@
1
+ import { RdfToken, SparqlLexer } from '@faubulous/mentor-rdf-parsers';
2
+ import { BaseTokenFormatter, } from '../base-token-formatter.js';
3
+ /**
4
+ * Formatter for SPARQL queries and updates.
5
+ *
6
+ * Uses the W3C-compliant SparqlLexer from mentor-rdf-parsers for tokenization.
7
+ *
8
+ * This formatter can:
9
+ * - Format SPARQL 1.1/1.2 queries and updates
10
+ * - Normalize keyword casing
11
+ * - Apply consistent indentation
12
+ * - Align triple patterns
13
+ * - Preserve comments
14
+ * - Preserve source newlines and blank lines
15
+ *
16
+ * @see https://www.w3.org/TR/sparql12-query/
17
+ */
18
+ export class SparqlFormatter extends BaseTokenFormatter {
19
+ lexer = new SparqlLexer();
20
+ // ========================================================================
21
+ // Public API
22
+ // ========================================================================
23
+ formatFromText(query, options) {
24
+ const opts = this.getOptions(options);
25
+ const result = this.lexer.tokenize(query);
26
+ if (result.errors.length > 0) {
27
+ return { output: query };
28
+ }
29
+ else {
30
+ return this.formatTokenStream(result.tokens, opts);
31
+ }
32
+ }
33
+ formatFromTokens(tokens, options) {
34
+ const opts = this.getOptions(options);
35
+ return this.formatTokenStream(tokens, opts);
36
+ }
37
+ // ========================================================================
38
+ // BaseTokenFormatter abstract implementations
39
+ // ========================================================================
40
+ getOptions(options) {
41
+ const base = this.mergeBaseOptions(options);
42
+ return {
43
+ ...base,
44
+ uppercaseKeywords: options?.uppercaseKeywords ?? true,
45
+ lowercaseKeywords: options?.lowercaseKeywords ?? false,
46
+ alignPatterns: options?.alignPatterns ?? true,
47
+ sameBraceLine: options?.sameBraceLine ?? true,
48
+ separateClauses: options?.separateClauses ?? true,
49
+ spaceBeforePunctuation: options?.spaceBeforePunctuation ?? false,
50
+ maxLineWidth: options?.maxLineWidth ?? base.maxLineWidth,
51
+ alignPredicates: options?.alignPredicates ?? base.alignPredicates,
52
+ alignObjects: options?.alignObjects ?? base.alignObjects,
53
+ objectListStyle: options?.objectListStyle ?? base.objectListStyle,
54
+ predicateListStyle: options?.predicateListStyle ?? base.predicateListStyle,
55
+ blankLinesBetweenSubjects: options?.blankLinesBetweenSubjects ?? base.blankLinesBetweenSubjects,
56
+ };
57
+ }
58
+ createContext(opts) {
59
+ return {
60
+ ...this.createBaseContext(),
61
+ opts,
62
+ justEndedPrefix: false,
63
+ inWhereBlock: false,
64
+ functionCallDepth: 0,
65
+ hasFromClause: false,
66
+ isAskQuery: false,
67
+ lastWasInlineBlock: false,
68
+ lastCommentPartIndex: 0,
69
+ lastCommentHadBlankLine: false,
70
+ inValuesClause: false,
71
+ valuesDepth: 0,
72
+ };
73
+ }
74
+ formatTokenValue(token, opts) {
75
+ if (this.isLowercaseOnly(token)) {
76
+ return token.image.toLowerCase();
77
+ }
78
+ if (this.isKeyword(token)) {
79
+ if (opts.lowercaseKeywords)
80
+ return token.image.toLowerCase();
81
+ if (opts.uppercaseKeywords)
82
+ return token.image.toUpperCase();
83
+ }
84
+ return token.image;
85
+ }
86
+ // ========================================================================
87
+ // Inline block calculation
88
+ // ========================================================================
89
+ /**
90
+ * Calculates the length of a block's content (from { to matching }).
91
+ * Returns -1 if the block should use multi-line formatting.
92
+ */
93
+ calculateBlockLength(tokens, startIndex, opts) {
94
+ let depth = 0;
95
+ let length = 0;
96
+ let needsSpace = false;
97
+ let lastTokenType = null;
98
+ let lastNonWsToken = null;
99
+ for (let i = startIndex; i < tokens.length; i++) {
100
+ const token = tokens[i];
101
+ if (token.tokenType === RdfToken.WS)
102
+ continue;
103
+ // Source has explicit newlines - preserve multi-line format
104
+ if (lastNonWsToken && token.startLine !== undefined && lastNonWsToken.endLine !== undefined) {
105
+ if (token.startLine > lastNonWsToken.endLine)
106
+ return -1;
107
+ }
108
+ if (token.tokenType === RdfToken.LCURLY) {
109
+ if (depth > 0) {
110
+ const nestedLength = this.calculateBlockLength(tokens, i + 1, opts);
111
+ if (nestedLength < 0)
112
+ return -1;
113
+ length += (needsSpace ? 1 : 0) + 1 + nestedLength + 1;
114
+ needsSpace = true;
115
+ let nestedDepth = 1;
116
+ i++;
117
+ while (i < tokens.length && nestedDepth > 0) {
118
+ if (tokens[i].tokenType === RdfToken.LCURLY)
119
+ nestedDepth++;
120
+ else if (tokens[i].tokenType === RdfToken.RCURLY)
121
+ nestedDepth--;
122
+ i++;
123
+ }
124
+ i--;
125
+ lastTokenType = RdfToken.RCURLY;
126
+ lastNonWsToken = tokens[i];
127
+ continue;
128
+ }
129
+ depth++;
130
+ length += (needsSpace ? 1 : 0) + 1;
131
+ needsSpace = false;
132
+ lastTokenType = token.tokenType;
133
+ lastNonWsToken = token;
134
+ continue;
135
+ }
136
+ if (token.tokenType === RdfToken.RCURLY) {
137
+ depth--;
138
+ if (depth < 0) {
139
+ length += (needsSpace ? 1 : 0) + 1;
140
+ return length;
141
+ }
142
+ length += (needsSpace ? 1 : 0) + 1;
143
+ needsSpace = true;
144
+ lastTokenType = token.tokenType;
145
+ lastNonWsToken = token;
146
+ continue;
147
+ }
148
+ if (token.tokenType === RdfToken.PERIOD || token.tokenType === RdfToken.SEMICOLON) {
149
+ length += token.image.length;
150
+ needsSpace = true;
151
+ lastTokenType = token.tokenType;
152
+ lastNonWsToken = token;
153
+ continue;
154
+ }
155
+ if (token.tokenType === RdfToken.LPARENT || token.tokenType === RdfToken.LBRACKET) {
156
+ const lastWasFunction = lastTokenType && this.isFunctionKeyword({ tokenType: lastTokenType });
157
+ length += (needsSpace && !lastWasFunction ? 1 : 0) + token.image.length;
158
+ needsSpace = false;
159
+ lastTokenType = token.tokenType;
160
+ lastNonWsToken = token;
161
+ continue;
162
+ }
163
+ if (token.tokenType === RdfToken.RPARENT || token.tokenType === RdfToken.RBRACKET) {
164
+ length += token.image.length;
165
+ needsSpace = true;
166
+ lastTokenType = token.tokenType;
167
+ lastNonWsToken = token;
168
+ continue;
169
+ }
170
+ if (token.tokenType === RdfToken.COMMA) {
171
+ length += token.image.length;
172
+ needsSpace = true;
173
+ lastTokenType = token.tokenType;
174
+ lastNonWsToken = token;
175
+ continue;
176
+ }
177
+ const tokenLength = this.formatTokenValue(token, opts).length;
178
+ const isOpenParen = lastTokenType === RdfToken.LPARENT || lastTokenType === RdfToken.LBRACKET;
179
+ length += (needsSpace && !isOpenParen ? 1 : 0) + tokenLength;
180
+ needsSpace = true;
181
+ lastTokenType = token.tokenType;
182
+ lastNonWsToken = token;
183
+ }
184
+ return -1;
185
+ }
186
+ shouldBlockBeInline(ctx, tokens, startIndex) {
187
+ if (ctx.opts.maxLineWidth <= 0)
188
+ return false;
189
+ const blockLength = this.calculateBlockLength(tokens, startIndex, ctx.opts);
190
+ if (blockLength < 0)
191
+ return false;
192
+ return ctx.currentLineLength + blockLength <= ctx.opts.maxLineWidth;
193
+ }
194
+ // ========================================================================
195
+ // SPARQL-specific token handlers
196
+ // ========================================================================
197
+ handleOpenBrace(ctx, tokens, startIndex) {
198
+ const shouldBeInline = this.shouldBlockBeInline(ctx, tokens, startIndex);
199
+ const le = ctx.opts.lineEnd;
200
+ const ind = ctx.opts.indent;
201
+ // When a blank line or newline was detected in the source *before* the
202
+ // opening brace, we need to honour it here so that the blank line
203
+ // appears before the brace, not after it. This mirrors the behaviour
204
+ // in handleSparqlTokenSpacing but is specialised for structural
205
+ // curly-brace tokens which bypass that helper.
206
+ if (ctx.needsNewline) {
207
+ if (!ctx.lastWasNewline)
208
+ this.addPart(ctx, le, le, true);
209
+ if (ctx.needsBlankLine) {
210
+ this.addPart(ctx, le, le, true);
211
+ ctx.needsBlankLine = false;
212
+ }
213
+ this.addPart(ctx, this.getIndent(ctx.indentLevel, ind), le);
214
+ ctx.lastWasNewline = false;
215
+ ctx.needsNewline = false;
216
+ }
217
+ else if (ctx.needsSpace && ctx.parts.length > 0 && ctx.opts.sameBraceLine) {
218
+ this.addPart(ctx, ' ', le);
219
+ }
220
+ else if (!ctx.opts.sameBraceLine) {
221
+ this.addPart(ctx, le + this.getIndent(ctx.indentLevel, ind), le, true);
222
+ }
223
+ this.addPart(ctx, '{', le);
224
+ ctx.lastWasNewline = false;
225
+ if (shouldBeInline) {
226
+ this.pushScope(ctx, 'curly', true, false);
227
+ ctx.needsNewline = false;
228
+ ctx.needsSpace = true;
229
+ }
230
+ else {
231
+ this.pushScope(ctx, 'curly', false, false);
232
+ ctx.needsNewline = ctx.opts.prettyPrint;
233
+ ctx.needsSpace = false;
234
+ }
235
+ ctx.inPrefix = false;
236
+ ctx.triplePosition = 0;
237
+ ctx.lastSubject = null;
238
+ // Track the VALUES block depth so we can clear inValuesClause
239
+ // when the matching close brace is encountered.
240
+ if (ctx.inValuesClause && ctx.valuesDepth === 0) {
241
+ ctx.valuesDepth = ctx.indentLevel;
242
+ }
243
+ // The source-originated newline/blank-line information has now been
244
+ // consumed for this brace, so it should not affect the next token.
245
+ ctx.sourceNewline = false;
246
+ }
247
+ handleCloseBrace(ctx) {
248
+ const scope = this.currentScope(ctx);
249
+ const isInline = scope?.isInline ?? false;
250
+ const le = ctx.opts.lineEnd;
251
+ const ind = ctx.opts.indent;
252
+ this.popScope(ctx);
253
+ if (isInline) {
254
+ this.addPart(ctx, ' }', le);
255
+ }
256
+ else if (ctx.opts.prettyPrint) {
257
+ this.addPart(ctx, le + this.getIndent(ctx.indentLevel, ind), le, true);
258
+ this.addPart(ctx, '}', le);
259
+ }
260
+ else {
261
+ this.addPart(ctx, '}', le);
262
+ }
263
+ ctx.lastWasNewline = false;
264
+ ctx.needsNewline = ctx.opts.prettyPrint && !isInline;
265
+ ctx.needsSpace = isInline;
266
+ // Only clear inWhereBlock when exiting the outermost WHERE block
267
+ // (i.e. indent level returns to 0). Inner blocks like VALUES { }
268
+ // or FILTER NOT EXISTS { } should not clear this flag.
269
+ if (ctx.indentLevel === 0) {
270
+ ctx.inWhereBlock = false;
271
+ }
272
+ ctx.triplePosition = 0;
273
+ ctx.inlineStatement = false;
274
+ ctx.lastSubject = null;
275
+ ctx.lastWasInlineBlock = isInline;
276
+ // Clear VALUES clause tracking when matching brace closes.
277
+ if (ctx.inValuesClause && ctx.indentLevel < ctx.valuesDepth) {
278
+ ctx.inValuesClause = false;
279
+ ctx.valuesDepth = 0;
280
+ }
281
+ }
282
+ handleSparqlPeriod(ctx, nextToken) {
283
+ const le = ctx.opts.lineEnd;
284
+ if (ctx.opts.spaceBeforePunctuation && !ctx.inPrefix && ctx.parts.length > 0) {
285
+ this.addPart(ctx, ' ', le);
286
+ }
287
+ this.addPart(ctx, '.', le);
288
+ if (ctx.inPrefix) {
289
+ ctx.needsNewline = ctx.opts.prettyPrint;
290
+ const nextIsPrefix = nextToken && (nextToken.tokenType === RdfToken.PREFIX || nextToken.tokenType === RdfToken.BASE);
291
+ if (!nextIsPrefix && nextToken) {
292
+ ctx.needsNewline = ctx.opts.prettyPrint;
293
+ }
294
+ }
295
+ else if (ctx.indentLevel > 0) {
296
+ ctx.needsNewline = ctx.opts.prettyPrint;
297
+ }
298
+ ctx.needsSpace = true;
299
+ ctx.inPrefix = false;
300
+ ctx.triplePosition = 0;
301
+ ctx.inlineStatement = false;
302
+ }
303
+ handleSparqlSemicolon(ctx) {
304
+ const le = ctx.opts.lineEnd;
305
+ const ind = ctx.opts.indent;
306
+ if (ctx.opts.spaceBeforePunctuation && ctx.parts.length > 0) {
307
+ this.addPart(ctx, ' ', le);
308
+ }
309
+ this.addPart(ctx, ';', le);
310
+ // Inline statement takes priority: source had it on one line and it fits.
311
+ if (ctx.inlineStatement) {
312
+ ctx.needsNewline = false;
313
+ ctx.needsSpace = true;
314
+ ctx.triplePosition = 1;
315
+ return;
316
+ }
317
+ const shouldMultiLine = ctx.opts.predicateListStyle === 'multi-line' ||
318
+ (ctx.opts.predicateListStyle === 'first-same-line' && ctx.indentLevel > 0);
319
+ if (ctx.opts.predicateListStyle === 'single-line') {
320
+ ctx.needsNewline = false;
321
+ ctx.needsSpace = true;
322
+ }
323
+ else if (shouldMultiLine && ctx.opts.prettyPrint) {
324
+ const extra = this.inBracketScope(ctx) ? '' : ind;
325
+ this.addPart(ctx, le + this.getIndent(ctx.indentLevel, ind) + extra, le, true);
326
+ ctx.lastWasNewline = true;
327
+ ctx.needsNewline = false;
328
+ ctx.needsSpace = false;
329
+ }
330
+ else if (ctx.opts.prettyPrint) {
331
+ const extra = this.inBracketScope(ctx) ? '' : ind;
332
+ this.addPart(ctx, le + this.getIndent(ctx.indentLevel, ind) + extra, le, true);
333
+ ctx.lastWasNewline = true;
334
+ ctx.needsNewline = false;
335
+ ctx.needsSpace = false;
336
+ }
337
+ else {
338
+ ctx.needsNewline = false;
339
+ ctx.needsSpace = true;
340
+ }
341
+ ctx.triplePosition = 1;
342
+ }
343
+ handleSparqlComma(ctx, nextToken) {
344
+ const le = ctx.opts.lineEnd;
345
+ this.addPart(ctx, ',', le);
346
+ if (ctx.opts.objectListStyle === 'multi-line') {
347
+ ctx.needsNewline = ctx.opts.prettyPrint && ctx.indentLevel > 0;
348
+ ctx.needsSpace = !ctx.needsNewline;
349
+ }
350
+ else if (ctx.opts.objectListStyle === 'auto' && ctx.opts.maxLineWidth > 0) {
351
+ const nextObjLen = nextToken ? nextToken.image.length + 1 : 0;
352
+ if (this.shouldWrap(ctx, nextObjLen, ctx.opts.maxLineWidth)) {
353
+ ctx.needsNewline = ctx.opts.prettyPrint;
354
+ ctx.needsSpace = !ctx.needsNewline;
355
+ }
356
+ else {
357
+ ctx.needsSpace = true;
358
+ }
359
+ }
360
+ else {
361
+ ctx.needsSpace = true;
362
+ }
363
+ }
364
+ handleSparqlOpenParen(ctx, token, tokens, index) {
365
+ const isLParen = token.tokenType === RdfToken.LPARENT;
366
+ const isLBracket = token.tokenType === RdfToken.LBRACKET;
367
+ const le = ctx.opts.lineEnd;
368
+ const ind = ctx.opts.indent;
369
+ if (isLParen) {
370
+ const lastWasFunction = ctx.lastNonWsToken && this.isFunctionKeyword(ctx.lastNonWsToken);
371
+ if (lastWasFunction)
372
+ ctx.functionCallDepth++;
373
+ }
374
+ const lastWasFunction = ctx.lastNonWsToken && this.isFunctionKeyword(ctx.lastNonWsToken);
375
+ if (ctx.needsNewline && ctx.functionCallDepth === 0 && !lastWasFunction) {
376
+ this.addPart(ctx, le + this.getIndent(ctx.indentLevel, ind), le, true);
377
+ ctx.lastWasNewline = true;
378
+ ctx.needsNewline = false;
379
+ }
380
+ else if (ctx.needsSpace && ctx.parts.length > 0) {
381
+ if (!isLParen || !lastWasFunction) {
382
+ this.addPart(ctx, ' ', le);
383
+ }
384
+ }
385
+ ctx.needsNewline = false;
386
+ this.addPart(ctx, token.image, le);
387
+ ctx.needsSpace = false;
388
+ // Square bracket [ ] → push bracket scope (for blank node property lists)
389
+ if (isLBracket) {
390
+ this.pushScope(ctx, 'bracket', false, false);
391
+ }
392
+ // Multi-line parenthesis → push paren scope for proper indent
393
+ if (isLParen && this.isParenBlockMultiLine(tokens, index)) {
394
+ this.pushScope(ctx, 'paren', false, true);
395
+ }
396
+ }
397
+ handleSparqlCloseParen(ctx, token) {
398
+ const le = ctx.opts.lineEnd;
399
+ const ind = ctx.opts.indent;
400
+ if (token.tokenType === RdfToken.RPARENT && ctx.functionCallDepth > 0) {
401
+ ctx.functionCallDepth--;
402
+ }
403
+ // Closing bracket ] for blank-node property lists
404
+ if (token.tokenType === RdfToken.RBRACKET) {
405
+ const scope = this.currentScope(ctx);
406
+ if (scope?.type === 'bracket') {
407
+ // Pop any trailing newline+indent from a semicolon
408
+ if (ctx.lastWasNewline && ctx.parts.length > 0) {
409
+ const lastPart = ctx.parts[ctx.parts.length - 1];
410
+ if (lastPart.includes(le) || /^\s+$/.test(lastPart)) {
411
+ ctx.parts.pop();
412
+ }
413
+ }
414
+ this.popScope(ctx);
415
+ if (ctx.opts.prettyPrint) {
416
+ this.addPart(ctx, le + this.getIndent(ctx.indentLevel, ind), le, true);
417
+ }
418
+ this.addPart(ctx, token.image, le);
419
+ ctx.lastWasNewline = false;
420
+ ctx.needsSpace = true;
421
+ ctx.needsNewline = false;
422
+ return;
423
+ }
424
+ }
425
+ // Closing multi-line paren
426
+ if (token.tokenType === RdfToken.RPARENT) {
427
+ const scope = this.currentScope(ctx);
428
+ if (scope?.type === 'paren' && scope.isMultiLine) {
429
+ this.popScope(ctx);
430
+ if (ctx.opts.prettyPrint) {
431
+ this.addPart(ctx, le + this.getIndent(ctx.indentLevel, ind), le, true);
432
+ }
433
+ this.addPart(ctx, token.image, le);
434
+ ctx.lastWasNewline = false;
435
+ ctx.needsSpace = true;
436
+ ctx.needsNewline = false;
437
+ return;
438
+ }
439
+ }
440
+ this.addPart(ctx, token.image, le);
441
+ ctx.needsSpace = true;
442
+ ctx.needsNewline = false;
443
+ }
444
+ // ========================================================================
445
+ // Comment handling
446
+ // ========================================================================
447
+ handleGroupComment(ctx, comment) {
448
+ const le = ctx.opts.lineEnd;
449
+ const ind = ctx.opts.indent;
450
+ if (ctx.parts.length > 0) {
451
+ if (ctx.lastNonWsToken && comment.startLine !== undefined &&
452
+ ctx.lastNonWsToken.endLine !== undefined &&
453
+ comment.startLine > ctx.lastNonWsToken.endLine) {
454
+ const lineGap = comment.startLine - ctx.lastNonWsToken.endLine;
455
+ if (lineGap > 1) {
456
+ if (!ctx.lastWasNewline)
457
+ this.addPart(ctx, le, le, true);
458
+ this.addPart(ctx, le, le, true);
459
+ this.addPart(ctx, this.getIndent(ctx.indentLevel, ind), le);
460
+ }
461
+ else {
462
+ this.addPart(ctx, le + this.getIndent(ctx.indentLevel, ind), le, true);
463
+ }
464
+ }
465
+ else {
466
+ this.addPart(ctx, ' ', le);
467
+ }
468
+ }
469
+ this.addPart(ctx, comment.image, le);
470
+ ctx.lastWasNewline = false;
471
+ ctx.needsNewline = true;
472
+ ctx.needsSpace = false;
473
+ ctx.lastWasComment = true;
474
+ ctx.lastNonWsToken = comment;
475
+ }
476
+ handleInlineComment(ctx, token) {
477
+ const le = ctx.opts.lineEnd;
478
+ const ind = ctx.opts.indent;
479
+ // Track where the first comment in a consecutive series starts in parts[],
480
+ // so that blank-line-between-subjects can be retroactively placed before the comment.
481
+ if (!ctx.lastWasComment) {
482
+ ctx.lastCommentPartIndex = ctx.parts.length;
483
+ ctx.lastCommentHadBlankLine = false;
484
+ }
485
+ if (ctx.needsNewline) {
486
+ if (!ctx.lastWasNewline)
487
+ this.addPart(ctx, le, le, true);
488
+ if (ctx.needsBlankLine) {
489
+ this.addPart(ctx, le, le, true);
490
+ ctx.needsBlankLine = false;
491
+ ctx.lastCommentHadBlankLine = true;
492
+ }
493
+ this.addPart(ctx, this.getIndent(ctx.indentLevel, ind), le);
494
+ ctx.lastWasNewline = false;
495
+ ctx.needsNewline = false;
496
+ }
497
+ else if (ctx.needsSpace && ctx.parts.length > 0) {
498
+ this.addPart(ctx, ' ', le);
499
+ }
500
+ this.addPart(ctx, token.image, le);
501
+ ctx.lastWasNewline = false;
502
+ ctx.needsNewline = true;
503
+ ctx.needsSpace = false;
504
+ ctx.lastWasComment = true;
505
+ }
506
+ // ========================================================================
507
+ // State tracking
508
+ // ========================================================================
509
+ updateClauseState(ctx, token) {
510
+ if (token.tokenType === RdfToken.PREFIX || token.tokenType === RdfToken.BASE)
511
+ ctx.inPrefix = true;
512
+ if (token.tokenType === RdfToken.WHERE)
513
+ ctx.inWhereBlock = true;
514
+ if (token.tokenType === RdfToken.ASK)
515
+ ctx.isAskQuery = true;
516
+ if (token.tokenType === RdfToken.FROM)
517
+ ctx.hasFromClause = true;
518
+ if (token.tokenType === RdfToken.VALUES)
519
+ ctx.inValuesClause = true;
520
+ }
521
+ handleClauseSeparation(ctx, token) {
522
+ const justOpenedBrace = ctx.lastNonWsToken?.tokenType === RdfToken.LCURLY;
523
+ const justClosedBrace = ctx.lastNonWsToken?.tokenType === RdfToken.RCURLY;
524
+ const isTopLevel = ctx.indentLevel === 0;
525
+ if (ctx.opts.separateClauses &&
526
+ this.isMajorClauseKeyword(token) &&
527
+ ctx.parts.length > 0 &&
528
+ !ctx.inPrefix &&
529
+ !ctx.justEndedPrefix &&
530
+ !ctx.lastWasComment &&
531
+ !ctx.lastWasInlineBlock &&
532
+ !justOpenedBrace &&
533
+ !justClosedBrace &&
534
+ !isTopLevel) {
535
+ ctx.needsBlankLine = true;
536
+ ctx.needsNewline = true;
537
+ }
538
+ if (ctx.opts.separateClauses &&
539
+ this.isNewlineKeyword(token) &&
540
+ ctx.parts.length > 0 &&
541
+ !ctx.inPrefix) {
542
+ if (token.tokenType === RdfToken.WHERE && ctx.isAskQuery && !ctx.hasFromClause) {
543
+ ctx.needsNewline = false;
544
+ ctx.needsSpace = true;
545
+ }
546
+ else {
547
+ ctx.needsNewline = true;
548
+ }
549
+ }
550
+ if (token.tokenType !== RdfToken.PREFIX && token.tokenType !== RdfToken.BASE) {
551
+ ctx.justEndedPrefix = false;
552
+ }
553
+ }
554
+ handleTriplePosition(ctx, token, tokens, index, prevWasComment = false) {
555
+ if (!ctx.inWhereBlock || ctx.indentLevel <= 0 || ctx.functionCallDepth > 0 || ctx.inValuesClause) {
556
+ return;
557
+ }
558
+ if (this.isTermToken(token)) {
559
+ if (ctx.triplePosition === 0) {
560
+ if (ctx.opts.blankLinesBetweenSubjects && ctx.lastSubject !== null && token.image !== ctx.lastSubject) {
561
+ if (!ctx.needsBlankLine) {
562
+ if (prevWasComment && !ctx.lastCommentHadBlankLine) {
563
+ // A comment precedes this new subject. Insert the blank line
564
+ // *before* the comment block so the comment stays attached to
565
+ // its associated statement, not floating after a blank line.
566
+ ctx.parts.splice(ctx.lastCommentPartIndex, 0, ctx.opts.lineEnd);
567
+ }
568
+ else if (!prevWasComment) {
569
+ ctx.needsBlankLine = true;
570
+ }
571
+ }
572
+ ctx.needsNewline = true;
573
+ }
574
+ ctx.lastSubject = token.image;
575
+ this.detectInlineStatement(ctx, tokens, index, ctx.opts.indent, ctx.opts.maxLineWidth);
576
+ }
577
+ ctx.triplePosition++;
578
+ if (ctx.triplePosition > 2)
579
+ ctx.triplePosition = 2;
580
+ }
581
+ }
582
+ // ========================================================================
583
+ // Spacing & wrapping
584
+ // ========================================================================
585
+ handleLineWrapping(ctx, value) {
586
+ const le = ctx.opts.lineEnd;
587
+ const ind = ctx.opts.indent;
588
+ if (ctx.opts.maxLineWidth > 0 && !ctx.needsNewline && ctx.needsSpace) {
589
+ const spaceNeeded = ctx.parts.length > 0 && ctx.lastNonWsToken &&
590
+ !this.isOpeningBracket(ctx.lastNonWsToken) ? 1 : 0;
591
+ if (this.shouldWrap(ctx, spaceNeeded + value.length, ctx.opts.maxLineWidth)) {
592
+ this.addPart(ctx, le + this.getIndent(ctx.indentLevel, ind), le, true);
593
+ ctx.lastWasNewline = true;
594
+ ctx.needsNewline = false;
595
+ ctx.needsSpace = false;
596
+ }
597
+ }
598
+ }
599
+ handleSparqlTokenSpacing(ctx, token) {
600
+ const le = ctx.opts.lineEnd;
601
+ const ind = ctx.opts.indent;
602
+ const isDatatypeContext = token.tokenType === RdfToken.DCARET ||
603
+ ctx.lastNonWsToken?.tokenType === RdfToken.DCARET;
604
+ const inFunctionCall = ctx.functionCallDepth > 0;
605
+ // Only suppress newlines in function calls when they are NOT from source.
606
+ // Source newlines (detected from token positions) should always be preserved.
607
+ const shouldAvoidNewline = isDatatypeContext || (inFunctionCall && !ctx.sourceNewline);
608
+ if (ctx.needsNewline && !shouldAvoidNewline) {
609
+ if (!ctx.lastWasNewline) {
610
+ this.addPart(ctx, le, le, true);
611
+ }
612
+ if (ctx.needsBlankLine) {
613
+ this.addPart(ctx, le, le, true);
614
+ ctx.needsBlankLine = false;
615
+ }
616
+ this.addPart(ctx, this.getIndent(ctx.indentLevel, ind), le);
617
+ ctx.lastWasNewline = false;
618
+ ctx.needsNewline = false;
619
+ }
620
+ else if (ctx.needsSpace && ctx.parts.length > 0) {
621
+ if (ctx.lastNonWsToken && !this.isOpeningBracket(ctx.lastNonWsToken)) {
622
+ const isOpenParen = token.tokenType === RdfToken.LPARENT;
623
+ const lastWasFunction = ctx.lastNonWsToken && this.isFunctionKeyword(ctx.lastNonWsToken);
624
+ if (!(isOpenParen && lastWasFunction) && !isDatatypeContext) {
625
+ this.addPart(ctx, ' ', le);
626
+ }
627
+ }
628
+ ctx.lastWasNewline = false;
629
+ }
630
+ else {
631
+ ctx.lastWasNewline = false;
632
+ }
633
+ if (shouldAvoidNewline) {
634
+ ctx.needsNewline = false;
635
+ ctx.needsBlankLine = false;
636
+ }
637
+ ctx.sourceNewline = false;
638
+ }
639
+ detectBlankLines(ctx, token) {
640
+ if (ctx.lastNonWsToken && token.startLine !== undefined && ctx.lastNonWsToken.endLine !== undefined) {
641
+ const lineGap = token.startLine - ctx.lastNonWsToken.endLine;
642
+ if (lineGap > 1) {
643
+ ctx.needsBlankLine = true;
644
+ ctx.needsNewline = true;
645
+ ctx.sourceNewline = true;
646
+ }
647
+ else if (lineGap === 1 && !ctx.lastWasNewline) {
648
+ ctx.needsNewline = true;
649
+ ctx.sourceNewline = true;
650
+ }
651
+ }
652
+ }
653
+ handleSparqlPrefixIri(ctx, value) {
654
+ const le = ctx.opts.lineEnd;
655
+ const ind = ctx.opts.indent;
656
+ if (ctx.needsNewline) {
657
+ this.addPart(ctx, le + this.getIndent(ctx.indentLevel, ind), le, true);
658
+ ctx.lastWasNewline = true;
659
+ ctx.needsNewline = false;
660
+ }
661
+ else if (ctx.needsSpace && ctx.parts.length > 0) {
662
+ if (ctx.lastNonWsToken && !this.isOpeningBracket(ctx.lastNonWsToken)) {
663
+ this.addPart(ctx, ' ', le);
664
+ }
665
+ }
666
+ this.addPart(ctx, value, le);
667
+ ctx.needsSpace = false;
668
+ ctx.needsNewline = ctx.opts.prettyPrint;
669
+ ctx.inPrefix = false;
670
+ ctx.justEndedPrefix = true;
671
+ }
672
+ formatTokenStream(tokens, options) {
673
+ const ctx = this.createContext(options);
674
+ const le = options.lineEnd;
675
+ for (let i = 0; i < tokens.length; i++) {
676
+ const token = tokens[i];
677
+ const nextToken = tokens[i + 1];
678
+ // Skip whitespace
679
+ if (token.tokenType === RdfToken.WS) {
680
+ ctx.lastToken = token;
681
+ continue;
682
+ }
683
+ // Detect source blank lines
684
+ this.detectBlankLines(ctx, token);
685
+ // Handle inline comments
686
+ if (token.tokenType === RdfToken.COMMENT) {
687
+ this.handleInlineComment(ctx, token);
688
+ ctx.lastToken = token;
689
+ ctx.lastNonWsToken = token;
690
+ continue;
691
+ }
692
+ const value = this.formatTokenValue(token, options);
693
+ // State tracking
694
+ this.updateClauseState(ctx, token);
695
+ this.handleClauseSeparation(ctx, token);
696
+ const prevWasComment = ctx.lastWasComment;
697
+ ctx.lastWasComment = false;
698
+ ctx.lastWasInlineBlock = false;
699
+ // ── Structural tokens ──────────────────────────────────────
700
+ if (token.tokenType === RdfToken.LCURLY) {
701
+ this.handleOpenBrace(ctx, tokens, i + 1);
702
+ ctx.lastToken = token;
703
+ ctx.lastNonWsToken = token;
704
+ continue;
705
+ }
706
+ if (token.tokenType === RdfToken.RCURLY) {
707
+ this.handleCloseBrace(ctx);
708
+ ctx.lastToken = token;
709
+ ctx.lastNonWsToken = token;
710
+ continue;
711
+ }
712
+ if (token.tokenType === RdfToken.PERIOD) {
713
+ this.handleSparqlPeriod(ctx, nextToken);
714
+ ctx.lastToken = token;
715
+ ctx.lastNonWsToken = token;
716
+ continue;
717
+ }
718
+ if (token.tokenType === RdfToken.SEMICOLON) {
719
+ this.handleSparqlSemicolon(ctx);
720
+ ctx.lastToken = token;
721
+ ctx.lastNonWsToken = token;
722
+ continue;
723
+ }
724
+ if (token.tokenType === RdfToken.COMMA) {
725
+ this.handleSparqlComma(ctx, nextToken);
726
+ ctx.lastToken = token;
727
+ ctx.lastNonWsToken = token;
728
+ continue;
729
+ }
730
+ if (token.tokenType === RdfToken.LPARENT || token.tokenType === RdfToken.LBRACKET) {
731
+ this.handleSparqlOpenParen(ctx, token, tokens, i);
732
+ ctx.lastToken = token;
733
+ ctx.lastNonWsToken = token;
734
+ continue;
735
+ }
736
+ if (token.tokenType === RdfToken.RPARENT || token.tokenType === RdfToken.RBRACKET) {
737
+ this.handleSparqlCloseParen(ctx, token);
738
+ ctx.lastToken = token;
739
+ ctx.lastNonWsToken = token;
740
+ continue;
741
+ }
742
+ // PREFIX/BASE IRI completion
743
+ if (ctx.inPrefix && (token.tokenType === RdfToken.IRIREF || token.tokenType === RdfToken.IRIREF_ABS)) {
744
+ this.handleSparqlPrefixIri(ctx, value);
745
+ ctx.lastToken = token;
746
+ ctx.lastNonWsToken = token;
747
+ continue;
748
+ }
749
+ // Triple position tracking
750
+ this.handleTriplePosition(ctx, token, tokens, i, prevWasComment);
751
+ // Line wrapping
752
+ this.handleLineWrapping(ctx, value);
753
+ // Spacing
754
+ this.handleSparqlTokenSpacing(ctx, token);
755
+ // Output the token
756
+ this.addPart(ctx, value, le);
757
+ ctx.needsSpace = true;
758
+ ctx.lastToken = token;
759
+ ctx.lastNonWsToken = token;
760
+ }
761
+ return { output: ctx.parts.join('').trim() };
762
+ }
763
+ }
764
+ //# sourceMappingURL=formatter.js.map