mirror of
https://github.com/voson-wang/toon.git
synced 2026-01-29 23:34:10 +08:00
test: add case for unquoted invalid numeric formats as strings
This commit is contained in:
28
src/shared/literal-utils.ts
Normal file
28
src/shared/literal-utils.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { FALSE_LITERAL, NULL_LITERAL, TRUE_LITERAL } from '../constants'
|
||||
|
||||
/**
|
||||
* Checks if a token is a boolean or null literal (`true`, `false`, `null`).
|
||||
*/
|
||||
export function isBooleanOrNullLiteral(token: string): boolean {
|
||||
return token === TRUE_LITERAL || token === FALSE_LITERAL || token === NULL_LITERAL
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a token represents a valid numeric literal.
|
||||
*
|
||||
* @remarks
|
||||
* Rejects numbers with leading zeros (except `"0"` itself or decimals like `"0.5"`).
|
||||
*/
|
||||
export function isNumericLiteral(token: string): boolean {
|
||||
if (!token)
|
||||
return false
|
||||
|
||||
// Must not have leading zeros (except for `"0"` itself or decimals like `"0.5"`)
|
||||
if (token.length > 1 && token[0] === '0' && token[1] !== '.') {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if it's a valid number
|
||||
const num = Number(token)
|
||||
return !Number.isNaN(num) && Number.isFinite(num)
|
||||
}
|
||||
127
src/shared/string-utils.ts
Normal file
127
src/shared/string-utils.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { BACKSLASH, CARRIAGE_RETURN, DOUBLE_QUOTE, NEWLINE, TAB } from '../constants'
|
||||
|
||||
/**
|
||||
* Escapes special characters in a string for encoding.
|
||||
*
|
||||
* @remarks
|
||||
* Handles backslashes, quotes, newlines, carriage returns, and tabs.
|
||||
*/
|
||||
export function escapeString(value: string): string {
|
||||
return value
|
||||
.replace(/\\/g, `${BACKSLASH}${BACKSLASH}`)
|
||||
.replace(/"/g, `${BACKSLASH}${DOUBLE_QUOTE}`)
|
||||
.replace(/\n/g, `${BACKSLASH}n`)
|
||||
.replace(/\r/g, `${BACKSLASH}r`)
|
||||
.replace(/\t/g, `${BACKSLASH}t`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescapes a string by processing escape sequences.
|
||||
*
|
||||
* @remarks
|
||||
* Handles `\n`, `\t`, `\r`, `\\`, and `\"` escape sequences.
|
||||
*/
|
||||
export function unescapeString(value: string): string {
|
||||
let result = ''
|
||||
let i = 0
|
||||
|
||||
while (i < value.length) {
|
||||
if (value[i] === BACKSLASH) {
|
||||
if (i + 1 >= value.length) {
|
||||
throw new SyntaxError('Invalid escape sequence: backslash at end of string')
|
||||
}
|
||||
|
||||
const next = value[i + 1]
|
||||
if (next === 'n') {
|
||||
result += NEWLINE
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (next === 't') {
|
||||
result += TAB
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (next === 'r') {
|
||||
result += CARRIAGE_RETURN
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (next === BACKSLASH) {
|
||||
result += BACKSLASH
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (next === DOUBLE_QUOTE) {
|
||||
result += DOUBLE_QUOTE
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
|
||||
throw new SyntaxError(`Invalid escape sequence: \\${next}`)
|
||||
}
|
||||
|
||||
result += value[i]
|
||||
i++
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the index of the closing double quote in a string, accounting for escape sequences.
|
||||
*
|
||||
* @param content The string to search in
|
||||
* @param start The index of the opening quote
|
||||
* @returns The index of the closing quote, or -1 if not found
|
||||
*/
|
||||
export function findClosingQuote(content: string, start: number): number {
|
||||
let i = start + 1
|
||||
while (i < content.length) {
|
||||
if (content[i] === BACKSLASH && i + 1 < content.length) {
|
||||
// Skip escaped character
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if (content[i] === DOUBLE_QUOTE) {
|
||||
return i
|
||||
}
|
||||
i++
|
||||
}
|
||||
return -1 // Not found
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the index of a specific character outside of quoted sections.
|
||||
*
|
||||
* @param content The string to search in
|
||||
* @param char The character to look for
|
||||
* @param start Optional starting index (defaults to 0)
|
||||
* @returns The index of the character, or -1 if not found outside quotes
|
||||
*/
|
||||
export function findUnquotedChar(content: string, char: string, start = 0): number {
|
||||
let inQuotes = false
|
||||
let i = start
|
||||
|
||||
while (i < content.length) {
|
||||
if (content[i] === BACKSLASH && i + 1 < content.length && inQuotes) {
|
||||
// Skip escaped character
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
|
||||
if (content[i] === DOUBLE_QUOTE) {
|
||||
inQuotes = !inQuotes
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
if (content[i] === char && !inQuotes) {
|
||||
return i
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
84
src/shared/validation.ts
Normal file
84
src/shared/validation.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { COMMA, LIST_ITEM_MARKER } from '../constants'
|
||||
import { isBooleanOrNullLiteral } from './literal-utils'
|
||||
|
||||
/**
|
||||
* Checks if a key can be used without quotes.
|
||||
*
|
||||
* @remarks
|
||||
* Valid unquoted keys must start with a letter or underscore,
|
||||
* followed by letters, digits, underscores, or dots.
|
||||
*/
|
||||
export function isValidUnquotedKey(key: string): boolean {
|
||||
return /^[A-Z_][\w.]*$/i.test(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a string value can be safely encoded without quotes.
|
||||
*
|
||||
* @remarks
|
||||
* A string needs quoting if it:
|
||||
* - Is empty
|
||||
* - Has leading or trailing whitespace
|
||||
* - Could be confused with a literal (boolean, null, number)
|
||||
* - Contains structural characters (colons, brackets, braces)
|
||||
* - Contains quotes or backslashes (need escaping)
|
||||
* - Contains control characters (newlines, tabs, etc.)
|
||||
* - Contains the active delimiter
|
||||
* - Starts with a list marker (hyphen)
|
||||
*/
|
||||
export function isSafeUnquoted(value: string, delimiter: string = COMMA): boolean {
|
||||
if (!value) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (value !== value.trim()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if it looks like any literal value (boolean, null, or numeric)
|
||||
if (isBooleanOrNullLiteral(value) || isNumericLike(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for colon (always structural)
|
||||
if (value.includes(':')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for quotes and backslash (always need escaping)
|
||||
if (value.includes('"') || value.includes('\\')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for brackets and braces (always structural)
|
||||
if (/[[\]{}]/.test(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for control characters (newline, carriage return, tab - always need quoting/escaping)
|
||||
if (/[\n\r\t]/.test(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for the active delimiter
|
||||
if (value.includes(delimiter)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for hyphen at start (list marker)
|
||||
if (value.startsWith(LIST_ITEM_MARKER)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string looks like a number.
|
||||
*
|
||||
* @remarks
|
||||
* Match numbers like `42`, `-3.14`, `1e-6`, `05`, etc.
|
||||
*/
|
||||
function isNumericLike(value: string): boolean {
|
||||
return /^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i.test(value) || /^0\d+$/.test(value)
|
||||
}
|
||||
Reference in New Issue
Block a user