openlimiter 0.1.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/dist/ingest.js ADDED
@@ -0,0 +1,114 @@
1
+ import path from "node:path";
2
+ import { MANUAL_FILE_MARKER, MANUAL_FILE_NAME } from "@openlimiter/connectors";
3
+ import { mergeSnapshotCache, readJsonFileSafely, resolveStateDirectory } from "@openlimiter/core";
4
+ /** Largest document accepted on standard input. */
5
+ export const STDIN_BYTE_LIMIT = 262_144;
6
+ /** Hard ceiling on how long a command waits for standard input. */
7
+ export const STDIN_TIMEOUT_MILLISECONDS = 500;
8
+ /** Largest manual document accepted from the state directory. */
9
+ export const MANUAL_FILE_BYTE_LIMIT = 65_536;
10
+ /**
11
+ * Read standard input without ever blocking a caller forever.
12
+ *
13
+ * A terminal is treated as no input at all, the stream is bounded by size, and
14
+ * a stream that never ends still resolves once the timeout elapses. Claude Code
15
+ * writes its statusline payload and closes the pipe, so the common path costs
16
+ * one tick rather than the full timeout.
17
+ */
18
+ export async function readStandardInputText(stream = process.stdin, byteLimit = STDIN_BYTE_LIMIT, timeoutMilliseconds = STDIN_TIMEOUT_MILLISECONDS) {
19
+ if (stream.isTTY === true)
20
+ return null;
21
+ return await new Promise((resolve) => {
22
+ const chunks = [];
23
+ let total = 0;
24
+ let settled = false;
25
+ const collected = () => chunks.length === 0 ? null : Buffer.concat(chunks).toString("utf8");
26
+ const finish = (value) => {
27
+ if (settled)
28
+ return;
29
+ settled = true;
30
+ clearTimeout(timer);
31
+ stream.off("data", onData);
32
+ stream.off("end", onEnd);
33
+ stream.off("error", onError);
34
+ stream.pause();
35
+ resolve(value);
36
+ };
37
+ const onData = (chunk) => {
38
+ total += chunk.length;
39
+ if (total > byteLimit) {
40
+ finish(null);
41
+ return;
42
+ }
43
+ chunks.push(chunk);
44
+ };
45
+ const onEnd = () => finish(collected());
46
+ const onError = () => finish(null);
47
+ const timer = setTimeout(() => finish(collected()), timeoutMilliseconds);
48
+ stream.on("data", onData);
49
+ stream.on("end", onEnd);
50
+ stream.on("error", onError);
51
+ stream.resume();
52
+ });
53
+ }
54
+ /** Parse untrusted text as JSON without throwing. */
55
+ export function parseJsonText(text) {
56
+ if (text === null)
57
+ return { ok: false };
58
+ const trimmed = text.trim();
59
+ if (trimmed === "" || trimmed.length > STDIN_BYTE_LIMIT)
60
+ return { ok: false };
61
+ try {
62
+ return { ok: true, value: JSON.parse(trimmed) };
63
+ }
64
+ catch {
65
+ return { ok: false };
66
+ }
67
+ }
68
+ function manualFilePath(directory) {
69
+ return path.join(directory ?? resolveStateDirectory(), MANUAL_FILE_NAME);
70
+ }
71
+ /**
72
+ * Read the manual quota document from the state directory.
73
+ *
74
+ * The document is untrusted input. It is bounded by size, opened through the
75
+ * same descriptor first read as the cache, and handed to the manual connector
76
+ * for validation. A missing or unreadable file is simply no data.
77
+ */
78
+ export async function readManualDocument(directory) {
79
+ const document = await readJsonFileSafely(manualFilePath(directory), MANUAL_FILE_BYTE_LIMIT);
80
+ return document.ok ? document.value : undefined;
81
+ }
82
+ /** Report whether a manual document is present, without reading it. */
83
+ export async function manualFilePresent(directory) {
84
+ const document = await readJsonFileSafely(manualFilePath(directory), MANUAL_FILE_BYTE_LIMIT);
85
+ return document.ok;
86
+ }
87
+ /**
88
+ * Build the environment map a connector sees.
89
+ *
90
+ * Local facts that only the CLI can observe become explicit markers, so
91
+ * detection stays a pure function of the environment and doctor never claims a
92
+ * connector is ready when it can receive no data.
93
+ */
94
+ export async function environmentWithLocalMarkers(environment, directory) {
95
+ const resolved = { ...environment };
96
+ if (await manualFilePresent(directory)) {
97
+ resolved[MANUAL_FILE_MARKER] = "available";
98
+ }
99
+ else {
100
+ delete resolved[MANUAL_FILE_MARKER];
101
+ }
102
+ return resolved;
103
+ }
104
+ /**
105
+ * Merge validated snapshots into the cache.
106
+ *
107
+ * Quota observed by one command stays visible to the next one. The read, the
108
+ * merge, and the write happen under one lock inside the core, so a concurrent
109
+ * writer cannot drop these rows.
110
+ */
111
+ export async function persistSnapshots(incoming, directory) {
112
+ return await mergeSnapshotCache(incoming, directory ?? resolveStateDirectory());
113
+ }
114
+ //# sourceMappingURL=ingest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ingest.js","sourceRoot":"","sources":["../src/ingest.ts"],"names":[],"mappings":"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"}
package/dist/qr.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A QR encoder, written here so the package keeps its promise of zero third
3
+ * party runtime dependencies.
4
+ *
5
+ * It covers exactly what `openlimiter serve` needs and nothing more: byte mode,
6
+ * error correction level M, and versions 1 through 6. That ceiling is a real
7
+ * simplification rather than an arbitrary one, because version information
8
+ * blocks only appear from version 7 upward, so this encoder never has to write
9
+ * them. Version 6 carries 106 bytes, and the longest address this tool prints
10
+ * is around 55 characters, so the ceiling is roughly twice what it needs.
11
+ *
12
+ * The output is a square matrix of booleans. Rendering it is a separate step,
13
+ * so the encoder itself is pure and can be checked against the published test
14
+ * vectors without a terminal anywhere near it.
15
+ *
16
+ * References used: ISO/IEC 18004 tables 7, 9 and 13 for capacities, block
17
+ * structure and alignment centres, annex I for the worked example the tests
18
+ * check the Reed Solomon step against.
19
+ */
20
+ /** Highest version this encoder writes. Above it, version blocks appear. */
21
+ export declare const MAX_QR_VERSION = 6;
22
+ /** Modules a version measures on a side. */
23
+ export declare function qrSize(version: number): number;
24
+ /**
25
+ * Reed Solomon error correction codewords for one block.
26
+ *
27
+ * Exported because it is the one step with a published test vector, which is
28
+ * how the tests prove the field arithmetic is right rather than merely
29
+ * self consistent.
30
+ */
31
+ export declare function errorCorrectionCodewords(data: Uint8Array, count: number): Uint8Array;
32
+ export interface QrMatrix {
33
+ readonly version: number;
34
+ readonly size: number;
35
+ readonly mask: number;
36
+ readonly modules: readonly (readonly boolean[])[];
37
+ }
38
+ /**
39
+ * Encode text as a QR matrix.
40
+ *
41
+ * Byte mode, error correction level M, and the mask chosen by running the four
42
+ * published penalty rules over all eight candidates.
43
+ */
44
+ export declare function encodeQr(text: string): QrMatrix;
45
+ export interface QrRenderOptions {
46
+ /**
47
+ * Paint each module with an explicit background colour.
48
+ *
49
+ * A scanner needs dark modules on a light field, and a terminal with a dark
50
+ * theme would otherwise invert the whole symbol and make it unreadable. When
51
+ * colour is off the symbol still renders, but only a light themed terminal
52
+ * will scan it.
53
+ */
54
+ color?: boolean;
55
+ }
56
+ /**
57
+ * Render a matrix as text, two module rows to every line.
58
+ *
59
+ * The upper half block character paints its top half in the foreground colour
60
+ * and its bottom half in the background colour, so one character carries two
61
+ * module rows and the symbol comes out roughly square in a terminal cell grid.
62
+ */
63
+ export declare function renderQr(matrix: QrMatrix, options?: QrRenderOptions): string;
64
+ //# sourceMappingURL=qr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qr.d.ts","sourceRoot":"","sources":["../src/qr.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,4EAA4E;AAC5E,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,4CAA4C;AAC5C,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9C;AAuED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,MAAM,GACZ,UAAU,CAaZ;AAmVD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,CAAC;CACnD;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAwB/C;AAOD,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAE,eAAoB,GAAG,MAAM,CA+BhF"}
package/dist/qr.js ADDED
@@ -0,0 +1,494 @@
1
+ /**
2
+ * A QR encoder, written here so the package keeps its promise of zero third
3
+ * party runtime dependencies.
4
+ *
5
+ * It covers exactly what `openlimiter serve` needs and nothing more: byte mode,
6
+ * error correction level M, and versions 1 through 6. That ceiling is a real
7
+ * simplification rather than an arbitrary one, because version information
8
+ * blocks only appear from version 7 upward, so this encoder never has to write
9
+ * them. Version 6 carries 106 bytes, and the longest address this tool prints
10
+ * is around 55 characters, so the ceiling is roughly twice what it needs.
11
+ *
12
+ * The output is a square matrix of booleans. Rendering it is a separate step,
13
+ * so the encoder itself is pure and can be checked against the published test
14
+ * vectors without a terminal anywhere near it.
15
+ *
16
+ * References used: ISO/IEC 18004 tables 7, 9 and 13 for capacities, block
17
+ * structure and alignment centres, annex I for the worked example the tests
18
+ * check the Reed Solomon step against.
19
+ */
20
+ /** Highest version this encoder writes. Above it, version blocks appear. */
21
+ export const MAX_QR_VERSION = 6;
22
+ /** Modules a version measures on a side. */
23
+ export function qrSize(version) {
24
+ return 17 + version * 4;
25
+ }
26
+ /* Level M only. Every row is the published structure for that version. */
27
+ const VERSIONS = [
28
+ { blocks: [16], ecPerBlock: 10, remainderBits: 0, alignment: [] },
29
+ { blocks: [28], ecPerBlock: 16, remainderBits: 7, alignment: [6, 18] },
30
+ { blocks: [44], ecPerBlock: 26, remainderBits: 7, alignment: [6, 22] },
31
+ { blocks: [32, 32], ecPerBlock: 18, remainderBits: 7, alignment: [6, 26] },
32
+ { blocks: [43, 43], ecPerBlock: 24, remainderBits: 7, alignment: [6, 30] },
33
+ { blocks: [27, 27, 27, 27], ecPerBlock: 16, remainderBits: 7, alignment: [6, 34] }
34
+ ];
35
+ function specFor(version) {
36
+ const spec = VERSIONS[version - 1];
37
+ if (spec === undefined)
38
+ throw new Error("Unsupported QR version");
39
+ return spec;
40
+ }
41
+ function dataCodewords(spec) {
42
+ return spec.blocks.reduce((total, count) => total + count, 0);
43
+ }
44
+ /* ------------------------------------------------------------- GF(256) */
45
+ const EXP = new Uint8Array(512);
46
+ const LOG = new Uint8Array(256);
47
+ {
48
+ let value = 1;
49
+ for (let i = 0; i < 255; i += 1) {
50
+ EXP[i] = value;
51
+ LOG[value] = i;
52
+ value <<= 1;
53
+ /* The primitive polynomial the QR standard fixes, x^8 + x^4 + x^3 + x^2 + 1. */
54
+ if (value & 0x100)
55
+ value ^= 0x11d;
56
+ }
57
+ for (let i = 255; i < 512; i += 1)
58
+ EXP[i] = EXP[i - 255] ?? 0;
59
+ }
60
+ function multiply(left, right) {
61
+ if (left === 0 || right === 0)
62
+ return 0;
63
+ return EXP[((LOG[left] ?? 0) + (LOG[right] ?? 0)) % 255] ?? 0;
64
+ }
65
+ /** The generator polynomial for a run of error correction codewords. */
66
+ function generatorPolynomial(degree) {
67
+ let polynomial = new Uint8Array([1]);
68
+ for (let index = 0; index < degree; index += 1) {
69
+ const next = new Uint8Array(polynomial.length + 1);
70
+ for (let position = 0; position < polynomial.length; position += 1) {
71
+ const coefficient = polynomial[position] ?? 0;
72
+ next[position] = (next[position] ?? 0) ^ coefficient;
73
+ next[position + 1] =
74
+ (next[position + 1] ?? 0) ^ multiply(coefficient, EXP[index] ?? 0);
75
+ }
76
+ polynomial = next;
77
+ }
78
+ return polynomial;
79
+ }
80
+ /**
81
+ * Reed Solomon error correction codewords for one block.
82
+ *
83
+ * Exported because it is the one step with a published test vector, which is
84
+ * how the tests prove the field arithmetic is right rather than merely
85
+ * self consistent.
86
+ */
87
+ export function errorCorrectionCodewords(data, count) {
88
+ const generator = generatorPolynomial(count);
89
+ const remainder = new Uint8Array(count);
90
+ for (const byte of data) {
91
+ const factor = byte ^ (remainder[0] ?? 0);
92
+ remainder.copyWithin(0, 1);
93
+ remainder[count - 1] = 0;
94
+ for (let index = 0; index < count; index += 1) {
95
+ remainder[index] =
96
+ (remainder[index] ?? 0) ^ multiply(generator[index + 1] ?? 0, factor);
97
+ }
98
+ }
99
+ return remainder;
100
+ }
101
+ /* --------------------------------------------------------------- bits */
102
+ class BitWriter {
103
+ bits = [];
104
+ push(value, length) {
105
+ for (let index = length - 1; index >= 0; index -= 1) {
106
+ this.bits.push((value >>> index) & 1);
107
+ }
108
+ }
109
+ get length() {
110
+ return this.bits.length;
111
+ }
112
+ toBytes() {
113
+ const bytes = new Uint8Array(Math.ceil(this.bits.length / 8));
114
+ this.bits.forEach((bit, index) => {
115
+ if (bit === 1) {
116
+ const position = index >>> 3;
117
+ bytes[position] = (bytes[position] ?? 0) | (0x80 >>> (index & 7));
118
+ }
119
+ });
120
+ return bytes;
121
+ }
122
+ }
123
+ function chooseVersion(byteLength) {
124
+ for (let version = 1; version <= MAX_QR_VERSION; version += 1) {
125
+ /* Four bits of mode plus eight bits of length is exactly two codewords. */
126
+ if (byteLength + 2 <= dataCodewords(specFor(version)))
127
+ return version;
128
+ }
129
+ throw new Error("Text is too long for this QR encoder");
130
+ }
131
+ function buildCodewords(bytes, version) {
132
+ const spec = specFor(version);
133
+ const capacity = dataCodewords(spec);
134
+ const writer = new BitWriter();
135
+ writer.push(0b0100, 4);
136
+ /* Byte mode on versions 1 to 9 carries its length in eight bits. */
137
+ writer.push(bytes.length, 8);
138
+ for (const byte of bytes)
139
+ writer.push(byte, 8);
140
+ const terminator = Math.min(4, capacity * 8 - writer.length);
141
+ writer.push(0, terminator);
142
+ while (writer.length % 8 !== 0)
143
+ writer.push(0, 1);
144
+ const padded = Array.from(writer.toBytes());
145
+ /* The two pad codewords the standard fixes, alternating to the capacity. */
146
+ const pad = [0xec, 0x11];
147
+ const filled = padded.length;
148
+ while (padded.length < capacity) {
149
+ padded.push(pad[(padded.length - filled) % 2] ?? 0xec);
150
+ }
151
+ return Uint8Array.from(padded);
152
+ }
153
+ /** Split into blocks, add error correction, and interleave as the standard says. */
154
+ function interleave(codewords, version) {
155
+ const spec = specFor(version);
156
+ const dataBlocks = [];
157
+ const ecBlocks = [];
158
+ let offset = 0;
159
+ for (const size of spec.blocks) {
160
+ const block = codewords.slice(offset, offset + size);
161
+ offset += size;
162
+ dataBlocks.push(block);
163
+ ecBlocks.push(errorCorrectionCodewords(block, spec.ecPerBlock));
164
+ }
165
+ const result = [];
166
+ const longest = Math.max(...spec.blocks);
167
+ for (let index = 0; index < longest; index += 1) {
168
+ for (const block of dataBlocks) {
169
+ const byte = block[index];
170
+ if (byte !== undefined)
171
+ result.push(byte);
172
+ }
173
+ }
174
+ for (let index = 0; index < spec.ecPerBlock; index += 1) {
175
+ for (const block of ecBlocks) {
176
+ const byte = block[index];
177
+ if (byte !== undefined)
178
+ result.push(byte);
179
+ }
180
+ }
181
+ return Uint8Array.from(result);
182
+ }
183
+ function emptyGrid(size) {
184
+ return Array.from({ length: size }, () => new Int8Array(size).fill(-1));
185
+ }
186
+ function place(grid, row, column, dark) {
187
+ const line = grid[row];
188
+ if (line === undefined)
189
+ return;
190
+ line[column] = dark ? 1 : 0;
191
+ }
192
+ function at(grid, row, column) {
193
+ return grid[row]?.[column] ?? -1;
194
+ }
195
+ function drawFinder(grid, row, column) {
196
+ for (let y = -1; y <= 7; y += 1) {
197
+ for (let x = -1; x <= 7; x += 1) {
198
+ const targetRow = row + y;
199
+ const targetColumn = column + x;
200
+ if (targetRow < 0 ||
201
+ targetColumn < 0 ||
202
+ targetRow >= grid.length ||
203
+ targetColumn >= grid.length)
204
+ continue;
205
+ const onRing = (y >= 0 && y <= 6 && (x === 0 || x === 6)) ||
206
+ (x >= 0 && x <= 6 && (y === 0 || y === 6));
207
+ const inCore = y >= 2 && y <= 4 && x >= 2 && x <= 4;
208
+ place(grid, targetRow, targetColumn, onRing || inCore);
209
+ }
210
+ }
211
+ }
212
+ function drawAlignment(grid, row, column) {
213
+ for (let y = -2; y <= 2; y += 1) {
214
+ for (let x = -2; x <= 2; x += 1) {
215
+ const ring = Math.max(Math.abs(x), Math.abs(y));
216
+ place(grid, row + y, column + x, ring !== 1);
217
+ }
218
+ }
219
+ }
220
+ function drawFunctionPatterns(grid, version) {
221
+ const size = grid.length;
222
+ drawFinder(grid, 0, 0);
223
+ drawFinder(grid, 0, size - 7);
224
+ drawFinder(grid, size - 7, 0);
225
+ for (let index = 8; index < size - 8; index += 1) {
226
+ const dark = index % 2 === 0;
227
+ place(grid, 6, index, dark);
228
+ place(grid, index, 6, dark);
229
+ }
230
+ /*
231
+ * One list of centres covers both axes, so every pair is a candidate. The
232
+ * three pairs that would land on a finder are the only ones skipped.
233
+ */
234
+ const centres = specFor(version).alignment;
235
+ for (const row of centres) {
236
+ for (const column of centres) {
237
+ const onFinder = (row === 6 && column === 6) ||
238
+ (row === 6 && column === size - 7) ||
239
+ (row === size - 7 && column === 6);
240
+ if (!onFinder)
241
+ drawAlignment(grid, row, column);
242
+ }
243
+ }
244
+ /* The dark module, fixed by the standard, and always dark. */
245
+ place(grid, size - 8, 8, true);
246
+ /* Reserve the format areas so data placement steps over them. */
247
+ for (let index = 0; index <= 8; index += 1) {
248
+ if (at(grid, 8, index) === -1)
249
+ place(grid, 8, index, false);
250
+ if (at(grid, index, 8) === -1)
251
+ place(grid, index, 8, false);
252
+ }
253
+ for (let index = 0; index < 8; index += 1) {
254
+ if (at(grid, 8, size - 1 - index) === -1)
255
+ place(grid, 8, size - 1 - index, false);
256
+ if (at(grid, size - 1 - index, 8) === -1)
257
+ place(grid, size - 1 - index, 8, false);
258
+ }
259
+ }
260
+ function functionMask(version) {
261
+ const size = qrSize(version);
262
+ const grid = emptyGrid(size);
263
+ drawFunctionPatterns(grid, version);
264
+ return Array.from({ length: size }, (_unused, row) => Array.from({ length: size }, (_ignored, column) => at(grid, row, column) !== -1));
265
+ }
266
+ /**
267
+ * Walk the symbol in the published order and drop the codeword bits in.
268
+ *
269
+ * The walk is two modules wide, starts at the bottom right, and alternates
270
+ * direction on every pair of columns. Column six carries the vertical timing
271
+ * pattern, so the pair that would straddle it shifts one column left and every
272
+ * later pair follows from there. Modules left over once the bits run out stay
273
+ * light, which is exactly what the remainder bits are.
274
+ */
275
+ function placeData(grid, reserved, bits) {
276
+ const size = grid.length;
277
+ let bitIndex = 0;
278
+ for (let right = size - 1; right >= 1; right -= 2) {
279
+ if (right === 6)
280
+ right = 5;
281
+ for (let step = 0; step < size; step += 1) {
282
+ for (let offset = 0; offset < 2; offset += 1) {
283
+ const column = right - offset;
284
+ const upward = ((right + 1) & 2) === 0;
285
+ const row = upward ? size - 1 - step : step;
286
+ if (reserved[row]?.[column] === true)
287
+ continue;
288
+ let dark = false;
289
+ if (bitIndex < bits.length * 8) {
290
+ const byte = bits[bitIndex >>> 3] ?? 0;
291
+ dark = ((byte >>> (7 - (bitIndex & 7))) & 1) === 1;
292
+ }
293
+ bitIndex += 1;
294
+ place(grid, row, column, dark);
295
+ }
296
+ }
297
+ }
298
+ }
299
+ const MASKS = [
300
+ (row, column) => (row + column) % 2 === 0,
301
+ (row) => row % 2 === 0,
302
+ (_row, column) => column % 3 === 0,
303
+ (row, column) => (row + column) % 3 === 0,
304
+ (row, column) => (Math.floor(row / 2) + Math.floor(column / 3)) % 2 === 0,
305
+ (row, column) => ((row * column) % 2) + ((row * column) % 3) === 0,
306
+ (row, column) => (((row * column) % 2) + ((row * column) % 3)) % 2 === 0,
307
+ (row, column) => (((row + column) % 2) + ((row * column) % 3)) % 2 === 0
308
+ ];
309
+ function applyMask(grid, reserved, mask) {
310
+ const rule = MASKS[mask];
311
+ if (rule === undefined)
312
+ throw new Error("Unknown mask");
313
+ const size = grid.length;
314
+ return Array.from({ length: size }, (_unused, row) => Array.from({ length: size }, (_ignored, column) => {
315
+ const value = at(grid, row, column) === 1;
316
+ if (reserved[row]?.[column] === true)
317
+ return value;
318
+ return rule(row, column) ? !value : value;
319
+ }));
320
+ }
321
+ function runPenalty(line) {
322
+ let penalty = 0;
323
+ let run = 1;
324
+ for (let index = 1; index < line.length; index += 1) {
325
+ if (line[index] === line[index - 1]) {
326
+ run += 1;
327
+ continue;
328
+ }
329
+ if (run >= 5)
330
+ penalty += 3 + (run - 5);
331
+ run = 1;
332
+ }
333
+ if (run >= 5)
334
+ penalty += 3 + (run - 5);
335
+ return penalty;
336
+ }
337
+ const FINDER_LIKE = [true, false, true, true, true, false, true, false, false, false, false];
338
+ function finderPenalty(line) {
339
+ let penalty = 0;
340
+ for (let index = 0; index + 11 <= line.length; index += 1) {
341
+ const forward = FINDER_LIKE.every((value, offset) => line[index + offset] === value);
342
+ const backward = FINDER_LIKE.every((value, offset) => line[index + 10 - offset] === value);
343
+ if (forward || backward)
344
+ penalty += 40;
345
+ }
346
+ return penalty;
347
+ }
348
+ function penalty(modules) {
349
+ const size = modules.length;
350
+ let total = 0;
351
+ let dark = 0;
352
+ const columns = Array.from({ length: size }, () => []);
353
+ for (let row = 0; row < size; row += 1) {
354
+ const line = modules[row] ?? [];
355
+ total += runPenalty(line);
356
+ total += finderPenalty(line);
357
+ for (let column = 0; column < size; column += 1) {
358
+ const value = line[column] === true;
359
+ columns[column]?.push(value);
360
+ if (value)
361
+ dark += 1;
362
+ if (row > 0 && column > 0) {
363
+ const up = modules[row - 1]?.[column] === true;
364
+ const left = line[column - 1] === true;
365
+ const upLeft = modules[row - 1]?.[column - 1] === true;
366
+ if (value === up && value === left && value === upLeft)
367
+ total += 3;
368
+ }
369
+ }
370
+ }
371
+ for (const column of columns) {
372
+ total += runPenalty(column);
373
+ total += finderPenalty(column);
374
+ }
375
+ const ratio = (dark * 100) / (size * size);
376
+ total += Math.floor(Math.abs(ratio - 50) / 5) * 10;
377
+ return total;
378
+ }
379
+ /** Fifteen bits of format information, error correction level M. */
380
+ function formatBits(mask) {
381
+ /* Level M is 00, and the three mask bits follow it. */
382
+ const data = mask;
383
+ let remainder = data << 10;
384
+ for (let bit = 14; bit >= 10; bit -= 1) {
385
+ if ((remainder >>> bit) & 1)
386
+ remainder ^= 0x537 << (bit - 10);
387
+ }
388
+ return ((data << 10) | remainder) ^ 0x5412;
389
+ }
390
+ /**
391
+ * Write the fifteen format bits into both of the places they live.
392
+ *
393
+ * The first copy wraps the top left finder, the second is split between the
394
+ * strip under the top right finder and the strip beside the bottom left one.
395
+ * The dark module is rewritten last, because it sits inside the second copy's
396
+ * run and is fixed by the standard whatever the bits say.
397
+ */
398
+ function writeFormat(modules, mask) {
399
+ const size = modules.length;
400
+ const bits = formatBits(mask);
401
+ const dark = (index) => ((bits >>> index) & 1) === 1;
402
+ const set = (row, column, value) => {
403
+ const line = modules[row];
404
+ if (line !== undefined)
405
+ line[column] = value;
406
+ };
407
+ for (let index = 0; index <= 5; index += 1)
408
+ set(index, 8, dark(index));
409
+ set(7, 8, dark(6));
410
+ set(8, 8, dark(7));
411
+ set(8, 7, dark(8));
412
+ for (let index = 9; index <= 14; index += 1)
413
+ set(8, 14 - index, dark(index));
414
+ for (let index = 0; index <= 7; index += 1)
415
+ set(8, size - 1 - index, dark(index));
416
+ for (let index = 8; index <= 14; index += 1)
417
+ set(size - 15 + index, 8, dark(index));
418
+ set(size - 8, 8, true);
419
+ }
420
+ /**
421
+ * Encode text as a QR matrix.
422
+ *
423
+ * Byte mode, error correction level M, and the mask chosen by running the four
424
+ * published penalty rules over all eight candidates.
425
+ */
426
+ export function encodeQr(text) {
427
+ const bytes = new TextEncoder().encode(text);
428
+ const version = chooseVersion(bytes.length);
429
+ const codewords = interleave(buildCodewords(bytes, version), version);
430
+ const reserved = functionMask(version);
431
+ const grid = emptyGrid(qrSize(version));
432
+ drawFunctionPatterns(grid, version);
433
+ placeData(grid, reserved, codewords);
434
+ let best = null;
435
+ let bestMask = 0;
436
+ let bestScore = Number.POSITIVE_INFINITY;
437
+ for (let mask = 0; mask < MASKS.length; mask += 1) {
438
+ const candidate = applyMask(grid, reserved, mask);
439
+ writeFormat(candidate, mask);
440
+ const score = penalty(candidate);
441
+ if (score < bestScore) {
442
+ bestScore = score;
443
+ best = candidate;
444
+ bestMask = mask;
445
+ }
446
+ }
447
+ if (best === null)
448
+ throw new Error("No mask could be chosen");
449
+ return { version, size: qrSize(version), mask: bestMask, modules: best };
450
+ }
451
+ /* ------------------------------------------------------------ rendering */
452
+ /** Modules of light margin around the symbol. Four is the published minimum. */
453
+ const QUIET_ZONE = 4;
454
+ /**
455
+ * Render a matrix as text, two module rows to every line.
456
+ *
457
+ * The upper half block character paints its top half in the foreground colour
458
+ * and its bottom half in the background colour, so one character carries two
459
+ * module rows and the symbol comes out roughly square in a terminal cell grid.
460
+ */
461
+ export function renderQr(matrix, options = {}) {
462
+ const color = options.color ?? false;
463
+ const span = matrix.size + QUIET_ZONE * 2;
464
+ const dark = (row, column) => {
465
+ const y = row - QUIET_ZONE;
466
+ const x = column - QUIET_ZONE;
467
+ if (y < 0 || x < 0 || y >= matrix.size || x >= matrix.size)
468
+ return false;
469
+ return matrix.modules[y]?.[x] === true;
470
+ };
471
+ const lines = [];
472
+ for (let row = 0; row < span; row += 2) {
473
+ let line = "";
474
+ let previous = "";
475
+ for (let column = 0; column < span; column += 1) {
476
+ const top = dark(row, column);
477
+ const bottom = row + 1 < span ? dark(row + 1, column) : false;
478
+ if (!color) {
479
+ line += top && bottom ? "█" : top ? "▀" : bottom ? "▄" : " ";
480
+ continue;
481
+ }
482
+ /* Black on bright white, stated explicitly so the theme cannot invert it. */
483
+ const codes = (top ? "30" : "97") + ";" + (bottom ? "40" : "107");
484
+ if (codes !== previous) {
485
+ line += "\u001b[" + codes + "m";
486
+ previous = codes;
487
+ }
488
+ line += "▀";
489
+ }
490
+ lines.push(color ? line + "\u001b[0m" : line);
491
+ }
492
+ return lines.join("\n");
493
+ }
494
+ //# sourceMappingURL=qr.js.map