Skip to content

Commit 903d154

Browse files
authored
feat: add line numbers for code blocks (#984)
1 parent 3a16a8f commit 903d154

File tree

6 files changed

+70
-18
lines changed

6 files changed

+70
-18
lines changed

apps/web/src/components/CodemirrorEditor/RightSlider.vue

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,25 @@ const formatOptions = ref<Format[]>([`rgb`, `hex`, `hsl`, `hsv`])
160160
</Button>
161161
</div>
162162
</div>
163+
<div class="space-y-2">
164+
<h2>代码块行号</h2>
165+
<div class="grid grid-cols-5 justify-items-center gap-2">
166+
<Button
167+
class="w-full" variant="outline" :class="{
168+
'border-black dark:border-white border-2': store.isShowLineNumber,
169+
}" @click="!store.isShowLineNumber && store.showLineNumberChanged()"
170+
>
171+
开启
172+
</Button>
173+
<Button
174+
class="w-full" variant="outline" :class="{
175+
'border-black dark:border-white border-2': !store.isShowLineNumber,
176+
}" @click="store.isShowLineNumber && store.showLineNumberChanged()"
177+
>
178+
关闭
179+
</Button>
180+
</div>
181+
</div>
163182
<div class="space-y-2">
164183
<h2>AI 工具箱</h2>
165184
<div class="grid grid-cols-5 justify-items-center gap-2">

apps/web/src/stores/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export const useStore = defineStore(`store`, () => {
5757
const isMacCodeBlock = useStorage(`isMacCodeBlock`, defaultStyleConfig.isMacCodeBlock)
5858
const toggleMacCodeBlock = useToggle(isMacCodeBlock)
5959

60+
// 是否开启代码块行号显示
61+
const isShowLineNumber = useStorage(`isShowLineNumber`, defaultStyleConfig.isShowLineNumber)
62+
const toggleShowLineNumber = useToggle(isShowLineNumber)
63+
6064
// 是否在左侧编辑
6165
const isEditOnLeft = useStorage(`isEditOnLeft`, true)
6266
const toggleEditOnLeft = useToggle(isEditOnLeft)
@@ -348,6 +352,7 @@ export const useStore = defineStore(`store`, () => {
348352
isUseIndent: isUseIndent.value,
349353
isUseJustify: isUseJustify.value,
350354
isMacCodeBlock: isMacCodeBlock.value,
355+
isShowLineNumber: isShowLineNumber.value,
351356
})
352357

353358
const readingTime = reactive({
@@ -373,6 +378,7 @@ export const useStore = defineStore(`store`, () => {
373378
isUseJustify: isUseJustify.value,
374379
countStatus: isCountStatus.value,
375380
isMacCodeBlock: isMacCodeBlock.value,
381+
isShowLineNumber: isShowLineNumber.value,
376382
})
377383

378384
const raw = editor.value!.getValue()
@@ -470,6 +476,7 @@ export const useStore = defineStore(`store`, () => {
470476
const resetStyle = () => {
471477
isCiteStatus.value = defaultStyleConfig.isCiteStatus
472478
isMacCodeBlock.value = defaultStyleConfig.isMacCodeBlock
479+
isShowLineNumber.value = defaultStyleConfig.isShowLineNumber
473480
isCountStatus.value = defaultStyleConfig.isCountStatus
474481

475482
theme.value = defaultStyleConfig.theme
@@ -571,6 +578,10 @@ export const useStore = defineStore(`store`, () => {
571578
toggleMacCodeBlock()
572579
})
573580

581+
const showLineNumberChanged = withAfterRefresh(() => {
582+
toggleShowLineNumber()
583+
})
584+
574585
const citeStatusChanged = withAfterRefresh(() => {
575586
toggleCiteStatus()
576587
})
@@ -691,6 +702,7 @@ export const useStore = defineStore(`store`, () => {
691702
toggleEditOnLeft,
692703

693704
isMacCodeBlock,
705+
isShowLineNumber,
694706
isCiteStatus,
695707
citeStatusChanged,
696708
showAIToolbox,
@@ -725,6 +737,7 @@ export const useStore = defineStore(`store`, () => {
725737
codeBlockThemeChanged,
726738
legendChanged,
727739
macCodeBlockChanged,
740+
showLineNumberChanged,
728741

729742
formatContent,
730743
exportEditorContent2HTML,
@@ -819,6 +832,7 @@ export function getAllStoreStates() {
819832
isDark: store.isDark,
820833
isEditOnLeft: store.isEditOnLeft,
821834
isMacCodeBlock: store.isMacCodeBlock,
835+
isShowLineNumber: store.isShowLineNumber,
822836
isCiteStatus: store.isCiteStatus,
823837
showAIToolbox: store.showAIToolbox,
824838
isCountStatus: store.isCountStatus,

packages/core/src/renderer/renderer-impl.ts

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -252,24 +252,40 @@ export function initRenderer(opts: IOpts): RendererAPI {
252252
const langText = lang.split(` `)[0]
253253
const language = hljs.getLanguage(langText) ? langText : `plaintext`
254254

255-
let highlighted = hljs.highlight(text, { language }).value
256-
257-
// 处理两个完整 span 标签之间的空格
258-
highlighted = highlighted.replace(/(<span[^>]*>[^<]*<\/span>)(\s+)(<span[^>]*>[^<]*<\/span>)/g, (_, span1, spaces, span2) => {
259-
return span1 + span2.replace(/^(<span[^>]*>)/, `$1${spaces}`)
260-
})
261-
262-
// 处理 span 标签开始前的空格
263-
highlighted = highlighted.replace(/(\s+)(<span[^>]*>)/g, (_, spaces, span) => {
264-
return span.replace(/^(<span[^>]*>)/, `$1${spaces}`)
265-
})
266-
267-
// tab to 4 spaces
268-
highlighted = highlighted.replace(/\t/g, ` `)
269-
highlighted = highlighted
270-
.replace(/\r\n/g, `<br/>`)
271-
.replace(/\n/g, `<br/>`)
272-
.replace(/(>[^<]+)|(^[^<]+)/g, str => str.replace(/\s/g, `&nbsp;`))
255+
let highlighted = ``
256+
257+
if (opts.isShowLineNumber) {
258+
const rawLines = text.replace(/\r\n/g, `\n`).split(`\n`)
259+
260+
const highlightedLines = rawLines.map((lineRaw) => {
261+
let lineHtml = hljs.highlight(lineRaw, { language }).value
262+
lineHtml = lineHtml.replace(/(<span[^>]*>[^<]*<\/span>)(\s+)(<span[^>]*>[^<]*<\/span>)/g, (_, span1, spaces, span2) => span1 + span2.replace(/^(<span[^>]*>)/, `$1${spaces}`))
263+
lineHtml = lineHtml.replace(/(\s+)(<span[^>]*>)/g, (_, spaces, span) => span.replace(/^(<span[^>]*>)/, `$1${spaces}`))
264+
lineHtml = lineHtml.replace(/\t/g, ` `)
265+
lineHtml = lineHtml.replace(/(>[^<]+)|(^[^<]+)/g, str => str.replace(/\s/g, `&nbsp;`))
266+
return lineHtml === `` ? `&nbsp;` : lineHtml
267+
})
268+
269+
const lineNumbersHtml = highlightedLines.map((_, idx) => `<section style="padding:0 10px 0 0;line-height:1.75">${idx + 1}</section>`).join(``)
270+
const codeInnerHtml = highlightedLines.join(`<br/>`)
271+
const codeLinesHtml = `<div style="white-space:pre;min-width:max-content;line-height:1.75">${codeInnerHtml}</div>`
272+
const lineNumberColumnStyles = `text-align:right;padding:8px 0;border-right:1px solid rgba(0,0,0,0.04);user-select:none;background:var(--code-bg,transparent);`
273+
274+
highlighted = `
275+
<section style="display:flex;align-items:flex-start;overflow-x:hidden;overflow-y:auto;width:100%;max-width:100%;padding:0;box-sizing:border-box">
276+
<section class="line-numbers" style="${lineNumberColumnStyles}">${lineNumbersHtml}</section>
277+
<section class="code-scroll" style="flex:1 1 auto;overflow-x:auto;overflow-y:visible;padding:8px;min-width:0;box-sizing:border-box">${codeLinesHtml}</section>
278+
</section>
279+
`
280+
}
281+
else {
282+
highlighted = hljs.highlight(text, { language }).value
283+
highlighted = highlighted.replace(/(<span[^>]*>[^<]*<\/span>)(\s+)(<span[^>]*>[^<]*<\/span>)/g, (_, span1, spaces, span2) => span1 + span2.replace(/^(<span[^>]*>)/, `$1${spaces}`))
284+
highlighted = highlighted.replace(/(\s+)(<span[^>]*>)/g, (_, spaces, span) => span.replace(/^(<span[^>]*>)/, `$1${spaces}`))
285+
highlighted = highlighted.replace(/\t/g, ` `)
286+
highlighted = highlighted.replace(/\r\n/g, `<br/>`).replace(/\n/g, `<br/>`).replace(/(>[^<]+)|(^[^<]+)/g, str => str.replace(/\s/g, `&nbsp;`))
287+
}
288+
273289
const span = `<span class="mac-sign" style="padding: 10px 14px 0;">${macCodeSvg}</span>`
274290
const code = `<code class="language-${lang}" ${styles(`code`)}>${highlighted}</code>`
275291
return `<pre class="hljs code__pre" ${styles(`code_pre`)}>${span}${code}</pre>`

packages/shared/src/configs/store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export const storeLabels: Record<string, string> = {
33
isDark: `深色模式`,
44
isEditOnLeft: `左侧编辑`,
55
isMacCodeBlock: `Mac 代码块`,
6+
isShowLineNumber: `代码块行号`,
67
isCiteStatus: `微信外链接底部引用状态`,
78
isCountStatus: `字数统计状态`,
89
isUseIndent: `使用缩进`,

packages/shared/src/configs/style.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ export const legendOptions: IConfigOption[] = [
232232
export const defaultStyleConfig = {
233233
isCiteStatus: false,
234234
isMacCodeBlock: true,
235+
isShowLineNumber: false,
235236
isCountStatus: false,
236237
theme: themeOptions[0].value,
237238
fontFamily: fontFamilyOptions[0].value,

packages/shared/src/types/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface IOpts {
3232
citeStatus?: boolean
3333
countStatus?: boolean
3434
isMacCodeBlock?: boolean
35+
isShowLineNumber?: boolean
3536
}
3637

3738
export type ThemeStyles = Record<Block | Inline, ExtendedProperties>

0 commit comments

Comments
 (0)