Skip to content

Commit a3f8b50

Browse files
authored
feat: added support for an empty string (#505)
1 parent cc026a3 commit a3f8b50

File tree

27 files changed

+326
-33
lines changed

27 files changed

+326
-33
lines changed

demo/components/Playground.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export type PlaygroundProps = {
6363
allowHTML?: boolean;
6464
settingsVisible?: boolean;
6565
initialEditor?: MarkdownEditorMode;
66+
preserveEmptyRows?: boolean;
6667
breaks?: boolean;
6768
linkify?: boolean;
6869
linkifyTlds?: string | string[];
@@ -115,6 +116,7 @@ export const Playground = React.memo<PlaygroundProps>((props) => {
115116
allowHTML,
116117
breaks,
117118
linkify,
119+
preserveEmptyRows,
118120
linkifyTlds,
119121
sanitizeHtml,
120122
prepareRawMarkup,
@@ -219,6 +221,7 @@ export const Playground = React.memo<PlaygroundProps>((props) => {
219221
experimental: {
220222
...experimental,
221223
directiveSyntax,
224+
preserveEmptyRows: preserveEmptyRows,
222225
},
223226
prepareRawMarkup: prepareRawMarkup
224227
? (value) => '**prepare raw markup**\n\n' + value

src/bundle/Editor.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import {EditorView as CMEditorView} from '@codemirror/view';
44
import {TextSelection} from 'prosemirror-state';
55
import {EditorView as PMEditorView} from 'prosemirror-view';
66

7+
import {TransformFn} from 'src/core/markdown/ProseMirrorTransformer';
8+
9+
import {getAutocompleteConfig} from '../../src/markup/codemirror/autocomplete';
710
import type {CommonEditor, MarkupString} from '../common';
811
import {
912
type ActionStorage,
@@ -124,6 +127,7 @@ export type EditorOptions = Pick<
124127
renderStorage: ReactRenderStorage;
125128
preset: EditorPreset;
126129
directiveSyntax: DirectiveSyntaxContext;
130+
pmTransformers: TransformFn[];
127131
};
128132

129133
/** @internal */
@@ -139,6 +143,8 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
139143
#markupConfig: MarkupConfig;
140144
#escapeConfig?: EscapeConfig;
141145
#mdOptions: Readonly<MarkdownEditorMdOptions>;
146+
#pmTransformers: TransformFn[] = [];
147+
#preserveEmptyRows: boolean;
142148

143149
readonly #preset: EditorPreset;
144150
#extensions?: WysiwygEditorOptions['extensions'];
@@ -248,6 +254,7 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
248254
mdPreset,
249255
initialContent: this.#markup,
250256
extensions: this.#extensions,
257+
pmTransformers: this.#pmTransformers,
251258
allowHTML: this.#mdOptions.html,
252259
linkify: this.#mdOptions.linkify,
253260
linkifyTlds: this.#mdOptions.linkifyTlds,
@@ -279,7 +286,12 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
279286
extensions: this.#markupConfig.extensions,
280287
disabledExtensions: this.#markupConfig.disabledExtensions,
281288
keymaps: this.#markupConfig.keymaps,
282-
yfmLangOptions: {languageData: this.#markupConfig.languageData},
289+
preserveEmptyRows: this.#preserveEmptyRows,
290+
yfmLangOptions: {
291+
languageData: getAutocompleteConfig({
292+
preserveEmptyRows: this.#preserveEmptyRows,
293+
}).concat(this.#markupConfig?.languageData || []),
294+
},
283295
autocompletion: this.#markupConfig.autocompletion,
284296
directiveSyntax: this.directiveSyntax,
285297
receiver: this,
@@ -330,6 +342,7 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
330342
this.#markup = initial.markup ?? '';
331343

332344
this.#preset = opts.preset ?? 'full';
345+
this.#pmTransformers = opts.pmTransformers;
333346
this.#mdOptions = md;
334347
this.#extensions = wysiwygConfig.extensions;
335348
this.#markupConfig = {...opts.markupConfig};
@@ -342,6 +355,7 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
342355
);
343356
this.#directiveSyntax = opts.directiveSyntax;
344357
this.#enableNewImageSizeCalculation = Boolean(experimental.enableNewImageSizeCalculation);
358+
this.#preserveEmptyRows = experimental.preserveEmptyRows || false;
345359
this.#prepareRawMarkup = experimental.prepareRawMarkup;
346360
this.#escapeConfig = wysiwygConfig.escapeConfig;
347361
this.#beforeEditorModeChange = experimental.beforeEditorModeChange;

src/bundle/config/action-names.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const names = [
2020
'heading4',
2121
'heading5',
2222
'heading6',
23+
'emptyRow',
2324
/** @deprecated use horizontalRule */
2425
'horizontalrule',
2526
'horizontalRule',

src/bundle/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@ export type MarkdownEditorExperimentalOptions = {
104104
* Default value is 'disabled'.
105105
*/
106106
directiveSyntax?: DirectiveSyntaxOption;
107+
/**
108+
* If we need support for empty strings
109+
*
110+
* @default false
111+
*/
112+
preserveEmptyRows?: boolean;
107113
};
108114

109115
export type MarkdownEditorMarkupConfig = {

src/bundle/useMarkdownEditor.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {useLayoutEffect, useMemo} from 'react';
22

33
import type {Extension} from '../core';
4+
import {getPMTransformers} from '../core/markdown/ProseMirrorTransformer/getTransformers';
45
import {ReactRenderStorage} from '../extensions';
56
import {logger} from '../logger';
67
import {DirectiveSyntaxContext} from '../utils/directive';
@@ -33,6 +34,7 @@ export function useMarkdownEditor<T extends object = {}>(
3334
} = props;
3435

3536
const breaks = md.breaks ?? props.breaks;
37+
const preserveEmptyRows = experimental.preserveEmptyRows;
3638
const preset: MarkdownEditorPreset = props.preset ?? 'full';
3739
const renderStorage = new ReactRenderStorage();
3840
const uploadFile = handlers.uploadFile ?? props.fileUploadHandler;
@@ -41,6 +43,10 @@ export function useMarkdownEditor<T extends object = {}>(
4143
props.needToSetDimensionsForUploadedImages;
4244
const enableNewImageSizeCalculation = experimental.enableNewImageSizeCalculation;
4345

46+
const pmTransformers = getPMTransformers({
47+
emptyRowTransformer: preserveEmptyRows,
48+
});
49+
4450
const directiveSyntax = new DirectiveSyntaxContext(experimental.directiveSyntax);
4551

4652
const extensions: Extension = (builder) => {
@@ -59,6 +65,7 @@ export function useMarkdownEditor<T extends object = {}>(
5965
editor.emit('submit', null);
6066
return true;
6167
},
68+
preserveEmptyRows: preserveEmptyRows,
6269
placeholderOptions: wysiwygConfig.placeholderOptions,
6370
mdBreaks: breaks,
6471
fileUploadHandler: uploadFile,
@@ -72,11 +79,13 @@ export function useMarkdownEditor<T extends object = {}>(
7279
}
7380
}
7481
};
82+
7583
return new EditorImpl({
7684
...props,
7785
preset,
7886
renderStorage,
7987
directiveSyntax,
88+
pmTransformers,
8089
md: {
8190
...md,
8291
breaks,

src/bundle/wysiwyg-preset.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export type BundlePresetOptions = ExtensionsOptions &
2626
EditorModeKeymapOptions & {
2727
preset: MarkdownEditorPreset;
2828
mdBreaks?: boolean;
29+
preserveEmptyRows?: boolean;
2930
fileUploadHandler?: FileUploadHandler;
3031
placeholderOptions?: WysiwygPlaceholderOptions;
3132
/**
@@ -81,6 +82,7 @@ export const BundlePreset: ExtensionAuto<BundlePresetOptions> = (builder, opts)
8182
? value()
8283
: value ?? i18nPlaceholder('doc_empty');
8384
},
85+
preserveEmptyRows: opts.preserveEmptyRows,
8486
...opts.baseSchema,
8587
},
8688
};

src/core/Editor.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {CommonEditor, ContentHandler, MarkupString} from '../common';
77
import type {ActionsManager} from './ActionsManager';
88
import {WysiwygContentHandler} from './ContentHandler';
99
import {ExtensionsManager} from './ExtensionsManager';
10+
import {TransformFn} from './markdown/ProseMirrorTransformer';
1011
import type {ActionStorage} from './types/actions';
1112
import type {Extension} from './types/extension';
1213
import type {Parser} from './types/parser';
@@ -30,6 +31,7 @@ export type WysiwygEditorOptions = {
3031
mdPreset?: PresetName;
3132
allowHTML?: boolean;
3233
linkify?: boolean;
34+
pmTransformers?: TransformFn[];
3335
linkifyTlds?: string | string[];
3436
escapeConfig?: EscapeConfig;
3537
/** Call on any state change (move cursor, change selection, etc...) */
@@ -74,6 +76,7 @@ export class WysiwygEditor implements CommonEditor, ActionStorage {
7476
allowHTML,
7577
mdPreset,
7678
linkify,
79+
pmTransformers,
7780
linkifyTlds,
7881
escapeConfig,
7982
onChange,
@@ -92,6 +95,7 @@ export class WysiwygEditor implements CommonEditor, ActionStorage {
9295
// "breaks" option only affects the renderer, but not the parser
9396
mdOpts: {html: allowHTML, linkify, breaks: true, preset: mdPreset},
9497
linkifyTlds,
98+
pmTransformers,
9599
});
96100

97101
const state = EditorState.create({

src/core/ExtensionsManager.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {ExtensionBuilder} from './ExtensionBuilder';
66
import {ParserTokensRegistry} from './ParserTokensRegistry';
77
import {SchemaSpecRegistry} from './SchemaSpecRegistry';
88
import {SerializerTokensRegistry} from './SerializerTokensRegistry';
9+
import {TransformFn} from './markdown/ProseMirrorTransformer';
910
import type {ActionSpec} from './types/actions';
1011
import type {
1112
Extension,
@@ -24,6 +25,7 @@ type ExtensionsManagerParams = {
2425
type ExtensionsManagerOptions = {
2526
mdOpts?: MarkdownIt.Options & {preset?: PresetName};
2627
linkifyTlds?: string | string[];
28+
pmTransformers?: TransformFn[];
2729
};
2830

2931
export class ExtensionsManager {
@@ -38,6 +40,8 @@ export class ExtensionsManager {
3840
#nodeViewCreators = new Map<string, (deps: ExtensionDeps) => NodeViewConstructor>();
3941
#markViewCreators = new Map<string, (deps: ExtensionDeps) => MarkViewConstructor>();
4042

43+
#pmTransformers: TransformFn[] = [];
44+
4145
#mdForMarkup: MarkdownIt;
4246
#mdForText: MarkdownIt;
4347
#extensions: Extension;
@@ -62,6 +66,10 @@ export class ExtensionsManager {
6266
this.#mdForText.linkify.tlds(options.linkifyTlds, true);
6367
}
6468

69+
if (options.pmTransformers) {
70+
this.#pmTransformers = options.pmTransformers;
71+
}
72+
6573
// TODO: add prefilled context
6674
this.#builder = new ExtensionBuilder();
6775
}
@@ -118,8 +126,16 @@ export class ExtensionsManager {
118126
this.#deps = {
119127
schema,
120128
actions: new ActionsManager(),
121-
markupParser: this.#parserRegistry.createParser(schema, this.#mdForMarkup),
122-
textParser: this.#parserRegistry.createParser(schema, this.#mdForText),
129+
markupParser: this.#parserRegistry.createParser(
130+
schema,
131+
this.#mdForMarkup,
132+
this.#pmTransformers,
133+
),
134+
textParser: this.#parserRegistry.createParser(
135+
schema,
136+
this.#mdForText,
137+
this.#pmTransformers,
138+
),
123139
serializer: this.#serializerRegistry.createSerializer(),
124140
};
125141
}

src/core/ParserTokensRegistry.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type MarkdownIt from 'markdown-it';
22
import type {Schema} from 'prosemirror-model';
33

44
import {MarkdownParser} from './markdown/MarkdownParser';
5+
import {TransformFn} from './markdown/ProseMirrorTransformer';
56
import type {Parser, ParserToken} from './types/parser';
67

78
export class ParserTokensRegistry {
@@ -12,7 +13,7 @@ export class ParserTokensRegistry {
1213
return this;
1314
}
1415

15-
createParser(schema: Schema, tokenizer: MarkdownIt): Parser {
16-
return new MarkdownParser(schema, tokenizer, this.#tokens);
16+
createParser(schema: Schema, tokenizer: MarkdownIt, pmTransformers: TransformFn[]): Parser {
17+
return new MarkdownParser(schema, tokenizer, this.#tokens, pmTransformers);
1718
}
1819
}

src/core/markdown/Markdown.test.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,28 @@ import {MarkdownSerializer} from './MarkdownSerializer';
1414

1515
const {schema} = builder;
1616
schema.nodes['hard_break'].spec.isBreak = true;
17-
const parser: Parser = new MarkdownParser(schema, new MarkdownIt('commonmark'), {
18-
paragraph: {type: 'block', name: 'paragraph'},
19-
heading: {
20-
type: 'block',
21-
name: 'heading',
22-
getAttrs: (tok) => ({level: Number(tok.tag.slice(1))}),
17+
const parser: Parser = new MarkdownParser(
18+
schema,
19+
new MarkdownIt('commonmark'),
20+
{
21+
paragraph: {type: 'block', name: 'paragraph'},
22+
heading: {
23+
type: 'block',
24+
name: 'heading',
25+
getAttrs: (tok) => ({level: Number(tok.tag.slice(1))}),
26+
},
27+
list_item: {type: 'block', name: 'list_item'},
28+
bullet_list: {type: 'block', name: 'bullet_list'},
29+
ordered_list: {type: 'block', name: 'ordered_list'},
30+
hardbreak: {type: 'node', name: 'hard_break'},
31+
fence: {type: 'block', name: 'code_block', noCloseToken: true},
32+
33+
em: {type: 'mark', name: 'em'},
34+
strong: {type: 'mark', name: 'strong'},
35+
code_inline: {type: 'mark', name: 'code', noCloseToken: true},
2336
},
24-
list_item: {type: 'block', name: 'list_item'},
25-
bullet_list: {type: 'block', name: 'bullet_list'},
26-
ordered_list: {type: 'block', name: 'ordered_list'},
27-
hardbreak: {type: 'node', name: 'hard_break'},
28-
fence: {type: 'block', name: 'code_block', noCloseToken: true},
29-
30-
em: {type: 'mark', name: 'em'},
31-
strong: {type: 'mark', name: 'strong'},
32-
code_inline: {type: 'mark', name: 'code', noCloseToken: true},
33-
});
37+
[],
38+
);
3439
const serializer = new MarkdownSerializer(
3540
{
3641
text: ((state, node) => {

0 commit comments

Comments
 (0)