Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.yfm .hljs.show-line-numbers {
display: flex;

white-space: pre;
}

.yfm pre > code > .yfm-line-numbers > .yfm-line-number {
display: block;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,17 @@ import {Decoration, DecorationSet} from 'prosemirror-view';
import type {ExtensionAuto} from '../../../../core';
import {capitalize} from '../../../../lodash';
import {globalLogger} from '../../../../logger';
import {CodeBlockNodeAttr, codeBlockNodeName, codeBlockType} from '../CodeBlockSpecs';
import {
CodeBlockNodeAttr,
type LineNumbersOptions,
codeBlockNodeName,
codeBlockType,
} from '../CodeBlockSpecs';

import {codeLangSelectTooltipViewCreator} from './TooltipPlugin';

import './CodeBlockHighlight.scss';

export type HighlightLangMap = Options['highlightLangs'];

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

export type CodeBlockHighlightOptions = {
lineNumbers?: LineNumbersOptions;
langs?: HighlightLangMap;
};

Expand Down Expand Up @@ -135,7 +143,13 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
return decos.map(tr.mapping, tr.doc);
},
},
view: (view) => codeLangSelectTooltipViewCreator(view, selectItems, mapping),
view: (view) =>
codeLangSelectTooltipViewCreator(
view,
selectItems,
mapping,
Boolean(opts.lineNumbers?.enabled),
),
props: {
decorations: (state) => {
return key.getState(state);
Expand All @@ -151,15 +165,24 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
node.attrs[CodeBlockNodeAttr.Line],
);

const contentDOM = document.createElement('code');
contentDOM.classList.add('hljs');
const code = document.createElement('code');
code.classList.add('hljs');

if (prevLang) {
dom.setAttribute(CodeBlockNodeAttr.Lang, prevLang);
contentDOM.classList.add(prevLang);
code.classList.add(prevLang);
}

dom.append(contentDOM);
const contentDOM = document.createElement('div');

let lineNumbersContainer: HTMLDivElement | undefined;

if (opts.lineNumbers?.enabled) {
lineNumbersContainer = initializeLineNumbers(node, code);
}

code.append(contentDOM);
dom.append(code);

return {
dom,
Expand All @@ -169,10 +192,10 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui

const newLang = newNode.attrs[CodeBlockNodeAttr.Lang];
if (prevLang !== newLang) {
contentDOM.className = 'hljs';
code.className = 'hljs';
updateDomAttribute(dom, CodeBlockNodeAttr.Lang, newLang);
if (newLang) {
contentDOM.classList.add(newLang);
code.classList.add(newLang);
}
prevLang = newLang;
}
Expand All @@ -183,6 +206,14 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
newNode.attrs[CodeBlockNodeAttr.Line],
);

if (opts.lineNumbers?.enabled) {
lineNumbersContainer = updateLineNumbers(
newNode,
code,
lineNumbersContainer,
);
}

return true;
},
};
Expand Down Expand Up @@ -259,3 +290,64 @@ function updateDomAttribute(elem: Element, attr: string, value: string | null |
elem.removeAttribute(attr);
}
}
function initializeLineNumbers(node: Node, code: HTMLElement) {
const showLineNumbers = node.attrs[CodeBlockNodeAttr.ShowLineNumbers];

if (!showLineNumbers) {
return undefined;
}

const lineNumbersContainer = document.createElement('div');
lineNumbersContainer.className = 'yfm-line-numbers';
lineNumbersContainer.contentEditable = 'false';

const lineNumbersContent = createLineNumbersContent(node.textContent);
lineNumbersContainer.innerHTML = lineNumbersContent;

code.prepend(lineNumbersContainer);
code.classList.add('show-line-numbers');

return lineNumbersContainer;
}

function updateLineNumbers(
node: Node,
code: HTMLElement,
prevLineNumbersContainer?: HTMLDivElement,
) {
const showLineNumbers = node.attrs[CodeBlockNodeAttr.ShowLineNumbers];

if (!prevLineNumbersContainer && showLineNumbers !== 'true') {
return undefined;
} else if (!prevLineNumbersContainer && showLineNumbers === 'true') {
return initializeLineNumbers(node, code);
} else if (prevLineNumbersContainer && showLineNumbers !== 'true') {
code.removeChild(prevLineNumbersContainer);
code.classList.remove('show-line-numbers');
return undefined;
}

if (!prevLineNumbersContainer) {
return prevLineNumbersContainer;
}

const lineNumbersContent = createLineNumbersContent(node.textContent);
prevLineNumbersContainer.innerHTML = lineNumbersContent;
code.classList.add('show-line-numbers');

return prevLineNumbersContainer;
}

function createLineNumbersContent(content: string) {
const lines = content ? content.split('\n') : [''];
const lineCount = lines.length;
const maxDigits = String(lineCount).length;

let lineNumbersHtml = '';
for (let i = 1; i <= lineCount; i++) {
const num = String(i).padStart(maxDigits, ' ');
lineNumbersHtml += `<div class="yfm-line-number">${num}</div>`;
}

return lineNumbersHtml;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@
.g-md-code-block__select-button {
margin: auto 0;
}

.g-md-code-block__show-line-numbers {
margin: auto 0;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type {ChangeEventHandler} from 'react';

import {TrashBin} from '@gravity-ui/icons';
import {Select, type SelectOption} from '@gravity-ui/uikit';
import {Checkbox, Select, type SelectOption} from '@gravity-ui/uikit';
import type {Node} from 'prosemirror-model';
import type {EditorView} from 'prosemirror-view';

import {i18n} from '../../../../../i18n/codeblock';
import {i18n as i18nPlaceholder} from '../../../../../i18n/placeholder';
import {BaseTooltipPluginView} from '../../../../../plugins/BaseTooltip';
import {Toolbar, ToolbarDataType} from '../../../../../toolbar';
import {Toolbar, type ToolbarData, ToolbarDataType} from '../../../../../toolbar';
import {removeNode} from '../../../../../utils/remove-node';
import {CodeBlockNodeAttr, codeBlockType} from '../../CodeBlockSpecs';

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

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

const handleClick = (type: string) => {
Expand All @@ -31,6 +34,7 @@ const CodeMenu: React.FC<CodeMenuProps> = ({view, pos, node, selectItems, mappin
view.dispatch(
view.state.tr.setNodeMarkup(pos, null, {
[CodeBlockNodeAttr.Lang]: type,
[CodeBlockNodeAttr.ShowLineNumbers]: showLineNumbers,
}),
);
};
Expand All @@ -56,56 +60,96 @@ const CodeMenu: React.FC<CodeMenuProps> = ({view, pos, node, selectItems, mappin
);
};

type ShowLineNumbersProps = {
view: EditorView;
pos: number;
node: Node;
};

const ShowLineNumbers: React.FC<ShowLineNumbersProps> = ({view, pos, node}) => {
const lang = node.attrs[CodeBlockNodeAttr.Lang];
const showLineNumbers = node.attrs[CodeBlockNodeAttr.ShowLineNumbers] === 'true';

const handleChange: ChangeEventHandler<HTMLInputElement> = (event) => {
view.dispatch(
view.state.tr.setNodeMarkup(pos, null, {
[CodeBlockNodeAttr.Lang]: lang,
[CodeBlockNodeAttr.ShowLineNumbers]: event.target.checked ? 'true' : '',
}),
);
};

return (
<Checkbox
checked={showLineNumbers}
className="g-md-code-block__show-line-numbers"
content={i18n('show_line_numbers')}
onChange={handleChange}
/>
);
};

export const codeLangSelectTooltipViewCreator = (
view: EditorView,
langItems: SelectOption[],
mapping: Record<string, string> = {},
showLineNumbers: boolean,
) => {
return new BaseTooltipPluginView(view, {
idPrefix: 'code-block-tooltip',
nodeType: codeBlockType(view.state.schema),
popupPlacement: ['bottom', 'top'],
content: (view, {node, pos}) => (
<Toolbar
editor={{}}
focus={() => view.focus()}
className="g-md-code-block-toolbar"
data={[
[
{
id: 'code-block-type',
type: ToolbarDataType.ReactComponent,
component: () => (
<CodeMenu
view={view}
pos={pos}
node={node}
selectItems={langItems}
mapping={mapping}
/>
),
width: 28,
},
],
[
{
id: 'code-block-remove',
icon: {data: TrashBin},
title: i18n('remove'),
type: ToolbarDataType.SingleButton,
isActive: () => false,
isEnable: () => true,
exec: () =>
removeNode({
pos: pos,
node: node,
tr: view.state.tr,
dispatch: view.dispatch.bind(view),
}),
},
],
]}
/>
),
content: (view, {node, pos}) => {
const lineNumbersCheckbox: ToolbarData<{}>[number][number] = {
id: 'code-block-showlinenumbers',
type: ToolbarDataType.ReactComponent,
component: () => <ShowLineNumbers view={view} pos={pos} node={node} />,
width: 28,
};

return (
<Toolbar
editor={{}}
focus={() => view.focus()}
className="g-md-code-block-toolbar"
data={[
[
{
id: 'code-block-type',
type: ToolbarDataType.ReactComponent,
component: () => (
<CodeMenu
view={view}
pos={pos}
node={node}
selectItems={langItems}
mapping={mapping}
/>
),
width: 28,
},
],
...(showLineNumbers ? [[lineNumbersCheckbox]] : []),
[
{
id: 'code-block-remove',
icon: {data: TrashBin},
title: i18n('remove'),
type: ToolbarDataType.SingleButton,
isActive: () => false,
isEnable: () => true,
exec: () =>
removeNode({
pos: pos,
node: node,
tr: view.state.tr,
dispatch: view.dispatch.bind(view),
}),
},
],
]}
/>
);
},
});
};
Loading
Loading