@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.
- package/LICENSE +21 -0
- package/README.md +419 -0
- package/dist/Cef.d.ts +188 -0
- package/dist/Cef.d.ts.map +1 -0
- package/dist/Cef.js +291 -0
- package/dist/CefFormat.d.ts +57 -0
- package/dist/CefFormat.d.ts.map +1 -0
- package/dist/CefFormat.js +133 -0
- package/dist/CommentLine.d.ts +18 -0
- package/dist/CommentLine.d.ts.map +1 -0
- package/dist/CommentLine.js +27 -0
- package/dist/Exception/CefFormatException.d.ts +16 -0
- package/dist/Exception/CefFormatException.d.ts.map +1 -0
- package/dist/Exception/CefFormatException.js +19 -0
- package/dist/Exception/CefWriteException.d.ts +12 -0
- package/dist/Exception/CefWriteException.d.ts.map +1 -0
- package/dist/Exception/CefWriteException.js +13 -0
- package/dist/Exception/DuplicateCandidateException.d.ts +9 -0
- package/dist/Exception/DuplicateCandidateException.d.ts.map +1 -0
- package/dist/Exception/DuplicateCandidateException.js +8 -0
- package/dist/Exception/InvalidUtf8Exception.d.ts +13 -0
- package/dist/Exception/InvalidUtf8Exception.d.ts.map +1 -0
- package/dist/Exception/InvalidUtf8Exception.js +12 -0
- package/dist/Exception/InvalidValueException.d.ts +15 -0
- package/dist/Exception/InvalidValueException.d.ts.map +1 -0
- package/dist/Exception/InvalidValueException.js +14 -0
- package/dist/Exception/InvalidWriterStateException.d.ts +14 -0
- package/dist/Exception/InvalidWriterStateException.d.ts.map +1 -0
- package/dist/Exception/InvalidWriterStateException.js +13 -0
- package/dist/Exception/ReservedCharacterException.d.ts +12 -0
- package/dist/Exception/ReservedCharacterException.d.ts.map +1 -0
- package/dist/Exception/ReservedCharacterException.js +11 -0
- package/dist/Exception/index.d.ts +8 -0
- package/dist/Exception/index.d.ts.map +1 -0
- package/dist/Exception/index.js +7 -0
- package/dist/FileWriteTarget.d.ts +27 -0
- package/dist/FileWriteTarget.d.ts.map +1 -0
- package/dist/FileWriteTarget.js +35 -0
- package/dist/Parameter/CandidatesParameter.d.ts +20 -0
- package/dist/Parameter/CandidatesParameter.d.ts.map +1 -0
- package/dist/Parameter/CandidatesParameter.js +39 -0
- package/dist/Parameter/CustomParameter.d.ts +19 -0
- package/dist/Parameter/CustomParameter.d.ts.map +1 -0
- package/dist/Parameter/CustomParameter.js +35 -0
- package/dist/Parameter/ImplicitRankingParameter.d.ts +11 -0
- package/dist/Parameter/ImplicitRankingParameter.d.ts.map +1 -0
- package/dist/Parameter/ImplicitRankingParameter.js +16 -0
- package/dist/Parameter/NumberOfSeatsParameter.d.ts +14 -0
- package/dist/Parameter/NumberOfSeatsParameter.d.ts.map +1 -0
- package/dist/Parameter/NumberOfSeatsParameter.js +23 -0
- package/dist/Parameter/ParameterInterface.d.ts +20 -0
- package/dist/Parameter/ParameterInterface.d.ts.map +1 -0
- package/dist/Parameter/ParameterInterface.js +1 -0
- package/dist/Parameter/StandardParameter.d.ts +14 -0
- package/dist/Parameter/StandardParameter.d.ts.map +1 -0
- package/dist/Parameter/StandardParameter.js +14 -0
- package/dist/Parameter/VotingMethodsParameter.d.ts +16 -0
- package/dist/Parameter/VotingMethodsParameter.d.ts.map +1 -0
- package/dist/Parameter/VotingMethodsParameter.js +29 -0
- package/dist/Parameter/WeightAllowedParameter.d.ts +11 -0
- package/dist/Parameter/WeightAllowedParameter.d.ts.map +1 -0
- package/dist/Parameter/WeightAllowedParameter.js +16 -0
- package/dist/Parameter/index.d.ts +9 -0
- package/dist/Parameter/index.d.ts.map +1 -0
- package/dist/Parameter/index.js +7 -0
- package/dist/Ranking.d.ts +91 -0
- package/dist/Ranking.d.ts.map +1 -0
- package/dist/Ranking.js +162 -0
- package/dist/VoteLine.d.ts +156 -0
- package/dist/VoteLine.d.ts.map +1 -0
- package/dist/VoteLine.js +289 -0
- package/dist/index.browser.d.ts +17 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +16 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.node.d.ts +19 -0
- package/dist/index.node.d.ts.map +1 -0
- package/dist/index.node.js +25 -0
- package/package.json +79 -0
- package/src/Cef.ts +405 -0
- package/src/CefFormat.ts +152 -0
- package/src/CommentLine.ts +32 -0
- package/src/Exception/CefFormatException.ts +19 -0
- package/src/Exception/CefWriteException.ts +13 -0
- package/src/Exception/DuplicateCandidateException.ts +8 -0
- package/src/Exception/InvalidUtf8Exception.ts +12 -0
- package/src/Exception/InvalidValueException.ts +14 -0
- package/src/Exception/InvalidWriterStateException.ts +13 -0
- package/src/Exception/ReservedCharacterException.ts +11 -0
- package/src/Exception/index.ts +7 -0
- package/src/FileWriteTarget.ts +42 -0
- package/src/Parameter/CandidatesParameter.ts +49 -0
- package/src/Parameter/CustomParameter.ts +45 -0
- package/src/Parameter/ImplicitRankingParameter.ts +17 -0
- package/src/Parameter/NumberOfSeatsParameter.ts +25 -0
- package/src/Parameter/ParameterInterface.ts +20 -0
- package/src/Parameter/StandardParameter.ts +13 -0
- package/src/Parameter/VotingMethodsParameter.ts +36 -0
- package/src/Parameter/WeightAllowedParameter.ts +17 -0
- package/src/Parameter/index.ts +8 -0
- package/src/Ranking.ts +197 -0
- package/src/VoteLine.ts +398 -0
- package/src/index.browser.ts +36 -0
- package/src/index.node.ts +47 -0
- 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"}
|