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>
|
<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;
|
// Use URL API to determine if href is relative or absolute
|
||||||
|
try {
|
||||||
const trim = (str: string, char: string, trimStart?: boolean, trimEnd?: boolean) => {
|
// this will only succeed if href is absolute
|
||||||
let start = 0,
|
const independentUrl = new URL(href);
|
||||||
end = str.length;
|
return independentUrl.toString();
|
||||||
|
} catch {
|
||||||
if (trimStart || trimStart === undefined) {
|
// now we can assume that href is relative or entirely invalid
|
||||||
while (start < end && str[start] === char) start++;
|
// 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>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { ClassValue, MouseEventHandler } from 'svelte/elements';
|
import type { ClassValue, MouseEventHandler } from 'svelte/elements';
|
||||||
|
import { trimEdges } from './util';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
href: string;
|
href: string;
|
||||||
|
basepath?: string | null;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
tab?: 'current' | 'new';
|
tab?: 'current' | 'new';
|
||||||
class?: ClassValue | null | undefined;
|
class?: ClassValue | null | undefined;
|
||||||
@@ -34,6 +47,7 @@
|
|||||||
|
|
||||||
let {
|
let {
|
||||||
href,
|
href,
|
||||||
|
basepath,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
tab = 'current',
|
tab = 'current',
|
||||||
class: classValue,
|
class: classValue,
|
||||||
@@ -41,10 +55,7 @@
|
|||||||
onclick
|
onclick
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
if (PUBLIC_BASEPATH && !href.startsWith('http://') && !href.startsWith('https://')) {
|
const computedHref = $derived(rewriteHref(href, basepath));
|
||||||
let prefix = trim(PUBLIC_BASEPATH, '/');
|
|
||||||
href = `/${prefix}/${trim(href, '/', true, false)}`;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
@@ -53,7 +64,7 @@
|
|||||||
disabled && 'pointer-events-none cursor-not-allowed opacity-50',
|
disabled && 'pointer-events-none cursor-not-allowed opacity-50',
|
||||||
classValue
|
classValue
|
||||||
]}
|
]}
|
||||||
{href}
|
href={computedHref}
|
||||||
target={tab === 'new' ? '_blank' : undefined}
|
target={tab === 'new' ? '_blank' : undefined}
|
||||||
rel={tab === 'new' ? 'noopener noreferrer' : undefined}
|
rel={tab === 'new' ? 'noopener noreferrer' : undefined}
|
||||||
{onclick}
|
{onclick}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export { default as InjectGoogleMaps } from './InjectGoogleMaps.svelte';
|
|||||||
export { default as InjectUmami } from './InjectUmami.svelte';
|
export { default as InjectUmami } from './InjectUmami.svelte';
|
||||||
export { default as InputGroup } from './InputGroup.svelte';
|
export { default as InputGroup } from './InputGroup.svelte';
|
||||||
export { default as Label } from './Label.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 PhoneInput } from './PhoneInput.svelte';
|
||||||
export { default as PinInput } from './PinInput.svelte';
|
export { default as PinInput } from './PinInput.svelte';
|
||||||
export { default as RadioGroup } from './RadioGroup.svelte';
|
export { default as RadioGroup } from './RadioGroup.svelte';
|
||||||
@@ -47,7 +47,8 @@ export {
|
|||||||
getValue,
|
getValue,
|
||||||
targetMust,
|
targetMust,
|
||||||
capitalizeFirstLetter,
|
capitalizeFirstLetter,
|
||||||
prefixZero
|
prefixZero,
|
||||||
|
trimEdges
|
||||||
} from './util';
|
} from './util';
|
||||||
export {
|
export {
|
||||||
type ToolbarToggleState,
|
type ToolbarToggleState,
|
||||||
|
|||||||
@@ -120,3 +120,26 @@ export const prefixZero = (str: string): string => {
|
|||||||
}
|
}
|
||||||
return str;
|
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}
|
{/snippet}
|
||||||
</div>
|
</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 -->
|
<!-- Regular Dialog Demo -->
|
||||||
<Dialog
|
<Dialog
|
||||||
bind:open={dialogOpen}
|
bind:open={dialogOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user