@@ -12,10 +12,17 @@ import {Decoration, DecorationSet} from 'prosemirror-view';
1212import type { ExtensionAuto } from '../../../../core' ;
1313import { capitalize } from '../../../../lodash' ;
1414import { globalLogger } from '../../../../logger' ;
15- import { CodeBlockNodeAttr , codeBlockNodeName , codeBlockType } from '../CodeBlockSpecs' ;
15+ import {
16+ CodeBlockNodeAttr ,
17+ type LineNumbersOptions ,
18+ codeBlockNodeName ,
19+ codeBlockType ,
20+ } from '../CodeBlockSpecs' ;
1621
1722import { codeLangSelectTooltipViewCreator } from './TooltipPlugin' ;
1823
24+ import './CodeBlockHighlight.scss' ;
25+
1926export type HighlightLangMap = Options [ 'highlightLangs' ] ;
2027
2128type Lowlight = ReturnType < typeof createLowlight > ;
@@ -29,6 +36,7 @@ type LangSelectItem = {
2936const key = new PluginKey < DecorationSet > ( 'code_block_highlight' ) ;
3037
3138export type CodeBlockHighlightOptions = {
39+ lineNumbers ?: LineNumbersOptions ;
3240 langs ?: HighlightLangMap ;
3341} ;
3442
@@ -135,7 +143,13 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
135143 return decos . map ( tr . mapping , tr . doc ) ;
136144 } ,
137145 } ,
138- view : ( view ) => codeLangSelectTooltipViewCreator ( view , selectItems , mapping ) ,
146+ view : ( view ) =>
147+ codeLangSelectTooltipViewCreator (
148+ view ,
149+ selectItems ,
150+ mapping ,
151+ Boolean ( opts . lineNumbers ?. enabled ) ,
152+ ) ,
139153 props : {
140154 decorations : ( state ) => {
141155 return key . getState ( state ) ;
@@ -151,15 +165,24 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
151165 node . attrs [ CodeBlockNodeAttr . Line ] ,
152166 ) ;
153167
154- const contentDOM = document . createElement ( 'code' ) ;
155- contentDOM . classList . add ( 'hljs' ) ;
168+ const code = document . createElement ( 'code' ) ;
169+ code . classList . add ( 'hljs' ) ;
156170
157171 if ( prevLang ) {
158172 dom . setAttribute ( CodeBlockNodeAttr . Lang , prevLang ) ;
159- contentDOM . classList . add ( prevLang ) ;
173+ code . classList . add ( prevLang ) ;
160174 }
161175
162- dom . append ( contentDOM ) ;
176+ const contentDOM = document . createElement ( 'div' ) ;
177+
178+ let lineNumbersContainer : HTMLDivElement | undefined ;
179+
180+ if ( opts . lineNumbers ?. enabled ) {
181+ lineNumbersContainer = initializeLineNumbers ( node , code ) ;
182+ }
183+
184+ code . append ( contentDOM ) ;
185+ dom . append ( code ) ;
163186
164187 return {
165188 dom,
@@ -169,10 +192,10 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
169192
170193 const newLang = newNode . attrs [ CodeBlockNodeAttr . Lang ] ;
171194 if ( prevLang !== newLang ) {
172- contentDOM . className = 'hljs' ;
195+ code . className = 'hljs' ;
173196 updateDomAttribute ( dom , CodeBlockNodeAttr . Lang , newLang ) ;
174197 if ( newLang ) {
175- contentDOM . classList . add ( newLang ) ;
198+ code . classList . add ( newLang ) ;
176199 }
177200 prevLang = newLang ;
178201 }
@@ -183,6 +206,14 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
183206 newNode . attrs [ CodeBlockNodeAttr . Line ] ,
184207 ) ;
185208
209+ if ( opts . lineNumbers ?. enabled ) {
210+ lineNumbersContainer = updateLineNumbers (
211+ newNode ,
212+ code ,
213+ lineNumbersContainer ,
214+ ) ;
215+ }
216+
186217 return true ;
187218 } ,
188219 } ;
@@ -259,3 +290,64 @@ function updateDomAttribute(elem: Element, attr: string, value: string | null |
259290 elem . removeAttribute ( attr ) ;
260291 }
261292}
293+ function initializeLineNumbers ( node : Node , code : HTMLElement ) {
294+ const showLineNumbers = node . attrs [ CodeBlockNodeAttr . ShowLineNumbers ] ;
295+
296+ if ( ! showLineNumbers ) {
297+ return undefined ;
298+ }
299+
300+ const lineNumbersContainer = document . createElement ( 'div' ) ;
301+ lineNumbersContainer . className = 'yfm-line-numbers' ;
302+ lineNumbersContainer . contentEditable = 'false' ;
303+
304+ const lineNumbersContent = createLineNumbersContent ( node . textContent ) ;
305+ lineNumbersContainer . innerHTML = lineNumbersContent ;
306+
307+ code . prepend ( lineNumbersContainer ) ;
308+ code . classList . add ( 'show-line-numbers' ) ;
309+
310+ return lineNumbersContainer ;
311+ }
312+
313+ function updateLineNumbers (
314+ node : Node ,
315+ code : HTMLElement ,
316+ prevLineNumbersContainer ?: HTMLDivElement ,
317+ ) {
318+ const showLineNumbers = node . attrs [ CodeBlockNodeAttr . ShowLineNumbers ] ;
319+
320+ if ( ! prevLineNumbersContainer && showLineNumbers !== 'true' ) {
321+ return undefined ;
322+ } else if ( ! prevLineNumbersContainer && showLineNumbers === 'true' ) {
323+ return initializeLineNumbers ( node , code ) ;
324+ } else if ( prevLineNumbersContainer && showLineNumbers !== 'true' ) {
325+ code . removeChild ( prevLineNumbersContainer ) ;
326+ code . classList . remove ( 'show-line-numbers' ) ;
327+ return undefined ;
328+ }
329+
330+ if ( ! prevLineNumbersContainer ) {
331+ return prevLineNumbersContainer ;
332+ }
333+
334+ const lineNumbersContent = createLineNumbersContent ( node . textContent ) ;
335+ prevLineNumbersContainer . innerHTML = lineNumbersContent ;
336+ code . classList . add ( 'show-line-numbers' ) ;
337+
338+ return prevLineNumbersContainer ;
339+ }
340+
341+ function createLineNumbersContent ( content : string ) {
342+ const lines = content ? content . split ( '\n' ) : [ '' ] ;
343+ const lineCount = lines . length ;
344+ const maxDigits = String ( lineCount ) . length ;
345+
346+ let lineNumbersHtml = '' ;
347+ for ( let i = 1 ; i <= lineCount ; i ++ ) {
348+ const num = String ( i ) . padStart ( maxDigits , ' ' ) ;
349+ lineNumbersHtml += `<div class="yfm-line-number">${ num } </div>` ;
350+ }
351+
352+ return lineNumbersHtml ;
353+ }
0 commit comments