Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/components/code-example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { clsx } from "clsx";
import dedent from "dedent";
import { createHighlighter } from "shiki";
import theme from "./syntax-highlighter/theme.json";
import { CopyButton } from "./copy-button";

import { highlightClasses } from "./highlight-classes";
import atApplyInjection from "./syntax-highlighter/at-apply.json";
import atRulesInjection from "./syntax-highlighter/at-rules.json";
import themeFnInjection from "./syntax-highlighter/theme-fn.json";
import { stripShikiComments } from "./shiki";

export function js(strings: TemplateStringsArray, ...args: any[]) {
return { lang: "js", code: dedent(strings, ...args) };
Expand Down Expand Up @@ -42,14 +44,29 @@ export async function CodeExample({
example,
filename,
className = "",
copyable = false,
}: {
example: { lang: string; code: string };
filename?: string;
className?: string;
copyable?: boolean;
}) {
return (
<CodeExampleWrapper className={className}>
{filename ? <CodeExampleFilename filename={filename} /> : null}
<div className="relative">
{filename ? <CodeExampleFilename filename={filename} /> : null}
{copyable && (
<CopyButton
className={clsx(
"absolute z-10 transition duration-150 group-hover/code-block:opacity-100",
filename
? "-top-1 right-0 text-white/50 hover:text-white/75"
: "top-2 right-2 rounded border border-black/15 bg-black/50 text-white/75 opacity-0 backdrop-blur-md hover:text-white",
)}
value={stripShikiComments(example.code)}
/>
)}
</div>
<HighlightedCode example={example} />
</CodeExampleWrapper>
);
Expand Down
52 changes: 52 additions & 0 deletions src/components/copy-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import clsx from "clsx";
import { useState } from "react";

export function CopyButton({ value, className }: { value: string; className?: string }) {
const [copied, setCopied] = useState(false);

const handleCopy = async () => {
if (copied) return;

try {
await navigator.clipboard.writeText(value);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy:", err);
}
};

return (
<button
onClick={handleCopy}
className={clsx("flex size-8 items-center justify-center", className)}
title="Copy to clipboard"
>
<div className="grid size-4">
<svg
viewBox="0 0 16 16"
strokeWidth={1}
className={clsx(
"col-start-1 row-start-1 fill-none stroke-current text-sky-400 transition-opacity duration-300 ease-in-out",
!copied && "opacity-0",
)}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M3.5 8.5L7.5 12.5L12.5 3.5" />
</svg>

<svg
viewBox="0 0 16 16"
strokeWidth={1}
className={clsx(
"col-start-1 row-start-1 fill-none stroke-current transition-opacity duration-300 ease-in-out",
copied && "opacity-0",
)}
>
<path d="M5.5 2.5V2.5C5.5 3.05228 5.94772 3.5 6.5 3.5H9.5C10.0523 3.5 10.5 3.05228 10.5 2.5V2.5M5.5 2.5V2.5C5.5 1.94772 5.94772 1.5 6.5 1.5H9.5C10.0523 1.5 10.5 1.94772 10.5 2.5V2.5M5.5 2.5H4.5C3.39543 2.5 2.5 3.39543 2.5 4.5V12.5C2.5 13.6046 3.39543 14.5 4.5 14.5H11.5C12.6046 14.5 13.5 13.6046 13.5 12.5V4.5C13.5 3.39543 12.6046 2.5 11.5 2.5H10.5" />
</svg>
</div>
</button>
);
}
1 change: 1 addition & 0 deletions src/components/installation-steps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export function Steps({ steps }: { steps: Step[] }) {
data-tabs={step.tabs?.join(" ") ?? null}
>
<CodeExample
copyable
filename={step.code.name}
example={{
lang: step.code.lang,
Expand Down
129 changes: 129 additions & 0 deletions src/components/shiki.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Note: can run these tests with `bun test`

import { describe, test } from "node:test";
import { stripShikiComments } from "./shiki";
import dedent from "dedent";

describe("comment removal", () => {
test("at end of line", (t) => {
let input = dedent`
keep me # [!code highlight]
keep me /* [!code highlight] */
keep me // [!code highlight]
keep me <!-- [!code highlight] -->
`;

t.assert.equal(
stripShikiComments(input),
dedent`
keep me \n\
keep me \n\
keep me \n\
keep me \
`,
);
});

test("on separate lines", (t) => {
let input = dedent`
# [!code highlight]
/* [!code highlight] */
// [!code highlight]
<!-- [!code highlight] -->
<div class="flex"></div>
`;

t.assert.equal(
stripShikiComments(input),
dedent`
<div class="flex"></div>
`,
);
});

test("on separate lines (prettier)", (t) => {
let input = dedent`
# prettier-ignore
/* prettier-ignore */
// prettier-ignore
<!-- prettier-ignore -->
<div class="flex"></div>
`;

t.assert.equal(
stripShikiComments(input),
dedent`
<div class="flex"></div>
`,
);
});
});

// [!code --] and [!code --:N] handling
describe("line removal", () => {
test("at end of line", (t) => {
let input = dedent`
keep me
remove me 1 # [!code --]
keep me
`;

t.assert.equal(
stripShikiComments(input),
dedent`
keep me
keep me
`,
);
});

test("on separate lines", (t) => {
let input = dedent`
keep me
# [!code --:3]
remove me 1
remove me 2
keep me
`;

t.assert.equal(
stripShikiComments(input),
dedent`
keep me
keep me
`,
);
});

test("an invalid number still removes the line its on", (t) => {
let input = dedent`
keep me
remove me # [!code --:foo]
keep me
`;

t.assert.equal(
stripShikiComments(input),
dedent`
keep me
keep me
`,
);
});

test("an invalid number is ignored on separate lines", (t) => {
let input = dedent`
keep me
# [!code --:foo]
keep me
`;

t.assert.equal(
stripShikiComments(input),
dedent`
keep me
keep me
`,
);
});
});
70 changes: 70 additions & 0 deletions src/components/shiki.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const commentPattern = /\/\*\s*(?=\[!)(.*?)\s*\*\/\s*$|<!--\s*(?=\[!)(.*?)\s*-->\s*$|(?:#|\/\/)\s*(?=\[!)(.*)\s*$/g;
const controlPattern = /^\[!code\s+([^:]+)(?::(.*))?\]$/;

export function stripShikiComments(code: string): string {
if (!code.includes("[!code ") && !code.includes("prettier-ignore")) return code;

let lines = code.split("\n");
let result = "";

for (let i = 0; i < lines.length; i++) {
let line = lines[i];
let trimmed = line.trim();
let removed = false;
let changed = false;

// Skip lines that are meant to control Prettier
if (trimmed === "<!-- prettier-ignore -->") {
continue;
} else if (trimmed === "# prettier-ignore") {
continue;
} else if (trimmed === "// prettier-ignore") {
continue;
} else if (trimmed === "/* prettier-ignore */") {
continue;
}

for (let c of line.matchAll(commentPattern)) {
let content = c[1] ?? c[2] ?? c[3];

let match = content.match(controlPattern);
if (!match) continue;

let kind = match[1];
let params = match[2];

// If we see a `[!code --]` or `[!code --:N]` directive it means we need
// to remove N lines starting at the current line
if (kind === "--") {
// Remove the line containing the `[!code --]` directive
removed = true;

// Remove the remaining N-1 lines after the current line (if specified)
let count = parseInt(params ?? "1", 10) - 1;
if (isNaN(count)) continue;
i += count;

break;
}

// Remove the comment from the current line
//
// NOTE: This has an implicit assumption that the line MUST end with a
// control comment. Processing multiple comments on one line would
// mangle the code. This is enforced by the regex patterns above.
line = line.slice(0, c.index) + line.slice(c.index + c[0].length);
changed = true;
}

// The current line was removed so we can skip it
if (removed) continue;

// This line only contained control comments which have been removed
if (changed && line.trim() === "") continue;

result += line;
result += "\n";
}

return result.trim();
}