Skip to content
Open
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
3 changes: 3 additions & 0 deletions demo/components/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
disableMarkdownItAttrs?: boolean;
markupParseHtmlOnPaste?: boolean;
style?: React.CSSProperties;
storyAdditionalControls?: Record<string, any>;

Check warning on line 89 in demo/components/Playground.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected any. Specify a different type
} & Pick<UseMarkdownEditorProps, 'experimental' | 'wysiwygConfig'> &
Pick<
MarkdownEditorViewProps,
Expand Down Expand Up @@ -153,7 +153,7 @@
}, [mdRaw]);

const renderPreview = useCallback<RenderPreview>(
({getValue, md, directiveSyntax}) => (

Check warning on line 156 in demo/components/Playground.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

'directiveSyntax' is already declared in the upper scope on line 141 column 9
<SplitModePreview
getValue={getValue}
allowHTML={md.html}
Expand All @@ -173,7 +173,7 @@
[sanitizeHtml, disabledHTMLBlockModes, disableMarkdownItAttrs],
);

const logger = useMemo(() => new Logger2().nested({env: 'playground'}), []);

Check warning on line 176 in demo/components/Playground.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

'logger' is already declared in the upper scope on line 28 column 5
useLogs(logger);

const mdEditor = useMarkdownEditor(
Expand Down Expand Up @@ -235,6 +235,9 @@
imgSize: {
parseInsertedUrlAsImage,
},
codeBlock: {
lineWrapping: {enabled: true},
},
yfmTable: {
table_ignoreSplittersInBlockCode: true,
table_ignoreSplittersInBlockMath: true,
Expand Down Expand Up @@ -343,7 +346,7 @@
mdEditor.off('change-split-mode-enabled', onChangeSplitModeEnabled);
mdEditor.off('change-toolbar-visibility', onChangeToolbarVisibility);
};
}, [mdEditor]);

Check warning on line 349 in demo/components/Playground.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

React Hook useEffect has a missing dependency: 'props'. Either include it or remove the dependency array. However, 'props' will change when *any* prop changes, so the preferred fix is to destructure the 'props' object outside of the useEffect call and refer to those specific props inside useEffect

return (
<PlaygroundLayout
Expand Down
2 changes: 1 addition & 1 deletion demo/components/PlaygroundLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const PlaygroundLayout: React.FC<PlaygroundLayoutProps> = function Playgr

<hr />

<div className={b('preview')}>
<div className={b('preview')} style={{width: props.viewWidth ?? 'initial'}}>
{editor.currentMode === 'wysiwyg' && (
<pre className={b('markup')}>{mdMarkup}</pre>
)}
Expand Down
16 changes: 16 additions & 0 deletions demo/stories/examples/code-block/CodeBlock.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type {StoryObj} from '@storybook/react';

import {CodeBlockDemo as component} from './CodeBlock';

export const Story: StoryObj<typeof component> = {
args: {
lineWrapping: 'disabled',
lineNumbers: 'enabled',
},
};
Story.storyName = 'Code block';

export default {
title: 'Examples / Code block',
component,
};
55 changes: 55 additions & 0 deletions demo/stories/examples/code-block/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {memo} from 'react';

import {MarkdownEditorView, useMarkdownEditor} from 'src/index';

import {PlaygroundLayout} from '../../../components/PlaygroundLayout';

import {markup} from './markup';

export type YfmTableDnDDemoProps = {
lineWrapping: 'disabled' | 'enabled';
lineNumbers: 'disabled' | 'enabled' | 'default';
};

export const CodeBlockDemo = memo<YfmTableDnDDemoProps>(function YfmTableDnDDemo({
lineWrapping,
lineNumbers,
}) {
const editor = useMarkdownEditor(
{
initial: {
mode: 'wysiwyg',
markup,
},
wysiwygConfig: {
extensionOptions: {
codeBlock: {
lineWrapping: {
enabled: lineWrapping === 'enabled',
},
lineNumbers: {
enabled: lineNumbers !== 'disabled',
showByDefault: lineNumbers === 'default',
},
},
},
},
},
[lineWrapping, lineNumbers],
);

return (
<PlaygroundLayout
editor={editor}
view={({className}) => (
<MarkdownEditorView
autofocus
stickyToolbar
settingsVisible
editor={editor}
className={className}
/>
)}
/>
);
});
28 changes: 28 additions & 0 deletions demo/stories/examples/code-block/markup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const markup = `
&nbsp;

~~~js
import React from 'react';
import { useMarkdownEditor, MarkdownEditorView } from '@gravity-ui/markdown-editor';
import { toaster } from '@gravity-ui/uikit/toaster-singleton';

function Editor({ onSubmit }) {
const editor = useMarkdownEditor({ allowHTML: false });

React.useEffect(() => {
function submitHandler() {
// Serialize current content to markdown markup
const value = editor.getValue();
onSubmit(value);
}

editor.on('submit', submitHandler);
return () => {
editor.off('submit', submitHandler);
};
}, [onSubmit]);

return <MarkdownEditorView stickyToolbar autofocus editor={editor} />;
}
~~~
`;
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
.yfm.ProseMirror .hljs.show-line-numbers {
display: flex;
.g-md-editor.ProseMirror {
&.yfm pre > code {
// disable line wrapping
white-space: pre;
}

white-space: pre;
}
pre > code {
&.wrap {
// enable line wrapping
/* stylelint-disable */
white-space: pre-wrap;
white-space: break-spaces;
/* stylelint-enable */

.yfm-line-number.ProseMirror-widget {
position: absolute;
// inline padding in code element
left: 16px;

padding: 0;
}
}

&.show-line-numbers {
position: relative;

display: flex;
}

.yfm.ProseMirror pre > code > .yfm-line-numbers > .yfm-line-number {
display: block;
.fake-line-number {
opacity: 0;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ import {
codeBlockType,
} from '../CodeBlockSpecs';

import {CodeBlockNodeView} from './CodeBlockNodeView';
import {codeLangSelectTooltipViewCreator} from './TooltipPlugin';
import {PlainTextLang} from './const';
import {codeBlockLineNumbersPlugin} from './plugins/codeBlockLineNumbersPlugin';
import {codeBlockLineWrappingPlugin} from './plugins/codeBlockLineWrappingPlugin';

import './CodeBlockHighlight.scss';

Expand All @@ -36,6 +40,9 @@ type LangSelectItem = {
const key = new PluginKey<DecorationSet>('code_block_highlight');

export type CodeBlockHighlightOptions = {
lineWrapping?: {
enabled?: boolean;
};
lineNumbers?: LineNumbersOptions;
langs?: HighlightLangMap;
};
Expand All @@ -62,10 +69,14 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
}
};

if (opts.lineWrapping?.enabled) builder.addPlugin(codeBlockLineWrappingPlugin);
if (opts.lineNumbers?.enabled) builder.addPlugin(codeBlockLineNumbersPlugin);

builder.addPlugin(() => {
let modulesLoaded = false;
let view: EditorView | null = null;

// empty array by default, but is filled after loading modules
const selectItems: LangSelectItem[] = [];
const mapping: Record<string, string> = {};

Expand All @@ -92,6 +103,8 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
}
}

selectItems.sort(sortLangs);

if (view && !view.isDestroyed) {
view.dispatch(view.state.tr.setMeta(key, {modulesLoaded}));
}
Expand Down Expand Up @@ -154,87 +167,17 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
},
view: (v) => {
view = v;
return codeLangSelectTooltipViewCreator(
view,
selectItems,
mapping,
Boolean(opts.lineNumbers?.enabled),
);
return codeLangSelectTooltipViewCreator(view, selectItems, mapping, {
showCodeWrapping: Boolean(opts.lineWrapping?.enabled),
showLineNumbers: Boolean(opts.lineNumbers?.enabled),
});
},
props: {
decorations: (state) => {
return key.getState(state);
},
nodeViews: {
[codeBlockNodeName]: (node) => {
let prevLang = node.attrs[CodeBlockNodeAttr.Lang];

const dom = document.createElement('pre');
updateDomAttribute(
dom,
CodeBlockNodeAttr.Line,
node.attrs[CodeBlockNodeAttr.Line],
);

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

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

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

let lineNumbersContainer: HTMLDivElement | undefined;
let prevLineCount = 0;

if (opts.lineNumbers?.enabled) {
const result = manageLineNumbers(node, code);
lineNumbersContainer = result.container;
prevLineCount = result.lineCount;
}

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

return {
dom,
contentDOM,
update(newNode) {
if (node.type !== newNode.type) return false;

const newLang = newNode.attrs[CodeBlockNodeAttr.Lang];
if (prevLang !== newLang) {
code.className = 'hljs';
updateDomAttribute(dom, CodeBlockNodeAttr.Lang, newLang);
if (newLang) {
code.classList.add(newLang);
}
prevLang = newLang;
}

updateDomAttribute(
dom,
CodeBlockNodeAttr.Line,
newNode.attrs[CodeBlockNodeAttr.Line],
);

if (opts.lineNumbers?.enabled) {
const result = manageLineNumbers(
newNode,
code,
lineNumbersContainer,
prevLineCount,
);
lineNumbersContainer = result.container;
prevLineCount = result.lineCount;
}

return true;
},
};
},
[codeBlockNodeName]: CodeBlockNodeView.withOpts(opts),
},
},
});
Expand Down Expand Up @@ -300,69 +243,9 @@ function stepHasFromTo(step: Step): step is Step & {from: number; to: number} {
return typeof step.from === 'number' && typeof step.to === 'number';
}

function updateDomAttribute(elem: Element, attr: string, value: string | null | undefined) {
if (value) {
elem.setAttribute(attr, value);
} else {
elem.removeAttribute(attr);
}
}

function manageLineNumbers(
node: Node,
code: HTMLElement,
prevContainer?: HTMLDivElement,
prevLineCount = 0,
): {container?: HTMLDivElement; lineCount: number} {
const showLineNumbers = node.attrs[CodeBlockNodeAttr.ShowLineNumbers] === 'true';

if (!showLineNumbers) {
if (prevContainer) {
code.removeChild(prevContainer);
code.classList.remove('show-line-numbers');
}
return {container: undefined, lineCount: 0};
}

const lines = node.textContent ? node.textContent.split('\n') : [''];
const currentLineCount = lines.length;

let container = prevContainer;
if (!container) {
container = document.createElement('div');
container.className = 'yfm-line-numbers';
container.contentEditable = 'false';
code.prepend(container);
}

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

if (currentLineCount !== prevLineCount) {
const maxDigits = String(currentLineCount).length;
const prevMaxDigits = String(prevLineCount).length;

if (currentLineCount > prevLineCount) {
for (let i = prevLineCount + 1; i <= currentLineCount; i++) {
const lineNumberElement = document.createElement('div');
lineNumberElement.className = 'yfm-line-number';
lineNumberElement.textContent = String(i).padStart(maxDigits, ' ');
container.appendChild(lineNumberElement);
}
} else if (currentLineCount < prevLineCount) {
for (let i = prevLineCount; i > currentLineCount; i--) {
if (container.lastChild) {
container.removeChild(container.lastChild);
}
}
}

if (maxDigits !== prevMaxDigits) {
Array.from(container.children).forEach((lineNumber, index) => {
const lineNum = index + 1;
lineNumber.textContent = String(lineNum).padStart(maxDigits, ' ');
});
}
}

return {container, lineCount: currentLineCount};
function sortLangs(a: LangSelectItem, b: LangSelectItem): number {
// plaintext always goes first
if (a.value === PlainTextLang) return -1;
if (b.value === PlainTextLang) return 1;
return 0;
}
Loading
Loading