Skip to content

Commit 82474b7

Browse files
committed
fix: allow unescaped hyphen in string in some cases
If a string starts with a hyphen (aka: minus sign) but a digit does not immediately follow it, there is no need to escape the hyphen.
1 parent ba8cdbd commit 82474b7

File tree

5 files changed

+24
-13
lines changed

5 files changed

+24
-13
lines changed

spec.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ For example, these characters are _always_ escaped with a backslash (`\`):
9898
And these characters are escaped if they're the first character, since they would otherwise imply another data type:
9999

100100
- digits (if not escaped, implies a number)
101-
- hyphens (if not escaped, implies a negative number)
101+
- hyphens, but only if followed by a digit (if not escaped, implies a negative number)
102102
- backslashes (if not escaped, implies an escape sequence)
103103

104104
The following object…

src/charCode.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ export const enum CharCode {
1818
Quote = 39, // '
1919
Space = 32,
2020
}
21+
22+
export function isDigit(charCode: number): boolean {
23+
return charCode >= CharCode.DigitMin && charCode <= CharCode.DigitMax
24+
}

src/decode.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CharCode } from './charCode.js'
1+
import { CharCode, isDigit } from './charCode.js'
22
import { CodableObject, CodableValue } from './types.js'
33

44
type URLSearchParams = typeof globalThis extends {
@@ -69,11 +69,13 @@ function decodeValue(input: string, cursor = { pos: 0 }): CodableValue {
6969
break
7070

7171
case CharCode.Minus:
72-
mode = ValueMode.NumberOrBigint
72+
mode = isDigit(input.charCodeAt(pos + 1))
73+
? ValueMode.NumberOrBigint
74+
: ValueMode.String
7375
break
7476

7577
default:
76-
if (charCode >= CharCode.DigitMin && charCode <= CharCode.DigitMax) {
78+
if (isDigit(charCode)) {
7779
mode = ValueMode.NumberOrBigint
7880
} else {
7981
endPos = nested ? findEndPos(input, pos) : input.length

src/encode.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { isArray } from 'radashi'
2-
import { CharCode } from './charCode.js'
2+
import { CharCode, isDigit } from './charCode.js'
33
import { CodableObject, CodableValue } from './types.js'
44

55
export type EncodeOptions = {
@@ -66,7 +66,7 @@ function encodeValue(value: CodableValue): string {
6666
}
6767
// For string values, escape the first character if it's a digit or
6868
// minus sign, since those are used to detect a number value.
69-
return encodeString(value, isNumberLike(value.charCodeAt(0)))
69+
return encodeString(value, isNumberLike(value))
7070
}
7171
if (typeof value === 'number') {
7272
if (Number.isNaN(value) || !Number.isFinite(value)) {
@@ -86,6 +86,14 @@ function encodeValue(value: CodableValue): string {
8686
throw new Error(`Unsupported value type: ${typeof value}`)
8787
}
8888

89+
function isNumberLike(value: string): boolean {
90+
const charCode = value.charCodeAt(0)
91+
return (
92+
isDigit(charCode) ||
93+
(charCode === CharCode.Minus && isDigit(value.charCodeAt(1)))
94+
)
95+
}
96+
8997
function encodeArray(array: readonly CodableValue[]): string {
9098
let result = ''
9199
for (let i = 0; i < array.length; i++) {
@@ -106,13 +114,6 @@ function encodeObject(obj: CodableObject): string {
106114
return `{${encodeProperties(obj, true)}}`
107115
}
108116

109-
function isNumberLike(charCode: number): boolean {
110-
return (
111-
(charCode >= CharCode.DigitMin && charCode <= CharCode.DigitMax) ||
112-
charCode === CharCode.Minus
113-
)
114-
}
115-
116117
function encodeString(str: string, escape?: boolean): string {
117118
// Regardless of the escape flag, we always escape backslashes.
118119
let result = escape || str.charCodeAt(0) === CharCode.Escape ? '\\' : ''

test/cases.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ export const cases: Record<string, Case | Case[]> = {
141141
encoded: 'a=+\\(\\)\\{\\}\\:\\,+',
142142
},
143143
],
144+
'string that starts with minus sign but is not a number': {
145+
decoded: { sort: '-title' },
146+
encoded: 'sort=-title',
147+
},
144148
'non-ASCII characters': [
145149
{
146150
decoded: { a: '💩' },

0 commit comments

Comments
 (0)