url-from
Version:
Type-safe URL generator with RFC3986 encoding support
696 lines (514 loc) β’ 26.4 kB
Markdown
# url-from
A URL generation library that supports type-safe path and query RFC3986 encoding.
- [Usage]
- [API]
- [Tips]
## Highlight
- π Embedding values with type-safe placeholders
- π Proper RFC3986 encoding for each component such as path and query
- π Flexible management of slashes
- π§© Separation of URL definition and generation.
- π± Support for various formats
- Absolute URL `https://example.com/`
- [Protocol-relative URL] `//example.com/`
- Root path `/path/to`
- Relative path `path/to`
url-from has no external dependencies.
## Install
This library can be used with TypeScript 4.7.2 or later.
```
npm install url-from
```
## Usage
```js
import urlFrom from "url-from";
// bindUrl: (params?: { tag?: string, "?query": QueryParams, "#fragment"?: string }) => string;
const bindUrl = urlFrom`https://example.com/tags${"/tag?:string"}`;
const url1 = bindUrl();
console.log(url1); // => "https://example.com/tags"
const url2 = bindUrl({
tag: "πΉ", // tag is string type only.
"?query": { foo: "!'", bar: 2, baz: false },
"#fragment": "()*",
});
console.log(url2); // => "https://example.com/tags/%F0%9F%90%B9?foo=%21%27&bar=2&baz=false#%28%29%2A"
```
Please also check the following when using this library:
- [Common Patterns for Throwing Exceptions](#main-patterns-that-cause-exceptions)
- [Common Patterns for Emitting Warnings](#main-patterns-for-issuing-warnings)
- [Literal Parts with `%` will Emit Warnings](#a-warning-is-issued-when--is-included-in-the-literal-part)
## API
- [Bind Function](#bind-function)
- [User Placeholder](#user-placeholder)
- [Utility Placeholder](#utility-placeholder)
- [Placeholder Options](#placeholder-options)
- [Argument](#argument)
- [Type Narrowing](#type-narrowing)
- [Helper](#helper)
- [Special Values](#special-values)
### Bind Function
`urlFrom` itself is a function that returns a Bind Function.
By calling the Bind Function, a URL is generated.
```js
const bindUrl = urlFrom`users/${"userId:number"}`;
const url1 = bindUrl({ userId: 279 });
const url2 = bindUrl({ userId: 642, "#fragment": "fragment" });
console.log(url1); // => "users/279"
console.log(url2); // => "users/642#fragment"
```
This means that you can create a URL definition file and use the definition to generate URLs in various places.
The Bind Function also allows restricting arguments to literal types through [Type Narrowing], enabling powerful URL management.
### User Placeholder
- [Primitive](#Primitive)
- [Spread](#Spread)
#### Primitive
Format: <code>${"xxx"}</code> (support placeholder options [Value Type] & [Optional] & [Conditional Slash])<br />
Value Type: `string | number`
```js
const url = urlFrom`https://example.com/${"foo"}/to`({ foo: "path" });
console.log(url); // => "https://example.com/path/to"
```
#### Spread
Format: <code>${"...xxx"}</code> (support placeholder options [Value Type] & [Optional] & [Conditional Slash])<br />
Value Type: `Array<string | number>`
```js
const url = urlFrom`https://example.com/${"...paths"}`({ paths: ["path", "to"] });
console.log(url); // => "https://example.com/path/to"
```
If you want to change the separator:
```js
const url = urlFrom`https://example.com/${"...paths"}`({ paths: { value: ["path", "to"], separator: "-" } });
console.log(url); // => "https://example.com/path-to"
```
### Utility Placeholder
- [Direct](#direct)
- [SchemeHost](#schemehost)
- [SchemeHostPath](#schemehostpath)
- [Scheme](#scheme)
- [Userinfo](#userinfo)
- [Subdomain](#subdomain)
- [Port](#port)
#### Direct
Format: <code>${["value"]}</code><br />
Value Type: `string | number`
This placeholder allows you to directly embed a string that is encoded as a literal string.
```js
const url = urlFrom`https://example.com/path/to/${["white space"]}`();
console.log(url); // => "https://example.com/path/to/white%20space"
```
Since this placeholder treats the value as required, you should not pass an empty string. If you pass an empty string, an exception will be thrown.
```js
try {
const bindUrl = urlFrom`https://example.com/path/to/${[""]}`;
} catch (error) {
console.log(error.message); // => "The value of the index 0 at direct placeholder is empty string."
}
```
#### SchemeHost
Format: <code>${"scheme://host"}</code> or <code>${"scheme://authority"}</code> (support placeholder options [Optional])<br />
Value Type: `string`
- Embeds a string representing the portion from scheme to host (including port).
- It is useful for switching base URLs depending on the environment.
```js
const url = urlFrom`${"scheme://host"}/path/to`({ "scheme://host": "https://example.com" });
console.log(url); // => "https://example.com/path/to"
```
If you include a path in the value, a warning will be issued. In such cases, use the [SchemeHostPath Placeholder].
```js
// Warn: The value of the placeholder "scheme://host" cannot contain a path.
// Use the placeholder "scheme://host/path" to include paths. Received: https://example.com/path
const url = urlFrom`${"scheme://host"}/to`({ "scheme://host": "https://example.com/path" });
console.log(url); // => "https://example.com/path/to"
```
**β Note:**
- Always pass static values prepared in settings or definitions.
- Avoid passing dynamically concatenated strings as values, as it can lead to vulnerabilities.
- If the host part contains Unicode, `%HH` format, or IPv6, an exception will be thrown (future support may be added).
- If you want to include characters other than the delimiter `:` or `@` in the userinfo part, convert them to `%3A` and `%40`, respectively.
- The value of this placeholder should not include a QueryString or Fragment, so any characters after `?` or `#` will be ignored.
#### SchemeHostPath
Format: <code>${"scheme://host/path"}</code> or <code>${"scheme://authority/path"}</code><br />
Value Type: `string`
- Embeds a string representing the portion from scheme to host or path.
- It is useful for switching base URLs depending on the environment.
```js
const url = urlFrom`${"scheme://host/path"}/to`({ "scheme://host/path": "https://example.com/path" });
console.log(url); // => "https://example.com/path/to"
```
**β Note:**
- Always pass static values prepared in settings or definitions.
- Avoid passing dynamically concatenated strings as values, as it can lead to vulnerabilities.
- If the host part contains Unicode, `%HH` format, or IPv6, an exception will be thrown (future support may be added).
- If you want to include characters other than the delimiter `:` or `@` in the userinfo part, convert them to `%3A` and `%40`, respectively.
- The value of this placeholder should not include a QueryString or Fragment, so any characters after `?` or `#` will be ignored.
- This placeholder is available only as required and cannot be optional.
#### Scheme
Format: <code>${"scheme:"}</code> (support placeholder options [Optional])<br />
Value Type: `string`
```js
const url = urlFrom`${"scheme:"}//example.com/path/to`({ "scheme:": "https" });
console.log(url); // => "https://example.com/path/to"
```
#### Userinfo
**Using usernames and passwords in URLs is generally a security risk, so please avoid it as much as possible.**
Format: <code>${"userinfo@"}</code> (support placeholder options [Optional])<br />
Value Type: `{ user?: string; password?: string }`
```js
const url = urlFrom`https://${"userinfo@"}example.com/path/to`({ "userinfo@": { user: "name", password: "pass" } });
console.log(url); // => "https://name:pass@example.com/path/to"
```
#### Subdomain
Format: <code>${"subdomain."}</code> (support placeholder options [Optional])<br />
Value Type: `string[]`
```js
const url = urlFrom`https://${"subdomain."}example.com/path/to`({ "subdomain.": ["foo", "bar"] });
console.log(url); // => "https://foo.bar.example.com/path/to"
```
If you want to change the separator:
```js
const url = urlFrom`https://${"subdomain."}example.com/path/to`({
"subdomain.": { value: ["foo", "bar"], separator: "-" },
});
console.log(url); // => "https://foo-bar.example.com/path/to"
```
#### Port
Format: <code>${":port"}</code> (support placeholder options [Optional])<br />
Value Type: `number`
```js
const url = urlFrom`https://localhost${":port"}/path/to`({ ":port": 3000 });
console.log(url); // => "https://localhost:3000/path/to"
```
### Placeholder Options
#### Value Type
This library supports TypeScript-like type specification.
You can specify the type by appending `:string` or `:number` after the placeholder name.
If the type is not specified, it accepts `string | number` as the default type.
```js
const url = urlFrom`https://example.com/users/${"userId:string"}`({ userId: "279642" }); // If you pass a number, it will result in a type error
console.log(url); // => "https://example.com/users/279642"
```
When using with [Spread], please use `:string[]` or `:number[]` for array types.
If the type is not specified, it accepts `Array<string | number>` as the default type.
```js
const url = urlFrom`https://example.com/${"...paths:string[]"}`({ paths: ["path", "to"] });
console.log(url); // => "https://example.com/path/to"
```
#### Optional
By appending `?` immediately after the placeholder name, it becomes optional.
```js
const bindUrl = urlFrom`https://example.com/users/${"userId?"}`; // ${"userId?:string"} is optional string
console.log(bindUrl()); // => "https://example.com/users/"
console.log(bindUrl({ userId: 279642 })); // => "https://example.com/users/279642"
```
#### Conditional Slash
By adding `/` at the beginning, end, or both ends of the placeholder string, the slash becomes effective only when a value is embedded.
```js
const bindUrl1 = urlFrom`https://example.com/users${"/userId?"}`;
console.log(bindUrl1()); // => "https://example.com/users"
console.log(bindUrl1({ userId: "279642" })); // => "https://example.com/users/279642"
const bindUrl2 = urlFrom`https://example.com/users${"/userId?/"}`;
console.log(bindUrl2()); // => "https://example.com/users"
console.log(bindUrl2({ userId: "279642" })); // => "https://example.com/users/279642/"
```
### Argument
- [Query]
- [Fragment]
#### Query
Key: `?query`
Type: `QueryParams`
```js
const url = urlFrom`https://example.com/path/to`({
"?query": {
foo: 1,
bar: ["a", "b"],
},
});
console.log(url); // => "https://example.com/path/to?foo=1&bar=a&bar=b"
```
Ways to specify in the format of [URLSearchParams] or as a string.
```js
// Directly specified in the literal part
const bindUrl = urlFrom`https://example.com/?foo=1&bar=2`;
// Adding values with the same keys to the QueryString in the literal part
const url2 = bindUrl({
// Array<[string, Value]>
"?query": [
["foo", 123],
["bar", 234],
],
});
console.log(url2); // => "https://example.com/?foo=1&bar=2&foo=123&bar=234"
// Specifying as a string (Warning: If it contains characters that need to be encoded according to RFC3986, it will be encoded)
const url3 = bindUrl({
// string
"?query": "foo=123&bar=234", // It will produce the same result even with "?foo=123&bar=234"
});
console.log(url3); // => "https://example.com/?foo=123&bar=234"
```
#### Fragment
Key: `#fragment`
Type: `string`
```js
const url = urlFrom`https://example.com/path/to`({ "#fragment": "fragment" });
console.log(url); // => "https://example.com/path/to#fragment"
```
Method to directly specify in the literal part.
```js
const url = urlFrom`https://example.com/path/to#fragment`();
console.log(url); // => "https://example.com/path/to#fragment"
```
### Type Narrowing
If you want Bind Function arguments to be of narrower types, you can use `narrowing` to make optional mandatory or restrict them to various literal types.
```ts
const bindUrl = urlFrom`${"scheme:"}//example.com/users/${"userId"}`.narrowing<{
"scheme:": "http" | "https";
}>;
const url1 = bindUrl({ "scheme:": "http", userId: 279642 }); // β
const url2 = bindUrl({ "scheme:": "https", userId: 279642 }); // β
const url3 = bindUrl({ "scheme:": "ftp", userId: 279642 }); // β TS2322: Type '"ftp"' is not assignable to type '"http" | "https"'.
```
**Rules**
- Only the parts that exist in the original argument type are targeted.
- The original argument type can be narrowed down.
- Optional types can be made mandatory.
- Mandatory types cannot be made optional.
#### Making specific keys or QueryParams mandatory
```ts
const bindUrl = urlFrom`https://example.com/users/${"userId?"}`.narrowing<{
userId: number;
"?query": { foo: string };
// Narrowing down while inheriting free QueryParams
// "?query": { foo: string } & URLFromQueryParams;
}>;
const url1 = bindUrl({ userId: 1, "?query": { foo: "bar" } }); // β
const url2 = bindUrl({ userId: 1, "?query": {} }); // β TS2741: Property 'foo' is missing in type '{}' but required in type '{ foo: string; }'.
```
### Allowing Only Specific Keywords
```ts
const bindUrl = urlFrom`https://example.com/theme/${"theme:string"}`.narrowing<{
theme: "lighter" | "dark";
}>;
const url1 = bindUrl({ theme: "dark" }); // β
const url2 = bindUrl({ theme: "foo" }); // β TS2322: Type '"foo"' is not assignable to type '"lighter" | "dark"'.
```
### Allowing Multiple Patterns
```ts
const bindUrl = urlFrom`https://example.com/theme/${"theme:string"}${"/color?:string"}`.narrowing<
| {
theme: "lighter" | "dark";
}
| {
theme: "original";
color: "red" | "blue";
}
>;
const url1 = bindUrl({ theme: "lighter" }); // β
const url2 = bindUrl({ theme: "dark" }); // β
const url3 = bindUrl({ theme: "original", color: "red" }); // β
// β TS2345: Argument of type '{ theme: "original"; }' is not assignable to parameter of type 'Readonly<{ "?query"?: QueryParams | undefined; "#fragment"?: string | undefined; color?: BindParam<string | null | undefined>; } & ({ theme: "lighter" | "dark"; } | { ...; })>'.
// Property 'color' is missing in type '{ theme: "original"; }' but required in type 'Readonly<{ "?query"?: QueryParams | undefined; "#fragment"?: string | undefined; color?: BindParam<string | null | undefined>; } & { theme: "original"; color: "red" | "blue"; }>'.
const url4 = bindUrl({ theme: "original" });
```
### Helper
#### encodeRFC3986(string)
Encodes a string using [RFC3986].
**string**
Type: `string`
```js
console.log(encodeRFC3986("!'()*")); // => "%21%27%28%29%2A"
```
#### stringifyQuery(query, fragment?)
Generates a string for the QueryString or Fragment portion.
This function always returns the result with a leading "?" regardless of what is passed. If you don't need the "?", you can use `stringifyQuery(query).slice(1)` to obtain the result without the "?".
**query**
Type: `URLFromQueryParams | QueryDelete`
**fragment**
Type: `string | undefined`
```js
console.log(stringifyQuery({ foo: 1 }, "fragment")); // => "?foo=1#fragment"
console.log(stringifyQuery({ foo: [1, 2] })); // => "?foo=1&foo=2"
console.log(
stringifyQuery([
["foo", 1],
["foo", 2],
])
); // => "?foo=1&foo=2"
console.log(stringifyQuery(undefined)); // => "?"
console.log(stringifyQuery(undefined, "fragment")); // => "?#fragment"
console.log(stringifyQuery({})); // => "?"
```
#### replaceQuery(url, query?, fragment?)
Performs replacement or deletion of the QueryString or Fragment portion.
**url**
Type: `string`
**query**
Type: `URLFromQueryParams | QueryDelete`
**fragment**
Type: `string | undefined`
Replacement example:
```js
console.log(replaceQuery("https://example.com?foo=1&bar=2#fragment", { bar: "baz" }, "hash")); // => "https://example.com?foo=1&bar=baz#hash"
// Additional examples
console.log(replaceQuery("?a=b", { foo: 1 })); // => "?a=b&foo=1"
console.log(replaceQuery("?a=b", { foo: [1, 2] })); // => "?a=b&foo=1&foo=2"
console.log(
replaceQuery("?a=b", [
["foo", 1],
["foo", 2],
])
); // => "?a=b&foo=1&foo=2"
```
Deletion example:
```js
console.log(replaceQuery("?foo=1&bar=baz#fragment", QueryDelete, "")); // => ""
console.log(replaceQuery("?foo=1&bar=baz#fragment", { foo: QueryDelete })); // => "?bar=baz#fragment"
```
Note that deletion cannot be done with `undefined`, `{}`, or `""`.
```js
console.log(replaceQuery("?foo=1&bar=baz#fragment", undefined, undefined)); // => "?foo=1&bar=baz#fragment"
console.log(replaceQuery("?foo=1&bar=baz#fragment", {}, undefined)); // => "?foo=1&bar=baz#fragment"
console.log(replaceQuery("?foo=1&bar=baz#fragment", "", undefined)); // => "?foo=1&bar=baz#fragment"
```
### Special Values
A list of effects when using special values in placeholders or queries.
| Type or Value | Effect of Placeholder | Effect in Query | Supplement |
| ------------- | --------------------- | --------------- | ---------------------------------------------------- |
| `""` | Skip | `"key="` | An exception is thrown when used in a required path. |
| `number` | `"0"` | `"key=0"` | |
| `NaN` | `"NaN"` | `"key=NaN"` | A warning is issued when passed as a value. |
| `true` | - | `"key=true"` | Cannot be used as a placeholder value. |
| `false` | - | `"key=false"` | Cannot be used as a placeholder value. |
| `null` | Skip | `"key"` | Represented only by the key in the Query. |
| `undefined` | Skip | Skip | |
| `QueryDelete` | - | Delete | Symbol for deleting the key in the Query. |
```js
const bindUrl = urlFrom`https://example.com/${"value?"}`;
// Placeholder
console.log(bindUrl({ value: "" })); // => "https://example.com/"
console.log(bindUrl({ value: 0 })); // => "https://example.com/0"
// warn: 'The value NaN was passed to the placeholder "value".'
console.log(bindUrl({ value: NaN })); // => "https://example.com/NaN"
console.log(bindUrl({ value: null })); // => "https://example.com/"
console.log(bindUrl({ value: undefined })); // => "https://example.com/"
// Query
console.log(bindUrl({ "?query": { value: "" } })); // => "https://example.com/?value="
console.log(bindUrl({ "?query": { value: 0 } })); // => "https://example.com/?value=0"
// warn: 'Invalid query value for key "value". Received: NaN'
console.log(bindUrl({ "?query": { value: NaN } })); // => "https://example.com/?value=NaN"
console.log(bindUrl({ "?query": { value: null } })); // => "https://example.com/?value"
console.log(bindUrl({ "?query": { value: undefined } })); // => "https://example.com/"
```
## Tips
### Main patterns that cause exceptions
If it is determined that an available URL cannot be generated or is at risk, url-from will throw an exception.
- Common for all placeholders:
- Empty string passed to a required placeholder value.
- Value was passed that does not match the type.
- Individual placeholders:
- Invalid characters included in the value of [Scheme Placeholder].
- [SchemeHost Placeholder] or [SchemeHostPath Placeholder] does not include `://` in its value.
- Empty string passed to the value of [Direct Placeholder].
- Value outside the range of 0-65535 or `NaN` is passed to the [Port Placeholder].
- Others:
- When passed to `new URL()`, it tried to generate a URL that would throw an exception.
### Main patterns for issuing warnings
If there is a possible mistake or a more appropriate way to write it, url-from will issue a warning to encourage improvement.
- Literal Part
- When the literal part contains encoded characters according to [RFC3986]
- Solution: Use [Direct Placeholder] to embed such values.
- When the usage of `?=&#` in the literal part is inappropriate.
- Common for All Placeholders
- When `NaN` is passed as the value for each placeholder (except for [Port Placeholder]).
- Individual Placeholders
- When the value of [SchemeHost Placeholder] or [SchemeHostPath Placeholder] contains encoded characters according to [RFC3986].
- Solution: For [SchemeHost Placeholder] or [SchemeHostPath Placeholder], if you need to include encoded characters, pass the percent-encoded (`%HH`) value.
- When the value of [SchemeHost Placeholder] or [SchemeHostPath Placeholder] contains `?` or `#`.
- Others
- When the content of the URL is completed when passed to `new URL()`.
- When the completion of `/` occurs.
Note: Square brackets [ ] are used to indicate placeholder names in the original text.
### A warning is issued when `%` is included in the literal part
Percent-encoding (`%HH`) is appropriate according to [RFC3986], but a warning is issued when it is used in the literal part for the following reasons:
- `%HH` is not easily understandable to humans in its decoded form and can lead to incorrect representations.
- Detecting single `%` or malformed `%HH` requires complex processing and rules.
When using `%` in the literal part, please use [Direct Placeholder].
```js
// warn: The literal part contains an unencoded path string "%". Received: `https://example.com/emoji/%F0%9F%90%B9`
const url1 = urlFrom`https://example.com/emoji/%F0%9F%90%B9%`(); // β Bad
const url2 = urlFrom`https://example.com/emoji/${["πΉ%"]}`(); // β
Good
console.log(url1); // => "https://example.com/emoji/%25F0%259F%2590%25B9%25"
console.log(url2); // => "https://example.com/emoji/%F0%9F%90%B9%25"
```
### Path Traversal Protection
In url-from, as a path traversal protection measure, if the conditions of `/./` or `/../` are satisfied through dynamic embedding, a warning is issued and the "." is replaced with a half-width space.
```js
// Assume a path two hierarchy below the tags
// https://example.com/tags/<tag>/foo
const bindUrl = urlFrom`https://example.com/tags/${"tag:string"}/foo`;
// When "." is passed as a value
// Normally, it would result in a path to a different hierarchy than intended... https://example.com/tags/./foo -> https://example.com/tags/foo
// warn: When embedding values in URLs, some dots are replaced with single-byte spaces because we tried to generate paths that include strings indicating the current or parent directory, such as "." or "..".
const url1 = bindUrl({ tag: "." });
// The hierarchy is maintained by replacing spaces with single-byte spaces.
console.log(url1); // => "https://example.com/tags/%20/foo"
// When ".." is passed as a value
// Normally, it would result in a path to a different hierarchy than intended... https://example.com/tags/../foo -> https://example.com/foo
// warn: When embedding values in URLs, some dots are replaced with single-byte spaces because we tried to generate paths that include strings indicating the current or parent directory, such as "." or "..".
const url2 = bindUrl({ tag: ".." });
// The hierarchy is maintained by replacing spaces with single-byte spaces.
console.log(url2); // => "https://example.com/tags/%20%20/foo"
```
The reason for replacing with a half-width space is based on evaluating which risk is lower: changing the directory structure or replacing the "." with a half-width space.
- It is unlikely that a path consisting only of a half-width space has any meaning.
- When used as a tag name, a half-width space is a character subject to trimming, so it is unlikely to have any meaning.
- When it interacts with databases or other storage systems, it is unlikely that a blank-only string would pass through validation.
In this way, while both changing the directory structure due to path traversal and replacing "." with a half-width space are unintended behaviors for implementers, if there is no solution to unintended behavior, it is preferable to choose the option with less risk.
**Additional Information**
- `/.../` is a valid path (e.g., [https://en.wikipedia.org/wiki/...](https://en.wikipedia.org/wiki/...) is an alias for [https://en.wikipedia.org/wiki/Ellipsis](https://en.wikipedia.org/wiki/Ellipsis)).
- Converting `/../` to `/%2E%2E/` is meaningless because it is interpreted the same as `/../`.
The replacement of "." with a half-width space only occurs for dynamically embedded values. In cases like the following example, where the `/../` occurs due to the static literal ".", the literal "." remains unchanged.
```js
const bindUrl = urlFrom`https://example.com/dot-files/.${"type:string"}/README.md`;
console.log(bindUrl({ type: "gitignore" })); // => "https://example.com/dot-files/.gitignore/README.md"
// warn: When embedding values in URLs, some dots are replaced with single-byte spaces because we tried to generate paths that include strings indicating the current or parent directory, such as "." or "..".
console.log(bindUrl({ type: "." })); // => "https://example.com/dot-files/.%20/README.md"
```
### Security Mechanisms
- With the url-from mechanism, there is no way to overlook encoding in any part, such as the path or query.
- Strict type checks prevent the inclusion of invalid values.
- Warnings are issued for implementations that need improvement, allowing for refinement into more appropriate and secure implementations.
- There is no risk of path traversal attacks.
- It enables concise and readable code.
## NOTE
The sample code in this document has been tested using [power-doctest](https://github.com/azu/power-doctest).
## LICENSE
[@misuken-now/url-from](https://github.com/misuken-now/url-from)γ»MIT
[URL]: https://developer.mozilla.org/en-US/docs/Web/API/URL
[URLSearchParams]: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
[Protocol-relative URL]: https://en.wikipedia.org/wiki/Wikipedia:Protocol-relative_URL
[RFC3986]: https://datatracker.ietf.org/doc/html/rfc3986
[Usage]: #usage
[API]: #api
[Bind Function]: #bind-function
[User Placeholder]: #user-placeholder
[Primitive]: #primitive
[Spread]: #spread
[Utility Placeholder]: #utility-placeholder
[Direct Placeholder]: #direct
[SchemeHost Placeholder]: #schemehost
[SchemeHostPath Placeholder]: #schemehostpath
[Scheme Placeholder]: #scheme
[Userinfo Placeholder]: #userinfo
[Subdomain Placeholder]: #subdomain
[Port Placeholder]: #port
[Placeholder Options]: #placeholder-options
[Value Type]: #value-type
[Optional]: #optional
[Conditional Slash]: #conditional-slash
[Argument]: #argument
[Query]: #query
[Fragment]: #fragment
[Type Narrowing]: #type-narrowing
[Helper]: #helper
[Special Values]: #special-values
[Tips]: #tips