neonctl 2.22.0 → 2.23.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 (116) hide show
  1. package/README.md +242 -16
  2. package/analytics.js +5 -2
  3. package/commands/branches.js +9 -1
  4. package/commands/checkout.js +249 -0
  5. package/commands/connection_string.js +15 -2
  6. package/commands/data_api.js +286 -0
  7. package/commands/functions.js +277 -0
  8. package/commands/index.js +12 -0
  9. package/commands/link.js +667 -0
  10. package/commands/neon_auth.js +1013 -0
  11. package/commands/projects.js +9 -1
  12. package/commands/psql.js +62 -0
  13. package/commands/set_context.js +7 -2
  14. package/context.js +86 -14
  15. package/functions_api.js +44 -0
  16. package/index.js +3 -0
  17. package/package.json +60 -51
  18. package/psql/cli.js +51 -0
  19. package/psql/command/cmd_cond.js +437 -0
  20. package/psql/command/cmd_connect.js +815 -0
  21. package/psql/command/cmd_copy.js +1025 -0
  22. package/psql/command/cmd_describe.js +1810 -0
  23. package/psql/command/cmd_format.js +909 -0
  24. package/psql/command/cmd_io.js +2187 -0
  25. package/psql/command/cmd_lo.js +385 -0
  26. package/psql/command/cmd_meta.js +970 -0
  27. package/psql/command/cmd_misc.js +187 -0
  28. package/psql/command/cmd_pipeline.js +1141 -0
  29. package/psql/command/cmd_restrict.js +171 -0
  30. package/psql/command/cmd_show.js +751 -0
  31. package/psql/command/dispatch.js +343 -0
  32. package/psql/command/inputQueue.js +42 -0
  33. package/psql/command/shared.js +71 -0
  34. package/psql/complete/filenames.js +139 -0
  35. package/psql/complete/index.js +104 -0
  36. package/psql/complete/matcher.js +314 -0
  37. package/psql/complete/psqlVars.js +247 -0
  38. package/psql/complete/queries.js +491 -0
  39. package/psql/complete/rules.js +2387 -0
  40. package/psql/core/common.js +1250 -0
  41. package/psql/core/help.js +576 -0
  42. package/psql/core/mainloop.js +1353 -0
  43. package/psql/core/prompt.js +437 -0
  44. package/psql/core/settings.js +684 -0
  45. package/psql/core/sqlHelp.js +1066 -0
  46. package/psql/core/startup.js +840 -0
  47. package/psql/core/syncVars.js +116 -0
  48. package/psql/core/variables.js +287 -0
  49. package/psql/describe/formatters.js +1277 -0
  50. package/psql/describe/processNamePattern.js +270 -0
  51. package/psql/describe/queries.js +2373 -0
  52. package/psql/describe/versionGate.js +43 -0
  53. package/psql/index.js +2005 -0
  54. package/psql/io/history.js +299 -0
  55. package/psql/io/input.js +120 -0
  56. package/psql/io/lineEditor/buffer.js +323 -0
  57. package/psql/io/lineEditor/complete.js +227 -0
  58. package/psql/io/lineEditor/filename.js +159 -0
  59. package/psql/io/lineEditor/index.js +891 -0
  60. package/psql/io/lineEditor/keymap.js +738 -0
  61. package/psql/io/lineEditor/vt100.js +363 -0
  62. package/psql/io/pgpass.js +202 -0
  63. package/psql/io/pgservice.js +194 -0
  64. package/psql/io/psqlrc.js +422 -0
  65. package/psql/print/aligned.js +1756 -0
  66. package/psql/print/asciidoc.js +248 -0
  67. package/psql/print/crosstab.js +460 -0
  68. package/psql/print/csv.js +92 -0
  69. package/psql/print/html.js +258 -0
  70. package/psql/print/json.js +96 -0
  71. package/psql/print/latex.js +396 -0
  72. package/psql/print/pager.js +265 -0
  73. package/psql/print/troff.js +258 -0
  74. package/psql/print/unaligned.js +118 -0
  75. package/psql/print/units.js +135 -0
  76. package/psql/scanner/slash.js +513 -0
  77. package/psql/scanner/sql.js +910 -0
  78. package/psql/scanner/stringutils.js +390 -0
  79. package/psql/types/backslash.js +1 -0
  80. package/psql/types/connection.js +1 -0
  81. package/psql/types/index.js +7 -0
  82. package/psql/types/printer.js +1 -0
  83. package/psql/types/repl.js +1 -0
  84. package/psql/types/scanner.js +24 -0
  85. package/psql/types/settings.js +1 -0
  86. package/psql/types/variables.js +1 -0
  87. package/psql/wire/connection.js +2844 -0
  88. package/psql/wire/copy.js +108 -0
  89. package/psql/wire/notify.js +59 -0
  90. package/psql/wire/pipeline.js +519 -0
  91. package/psql/wire/protocol.js +466 -0
  92. package/psql/wire/sasl.js +296 -0
  93. package/psql/wire/tls.js +596 -0
  94. package/test_utils/fixtures.js +1 -0
  95. package/utils/enrichers.js +18 -1
  96. package/utils/esbuild.js +147 -0
  97. package/utils/middlewares.js +1 -1
  98. package/utils/psql.js +107 -11
  99. package/utils/zip.js +4 -0
  100. package/writer.js +1 -1
  101. package/commands/auth.test.js +0 -211
  102. package/commands/branches.test.js +0 -460
  103. package/commands/connection_string.test.js +0 -196
  104. package/commands/databases.test.js +0 -39
  105. package/commands/help.test.js +0 -9
  106. package/commands/init.test.js +0 -56
  107. package/commands/ip_allow.test.js +0 -59
  108. package/commands/operations.test.js +0 -7
  109. package/commands/orgs.test.js +0 -7
  110. package/commands/projects.test.js +0 -144
  111. package/commands/roles.test.js +0 -37
  112. package/commands/set_context.test.js +0 -159
  113. package/commands/vpc_endpoints.test.js +0 -69
  114. package/env.test.js +0 -55
  115. package/utils/formats.test.js +0 -32
  116. package/writer.test.js +0 -104
@@ -0,0 +1,437 @@
1
+ /**
2
+ * psql conditional backslash commands: `\if`, `\elif`, `\else`, `\endif`.
3
+ *
4
+ * TypeScript port of `exec_command_if`/`exec_command_elif`/`exec_command_else`/
5
+ * `exec_command_endif` in `src/bin/psql/command.c`, plus the `ConditionalStack`
6
+ * machinery from `src/fe_utils/conditional.c`.
7
+ *
8
+ * Semantics (mirroring upstream exactly):
9
+ *
10
+ * - `\if <expr>`
11
+ * • If outer branch is active, push a new frame whose state is `TRUE` or
12
+ * `FALSE` depending on the parsed expression value.
13
+ * • Otherwise push `IGNORED`: every nested branch is suppressed regardless
14
+ * of expression. This is how upstream achieves transitive suppression
15
+ * without `conditional_active()` itself being transitive.
16
+ * - `\elif <expr>`
17
+ * • Top is `TRUE` → branch already taken; skip rest until `\endif`
18
+ * (poke to `IGNORED`).
19
+ * • Top is `FALSE` → first true branch wins; evaluate expression.
20
+ * • Top is `IGNORED` → leave untouched, ignore expression.
21
+ * • Top is `ELSE_*` → error: `\elif: cannot occur after \else`.
22
+ * • Top is `NONE` (empty) → error: `\elif: no matching \if`.
23
+ * - `\else`
24
+ * • Top is `TRUE` → poke `ELSE_FALSE`.
25
+ * • Top is `FALSE` → poke `ELSE_TRUE`.
26
+ * • Top is `IGNORED` → poke `ELSE_FALSE` (still suppressed).
27
+ * • Top is `ELSE_*` → error: `\else: cannot occur after \else`.
28
+ * • Top is `NONE` → error: `\else: no matching \if`.
29
+ * - `\endif`
30
+ * • Pop the top frame.
31
+ * • Top was `NONE` → error: `\endif: no matching \if`.
32
+ *
33
+ * Expression evaluation: upstream `read_boolean_expression` reads tokens
34
+ * with `OT_NORMAL` (which expands `:vars` and backticks) and concatenates
35
+ * them with single spaces; the assembled string is passed through
36
+ * `ParseVariableBool`. We mirror that pipeline in {@link collectExpr} +
37
+ * {@link parseBool}: collect every `nextArg('normal')` token, join with
38
+ * spaces, parse. Unrecognised tokens emit the upstream
39
+ * `unrecognized value "<tok>" for "\<cmd> expression": Boolean expected`
40
+ * diagnostic and evaluate to false.
41
+ *
42
+ * Inactive branches: when the surrounding scope is suppressed, upstream
43
+ * `ignore_boolean_expression` drops the argument tokens WITHOUT running
44
+ * `:var` / backtick expansion (regress psql.sql ~line 1028 covers this).
45
+ * We achieve the same by NOT calling `nextArg` at all — the
46
+ * BackslashContext factory only invokes the slash scanner lazily on the
47
+ * first `nextArg` request, so leaving the args queue untouched is
48
+ * equivalent to upstream's "discard without expansion".
49
+ *
50
+ * Diagnostic format: cond commands emit their errors BARE (no
51
+ * `psql: ERROR:` prefix). This mirrors `expected/psql.out` (e.g.
52
+ * `\endif: no matching \if` on a single line). We use `writeErr` directly
53
+ * and return `errorWritten: true` so the mainloop's `psql: ERROR:` fallback
54
+ * is suppressed.
55
+ *
56
+ * Note on the "transitive suppression" requirement: upstream's
57
+ * `conditional_active()` only inspects the top frame, but the resulting state
58
+ * machine *is* transitive because `\if` inside an inactive outer always pushes
59
+ * `IGNORED`, and `IGNORED` never transitions to `TRUE`. So `isActive()`
60
+ * inspecting just the top frame produces the right answer.
61
+ */
62
+ import { writeErr } from './shared.js';
63
+ // ---------------------------------------------------------------------------
64
+ // Conditional stack
65
+ // ---------------------------------------------------------------------------
66
+ const INACTIVE_STATES = ['false', 'else-false', 'ignored'];
67
+ /**
68
+ * Build an empty {@link CondStack}.
69
+ *
70
+ * Frames are stored in an array; `top()` is the last element. `branchTaken`
71
+ * records whether a `TRUE`/`ELSE_TRUE` branch has been seen at this level —
72
+ * mainloop and the elif/else commands use it implicitly via the state-machine
73
+ * transitions described above (we don't expose it through the public API, but
74
+ * it's part of the frame shape declared in `types/repl.ts`).
75
+ */
76
+ export const createCondStack = () => {
77
+ const frames = [];
78
+ const branchTakenForInitial = (state) => state === 'true' || state === 'else-true';
79
+ return {
80
+ push(state, savedQueryBufLen = 0) {
81
+ frames.push({
82
+ state,
83
+ branchTaken: branchTakenForInitial(state),
84
+ savedQueryBufLen,
85
+ });
86
+ },
87
+ pop() {
88
+ return frames.pop();
89
+ },
90
+ top() {
91
+ return frames.length === 0 ? undefined : frames[frames.length - 1];
92
+ },
93
+ isActive() {
94
+ // Upstream `conditional_active()`: top is NONE/TRUE/ELSE_TRUE → active.
95
+ // The transitive suppression is encoded by `\if` pushing `IGNORED` when
96
+ // its surrounding context is inactive — see cmdIf below.
97
+ if (frames.length === 0)
98
+ return true;
99
+ return !INACTIVE_STATES.includes(frames[frames.length - 1].state);
100
+ },
101
+ setState(state) {
102
+ if (frames.length === 0)
103
+ return;
104
+ const top = frames[frames.length - 1];
105
+ top.state = state;
106
+ if (state === 'true' || state === 'else-true')
107
+ top.branchTaken = true;
108
+ },
109
+ setSavedQueryBufLen(len) {
110
+ if (frames.length === 0)
111
+ return;
112
+ frames[frames.length - 1].savedQueryBufLen = len;
113
+ },
114
+ depth() {
115
+ return frames.length;
116
+ },
117
+ };
118
+ };
119
+ // ---------------------------------------------------------------------------
120
+ // ParseVariableBool — mirrors variables.c::ParseVariableBool.
121
+ //
122
+ // We re-implement it here (rather than importing from core/variables.ts) so
123
+ // the slash-cmd modules stay decoupled from the var-store implementation;
124
+ // they just need a value-parser. Recognised forms are case-insensitive with
125
+ // unique-prefix matching for word forms:
126
+ //
127
+ // true / false / yes / no (unique-prefix accepted)
128
+ // on / off (need at least 2 chars; bare 'o' ambiguous)
129
+ // 1 / 0 (literal)
130
+ //
131
+ // Anything else is an error (upstream prints a warning and returns false). We
132
+ // follow upstream by treating unrecognised tokens as `false` while pushing
133
+ // the frame.
134
+ // ---------------------------------------------------------------------------
135
+ const isPrefixOf = (value, prefix) => value.length > 0 &&
136
+ value.length <= prefix.length &&
137
+ prefix.slice(0, value.length).toLowerCase() === value.toLowerCase();
138
+ /** Returns the parsed boolean, or `null` if the string was not recognised. */
139
+ export const parseBool = (value) => {
140
+ if (value.length === 0)
141
+ return null;
142
+ if (isPrefixOf(value, 'true'))
143
+ return true;
144
+ if (isPrefixOf(value, 'false'))
145
+ return false;
146
+ if (isPrefixOf(value, 'yes'))
147
+ return true;
148
+ if (isPrefixOf(value, 'no'))
149
+ return false;
150
+ if (value.length >= 2) {
151
+ const lower = value.toLowerCase();
152
+ if ('on'.startsWith(lower))
153
+ return true;
154
+ if ('off'.startsWith(lower))
155
+ return false;
156
+ }
157
+ if (value === '1')
158
+ return true;
159
+ if (value === '0')
160
+ return false;
161
+ return null;
162
+ };
163
+ // ---------------------------------------------------------------------------
164
+ // Backslash command implementations
165
+ // ---------------------------------------------------------------------------
166
+ /**
167
+ * Read every remaining `'normal'`-mode argument off the BackslashContext and
168
+ * concatenate them with single spaces. Mirrors upstream
169
+ * `read_boolean_expression`, which calls `psql_scan_slash_option(OT_NORMAL)`
170
+ * in a loop and joins tokens with `" "`. Returns the empty string when no
171
+ * args follow the command name — upstream's "missing expression" path
172
+ * surfaces the same `unrecognized value ""...` diagnostic as any other
173
+ * unparseable token, then evaluates to false.
174
+ *
175
+ * After joining, we resolve the `:{?varname}` "defined-variable" substitution
176
+ * form. Upstream's slash lexer recognises `:{?name}` directly and emits
177
+ * `TRUE` / `FALSE` depending on whether the named variable is currently
178
+ * set; we re-create that pass here because the underlying scanner only
179
+ * handles the plain `:name`, `:'name'`, `:"name"` forms (the `:{?name}`
180
+ * form lives in a separate `xslashdefined` flex rule upstream). Doing it
181
+ * after the join keeps the rule local to the conditional-expression
182
+ * pipeline — every other call site of the slash scanner has its own
183
+ * variable-expansion needs that we don't want to disturb.
184
+ */
185
+ const DEFINED_VAR_RE = /:\{\?([A-Za-z_][A-Za-z0-9_]*)\}/g;
186
+ const expandDefinedVar = (text, isDefined) => text.replace(DEFINED_VAR_RE, (_match, name) => isDefined(name) ? 'TRUE' : 'FALSE');
187
+ const collectExpr = (ctx) => {
188
+ const parts = [];
189
+ for (;;) {
190
+ const arg = ctx.nextArg('normal');
191
+ if (arg === null)
192
+ break;
193
+ parts.push(arg);
194
+ }
195
+ return expandDefinedVar(parts.join(' '), (name) => ctx.settings.vars.has(name));
196
+ };
197
+ /**
198
+ * Marker call indicating "discard the expression without evaluating it".
199
+ * Mirrors upstream `ignore_boolean_expression` — when we're already inside
200
+ * an inactive branch, `\if` / `\elif` arguments are dropped without
201
+ * expanding `:vars` or running backticks. We achieve this by simply NOT
202
+ * calling `nextArg`: the BackslashContext factory in `mainloop.ts` only
203
+ * invokes the slash scanner lazily on the first arg request, so leaving
204
+ * the queue untouched skips all expansion. The unconsumed `rawArgs` are
205
+ * dropped after the cmd returns.
206
+ *
207
+ * Kept as a named no-op so call sites read intent-fully ("dropExpr") and
208
+ * future refactors can replace the body without touching every caller.
209
+ */
210
+ const dropExpr = () => {
211
+ // Intentionally empty — see the doc comment.
212
+ };
213
+ /**
214
+ * Evaluate the joined expression against {@link parseBool}. Unrecognised
215
+ * tokens surface `unrecognized value "<tok>" for "\<cmd> expression":
216
+ * Boolean expected` to stderr (bare, no `psql: ERROR:` prefix — upstream
217
+ * `psql_error` shape) and evaluate to false. The caller is responsible for
218
+ * setting the stack state to `'false'` on this path. `cmdName` is the
219
+ * caller's command identifier (`'if'` / `'elif'`) so the diagnostic matches
220
+ * upstream verbatim.
221
+ */
222
+ const evalExpr = (ctx, cmdName) => {
223
+ const raw = collectExpr(ctx);
224
+ const parsed = parseBool(raw);
225
+ if (parsed === null) {
226
+ const message = `unrecognized value "${raw}" for "\\${cmdName} expression": Boolean expected`;
227
+ ctx.settings.lastErrorResult = { message };
228
+ writeErr(`${message}\n`);
229
+ return false;
230
+ }
231
+ return parsed;
232
+ };
233
+ /**
234
+ * Marker symbol on BackslashContext.cmdName so the mainloop can recognise the
235
+ * cond commands without an interface-pollution argument. We instead attach the
236
+ * CondStack via a well-known field on the `settings` object — see {@link
237
+ * attachCondStack} / {@link getCondStack}.
238
+ *
239
+ * The mainloop is the sole owner of the CondStack, and it threads it onto the
240
+ * BackslashContext via this helper pair so command modules don't have to know
241
+ * about REPLContext.
242
+ */
243
+ const COND_STACK_KEY = Symbol.for('neonctl.psql.condStack');
244
+ export const attachCondStack = (ctx, cond) => {
245
+ const settings = ctx.settings;
246
+ settings[COND_STACK_KEY] = cond;
247
+ };
248
+ export const getCondStack = (ctx) => {
249
+ const settings = ctx.settings;
250
+ const stack = settings[COND_STACK_KEY];
251
+ if (stack === undefined) {
252
+ throw new Error('cond stack not attached; cmd_cond commands must be dispatched via runMainLoop');
253
+ }
254
+ return stack;
255
+ };
256
+ const errResult = (ctx, message) => {
257
+ ctx.settings.lastErrorResult = { message };
258
+ // Upstream emits cond diagnostics bare via `psql_error("%s\n", ...)`: no
259
+ // `psql: ERROR:` prefix, no `\<cmd>:` prefix on top of the message (the
260
+ // message already includes it). We mirror that exactly so the regress
261
+ // expected output (`\elif: cannot occur after \else`) matches verbatim.
262
+ // The `errorWritten` flag tells the mainloop not to add its own
263
+ // `psql: ERROR: <msg>` fallback line.
264
+ writeErr(`${message}\n`);
265
+ return { status: 'error', errorWritten: true };
266
+ };
267
+ const okResult = () => ({ status: 'ok' });
268
+ /**
269
+ * Build an `{ status: 'ok' }` result that also asks the mainloop to truncate
270
+ * `queryBuf` back to `len`. Mirrors upstream `discard_query_text` — called
271
+ * by `\elif` / `\else` / `\endif` when the just-completed branch was
272
+ * INACTIVE, so the SQL text the skipped branch accumulated doesn't bleed
273
+ * into the surrounding statement.
274
+ */
275
+ const truncResult = (len) => ({
276
+ status: 'ok',
277
+ truncateBufTo: len,
278
+ });
279
+ /**
280
+ * `true` when {@link IfState} corresponds to a branch that was skipping
281
+ * statements. Upstream `conditional_active` returns false for these. Used
282
+ * by the elif/else/endif commands to decide whether the
283
+ * `discard_query_text` step should fire on transition out of the branch.
284
+ */
285
+ const isInactiveState = (state) => state === 'false' || state === 'else-false' || state === 'ignored';
286
+ export const cmdIf = {
287
+ name: 'if',
288
+ argMode: 'lex',
289
+ async run(ctx) {
290
+ const cond = getCondStack(ctx);
291
+ // Save the queryBuf state at the point the `\if` was encountered. The
292
+ // mainloop dispatches the cond command AFTER folding any preceding
293
+ // text into `queryBuf` (e.g. the "select" in `select \if true 42 ...`),
294
+ // so `ctx.queryBuf.length` is exactly upstream `save_query_text_state`'s
295
+ // snapshot. Inactive branches will accumulate scan-time SQL we'll later
296
+ // roll back via `discard_query_text` (the truncate-to-saved step in the
297
+ // matching `\elif`/`\else`/`\endif`).
298
+ const savedLen = ctx.queryBuf.length;
299
+ if (!cond.isActive()) {
300
+ // Suppressed by outer: push IGNORED and drop the expression WITHOUT
301
+ // expanding it. Upstream `ignore_boolean_expression` calls the lexer
302
+ // with backticks/vars disabled (psql.sql:1028 covers this with
303
+ // `\if false { \if \`nosuchcommand\` ... }`).
304
+ dropExpr();
305
+ cond.push('ignored', savedLen);
306
+ return Promise.resolve(okResult());
307
+ }
308
+ const truthy = evalExpr(ctx, 'if');
309
+ cond.push(truthy ? 'true' : 'false', savedLen);
310
+ return Promise.resolve(okResult());
311
+ },
312
+ };
313
+ export const cmdElif = {
314
+ name: 'elif',
315
+ argMode: 'lex',
316
+ async run(ctx) {
317
+ const cond = getCondStack(ctx);
318
+ const top = cond.top();
319
+ if (top === undefined) {
320
+ dropExpr();
321
+ return Promise.resolve(errResult(ctx, '\\elif: no matching \\if'));
322
+ }
323
+ // If the branch we're leaving was INACTIVE, anything its body added to
324
+ // `queryBuf` is scan-time accumulation we don't want — discard back to
325
+ // the snapshot captured at the matching `\if`/`\elif`. The truncate is
326
+ // applied by the mainloop; we just report the target length.
327
+ const wasInactive = isInactiveState(top.state);
328
+ const savedLen = top.savedQueryBufLen;
329
+ switch (top.state) {
330
+ case 'true': {
331
+ // Branch already taken — flip to IGNORED. Drop the expression
332
+ // without expansion (regress suite: `\if true \elif \`bad\` ...`).
333
+ dropExpr();
334
+ cond.setState('ignored');
335
+ // Active branch — keep buffer text. Re-anchor savedQueryBufLen
336
+ // to the start of the new (ignored) branch so a later `\else`/
337
+ // `\endif` discards just this branch's additions.
338
+ cond.setSavedQueryBufLen(ctx.queryBuf.length);
339
+ return Promise.resolve(okResult());
340
+ }
341
+ case 'false': {
342
+ // Have not yet found a true branch — evaluate this one.
343
+ // evalExpr emits its own `unrecognized value` diagnostic on failure
344
+ // and falls through to false, mirroring upstream.
345
+ const truthy = evalExpr(ctx, 'elif');
346
+ cond.setState(truthy ? 'true' : 'false');
347
+ // Apply the discard (was INACTIVE) and re-anchor at the rolled-back
348
+ // length so the new branch's bookkeeping starts fresh.
349
+ cond.setSavedQueryBufLen(savedLen);
350
+ return wasInactive
351
+ ? Promise.resolve(truncResult(savedLen))
352
+ : Promise.resolve(okResult());
353
+ }
354
+ case 'ignored': {
355
+ // Outer is suppressed — stay ignored, drop args without expanding.
356
+ // Still discard accumulated buffer text and re-anchor.
357
+ dropExpr();
358
+ cond.setSavedQueryBufLen(savedLen);
359
+ return wasInactive
360
+ ? Promise.resolve(truncResult(savedLen))
361
+ : Promise.resolve(okResult());
362
+ }
363
+ case 'else-true':
364
+ case 'else-false':
365
+ dropExpr();
366
+ return Promise.resolve(errResult(ctx, '\\elif: cannot occur after \\else'));
367
+ case 'none':
368
+ dropExpr();
369
+ return Promise.resolve(errResult(ctx, '\\elif: no matching \\if'));
370
+ }
371
+ },
372
+ };
373
+ export const cmdElse = {
374
+ name: 'else',
375
+ argMode: 'lex',
376
+ async run(ctx) {
377
+ const cond = getCondStack(ctx);
378
+ const top = cond.top();
379
+ if (top === undefined) {
380
+ return Promise.resolve(errResult(ctx, '\\else: no matching \\if'));
381
+ }
382
+ const wasInactive = isInactiveState(top.state);
383
+ const savedLen = top.savedQueryBufLen;
384
+ switch (top.state) {
385
+ case 'true':
386
+ cond.setState('else-false');
387
+ // Was ACTIVE — keep buffer text. Re-anchor at the current length so
388
+ // the upcoming else-false branch's additions can be discarded by
389
+ // the matching `\endif`.
390
+ cond.setSavedQueryBufLen(ctx.queryBuf.length);
391
+ return Promise.resolve(okResult());
392
+ case 'false':
393
+ cond.setState('else-true');
394
+ cond.setSavedQueryBufLen(savedLen);
395
+ return wasInactive
396
+ ? Promise.resolve(truncResult(savedLen))
397
+ : Promise.resolve(okResult());
398
+ case 'ignored':
399
+ cond.setState('else-false');
400
+ cond.setSavedQueryBufLen(savedLen);
401
+ return wasInactive
402
+ ? Promise.resolve(truncResult(savedLen))
403
+ : Promise.resolve(okResult());
404
+ case 'else-true':
405
+ case 'else-false':
406
+ return Promise.resolve(errResult(ctx, '\\else: cannot occur after \\else'));
407
+ case 'none':
408
+ return Promise.resolve(errResult(ctx, '\\else: no matching \\if'));
409
+ }
410
+ },
411
+ };
412
+ export const cmdEndif = {
413
+ name: 'endif',
414
+ argMode: 'lex',
415
+ async run(ctx) {
416
+ const cond = getCondStack(ctx);
417
+ const top = cond.top();
418
+ if (top === undefined) {
419
+ return Promise.resolve(errResult(ctx, '\\endif: no matching \\if'));
420
+ }
421
+ const wasInactive = isInactiveState(top.state);
422
+ const savedLen = top.savedQueryBufLen;
423
+ cond.pop();
424
+ return wasInactive
425
+ ? Promise.resolve(truncResult(savedLen))
426
+ : Promise.resolve(okResult());
427
+ },
428
+ };
429
+ /** Names of the conditional commands — the mainloop dispatches these
430
+ * unconditionally (i.e. ignoring `cond.isActive()`) so an `\if false` block
431
+ * can still be closed by `\endif`. */
432
+ export const COND_COMMAND_NAMES = new Set([
433
+ 'if',
434
+ 'elif',
435
+ 'else',
436
+ 'endif',
437
+ ]);