chore(encoding): eslint/tsconfig migration
Migrate to eslint flat config (qr-code.ts override for the mask-penalty sliding-window) and composite tsconfig.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { base, compose, imports, stylistic, typescript } from '@robonen/eslint';
|
||||
|
||||
export default compose(base, typescript, imports, stylistic, {
|
||||
name: 'encoding/overrides',
|
||||
files: ['src/qr/qr-code.ts'],
|
||||
rules: {
|
||||
'@stylistic/max-statements-per-line': 'off',
|
||||
'@stylistic/no-mixed-operators': 'off',
|
||||
/* Uniform sliding-window register shift (h6 = h5; h5 = h4; …) where the
|
||||
oldest register's seed/last write is intentionally dead — keep symmetry. */
|
||||
'no-useless-assignment': 'off',
|
||||
},
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import { defineConfig } from 'oxlint';
|
||||
import { compose, base, typescript, imports, stylistic } from '@robonen/oxlint';
|
||||
|
||||
export default defineConfig(compose(base, typescript, imports, stylistic, {
|
||||
overrides: [
|
||||
{
|
||||
files: ['src/qr/qr-code.ts'],
|
||||
rules: {
|
||||
'@stylistic/max-statements-per-line': 'off',
|
||||
'@stylistic/no-mixed-operators': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
@@ -34,19 +34,18 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"lint:check": "oxlint -c oxlint.config.ts",
|
||||
"lint:fix": "oxlint -c oxlint.config.ts --fix",
|
||||
"lint:check": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"test": "vitest run",
|
||||
"dev": "vitest dev",
|
||||
"bench": "vitest bench",
|
||||
"build": "tsdown"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@robonen/oxlint": "workspace:*",
|
||||
"@robonen/eslint": "workspace:*",
|
||||
"@robonen/tsconfig": "workspace:*",
|
||||
"@robonen/tsdown": "workspace:*",
|
||||
"@stylistic/eslint-plugin": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"eslint": "catalog:",
|
||||
"tsdown": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { bench, describe } from 'vitest';
|
||||
import { encodeBinary, encodeSegments, encodeText, makeSegments, LOW, EccMap } from '..';
|
||||
import { EccMap, LOW, encodeBinary, encodeSegments, encodeText, makeSegments } from '..';
|
||||
|
||||
/* -- Test data -- */
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { encodeText, encodeBinary, encodeSegments, makeSegments, isNumeric, isAlphanumeric, QrCode, QrCodeDataType, EccMap, LOW, MEDIUM, QUARTILE, HIGH } from '..';
|
||||
import { EccMap, HIGH, LOW, MEDIUM, QUARTILE, QrCode, QrCodeDataType, encodeBinary, encodeSegments, encodeText, isAlphanumeric, isNumeric, makeSegments } from '..';
|
||||
|
||||
describe('isNumeric', () => {
|
||||
it('accepts pure digit strings', () => {
|
||||
@@ -255,3 +255,39 @@ describe('getType semantics', () => {
|
||||
expect(qr.getType(8, 6)).toBe(QrCodeDataType.Timing);
|
||||
});
|
||||
});
|
||||
|
||||
describe('automatic mask selection (regression)', () => {
|
||||
// The mask is chosen by minimizing getPenaltyScore() over all 8 patterns.
|
||||
// These version/mask pairs were verified against the canonical Project Nayuki
|
||||
// penalty algorithm. A regression in the finder-pattern (PENALTY_N3) rolling
|
||||
// window — e.g. dropping a history slot during the shift — silently selects a
|
||||
// different, non-spec-optimal mask, which these fixtures catch.
|
||||
it.each([
|
||||
['HELLO WORLD', QUARTILE, 1, 0],
|
||||
['HELLO WORLD', MEDIUM, 1, 0],
|
||||
['https://example.com', LOW, 2, 0],
|
||||
['12345678901234567890', LOW, 1, 4],
|
||||
['Hello, World!', MEDIUM, 1, 3],
|
||||
['a'.repeat(200), LOW, 9, 1],
|
||||
] as const)('locks version/mask for %j', (text, ecc, version, mask) => {
|
||||
const qr = encodeText(text, ecc);
|
||||
expect(qr.version).toBe(version);
|
||||
expect(qr.mask).toBe(mask);
|
||||
});
|
||||
|
||||
it('produces a stable full-grid fingerprint for HELLO WORLD / QUARTILE', () => {
|
||||
const qr = encodeText('HELLO WORLD', QUARTILE);
|
||||
let dark = 0;
|
||||
let hash = 0;
|
||||
for (let y = 0; y < qr.size; y++) {
|
||||
for (let x = 0; x < qr.size; x++) {
|
||||
const bit = qr.getModule(x, y) ? 1 : 0;
|
||||
dark += bit;
|
||||
hash = (hash * 31 + bit) >>> 0;
|
||||
}
|
||||
}
|
||||
expect(qr.size).toBe(21);
|
||||
expect(dark).toBe(218);
|
||||
expect(hash).toBe(1_118_257_212);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { QrSegment, makeNumeric, makeAlphanumeric, makeBytes } from '../segment';
|
||||
import { QrSegment, makeAlphanumeric, makeBytes, makeNumeric } from '../segment';
|
||||
import { MODE_ALPHANUMERIC, MODE_BYTE, MODE_NUMERIC } from '../constants';
|
||||
|
||||
describe('QrSegment', () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ export function encodeText(text: string, ecl: QrCodeEcc): QrCode {
|
||||
* This function always encodes using the binary segment mode, not any text mode.
|
||||
* The maximum number of bytes allowed is 2953.
|
||||
*/
|
||||
export function encodeBinary(data: Readonly<number[]>, ecl: QrCodeEcc): QrCode {
|
||||
export function encodeBinary(data: readonly number[], ecl: QrCodeEcc): QrCode {
|
||||
const seg = makeBytes(data);
|
||||
return encodeSegments([seg], ecl);
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export function encodeBinary(data: Readonly<number[]>, ecl: QrCodeEcc): QrCode {
|
||||
* This is a mid-level API; the high-level API is encodeText() and encodeBinary().
|
||||
*/
|
||||
export function encodeSegments(
|
||||
segs: Readonly<QrSegment[]>,
|
||||
segs: readonly QrSegment[],
|
||||
ecl: QrCodeEcc,
|
||||
minVersion = 1,
|
||||
maxVersion = 40,
|
||||
|
||||
@@ -38,7 +38,7 @@ export class QrCode {
|
||||
public readonly version: number,
|
||||
/** The error correction level used in this QR Code. */
|
||||
public readonly ecc: QrCodeEcc,
|
||||
dataCodewords: Readonly<number[]>,
|
||||
dataCodewords: readonly number[],
|
||||
msk: number,
|
||||
) {
|
||||
if (version < MIN_VERSION || version > MAX_VERSION)
|
||||
@@ -202,7 +202,7 @@ export class QrCode {
|
||||
|
||||
/* -- Private helper methods for constructor: Codewords and masking -- */
|
||||
|
||||
private addEccAndInterleave(data: Readonly<number[]>): number[] {
|
||||
private addEccAndInterleave(data: readonly number[]): number[] {
|
||||
const ver = this.version;
|
||||
const ecl = this.ecc;
|
||||
if (data.length !== getNumDataCodewords(ver, ecl))
|
||||
@@ -239,7 +239,7 @@ export class QrCode {
|
||||
return result;
|
||||
}
|
||||
|
||||
private drawCodewords(data: Readonly<number[]>): void {
|
||||
private drawCodewords(data: readonly number[]): void {
|
||||
if (data.length !== ((getNumRawDataModules(this.version) / 8) | 0))
|
||||
throw new RangeError('Invalid argument');
|
||||
|
||||
@@ -317,8 +317,9 @@ export class QrCode {
|
||||
result++;
|
||||
}
|
||||
else {
|
||||
if (h0 === 0) runX += size;
|
||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = runX; h0 = runX;
|
||||
let v = runX;
|
||||
if (h0 === 0) v += size;
|
||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
|
||||
if (runColor === 0) {
|
||||
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
|
||||
if (core && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
|
||||
@@ -331,13 +332,15 @@ export class QrCode {
|
||||
{
|
||||
let currentRunLength = runX;
|
||||
if (runColor === 1) {
|
||||
if (h0 === 0) currentRunLength += size;
|
||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = currentRunLength;
|
||||
let v = currentRunLength;
|
||||
if (h0 === 0) v += size;
|
||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
|
||||
currentRunLength = 0;
|
||||
}
|
||||
currentRunLength += size;
|
||||
if (h0 === 0) currentRunLength += size;
|
||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = currentRunLength;
|
||||
let v = currentRunLength;
|
||||
if (h0 === 0) v += size;
|
||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
|
||||
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
|
||||
if (core && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
|
||||
if (core && h6 >= h1 * 4 && h0 >= h1) result += PENALTY_N3;
|
||||
@@ -359,8 +362,9 @@ export class QrCode {
|
||||
result++;
|
||||
}
|
||||
else {
|
||||
if (h0 === 0) runY += size;
|
||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = runY; h0 = runY;
|
||||
let v = runY;
|
||||
if (h0 === 0) v += size;
|
||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
|
||||
if (runColor === 0) {
|
||||
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
|
||||
if (core && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
|
||||
@@ -373,13 +377,15 @@ export class QrCode {
|
||||
{
|
||||
let currentRunLength = runY;
|
||||
if (runColor === 1) {
|
||||
if (h0 === 0) currentRunLength += size;
|
||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = currentRunLength;
|
||||
let v = currentRunLength;
|
||||
if (h0 === 0) v += size;
|
||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
|
||||
currentRunLength = 0;
|
||||
}
|
||||
currentRunLength += size;
|
||||
if (h0 === 0) currentRunLength += size;
|
||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = currentRunLength;
|
||||
let v = currentRunLength;
|
||||
if (h0 === 0) v += size;
|
||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
|
||||
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
|
||||
if (core && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
|
||||
if (core && h6 >= h1 * 4 && h0 >= h1) result += PENALTY_N3;
|
||||
|
||||
@@ -67,7 +67,7 @@ export function getNumDataCodewords(ver: number, ecl: QrCodeEcc): number {
|
||||
* Calculates and returns the number of bits needed to encode the given segments at the given version.
|
||||
* The result is infinity if a segment has too many characters to fit its length field.
|
||||
*/
|
||||
export function getTotalBits(segs: Readonly<QrSegment[]>, version: number): number {
|
||||
export function getTotalBits(segs: readonly QrSegment[], version: number): number {
|
||||
let result = 0;
|
||||
for (const seg of segs) {
|
||||
const ccbits = numCharCountBits(seg.mode, version);
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
{
|
||||
"extends": "@robonen/tsconfig/tsconfig.json"
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.src.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@robonen/tsconfig/tsconfig.node.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||
},
|
||||
"include": ["*.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@robonen/tsconfig/tsconfig.dom.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": [],
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.src.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -3,5 +3,6 @@ import { sharedConfig } from '@robonen/tsdown';
|
||||
|
||||
export default defineConfig({
|
||||
...sharedConfig,
|
||||
tsconfig: './tsconfig.src.json',
|
||||
entry: ['src/index.ts'],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user