1/**
2 * Helpers for escaping JSON Pointer path segments and converting JSON Pointer
3 * URI fragments. JSON Pointer uses `/` to separate path tokens inside a JSON
4 * document, so token text must encode literal `~` and `/` characters. URI
5 * fragments additionally apply percent-encoding after JSON Pointer escaping.
6 *
7 * @since 4.0.0
8 */
9
10/**
11 * Escapes a JSON Pointer reference token according to RFC 6901 by encoding special characters so the token can be safely used as a segment in a JSON Pointer.
12 *
13 * **When to use**
14 *
15 * Use when you need to escape a single JSON Pointer path segment.
16 *
17 * **Details**
18 *
19 * - Returns a new escaped string
20 * - Replaces `~` (tilde) with `~0` and `/` (forward slash) with `~1`
21 * - Returns the input unchanged if it contains no special characters
22 * - Empty strings are valid and returned unchanged
23 *
24 * **Gotchas**
25 *
26 * The replacement order matters: `~` is replaced before `/` to prevent double-escaping.
27 *
28 * **Example** (Escaping special characters)
29 *
30 * ```ts import.meta.vitest
31 * import { JsonPointer } from "effect"
32 *
33 * JsonPointer.escapeToken("a/b") // => "a~1b"
34 * JsonPointer.escapeToken("c~d") // => "c~0d"
35 * JsonPointer.escapeToken("path/to~key") // => "path~1to~0key"
36 * ```
37 *
38 * @see {@link unescapeToken} The inverse operation for decoding escaped tokens
39 * @category encoding
40 * @since 4.0.0
41 */
42export function escapeToken(token: string): string {
43 return token.replace(/~/g, "~0").replace(/\//g, "~1")
44}
45
46/**
47 * Decodes a JSON Pointer reference token according to RFC 6901 escaping rules.
48 *
49 * **When to use**
50 *
51 * Use when you need to decode a single escaped JSON Pointer path segment.
52 *
53 * **Details**
54 *
55 * - Returns a new unescaped string
56 * - Replaces `~1` with `/` (forward slash) and `~0` with `~` (tilde)
57 * - Returns the input unchanged if it contains no escaped sequences
58 * - Empty strings are valid and returned unchanged
59 *
60 * **Gotchas**
61 *
62 * The replacement order matters: `~1` is replaced before `~0` to prevent incorrect decoding.
63 *
64 * **Example** (Unescaping special characters)
65 *
66 * ```ts import.meta.vitest
67 * import { JsonPointer } from "effect"
68 *
69 * JsonPointer.unescapeToken("a~1b") // => "a/b"
70 * JsonPointer.unescapeToken("c~0d") // => "c~d"
71 * JsonPointer.unescapeToken("path~1to~0key") // => "path/to~key"
72 * ```
73 *
74 * @see {@link escapeToken} The inverse operation for encoding tokens
75 * @category decoding
76 * @since 4.0.0
77 */
78export function unescapeToken(token: string): string {
79 return token.replace(/~1/g, "/").replace(/~0/g, "~")
80}
81
82/** @internal */
83export function formatUriFragmentToken(token: string): string {
84 return encodeURI(escapeToken(token)).replace(/#/g, "%23")
85}
86
87/** @internal */
88export function decodeUriFragment(fragment: string): string | undefined {
89 if (fragment.length === 0 || fragment === "#") return ""
90 if (!fragment.startsWith("#")) return undefined
91 const encoded = fragment.slice(1)
92 try {
93 if (encodeURI(encoded).replace(/%25/g, "%").replace(/#/g, "%23") !== encoded) return undefined
94 const pointer = decodeURIComponent(encoded)
95 return pointer.startsWith("/") && !/~(?:[^01]|$)/.test(pointer) ? pointer : undefined
96 } catch {
97 return undefined
98 }
99}
100
101/**
102 * Parses a JSON Pointer URI fragment into decoded path tokens.
103 *
104 * **When to use**
105 *
106 * Use when you need to resolve a URI fragment against a JSON document.
107 *
108 * **Details**
109 *
110 * Percent-encoding is decoded before the pointer is split into tokens, then
111 * each token is decoded with {@link unescapeToken}. The empty string and `#`
112 * both represent the document root.
113 *
114 * **Gotchas**
115 *
116 * Returns `undefined` when the input is not a URI fragment, contains characters
117 * that require percent-encoding, or contains an invalid JSON Pointer escape
118 * sequence.
119 *
120 * **Example** (Parsing URI fragments)
121 *
122 * ```ts import.meta.vitest
123 * import { JsonPointer } from "effect"
124 *
125 * JsonPointer.parseUriFragment("#/users/a~1b") // => ["users", "a/b"]
126 * JsonPointer.parseUriFragment("#/caf%C3%A9") // => ["café"]
127 * JsonPointer.parseUriFragment("#/%") // => undefined
128 * JsonPointer.parseUriFragment("#/a#b") // => undefined
129 * ```
130 *
131 * @see {@link formatUriFragment} for the inverse operation
132 * @category decoding
133 * @since 4.0.0
134 */
135export function parseUriFragment(fragment: string): ReadonlyArray<string> | undefined {
136 const pointer = decodeUriFragment(fragment)
137 return pointer === undefined ? undefined : pointer.length === 0 ? [] : pointer.slice(1).split("/").map(unescapeToken)
138}
139
140/**
141 * Formats path tokens as a JSON Pointer URI fragment.
142 *
143 * **When to use**
144 *
145 * Use when you need a URI fragment that identifies a value in a JSON document.
146 *
147 * **Details**
148 *
149 * Each token is encoded with {@link escapeToken} before URI percent-encoding
150 * is applied. An empty path is formatted as `#`.
151 *
152 * **Gotchas**
153 *
154 * Throws a `URIError` when a token contains an unpaired surrogate.
155 *
156 * **Example** (Formatting a URI fragment)
157 *
158 * ```ts import.meta.vitest
159 * import { JsonPointer } from "effect"
160 *
161 * JsonPointer.formatUriFragment(["users", "a/b", "Rate%"]) // => "#/users/a~1b/Rate%25"
162 * ```
163 *
164 * @see {@link parseUriFragment} for the inverse operation
165 * @category encoding
166 * @since 4.0.0
167 */
168export function formatUriFragment(path: ReadonlyArray<string>): string {
169 return path.reduce(
170 (fragment, token) => `${fragment}/${formatUriFragmentToken(token)}`,
171 "#"
172 )
173}