prettier-plugin-wolfram 0.7.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 (45) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +290 -0
  3. package/bin/prettier-wolfram.js +55 -0
  4. package/package.json +58 -0
  5. package/src/index.js +80 -0
  6. package/src/options.js +206 -0
  7. package/src/parser/adapter.js +690 -0
  8. package/src/parser/cstEqual.js +18 -0
  9. package/src/parser/index.js +29 -0
  10. package/src/parser/operators.js +35 -0
  11. package/src/parser/position.js +62 -0
  12. package/src/parser/tree-sitter-wolfram.wasm +0 -0
  13. package/src/range.js +98 -0
  14. package/src/rules/index.js +57 -0
  15. package/src/rules/line-width.js +129 -0
  16. package/src/rules/newlines-between-definitions.js +103 -0
  17. package/src/rules/no-bare-symbol-set.js +19 -0
  18. package/src/rules/no-dynamic-module-leak.js +74 -0
  19. package/src/rules/no-general-infix-function.js +52 -0
  20. package/src/rules/no-shadowed-pattern.js +71 -0
  21. package/src/rules/no-unused-module-var.js +84 -0
  22. package/src/rules/prefer-rule-delayed.js +59 -0
  23. package/src/rules/spacing-commas.js +64 -0
  24. package/src/rules/spacing-operators.js +87 -0
  25. package/src/translator/commentSpacing.js +51 -0
  26. package/src/translator/docComments.js +89 -0
  27. package/src/translator/index.js +98 -0
  28. package/src/translator/nodes/binary.js +205 -0
  29. package/src/translator/nodes/call.js +254 -0
  30. package/src/translator/nodes/compound.js +117 -0
  31. package/src/translator/nodes/container.js +194 -0
  32. package/src/translator/nodes/group.js +159 -0
  33. package/src/translator/nodes/infix.js +408 -0
  34. package/src/translator/nodes/leaf.js +605 -0
  35. package/src/translator/nodes/postfix.js +29 -0
  36. package/src/translator/nodes/prefix.js +27 -0
  37. package/src/translator/nodes/ternary.js +82 -0
  38. package/src/translator/ruleAlignment.js +133 -0
  39. package/src/translator/sourceLines.js +49 -0
  40. package/src/translator/sourcePreservation.js +22 -0
  41. package/src/translator/specialForms.js +665 -0
  42. package/src/utils/codeSpacing.js +420 -0
  43. package/src/utils/cstErrors.js +36 -0
  44. package/src/utils/offsets.js +132 -0
  45. package/src/utils/operatorSpacing.js +49 -0
@@ -0,0 +1,665 @@
1
+ // src/translator/specialForms.js
2
+ import { doc } from "prettier";
3
+ const { builders } = doc;
4
+ import {
5
+ isTrivia,
6
+ singleLineStringLiteralRunDoc,
7
+ stringLineIndentDepth,
8
+ stringLiteralRunDocs,
9
+ } from "./nodes/leaf.js";
10
+ import {
11
+ argPathEntries,
12
+ hasDirectCommentArg,
13
+ printCall,
14
+ printedArgs,
15
+ } from "./nodes/call.js";
16
+ import { normalizeWolframOptions } from "../options.js";
17
+ const { conditionalGroup, group, indent, hardline, line, softline, join } =
18
+ builders;
19
+ const { willBreak } = doc.utils;
20
+
21
+ const DEFAULT_CONDITION_FIRST_FUNCTIONS = "If,Switch";
22
+ const DEFAULT_BLOCK_STRUCTURE_FUNCTIONS = "Module,With,Block,DynamicModule";
23
+ const DEFAULT_CASE_STRUCTURE_FUNCTIONS = "Which";
24
+
25
+ const PATTERN_BLANK_OPS = new Set([
26
+ "PatternBlank",
27
+ "PatternBlankSequence",
28
+ "PatternBlankNullSequence",
29
+ ]);
30
+
31
+ const BLANK_OPS = new Set(["Blank", "BlankSequence", "BlankNullSequence"]);
32
+
33
+ // Build Sets from comma-separated option strings
34
+ export function buildDispatchSets(options = {}) {
35
+ options = normalizeWolframOptions(options);
36
+ const toOptionString = (name, fallback) => {
37
+ const value = options[name];
38
+ return value == null ? fallback : String(value);
39
+ };
40
+ const toSet = (str) =>
41
+ new Set(
42
+ str
43
+ .split(",")
44
+ .map((s) => s.trim())
45
+ .filter(Boolean),
46
+ );
47
+ return {
48
+ conditionFirst: toSet(
49
+ toOptionString(
50
+ "wolframConditionFirstFunctions",
51
+ DEFAULT_CONDITION_FIRST_FUNCTIONS,
52
+ ),
53
+ ),
54
+ blockStructure: toSet(
55
+ toOptionString(
56
+ "wolframBlockStructureFunctions",
57
+ DEFAULT_BLOCK_STRUCTURE_FUNCTIONS,
58
+ ),
59
+ ),
60
+ caseStructure: toSet(
61
+ toOptionString(
62
+ "wolframCaseStructureFunctions",
63
+ DEFAULT_CASE_STRUCTURE_FUNCTIONS,
64
+ ),
65
+ ),
66
+ };
67
+ }
68
+
69
+ function getHeadName(node) {
70
+ if (node.head?.type === "LeafNode" && node.head.kind === "Symbol") {
71
+ return node.head.value;
72
+ }
73
+ return null;
74
+ }
75
+
76
+ function containsComment(node) {
77
+ if (!node) return false;
78
+ if (node.type === "LeafNode" && node.kind === "Token`Comment") return true;
79
+ return (node.children ?? []).some((child) => containsComment(child));
80
+ }
81
+
82
+ function isStringJoinCall(node) {
83
+ return node?.type === "CallNode" && getHeadName(node) === "StringJoin";
84
+ }
85
+
86
+ function isStringJoinInfix(node) {
87
+ return node?.type === "InfixNode" && node.op === "StringJoin";
88
+ }
89
+
90
+ function isStringJoinForm(node) {
91
+ return isStringJoinCall(node) || isStringJoinInfix(node);
92
+ }
93
+
94
+ function isCommaToken(node) {
95
+ return node?.type === "LeafNode" && node.kind === "Token`Comma";
96
+ }
97
+
98
+ function isStringJoinOperatorToken(node) {
99
+ return node?.type === "LeafNode" && node.kind === "Token`LessGreater";
100
+ }
101
+
102
+ function stringJoinPathEntries(node) {
103
+ if (isStringJoinCall(node)) return argPathEntries(node);
104
+
105
+ return (node.children ?? []).reduce((entries, child, index) => {
106
+ if (isTrivia(child) || isStringJoinOperatorToken(child)) return entries;
107
+ entries.push({ node: child, path: ["children", index] });
108
+ return entries;
109
+ }, []);
110
+ }
111
+
112
+ function hasFollowingCommaSibling(path) {
113
+ let child = path?.getValue?.();
114
+
115
+ for (const ancestor of path?.ancestors ?? []) {
116
+ if (ancestor?.type === "InfixNode" && ancestor.op === "Comma") {
117
+ const index = ancestor.children?.indexOf(child) ?? -1;
118
+ if (index !== -1) {
119
+ return ancestor.children
120
+ .slice(index + 1)
121
+ .some((node) => !isTrivia(node) && !isCommaToken(node));
122
+ }
123
+ }
124
+ child = ancestor;
125
+ }
126
+
127
+ return false;
128
+ }
129
+
130
+ function flattenStringJoinParts(path, print, node, basePath = []) {
131
+ const parts = [];
132
+
133
+ for (const entry of stringJoinPathEntries(node)) {
134
+ if (
135
+ entry.node?.type === "LeafNode" &&
136
+ entry.node.kind === "Token`Comma"
137
+ ) {
138
+ continue;
139
+ }
140
+
141
+ const entryPath = [...basePath, ...entry.path];
142
+ if (isStringJoinForm(entry.node)) {
143
+ parts.push(
144
+ ...flattenStringJoinParts(path, print, entry.node, entryPath),
145
+ );
146
+ continue;
147
+ }
148
+
149
+ if (entry.node?.type === "LeafNode" && entry.node.kind === "String") {
150
+ parts.push({ type: "string", node: entry.node });
151
+ continue;
152
+ }
153
+
154
+ parts.push({ type: "doc", doc: path.call(print, ...entryPath) });
155
+ }
156
+
157
+ return parts;
158
+ }
159
+
160
+ function stringJoinPartDocs(parts, options, indentDepth) {
161
+ const docs = [];
162
+ let stringRun = [];
163
+
164
+ function flushStrings() {
165
+ if (stringRun.length === 0) return;
166
+ docs.push(
167
+ ...stringLiteralRunDocs(stringRun, options, {
168
+ indentDepth,
169
+ mode: "stringJoinArg",
170
+ }),
171
+ );
172
+ stringRun = [];
173
+ }
174
+
175
+ for (const part of parts) {
176
+ if (part.type === "string") {
177
+ stringRun.push(part.node);
178
+ continue;
179
+ }
180
+
181
+ flushStrings();
182
+ docs.push(part.doc);
183
+ }
184
+
185
+ flushStrings();
186
+ return docs;
187
+ }
188
+
189
+ function printStringJoin(path, options, print, node) {
190
+ if (containsComment(node)) return printCall(path, options, print, node);
191
+
192
+ const head = isStringJoinCall(node) ? path.call(print, "head") : "StringJoin";
193
+ const parts = flattenStringJoinParts(path, print, node);
194
+ const indentDepth = stringLineIndentDepth(path);
195
+ const allStringNodes = parts.every((part) => part.type === "string")
196
+ ? parts.map((part) => part.node)
197
+ : null;
198
+ const singleLineLiteral = allStringNodes
199
+ ? singleLineStringLiteralRunDoc(allStringNodes, options, {
200
+ indentDepth,
201
+ widthOffset: hasFollowingCommaSibling(path) ? 1 : 0,
202
+ })
203
+ : null;
204
+ if (singleLineLiteral) return singleLineLiteral;
205
+
206
+ const argDocs = stringJoinPartDocs(
207
+ parts,
208
+ options,
209
+ indentDepth,
210
+ );
211
+ if (argDocs.length === 0) return [head, "[]"];
212
+
213
+ return group([
214
+ head,
215
+ "[",
216
+ indent([line, join([",", line], argDocs)]),
217
+ softline,
218
+ "]",
219
+ ]);
220
+ }
221
+
222
+ // If[cond, then, else] — keep the condition beside If[ only when it fits.
223
+ function printConditionFirst(path, options, print, node) {
224
+ if (hasDirectCommentArg(node)) return printCall(path, options, print, node);
225
+
226
+ const head = path.call(print, "head");
227
+ const args = printedArgs(path, options, print, node);
228
+ if (args.length === 0) return [head, "[]"];
229
+
230
+ const [cond, ...rest] = args;
231
+
232
+ if (rest.length === 0) {
233
+ return group([head, "[", indent([softline, cond]), softline, "]"]);
234
+ }
235
+
236
+ const brokenWithHeadCondition = [
237
+ head,
238
+ "[",
239
+ cond,
240
+ ",",
241
+ indent(
242
+ rest.flatMap((r, i) => [
243
+ hardline,
244
+ r,
245
+ i < rest.length - 1 ? "," : "",
246
+ ]),
247
+ ),
248
+ hardline,
249
+ "]",
250
+ ];
251
+ const fullyBroken = [
252
+ head,
253
+ "[",
254
+ indent([
255
+ hardline,
256
+ cond,
257
+ ",",
258
+ ...rest.flatMap((r, i) => [
259
+ hardline,
260
+ r,
261
+ i < rest.length - 1 ? "," : "",
262
+ ]),
263
+ ]),
264
+ hardline,
265
+ "]",
266
+ ];
267
+
268
+ if (args.some((arg) => willBreak(arg))) {
269
+ return brokenWithHeadCondition;
270
+ }
271
+
272
+ return conditionalGroup([
273
+ [head, "[", join([", "], [cond, ...rest]), "]"],
274
+ brokenWithHeadCondition,
275
+ fullyBroken,
276
+ ]);
277
+ }
278
+
279
+ function buildCasePairs(args) {
280
+ const pairs = [];
281
+ for (let i = 0; i + 1 < args.length; i += 2) {
282
+ pairs.push([args[i], args[i + 1]]);
283
+ }
284
+
285
+ return {
286
+ pairs,
287
+ trailing: args.length % 2 === 1 ? args[args.length - 1] : null,
288
+ };
289
+ }
290
+
291
+ function casePairDocs(pairs, { forceBreak = false } = {}) {
292
+ const pairLine = forceBreak ? hardline : line;
293
+ return pairs.map(([cond, val]) => [cond, ",", indent([pairLine, val])]);
294
+ }
295
+
296
+ // Module[{vars}, body] — var list breaks per-var when long or when printWidth requires it.
297
+ function printBlockStructure(path, options, print, node) {
298
+ if (hasDirectCommentArg(node)) return printCall(path, options, print, node);
299
+
300
+ const head = path.call(print, "head");
301
+ const args = printedArgs(path, options, print, node);
302
+ if (args.length === 0) return [head, "[]"];
303
+
304
+ // We still need the raw node for the first arg to inspect its structure
305
+ const { argChildren } = _getArgNodesAndPrinted(path, options, print, node);
306
+
307
+ const varListNode = argChildren[0];
308
+ if (!varListNode || containsComment(varListNode))
309
+ return printCall(path, options, print, node);
310
+
311
+ const body = args.slice(1);
312
+ const varListDoc = formatVarListPath(
313
+ path,
314
+ options,
315
+ print,
316
+ node,
317
+ varListNode,
318
+ );
319
+
320
+ if (body.length === 0) {
321
+ return group([head, "[", varListDoc, "]"]);
322
+ }
323
+
324
+ return group([
325
+ head,
326
+ "[",
327
+ varListDoc,
328
+ ",",
329
+ indent([line, join([",", line], body)]),
330
+ softline,
331
+ "]",
332
+ ]);
333
+ }
334
+
335
+ function printSwitchStructure(path, options, print, node) {
336
+ if (hasDirectCommentArg(node)) return printCall(path, options, print, node);
337
+
338
+ const head = path.call(print, "head");
339
+ const args = printedArgs(path, options, print, node);
340
+ if (args.length === 0) return [head, "[]"];
341
+
342
+ const [expr, ...rest] = args;
343
+ if (rest.length === 0) {
344
+ return group([head, "[", indent([softline, expr]), softline, "]"]);
345
+ }
346
+
347
+ const { pairs, trailing } = buildCasePairs(rest);
348
+ const pairDocs = casePairDocs(pairs, { forceBreak: true });
349
+ const brokenTail = [
350
+ join([",", hardline], pairDocs),
351
+ trailing ? [",", hardline, trailing] : "",
352
+ ];
353
+
354
+ const brokenSwitch = [
355
+ head,
356
+ "[",
357
+ expr,
358
+ ",",
359
+ indent([hardline, ...brokenTail]),
360
+ hardline,
361
+ "]",
362
+ ];
363
+
364
+ if (args.some((arg) => willBreak(arg))) {
365
+ return brokenSwitch;
366
+ }
367
+
368
+ return conditionalGroup([
369
+ [head, "[", join([", "], args), "]"],
370
+ brokenSwitch,
371
+ ]);
372
+ }
373
+
374
+ /** Helper: returns both the raw arg nodes and their printed forms. */
375
+ function _getArgNodesAndPrinted(path, options, print, node) {
376
+ // Determine whether there's an InfixNode[Comma] wrapper
377
+ const wrapperIdx = node.children.findIndex(
378
+ (c) => c.type === "InfixNode" && c.op === "Comma",
379
+ );
380
+ let rawArgs;
381
+ if (wrapperIdx !== -1) {
382
+ const wrapper = node.children[wrapperIdx];
383
+ rawArgs = wrapper.children.filter((c) => {
384
+ if (
385
+ c.type === "LeafNode" &&
386
+ (c.kind === "Token`Comma" ||
387
+ c.kind === "Token`Whitespace" ||
388
+ c.kind === "Whitespace")
389
+ )
390
+ return false;
391
+ return true;
392
+ });
393
+ } else {
394
+ rawArgs = node.children.filter((c) => {
395
+ if (
396
+ c.type === "LeafNode" &&
397
+ (c.kind === "Token`OpenSquare" ||
398
+ c.kind === "Token`CloseSquare" ||
399
+ c.kind === "Token`Comma" ||
400
+ c.kind === "Token`Whitespace" ||
401
+ c.kind === "Whitespace")
402
+ )
403
+ return false;
404
+ return true;
405
+ });
406
+ }
407
+ const printed = printedArgs(path, options, print, node);
408
+ return { argChildren: rawArgs, printed };
409
+ }
410
+
411
+ function semanticArgsGroup(node) {
412
+ return (node.children ?? []).filter(
413
+ (c) =>
414
+ !isTrivia(c) &&
415
+ !(
416
+ c.type === "LeafNode" &&
417
+ (c.kind === "Token`OpenCurly" ||
418
+ c.kind === "Token`CloseCurly" ||
419
+ c.kind === "Token`LessBar" ||
420
+ c.kind === "Token`BarGreater" ||
421
+ c.kind === "Token`Comma")
422
+ ),
423
+ );
424
+ }
425
+
426
+ function semanticGroupEntries(callNode, groupNode) {
427
+ const wrapperIdx = callNode.children.findIndex(
428
+ (c) => c.type === "InfixNode" && c.op === "Comma",
429
+ );
430
+ const basePath = [];
431
+
432
+ if (wrapperIdx !== -1) {
433
+ const wrapper = callNode.children[wrapperIdx];
434
+ const groupIdx = wrapper.children.indexOf(groupNode);
435
+ if (groupIdx === -1) return [];
436
+ basePath.push("children", wrapperIdx, "children", groupIdx);
437
+ } else {
438
+ const groupIdx = callNode.children.indexOf(groupNode);
439
+ if (groupIdx === -1) return [];
440
+ basePath.push("children", groupIdx);
441
+ }
442
+
443
+ const semanticChildren = (groupNode.children ?? []).filter(
444
+ (child) =>
445
+ !isTrivia(child) &&
446
+ !(
447
+ child.type === "LeafNode" &&
448
+ (child.kind === "Token`OpenCurly" ||
449
+ child.kind === "Token`CloseCurly" ||
450
+ child.kind === "Token`LessBar" ||
451
+ child.kind === "Token`BarGreater")
452
+ ),
453
+ );
454
+
455
+ if (
456
+ semanticChildren.length === 1 &&
457
+ semanticChildren[0].type === "InfixNode" &&
458
+ semanticChildren[0].op === "Comma"
459
+ ) {
460
+ const commaWrapperIdx = groupNode.children.indexOf(semanticChildren[0]);
461
+ return semanticChildren[0].children.reduce((entries, child, idx) => {
462
+ if (isTrivia(child)) return entries;
463
+ if (child.type === "LeafNode" && child.kind === "Token`Comma")
464
+ return entries;
465
+ entries.push({
466
+ node: child,
467
+ path: [
468
+ ...basePath,
469
+ "children",
470
+ commaWrapperIdx,
471
+ "children",
472
+ idx,
473
+ ],
474
+ });
475
+ return entries;
476
+ }, []);
477
+ }
478
+
479
+ return (groupNode.children ?? []).reduce((entries, child, idx) => {
480
+ if (isTrivia(child)) return entries;
481
+ if (
482
+ child.type === "LeafNode" &&
483
+ (child.kind === "Token`OpenCurly" ||
484
+ child.kind === "Token`CloseCurly" ||
485
+ child.kind === "Token`LessBar" ||
486
+ child.kind === "Token`BarGreater" ||
487
+ child.kind === "Token`Comma")
488
+ ) {
489
+ return entries;
490
+ }
491
+ entries.push({ node: child, path: [...basePath, "children", idx] });
492
+ return entries;
493
+ }, []);
494
+ }
495
+
496
+ function inlineNodeText(node) {
497
+ if (!node) return "";
498
+ if (node.type === "LeafNode") return String(node.value ?? "");
499
+ if (node.type === "CompoundNode" && PATTERN_BLANK_OPS.has(node.op)) {
500
+ return (node.children ?? []).map(inlineNodeText).join("");
501
+ }
502
+ if (node.type === "CompoundNode" && BLANK_OPS.has(node.op)) {
503
+ return (node.children ?? []).map(inlineNodeText).join("");
504
+ }
505
+ if (
506
+ node.type === "CompoundNode" &&
507
+ (node.op === "Slot" || node.op === "SlotSequence")
508
+ ) {
509
+ return (node.children ?? []).map(inlineNodeText).join("");
510
+ }
511
+ if (
512
+ node.type === "BinaryNode" &&
513
+ (node.op === "Set" || node.op === "SetDelayed")
514
+ ) {
515
+ const semantic = (node.children ?? []).filter(
516
+ (c) =>
517
+ !isTrivia(c) &&
518
+ !(
519
+ c.type === "LeafNode" &&
520
+ c.kind.startsWith("Token`") &&
521
+ !["Token`Hash", "Token`HashHash"].includes(c.kind)
522
+ ),
523
+ );
524
+ const op = node.op === "Set" ? "=" : ":=";
525
+ if (semantic.length === 2)
526
+ return `${inlineNodeText(semantic[0])} ${op} ${inlineNodeText(semantic[1])}`;
527
+ }
528
+ if (node.type === "BinaryNode") {
529
+ const semantic = (node.children ?? []).filter(
530
+ (c) =>
531
+ !isTrivia(c) &&
532
+ !(
533
+ c.type === "LeafNode" &&
534
+ c.kind.startsWith("Token`") &&
535
+ !["Token`Hash", "Token`HashHash"].includes(c.kind)
536
+ ),
537
+ );
538
+ const opMap = {
539
+ Power: "^",
540
+ Divide: "/",
541
+ ReplaceAll: "/.",
542
+ Rule: "->",
543
+ RuleDelayed: ":>",
544
+ Condition: "/;",
545
+ };
546
+ const op = opMap[node.op] ?? node.op;
547
+ if (semantic.length === 2)
548
+ return `${inlineNodeText(semantic[0])} ${op} ${inlineNodeText(semantic[1])}`;
549
+ }
550
+ if (node.type === "InfixNode") {
551
+ const semantic = (node.children ?? []).filter(
552
+ (c) =>
553
+ !isTrivia(c) &&
554
+ !(c.type === "LeafNode" && c.kind === "Token`Comma"),
555
+ );
556
+ if (node.op === "Comma") return semantic.map(inlineNodeText).join(", ");
557
+ if (node.op === "Plus") return semantic.map(inlineNodeText).join(" + ");
558
+ if (node.op === "Times")
559
+ return semantic.map(inlineNodeText).join(" * ");
560
+ if (node.op === "InfixInequality" && semantic.length === 3)
561
+ return `${inlineNodeText(semantic[0])} ${inlineNodeText(node.children[1])} ${inlineNodeText(semantic[2])}`;
562
+ if (node.op === "CompoundExpression")
563
+ return semantic.map(inlineNodeText).join("; ");
564
+ }
565
+ if (node.type === "CallNode") {
566
+ const head = inlineNodeText(node.head);
567
+ const args = (node.children ?? []).filter(
568
+ (c) =>
569
+ !isTrivia(c) &&
570
+ !(
571
+ c.type === "LeafNode" &&
572
+ (c.kind === "Token`OpenSquare" ||
573
+ c.kind === "Token`CloseSquare")
574
+ ),
575
+ );
576
+ if (
577
+ args.length === 1 &&
578
+ args[0].type === "InfixNode" &&
579
+ args[0].op === "Comma"
580
+ ) {
581
+ return `${head}[${inlineNodeText(args[0])}]`;
582
+ }
583
+ return `${head}[${args.map(inlineNodeText).join(", ")}]`;
584
+ }
585
+ if (node.type === "GroupNode" && node.kind === "List") {
586
+ return `{${semanticArgsGroup(node).map(inlineNodeText).join(", ")}}`;
587
+ }
588
+ if (node.type === "GroupNode" && node.kind === "Association") {
589
+ return `<|${semanticArgsGroup(node).map(inlineNodeText).join(", ")}|>`;
590
+ }
591
+ return String(node.value ?? "");
592
+ }
593
+
594
+ function moduleVarsBreakThreshold(options) {
595
+ options = normalizeWolframOptions(options);
596
+ const threshold = Number(options.wolframModuleVarsBreakThreshold ?? 40);
597
+ if (!Number.isFinite(threshold)) return 40;
598
+ return Math.max(0, threshold);
599
+ }
600
+
601
+ function formatVarListPath(path, options, print, callNode, varListNode) {
602
+ if (varListNode.type !== "GroupNode") {
603
+ const printed = printedArgs(path, options, print, callNode);
604
+ return printed[0] ?? "{}";
605
+ }
606
+
607
+ const entries = semanticGroupEntries(callNode, varListNode);
608
+ if (entries.length === 0) return "{}";
609
+
610
+ const inline = `{${entries.map((entry) => inlineNodeText(entry.node)).join(", ")}}`;
611
+ const shouldBreak = inline.length > moduleVarsBreakThreshold(options);
612
+ const entryDocs = entries.map((entry) => path.call(print, ...entry.path));
613
+
614
+ return group(
615
+ indent([
616
+ "{",
617
+ indent([softline, join([",", line], entryDocs)]),
618
+ softline,
619
+ "}",
620
+ ]),
621
+ { shouldBreak },
622
+ );
623
+ }
624
+
625
+ // Which[cond1, val1, cond2, val2] — alternating 1/2 indent levels
626
+ function printCaseStructure(path, options, print, node) {
627
+ if (hasDirectCommentArg(node)) return printCall(path, options, print, node);
628
+
629
+ const head = path.call(print, "head");
630
+ const args = printedArgs(path, options, print, node);
631
+ if (args.length === 0) return [head, "[]"];
632
+
633
+ const { pairs, trailing } = buildCasePairs(args);
634
+ const pairDocs = casePairDocs(pairs);
635
+
636
+ return group([
637
+ head,
638
+ "[",
639
+ indent([
640
+ line,
641
+ join([",", line], pairDocs),
642
+ trailing ? [",", line, trailing] : "",
643
+ ]),
644
+ softline,
645
+ "]",
646
+ ]);
647
+ }
648
+
649
+ /** Returns the specialized printer for a supported node, or null if none applies. */
650
+ export function getSpecialPrinter(node, options) {
651
+ if (isStringJoinInfix(node)) {
652
+ return containsComment(node) ? null : printStringJoin;
653
+ }
654
+
655
+ const name = getHeadName(node);
656
+ if (!name) return null;
657
+ if (name === "StringJoin") return printStringJoin;
658
+ const sets = buildDispatchSets(options);
659
+ if (name === "Switch" && sets.conditionFirst.has(name))
660
+ return printSwitchStructure;
661
+ if (sets.conditionFirst.has(name)) return printConditionFirst;
662
+ if (sets.blockStructure.has(name)) return printBlockStructure;
663
+ if (sets.caseStructure.has(name)) return printCaseStructure;
664
+ return null;
665
+ }