xryul_gremlins/tests/fix.test.ts

267 lines
6.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import { GREMLIN_DEFINITIONS } from '../src/characters.ts';
import {
buildGremlinFixChanges,
isGremlinFixable,
} from '../src/fix.ts';
import type { GremlinMatch } from '../src/types.ts';
function characterMatch(
codePoint: number,
from: number,
to: number,
count = 1,
): GremlinMatch {
return {
codePoint,
count,
from,
kind: 'character',
line: 0,
name: 'test character',
severity: 'warning',
to,
zeroWidth: false,
};
}
function listIndentationMatch(count: number): GremlinMatch {
return {
codePoint: null,
count,
from: 20,
kind: 'list-indentation',
line: 2,
name: 'list indentation',
severity: 'warning',
to: 20 + count,
zeroWidth: false,
};
}
describe('isGremlinFixable', () => {
it('provides an automatic fix for every defined gremlin character', () => {
for (const definition of GREMLIN_DEFINITIONS) {
assert.equal(
isGremlinFixable(characterMatch(definition.codePoint, 0, 1)),
true,
definition.name,
);
}
});
it('provides an automatic fix for malformed list indentation', () => {
assert.equal(isGremlinFixable(listIndentationMatch(2)), true);
});
});
describe('buildGremlinFixChanges', () => {
it('replaces Unicode spacing characters with ordinary spaces', () => {
const line = `a\u00a0\u00a0b`;
assert.deepEqual(
buildGremlinFixChanges(
[characterMatch(0x00a0, 11, 13, 2)],
line,
10,
),
[{ from: 11, insert: ' ', to: 13 }],
);
});
it('removes unsupported controls and soft hyphens', () => {
const line = `a\u0003\u00adb`;
assert.deepEqual(
buildGremlinFixChanges(
[characterMatch(0x0003, 1, 2), characterMatch(0x00ad, 2, 3)],
line,
0,
),
[
{ from: 1, insert: '', to: 2 },
{ from: 2, insert: '', to: 3 },
],
);
});
it('replaces typographic punctuation with ASCII equivalents', () => {
const line = '“quoted” —';
assert.deepEqual(
buildGremlinFixChanges(
[
characterMatch(0x201c, 0, 1),
characterMatch(0x201d, 7, 8),
characterMatch(0x2013, 9, 10),
characterMatch(0x2014, 11, 12),
],
line,
0,
),
[
{ from: 0, insert: '"', to: 1 },
{ from: 7, insert: '"', to: 8 },
{ from: 9, insert: '-', to: 10 },
{ from: 11, insert: '-', to: 12 },
],
);
});
it('normalizes Unicode line separators to ordinary newlines', () => {
const line = `a\u2028b\u2029c`;
assert.deepEqual(
buildGremlinFixChanges(
[characterMatch(0x2028, 1, 2), characterMatch(0x2029, 3, 4)],
line,
0,
),
[
{ from: 1, insert: '\n', to: 2 },
{ from: 3, insert: '\n\n', to: 4 },
],
);
});
it('preserves tab indentation when removing multiple trailing spaces', () => {
const match: GremlinMatch = {
codePoint: null,
count: 3,
from: 20,
kind: 'mixed-indentation',
line: 2,
name: 'mixed indentation',
severity: 'warning',
to: 23,
zeroWidth: false,
};
assert.deepEqual(buildGremlinFixChanges([match], '\t item', 20), [
{ from: 20, insert: '\t', to: 23 },
]);
});
it('preserves tab indentation when removing one stray space', () => {
const match: GremlinMatch = {
codePoint: null,
count: 2,
from: 20,
kind: 'mixed-indentation',
line: 2,
name: 'mixed indentation',
severity: 'warning',
to: 22,
zeroWidth: false,
};
assert.deepEqual(buildGremlinFixChanges([match], '\t - Item', 20), [
{ from: 20, insert: '\t', to: 22 },
]);
});
it('converts other mixed indentation to width-preserving spaces', () => {
const match: GremlinMatch = {
codePoint: null,
count: 2,
from: 20,
kind: 'mixed-indentation',
line: 2,
name: 'mixed indentation',
severity: 'warning',
to: 22,
zeroWidth: false,
};
assert.deepEqual(buildGremlinFixChanges([match], ' \tItem', 20), [
{ from: 20, insert: ' ', to: 22 },
]);
});
it('rounds list indentation to the nearest four-space level', () => {
const cases = [
{ indentation: ' ', normalized: '' },
{ indentation: ' ', normalized: ' ' },
{ indentation: ' ', normalized: ' ' },
{ indentation: ' ', normalized: ' ' },
{ indentation: ' ', normalized: ' ' },
{ indentation: ' ', normalized: ' ' },
];
for (const { indentation, normalized } of cases) {
const match = listIndentationMatch(indentation.length);
assert.deepEqual(
buildGremlinFixChanges(
[match],
`${indentation}- Item`,
20,
4,
),
[{ from: 20, insert: normalized, to: 20 + indentation.length }],
);
}
});
it('uses the current indent width while preserving space indentation', () => {
const match = listIndentationMatch(3);
assert.deepEqual(
buildGremlinFixChanges([match], ' - Item', 20, 2),
[{ from: 20, insert: ' ', to: 23 }],
);
});
it('uses the current indent width when measuring mixed indentation', () => {
const match: GremlinMatch = {
codePoint: null,
count: 2,
from: 20,
kind: 'mixed-indentation',
line: 2,
name: 'mixed indentation',
severity: 'warning',
to: 22,
zeroWidth: false,
};
assert.deepEqual(
buildGremlinFixChanges([match], ' \tItem', 20, 2),
[{ from: 20, insert: ' ', to: 22 }],
);
});
it('removes zero-width, joining, and bidi controls', () => {
const line = `a\u200b\u200c\u202eb`;
assert.deepEqual(
buildGremlinFixChanges(
[
characterMatch(0x200b, 1, 2),
characterMatch(0x200c, 2, 3),
characterMatch(0x202e, 3, 4),
],
line,
0,
),
[
{ from: 1, insert: '', to: 2 },
{ from: 2, insert: '', to: 3 },
{ from: 3, insert: '', to: 4 },
],
);
});
it('skips character matches that have no known safe replacement', () => {
assert.deepEqual(
buildGremlinFixChanges(
[characterMatch(0x1f47e, 0, 2)],
'👾',
0,
),
[],
);
});
});