+
diff --git a/web/src/lib/components/ui/MarkdownEditor.svelte b/web/src/lib/components/ui/MarkdownEditor.svelte
index 23acdcb3d..5d0c2992b 100644
--- a/web/src/lib/components/ui/MarkdownEditor.svelte
+++ b/web/src/lib/components/ui/MarkdownEditor.svelte
@@ -124,6 +124,7 @@
} from "$lib/components/ui/markdownScroll";
import { renderMarkup, type MarkupContext } from "$lib/markup";
import { enhanceCodeblocks } from "$lib/markup/highlight";
+ import { codeblockToolbars } from "$lib/components/ui/codeblockToolbars";
interface Tool {
label: string;
@@ -449,6 +450,7 @@
class="markup px-2.5 py-xs"
data-preview-ready
use:enhanceCodeblocks={html}
+ use:codeblockToolbars={html}
{@attach alignPreview}
>
diff --git a/web/src/lib/components/ui/codeblockToolbars.ts b/web/src/lib/components/ui/codeblockToolbars.ts
new file mode 100644
index 000000000..70d7170b8
--- /dev/null
+++ b/web/src/lib/components/ui/codeblockToolbars.ts
@@ -0,0 +1,36 @@
+import { mount, unmount } from "svelte";
+import type { Action } from "svelte/action";
+import CodeBlockToolbar from "$lib/components/ui/CodeBlockToolbar.svelte";
+
+export const codeblockToolbars: Action
+
+
+ {lines} lines
+
+
+
+
+
+
+${"a\n".repeat(COLLAPSED_LINES - 1)}`;
+ expect(collapseCodeblocks(html)).toBe(html);
+ });
+
+ it("wraps tall blocks with their line count and a toolbar slot", () => {
+ const html = `x
${"a\n".repeat(COLLAPSED_LINES)}`;
+ expect(collapseCodeblocks(html)).toBe(
+ `x
${"a\n".repeat(COLLAPSED_LINES)}${"a\n".repeat(COLLAPSED_LINES - 1)}a`;
+ expect(collapseCodeblocks(html)).toContain(`data-codeblock="${COLLAPSED_LINES}"`);
+ });
+
+ it("wraps only the innermost of nested pre elements", () => {
+ const inner = `${"a\n".repeat(COLLAPSED_LINES)}`;
+ const out = collapseCodeblocks(`x${inner}`);
+ expect(out.match(/data-codeblock=/g)).toHaveLength(1);
+ expect(out.startsWith("x{ + it("collapses tall code in bodies", async () => { + expect(await renderMarkup(fence(40), ctx)).toContain('data-codeblock="40"'); + }); + + it("keeps tall code in documents expanded", async () => { + expect(await renderDocument("README.md", fence(40), ctx)).not.toContain("data-codeblock"); + }); + + it("drops a data-codeblock written in the source", async () => { + expect(await renderMarkup('x', ctx)).not.toContain( + "data-codeblock" + ); + }); +}); diff --git a/web/src/lib/markup/collapse.ts b/web/src/lib/markup/collapse.ts new file mode 100644 index 000000000..a3d44ff29 --- /dev/null +++ b/web/src/lib/markup/collapse.ts @@ -0,0 +1,17 @@ +export const COLLAPSED_LINES = 28; + +const PRE = /]*)?>(?:(?!])[\s\S])*?<\/pre>/g; + +const lineCount = (pre: string): number => + pre + .replace(/<[^>]*>/g, "") + .replace(/\n$/, "") + .split("\n").length; + +export const collapseCodeblocks = (html: string): string => + html.replace(PRE, (pre) => { + const lines = lineCount(pre); + return lines < COLLAPSED_LINES + ? pre + : `${pre}`; + }); diff --git a/web/src/lib/markup/markdown.ts b/web/src/lib/markup/markdown.ts index a9984f983..0001419d0 100644 --- a/web/src/lib/markup/markdown.ts +++ b/web/src/lib/markup/markdown.ts @@ -4,6 +4,7 @@ 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/markup/collapse"; import { highlightCodeblocks } from "$lib/markup/highlight"; import { sanitizeMarkup } from "$lib/markup/sanitize"; import type { MarkupContext } from "$lib/markup/paths"; @@ -137,5 +138,6 @@ const rendererFor = (host: string, lines: boolean): MarkdownIt => { export const renderMarkdown = async (source: string, ctx: MarkupContext): Promise=> { const raw = rendererFor(ctx.host ?? "", !!ctx.sourceLines).render(source); // sanitize before highlighting so the allowlist doesn't have to allow spans - return highlightCodeblocks(sanitizeMarkup(raw, ctx)); + const clean = sanitizeMarkup(raw, ctx); + return highlightCodeblocks(ctx.collapseCode ? collapseCodeblocks(clean) : clean); }; diff --git a/web/src/lib/markup/paths.ts b/web/src/lib/markup/paths.ts index b957333a9..4604a60a3 100644 --- a/web/src/lib/markup/paths.ts +++ b/web/src/lib/markup/paths.ts @@ -8,6 +8,7 @@ export interface MarkupContext { host?: string; camo?: boolean; sourceLines?: boolean; + collapseCode?: boolean; } const ABSOLUTE = /^[a-z][a-z0-9+.-]*:|^\/\//i; diff --git a/web/src/lib/markup/render.ts b/web/src/lib/markup/render.ts index fc5717551..6adf707c5 100644 --- a/web/src/lib/markup/render.ts +++ b/web/src/lib/markup/render.ts @@ -24,6 +24,7 @@ const keyOf = (filename: string, contents: string, ctx: MarkupContext): string = ctx.ref, ctx.dir ?? "", String(!!ctx.sourceLines), + String(!!ctx.collapseCode), filename ].join("\n") ), @@ -63,5 +64,7 @@ export const renderDocument = async ( // like renderDocument, but for content that is always markdown (issue and // comment bodies) rather than a repo file with an extension to sniff -export const renderMarkup = (contents: string, ctx: MarkupContext): Promise => - render(keyOf("", contents, ctx), contents, ctx); +export const renderMarkup = (contents: string, ctx: MarkupContext): Promise => { + const body = { ...ctx, collapseCode: true }; + return render(keyOf("", contents, body), contents, body); +}; diff --git a/web/src/markup.css b/web/src/markup.css index 95dcfa38d..5a27b2c18 100644 --- a/web/src/markup.css +++ b/web/src/markup.css @@ -121,6 +121,26 @@ @apply bg-transparent p-none font-mono text-[0.875em] leading-[calc(20/13)]; } + .markup :where([data-codeblock]) { + @apply my-sm flex max-h-150 flex-col overflow-hidden rounded border border-border-default; + } + + .markup :where([data-codeblock][data-expanded]) { + @apply max-h-[none]; + } + + .markup :where([data-codeblock]) > pre { + @apply m-none min-h-0 flex-1 overflow-hidden rounded-none border-none; + } + + .markup :where([data-codeblock][data-expanded]) > pre { + @apply overflow-auto; + } + + .markup :where([data-codeblock-toolbar]) { + @apply box-content min-h-8 shrink-0 border-b border-border-default bg-background-default p-xs; + } + .markup kbd { @apply rounded border border-border-default bg-background-subtle px-1.5 py-3xs font-mono text-[0.875em]; }