diff --git a/src/hooks/useMarkdown.hook.spec.ts b/src/hooks/useMarkdown.hook.spec.ts new file mode 100644 index 0000000..a5cf12a --- /dev/null +++ b/src/hooks/useMarkdown.hook.spec.ts @@ -0,0 +1,42 @@ +import { describe, expect, it, vi } from "vitest" + +vi.mock("@/data/data", () => ({ + data: {}, + generateId: (type: string, id: string) => `${type}-${id}` +})) + +import { renderCodeFile } from "@/hooks/useMarkdown.hook" + +describe("renderCodeFile", () => { + it("wraps highlighted files in a code-file container with a sized gutter", async () => { + const html = await renderCodeFile({ + rawContent: "const a = 1\nconst b = 2\n", + lang: "typescript", + filename: "example.ts" + }) + + expect(html).toContain('
{ + const html = await renderCodeFile({ + rawContent: "line one\nline \n", + lang: null + }) + + expect(html).toContain('line one') + expect(html).toContain('line <two>') + expect(html.match(/class="line"/g)).toHaveLength(2) + }) + + it("sizes the gutter for files with many lines", async () => { + const html = await renderCodeFile({ + rawContent: Array.from({ length: 120 }, (_, i) => `line ${i}`).join("\n"), + lang: null + }) + + expect(html).toContain("--line-number-width:3ch") + }) +}) diff --git a/src/hooks/useMarkdown.hook.ts b/src/hooks/useMarkdown.hook.ts index 5fa6562..32251ae 100644 --- a/src/hooks/useMarkdown.hook.ts +++ b/src/hooks/useMarkdown.hook.ts @@ -421,10 +421,22 @@ export const renderCodeFile = async ({ }): Promise => { await useShikiji() const heading = filename ? `# ${filename}\n\n` : "" + // Drop the trailing newline so the final empty line isn't numbered. + const lines = rawContent.replace(/\n$/, "").split("\n") + // Sized gutter so line numbers stay right-aligned past 3 digits. + const wrapper = `
` if (lang !== null) { - return renderMarkdown(`${heading}\`\`\`\`${lang}\n${rawContent}\n\`\`\`\``) + const rendered = renderMarkdown( + `${heading}\`\`\`\`${lang}\n${lines.join("\n")}\n\`\`\`\`` + ) + // Shikiji always appends an empty line span; drop it so it isn't numbered. + return `${wrapper}${rendered.replace('\n', "")}
` } - return `${renderMarkdown(heading)}
${md.utils.escapeHtml(rawContent)}
` + // Mirror Shikiji's per-line spans so the line-number CSS applies uniformly. + const code = lines + .map((line) => `${md.utils.escapeHtml(line)}`) + .join("\n") + return `${wrapper}${renderMarkdown(heading)}
${code}
` } export const markdownBuilder = (defaultPrefix?: Ref | string) => { diff --git a/src/styles/app.css b/src/styles/app.css index e075667..6b62733 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -186,6 +186,24 @@ pre { overflow-wrap: anywhere; } +/* Line numbers for whole-file code views (non-markdown files). + Scoped to .code-file so fenced blocks inside notes stay untouched. */ +.code-file pre code { + counter-reset: line; +} + +.code-file pre code .line::before { + counter-increment: line; + content: counter(line); + display: inline-block; + width: var(--line-number-width, 3ch); + margin-right: 1.5ch; + text-align: right; + color: var(--color-base-content); + opacity: 0.35; + user-select: none; +} + pre.mermaid { display: flex; justify-content: center;