Fixes and prepare to publish
This commit is contained in:
parent
d9ae312547
commit
58bb757d22
@ -65,3 +65,9 @@ let input = document.querySelector("#creditCardNumber")
|
||||
|
||||
minimask(input, formatter)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2025 Signal Messenger, LLC
|
||||
|
||||
Licensed under the [AGPLv3](LICENSE)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// @ts-check
|
||||
|
||||
import eslint from "@eslint/js"
|
||||
import tseslint from "typescript-eslint"
|
||||
|
||||
|
||||
@ -1,3 +1,8 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@plugin "@tailwindcss/forms";
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
<!-- Copyright 2025 Signal Messenger, LLC -->
|
||||
<!-- SPDX-License-Identifier: AGPL-3.0-only -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { minimask, type Formatter, type FormatterToken } from "../src/minimask"
|
||||
import { createCreditCardExpirationFormatter } from "../src/formatters/cc-exp"
|
||||
import creditCardType from "credit-card-type"
|
||||
|
||||
let CC_EXP_FORMATTER = createCreditCardExpirationFormatter({
|
||||
zero: "optional",
|
||||
slash: "eager",
|
||||
month: "loose",
|
||||
})
|
||||
let CC_EXP_FORMATTER = createCreditCardExpirationFormatter()
|
||||
|
||||
let CC_NUMBER_FORMATTER: Formatter = (input) => {
|
||||
let [cardType] = creditCardType(input)
|
||||
|
||||
@ -1,197 +1,98 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { Formatter, FormatterToken } from "../minimask"
|
||||
|
||||
function isDigit(char: string) {
|
||||
function isDigit(char: string): boolean {
|
||||
return /\d/.test(char)
|
||||
}
|
||||
|
||||
function isMonth(input: string) {
|
||||
const number = Number(input)
|
||||
return number >= 1 && number <= 12
|
||||
}
|
||||
export function createCreditCardExpirationFormatter(): Formatter {
|
||||
return function creditCardExpirationFormatter(input: string) {
|
||||
let chars = input.split("")
|
||||
let index = 0
|
||||
let char = chars[index]
|
||||
|
||||
export type Options = Readonly<{
|
||||
/**
|
||||
* required:
|
||||
* - `02/12`
|
||||
* optional:
|
||||
* - `2/12` or `02/12`
|
||||
*/
|
||||
zero?: "required" | "optional"
|
||||
/**
|
||||
* eager: Immediately insert a `/` as soon as we know we have a valid month (01-12)
|
||||
* - `1` -> `1`
|
||||
* - `2` -> `02/` (eager)
|
||||
* - `12` -> `12/` (eager)
|
||||
* - `123` -> `12/3`
|
||||
* lazy: Wait to insert a `/` until we have our first year digit
|
||||
* - `2` -> `2` (lazy)
|
||||
* - `12` -> `12` (lazy)
|
||||
* - `123` -> `12/3`
|
||||
*/
|
||||
slash?: "eager" | "lazy"
|
||||
|
||||
/**
|
||||
* strict: Never allow the user to create a month that's not 1-12 (default)
|
||||
* - `13` -> `01/3`
|
||||
* - `13/45` -> `01/34` (strict)
|
||||
* loose: Allow months 13-99 if immediately followed by a `/`
|
||||
* - `13` -> `01/3`
|
||||
* - `13/45` -> `13/45` (loose)
|
||||
*
|
||||
* Forcing months to be `strict` will make the input a little bit aggressive about
|
||||
* reformatting the date (inserting zero, moving the slash around) when going back
|
||||
* and editing the month, but `loose` will required you to validate the input and
|
||||
* give the user feedback.
|
||||
*/
|
||||
month?: "strict" | "loose"
|
||||
}>
|
||||
|
||||
export function createCreditCardExpirationFormatter(options: Options = {}): Formatter {
|
||||
let zeroMode = options.zero ?? "required"
|
||||
let slashMode = options.slash ?? "eager"
|
||||
let monthMode = options.month ?? "strict"
|
||||
|
||||
return (input: string) => {
|
||||
let cursor = 0
|
||||
|
||||
// Helper so we can use simple for-of loops to scan ahead
|
||||
function* scan() {
|
||||
while (cursor < input.length) {
|
||||
let index = cursor
|
||||
let char = input[index]!
|
||||
yield [index, char] as const
|
||||
if (cursor === index) {
|
||||
throw new Error("cursor was not updated for next loop")
|
||||
}
|
||||
}
|
||||
function next() {
|
||||
char = chars[++index]
|
||||
}
|
||||
|
||||
// Find the first digit in the input
|
||||
let digit1: FormatterToken | null = null
|
||||
for (let [index, char] of scan()) {
|
||||
if (isDigit(char)) {
|
||||
digit1 = { char, index, mask: false }
|
||||
cursor++ // take
|
||||
break
|
||||
}
|
||||
cursor++ // skip
|
||||
function take(): FormatterToken {
|
||||
if (char == null) throw new Error()
|
||||
let token: FormatterToken = { char, index, mask: false }
|
||||
next()
|
||||
return token
|
||||
}
|
||||
|
||||
// Return an empty input early if no digits
|
||||
if (digit1 == null) {
|
||||
if (char == "/") {
|
||||
return []
|
||||
}
|
||||
|
||||
// Note: A single digit month is shifted right to `month2` because we need
|
||||
// to make room for a zero.
|
||||
let month1: FormatterToken | null = digit1
|
||||
let month2: FormatterToken | null = null
|
||||
while (char != null && !isDigit(char)) {
|
||||
next()
|
||||
}
|
||||
|
||||
if (char == null) {
|
||||
return []
|
||||
}
|
||||
|
||||
let month1: FormatterToken | null = take()
|
||||
let month2: FormatterToken | null
|
||||
|
||||
if (month1.char === "0") {
|
||||
for (let [index, char] of scan()) {
|
||||
// Skip any additional non-digits/zeroes until we reach another digit 1-9
|
||||
if (isDigit(char) && char !== "0") {
|
||||
month2 = { char, index, mask: false }
|
||||
cursor++ // take
|
||||
if (char == null || !isDigit(char) || char === "0") {
|
||||
return [month1]
|
||||
} else {
|
||||
month2 = take()
|
||||
}
|
||||
} else if (month1.char === "1") {
|
||||
if (char == null) {
|
||||
return [month1]
|
||||
} else if (char === "0" || char === "1" || char === "2") {
|
||||
month2 = take()
|
||||
} else {
|
||||
month2 = month1
|
||||
month1 = null
|
||||
}
|
||||
} else {
|
||||
month2 = month1
|
||||
month1 = null
|
||||
}
|
||||
|
||||
let slashIndex: number | null = null
|
||||
if (char != null && !isDigit(char)) {
|
||||
slashIndex = index
|
||||
} else {
|
||||
slashIndex = month2.index
|
||||
month1 ??= { char: "0", index: month2.index, mask: false }
|
||||
}
|
||||
|
||||
let year = []
|
||||
|
||||
while (char != null) {
|
||||
if (isDigit(char)) {
|
||||
year.push(take())
|
||||
if (year.length >= 4) {
|
||||
break
|
||||
}
|
||||
cursor++ // skip
|
||||
}
|
||||
} else {
|
||||
for (let [index, char] of scan()) {
|
||||
if (isDigit(char)) {
|
||||
|
||||
// If the next digit forms a month, immediately take it and move on
|
||||
if (isMonth(`${digit1.char}${char}`)) {
|
||||
month2 = { char, index, mask: false }
|
||||
cursor++ // take
|
||||
break
|
||||
}
|
||||
|
||||
// In loose mode, 13-99 are treated as months if they are immediately
|
||||
// followed by a slash. This makes the input thrash less when editing
|
||||
// the month
|
||||
if (monthMode === "loose") {
|
||||
const next = input[cursor + 1]
|
||||
if (next != null && !isDigit(next)) {
|
||||
month2 = { char, index, mask: false }
|
||||
cursor++ // take
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Make room for zero
|
||||
month2 = month1
|
||||
month1 = null
|
||||
break
|
||||
}
|
||||
|
||||
// If we never found another digit, move 2-9 to `month2`
|
||||
if (month2 == null && digit1.char !== "1") {
|
||||
month2 = month1
|
||||
month1 = null
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a second month char, eagerly append a slash
|
||||
let slash: FormatterToken | null = null
|
||||
if (month2 != null) {
|
||||
for (let [index, char] of scan()) {
|
||||
// Try to use an existing non-digit if it exists
|
||||
if (!isDigit(char)) {
|
||||
slash = { char: "/", index, mask: true }
|
||||
cursor++ // take
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let year: FormatterToken[] = []
|
||||
|
||||
// Take up to the next 4 digits as year chars
|
||||
for (let [index, char] of scan()) {
|
||||
if (isDigit(char)) {
|
||||
year.push({ char, index, mask: false })
|
||||
cursor++ // take
|
||||
if (year.length >= 4) break
|
||||
} else {
|
||||
cursor++ // skip
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a 4-digit year (could be pasted) take the last two digits,
|
||||
// otherwise force the user to type only the first two digits
|
||||
if (year.length === 4) {
|
||||
year = year.slice(2) // take the last two YYYY chars
|
||||
console.log(year)
|
||||
|
||||
if (year.length >= 4) {
|
||||
year = year.slice(2, 4)
|
||||
} else {
|
||||
year = year.slice(0, 2) // take the first two YY chars
|
||||
year = year.slice(0, 2)
|
||||
}
|
||||
|
||||
// This is where we decide if we want to inject a `/` and/or a leading zero
|
||||
// depending on if we have our "2nd" month digit yet (which could be a
|
||||
// single digit month 2-9)
|
||||
let tokens: Array<FormatterToken> = []
|
||||
if (month1 != null) tokens.push(month1)
|
||||
if (month2 != null) tokens.push(month2)
|
||||
if (month2 != null) {
|
||||
// slash=eager: Always insert a slash if we have a 2nd month digit
|
||||
// slash=lazy: Only insert a slash if we have some year digits
|
||||
if (slashMode === "eager" || (slashMode === "lazy" && year.length > 0)) {
|
||||
slash ??= { char: "/", index: month2.index, mask: true }
|
||||
}
|
||||
|
||||
// zero=required: Only insert a zero...
|
||||
// - slash=lazy: Always
|
||||
// - slash=eager: If the slash was inserted
|
||||
// zero=optional: Never, only use a zero if the user typed it
|
||||
if (zeroMode === "required" && (slashMode === "lazy" || slash != null)) {
|
||||
month1 ??= { char: "0", index: month2.index, mask: false }
|
||||
}
|
||||
tokens.push({ char: "/", index: slashIndex, mask: true })
|
||||
}
|
||||
|
||||
// Build the final set of tokens
|
||||
let results: FormatterToken[] = []
|
||||
if (month1 != null) results.push(month1)
|
||||
if (month2 != null) results.push(month2)
|
||||
if (slash != null) results.push(slash)
|
||||
return results.concat(year)
|
||||
return tokens.concat(year)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { Formatter } from "./minimask"
|
||||
|
||||
export type InputState = Readonly<{
|
||||
@ -12,30 +14,43 @@ export type InputState = Readonly<{
|
||||
*/
|
||||
export function getNextInputState(
|
||||
formatter: Formatter,
|
||||
includeMask: boolean,
|
||||
prevValue: string,
|
||||
prevStart: number,
|
||||
prevEnd: number,
|
||||
isDeleting: boolean,
|
||||
): InputState {
|
||||
let value = ""
|
||||
let start = null
|
||||
let end = null
|
||||
let cursor = 0
|
||||
let lastIndex = 0
|
||||
|
||||
for (let token of formatter(prevValue)) {
|
||||
let index = value.length
|
||||
// Only include mask chars if requested
|
||||
if (!token.mask || includeMask) {
|
||||
value += token.char
|
||||
}
|
||||
value += token.char
|
||||
|
||||
// If deleting backwards, place the cursor before any mask chars
|
||||
if (isDeleting && token.mask) continue
|
||||
|
||||
// We may have skipped some indexes
|
||||
while (cursor <= token.index) {
|
||||
if (cursor === prevStart) start ??= index
|
||||
if (cursor === prevEnd) end ??= index
|
||||
if (cursor === prevStart) start ??= lastIndex
|
||||
if (cursor === prevEnd) end ??= lastIndex
|
||||
cursor += 1
|
||||
}
|
||||
|
||||
// Last index will skip mask chars when deleting
|
||||
lastIndex = value.length
|
||||
}
|
||||
|
||||
// If deleting from the end of the input, drop any trailing mask chars
|
||||
if (isDeleting && end === null) {
|
||||
value = value.slice(0, lastIndex)
|
||||
}
|
||||
|
||||
// If we never found our selection, it must have been after the last token
|
||||
start ??= value.length
|
||||
end ??= value.length
|
||||
|
||||
return { value, start, end }
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { getNextInputState } from "./getNextInputState"
|
||||
|
||||
export type FormatterToken = Readonly<{
|
||||
/**
|
||||
* This should be a single character
|
||||
*/
|
||||
* This should be a single character
|
||||
*/
|
||||
char: string
|
||||
/**
|
||||
* The index of the `char` in the original string. Two tokens can share the
|
||||
@ -48,68 +50,47 @@ export function unformat(formatter: Formatter, input: string): string {
|
||||
return result
|
||||
}
|
||||
|
||||
let DELETE_BACKWARD = new Set(["deleteContentBackward", "deleteWordBackward", "deleteSoftLineBackward", "deleteHardLineBackward"])
|
||||
let DELETE_FORWARD = new Set(["deleteContentForward", "deleteWordForward", "deleteSoftLineForward", "deleteHardLineForward"])
|
||||
|
||||
function shouldUnformat(input: HTMLInputElement, inputType: string) {
|
||||
return (
|
||||
// If the user has selected a range, don't bother unformatting the input
|
||||
// because they've been specific about what they are trying to edit, we'll
|
||||
// re-format it later
|
||||
input.selectionStart === input.selectionEnd &&
|
||||
(
|
||||
// Can't delete backwards if we're already at the start
|
||||
(DELETE_BACKWARD.has(inputType) && input.selectionEnd !== 0) ||
|
||||
// Can't delete forwards if we're already at the end
|
||||
(DELETE_FORWARD.has(inputType) && input.selectionStart !== input.value.length)
|
||||
)
|
||||
)
|
||||
}
|
||||
let DELETE_BACKWARD = new Set([
|
||||
"deleteContentBackward",
|
||||
"deleteWordBackward",
|
||||
"deleteSoftLineBackward",
|
||||
"deleteHardLineBackward",
|
||||
])
|
||||
|
||||
/**
|
||||
* Bind to an input element, masking the value of it.
|
||||
*/
|
||||
export function minimask(input: HTMLInputElement, formatter: Formatter) {
|
||||
let update = (includeMask: boolean) => {
|
||||
let { value, start, end } = getNextInputState(
|
||||
formatter,
|
||||
includeMask,
|
||||
input.value,
|
||||
input.selectionStart ?? 0,
|
||||
input.selectionEnd ?? 0,
|
||||
)
|
||||
input.value = value
|
||||
input.setSelectionRange(start, end)
|
||||
}
|
||||
|
||||
// This should never be completely empty, the historyIndex should always
|
||||
// point to the current value
|
||||
let history: string[] = [input.value]
|
||||
let historyIndex = 0
|
||||
|
||||
let onBeforeInput = (event: InputEvent) => {
|
||||
// Avoid unformatting the input if its not going to result in any change
|
||||
// since it won't fire an `input` event if it doesn't change.
|
||||
//
|
||||
// Really this is only needed when hitting Delete/Backspace s
|
||||
if (shouldUnformat(input, event.inputType)) {
|
||||
update(false)
|
||||
}
|
||||
}
|
||||
|
||||
let onInput = (event: InputEvent) => {
|
||||
if (event.inputType === "historyUndo") {
|
||||
const inputType = event.inputType
|
||||
|
||||
if (inputType === "historyUndo") {
|
||||
// Move the historyIndex back, but retain items to redo later
|
||||
historyIndex = Math.max(historyIndex - 1, 0)
|
||||
input.value = history[historyIndex]!
|
||||
} else if (event.inputType === "historyRedo") {
|
||||
} else if (inputType === "historyRedo") {
|
||||
// Move the historyIndex forwards
|
||||
historyIndex = Math.min(historyIndex + 1, history.length - 1)
|
||||
input.value = history[historyIndex]!
|
||||
} else {
|
||||
update(true)
|
||||
let isDeleting = DELETE_BACKWARD.has(inputType)
|
||||
let { value, start, end } = getNextInputState(
|
||||
formatter,
|
||||
input.value,
|
||||
input.selectionStart ?? 0,
|
||||
input.selectionEnd ?? 0,
|
||||
isDeleting,
|
||||
)
|
||||
input.value = value
|
||||
input.setSelectionRange(start, end)
|
||||
|
||||
// If the input has changed:
|
||||
if (input.value !== history[historyIndex]) {
|
||||
if (value !== history[historyIndex]) {
|
||||
// Move the historyIndex forwards
|
||||
historyIndex++
|
||||
// Truncate the redos from our current position, and add our new state to the end
|
||||
@ -118,13 +99,9 @@ export function minimask(input: HTMLInputElement, formatter: Formatter) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fires any time the input *might* be about to change
|
||||
input.addEventListener("beforeinput", onBeforeInput)
|
||||
// Fires any time the input *did* change (not guaranteed every time beforeinput fires)
|
||||
input.addEventListener("input", onInput as (event: Event) => void)
|
||||
|
||||
return function unsubscribe(): void {
|
||||
input.removeEventListener("beforeinput", onBeforeInput)
|
||||
input.removeEventListener("input", onInput as (event: Event) => void)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { suite, test, expect } from "vitest"
|
||||
import { createCreditCardExpirationFormatter } from "../../src/formatters/cc-exp"
|
||||
import type { Options } from "../../src/formatters/cc-exp"
|
||||
|
||||
function setup(options: Options = {}) {
|
||||
let formatter = createCreditCardExpirationFormatter(options)
|
||||
function setup() {
|
||||
let formatter = createCreditCardExpirationFormatter()
|
||||
return function check(input: string, expectedResult: string, expectedIndexes: number[]) {
|
||||
test(`${input} -> ${expectedResult} (${expectedIndexes.join(",")})`, () => {
|
||||
let tokens = Array.from(formatter(input))
|
||||
@ -63,15 +64,17 @@ suite("CC_EXP_FORMATTER", () => {
|
||||
check("19", "01/9", [0, 0, 0, 1])
|
||||
})
|
||||
})
|
||||
suite("leading non-digits: drop", () => {
|
||||
suite("leading non-digits/non-slash: drop", () => {
|
||||
check(" ", "", [])
|
||||
check("/", "", [])
|
||||
check("%", "", [])
|
||||
check("!@#$%", "", [])
|
||||
check(" 1", "1", [1])
|
||||
check("/1", "1", [1])
|
||||
check("!@#$%1", "1", [5])
|
||||
})
|
||||
suite("leading slash: drop all chars", () => {
|
||||
check("/1", "", [])
|
||||
})
|
||||
})
|
||||
suite("month/year splitter", () => {
|
||||
suite("0/ -> 0: drop slash", () => {
|
||||
@ -88,16 +91,16 @@ suite("CC_EXP_FORMATTER", () => {
|
||||
check("08/", "08/", [0, 1, 2])
|
||||
check("09/", "09/", [0, 1, 2])
|
||||
})
|
||||
suite("1/-9/ -> 01/-09/: prefix with 0", () => {
|
||||
check("1/", "01/", [0, 0, 1])
|
||||
check("2/", "02/", [0, 0, 1])
|
||||
check("3/", "03/", [0, 0, 1])
|
||||
check("4/", "04/", [0, 0, 1])
|
||||
check("5/", "05/", [0, 0, 1])
|
||||
check("6/", "06/", [0, 0, 1])
|
||||
check("7/", "07/", [0, 0, 1])
|
||||
check("8/", "08/", [0, 0, 1])
|
||||
check("9/", "09/", [0, 0, 1])
|
||||
suite("1/-9/ -> 1/-9/: don't prefix with 0", () => {
|
||||
check("1/", "1/", [0, 1])
|
||||
check("2/", "2/", [0, 1])
|
||||
check("3/", "3/", [0, 1])
|
||||
check("4/", "4/", [0, 1])
|
||||
check("5/", "5/", [0, 1])
|
||||
check("6/", "6/", [0, 1])
|
||||
check("7/", "7/", [0, 1])
|
||||
check("8/", "8/", [0, 1])
|
||||
check("9/", "9/", [0, 1])
|
||||
})
|
||||
suite("10-12/: unchanged", () => {
|
||||
check("10/", "10/", [0, 1, 2])
|
||||
@ -154,10 +157,10 @@ suite("CC_EXP_FORMATTER", () => {
|
||||
suite("000 -> 0: drop extra zeroes", () => {
|
||||
check("000", "0", [0])
|
||||
})
|
||||
suite("001-009 -> 01/-09/: drop extra zeroes, insert slash", () => {
|
||||
check("001", "01/", [0, 2, 2])
|
||||
check("002", "02/", [0, 2, 2])
|
||||
check("009", "09/", [0, 2, 2])
|
||||
suite("001-009 -> 0: drop everything on second zero", () => {
|
||||
check("001", "0", [0])
|
||||
check("002", "0", [0])
|
||||
check("009", "0", [0])
|
||||
})
|
||||
suite("01Y-09Y -> 01/Y-09/Y: insert slash", () => {
|
||||
check("010", "01/0", [0, 1, 1, 2])
|
||||
@ -190,15 +193,15 @@ suite("CC_EXP_FORMATTER", () => {
|
||||
suite("0000 -> 0: drop extra zeroes", () => {
|
||||
check("0000", "0", [0])
|
||||
})
|
||||
suite("0001-0009 -> 01/-09/: drop extra zeroes, insert slash", () => {
|
||||
check("0001", "01/", [0, 3, 3])
|
||||
check("0002", "02/", [0, 3, 3])
|
||||
check("0009", "09/", [0, 3, 3])
|
||||
suite("0001-0009 -> 0: drop everything on second zero", () => {
|
||||
check("0001", "0", [0])
|
||||
check("0002", "0", [0])
|
||||
check("0009", "0", [0])
|
||||
})
|
||||
suite("001Y-009Y -> 01/Y-09/Y: drop extra zero, insert slash", () => {
|
||||
check("0010", "01/0", [0, 2, 2, 3])
|
||||
check("0012", "01/2", [0, 2, 2, 3])
|
||||
check("0099", "09/9", [0, 2, 2, 3])
|
||||
suite("001*-009* -> 0: drop everything on second zero", () => {
|
||||
check("0010", "0", [0])
|
||||
check("0012", "0", [0])
|
||||
check("0099", "0", [0])
|
||||
})
|
||||
suite("01YY-09YY -> 01/YY-09/YY: insert slash", () => {
|
||||
check("0100", "01/00", [0, 1, 1, 2, 3])
|
||||
@ -231,20 +234,20 @@ suite("CC_EXP_FORMATTER", () => {
|
||||
suite("00000 -> 0: drop extra zeroes", () => {
|
||||
check("00000", "0", [0])
|
||||
})
|
||||
suite("00001-00009 -> 01/-09/: drop extra zeroes, insert slash", () => {
|
||||
check("00001", "01/", [0, 4, 4])
|
||||
check("00002", "02/", [0, 4, 4])
|
||||
check("00009", "09/", [0, 4, 4])
|
||||
suite("00001-00009 -> 0: drop everything on second zero", () => {
|
||||
check("00001", "0", [0])
|
||||
check("00002", "0", [0])
|
||||
check("00009", "0", [0])
|
||||
})
|
||||
suite("0001Y-0009Y -> 01/Y-09/Y: drop extra zeroes, insert slash", () => {
|
||||
check("00010", "01/0", [0, 3, 3, 4])
|
||||
check("00012", "01/2", [0, 3, 3, 4])
|
||||
check("00099", "09/9", [0, 3, 3, 4])
|
||||
suite("0001Y-0009Y -> 0: drop everything on second zero", () => {
|
||||
check("00010", "0", [0])
|
||||
check("00012", "0", [0])
|
||||
check("00099", "0", [0])
|
||||
})
|
||||
suite("001YY-009YY -> 01/YY-09/YY: drop extra zero, insert slash", () => {
|
||||
check("00100", "01/00", [0, 2, 2, 3, 4])
|
||||
check("00123", "01/23", [0, 2, 2, 3, 4])
|
||||
check("00999", "09/99", [0, 2, 2, 3, 4])
|
||||
suite("001YY-009YY -> 0: drop everything on second zero", () => {
|
||||
check("00100", "0", [0])
|
||||
check("00123", "0", [0])
|
||||
check("00999", "0", [0])
|
||||
})
|
||||
suite("01YY*-09YY* -> 01/YY-09/YY: insert slash, drop extra YY digit", () => {
|
||||
check("01000", "01/00", [0, 1, 1, 2, 3])
|
||||
@ -277,25 +280,25 @@ suite("CC_EXP_FORMATTER", () => {
|
||||
suite("000000 -> 0: drop extra zeroes", () => {
|
||||
check("000000", "0", [0])
|
||||
})
|
||||
suite("000001-000009 -> 01/-09/: drop extra zeroes, insert slash", () => {
|
||||
check("000001", "01/", [0, 5, 5])
|
||||
check("000002", "02/", [0, 5, 5])
|
||||
check("000009", "09/", [0, 5, 5])
|
||||
suite("000001-000009 -> 0: drop everything on second zero", () => {
|
||||
check("000001", "0", [0])
|
||||
check("000002", "0", [0])
|
||||
check("000009", "0", [0])
|
||||
})
|
||||
suite("00001*-00009* -> 01/Y-09/Y: drop extra zeroes, insert slash", () => {
|
||||
check("000010", "01/0", [0, 4, 4, 5])
|
||||
check("000012", "01/2", [0, 4, 4, 5])
|
||||
check("000099", "09/9", [0, 4, 4, 5])
|
||||
suite("00001*-00009* -> 0: drop everything on second zero", () => {
|
||||
check("000010", "0", [0])
|
||||
check("000012", "0", [0])
|
||||
check("000099", "0", [0])
|
||||
})
|
||||
suite("0001YY-0009YY -> 01/YY-09/YY: drop extra zeroes, insert slash", () => {
|
||||
check("000100", "01/00", [0, 3, 3, 4, 5])
|
||||
check("000123", "01/23", [0, 3, 3, 4, 5])
|
||||
check("000999", "09/99", [0, 3, 3, 4, 5])
|
||||
suite("0001YY-0009YY -> 0: drop everything on second zero", () => {
|
||||
check("000100", "0", [0])
|
||||
check("000123", "0", [0])
|
||||
check("000999", "0", [0])
|
||||
})
|
||||
suite("001YY*-009YY* -> 01/YY-09/YY: drop extra zeroes, insert slash, drop extra YY digit", () => {
|
||||
check("001000", "01/00", [0, 2, 2, 3, 4])
|
||||
check("001234", "01/23", [0, 2, 2, 3, 4])
|
||||
check("009999", "09/99", [0, 2, 2, 3, 4])
|
||||
suite("001YY*-009YY* -> 0: drop everything on second zero", () => {
|
||||
check("001000", "0", [0])
|
||||
check("001234", "0", [0])
|
||||
check("009999", "0", [0])
|
||||
})
|
||||
suite("01**YY-09**YY -> 01/YY-09/YY: insert slash, drop extra YYYY digits", () => {
|
||||
check("010000", "01/00", [0, 1, 1, 4, 5])
|
||||
@ -328,30 +331,30 @@ suite("CC_EXP_FORMATTER", () => {
|
||||
suite("0000000 -> 0: drop extra zeroes", () => {
|
||||
check("0000000", "0", [0])
|
||||
})
|
||||
suite("0000001-0000009 -> 01/-09/: drop extra zeroes, insert slash", () => {
|
||||
check("0000001", "01/", [0, 6, 6])
|
||||
check("0000002", "02/", [0, 6, 6])
|
||||
check("0000009", "09/", [0, 6, 6])
|
||||
suite("0000001-0000009 -> 0: drop everything on second zero", () => {
|
||||
check("0000001", "0", [0])
|
||||
check("0000002", "0", [0])
|
||||
check("0000009", "0", [0])
|
||||
})
|
||||
suite("000001*-000009* -> 01/Y-09/Y: drop extra zeroes, insert slash", () => {
|
||||
check("0000010", "01/0", [0, 5, 5, 6])
|
||||
check("0000012", "01/2", [0, 5, 5, 6])
|
||||
check("0000099", "09/9", [0, 5, 5, 6])
|
||||
suite("000001*-000009* -> 0: drop everything on second zero", () => {
|
||||
check("0000010", "0", [0])
|
||||
check("0000012", "0", [0])
|
||||
check("0000099", "0", [0])
|
||||
})
|
||||
suite("00001YY-00009YY -> 01/YY-09/YY: drop extra zeroes, insert slash", () => {
|
||||
check("0000100", "01/00", [0, 4, 4, 5, 6])
|
||||
check("0000123", "01/23", [0, 4, 4, 5, 6])
|
||||
check("0000999", "09/99", [0, 4, 4, 5, 6])
|
||||
suite("00001YY-00009YY -> 0: drop everything on second zero", () => {
|
||||
check("0000100", "0", [0])
|
||||
check("0000123", "0", [0])
|
||||
check("0000999", "0", [0])
|
||||
})
|
||||
suite("0001YY*-0009YY* -> 01/YY-09/YY: drop extra zeroes, insert slash, drop extra YY digit", () => {
|
||||
check("0001000", "01/00", [0, 3, 3, 4, 5])
|
||||
check("0001234", "01/23", [0, 3, 3, 4, 5])
|
||||
check("0009999", "09/99", [0, 3, 3, 4, 5])
|
||||
suite("0001YY*-0009YY* -> 0: drop everything on second zero", () => {
|
||||
check("0001000", "0", [0])
|
||||
check("0001234", "0", [0])
|
||||
check("0009999", "0", [0])
|
||||
})
|
||||
suite("001**YY-009**YY -> 01/YY-09/YY: drop extra zeroes, insert slash, drop extra YYYY digits", () => {
|
||||
check("0010000", "01/00", [0, 2, 2, 5, 6])
|
||||
check("0012345", "01/45", [0, 2, 2, 5, 6])
|
||||
check("0099999", "09/99", [0, 2, 2, 5, 6])
|
||||
suite("001**YY-009**YY -> 0: drop everything on second zero", () => {
|
||||
check("0010000", "0", [0])
|
||||
check("0012345", "0", [0])
|
||||
check("0099999", "0", [0])
|
||||
})
|
||||
suite("01**YY*-09**YY* -> 01/YY-09/YY: insert slash, drop extra YYYY digits", () => {
|
||||
check("0100000", "01/00", [0, 1, 1, 4, 5])
|
||||
@ -381,90 +384,4 @@ suite("CC_EXP_FORMATTER", () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
suite("options", () => {
|
||||
suite("zero:required", () => {
|
||||
const check = setup({ zero: "required" })
|
||||
check("0", "0", [0])
|
||||
check("00", "0", [0])
|
||||
check("1", "1", [0])
|
||||
check("2", "02/", [0, 0, 0])
|
||||
check("12", "12/", [0, 1, 1])
|
||||
check("13", "01/3", [0, 0, 0, 1])
|
||||
check("01", "01/", [0, 1, 1])
|
||||
check("02", "02/", [0, 1, 1])
|
||||
check("01/", "01/", [0, 1, 2])
|
||||
})
|
||||
suite("zero:optional", () => {
|
||||
const check = setup({ zero: "optional" })
|
||||
check("0", "0", [0])
|
||||
check("00", "0", [0])
|
||||
check("1", "1", [0])
|
||||
check("2", "2/", [0, 0])
|
||||
check("12", "12/", [0, 1, 1])
|
||||
check("13", "1/3", [0, 0, 1])
|
||||
check("01", "01/", [0, 1, 1])
|
||||
check("02", "02/", [0, 1, 1])
|
||||
check("01/", "01/", [0, 1, 2])
|
||||
})
|
||||
suite("slash:eager", () => {
|
||||
const check = setup({ slash: "eager" })
|
||||
check("0", "0", [0])
|
||||
check("1", "1", [0])
|
||||
check("1/", "01/", [0, 0, 1])
|
||||
check("2", "02/", [0, 0, 0])
|
||||
check("12", "12/", [0, 1, 1])
|
||||
check("13", "01/3", [0, 0, 0, 1])
|
||||
check("01", "01/", [0, 1, 1])
|
||||
})
|
||||
suite("slash:lazy", () => {
|
||||
const check = setup({ slash: "lazy" })
|
||||
check("0", "0", [0])
|
||||
check("1", "1", [0])
|
||||
check("1/", "01/", [0, 0, 1])
|
||||
check("2", "02", [0, 0])
|
||||
check("12", "12", [0, 1])
|
||||
check("13", "01/3", [0, 0, 0, 1])
|
||||
check("01", "01", [0, 1])
|
||||
})
|
||||
suite("month:strict", () => {
|
||||
const check = setup({ month: "strict" })
|
||||
check("0", "0", [0])
|
||||
check("00", "0", [0])
|
||||
check("1", "1", [0])
|
||||
check("12", "12/", [0, 1, 1])
|
||||
check("13", "01/3", [0, 0, 0, 1])
|
||||
check("23", "02/3", [0, 0, 0, 1])
|
||||
check("0/", "0", [0])
|
||||
check("1/", "01/", [0, 0, 1])
|
||||
check("2/", "02/", [0, 0, 1])
|
||||
check("13/", "01/3", [0, 0, 0, 1])
|
||||
check("23/", "02/3", [0, 0, 0, 1])
|
||||
})
|
||||
suite("month:loose", () => {
|
||||
const check = setup({ month: "loose" })
|
||||
check("0", "0", [0])
|
||||
check("00", "0", [0])
|
||||
check("1", "1", [0])
|
||||
check("2", "02/", [0, 0, 0])
|
||||
check("12", "12/", [0, 1, 1])
|
||||
check("13", "01/3", [0, 0, 0, 1])
|
||||
check("23", "02/3", [0, 0, 0, 1])
|
||||
check("0/", "0", [0])
|
||||
check("1/", "01/", [0, 0, 1])
|
||||
check("2/", "02/", [0, 0, 1])
|
||||
check("13/", "13/", [0, 1, 2])
|
||||
check("23/", "23/", [0, 1, 2])
|
||||
})
|
||||
|
||||
suite("loose settings", () => {
|
||||
const check = setup({
|
||||
zero: "optional",
|
||||
slash: "lazy",
|
||||
month: "loose",
|
||||
})
|
||||
check("1/", "1/", [0, 1])
|
||||
check("13/", "13/", [0, 1, 2])
|
||||
check("13", "1/3", [0, 0, 1])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,25 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import type { FormatterToken } from "../dist/minimask"
|
||||
import { getNextInputState, type InputState } from "../src/getNextInputState"
|
||||
import type { Formatter } from "../src/minimask"
|
||||
import { assert, expect, suite, test } from "vitest"
|
||||
|
||||
/**
|
||||
* In order to make before/after input states easier to test, you can use a
|
||||
* visual string representation (a "viz") of the input like:
|
||||
*
|
||||
* "ABC|DEF"
|
||||
*
|
||||
* Where the input is "ABCDEF" and the cursor is placed between "C" and "D"
|
||||
*
|
||||
* Or you can have a selection like:
|
||||
*
|
||||
* "AB|CD|EF"
|
||||
*
|
||||
* Where the selection starts between "B" and "C" and ends between "D" and "E"
|
||||
*/
|
||||
|
||||
function vizToState(input: string): InputState {
|
||||
let value = input.split("|").join("")
|
||||
let start = input.indexOf("|")
|
||||
@ -63,132 +81,169 @@ suite("viz", () => {
|
||||
fail("|A|B|C|", "cannot have more than two cursors")
|
||||
})
|
||||
|
||||
let TEST_FORMATTER: Formatter = function* (input) {
|
||||
let cursor = 0
|
||||
for (let match of input.matchAll(/[A-Z]/g)) {
|
||||
if (cursor === 3) {
|
||||
yield { char: "-", index: match.index, mask: true }
|
||||
}
|
||||
if (cursor === 6) {
|
||||
yield { char: "-", index: match.index, mask: true }
|
||||
yield { char: "-", index: match.index, mask: true }
|
||||
}
|
||||
|
||||
yield { char: match[0], index: match.index, mask: false }
|
||||
cursor += 1
|
||||
}
|
||||
function isLetter(char: string): boolean {
|
||||
return /[A-Z]/.test(char)
|
||||
}
|
||||
|
||||
suite("unformat", () => {
|
||||
function check(input: string, expected: string) {
|
||||
test(`${input} -> ${expected}`, () => {
|
||||
let TEST_FORMATTER: Formatter = (input) => {
|
||||
let tokens: FormatterToken[] = []
|
||||
let chars = input.split("")
|
||||
let index = 0
|
||||
|
||||
function letter() {
|
||||
while (true) {
|
||||
let char = chars[index]
|
||||
if (char == null) return
|
||||
if (isLetter(char)) {
|
||||
tokens.push({ char, index, mask: false })
|
||||
index++
|
||||
break
|
||||
} else {
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dash() {
|
||||
let char = chars[index]
|
||||
if (char == null) return
|
||||
|
||||
if (!isLetter(char)) {
|
||||
tokens.push({ char: "-", index, mask: true })
|
||||
index++
|
||||
} else if (index < chars.length) {
|
||||
tokens.push({ char: "-", index: index - 1, mask: true })
|
||||
}
|
||||
}
|
||||
|
||||
letter()
|
||||
letter()
|
||||
letter()
|
||||
dash()
|
||||
letter()
|
||||
letter()
|
||||
letter()
|
||||
dash()
|
||||
dash()
|
||||
letter()
|
||||
letter()
|
||||
letter()
|
||||
|
||||
return tokens
|
||||
}
|
||||
|
||||
suite("getNextInputState", () => {
|
||||
function check(input: string, expectedNormal: string, expectedDeleting: string) {
|
||||
test(`${input} -> ${expectedNormal} (normal)`, () => {
|
||||
let state = vizToState(input)
|
||||
let result = getNextInputState(TEST_FORMATTER, false, state.value, state.start, state.end)
|
||||
let result = getNextInputState(TEST_FORMATTER, state.value, state.start, state.end, false)
|
||||
let actual = stateToViz(result)
|
||||
expect(actual).toBe(expected)
|
||||
expect(actual).toBe(expectedNormal)
|
||||
})
|
||||
|
||||
test(`${input} -> ${expectedNormal} (deleting)`, () => {
|
||||
let state = vizToState(input)
|
||||
console.log(state)
|
||||
let result = getNextInputState(TEST_FORMATTER, state.value, state.start, state.end, true)
|
||||
// console.log(result)
|
||||
let actual = stateToViz(result)
|
||||
expect(actual).toBe(expectedDeleting)
|
||||
})
|
||||
}
|
||||
|
||||
suite("cursor", () => {
|
||||
check("|ABC-DEF", "|ABCDEF")
|
||||
check("A|BC-DEF", "A|BCDEF")
|
||||
check("AB|C-DEF", "AB|CDEF")
|
||||
check("ABC|-DEF", "ABC|DEF")
|
||||
check("ABC-|DEF", "ABC|DEF")
|
||||
check("ABC-D|EF", "ABCD|EF")
|
||||
check("ABC-DE|F", "ABCDE|F")
|
||||
check("ABC-DEF|", "ABCDEF|")
|
||||
})
|
||||
suite("selection", () => {
|
||||
check("|A|BC-DEF", "|A|BCDEF")
|
||||
check("|AB|C-DEF", "|AB|CDEF")
|
||||
check("|ABC|-DEF", "|ABC|DEF")
|
||||
check("|ABC-|DEF", "|ABC|DEF")
|
||||
check("|ABC-D|EF", "|ABCD|EF")
|
||||
check("|ABC-DE|F", "|ABCDE|F")
|
||||
check("|ABC-DEF|", "|ABCDEF|")
|
||||
check("A|BC-DEF|", "A|BCDEF|")
|
||||
check("AB|C-DEF|", "AB|CDEF|")
|
||||
check("ABC|-DEF|", "ABC|DEF|")
|
||||
check("ABC-|DEF|", "ABC|DEF|")
|
||||
check("ABC-D|EF|", "ABCD|EF|")
|
||||
check("ABC-DE|F|", "ABCDE|F|")
|
||||
})
|
||||
suite("multiple dropped chars before", () => {
|
||||
check("ABC-DEF--G|HI", "ABCDEFG|HI")
|
||||
check("ABC-DEF--|GHI", "ABCDEF|GHI")
|
||||
check("ABC-DEF-|-GHI", "ABCDEF|GHI")
|
||||
check("ABC-DEF|--GHI", "ABCDEF|GHI")
|
||||
check("ABC-DE|F--GHI", "ABCDE|FGHI")
|
||||
})
|
||||
suite("poorly formatted", () => {
|
||||
check("|*A*B*C*D*E*F*", "|ABCDEF")
|
||||
check("*|A*B*C*D*E*F*", "|ABCDEF")
|
||||
check("*A|*B*C*D*E*F*", "A|BCDEF")
|
||||
check("*A*|B*C*D*E*F*", "A|BCDEF")
|
||||
check("*A*B|*C*D*E*F*", "AB|CDEF")
|
||||
check("*A*B*|C*D*E*F*", "AB|CDEF")
|
||||
check("*A*B*C|*D*E*F*", "ABC|DEF")
|
||||
check("*A*B*C*|D*E*F*", "ABC|DEF")
|
||||
check("*A*B*C*D|*E*F*", "ABCD|EF")
|
||||
check("*A*B*C*D*|E*F*", "ABCD|EF")
|
||||
check("*A*B*C*D*E|*F*", "ABCDE|F")
|
||||
check("*A*B*C*D*E*|F*", "ABCDE|F")
|
||||
check("*A*B*C*D*E*F|*", "ABCDEF|")
|
||||
check("*A*B*C*D*E*F*|", "ABCDEF|")
|
||||
})
|
||||
})
|
||||
|
||||
suite("format", () => {
|
||||
function check(input: string, expected: string) {
|
||||
test(`${input} -> ${expected}`, () => {
|
||||
let state = vizToState(input)
|
||||
let result = getNextInputState(TEST_FORMATTER, true, state.value, state.start, state.end)
|
||||
let actual = stateToViz(result)
|
||||
expect(actual).toBe(expected)
|
||||
suite("formatted", () => {
|
||||
suite("cursor", () => {
|
||||
check("|ABC-DEF", "|ABC-DEF", "|ABC-DEF")
|
||||
check("A|BC-DEF", "A|BC-DEF", "A|BC-DEF")
|
||||
check("AB|C-DEF", "AB|C-DEF", "AB|C-DEF")
|
||||
check("ABC|-DEF", "ABC|-DEF", "ABC|-DEF") // deleting moves before mask chars
|
||||
check("ABC-|DEF", "ABC-|DEF", "ABC|-DEF") // deleting moves before mask chars
|
||||
check("ABC-D|EF", "ABC-D|EF", "ABC-D|EF")
|
||||
check("ABC-DE|F", "ABC-DE|F", "ABC-DE|F")
|
||||
check("ABC-DEF|", "ABC-DEF|", "ABC-DEF|")
|
||||
})
|
||||
}
|
||||
suite("selection", () => {
|
||||
check("|A|BC-DEF", "|A|BC-DEF", "|A|BC-DEF")
|
||||
check("|AB|C-DEF", "|AB|C-DEF", "|AB|C-DEF")
|
||||
check("|ABC|-DEF", "|ABC|-DEF", "|ABC|-DEF")
|
||||
check("|ABC-|DEF", "|ABC-|DEF", "|ABC|-DEF") // deleting moves before mask chars
|
||||
check("|ABC-D|EF", "|ABC-D|EF", "|ABC-D|EF")
|
||||
check("|ABC-DE|F", "|ABC-DE|F", "|ABC-DE|F")
|
||||
check("|ABC-DEF|", "|ABC-DEF|", "|ABC-DEF|")
|
||||
check("A|BC-DEF|", "A|BC-DEF|", "A|BC-DEF|")
|
||||
check("AB|C-DEF|", "AB|C-DEF|", "AB|C-DEF|")
|
||||
check("ABC|-DEF|", "ABC|-DEF|", "ABC|-DEF|")
|
||||
check("ABC-|DEF|", "ABC-|DEF|", "ABC|-DEF|") // deleting moves before mask chars
|
||||
check("ABC-D|EF|", "ABC-D|EF|", "ABC-D|EF|")
|
||||
check("ABC-DE|F|", "ABC-DE|F|", "ABC-DE|F|")
|
||||
})
|
||||
suite("multiple dropped chars before", () => {
|
||||
check("ABC-DEF--G|HI", "ABC-DEF--G|HI", "ABC-DEF--G|HI")
|
||||
check("ABC-DEF--|GHI", "ABC-DEF--|GHI", "ABC-DEF|--GHI") // deleting moves before mask chars
|
||||
check("ABC-DEF-|-GHI", "ABC-DEF-|-GHI", "ABC-DEF|--GHI") // deleting moves before mask chars
|
||||
check("ABC-DEF|--GHI", "ABC-DEF|--GHI", "ABC-DEF|--GHI")
|
||||
check("ABC-DE|F--GHI", "ABC-DE|F--GHI", "ABC-DE|F--GHI")
|
||||
})
|
||||
suite("poorly formatted", () => {
|
||||
check("|*A*B*C*D*E*F*", "|ABC-DEF-", "|ABC-DEF-")
|
||||
check("*|A*B*C*D*E*F*", "|ABC-DEF-", "|ABC-DEF-")
|
||||
check("*A|*B*C*D*E*F*", "A|BC-DEF-", "A|BC-DEF-")
|
||||
check("*A*|B*C*D*E*F*", "A|BC-DEF-", "A|BC-DEF-")
|
||||
check("*A*B|*C*D*E*F*", "AB|C-DEF-", "AB|C-DEF-")
|
||||
check("*A*B*|C*D*E*F*", "AB|C-DEF-", "AB|C-DEF-")
|
||||
check("*A*B*C|*D*E*F*", "ABC|-DEF-", "ABC|-DEF-")
|
||||
check("*A*B*C*|D*E*F*", "ABC-|DEF-", "ABC|-DEF-") // deleting moves before mask chars
|
||||
check("*A*B*C*D|*E*F*", "ABC-D|EF-", "ABC-D|EF-")
|
||||
check("*A*B*C*D*|E*F*", "ABC-D|EF-", "ABC-D|EF-")
|
||||
check("*A*B*C*D*E|*F*", "ABC-DE|F-", "ABC-DE|F-")
|
||||
check("*A*B*C*D*E*|F*", "ABC-DE|F-", "ABC-DE|F-")
|
||||
check("*A*B*C*D*E*F|*", "ABC-DEF|-", "ABC-DEF|") // deleting trims trailing mask chars
|
||||
check("*A*B*C*D*E*F*|", "ABC-DEF-|", "ABC-DEF|") // deleting trims trailing mask chars
|
||||
})
|
||||
})
|
||||
|
||||
suite("cursor", () => {
|
||||
check("|ABCDEF", "|ABC-DEF")
|
||||
check("A|BCDEF", "A|BC-DEF")
|
||||
check("AB|CDEF", "AB|C-DEF")
|
||||
check("ABC|DEF", "ABC|-DEF")
|
||||
check("ABCD|EF", "ABC-D|EF")
|
||||
check("ABCDE|F", "ABC-DE|F")
|
||||
check("ABCDEF|", "ABC-DEF|")
|
||||
})
|
||||
suite("selection", () => {
|
||||
check("|A|BCDEF", "|A|BC-DEF")
|
||||
check("|AB|CDEF", "|AB|C-DEF")
|
||||
check("|ABC|DEF", "|ABC|-DEF")
|
||||
check("|ABCD|EF", "|ABC-D|EF")
|
||||
check("|ABCDE|F", "|ABC-DE|F")
|
||||
check("|ABCDEF|", "|ABC-DEF|")
|
||||
check("A|BCDEF|", "A|BC-DEF|")
|
||||
check("AB|CDEF|", "AB|C-DEF|")
|
||||
check("ABC|DEF|", "ABC|-DEF|")
|
||||
check("ABCD|EF|", "ABC-D|EF|")
|
||||
check("ABCDE|F|", "ABC-DE|F|")
|
||||
})
|
||||
suite("multiple dropped chars before", () => {
|
||||
check("ABCDEFG|HI", "ABC-DEF--G|HI")
|
||||
check("ABCDEF|GHI", "ABC-DEF|--GHI")
|
||||
check("ABCDE|FGHI", "ABC-DE|F--GHI")
|
||||
})
|
||||
suite("poorly formatted", () => {
|
||||
check("|*A*B*C*D*E*F*", "|ABC-DEF")
|
||||
check("*|A*B*C*D*E*F*", "|ABC-DEF")
|
||||
check("*A|*B*C*D*E*F*", "A|BC-DEF")
|
||||
check("*A*|B*C*D*E*F*", "A|BC-DEF")
|
||||
check("*A*B|*C*D*E*F*", "AB|C-DEF")
|
||||
check("*A*B*|C*D*E*F*", "AB|C-DEF")
|
||||
check("*A*B*C|*D*E*F*", "ABC|-DEF")
|
||||
check("*A*B*C*D|*E*F*", "ABC-D|EF")
|
||||
check("*A*B*C*D*|E*F*", "ABC-D|EF")
|
||||
check("*A*B*C*D*E|*F*", "ABC-DE|F")
|
||||
check("*A*B*C*D*E*|F*", "ABC-DE|F")
|
||||
check("*A*B*C*D*E*F|*", "ABC-DEF|")
|
||||
check("*A*B*C*D*E*F*|", "ABC-DEF|")
|
||||
suite("unformatted", () => {
|
||||
suite("cursor", () => {
|
||||
check("|ABCDEF", "|ABC-DEF", "|ABC-DEF")
|
||||
check("A|BCDEF", "A|BC-DEF", "A|BC-DEF")
|
||||
check("AB|CDEF", "AB|C-DEF", "AB|C-DEF")
|
||||
check("ABC|DEF", "ABC-|DEF", "ABC|-DEF")
|
||||
check("ABCD|EF", "ABC-D|EF", "ABC-D|EF")
|
||||
check("ABCDE|F", "ABC-DE|F", "ABC-DE|F")
|
||||
check("ABCDEF|", "ABC-DEF|", "ABC-DEF|")
|
||||
})
|
||||
suite("selection", () => {
|
||||
check("|A|BCDEF", "|A|BC-DEF", "|A|BC-DEF")
|
||||
check("|AB|CDEF", "|AB|C-DEF", "|AB|C-DEF")
|
||||
check("|ABC|DEF", "|ABC-|DEF", "|ABC|-DEF") // deleting moves before mask chars
|
||||
check("|ABCD|EF", "|ABC-D|EF", "|ABC-D|EF")
|
||||
check("|ABCDE|F", "|ABC-DE|F", "|ABC-DE|F")
|
||||
check("|ABCDEF|", "|ABC-DEF|", "|ABC-DEF|")
|
||||
check("A|BCDEF|", "A|BC-DEF|", "A|BC-DEF|")
|
||||
check("AB|CDEF|", "AB|C-DEF|", "AB|C-DEF|")
|
||||
check("ABC|DEF|", "ABC-|DEF|", "ABC|-DEF|") // deleting moves before mask chars
|
||||
check("ABCD|EF|", "ABC-D|EF|", "ABC-D|EF|")
|
||||
check("ABCDE|F|", "ABC-DE|F|", "ABC-DE|F|")
|
||||
})
|
||||
suite("multiple dropped chars before", () => {
|
||||
check("ABCDEFG|HI", "ABC-DEF--G|HI", "ABC-DEF--G|HI")
|
||||
check("ABCDEF|GHI", "ABC-DEF--|GHI", "ABC-DEF|--GHI")
|
||||
check("ABCDE|FGHI", "ABC-DE|F--GHI", "ABC-DE|F--GHI")
|
||||
})
|
||||
suite("poorly formatted", () => {
|
||||
check("|*A*B*C*D*E*F*", "|ABC-DEF-", "|ABC-DEF-")
|
||||
check("*|A*B*C*D*E*F*", "|ABC-DEF-", "|ABC-DEF-")
|
||||
check("*A|*B*C*D*E*F*", "A|BC-DEF-", "A|BC-DEF-")
|
||||
check("*A*|B*C*D*E*F*", "A|BC-DEF-", "A|BC-DEF-")
|
||||
check("*A*B|*C*D*E*F*", "AB|C-DEF-", "AB|C-DEF-")
|
||||
check("*A*B*|C*D*E*F*", "AB|C-DEF-", "AB|C-DEF-")
|
||||
check("*A*B*C|*D*E*F*", "ABC|-DEF-", "ABC|-DEF-")
|
||||
check("*A*B*C*D|*E*F*", "ABC-D|EF-", "ABC-D|EF-")
|
||||
check("*A*B*C*D*|E*F*", "ABC-D|EF-", "ABC-D|EF-")
|
||||
check("*A*B*C*D*E|*F*", "ABC-DE|F-", "ABC-DE|F-")
|
||||
check("*A*B*C*D*E*|F*", "ABC-DE|F-", "ABC-DE|F-")
|
||||
check("*A*B*C*D*E*F|*", "ABC-DEF|-", "ABC-DEF|") // deleting trims trailing mask chars
|
||||
check("*A*B*C*D*E*F*|", "ABC-DEF-|", "ABC-DEF|") // deleting trims trailing mask chars
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user