@condorcet.vote/cef-writer 1.2.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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +419 -0
  3. package/dist/Cef.d.ts +188 -0
  4. package/dist/Cef.d.ts.map +1 -0
  5. package/dist/Cef.js +291 -0
  6. package/dist/CefFormat.d.ts +57 -0
  7. package/dist/CefFormat.d.ts.map +1 -0
  8. package/dist/CefFormat.js +133 -0
  9. package/dist/CommentLine.d.ts +18 -0
  10. package/dist/CommentLine.d.ts.map +1 -0
  11. package/dist/CommentLine.js +27 -0
  12. package/dist/Exception/CefFormatException.d.ts +16 -0
  13. package/dist/Exception/CefFormatException.d.ts.map +1 -0
  14. package/dist/Exception/CefFormatException.js +19 -0
  15. package/dist/Exception/CefWriteException.d.ts +12 -0
  16. package/dist/Exception/CefWriteException.d.ts.map +1 -0
  17. package/dist/Exception/CefWriteException.js +13 -0
  18. package/dist/Exception/DuplicateCandidateException.d.ts +9 -0
  19. package/dist/Exception/DuplicateCandidateException.d.ts.map +1 -0
  20. package/dist/Exception/DuplicateCandidateException.js +8 -0
  21. package/dist/Exception/InvalidUtf8Exception.d.ts +13 -0
  22. package/dist/Exception/InvalidUtf8Exception.d.ts.map +1 -0
  23. package/dist/Exception/InvalidUtf8Exception.js +12 -0
  24. package/dist/Exception/InvalidValueException.d.ts +15 -0
  25. package/dist/Exception/InvalidValueException.d.ts.map +1 -0
  26. package/dist/Exception/InvalidValueException.js +14 -0
  27. package/dist/Exception/InvalidWriterStateException.d.ts +14 -0
  28. package/dist/Exception/InvalidWriterStateException.d.ts.map +1 -0
  29. package/dist/Exception/InvalidWriterStateException.js +13 -0
  30. package/dist/Exception/ReservedCharacterException.d.ts +12 -0
  31. package/dist/Exception/ReservedCharacterException.d.ts.map +1 -0
  32. package/dist/Exception/ReservedCharacterException.js +11 -0
  33. package/dist/Exception/index.d.ts +8 -0
  34. package/dist/Exception/index.d.ts.map +1 -0
  35. package/dist/Exception/index.js +7 -0
  36. package/dist/FileWriteTarget.d.ts +27 -0
  37. package/dist/FileWriteTarget.d.ts.map +1 -0
  38. package/dist/FileWriteTarget.js +35 -0
  39. package/dist/Parameter/CandidatesParameter.d.ts +20 -0
  40. package/dist/Parameter/CandidatesParameter.d.ts.map +1 -0
  41. package/dist/Parameter/CandidatesParameter.js +39 -0
  42. package/dist/Parameter/CustomParameter.d.ts +19 -0
  43. package/dist/Parameter/CustomParameter.d.ts.map +1 -0
  44. package/dist/Parameter/CustomParameter.js +35 -0
  45. package/dist/Parameter/ImplicitRankingParameter.d.ts +11 -0
  46. package/dist/Parameter/ImplicitRankingParameter.d.ts.map +1 -0
  47. package/dist/Parameter/ImplicitRankingParameter.js +16 -0
  48. package/dist/Parameter/NumberOfSeatsParameter.d.ts +14 -0
  49. package/dist/Parameter/NumberOfSeatsParameter.d.ts.map +1 -0
  50. package/dist/Parameter/NumberOfSeatsParameter.js +23 -0
  51. package/dist/Parameter/ParameterInterface.d.ts +20 -0
  52. package/dist/Parameter/ParameterInterface.d.ts.map +1 -0
  53. package/dist/Parameter/ParameterInterface.js +1 -0
  54. package/dist/Parameter/StandardParameter.d.ts +14 -0
  55. package/dist/Parameter/StandardParameter.d.ts.map +1 -0
  56. package/dist/Parameter/StandardParameter.js +14 -0
  57. package/dist/Parameter/VotingMethodsParameter.d.ts +16 -0
  58. package/dist/Parameter/VotingMethodsParameter.d.ts.map +1 -0
  59. package/dist/Parameter/VotingMethodsParameter.js +29 -0
  60. package/dist/Parameter/WeightAllowedParameter.d.ts +11 -0
  61. package/dist/Parameter/WeightAllowedParameter.d.ts.map +1 -0
  62. package/dist/Parameter/WeightAllowedParameter.js +16 -0
  63. package/dist/Parameter/index.d.ts +9 -0
  64. package/dist/Parameter/index.d.ts.map +1 -0
  65. package/dist/Parameter/index.js +7 -0
  66. package/dist/Ranking.d.ts +91 -0
  67. package/dist/Ranking.d.ts.map +1 -0
  68. package/dist/Ranking.js +162 -0
  69. package/dist/VoteLine.d.ts +156 -0
  70. package/dist/VoteLine.d.ts.map +1 -0
  71. package/dist/VoteLine.js +289 -0
  72. package/dist/index.browser.d.ts +17 -0
  73. package/dist/index.browser.d.ts.map +1 -0
  74. package/dist/index.browser.js +16 -0
  75. package/dist/index.d.ts +8 -0
  76. package/dist/index.d.ts.map +1 -0
  77. package/dist/index.js +7 -0
  78. package/dist/index.node.d.ts +19 -0
  79. package/dist/index.node.d.ts.map +1 -0
  80. package/dist/index.node.js +25 -0
  81. package/package.json +79 -0
  82. package/src/Cef.ts +405 -0
  83. package/src/CefFormat.ts +152 -0
  84. package/src/CommentLine.ts +32 -0
  85. package/src/Exception/CefFormatException.ts +19 -0
  86. package/src/Exception/CefWriteException.ts +13 -0
  87. package/src/Exception/DuplicateCandidateException.ts +8 -0
  88. package/src/Exception/InvalidUtf8Exception.ts +12 -0
  89. package/src/Exception/InvalidValueException.ts +14 -0
  90. package/src/Exception/InvalidWriterStateException.ts +13 -0
  91. package/src/Exception/ReservedCharacterException.ts +11 -0
  92. package/src/Exception/index.ts +7 -0
  93. package/src/FileWriteTarget.ts +42 -0
  94. package/src/Parameter/CandidatesParameter.ts +49 -0
  95. package/src/Parameter/CustomParameter.ts +45 -0
  96. package/src/Parameter/ImplicitRankingParameter.ts +17 -0
  97. package/src/Parameter/NumberOfSeatsParameter.ts +25 -0
  98. package/src/Parameter/ParameterInterface.ts +20 -0
  99. package/src/Parameter/StandardParameter.ts +13 -0
  100. package/src/Parameter/VotingMethodsParameter.ts +36 -0
  101. package/src/Parameter/WeightAllowedParameter.ts +17 -0
  102. package/src/Parameter/index.ts +8 -0
  103. package/src/Ranking.ts +197 -0
  104. package/src/VoteLine.ts +398 -0
  105. package/src/index.browser.ts +36 -0
  106. package/src/index.node.ts +47 -0
  107. package/src/index.ts +8 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 CEF Writer Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,419 @@
1
+ # CEF Writer Typescript
2
+
3
+ A TypeScript library that streams valid [Condorcet Election Format](https://github.com/CondorcetVote/CondorcetElectionFormat) (CEF) documents to a file or string buffer with a friendly object API.
4
+
5
+ This is a faithful TypeScript port of the PHP library [CondorcetVote/CEF-Writer](https://github.com/CondorcetVote/CEF-Writer): same public API, same architecture, same guarantees.
6
+
7
+ - **Streaming**: every `add*()` call writes one line immediately — nothing is buffered, nothing can be edited afterwards.
8
+ - **Format-safe**: the spec's syntactic rules (reserved characters, blank-ballot sentinel, single-line constraints, parameter-before-vote ordering) are enforced. Invalid input throws.
9
+ - **Semantics-free on purpose**: this library checks format, never election logic (it will, for example, happily let a vote reference a candidate that is not in `#/Candidates:`).
10
+ - **Zero runtime dependencies** and full TypeScript types.
11
+ - Works with a filesystem path, any `WriteTarget`, or a `StringBuffer`.
12
+
13
+ ## Requirements
14
+
15
+ - Node.js 24+ (ES2024, ESM only)
16
+ - Bun (compatible with modern versions)
17
+ - Modern browsers with ES2024 support (Chrome 127+, Firefox 133+, Safari 18+, Edge 127+)
18
+
19
+ For file output in Node.js, the native `node:fs` API is used.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @condorcet.vote/cef-writer
25
+ # or
26
+ yarn add @condorcet.vote/cef-writer
27
+ # or
28
+ pnpm add @condorcet.vote/cef-writer
29
+ # or
30
+ bun add @condorcet.vote/cef-writer
31
+ ```
32
+
33
+ ## Quick start
34
+
35
+ ```typescript
36
+ import {
37
+ Cef,
38
+ CommentLine,
39
+ VoteLine,
40
+ CandidatesParameter,
41
+ ImplicitRankingParameter,
42
+ WeightAllowedParameter,
43
+ } from '@condorcet.vote/cef-writer';
44
+
45
+ const cef = new Cef({ file: '/tmp/election.cvotes' });
46
+
47
+ cef.addComment(new CommentLine('My beautiful election'));
48
+ cef.addParameter(new CandidatesParameter(['Alice', 'Bob', 'Charlie']));
49
+ cef.addParameter(new ImplicitRankingParameter(true));
50
+ cef.addParameter(new WeightAllowedParameter(true));
51
+
52
+ cef.addVote(VoteLine.fromRanking([['Alice'], ['Bob'], ['Charlie']], { quantifier: 42 }));
53
+ cef.addVote(VoteLine.fromRanking([['Charlie'], ['Alice', 'Bob']], { weight: 7, quantifier: 8 }));
54
+ cef.addVote(VoteLine.fromRanking([])); // blank ballot (/EMPTY_RANKING/)
55
+
56
+ cef.close();
57
+ ```
58
+
59
+ produces:
60
+
61
+ ```
62
+ # My beautiful election
63
+ #/Candidates: Alice ; Bob ; Charlie
64
+ #/Implicit Ranking: true
65
+ #/Weight Allowed: true
66
+
67
+ Alice > Bob > Charlie * 42
68
+ Charlie > Alice = Bob ^7 * 8
69
+ /EMPTY_RANKING/
70
+ ```
71
+
72
+ ## Browser usage
73
+
74
+ The library is distributed as modern ESM and can be used in browser environments. File operations (`FileWriteTarget`) require Node.js, but you can use `StringBuffer` or implement a custom `WriteTarget` for browser-based workflows:
75
+
76
+ ```typescript
77
+ import {
78
+ Cef,
79
+ VoteLine,
80
+ CandidatesParameter,
81
+ StringBuffer,
82
+ } from '@condorcet.vote/cef-writer';
83
+
84
+ // In browser: use StringBuffer to generate CEF as a string
85
+ const buffer = new StringBuffer();
86
+ const cef = new Cef({ string: buffer });
87
+
88
+ cef.addParameter(new CandidatesParameter(['Alice', 'Bob', 'Charlie']));
89
+ cef.addVote(VoteLine.fromRanking([['Alice'], ['Bob'], ['Charlie']]));
90
+
91
+ const csvContent = buffer.toString();
92
+ console.log(csvContent);
93
+
94
+ // Download the file client-side
95
+ const blob = new Blob([csvContent], { type: 'text/plain' });
96
+ const url = URL.createObjectURL(blob);
97
+ const link = document.createElement('a');
98
+ link.href = url;
99
+ link.download = 'election.cvotes';
100
+ link.click();
101
+ ```
102
+
103
+ Or implement your own `WriteTarget` for custom backends:
104
+
105
+ ```typescript
106
+ class CustomTarget implements WriteTarget {
107
+ private lines: string[] = [];
108
+
109
+ write(chunk: string): number {
110
+ this.lines.push(chunk);
111
+ return chunk.length;
112
+ }
113
+
114
+ getLinesArray(): string[] {
115
+ return this.lines;
116
+ }
117
+ }
118
+
119
+ const target = new CustomTarget();
120
+ const cef = new Cef({ file: target });
121
+ // ... add parameters and votes
122
+ ```
123
+
124
+ ## Output targets
125
+
126
+ The `Cef` constructor takes an options object with **exactly one** of the
127
+ following keys:
128
+
129
+ | Option | Type | Behavior |
130
+ | --- | --- | --- |
131
+ | `file: path` | `string` | A filesystem path, opened with mode `w` (created/truncated). **Node.js only.** |
132
+ | `file: target` | `WriteTarget` | Any object with `write(chunk: string): number` (e.g. an already-open `FileWriteTarget`); used as-is. |
133
+ | `string: buffer` | `StringBuffer` | Each line is appended to the supplied buffer. |
134
+
135
+ The "string passed by reference" target of the PHP original maps to the
136
+ `StringBuffer` value object — the idiomatic TypeScript stand-in:
137
+
138
+ ```typescript
139
+ import { Cef, CandidatesParameter, StringBuffer } from '@condorcet.vote/cef-writer';
140
+
141
+ const buffer = new StringBuffer();
142
+ const cef = new Cef({ string: buffer });
143
+
144
+ cef.addParameter(new CandidatesParameter(['A', 'B']));
145
+
146
+ console.log(buffer.toString()); // "#/Candidates: A ; B\n"
147
+ ```
148
+
149
+ `cef.file` is the active `WriteTarget` in file mode and `null` in string mode.
150
+ Call `cef.close()` to close the underlying file descriptor when you are done
151
+ (no-op in string mode).
152
+
153
+ ## `autoFormat`
154
+
155
+ `cef.autoFormat` is a public `boolean` (default `true`):
156
+
157
+ - `true` — writes the readable flavor of the spec: spaces around `>`, `=`, `;`, `,`, `||`, `^`, `*`; one blank line is inserted automatically between the parameter block and the first vote.
158
+ - `false` — writes the compact form with no optional whitespace and no auto blank line.
159
+
160
+ ```typescript
161
+ cef.autoFormat = false;
162
+ cef.addParameter(new CandidatesParameter(['A', 'B']));
163
+ cef.addVote(VoteLine.fromRanking([['A'], ['B']]));
164
+ // "#/Candidates:A;B\nA>B\n"
165
+ ```
166
+
167
+ ## Building blocks
168
+
169
+ ### Parameters
170
+
171
+ Each standard parameter has its own typed class. Custom parameters are supported
172
+ via `CustomParameter`. The `StandardParameter` enum lists the exact spec names.
173
+
174
+ | Class | Parameter | Value |
175
+ | --- | --- | --- |
176
+ | `CandidatesParameter` | `Candidates` | `string[]` |
177
+ | `NumberOfSeatsParameter` | `Number of Seats` | integer ≥ 1 |
178
+ | `ImplicitRankingParameter` | `Implicit Ranking` | `boolean` |
179
+ | `VotingMethodsParameter` | `Voting Methods` | `string[]` |
180
+ | `WeightAllowedParameter` | `Weight Allowed` | `boolean` |
181
+ | `CustomParameter` | (free-form) | `(name: string, value: string)` |
182
+
183
+ Parameters can only be added before the first vote — any later call throws
184
+ `InvalidWriterStateException`.
185
+
186
+ ### Vote lines
187
+
188
+ `VoteLine` instances are built through static named constructors — its
189
+ constructor is private, so never use `new VoteLine(...)`.
190
+
191
+ The typed way — `VoteLine.fromRanking()` — then pass it to `cef.addVote()`:
192
+
193
+ ```typescript
194
+ VoteLine.fromRanking(
195
+ [['Alice'], ['Bob', 'Charlie']], // [] => /EMPTY_RANKING/
196
+ {
197
+ tags: ['voter@example.com'],
198
+ weight: 7,
199
+ quantifier: 3,
200
+ inlineComment: 'late ballot',
201
+ },
202
+ );
203
+ ```
204
+
205
+ Each rank is itself a list of tied candidates. An empty top-level ranking emits
206
+ the `/EMPTY_RANKING/` blank-ballot sentinel.
207
+
208
+ The first argument also accepts a ready-made `Ranking` object (see below). Once
209
+ built, the parsed ranking is exposed on the read-only `voteLine.ranking`
210
+ property (use `voteLine.ranking.ranks` for the `string[][]` structure). It is
211
+ `null` only when the ballot was built verbatim via
212
+ `VoteLine.fromRawRankingString()`.
213
+
214
+ #### The `Ranking` value object
215
+
216
+ A ranking can be built, validated and rendered on its own through the `Ranking`
217
+ class — the same abstraction `VoteLine` uses internally:
218
+
219
+ ```typescript
220
+ import { Ranking, VoteLine } from '@condorcet.vote/cef-writer';
221
+
222
+ const ranking = new Ranking([['Alice'], ['Bob', 'Charlie']]); // [] => /EMPTY_RANKING/
223
+ Ranking.fromString('Alice > Bob = Charlie'); // or parse a ranking-only string
224
+
225
+ ranking.ranks; // [['Alice'], ['Bob', 'Charlie']]
226
+ ranking.format(); // "Alice > Bob = Charlie" (relaxed flavor)
227
+ ranking.format(false); // "Alice>Bob=Charlie" (compact flavor)
228
+ String(ranking); // same as format()
229
+
230
+ VoteLine.fromRanking(ranking, { weight: 7 });
231
+ ```
232
+
233
+ `Ranking` is immutable and self-validating: reserved characters, empty ranks and
234
+ duplicate candidates throw a `CefFormatException` at construction time.
235
+ `Ranking.fromString()` accepts only a ranking — every reserved character, the
236
+ `||` tag separator and line breaks are rejected.
237
+
238
+ #### Verbatim ranking — `VoteLine.fromRawRankingString()`
239
+
240
+ When you already have a ranking as text and want it written verbatim (its exact
241
+ spacing preserved, no re-rendering), build the ballot with
242
+ `fromRawRankingString()`. It validates the ranking string with the same rules as
243
+ `Ranking.fromString()` but skips parsing it — the string is stored as-is and
244
+ `voteLine.ranking` is therefore `null`:
245
+
246
+ ```typescript
247
+ const line = VoteLine.fromRawRankingString('Alice>Bob=Charlie', { weight: 7 });
248
+ line.format(true); // "Alice>Bob=Charlie ^7" (ranking kept verbatim)
249
+ line.ranking; // null
250
+ ```
251
+
252
+ Only the library-built companions (the `||` tag separator, `^weight`,
253
+ `*quantifier`) follow `autoFormat`; the ranking itself is never reformatted.
254
+ This is the engine behind `Cef.addRawVote()`.
255
+
256
+ #### From a raw string — `VoteLine.fromString()`
257
+
258
+ Parse a full CEF vote-line string into a `VoteLine` instance. Every component is
259
+ optional except the ranking; both the relaxed (`A > B ^7 * 2`) and the compact
260
+ (`A>B^7*2`) spacing flavors are accepted, plus the `/EMPTY_RANKING/` sentinel.
261
+
262
+ ```typescript
263
+ cef.addVote(VoteLine.fromString('voter@example.com || Alice > Bob ^7 * 3 # late ballot'));
264
+ ```
265
+
266
+ Throws `CefFormatException` on any malformed component.
267
+
268
+ #### Pre-validated raw lines — `Cef.addRawVoteLine()`
269
+
270
+ When you already have ballots as text and want the fastest write path,
271
+ `addRawVoteLine()` skips the `VoteLine` allocation while still enforcing the full
272
+ CEF format:
273
+
274
+ ```typescript
275
+ cef.addRawVoteLine('Alice > Bob = Charlie ^7 * 8');
276
+ ```
277
+
278
+ It strips one trailing line terminator (`\r\n`, `\n`, `\r`), trims, rejects
279
+ empty / multi-line / leading-`#` inputs, then runs `VoteLine.assertValidString()`
280
+ for the same deep validation as `fromString()`. The `autoFormat` flag is not
281
+ applied — what you pass is what gets written.
282
+
283
+ #### Strict, ranking-only raw votes — `Cef.addRawVote()`
284
+
285
+ `addRawVoteLine()` is deliberately permissive: because it accepts a whole vote
286
+ line, the caller can embed tags, a weight, a quantifier or an inline comment
287
+ directly in the text. When the ranking comes from an untrusted source and you
288
+ want a hard guarantee that it cannot smuggle any of that in, use the strict
289
+ sibling `addRawVote()`:
290
+
291
+ ```typescript
292
+ cef.addRawVote('Alice > Bob = Charlie', {
293
+ quantifier: 8,
294
+ weight: 7,
295
+ tags: ['voter@example.com'],
296
+ });
297
+ // "voter@example.com || Alice > Bob = Charlie ^7 * 8"
298
+ ```
299
+
300
+ `vote` may contain only a ranking — candidate names joined by `>` and `=`, or
301
+ the `/EMPTY_RANKING/` sentinel. Any line break, the `||` tag separator, and every
302
+ reserved character (`^`, `*`, `#`, `;`, `,`, `/`) is rejected, so the string can
303
+ never inject a weight, quantifier, tag, inline comment or a second vote. Those
304
+ companions are supplied exclusively through the typed options:
305
+
306
+ ```typescript
307
+ cef.addRawVote(
308
+ vote: string,
309
+ options?: {
310
+ quantifier?: number | null;
311
+ weight?: number | null;
312
+ tags?: readonly string[] | null;
313
+ },
314
+ ): Cef
315
+ ```
316
+
317
+ `weight` and `quantifier` are nullable and default to `null`, in which case they
318
+ are omitted from the output; when provided they must be strictly positive
319
+ integers. Just like `addRawVoteLine()`, the ranking string is written verbatim —
320
+ its original spacing is preserved and `autoFormat` does not reformat it. The
321
+ `autoFormat` flag still governs the layout of the library-built companions.
322
+ Throws `CefFormatException` on any malformed input.
323
+
324
+ #### Validation-only — `VoteLine.assertValidString()`
325
+
326
+ If you want to validate a vote-line string without allocating a `VoteLine` (e.g.
327
+ to pre-flight user input before queueing it elsewhere), call the static
328
+ `assertValidString()` — same pipeline as `fromString()`, no object returned,
329
+ throws `CefFormatException` on any violation.
330
+
331
+ ### Comments and blank lines
332
+
333
+ ```typescript
334
+ cef.addComment(new CommentLine('section divider'));
335
+ cef.addCommentLine('shortcut — builds the CommentLine for you');
336
+ cef.addEmptyLine();
337
+ ```
338
+
339
+ Inline comments attached to vote lines live on `VoteLine.inlineComment`. The CEF
340
+ spec forbids inline comments on parameter lines, so the parameter classes
341
+ intentionally do not expose one.
342
+
343
+ ## Errors
344
+
345
+ Two top-level hierarchies, each for a different layer.
346
+
347
+ ### Format & input violations — `CefFormatException`
348
+
349
+ Base class for every specification or input violation. Catch this one to handle
350
+ any format-related failure uniformly; catch a specific subclass to branch on a
351
+ kind of violation. Each message names the offending field and the rule that was
352
+ broken.
353
+
354
+ | Subclass | Cause |
355
+ | --- | --- |
356
+ | `InvalidUtf8Exception` | A string carrying an unpaired UTF-16 surrogate, which cannot be encoded to well-formed UTF-8 (the TypeScript analog of "non-UTF-8 bytes"). |
357
+ | `ReservedCharacterException` | One of the spec-reserved characters (`> = ; , # / * ^`), a `:` in a custom parameter name, `||` inside a tag, or a leading `#` on a raw vote line. |
358
+ | `InvalidValueException` | Empty required string, embedded line break, null byte, non-positive weight / quantifier, empty `#/Candidates:` or `#/Voting Methods:` list, or empty rank inside a ranking. |
359
+ | `DuplicateCandidateException` | Same candidate label appearing twice in `#/Candidates:` or anywhere inside a ranking (including across tied groups). |
360
+ | `InvalidWriterStateException` | `Cef` constructed with neither a file nor a string target (or with both); parameter added after the first vote; vote-line string parsed without a ranking. |
361
+
362
+ All subclasses extend `CefFormatException`.
363
+
364
+ ### I/O failures — `CefWriteException`
365
+
366
+ Thrown when writing to the underlying target (file or string buffer) fails —
367
+ typically a closed handle, a read-only file, or a full disk. Distinct from
368
+ `CefFormatException` because the cause is I/O, not your input. When the write
369
+ target throws, the original error is preserved on the `cause` property.
370
+
371
+ > [!NOTE]
372
+ > **Differences from the PHP original.** TypeScript strings are sequences of
373
+ > UTF-16 code units, so "invalid UTF-8" is detected as an ill-formed string
374
+ > carrying an unpaired surrogate. The "string passed by reference" target is
375
+ > modeled as the `StringBuffer` value object, and the named-argument
376
+ > constructors (`fromRanking`, `addRawVote`, …) take an options object instead.
377
+ > The public class names, methods, validation rules and output are otherwise
378
+ > identical to the source library.
379
+
380
+ ## Development
381
+
382
+ This repository uses [Bun](https://bun.sh) for development.
383
+
384
+ ```bash
385
+ bun install
386
+ bun run dev # TypeScript watch mode
387
+ bun run build # Compile TypeScript to dist/
388
+ bun run lint # ESLint
389
+ bun run lint:fix # ESLint with auto-fix
390
+ bun run format # Prettier
391
+ bun run type-check # tsc --noEmit
392
+ bun test # Run the test suite
393
+ ```
394
+
395
+ The library is organized to mirror the PHP source architecture:
396
+
397
+ ```
398
+ src/
399
+ ├── index.ts # Public API barrel
400
+ ├── Cef.ts # Streaming writer
401
+ ├── CefFormat.ts # Internal validation helpers (@internal)
402
+ ├── Ranking.ts # Ranking value object
403
+ ├── VoteLine.ts # Ballot value object
404
+ ├── CommentLine.ts # Standalone comment line
405
+ ├── Exception/ # CefFormatException hierarchy + CefWriteException
406
+ └── Parameter/ # ParameterInterface, StandardParameter + typed parameters
407
+ ```
408
+
409
+ ## Contributing
410
+
411
+ Contributions are welcome! Please read our [contributing guide](CONTRIBUTING.md) for details.
412
+
413
+ ## License
414
+
415
+ MIT — see [LICENSE](LICENSE).
416
+
417
+ ## Changelog
418
+
419
+ See [CHANGELOG.md](CHANGELOG.md) for a list of changes in each version.
package/dist/Cef.d.ts ADDED
@@ -0,0 +1,188 @@
1
+ import { CommentLine } from './CommentLine';
2
+ import type { ParameterInterface } from './Parameter';
3
+ import { VoteLine } from './VoteLine';
4
+ /**
5
+ * A sink the writer can stream lines into, mirroring the contract of PHP's
6
+ * `\SplFileObject::fwrite()`: `write()` returns the number of bytes actually
7
+ * written. Returning fewer bytes than supplied (or throwing) signals an I/O
8
+ * failure and triggers a {@link CefWriteException}.
9
+ */
10
+ export interface WriteTarget {
11
+ write(chunk: string): number;
12
+ }
13
+ /**
14
+ * A mutable string sink, the idiomatic TypeScript stand-in for PHP's
15
+ * "string passed by reference" target. The writer appends every line to it;
16
+ * read the accumulated document back with {@link toString} (or {@link value}).
17
+ */
18
+ export declare class StringBuffer {
19
+ private content;
20
+ append(chunk: string): void;
21
+ get value(): string;
22
+ toString(): string;
23
+ }
24
+ /**
25
+ * Options accepted by the {@link Cef} constructor. Exactly one of `file` or
26
+ * `string` must be provided.
27
+ */
28
+ export interface CefOptions {
29
+ /**
30
+ * A filesystem path (opened in truncating write mode) or any
31
+ * {@link WriteTarget} (e.g. an already-open {@link FileWriteTarget}).
32
+ */
33
+ file?: string | WriteTarget;
34
+ /**
35
+ * A {@link StringBuffer} the writer appends every line to.
36
+ */
37
+ string?: StringBuffer;
38
+ }
39
+ /**
40
+ * Streaming writer for a single Condorcet Election Format document.
41
+ *
42
+ * Each `add*()` call emits one line to the underlying target *immediately* —
43
+ * the library never buffers more than a single line in memory and previously
44
+ * written content cannot be edited.
45
+ *
46
+ * The target is chosen at construction time:
47
+ * - a {@link WriteTarget} (passed through);
48
+ * - a filesystem path (opened with mode `w`);
49
+ * - a {@link StringBuffer} that the writer will append to.
50
+ *
51
+ * # Phases
52
+ *
53
+ * Parameters must be emitted before votes. Comments and empty lines may be
54
+ * emitted at any time. Once the first {@link VoteLine} is written, calling
55
+ * {@link addParameter} throws an {@link InvalidWriterStateException}.
56
+ *
57
+ * # autoFormat
58
+ *
59
+ * When `true` (default), the writer follows the visually relaxed flavor of the
60
+ * spec — spaces around `>`, `=`, `;`, `,`; one blank line automatically
61
+ * inserted between the parameter block and the first vote. When `false`, the
62
+ * most compact form is emitted.
63
+ */
64
+ export declare class Cef {
65
+ /**
66
+ * Factory that turns a filesystem path into a {@link WriteTarget}.
67
+ *
68
+ * The browser entry point leaves this `null`, so the browser bundle never
69
+ * references {@link FileWriteTarget} (and therefore never pulls in `node:fs`).
70
+ * The Node entry point wires it to {@link FileWriteTarget}, enabling the
71
+ * `new Cef({ file: '/some/path' })` convenience.
72
+ *
73
+ * @internal
74
+ */
75
+ static fileWriteTargetFactory: ((path: string) => WriteTarget) | null;
76
+ autoFormat: boolean;
77
+ /**
78
+ * The active file target, or `null` when writing to a string.
79
+ */
80
+ readonly file: WriteTarget | null;
81
+ /**
82
+ * Reference to the caller's string buffer in string mode, `null` in file
83
+ * mode.
84
+ */
85
+ private readonly stringTarget;
86
+ private parameterEmitted;
87
+ private voteEmitted;
88
+ private autoSeparatorWritten;
89
+ /**
90
+ * Exactly one of `options.file` or `options.string` must be provided.
91
+ *
92
+ * @throws {InvalidWriterStateException}
93
+ */
94
+ constructor(options?: CefOptions);
95
+ /**
96
+ * Create a file-backed {@link WriteTarget} from a path, via the factory the
97
+ * Node entry point registers in {@link Cef.fileWriteTargetFactory}.
98
+ *
99
+ * @throws {InvalidWriterStateException} when no factory is registered — i.e.
100
+ * in the browser bundle, where filesystem access is unavailable.
101
+ */
102
+ private createFileWriteTarget;
103
+ /**
104
+ * Emit a parameter line `#/Name: value`.
105
+ *
106
+ * @throws {CefFormatException} if a vote has already been written
107
+ */
108
+ addParameter(parameter: ParameterInterface): this;
109
+ /**
110
+ * Emit a vote line. Locks parameter mode permanently.
111
+ */
112
+ addVote(vote: VoteLine): this;
113
+ /**
114
+ * Emit a vote line directly from a pre-built string, skipping the allocation
115
+ * of a {@link VoteLine} instance. Use this when you already have ballots as
116
+ * text and want the fastest path to the output.
117
+ *
118
+ * The full CEF vote-line format is enforced — the same validation rules that
119
+ * {@link VoteLine.fromString} applies are run via
120
+ * {@link VoteLine.assertValidString}. In particular:
121
+ * - structural checks first: a single trailing line terminator
122
+ * (`\r\n`, `\n`, `\r`) is stripped, surrounding whitespace is trimmed,
123
+ * the result must be non-empty, must not contain any remaining
124
+ * `\r`/`\n`, and must not start with `#` (which would be a comment or a
125
+ * parameter line, not a vote);
126
+ * - format checks then: tags, ranking, weight, quantifier and inline
127
+ * comment are parsed and validated against every CEF rule.
128
+ *
129
+ * The `autoFormat` flag has no effect on a raw line: what you pass is what
130
+ * gets written (after structural cleaning).
131
+ *
132
+ * @throws {CefFormatException}
133
+ */
134
+ addRawVoteLine(line: string): this;
135
+ /**
136
+ * Emit a vote line from a **ranking-only** string plus strictly-typed
137
+ * companions — the secure, paranoid sibling of {@link addRawVoteLine}.
138
+ *
139
+ * Whereas {@link addRawVoteLine} accepts a full vote line (and therefore lets
140
+ * the caller embed tags, a weight, a quantifier or an inline comment inside
141
+ * the text), `addRawVote()` guarantees that `vote` carries *only* a ranking.
142
+ * Any line break, the `||` tag separator, and every reserved character
143
+ * (`^`, `*`, `#`, `;`, `,`, `/`) are rejected, so the string can never
144
+ * smuggle a weight, quantifier, tag, inline comment or second vote into the
145
+ * output. Use this when the ranking comes from an untrusted source.
146
+ *
147
+ * Weight, quantifier and tags are supplied exclusively through the typed
148
+ * options. `weight` and `quantifier` are nullable and default to `null`, in
149
+ * which case they are omitted from the output; when provided they must be
150
+ * strictly positive. Just like {@link addRawVoteLine}, the ranking string is
151
+ * written verbatim — its original spacing is preserved and `autoFormat` does
152
+ * not reformat it. The `autoFormat` flag still governs the layout of the
153
+ * library-built companions (the `||` tag separator, `^weight`, `*quantifier`).
154
+ *
155
+ * @throws {CefFormatException}
156
+ */
157
+ addRawVote(vote: string, options?: {
158
+ quantifier?: number | null;
159
+ weight?: number | null;
160
+ tags?: readonly string[] | null;
161
+ }): this;
162
+ /**
163
+ * Emit a standalone comment line.
164
+ */
165
+ addComment(comment: CommentLine): this;
166
+ /**
167
+ * Convenience helper: build a {@link CommentLine} from raw text and emit it
168
+ * in a single call.
169
+ */
170
+ addCommentLine(text: string): this;
171
+ /**
172
+ * Emit an empty line.
173
+ */
174
+ addEmptyLine(): this;
175
+ /**
176
+ * Close the underlying file target, if any. No-op in string mode or when the
177
+ * target does not expose a `close()` method.
178
+ */
179
+ close(): void;
180
+ /**
181
+ * Insert one blank line between the parameter block and the first vote when
182
+ * `autoFormat` is on. Idempotent.
183
+ */
184
+ private writeAutoSeparatorIfNeeded;
185
+ private renderInlineComment;
186
+ private writeLine;
187
+ }
188
+ //# sourceMappingURL=Cef.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Cef.d.ts","sourceRoot":"","sources":["../src/Cef.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAO5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CAC9B;AAED;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAM;IAEd,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIlC,IAAW,KAAK,IAAI,MAAM,CAEzB;IAEM,QAAQ,IAAI,MAAM;CAG1B;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IAE5B;;OAEG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,GAAG;IACd;;;;;;;;;OASG;IACH,OAAc,sBAAsB,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,KAAK,WAAW,CAAC,GAAG,IAAI,CAAQ;IAE7E,UAAU,UAAQ;IAEzB;;OAEG;IACH,SAAgB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IAEzC;;;OAGG;IACH,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsB;IAEnD,OAAO,CAAC,gBAAgB,CAAS;IAEjC,OAAO,CAAC,WAAW,CAAS;IAE5B,OAAO,CAAC,oBAAoB,CAAS;IAErC;;;;OAIG;gBACgB,OAAO,GAAE,UAAe;IAkC3C;;;;;;OAMG;IACH,OAAO,CAAC,qBAAqB;IAc7B;;;;OAIG;IACI,YAAY,CAAC,SAAS,EAAE,kBAAkB,GAAG,IAAI;IAexD;;OAEG;IACI,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAepC;;;;;;;;;;;;;;;;;;;;OAoBG;IACI,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IA6BzC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACI,UAAU,CACf,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;QACP,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;KAC5B,GACL,IAAI;IAcP;;OAEG;IACI,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;IAM7C;;;OAGG;IACI,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIzC;;OAEG;IACI,YAAY,IAAI,IAAI;IAM3B;;;OAGG;IACI,KAAK,IAAI,IAAI;IAMpB;;;OAGG;IACH,OAAO,CAAC,0BAA0B;IAYlC,OAAO,CAAC,mBAAmB;IAU3B,OAAO,CAAC,SAAS;CAoClB"}