feat(notes): show line numbers on non-markdown file views
All checks were successful
CI / verify (push) Successful in 2m8s
All checks were successful
CI / verify (push) Successful in 2m8s
CSS counters on the per-line spans, scoped to .code-file so fenced blocks inside notes stay untouched. The gutter width adapts to the file's line count and numbers are excluded from text selection.
This commit is contained in:
42
src/hooks/useMarkdown.hook.spec.ts
Normal file
42
src/hooks/useMarkdown.hook.spec.ts
Normal file
@@ -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('<div class="code-file"')
|
||||||
|
expect(html).toContain("--line-number-width:1ch")
|
||||||
|
expect(html.match(/class="line"/g)).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("wraps each line of unknown-language files in line spans", async () => {
|
||||||
|
const html = await renderCodeFile({
|
||||||
|
rawContent: "line one\nline <two>\n",
|
||||||
|
lang: null
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(html).toContain('<span class="line">line one</span>')
|
||||||
|
expect(html).toContain('<span class="line">line <two></span>')
|
||||||
|
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")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -421,10 +421,22 @@ export const renderCodeFile = async ({
|
|||||||
}): Promise<string> => {
|
}): Promise<string> => {
|
||||||
await useShikiji()
|
await useShikiji()
|
||||||
const heading = filename ? `# ${filename}\n\n` : ""
|
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 = `<div class="code-file" style="--line-number-width:${String(lines.length).length}ch">`
|
||||||
if (lang !== null) {
|
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<span class="line"></span></code>', "</code>")}</div>`
|
||||||
}
|
}
|
||||||
return `${renderMarkdown(heading)}<pre><code>${md.utils.escapeHtml(rawContent)}</code></pre>`
|
// Mirror Shikiji's per-line spans so the line-number CSS applies uniformly.
|
||||||
|
const code = lines
|
||||||
|
.map((line) => `<span class="line">${md.utils.escapeHtml(line)}</span>`)
|
||||||
|
.join("\n")
|
||||||
|
return `${wrapper}${renderMarkdown(heading)}<pre><code>${code}</code></pre></div>`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const markdownBuilder = (defaultPrefix?: Ref<string> | string) => {
|
export const markdownBuilder = (defaultPrefix?: Ref<string> | string) => {
|
||||||
|
|||||||
@@ -186,6 +186,24 @@ pre {
|
|||||||
overflow-wrap: anywhere;
|
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 {
|
pre.mermaid {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user