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": {
|
"scripts": {
|
||||||
"lint:check": "oxlint -c oxlint.config.ts",
|
"lint:check": "eslint .",
|
||||||
"lint:fix": "oxlint -c oxlint.config.ts --fix",
|
"lint:fix": "eslint . --fix",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"dev": "vitest dev",
|
"dev": "vitest dev",
|
||||||
"bench": "vitest bench",
|
"bench": "vitest bench",
|
||||||
"build": "tsdown"
|
"build": "tsdown"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@robonen/oxlint": "workspace:*",
|
"@robonen/eslint": "workspace:*",
|
||||||
"@robonen/tsconfig": "workspace:*",
|
"@robonen/tsconfig": "workspace:*",
|
||||||
"@robonen/tsdown": "workspace:*",
|
"@robonen/tsdown": "workspace:*",
|
||||||
"@stylistic/eslint-plugin": "catalog:",
|
"eslint": "catalog:",
|
||||||
"oxlint": "catalog:",
|
|
||||||
"tsdown": "catalog:"
|
"tsdown": "catalog:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { bench, describe } from 'vitest';
|
import { bench, describe } from 'vitest';
|
||||||
import { encodeBinary, encodeSegments, encodeText, makeSegments, LOW, EccMap } from '..';
|
import { EccMap, LOW, encodeBinary, encodeSegments, encodeText, makeSegments } from '..';
|
||||||
|
|
||||||
/* -- Test data -- */
|
/* -- Test data -- */
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
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', () => {
|
describe('isNumeric', () => {
|
||||||
it('accepts pure digit strings', () => {
|
it('accepts pure digit strings', () => {
|
||||||
@@ -255,3 +255,39 @@ describe('getType semantics', () => {
|
|||||||
expect(qr.getType(8, 6)).toBe(QrCodeDataType.Timing);
|
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 { 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';
|
import { MODE_ALPHANUMERIC, MODE_BYTE, MODE_NUMERIC } from '../constants';
|
||||||
|
|
||||||
describe('QrSegment', () => {
|
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.
|
* This function always encodes using the binary segment mode, not any text mode.
|
||||||
* The maximum number of bytes allowed is 2953.
|
* 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);
|
const seg = makeBytes(data);
|
||||||
return encodeSegments([seg], ecl);
|
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().
|
* This is a mid-level API; the high-level API is encodeText() and encodeBinary().
|
||||||
*/
|
*/
|
||||||
export function encodeSegments(
|
export function encodeSegments(
|
||||||
segs: Readonly<QrSegment[]>,
|
segs: readonly QrSegment[],
|
||||||
ecl: QrCodeEcc,
|
ecl: QrCodeEcc,
|
||||||
minVersion = 1,
|
minVersion = 1,
|
||||||
maxVersion = 40,
|
maxVersion = 40,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export class QrCode {
|
|||||||
public readonly version: number,
|
public readonly version: number,
|
||||||
/** The error correction level used in this QR Code. */
|
/** The error correction level used in this QR Code. */
|
||||||
public readonly ecc: QrCodeEcc,
|
public readonly ecc: QrCodeEcc,
|
||||||
dataCodewords: Readonly<number[]>,
|
dataCodewords: readonly number[],
|
||||||
msk: number,
|
msk: number,
|
||||||
) {
|
) {
|
||||||
if (version < MIN_VERSION || version > MAX_VERSION)
|
if (version < MIN_VERSION || version > MAX_VERSION)
|
||||||
@@ -202,7 +202,7 @@ export class QrCode {
|
|||||||
|
|
||||||
/* -- Private helper methods for constructor: Codewords and masking -- */
|
/* -- 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 ver = this.version;
|
||||||
const ecl = this.ecc;
|
const ecl = this.ecc;
|
||||||
if (data.length !== getNumDataCodewords(ver, ecl))
|
if (data.length !== getNumDataCodewords(ver, ecl))
|
||||||
@@ -239,7 +239,7 @@ export class QrCode {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawCodewords(data: Readonly<number[]>): void {
|
private drawCodewords(data: readonly number[]): void {
|
||||||
if (data.length !== ((getNumRawDataModules(this.version) / 8) | 0))
|
if (data.length !== ((getNumRawDataModules(this.version) / 8) | 0))
|
||||||
throw new RangeError('Invalid argument');
|
throw new RangeError('Invalid argument');
|
||||||
|
|
||||||
@@ -317,8 +317,9 @@ export class QrCode {
|
|||||||
result++;
|
result++;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (h0 === 0) runX += size;
|
let v = runX;
|
||||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = runX; h0 = runX;
|
if (h0 === 0) v += size;
|
||||||
|
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
|
||||||
if (runColor === 0) {
|
if (runColor === 0) {
|
||||||
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
|
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 && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
|
||||||
@@ -331,13 +332,15 @@ export class QrCode {
|
|||||||
{
|
{
|
||||||
let currentRunLength = runX;
|
let currentRunLength = runX;
|
||||||
if (runColor === 1) {
|
if (runColor === 1) {
|
||||||
if (h0 === 0) currentRunLength += size;
|
let v = currentRunLength;
|
||||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = currentRunLength;
|
if (h0 === 0) v += size;
|
||||||
|
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
|
||||||
currentRunLength = 0;
|
currentRunLength = 0;
|
||||||
}
|
}
|
||||||
currentRunLength += size;
|
currentRunLength += size;
|
||||||
if (h0 === 0) currentRunLength += size;
|
let v = currentRunLength;
|
||||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = 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;
|
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 && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
|
||||||
if (core && h6 >= h1 * 4 && h0 >= h1) result += PENALTY_N3;
|
if (core && h6 >= h1 * 4 && h0 >= h1) result += PENALTY_N3;
|
||||||
@@ -359,8 +362,9 @@ export class QrCode {
|
|||||||
result++;
|
result++;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (h0 === 0) runY += size;
|
let v = runY;
|
||||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = runY; h0 = runY;
|
if (h0 === 0) v += size;
|
||||||
|
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
|
||||||
if (runColor === 0) {
|
if (runColor === 0) {
|
||||||
const core = h1 > 0 && h2 === h1 && h3 === h1 * 3 && h4 === h1 && h5 === h1;
|
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 && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
|
||||||
@@ -373,13 +377,15 @@ export class QrCode {
|
|||||||
{
|
{
|
||||||
let currentRunLength = runY;
|
let currentRunLength = runY;
|
||||||
if (runColor === 1) {
|
if (runColor === 1) {
|
||||||
if (h0 === 0) currentRunLength += size;
|
let v = currentRunLength;
|
||||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = currentRunLength;
|
if (h0 === 0) v += size;
|
||||||
|
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = h0; h0 = v;
|
||||||
currentRunLength = 0;
|
currentRunLength = 0;
|
||||||
}
|
}
|
||||||
currentRunLength += size;
|
currentRunLength += size;
|
||||||
if (h0 === 0) currentRunLength += size;
|
let v = currentRunLength;
|
||||||
h6 = h5; h5 = h4; h4 = h3; h3 = h2; h2 = h1; h1 = currentRunLength; h0 = 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;
|
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 && h0 >= h1 * 4 && h6 >= h1) result += PENALTY_N3;
|
||||||
if (core && h6 >= h1 * 4 && h0 >= 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.
|
* 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.
|
* 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;
|
let result = 0;
|
||||||
for (const seg of segs) {
|
for (const seg of segs) {
|
||||||
const ccbits = numCharCountBits(seg.mode, version);
|
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({
|
export default defineConfig({
|
||||||
...sharedConfig,
|
...sharedConfig,
|
||||||
|
tsconfig: './tsconfig.src.json',
|
||||||
entry: ['src/index.ts'],
|
entry: ['src/index.ts'],
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user