link: refactor href rewrite
- uses prop instead of attempting to read env variable - uses URL type instead of custom heuristics
This commit is contained in:
@@ -1,30 +1,43 @@
|
||||
<script lang="ts" module>
|
||||
import { env } from '$env/dynamic/public';
|
||||
/**
|
||||
* Rewrites the href based on a given basepath.
|
||||
* If the href is absolute, it is returned as is.
|
||||
* If the href is relative, the basepath is prepended.
|
||||
* @param href The original href.
|
||||
* @returns The rewritten href.
|
||||
*/
|
||||
export const rewriteHref = (href: string, basepath?: string | null): string => {
|
||||
// If no base path is set, return the href as is
|
||||
if (!basepath) return href;
|
||||
|
||||
const { PUBLIC_BASEPATH } = env;
|
||||
|
||||
const trim = (str: string, char: string, trimStart?: boolean, trimEnd?: boolean) => {
|
||||
let start = 0,
|
||||
end = str.length;
|
||||
|
||||
if (trimStart || trimStart === undefined) {
|
||||
while (start < end && str[start] === char) start++;
|
||||
// Use URL API to determine if href is relative or absolute
|
||||
try {
|
||||
// this will only succeed if href is absolute
|
||||
const independentUrl = new URL(href);
|
||||
return independentUrl.toString();
|
||||
} catch {
|
||||
// now we can assume that href is relative or entirely invalid
|
||||
// test with a generic baseURI to see if it's valid relative
|
||||
try {
|
||||
const relativeUrl = new URL(href, 'http://example.com');
|
||||
// if we reach here, it's a valid relative URL
|
||||
const prefix = trimEdges(basepath, '/');
|
||||
return `/${prefix}/${trimEdges(relativeUrl.pathname, '/', true, false)}`;
|
||||
} catch {
|
||||
throw new Error(`Attempted to rewrite invalid href: ${href}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (trimEnd || trimEnd === undefined) {
|
||||
while (end > start && str[end - 1] === char) end--;
|
||||
}
|
||||
|
||||
return str.substring(start, end);
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { ClassValue, MouseEventHandler } from 'svelte/elements';
|
||||
import { trimEdges } from './util';
|
||||
|
||||
interface Props {
|
||||
href: string;
|
||||
basepath?: string | null;
|
||||
disabled?: boolean;
|
||||
tab?: 'current' | 'new';
|
||||
class?: ClassValue | null | undefined;
|
||||
@@ -34,6 +47,7 @@
|
||||
|
||||
let {
|
||||
href,
|
||||
basepath,
|
||||
disabled = false,
|
||||
tab = 'current',
|
||||
class: classValue,
|
||||
@@ -41,10 +55,7 @@
|
||||
onclick
|
||||
}: Props = $props();
|
||||
|
||||
if (PUBLIC_BASEPATH && !href.startsWith('http://') && !href.startsWith('https://')) {
|
||||
let prefix = trim(PUBLIC_BASEPATH, '/');
|
||||
href = `/${prefix}/${trim(href, '/', true, false)}`;
|
||||
}
|
||||
const computedHref = $derived(rewriteHref(href, basepath));
|
||||
</script>
|
||||
|
||||
<a
|
||||
@@ -53,7 +64,7 @@
|
||||
disabled && 'pointer-events-none cursor-not-allowed opacity-50',
|
||||
classValue
|
||||
]}
|
||||
{href}
|
||||
href={computedHref}
|
||||
target={tab === 'new' ? '_blank' : undefined}
|
||||
rel={tab === 'new' ? 'noopener noreferrer' : undefined}
|
||||
{onclick}
|
||||
|
||||
@@ -18,7 +18,7 @@ export { default as InjectGoogleMaps } from './InjectGoogleMaps.svelte';
|
||||
export { default as InjectUmami } from './InjectUmami.svelte';
|
||||
export { default as InputGroup } from './InputGroup.svelte';
|
||||
export { default as Label } from './Label.svelte';
|
||||
export { default as Link } from './Link.svelte';
|
||||
export { default as Link, rewriteHref } from './Link.svelte';
|
||||
export { default as PhoneInput } from './PhoneInput.svelte';
|
||||
export { default as PinInput } from './PinInput.svelte';
|
||||
export { default as RadioGroup } from './RadioGroup.svelte';
|
||||
@@ -47,7 +47,8 @@ export {
|
||||
getValue,
|
||||
targetMust,
|
||||
capitalizeFirstLetter,
|
||||
prefixZero
|
||||
prefixZero,
|
||||
trimEdges
|
||||
} from './util';
|
||||
export {
|
||||
type ToolbarToggleState,
|
||||
|
||||
@@ -120,3 +120,26 @@ export const prefixZero = (str: string): string => {
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Trims the specified character from the start and/or end of the string.
|
||||
* @param str The string to trim.
|
||||
* @param char The character to trim.
|
||||
* @param trimStart Whether to trim from the start of the string. Default: true.
|
||||
* @param trimEnd Whether to trim from the end of the string. Default: true.
|
||||
* @returns The trimmed string.
|
||||
*/
|
||||
export const trimEdges = (str: string, char: string, trimStart?: boolean, trimEnd?: boolean) => {
|
||||
let start = 0,
|
||||
end = str.length;
|
||||
|
||||
if (trimStart || trimStart === undefined) {
|
||||
while (start < end && str[start] === char) start++;
|
||||
}
|
||||
|
||||
if (trimEnd || trimEnd === undefined) {
|
||||
while (end > start && str[end - 1] === char) end--;
|
||||
}
|
||||
|
||||
return str.substring(start, end);
|
||||
};
|
||||
|
||||
@@ -476,6 +476,23 @@
|
||||
{/snippet}
|
||||
</div>
|
||||
|
||||
<!-- Link with href rewriting -->
|
||||
<div class="component">
|
||||
<p class="title">Link (with href rewriting)</p>
|
||||
|
||||
<p class="mb-3">
|
||||
href rewriting allows you to prepend a basepath to relative links, making it easier to manage
|
||||
URLs in your application. It is recommended to wrap this element with your own, e.g. AppLink,
|
||||
that automatically provides the basepath from your app's configuration.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<Link href="/about" basepath="/sui-demo">Go to About Page (with basepath)</Link>
|
||||
<Link href="https://svelte.dev" basepath="/sui-demo">External Svelte Site</Link>
|
||||
<Link href="contact">Contact Us (relative link, no basepath)</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Regular Dialog Demo -->
|
||||
<Dialog
|
||||
bind:open={dialogOpen}
|
||||
|
||||
Reference in New Issue
Block a user