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:
Elijah Duffy
2025-12-12 13:59:21 -08:00
parent ae3abad769
commit 3885ac09a1
4 changed files with 74 additions and 22 deletions

View File

@@ -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}

View File

@@ -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,

View File

@@ -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);
};