diff --git a/web/.storybook/markupPreview.ts b/web/.storybook/markupPreview.ts
deleted file mode 100644
index 98da1caeb..000000000
--- a/web/.storybook/markupPreview.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-const ENDPOINT = "/_internal/markup";
-
-const escape = (text: string): string =>
- text.replace(/&/g, "&").replace(//g, ">");
-
-const render = (source: string, sourceLines: boolean): string => {
- const blocks: string[] = [];
- let start = 0;
- let lines: string[] = [];
- const flush = () => {
- if (lines.length === 0) return;
- const attr = sourceLines ? ` data-line="${start}"` : "";
- const first = lines[0];
- const heading = /^(#{1,6}) (.*)$/.exec(first);
- blocks.push(
- heading
- ? `${escape(heading[2])}`
- : `
${escape(lines.join(" "))}
`
- );
- lines = [];
- };
- source.split("\n").forEach((line, index) => {
- if (line.trim() === "") return flush();
- if (lines.length === 0) start = index;
- lines.push(line);
- if (/^#{1,6} /.test(line)) flush();
- });
- flush();
- return `${blocks.join("\n")}
`;
-};
-
-export const stubMarkupEndpoint = (): void => {
- const original = window.fetch;
- window.fetch = async (input, init) => {
- const href = input instanceof Request ? input.url : String(input);
- if (new URL(href, location.href).pathname !== ENDPOINT) return original(input, init);
- const { contents, sourceLines } = JSON.parse(String(init?.body));
- return Response.json({ html: render(contents, sourceLines === true) });
- };
-};
diff --git a/web/.storybook/preview.ts b/web/.storybook/preview.ts
index b50c3855c..40ab21e7d 100644
--- a/web/.storybook/preview.ts
+++ b/web/.storybook/preview.ts
@@ -3,14 +3,11 @@ import { withThemeByDataAttribute } from "@storybook/addon-themes";
import MockAuth, { type MockAuthParam } from "./MockAuth.svelte";
import MockNotifications, { type MockNotificationsParam } from "./MockNotifications.svelte";
import BoneCaptureDecorator from "./BoneCaptureDecorator.svelte";
-import { stubMarkupEndpoint } from "./markupPreview";
// Load the app's global styles (Tailwind + theme tokens) so components
// render with real styling inside Storybook.
import "../src/app.css";
-stubMarkupEndpoint();
-
const preview: Preview = {
decorators: [
withThemeByDataAttribute({
diff --git a/web/src/lib/components/comment/CommentEditor.svelte b/web/src/lib/components/comment/CommentEditor.svelte
index a85650b23..daf91fc95 100644
--- a/web/src/lib/components/comment/CommentEditor.svelte
+++ b/web/src/lib/components/comment/CommentEditor.svelte
@@ -18,7 +18,7 @@
import ErrorAlert from "$lib/components/ui/Error.svelte";
import MarkdownEditor from "$lib/components/ui/MarkdownEditor.svelte";
import Spinner from "$lib/components/ui/Spinner.svelte";
- import { previewMarkup, type MarkupContext } from "$lib/markup";
+ import { renderPreview, type MarkupContext } from "$lib/markup";
import { createAction } from "$lib/action.svelte";
import type { ThreadInput } from "$lib/components/comment/comments";
@@ -123,7 +123,7 @@
console.error("Failed to auto-subscribe via comment:", err);
});
}
- const bodyHtml = await previewMarkup(body, markup).catch(() => null);
+ const bodyHtml = await renderPreview(body, markup).catch(() => null);
onsubmitted?.({
comment: {
uri: saved.uri,
diff --git a/web/src/lib/components/repo/issues/IssueThreadView.svelte b/web/src/lib/components/repo/issues/IssueThreadView.svelte
index e43171611..3362cae38 100644
--- a/web/src/lib/components/repo/issues/IssueThreadView.svelte
+++ b/web/src/lib/components/repo/issues/IssueThreadView.svelte
@@ -49,7 +49,7 @@
import Spinner from "$lib/components/ui/Spinner.svelte";
import Card from "$lib/components/ui/Card.svelte";
import { setMentionPeople, type MentionPerson } from "$lib/components/ui/mentionPeople";
- import { previewMarkup } from "$lib/markup";
+ import { renderPreview } from "$lib/markup";
import type { MarkupContext } from "$lib/markup/paths";
interface Props {
@@ -117,7 +117,7 @@
if (props.bobbinUrl) {
await awaitIndexedOrSaved(createBobbinClient({ serviceUrl: props.bobbinUrl }), written);
}
- const bodyHtml = body ? await previewMarkup(body, props.markup).catch(() => null) : null;
+ const bodyHtml = body ? await renderPreview(body, props.markup).catch(() => null) : null;
issue = { ...issue, cid: written.cid, title, body, bodyHtml };
};
diff --git a/web/src/lib/components/repo/pulls/PullReviewComment.svelte b/web/src/lib/components/repo/pulls/PullReviewComment.svelte
index e5cee5f8b..b30cc8845 100644
--- a/web/src/lib/components/repo/pulls/PullReviewComment.svelte
+++ b/web/src/lib/components/repo/pulls/PullReviewComment.svelte
@@ -10,7 +10,7 @@
import User from "$lib/components/ui/User.svelte";
import type { MarkupContext } from "$lib/markup";
import { codeblockToolbars } from "$lib/components/ui/codeblockToolbars";
- import { previewMarkup } from "$lib/markup";
+ import { renderPreview } from "$lib/markup";
import PullReviewCommentEditor from "$lib/components/repo/pulls/PullReviewCommentEditor.svelte";
import ProfilePopover from "$lib/components/profile/ProfilePopover.svelte";
import { getAuth } from "$lib/auth.svelte";
@@ -40,7 +40,7 @@
const auth = getAuth();
async function submit(body: string) {
- const bodyHtml = await previewMarkup(body, markup);
+ const bodyHtml = await renderPreview(body, markup);
await onedit({ ...comment, body, bodyHtml });
editing = false;
}
diff --git a/web/src/lib/components/ui/MarkdownEditor.svelte b/web/src/lib/components/ui/MarkdownEditor.svelte
index 80f1a9a7e..a93c3235e 100644
--- a/web/src/lib/components/ui/MarkdownEditor.svelte
+++ b/web/src/lib/components/ui/MarkdownEditor.svelte
@@ -118,7 +118,7 @@
scrollEditorToLine,
scrollPreviewToLine
} from "$lib/components/ui/markdownScroll";
- import { previewMarkup, type MarkupContext } from "$lib/markup";
+ import { loadMarkup, renderPreview, type MarkupContext } from "$lib/markup";
import { codeblockToolbars } from "$lib/components/ui/codeblockToolbars";
interface Tool {
@@ -288,7 +288,7 @@
const rendered = $derived(
preview && hasContent
- ? previewMarkup(value, { ...markup, sourceLines: true }).then((html) => html ?? "")
+ ? renderPreview(value, { ...markup, sourceLines: true }).then((html) => html ?? "")
: null
);
@@ -514,6 +514,7 @@
oninput={() => typeahead?.refresh()}
onclick={() => typeahead?.refresh()}
onscroll={() => typeahead?.refresh()}
+ onfocus={() => void loadMarkup().catch(() => {})}
onblur={() => typeahead?.close()}
class={classes.field()}
/>
diff --git a/web/src/lib/server/markup/collapse.test.ts b/web/src/lib/markup/collapse.test.ts
similarity index 92%
rename from web/src/lib/server/markup/collapse.test.ts
rename to web/src/lib/markup/collapse.test.ts
index 8c92baeb4..e3c3a0695 100644
--- a/web/src/lib/server/markup/collapse.test.ts
+++ b/web/src/lib/markup/collapse.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
-import { COLLAPSED_LINES, collapseCodeblocks } from "$lib/server/markup/collapse";
-import { renderDocument, renderMarkup } from "$lib/server/markup/render";
+import { COLLAPSED_LINES, collapseCodeblocks } from "$lib/markup/collapse";
+import { renderDocument, renderMarkup } from "$lib/markup/render";
import type { MarkupContext } from "$lib/markup/paths";
const ctx: MarkupContext = { repo: "ada.test/infra", ref: "main" };
diff --git a/web/src/lib/server/markup/collapse.ts b/web/src/lib/markup/collapse.ts
similarity index 100%
rename from web/src/lib/server/markup/collapse.ts
rename to web/src/lib/markup/collapse.ts
diff --git a/web/src/lib/server/markup/highlight.ts b/web/src/lib/markup/highlight.ts
similarity index 100%
rename from web/src/lib/server/markup/highlight.ts
rename to web/src/lib/markup/highlight.ts
diff --git a/web/src/lib/server/markup/markdown.test.ts b/web/src/lib/markup/markdown.test.ts
similarity index 99%
rename from web/src/lib/server/markup/markdown.test.ts
rename to web/src/lib/markup/markdown.test.ts
index 35082f913..02a4f4204 100644
--- a/web/src/lib/server/markup/markdown.test.ts
+++ b/web/src/lib/markup/markdown.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { isMarkdownFile } from "$lib/markup/format";
-import { renderMarkdown } from "$lib/server/markup/markdown";
+import { renderMarkdown } from "$lib/markup/markdown";
import type { MarkupContext } from "$lib/markup/paths";
const ctx: MarkupContext = { repo: "ada.test/infra", ref: "main", host: "tangled.org" };
diff --git a/web/src/lib/server/markup/markdown.ts b/web/src/lib/markup/markdown.ts
similarity index 96%
rename from web/src/lib/server/markup/markdown.ts
rename to web/src/lib/markup/markdown.ts
index 5c417aa3c..0001419d0 100644
--- a/web/src/lib/server/markup/markdown.ts
+++ b/web/src/lib/markup/markdown.ts
@@ -4,9 +4,9 @@ import { tasklist } from "@mdit/plugin-tasklist";
import MarkdownIt from "markdown-it";
import anchor from "markdown-it-anchor";
import { full as emoji } from "markdown-it-emoji";
-import { collapseCodeblocks } from "$lib/server/markup/collapse";
-import { highlightCodeblocks } from "$lib/server/markup/highlight";
-import { sanitizeMarkup } from "$lib/server/markup/sanitize";
+import { collapseCodeblocks } from "$lib/markup/collapse";
+import { highlightCodeblocks } from "$lib/markup/highlight";
+import { sanitizeMarkup } from "$lib/markup/sanitize";
import type { MarkupContext } from "$lib/markup/paths";
// past this a document renders as plain text, a readme this big is pathological
diff --git a/web/src/lib/markup/preview.test.ts b/web/src/lib/markup/preview.test.ts
new file mode 100644
index 000000000..783a6f873
--- /dev/null
+++ b/web/src/lib/markup/preview.test.ts
@@ -0,0 +1,17 @@
+import { describe, expect, it } from "vitest";
+import { renderPreview } from "$lib/markup/preview";
+import { renderMarkup } from "$lib/markup/render";
+import type { MarkupContext } from "$lib/markup/paths";
+
+const ctx: MarkupContext = { repo: "ada.test/infra", ref: "main", host: "tangled.org" };
+
+describe("renderPreview", () => {
+ it("renders what the server renders", async () => {
+ const body = "# hello\n\n- a\n- b\n\n";
+ expect(await renderPreview(body, ctx)).toBe(await renderMarkup(body, ctx));
+ });
+
+ it("leaves a body over the source limit unrendered", async () => {
+ expect(await renderPreview("a".repeat(512 * 1024 + 1), ctx)).toBeNull();
+ });
+});
diff --git a/web/src/lib/markup/preview.ts b/web/src/lib/markup/preview.ts
index 43edcafa4..b703a6523 100644
--- a/web/src/lib/markup/preview.ts
+++ b/web/src/lib/markup/preview.ts
@@ -1,15 +1,11 @@
import type { MarkupContext } from "$lib/markup/paths";
-export const previewMarkup = async (
+export const loadMarkup = () => import("$lib/markup/render");
+
+export const renderPreview = async (
contents: string,
- { repo, ref, dir, sourceLines }: MarkupContext
+ ctx: MarkupContext
): Promise => {
- const response = await fetch("/_internal/markup", {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({ contents, repo, ref, dir, sourceLines })
- });
- if (!response.ok) throw new Error(`markup render failed: ${response.status}`);
- const { html } = (await response.json()) as { html: string | null };
- return html;
+ const { renderMarkup } = await loadMarkup();
+ return renderMarkup(contents, ctx);
};
diff --git a/web/src/lib/server/markup/render.test.ts b/web/src/lib/markup/render.test.ts
similarity index 88%
rename from web/src/lib/server/markup/render.test.ts
rename to web/src/lib/markup/render.test.ts
index a9052dd4b..db758b490 100644
--- a/web/src/lib/server/markup/render.test.ts
+++ b/web/src/lib/markup/render.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
-import { SOURCE_LIMIT } from "$lib/server/markup/markdown";
-import { renderDocument, renderMarkup } from "$lib/server/markup/render";
+import { SOURCE_LIMIT } from "$lib/markup/markdown";
+import { renderDocument, renderMarkup } from "$lib/markup/render";
import type { MarkupContext } from "$lib/markup/paths";
const ctx: MarkupContext = { repo: "ada.test/infra", ref: "main", host: "tangled.org" };
diff --git a/web/src/lib/server/markup/render.ts b/web/src/lib/markup/render.ts
similarity index 89%
rename from web/src/lib/server/markup/render.ts
rename to web/src/lib/markup/render.ts
index f84c24cf4..8e6e779f0 100644
--- a/web/src/lib/server/markup/render.ts
+++ b/web/src/lib/markup/render.ts
@@ -1,6 +1,6 @@
import { isMarkdownFile } from "$lib/markup/format";
import type { MarkupContext } from "$lib/markup/paths";
-import { SOURCE_LIMIT, renderMarkdown } from "$lib/server/markup/markdown";
+import { SOURCE_LIMIT, renderMarkdown } from "$lib/markup/markdown";
const render = async (contents: string, ctx: MarkupContext): Promise => {
if (contents.length > SOURCE_LIMIT) return null;
diff --git a/web/src/lib/server/markup/sanitize.ts b/web/src/lib/markup/sanitize.ts
similarity index 100%
rename from web/src/lib/server/markup/sanitize.ts
rename to web/src/lib/markup/sanitize.ts
diff --git a/web/src/lib/server/blob.ts b/web/src/lib/server/blob.ts
index 04bba1c75..26fbe2dbe 100644
--- a/web/src/lib/server/blob.ts
+++ b/web/src/lib/server/blob.ts
@@ -16,7 +16,7 @@ import { orNull, toHttpError } from "$lib/api/load";
import { toTreeCommitSummary } from "$lib/api/repo";
import type { StringRecord } from "$lib/api/records";
import { isMarkdownFile, type MarkupContext } from "$lib/markup";
-import { renderDocument } from "$lib/server/markup/render";
+import { renderDocument } from "$lib/markup/render";
import { renderReadme, type RepoLoadEvent, type RepoParent } from "$lib/server/repoIndex";
export const loadRepoBlob = async (
diff --git a/web/src/lib/server/comments.ts b/web/src/lib/server/comments.ts
index 7f343a907..20a318c3a 100644
--- a/web/src/lib/server/comments.ts
+++ b/web/src/lib/server/comments.ts
@@ -14,7 +14,7 @@ import {
type ThreadInput
} from "$lib/components/comment/comments";
import type { MarkupContext } from "$lib/markup/paths";
-import { renderMarkup } from "$lib/server/markup/render";
+import { renderMarkup } from "$lib/markup/render";
export const toThreadInputs = async (
items: CommentListPage["items"],
diff --git a/web/src/lib/server/repoIndex.ts b/web/src/lib/server/repoIndex.ts
index cd5754b2e..5be7dbd99 100644
--- a/web/src/lib/server/repoIndex.ts
+++ b/web/src/lib/server/repoIndex.ts
@@ -34,7 +34,7 @@ import {
type RepoAvailability
} from "$lib/api/repoIndex";
import type { RepoInfo } from "$lib/components/repo/types";
-import { renderDocument } from "$lib/server/markup/render";
+import { renderDocument } from "$lib/markup/render";
export interface RepoParent {
publicConfig: { bobbinUrl: string; camoEnabled: boolean };
diff --git a/web/src/routes/[handle]/[repo]/issues/[rkey]/+page.server.ts b/web/src/routes/[handle]/[repo]/issues/[rkey]/+page.server.ts
index c54dbca12..2b0de42e8 100644
--- a/web/src/routes/[handle]/[repo]/issues/[rkey]/+page.server.ts
+++ b/web/src/routes/[handle]/[repo]/issues/[rkey]/+page.server.ts
@@ -11,7 +11,7 @@ import { TICKET_COLLECTION } from "$lib/api/ticket";
import type { RecordView, TicketViewDetailed } from "$lib/api/records";
import { authorDidOf, rkeyFromUri } from "$lib/api/uri";
import { httpStatusFor, settle, stream } from "$lib/api/load";
-import { renderMarkup } from "$lib/server/markup/render";
+import { renderMarkup } from "$lib/markup/render";
import type { MarkupContext } from "$lib/markup/paths";
import {
buildCommentThreads,
diff --git a/web/src/routes/[handle]/[repo]/pulls/[rkey]/+layout.server.ts b/web/src/routes/[handle]/[repo]/pulls/[rkey]/+layout.server.ts
index cf2d9c4fb..2e1fb47d8 100644
--- a/web/src/routes/[handle]/[repo]/pulls/[rkey]/+layout.server.ts
+++ b/web/src/routes/[handle]/[repo]/pulls/[rkey]/+layout.server.ts
@@ -7,7 +7,7 @@ import { getTicketView, listPatches } from "$lib/api/records";
import { rkeyFromUri } from "$lib/api/uri";
import { early, settle, stream } from "$lib/api/load";
import type { MarkupContext } from "$lib/markup";
-import { renderMarkup } from "$lib/server/markup/render";
+import { renderMarkup } from "$lib/markup/render";
import type { CommentListPage } from "$lib/api/comment";
import {
COMMENT_ENRICH,
diff --git a/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte b/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte
index d0a7cd234..d92f5b32d 100644
--- a/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte
+++ b/web/src/routes/[handle]/[repo]/pulls/[rkey]/[version]/[[range]]/PullViewPage.svelte
@@ -38,7 +38,7 @@
import ResizeHandle from "$lib/components/ui/ResizeHandle.svelte";
import { getAuth } from "$lib/auth.svelte";
import { getNotifications } from "$lib/notifications.svelte";
- import { previewMarkup } from "$lib/markup";
+ import { renderPreview } from "$lib/markup";
import RepoTabs from "$lib/components/repo/RepoTabs.svelte";
import Ticket from "$lib/components/repo/tickets/Ticket.svelte";
import TicketSubscribeButton from "$lib/components/repo/tickets/TicketSubscribeButton.svelte";
@@ -415,7 +415,7 @@
authorHandle: user.handle,
createdAt,
body,
- bodyHtml: await previewMarkup(body, data.markup).catch(() => null)
+ bodyHtml: await renderPreview(body, data.markup).catch(() => null)
}
];
};
@@ -452,7 +452,7 @@
cid: saved.cid,
title,
body,
- bodyHtml: body ? await previewMarkup(body, data.markup).catch(() => null) : null,
+ bodyHtml: body ? await renderPreview(body, data.markup).catch(() => null) : null,
targetBranch: targetBranch ?? pull.targetBranch
};
// mockTargetBranch = targetBranch === pull.targetBranch ? undefined : targetBranch;
diff --git a/web/src/routes/_internal/markup/+server.ts b/web/src/routes/_internal/markup/+server.ts
deleted file mode 100644
index 5e2105d7e..000000000
--- a/web/src/routes/_internal/markup/+server.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { error, json } from "@sveltejs/kit";
-import type { RequestHandler } from "./$types";
-import type { MarkupContext } from "$lib/markup/paths";
-import { getPublicConfig } from "$lib/server/config";
-import { SOURCE_LIMIT } from "$lib/server/markup/markdown";
-import { renderMarkup } from "$lib/server/markup/render";
-
-const text = (value: unknown, limit: number): value is string =>
- typeof value === "string" && value.length <= limit;
-
-export const POST: RequestHandler = async ({ request, url, platform }) => {
- const body: unknown = await request.json().catch(() => null);
- if (typeof body !== "object" || body === null) error(400, "Invalid request");
-
- const { contents, repo, ref, dir, sourceLines } = body as Record;
- if (!text(contents, SOURCE_LIMIT) || !text(repo, 512) || !text(ref, 512)) {
- error(400, "Invalid request");
- }
- if (dir !== undefined && !text(dir, 4096)) error(400, "Invalid request");
-
- const ctx: MarkupContext = {
- repo,
- ref,
- dir,
- host: url.host,
- camo: getPublicConfig(platform).camoEnabled,
- sourceLines: sourceLines === true
- };
- return json({ html: await renderMarkup(contents, ctx) });
-};
diff --git a/web/vite.config.ts b/web/vite.config.ts
index e8843f69e..7588341f7 100644
--- a/web/vite.config.ts
+++ b/web/vite.config.ts
@@ -108,10 +108,10 @@ export default defineConfig({
})
],
// vite only crawls kit routes here, so undeclared story deps would
- // optimize mid-run and wedge chromium
+ // optimize mid-run and wedge chromium; sanitize-html is cjs
optimizeDeps: {
noDiscovery: true,
- include: ["storybook/test", "textarea-caret"]
+ include: ["storybook/test", "sanitize-html", "textarea-caret"]
},
test: {
name: "storybook",