Repository navigation
Expand file tree
/
Copy pathstring.test.ts
More file actions
119 lines (105 loc) · 4.22 KB
/
Copy pathstring.test.ts
File metadata and controls
119 lines (105 loc) · 4.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import { describe, expect, it } from 'vitest'
import {
compareStrings,
escapeRegExp,
forEachSearchOccurrence,
projectEscapedMarkdownForSearch,
sanitizeForJsonb,
sanitizeValueForJsonb,
stripTrailingSlashes,
toWellFormed,
truncateAtCodePoint,
} from './string.js'
describe('sanitizeForJsonb', () => {
it('replaces a lone high surrogate left by mid-character truncation', () => {
// '𝐀'.slice(0, 1) cuts the surrogate pair in half
const cut = '\uD835\uDC00'.slice(0, 1)
expect(sanitizeForJsonb(`FIFA WORLD CU${cut}`)).toBe('FIFA WORLD CU\uFFFD')
})
it('replaces NUL characters', () => {
expect(sanitizeForJsonb('a\u0000b')).toBe('a\uFFFDb')
})
it('handles a lone high surrogate followed by a valid pair', () => {
expect(sanitizeForJsonb('\uD835\uD835\uDC00')).toBe('\uFFFD\uD835\uDC00')
})
})
describe('toWellFormed', () => {
it('replaces each unpaired half one-for-one, keeping pairs, NUL, and length', () => {
const input = '\uDE00a\uDE00\uD83D\uD83D\uDE00b\u0000\uD83D'
const output = toWellFormed(input)
expect(output).toBe('\uFFFDa\uFFFD\uFFFD\uD83D\uDE00b\u0000\uFFFD')
expect(output).toHaveLength(input.length)
})
})
describe('sanitizeValueForJsonb', () => {
it('returns the same reference when nothing needs rewriting', () => {
const input = { a: ['clean', { b: 'also clean 🏆' }], n: 3 }
expect(sanitizeValueForJsonb(input)).toBe(input)
})
})
describe('projectEscapedMarkdownForSearch', () => {
/* The span must cover the backslash, or replacing a match would leave it stranded. */
it('maps a projected range back over the escape it consumed', () => {
const source = 'x SB\\_ACTION y'
const { text, starts } = projectEscapedMarkdownForSearch(source)
const start = text.indexOf('SB_ACTION')
const end = start + 'SB_ACTION'.length
expect(source.slice(starts[start], starts[end])).toBe('SB\\_ACTION')
})
it('maps every position when escapes repeat', () => {
const source = 'a\\_b\\_c'
const { text, starts } = projectEscapedMarkdownForSearch(source)
expect(text).toBe('a_b_c')
for (let i = 0; i < text.length; i += 1) {
expect(source.slice(starts[i], starts[i + 1]).endsWith(text[i])).toBe(true)
}
expect(starts[text.length]).toBe(source.length)
})
})
describe('forEachSearchOccurrence', () => {
const spans = (text: string, query: string, caseSensitive?: boolean) => {
const found: string[] = []
forEachSearchOccurrence(
text,
query,
(start, end) => found.push(text.slice(start, end)),
caseSensitive
)
return found
}
it('keeps context-sensitive lowercasing in the length-preserving fallback', () => {
// '\u0130' expands, so the whole-string fast path is unavailable. Folding the rest character by
// character would lowercase the word-final '\u03a3' to '\u03c3' instead of '\u03c2', making one
// unrelated code point change how every sigma in the string matches.
expect(spans('\u0130\u03a3', '\u0130\u03c2')).toEqual(['\u0130\u03a3'])
})
it('reports bounds into the caller\u2019s own string after a length-changing lowercase', () => {
// '\u0130'.toLowerCase() is TWO characters. A plain lowercase would slide every later index by
// one, so the caller would slice the wrong span out of the string it passed in.
expect(spans('\u0130xyz target', 'target')).toEqual(['target'])
})
})
describe('escapeRegExp', () => {
it('escapes every occurrence, not just the first', () => {
expect(escapeRegExp('a.b.c')).toBe('a\\.b\\.c')
})
})
describe('stripTrailingSlashes', () => {
it('removes only the trailing run', () => {
expect(stripTrailingSlashes('/a//b///')).toBe('/a//b')
expect(stripTrailingSlashes('///')).toBe('')
expect(stripTrailingSlashes('a')).toBe('a')
})
})
describe('compareStrings', () => {
it('sorts uppercase before lowercase, unlike localeCompare', () => {
expect(compareStrings('Z', 'a')).toBe(-1)
expect(['a', 'Z'].sort(compareStrings)).toEqual(['Z', 'a'])
})
})
describe('truncateAtCodePoint', () => {
it('moves the cut back one unit rather than splitting a surrogate pair', () => {
expect(truncateAtCodePoint('ab😀cd', 3)).toBe('ab...')
expect(truncateAtCodePoint('😀'.repeat(3), 3)).toBe('😀...')
})
})