@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/dist/Cef.js ADDED
@@ -0,0 +1,291 @@
1
+ import { CommentLine } from './CommentLine';
2
+ import { CefWriteException, InvalidValueException, InvalidWriterStateException, ReservedCharacterException, } from './Exception';
3
+ import { VoteLine } from './VoteLine';
4
+ /**
5
+ * A mutable string sink, the idiomatic TypeScript stand-in for PHP's
6
+ * "string passed by reference" target. The writer appends every line to it;
7
+ * read the accumulated document back with {@link toString} (or {@link value}).
8
+ */
9
+ export class StringBuffer {
10
+ content = '';
11
+ append(chunk) {
12
+ this.content += chunk;
13
+ }
14
+ get value() {
15
+ return this.content;
16
+ }
17
+ toString() {
18
+ return this.content;
19
+ }
20
+ }
21
+ /**
22
+ * Streaming writer for a single Condorcet Election Format document.
23
+ *
24
+ * Each `add*()` call emits one line to the underlying target *immediately* —
25
+ * the library never buffers more than a single line in memory and previously
26
+ * written content cannot be edited.
27
+ *
28
+ * The target is chosen at construction time:
29
+ * - a {@link WriteTarget} (passed through);
30
+ * - a filesystem path (opened with mode `w`);
31
+ * - a {@link StringBuffer} that the writer will append to.
32
+ *
33
+ * # Phases
34
+ *
35
+ * Parameters must be emitted before votes. Comments and empty lines may be
36
+ * emitted at any time. Once the first {@link VoteLine} is written, calling
37
+ * {@link addParameter} throws an {@link InvalidWriterStateException}.
38
+ *
39
+ * # autoFormat
40
+ *
41
+ * When `true` (default), the writer follows the visually relaxed flavor of the
42
+ * spec — spaces around `>`, `=`, `;`, `,`; one blank line automatically
43
+ * inserted between the parameter block and the first vote. When `false`, the
44
+ * most compact form is emitted.
45
+ */
46
+ export class Cef {
47
+ /**
48
+ * Factory that turns a filesystem path into a {@link WriteTarget}.
49
+ *
50
+ * The browser entry point leaves this `null`, so the browser bundle never
51
+ * references {@link FileWriteTarget} (and therefore never pulls in `node:fs`).
52
+ * The Node entry point wires it to {@link FileWriteTarget}, enabling the
53
+ * `new Cef({ file: '/some/path' })` convenience.
54
+ *
55
+ * @internal
56
+ */
57
+ static fileWriteTargetFactory = null;
58
+ autoFormat = true;
59
+ /**
60
+ * The active file target, or `null` when writing to a string.
61
+ */
62
+ file;
63
+ /**
64
+ * Reference to the caller's string buffer in string mode, `null` in file
65
+ * mode.
66
+ */
67
+ stringTarget;
68
+ parameterEmitted = false;
69
+ voteEmitted = false;
70
+ autoSeparatorWritten = false;
71
+ /**
72
+ * Exactly one of `options.file` or `options.string` must be provided.
73
+ *
74
+ * @throws {InvalidWriterStateException}
75
+ */
76
+ constructor(options = {}) {
77
+ const hasFile = options.file !== undefined;
78
+ const hasString = options.string !== undefined;
79
+ if (hasFile === hasString) {
80
+ throw new InvalidWriterStateException('Exactly one of file or string must be provided to the Cef constructor.');
81
+ }
82
+ if (hasString) {
83
+ this.file = null;
84
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
85
+ this.stringTarget = options.string;
86
+ return;
87
+ }
88
+ this.stringTarget = null;
89
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
90
+ const file = options.file;
91
+ if (typeof file === 'string') {
92
+ this.file = this.createFileWriteTarget(file);
93
+ }
94
+ else if (typeof file === 'object' && typeof file.write === 'function') {
95
+ this.file = file;
96
+ }
97
+ else {
98
+ throw new InvalidWriterStateException('The file option must be a string path or a WriteTarget.');
99
+ }
100
+ }
101
+ /**
102
+ * Create a file-backed {@link WriteTarget} from a path, via the factory the
103
+ * Node entry point registers in {@link Cef.fileWriteTargetFactory}.
104
+ *
105
+ * @throws {InvalidWriterStateException} when no factory is registered — i.e.
106
+ * in the browser bundle, where filesystem access is unavailable.
107
+ */
108
+ createFileWriteTarget(path) {
109
+ const factory = Cef.fileWriteTargetFactory;
110
+ if (factory === null) {
111
+ throw new InvalidWriterStateException('Writing to a file path is not available in this environment (browser?). ' +
112
+ 'Use StringBuffer instead, or pass a custom WriteTarget. In Node.js, ' +
113
+ "import from '@condorcet.vote/cef-writer' (not '@condorcet.vote/cef-writer/browser').");
114
+ }
115
+ return factory(path);
116
+ }
117
+ /**
118
+ * Emit a parameter line `#/Name: value`.
119
+ *
120
+ * @throws {CefFormatException} if a vote has already been written
121
+ */
122
+ addParameter(parameter) {
123
+ if (this.voteEmitted) {
124
+ throw new InvalidWriterStateException('Parameters must be written before any vote line.');
125
+ }
126
+ const separator = this.autoFormat ? ': ' : ':';
127
+ const line = '#/' + parameter.getName() + separator + parameter.getFormattedValue(this.autoFormat);
128
+ this.writeLine(line);
129
+ this.parameterEmitted = true;
130
+ return this;
131
+ }
132
+ /**
133
+ * Emit a vote line. Locks parameter mode permanently.
134
+ */
135
+ addVote(vote) {
136
+ this.writeAutoSeparatorIfNeeded();
137
+ let line = vote.format(this.autoFormat);
138
+ if (vote.inlineComment !== null) {
139
+ line += this.renderInlineComment(vote.inlineComment);
140
+ }
141
+ this.writeLine(line);
142
+ this.voteEmitted = true;
143
+ return this;
144
+ }
145
+ /**
146
+ * Emit a vote line directly from a pre-built string, skipping the allocation
147
+ * of a {@link VoteLine} instance. Use this when you already have ballots as
148
+ * text and want the fastest path to the output.
149
+ *
150
+ * The full CEF vote-line format is enforced — the same validation rules that
151
+ * {@link VoteLine.fromString} applies are run via
152
+ * {@link VoteLine.assertValidString}. In particular:
153
+ * - structural checks first: a single trailing line terminator
154
+ * (`\r\n`, `\n`, `\r`) is stripped, surrounding whitespace is trimmed,
155
+ * the result must be non-empty, must not contain any remaining
156
+ * `\r`/`\n`, and must not start with `#` (which would be a comment or a
157
+ * parameter line, not a vote);
158
+ * - format checks then: tags, ranking, weight, quantifier and inline
159
+ * comment are parsed and validated against every CEF rule.
160
+ *
161
+ * The `autoFormat` flag has no effect on a raw line: what you pass is what
162
+ * gets written (after structural cleaning).
163
+ *
164
+ * @throws {CefFormatException}
165
+ */
166
+ addRawVoteLine(line) {
167
+ let cleaned = line.replace(/\r\n$|[\r\n]$/, '');
168
+ cleaned = cleaned.trim();
169
+ if (cleaned === '') {
170
+ throw new InvalidValueException('Raw vote line cannot be empty.');
171
+ }
172
+ if (/[\r\n]/.test(cleaned)) {
173
+ throw new InvalidValueException('Raw vote line must be a single line; embedded newlines are not allowed.');
174
+ }
175
+ if (cleaned.startsWith('#')) {
176
+ throw new ReservedCharacterException('Raw vote line cannot start with "#"; that would be a comment or parameter line, not a vote.');
177
+ }
178
+ VoteLine.assertValidString(cleaned);
179
+ this.writeAutoSeparatorIfNeeded();
180
+ this.writeLine(cleaned);
181
+ this.voteEmitted = true;
182
+ return this;
183
+ }
184
+ /**
185
+ * Emit a vote line from a **ranking-only** string plus strictly-typed
186
+ * companions — the secure, paranoid sibling of {@link addRawVoteLine}.
187
+ *
188
+ * Whereas {@link addRawVoteLine} accepts a full vote line (and therefore lets
189
+ * the caller embed tags, a weight, a quantifier or an inline comment inside
190
+ * the text), `addRawVote()` guarantees that `vote` carries *only* a ranking.
191
+ * Any line break, the `||` tag separator, and every reserved character
192
+ * (`^`, `*`, `#`, `;`, `,`, `/`) are rejected, so the string can never
193
+ * smuggle a weight, quantifier, tag, inline comment or second vote into the
194
+ * output. Use this when the ranking comes from an untrusted source.
195
+ *
196
+ * Weight, quantifier and tags are supplied exclusively through the typed
197
+ * options. `weight` and `quantifier` are nullable and default to `null`, in
198
+ * which case they are omitted from the output; when provided they must be
199
+ * strictly positive. Just like {@link addRawVoteLine}, the ranking string is
200
+ * written verbatim — its original spacing is preserved and `autoFormat` does
201
+ * not reformat it. The `autoFormat` flag still governs the layout of the
202
+ * library-built companions (the `||` tag separator, `^weight`, `*quantifier`).
203
+ *
204
+ * @throws {CefFormatException}
205
+ */
206
+ addRawVote(vote, options = {}) {
207
+ const voteLine = VoteLine.fromRawRankingString(vote, {
208
+ tags: options.tags ?? [],
209
+ weight: options.weight ?? null,
210
+ quantifier: options.quantifier ?? null,
211
+ });
212
+ this.writeAutoSeparatorIfNeeded();
213
+ this.writeLine(voteLine.format(this.autoFormat));
214
+ this.voteEmitted = true;
215
+ return this;
216
+ }
217
+ /**
218
+ * Emit a standalone comment line.
219
+ */
220
+ addComment(comment) {
221
+ this.writeLine(comment.format(this.autoFormat));
222
+ return this;
223
+ }
224
+ /**
225
+ * Convenience helper: build a {@link CommentLine} from raw text and emit it
226
+ * in a single call.
227
+ */
228
+ addCommentLine(text) {
229
+ return this.addComment(new CommentLine(text));
230
+ }
231
+ /**
232
+ * Emit an empty line.
233
+ */
234
+ addEmptyLine() {
235
+ this.writeLine('');
236
+ return this;
237
+ }
238
+ /**
239
+ * Close the underlying file target, if any. No-op in string mode or when the
240
+ * target does not expose a `close()` method.
241
+ */
242
+ close() {
243
+ if (this.file !== null && 'close' in this.file && typeof this.file.close === 'function') {
244
+ this.file.close();
245
+ }
246
+ }
247
+ /**
248
+ * Insert one blank line between the parameter block and the first vote when
249
+ * `autoFormat` is on. Idempotent.
250
+ */
251
+ writeAutoSeparatorIfNeeded() {
252
+ if (this.autoFormat &&
253
+ this.parameterEmitted &&
254
+ !this.voteEmitted &&
255
+ !this.autoSeparatorWritten) {
256
+ this.writeLine('');
257
+ this.autoSeparatorWritten = true;
258
+ }
259
+ }
260
+ renderInlineComment(comment) {
261
+ if (!this.autoFormat) {
262
+ return '#' + comment;
263
+ }
264
+ const needsLeadingSpace = comment === '' || !comment.startsWith(' ');
265
+ return ' #' + (needsLeadingSpace ? ' ' : '') + comment;
266
+ }
267
+ writeLine(content) {
268
+ const line = content + '\n';
269
+ if (this.file !== null) {
270
+ const expected = Buffer.byteLength(line, 'utf-8');
271
+ let written;
272
+ try {
273
+ written = this.file.write(line);
274
+ }
275
+ catch (error) {
276
+ throw new CefWriteException(`Failed to write ${String(expected)} bytes to the file target. ` +
277
+ 'The underlying handle may be closed, read-only, or out of space.', { cause: error });
278
+ }
279
+ if (written < expected) {
280
+ throw new CefWriteException(`Failed to write ${String(expected)} bytes to the file target (write returned ${String(written)}). ` +
281
+ 'The underlying handle may be closed, read-only, or out of space.');
282
+ }
283
+ return;
284
+ }
285
+ if (this.stringTarget === null) {
286
+ throw new InvalidWriterStateException('Cef writer has no target: neither file nor string is set. ' +
287
+ 'This indicates a corrupted internal state that the constructor should have prevented.');
288
+ }
289
+ this.stringTarget.append(line);
290
+ }
291
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Internal helpers shared across the writer and the value objects.
3
+ *
4
+ * A namespace of static utilities that encode rules from the CEF specification
5
+ * (reserved characters, blank-ballot sentinel, etc.).
6
+ *
7
+ * @internal
8
+ */
9
+ export declare namespace CefFormat {
10
+ /**
11
+ * Characters that the specification reserves for syntactic use and that
12
+ * therefore must never appear inside any user-supplied value.
13
+ */
14
+ const RESERVED_CHARACTERS: readonly string[];
15
+ /**
16
+ * Sentinel value emitted as the whole ranking of a blank ballot.
17
+ */
18
+ const EMPTY_RANKING = "/EMPTY_RANKING/";
19
+ /**
20
+ * Tag/ranking separator on a vote line.
21
+ */
22
+ const TAGS_SEPARATOR = "||";
23
+ /**
24
+ * Reject any value that contains a reserved character or a line break. Empty
25
+ * strings are rejected too — this helper is meant for *required* structural
26
+ * values (names, tags, candidate labels, …).
27
+ *
28
+ * @throws {CefFormatException}
29
+ */
30
+ function assertValueIsClean(value: string, context: string): void;
31
+ /**
32
+ * Reject any value that contains a reserved character, a line break, a null
33
+ * byte, or an invalid UTF-8 byte sequence. Accept the empty string. Use for
34
+ * optionally-empty value strings (e.g. a custom parameter's free-form value).
35
+ *
36
+ * @throws {CefFormatException}
37
+ */
38
+ function assertNoReservedNorLineBreak(value: string, context: string): void;
39
+ /**
40
+ * Inline comments are free-form text but must stay on a single line and
41
+ * contain only valid UTF-8 (with no null byte).
42
+ *
43
+ * @throws {CefFormatException}
44
+ */
45
+ function assertSingleLine(value: string, context: string): void;
46
+ /**
47
+ * Reject any value that contains the `||` tag separator.
48
+ *
49
+ * The separator is the only forbidden pattern that per-character validation
50
+ * cannot catch on its own, because `|` is not itself a reserved character.
51
+ * Both ranking strings and tag values rely on this check.
52
+ *
53
+ * @throws {CefFormatException}
54
+ */
55
+ function assertNoTagSeparator(value: string, context: string): void;
56
+ }
57
+ //# sourceMappingURL=CefFormat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CefFormat.d.ts","sourceRoot":"","sources":["../src/CefFormat.ts"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AAEH,yBAAiB,SAAS,CAAC;IACzB;;;OAGG;IACI,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAShD,CAAC;IAEF;;OAEG;IACI,MAAM,aAAa,oBAAoB,CAAC;IAE/C;;OAEG;IACI,MAAM,cAAc,OAAO,CAAC;IAEnC;;;;;;OAMG;IACH,SAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAMvE;IAED;;;;;;OAMG;IACH,SAAgB,4BAA4B,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAUjF;IAED;;;;;OAKG;IACH,SAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAErE;IAED;;;;;;;;OAQG;IACH,SAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAMzE;CAmDF"}
@@ -0,0 +1,133 @@
1
+ import { InvalidUtf8Exception, InvalidValueException, ReservedCharacterException, } from './Exception';
2
+ /**
3
+ * Internal helpers shared across the writer and the value objects.
4
+ *
5
+ * A namespace of static utilities that encode rules from the CEF specification
6
+ * (reserved characters, blank-ballot sentinel, etc.).
7
+ *
8
+ * @internal
9
+ */
10
+ // eslint-disable-next-line @typescript-eslint/no-namespace
11
+ export var CefFormat;
12
+ (function (CefFormat) {
13
+ /**
14
+ * Characters that the specification reserves for syntactic use and that
15
+ * therefore must never appear inside any user-supplied value.
16
+ */
17
+ CefFormat.RESERVED_CHARACTERS = [
18
+ '>',
19
+ '=',
20
+ ';',
21
+ ',',
22
+ '#',
23
+ '/',
24
+ '*',
25
+ '^',
26
+ ];
27
+ /**
28
+ * Sentinel value emitted as the whole ranking of a blank ballot.
29
+ */
30
+ CefFormat.EMPTY_RANKING = '/EMPTY_RANKING/';
31
+ /**
32
+ * Tag/ranking separator on a vote line.
33
+ */
34
+ CefFormat.TAGS_SEPARATOR = '||';
35
+ /**
36
+ * Reject any value that contains a reserved character or a line break. Empty
37
+ * strings are rejected too — this helper is meant for *required* structural
38
+ * values (names, tags, candidate labels, …).
39
+ *
40
+ * @throws {CefFormatException}
41
+ */
42
+ function assertValueIsClean(value, context) {
43
+ if (value === '') {
44
+ throw new InvalidValueException(`${context} cannot be empty.`);
45
+ }
46
+ assertNoReservedNorLineBreak(value, context);
47
+ }
48
+ CefFormat.assertValueIsClean = assertValueIsClean;
49
+ /**
50
+ * Reject any value that contains a reserved character, a line break, a null
51
+ * byte, or an invalid UTF-8 byte sequence. Accept the empty string. Use for
52
+ * optionally-empty value strings (e.g. a custom parameter's free-form value).
53
+ *
54
+ * @throws {CefFormatException}
55
+ */
56
+ function assertNoReservedNorLineBreak(value, context) {
57
+ assertSafeText(value, context);
58
+ for (const reserved of CefFormat.RESERVED_CHARACTERS) {
59
+ if (value.includes(reserved)) {
60
+ throw new ReservedCharacterException(`${context} cannot contain the reserved character "${reserved}".`);
61
+ }
62
+ }
63
+ }
64
+ CefFormat.assertNoReservedNorLineBreak = assertNoReservedNorLineBreak;
65
+ /**
66
+ * Inline comments are free-form text but must stay on a single line and
67
+ * contain only valid UTF-8 (with no null byte).
68
+ *
69
+ * @throws {CefFormatException}
70
+ */
71
+ function assertSingleLine(value, context) {
72
+ assertSafeText(value, context);
73
+ }
74
+ CefFormat.assertSingleLine = assertSingleLine;
75
+ /**
76
+ * Reject any value that contains the `||` tag separator.
77
+ *
78
+ * The separator is the only forbidden pattern that per-character validation
79
+ * cannot catch on its own, because `|` is not itself a reserved character.
80
+ * Both ranking strings and tag values rely on this check.
81
+ *
82
+ * @throws {CefFormatException}
83
+ */
84
+ function assertNoTagSeparator(value, context) {
85
+ if (value.includes(CefFormat.TAGS_SEPARATOR)) {
86
+ throw new ReservedCharacterException(`${context} cannot contain the "${CefFormat.TAGS_SEPARATOR}" tag separator.`);
87
+ }
88
+ }
89
+ CefFormat.assertNoTagSeparator = assertNoTagSeparator;
90
+ /**
91
+ * Verify that `value` is valid UTF-8, single-line, and contains no null byte.
92
+ * Shared base for every value-bound assertion above.
93
+ *
94
+ * UTF-8 validity is checked by rejecting ill-formed strings — those carrying
95
+ * an unpaired UTF-16 surrogate, which cannot be encoded to well-formed
96
+ * UTF-8. This is the TypeScript analog of PHP's `mb_check_encoding()`.
97
+ *
98
+ * @throws {CefFormatException}
99
+ */
100
+ function assertSafeText(value, context) {
101
+ if (!isWellFormedUtf16(value)) {
102
+ throw new InvalidUtf8Exception(`${context} contains an invalid UTF-8 byte sequence.`);
103
+ }
104
+ if (/[\r\n]/.test(value)) {
105
+ throw new InvalidValueException(`${context} cannot contain a line break.`);
106
+ }
107
+ if (value.includes('\0')) {
108
+ throw new InvalidValueException(`${context} cannot contain a null byte.`);
109
+ }
110
+ }
111
+ /**
112
+ * Return `true` when every UTF-16 surrogate in `value` is correctly paired,
113
+ * i.e. the string can be encoded to well-formed UTF-8.
114
+ */
115
+ function isWellFormedUtf16(value) {
116
+ for (let i = 0; i < value.length; i++) {
117
+ const code = value.charCodeAt(i);
118
+ if (code >= 0xd800 && code <= 0xdbff) {
119
+ // High surrogate: must be immediately followed by a low surrogate.
120
+ const next = value.charCodeAt(i + 1);
121
+ if (Number.isNaN(next) || next < 0xdc00 || next > 0xdfff) {
122
+ return false;
123
+ }
124
+ i++;
125
+ }
126
+ else if (code >= 0xdc00 && code <= 0xdfff) {
127
+ // Lone low surrogate.
128
+ return false;
129
+ }
130
+ }
131
+ return true;
132
+ }
133
+ })(CefFormat || (CefFormat = {}));
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A standalone comment line (`# text`).
3
+ *
4
+ * Inline comments attached to vote lines are not represented by this class —
5
+ * they live on `VoteLine.inlineComment` instead.
6
+ */
7
+ export declare class CommentLine {
8
+ readonly text: string;
9
+ /**
10
+ * @throws {CefFormatException}
11
+ */
12
+ constructor(text: string);
13
+ /**
14
+ * Render the line *without* trailing newline.
15
+ */
16
+ format(autoFormat?: boolean): string;
17
+ }
18
+ //# sourceMappingURL=CommentLine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommentLine.d.ts","sourceRoot":"","sources":["../src/CommentLine.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,qBAAa,WAAW;IACtB,SAAgB,IAAI,EAAE,MAAM,CAAC;IAE7B;;OAEG;gBACgB,IAAI,EAAE,MAAM;IAK/B;;OAEG;IACI,MAAM,CAAC,UAAU,UAAO,GAAG,MAAM;CASzC"}
@@ -0,0 +1,27 @@
1
+ import { CefFormat } from './CefFormat';
2
+ /**
3
+ * A standalone comment line (`# text`).
4
+ *
5
+ * Inline comments attached to vote lines are not represented by this class —
6
+ * they live on `VoteLine.inlineComment` instead.
7
+ */
8
+ export class CommentLine {
9
+ text;
10
+ /**
11
+ * @throws {CefFormatException}
12
+ */
13
+ constructor(text) {
14
+ CefFormat.assertSingleLine(text, 'Comment');
15
+ this.text = text;
16
+ }
17
+ /**
18
+ * Render the line *without* trailing newline.
19
+ */
20
+ format(autoFormat = true) {
21
+ if (this.text === '') {
22
+ return '#';
23
+ }
24
+ const needsLeadingSpace = autoFormat && !this.text.startsWith(' ');
25
+ return '#' + (needsLeadingSpace ? ' ' : '') + this.text;
26
+ }
27
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Base class for every input/format violation thrown by the library.
3
+ *
4
+ * Catch this class to handle any format-related failure uniformly; catch one
5
+ * of the dedicated subclasses ({@link InvalidUtf8Exception},
6
+ * {@link ReservedCharacterException}, {@link InvalidValueException},
7
+ * {@link DuplicateCandidateException}, {@link InvalidWriterStateException}) to
8
+ * branch on a specific kind of violation.
9
+ *
10
+ * Non-final on purpose so the library and downstream callers can refine the
11
+ * hierarchy further if needed.
12
+ */
13
+ export declare class CefFormatException extends Error {
14
+ constructor(message: string);
15
+ }
16
+ //# sourceMappingURL=CefFormatException.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CefFormatException.d.ts","sourceRoot":"","sources":["../../src/Exception/CefFormatException.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM;CAKnC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Base class for every input/format violation thrown by the library.
3
+ *
4
+ * Catch this class to handle any format-related failure uniformly; catch one
5
+ * of the dedicated subclasses ({@link InvalidUtf8Exception},
6
+ * {@link ReservedCharacterException}, {@link InvalidValueException},
7
+ * {@link DuplicateCandidateException}, {@link InvalidWriterStateException}) to
8
+ * branch on a specific kind of violation.
9
+ *
10
+ * Non-final on purpose so the library and downstream callers can refine the
11
+ * hierarchy further if needed.
12
+ */
13
+ export class CefFormatException extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = new.target.name;
17
+ Object.setPrototypeOf(this, new.target.prototype);
18
+ }
19
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Thrown when writing to the underlying target (a file or string buffer) fails.
3
+ * Distinct from {@link CefFormatException} because the cause is the I/O layer —
4
+ * disk full, broken pipe, closed handle, read-only file — not an invalid input
5
+ * from the caller.
6
+ */
7
+ export declare class CefWriteException extends Error {
8
+ constructor(message: string, options?: {
9
+ cause?: unknown;
10
+ });
11
+ }
12
+ //# sourceMappingURL=CefWriteException.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CefWriteException.d.ts","sourceRoot":"","sources":["../../src/Exception/CefWriteException.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;gBACvB,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAKlE"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Thrown when writing to the underlying target (a file or string buffer) fails.
3
+ * Distinct from {@link CefFormatException} because the cause is the I/O layer —
4
+ * disk full, broken pipe, closed handle, read-only file — not an invalid input
5
+ * from the caller.
6
+ */
7
+ export class CefWriteException extends Error {
8
+ constructor(message, options) {
9
+ super(message, options);
10
+ this.name = new.target.name;
11
+ Object.setPrototypeOf(this, new.target.prototype);
12
+ }
13
+ }
@@ -0,0 +1,9 @@
1
+ import { CefFormatException } from './CefFormatException';
2
+ /**
3
+ * Thrown when the same candidate label appears more than once where the CEF
4
+ * specification forbids it — either in `#/Candidates:` or in a single vote's
5
+ * ranking (across tied groups included).
6
+ */
7
+ export declare class DuplicateCandidateException extends CefFormatException {
8
+ }
9
+ //# sourceMappingURL=DuplicateCandidateException.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DuplicateCandidateException.d.ts","sourceRoot":"","sources":["../../src/Exception/DuplicateCandidateException.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;;;GAIG;AACH,qBAAa,2BAA4B,SAAQ,kBAAkB;CAAG"}
@@ -0,0 +1,8 @@
1
+ import { CefFormatException } from './CefFormatException';
2
+ /**
3
+ * Thrown when the same candidate label appears more than once where the CEF
4
+ * specification forbids it — either in `#/Candidates:` or in a single vote's
5
+ * ranking (across tied groups included).
6
+ */
7
+ export class DuplicateCandidateException extends CefFormatException {
8
+ }
@@ -0,0 +1,13 @@
1
+ import { CefFormatException } from './CefFormatException';
2
+ /**
3
+ * Thrown when a value contains a byte sequence that does not decode as valid
4
+ * UTF-8. The CEF specification mandates UTF-8, so any non-UTF-8 input is
5
+ * rejected before it can land in the output stream.
6
+ *
7
+ * In this TypeScript port — where strings are sequences of UTF-16 code units —
8
+ * "non-UTF-8" means an ill-formed string carrying an unpaired surrogate, which
9
+ * cannot be encoded to well-formed UTF-8.
10
+ */
11
+ export declare class InvalidUtf8Exception extends CefFormatException {
12
+ }
13
+ //# sourceMappingURL=InvalidUtf8Exception.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InvalidUtf8Exception.d.ts","sourceRoot":"","sources":["../../src/Exception/InvalidUtf8Exception.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;;;;;;;GAQG;AACH,qBAAa,oBAAqB,SAAQ,kBAAkB;CAAG"}
@@ -0,0 +1,12 @@
1
+ import { CefFormatException } from './CefFormatException';
2
+ /**
3
+ * Thrown when a value contains a byte sequence that does not decode as valid
4
+ * UTF-8. The CEF specification mandates UTF-8, so any non-UTF-8 input is
5
+ * rejected before it can land in the output stream.
6
+ *
7
+ * In this TypeScript port — where strings are sequences of UTF-16 code units —
8
+ * "non-UTF-8" means an ill-formed string carrying an unpaired surrogate, which
9
+ * cannot be encoded to well-formed UTF-8.
10
+ */
11
+ export class InvalidUtf8Exception extends CefFormatException {
12
+ }