Skip to content

Commit

Permalink
feat: add user site rendering for pages.
Browse files Browse the repository at this point in the history
This allows user sites to render their pages in addition to
their profile page.
  • Loading branch information
zicklag committed Jan 19, 2025
1 parent 2a99fce commit 5b940d4
Show file tree
Hide file tree
Showing 11 changed files with 380 additions and 78 deletions.
143 changes: 110 additions & 33 deletions src/lib/renderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@ type RendererExports = {
drop_output(): void;
wasm_alloc(size: number, align: number): number;
wasm_dealloc(ptr: number, size: number, align: number): void;
wasm_render(
wasm_render_profile(
profile_data_json_ptr: number,
profile_data_json_len: number,
theme_data_ptr: number,
theme_data_len: number
): [number, number];
): void;
wasm_render_page(
profile_data_json_ptr: number,
profile_data_json_len: number,
theme_data_ptr: number,
theme_data_len: number
): void;
};

let wasm: RendererExports;
Expand Down Expand Up @@ -51,10 +57,11 @@ const getOutputPtrLen = () => {
return [wasmDataView.getInt32(0, true), wasmDataView.getInt32(4, true)];
};

export type InstanceInfo = {
url: string;
};

export type ProfileData = {
instance_info: {
url: string;
};
handle: string;
display_name?: string;
bio?: string;
Expand All @@ -64,21 +71,57 @@ export type ProfileData = {
pages?: { slug: string; name?: string }[];
};
export type PageData = {
instance_info: {
url: string;
};
handle: string;
display_name?: string;
markdown: string;
profile: ProfileData;
title: string;
slug: string;
markdown: string;
};

export async function renderProfile(
profile: Omit<ProfileData, 'instance_info'>,
themeData: Uint8Array
): Promise<string> {
const data: ProfileData = {
export type ProfileRenderInput = {
instance_info: InstanceInfo;
} & ProfileData;

export type PageRenderInput = {
instance_info: InstanceInfo;
} & PageData;

/**
* For now, to keep things simple, we have the theme data split into profile and page templates.
* They are both stored in the theme data, separated by a null byte. If there is no null byte, the
* it is assumed the page template has not been set, from a previous version of the theme data.
*/
export function parseThemeDataBytes(themeData: Uint8Array): {
profile: Uint8Array;
page?: Uint8Array;
} {
const splitIdx = themeData.findIndex((x) => x == 0);
if (splitIdx == -1) {
return { profile: themeData };
} else {
const profileData = themeData.slice(0, splitIdx);
const pageData = themeData.slice(splitIdx + 1);
return { profile: profileData, page: pageData };
}
}

export function parseThemeData(themeData: Uint8Array): { profile: string; page?: string } {
const decoder = new TextDecoder();
const { profile, page } = parseThemeDataBytes(themeData);
return { profile: decoder.decode(profile), page: page && decoder.decode(page) };
}

export function createThemeData(profile: string, page: string): Uint8Array {
const encoder = new TextEncoder();
const profileData = encoder.encode(profile);
const pageData = encoder.encode(page);
const out = new Uint8Array(profileData.length + pageData.length + 1);
out.set(profileData, 0);
out.set(pageData, profileData.length + 1);
return out;
}

export async function renderProfile(profile: ProfileData, themeData: Uint8Array): Promise<string> {
const data: ProfileRenderInput = {
handle: profile.handle,
bio: profile.bio,
display_name: profile.display_name,
Expand Down Expand Up @@ -107,60 +150,94 @@ export async function renderProfile(
const pageDataJsonBinaryLen = pageDataJsonBinary.length;
const pageDataJsonPtr = wasm.wasm_alloc(pageDataJsonBinaryLen, 1);

const themeDataLen = themeData.length;
const themeDataPtr = wasm.wasm_alloc(themeDataLen, 1);
const { profile: profileThemeData } = parseThemeDataBytes(themeData);

const profileThemeDataLen = profileThemeData.length;
const profileThemeDataPtr = wasm.wasm_alloc(profileThemeDataLen, 1);

const view = new Uint8Array(wasm.memory.buffer);
view.set(pageDataJsonBinary, pageDataJsonPtr);
view.set(themeData, themeDataPtr);
view.set(profileThemeData, profileThemeDataPtr);

wasm.wasm_render(pageDataJsonPtr, pageDataJsonBinaryLen, themeDataPtr, themeDataLen);
wasm.wasm_render_profile(
pageDataJsonPtr,
pageDataJsonBinaryLen,
profileThemeDataPtr,
profileThemeDataLen
);
const [renderedPtr, renderedLen] = getOutputPtrLen();

const renderedData = wasm.memory.buffer.slice(renderedPtr, renderedPtr + renderedLen);
const renderedString = decoder.decode(renderedData);

wasm.drop_output();

wasm.wasm_dealloc(themeDataPtr, themeDataLen, 1);
wasm.wasm_dealloc(profileThemeDataPtr, profileThemeDataLen, 1);
wasm.wasm_dealloc(pageDataJsonPtr, pageDataJsonBinaryLen, 1);

return renderedString;
}

export async function renderPage(
page: Omit<PageData, 'instance_info'>,
profile: ProfileData,
page: Omit<PageData, 'profile'>,
themeData: Uint8Array
): Promise<string> {
const data: PageData = {
handle: page.handle,
display_name: page.display_name,
markdown: page.markdown,
slug: page.slug,
const profileData: ProfileData = {
handle: profile.handle,
bio: profile.bio,
display_name: profile.display_name,
links: profile.links,
pages: profile.pages,
social_links: await Promise.all(
profile.social_links?.map(async (x) => {
const details = getSocialMediaDetails(x.url);
const i = buildIcon(await loadIcon(details.icon));
return {
url: x.url,
label: x.label,
platform_name: details.name.toLocaleLowerCase(),
icon: `<svg ${Object.entries(i.attributes)
.map(([k, v]) => `${k}="${v}"`)
.join(' ')} >${i.body}</svg>`,
icon_name: details.icon
};
}) || []
),
tags: profile.tags
};
const pageData: PageRenderInput = {
profile: profileData,
title: page.title,
slug: page.slug,
markdown: page.markdown,
instance_info: { url: env.PUBLIC_URL }
};
const pageDataJson = JSON.stringify(data);
const pageDataJson = JSON.stringify(pageData);
const pageDataJsonBinary = encoder.encode(pageDataJson);
const pageDataJsonBinaryLen = pageDataJsonBinary.length;
const pageDataJsonPtr = wasm.wasm_alloc(pageDataJsonBinaryLen, 1);

const themeDataLen = themeData.length;
const themeDataPtr = wasm.wasm_alloc(themeDataLen, 1);
const { page: pageThemeData } = parseThemeDataBytes(themeData);

if (!pageThemeData) throw 'Page theme missing';

const pageThemeDataLen = pageThemeData.length;
const pageThemeDataPtr = wasm.wasm_alloc(pageThemeDataLen, 1);

const view = new Uint8Array(wasm.memory.buffer);
view.set(pageDataJsonBinary, pageDataJsonPtr);
view.set(themeData, themeDataPtr);
view.set(pageThemeData, pageThemeDataPtr);

wasm.wasm_render(pageDataJsonPtr, pageDataJsonBinaryLen, themeDataPtr, themeDataLen);
wasm.wasm_render_page(pageDataJsonPtr, pageDataJsonBinaryLen, pageThemeDataPtr, pageThemeDataLen);
const [renderedPtr, renderedLen] = getOutputPtrLen();

const renderedData = wasm.memory.buffer.slice(renderedPtr, renderedPtr + renderedLen);
const renderedString = decoder.decode(renderedData);

wasm.drop_output();

wasm.wasm_dealloc(themeDataPtr, themeDataLen, 1);
wasm.wasm_dealloc(pageThemeDataPtr, pageThemeDataLen, 1);
wasm.wasm_dealloc(pageDataJsonPtr, pageDataJsonBinaryLen, 1);

return renderedString;
Expand Down
39 changes: 35 additions & 4 deletions src/lib/themes/weird/page.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,31 @@
max-width: var(--size-content-3);
}
header {
background-color: rgba(255, 255, 255, 0.1);
padding: var(--size-fluid-1);
border: var(--border-size-2) solid black;
border-radius: var(--radius-3);
width: 100%;
margin: 0 auto;
max-width: var(--size-content-3);
display: flex;
gap: 2em;
align-items: center;
}
header h1 {
padding: 0;
margin: 0;
}
header .avatar-figure img {
width: 75px;
margin-left: auto;
margin-right: auto;
border-radius: 100%;
}
.avatar-figure img {
width: 200px;
margin-left: auto;
Expand Down Expand Up @@ -120,11 +145,17 @@
<div class="stars"></div>

<main>
<a href="/" title="Home" style="display: block; text-decoration: none;">
<header>
<figure class="avatar-figure">
<img src="/avatar" alt="{{profile.display_name}} avatar" />
</figure>
<h1>
{{profile.display_name}}
</h1>
</header>
</a>
<section>
<figure class="avatar-figure">
<img src="/avatar" alt="{{display_name}} avatar" />
</figure>

<h1>{{title}}</h1>

{% if markdown %}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/themes/weird/profile.html.j2
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@
<section class="links">
<h1>Pages</h1>
{% for page in pages %}
<a href="{{instance_info.url}}/{{handle}}/{{page.slug}}" target="_blank" class="link">
<a href="/{{page.slug}}" class="link">
{{page.name or page.slug}}
</a>
{% endfor %}
Expand All @@ -177,4 +177,4 @@
</main>
</body>

</html>
</html>
12 changes: 10 additions & 2 deletions src/routes/(app)/[username]/settings/setTheme/+server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { profileLinkById, setTheme } from '$lib/leaf/profile';
import { getSession } from '$lib/rauthy/server';
import { createThemeData } from '$lib/renderer';
import { type RequestHandler, json } from '@sveltejs/kit';
import { z } from 'zod';

const Req = z.object({
template: z.optional(z.string())
templates: z.optional(
z.object({
profile: z.string(),
page: z.string()
})
)
});
type Req = z.infer<typeof Req>;

Expand All @@ -22,7 +28,9 @@ export const POST: RequestHandler = async ({ request, fetch }) => {
const profileLink = await profileLinkById(sessionInfo.user_id);
await setTheme(
profileLink,
parsed.data.template ? { data: new TextEncoder().encode(parsed.data.template) } : undefined
parsed.data.templates
? { data: createThemeData(parsed.data.templates.profile, parsed.data.templates.page) }
: undefined
);

return new Response();
Expand Down
7 changes: 5 additions & 2 deletions src/routes/(app)/[username]/theme-editor/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ import { getSession } from '$lib/rauthy/server';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { getTheme, profileLinkById } from '$lib/leaf/profile';
import { parseThemeData } from '$lib/renderer';

export const load: PageServerLoad = async ({
fetch,
request
}): Promise<{ theme?: { data: Uint8Array } }> => {
}): Promise<{ theme?: { page?: string; profile: string } }> => {
let { sessionInfo } = await getSession(fetch, request);
if (!sessionInfo) return error(403, 'Not logged in');
const profileLink = await profileLinkById(sessionInfo.user_id);
if (!profileLink) return error(404);
const { data } = (await getTheme(profileLink)) || {};
const theme = data ? parseThemeData(data) : undefined;

return { theme: await getTheme(profileLink) };
return { theme };
};
Loading

0 comments on commit 5b940d4

Please sign in to comment.