Skip to content

Commit c02bc1e

Browse files
committed
implemented line numbers
1 parent e873006 commit c02bc1e

File tree

8 files changed

+267
-62
lines changed

8 files changed

+267
-62
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.yfm .hljs.show-line-numbers {
2+
display: flex;
3+
4+
white-space: pre;
5+
}
6+
7+
.yfm pre > code > .yfm-line-numbers > .yfm-line-number {
8+
display: block;
9+
}

src/extensions/markdown/CodeBlock/CodeBlockHighlight/CodeBlockHighlight.ts

Lines changed: 100 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,17 @@ import {Decoration, DecorationSet} from 'prosemirror-view';
1212
import type {ExtensionAuto} from '../../../../core';
1313
import {capitalize} from '../../../../lodash';
1414
import {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

1722
import {codeLangSelectTooltipViewCreator} from './TooltipPlugin';
1823

24+
import './CodeBlockHighlight.scss';
25+
1926
export type HighlightLangMap = Options['highlightLangs'];
2027

2128
type Lowlight = ReturnType<typeof createLowlight>;
@@ -29,6 +36,7 @@ type LangSelectItem = {
2936
const key = new PluginKey<DecorationSet>('code_block_highlight');
3037

3138
export 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+
}

src/extensions/markdown/CodeBlock/CodeBlockHighlight/TooltipPlugin/TooltipView.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,7 @@
2828
.g-md-code-block__select-button {
2929
margin: auto 0;
3030
}
31+
32+
.g-md-code-block__show-line-numbers {
33+
margin: auto 0;
34+
}
Lines changed: 88 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
import type {ChangeEventHandler} from 'react';
2+
13
import {TrashBin} from '@gravity-ui/icons';
2-
import {Select, type SelectOption} from '@gravity-ui/uikit';
4+
import {Checkbox, Select, type SelectOption} from '@gravity-ui/uikit';
35
import type {Node} from 'prosemirror-model';
46
import type {EditorView} from 'prosemirror-view';
57

68
import {i18n} from '../../../../../i18n/codeblock';
79
import {i18n as i18nPlaceholder} from '../../../../../i18n/placeholder';
810
import {BaseTooltipPluginView} from '../../../../../plugins/BaseTooltip';
9-
import {Toolbar, ToolbarDataType} from '../../../../../toolbar';
11+
import {Toolbar, type ToolbarData, ToolbarDataType} from '../../../../../toolbar';
1012
import {removeNode} from '../../../../../utils/remove-node';
1113
import {CodeBlockNodeAttr, codeBlockType} from '../../CodeBlockSpecs';
1214

@@ -22,6 +24,7 @@ type CodeMenuProps = {
2224

2325
const CodeMenu: React.FC<CodeMenuProps> = ({view, pos, node, selectItems, mapping}) => {
2426
const lang = node.attrs[CodeBlockNodeAttr.Lang];
27+
const showLineNumbers = node.attrs[CodeBlockNodeAttr.ShowLineNumbers];
2528
const value = mapping[lang] ?? lang;
2629

2730
const handleClick = (type: string) => {
@@ -31,6 +34,7 @@ const CodeMenu: React.FC<CodeMenuProps> = ({view, pos, node, selectItems, mappin
3134
view.dispatch(
3235
view.state.tr.setNodeMarkup(pos, null, {
3336
[CodeBlockNodeAttr.Lang]: type,
37+
[CodeBlockNodeAttr.ShowLineNumbers]: showLineNumbers,
3438
}),
3539
);
3640
};
@@ -56,56 +60,96 @@ const CodeMenu: React.FC<CodeMenuProps> = ({view, pos, node, selectItems, mappin
5660
);
5761
};
5862

63+
type ShowLineNumbersProps = {
64+
view: EditorView;
65+
pos: number;
66+
node: Node;
67+
};
68+
69+
const ShowLineNumbers: React.FC<ShowLineNumbersProps> = ({view, pos, node}) => {
70+
const lang = node.attrs[CodeBlockNodeAttr.Lang];
71+
const showLineNumbers = node.attrs[CodeBlockNodeAttr.ShowLineNumbers] === 'true';
72+
73+
const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
74+
view.dispatch(
75+
view.state.tr.setNodeMarkup(pos, null, {
76+
[CodeBlockNodeAttr.Lang]: lang,
77+
[CodeBlockNodeAttr.ShowLineNumbers]: event.target.checked ? 'true' : '',
78+
}),
79+
);
80+
};
81+
82+
return (
83+
<Checkbox
84+
checked={showLineNumbers}
85+
className="g-md-code-block__show-line-numbers"
86+
content={i18n('show_line_numbers')}
87+
onChange={handleChange}
88+
/>
89+
);
90+
};
91+
5992
export const codeLangSelectTooltipViewCreator = (
6093
view: EditorView,
6194
langItems: SelectOption[],
6295
mapping: Record<string, string> = {},
96+
showLineNumbers: boolean,
6397
) => {
6498
return new BaseTooltipPluginView(view, {
6599
idPrefix: 'code-block-tooltip',
66100
nodeType: codeBlockType(view.state.schema),
67101
popupPlacement: ['bottom', 'top'],
68-
content: (view, {node, pos}) => (
69-
<Toolbar
70-
editor={{}}
71-
focus={() => view.focus()}
72-
className="g-md-code-block-toolbar"
73-
data={[
74-
[
75-
{
76-
id: 'code-block-type',
77-
type: ToolbarDataType.ReactComponent,
78-
component: () => (
79-
<CodeMenu
80-
view={view}
81-
pos={pos}
82-
node={node}
83-
selectItems={langItems}
84-
mapping={mapping}
85-
/>
86-
),
87-
width: 28,
88-
},
89-
],
90-
[
91-
{
92-
id: 'code-block-remove',
93-
icon: {data: TrashBin},
94-
title: i18n('remove'),
95-
type: ToolbarDataType.SingleButton,
96-
isActive: () => false,
97-
isEnable: () => true,
98-
exec: () =>
99-
removeNode({
100-
pos: pos,
101-
node: node,
102-
tr: view.state.tr,
103-
dispatch: view.dispatch.bind(view),
104-
}),
105-
},
106-
],
107-
]}
108-
/>
109-
),
102+
content: (view, {node, pos}) => {
103+
const lineNumbersCheckbox: ToolbarData<{}>[number][number] = {
104+
id: 'code-block-showlinenumbers',
105+
type: ToolbarDataType.ReactComponent,
106+
component: () => <ShowLineNumbers view={view} pos={pos} node={node} />,
107+
width: 28,
108+
};
109+
110+
return (
111+
<Toolbar
112+
editor={{}}
113+
focus={() => view.focus()}
114+
className="g-md-code-block-toolbar"
115+
data={[
116+
[
117+
{
118+
id: 'code-block-type',
119+
type: ToolbarDataType.ReactComponent,
120+
component: () => (
121+
<CodeMenu
122+
view={view}
123+
pos={pos}
124+
node={node}
125+
selectItems={langItems}
126+
mapping={mapping}
127+
/>
128+
),
129+
width: 28,
130+
},
131+
],
132+
...(showLineNumbers ? [[lineNumbersCheckbox]] : []),
133+
[
134+
{
135+
id: 'code-block-remove',
136+
icon: {data: TrashBin},
137+
title: i18n('remove'),
138+
type: ToolbarDataType.SingleButton,
139+
isActive: () => false,
140+
isEnable: () => true,
141+
exec: () =>
142+
removeNode({
143+
pos: pos,
144+
node: node,
145+
tr: view.state.tr,
146+
dispatch: view.dispatch.bind(view),
147+
}),
148+
},
149+
],
150+
]}
151+
/>
152+
);
153+
},
110154
});
111155
};

0 commit comments

Comments
 (0)