Skip to content

Commit 59549eb

Browse files
authored
feat(core): added preserving and managing original markdown formatting for YfmTable (#558)
1 parent 0bf2af1 commit 59549eb

25 files changed

+1119
-68
lines changed

demo/stories/experiments/empty-row/EmptyRow.stories.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ export const Story: StoryObj<typeof component> = {
1010
Story.storyName = 'Preserve Empty Rows';
1111

1212
export default {
13-
title: 'Experiments',
13+
title: 'Experiments / Preserve Empty Rows',
1414
component,
1515
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type {StoryObj} from '@storybook/react';
2+
3+
import {StoreRawMarkupDemo as component} from './StoreRawMarkup';
4+
5+
export const Story: StoryObj<typeof component> = {
6+
args: {},
7+
};
8+
Story.storyName = 'Store Raw Markup';
9+
10+
export default {
11+
title: 'Experiments',
12+
component,
13+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import React, {useCallback, useLayoutEffect, useState} from 'react';
2+
3+
import {toaster} from '@gravity-ui/uikit/toaster-singleton-react-18';
4+
5+
import {MarkdownEditorView, type RenderPreview, useMarkdownEditor} from '../../../../src';
6+
import {PlaygroundLayout} from '../../../components/PlaygroundLayout';
7+
import {SplitModePreview} from '../../../components/SplitModePreview';
8+
import {plugins} from '../../../defaults/md-plugins';
9+
import {useMarkdownEditorValue} from '../../../hooks/useMarkdownEditorValue';
10+
11+
const initialMarkup = `
12+
## YFM Table
13+
### Simple table
14+
#|
15+
|| **Header1** | **Header2** ||
16+
|| Text | Text ||
17+
|#
18+
### Multiline content
19+
#|
20+
|| Text
21+
on two lines |
22+
- Potatoes
23+
- Carrot
24+
- Onion
25+
- Cucumber
26+
||
27+
|#
28+
29+
### Nested tables
30+
#|
31+
|| 1 | Text before other table
32+
33+
#|
34+
|| 5 | 6 ||
35+
|| 7 | 8 ||
36+
|#
37+
38+
Text after other table
39+
||
40+
|| 3 | 4 ||
41+
42+
|#
43+
44+
### Table inside quote
45+
> #|
46+
> || **Header1** | **Header2** ||
47+
> || Text | Text ||
48+
> |#
49+
50+
### Table inside tabs
51+
52+
{% list tabs %}
53+
54+
- tab1
55+
56+
#|
57+
|| **Header1** | **Header2** ||
58+
|| Text | Text ||
59+
|#
60+
61+
- tab2
62+
63+
#|
64+
|| Text
65+
on two lines |
66+
- Potatoes
67+
- Carrot
68+
- Onion
69+
- Cucumber
70+
||
71+
|#
72+
73+
74+
{% endlist %}
75+
`;
76+
77+
type StoreRawMarkupDemoProps = {
78+
preserveMarkupFormatting: boolean;
79+
};
80+
81+
export const StoreRawMarkupDemo = React.memo<StoreRawMarkupDemoProps>((props) => {
82+
const {preserveMarkupFormatting} = props;
83+
const [mdMarkup, setMdMarkup] = useState(initialMarkup);
84+
85+
const renderPreview = useCallback<RenderPreview>(
86+
({getValue, md}) => (
87+
<SplitModePreview
88+
getValue={getValue}
89+
allowHTML={md.html}
90+
linkify={md.linkify}
91+
linkifyTlds={md.linkifyTlds}
92+
breaks={md.breaks}
93+
needToSanitizeHtml
94+
plugins={plugins}
95+
/>
96+
),
97+
[],
98+
);
99+
100+
const editor = useMarkdownEditor(
101+
{
102+
initial: {markup: mdMarkup},
103+
markupConfig: {renderPreview},
104+
experimental: {preserveMarkupFormatting},
105+
},
106+
[],
107+
);
108+
109+
// for preserve edited content
110+
const value = useMarkdownEditorValue(editor);
111+
useLayoutEffect(() => {
112+
setMdMarkup(value);
113+
}, [value]);
114+
115+
return (
116+
<PlaygroundLayout
117+
editor={editor}
118+
view={({className}) => (
119+
<MarkdownEditorView
120+
autofocus
121+
stickyToolbar
122+
settingsVisible
123+
editor={editor}
124+
toaster={toaster}
125+
className={className}
126+
/>
127+
)}
128+
/>
129+
);
130+
});
131+
132+
StoreRawMarkupDemo.displayName = 'Experiments / StoreRawMarkup';

package-lock.json

+36-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,8 @@
199199
"react-error-boundary": "^3.1.4",
200200
"react-hotkeys-hook": "4.5.0",
201201
"react-use": "^17.3.2",
202-
"tslib": "^2.3.1"
202+
"tslib": "^2.3.1",
203+
"uuid": "11.0.5"
203204
},
204205
"devDependencies": {
205206
"@diplodoc/cut-extension": "^0.6.1",

src/bundle/Editor.ts

+9
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type WysiwygEditorOptions,
1313
} from '../core';
1414
import type {TransformFn} from '../core/markdown/ProseMirrorTransformer';
15+
import {DynamicModifiers} from '../core/types/dynamicModifiers';
1516
import {ReactRenderStorage, type RenderStorage} from '../extensions';
1617
import {i18n} from '../i18n/bundle';
1718
import {logger} from '../logger';
@@ -21,6 +22,8 @@ import {type CodeEditor, Editor as MarkupEditor} from '../markup/editor';
2122
import {type Emitter, FileUploadHandler, type Receiver, SafeEventEmitter} from '../utils';
2223
import type {DirectiveSyntaxContext} from '../utils/directive';
2324

25+
import {MarkupManager} from './MarkupManager';
26+
import {createDynamicModifiers} from './config/dynamicModifiers';
2427
import type {
2528
MarkdownEditorMode as EditorMode,
2629
MarkdownEditorPreset as EditorPreset,
@@ -144,6 +147,7 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
144147
#mdOptions: Readonly<MarkdownEditorMdOptions>;
145148
#pmTransformers: TransformFn[] = [];
146149
#preserveEmptyRows: boolean;
150+
#modifiers?: DynamicModifiers[];
147151

148152
readonly #preset: EditorPreset;
149153
#extensions?: WysiwygEditorOptions['extensions'];
@@ -254,6 +258,7 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
254258
initialContent: this.#markup,
255259
extensions: this.#extensions,
256260
pmTransformers: this.#pmTransformers,
261+
modifiers: this.#modifiers,
257262
allowHTML: this.#mdOptions.html,
258263
linkify: this.#mdOptions.linkify,
259264
linkifyTlds: this.#mdOptions.linkifyTlds,
@@ -332,6 +337,10 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
332337
wysiwygConfig = {},
333338
} = opts;
334339

340+
this.#modifiers = experimental.preserveMarkupFormatting
341+
? createDynamicModifiers(new MarkupManager())
342+
: undefined;
343+
335344
this.#editorMode = initial.mode ?? 'wysiwyg';
336345
this.#toolbarVisible = initial.toolbarVisible ?? true;
337346
this.#splitMode = (markupConfig.renderPreview && markupConfig.splitMode) ?? false;

src/bundle/MarkupManager.ts

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {Node} from 'prosemirror-model';
2+
import {v4} from 'uuid';
3+
4+
import {SafeEventEmitter} from '../utils';
5+
6+
export interface IMarkupManager {
7+
setMarkup(id: string, rawMarkup: string): void;
8+
setNode(id: string, node: Node): void;
9+
getMarkup(id: string): string | null;
10+
getNode(id: string): Node | null;
11+
reset(): void;
12+
on<K extends keyof Events>(event: K, listener: (value: Events[K]) => void): void;
13+
}
14+
15+
export interface Logger {
16+
log(message: string): void;
17+
error(message: string): void;
18+
}
19+
20+
interface Events {
21+
markupChanged: {id: string; rawMarkup: string};
22+
nodeChanged: {id: string; node: Node};
23+
reset: {};
24+
}
25+
26+
export class MarkupManager extends SafeEventEmitter<Events> implements IMarkupManager {
27+
private _markups: Map<string, string> = new Map();
28+
private _nodes: Map<string, Node> = new Map();
29+
private _namespace: string;
30+
31+
private readonly logger?: Logger;
32+
33+
constructor(logger?: Logger) {
34+
super();
35+
this.logger = logger;
36+
this._namespace = v4();
37+
}
38+
39+
/**
40+
* Set raw markup for a specific id
41+
*/
42+
setMarkup(id: string, rawMarkup: string): void {
43+
if (typeof rawMarkup !== 'string') {
44+
this.logger?.error('[MarkupManager] rawMarkup must be a string');
45+
return;
46+
}
47+
this._markups.set(id, rawMarkup);
48+
49+
this.emit('markupChanged', {id, rawMarkup});
50+
this.logger?.log(`[MarkupManager] Raw markup for ID ${id} set successfully`);
51+
}
52+
53+
/**
54+
* Set a node for a specific id
55+
*/
56+
setNode(id: string, node: Node): void {
57+
if (!node) {
58+
this.logger?.error('[MarkupManager] Node must be a valid ProseMirror Node');
59+
return;
60+
}
61+
this._nodes.set(id, node);
62+
63+
this.emit('nodeChanged', {id, node});
64+
this.logger?.log(`[MarkupManager] Node for ID ${id} set successfully`);
65+
}
66+
67+
/**
68+
* Get raw markup for a specific id
69+
*/
70+
getMarkup(id: string): string | null {
71+
return this._markups.get(id) ?? null;
72+
}
73+
74+
/**
75+
* Get a node for a specific id
76+
*/
77+
getNode(id: string): Node | null {
78+
return this._nodes.get(id) ?? null;
79+
}
80+
81+
getNamespace(): string {
82+
return this._namespace;
83+
}
84+
85+
/**
86+
* Reset the stored markups and nodes
87+
*/
88+
reset(): void {
89+
this._markups.clear();
90+
this._nodes.clear();
91+
92+
this.emit('reset', {});
93+
this.logger?.log('[MarkupManager] MarkupManager has been reset');
94+
}
95+
}

0 commit comments

Comments
 (0)