Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added support for an empty string #505

Merged
merged 21 commits into from
Dec 23, 2024
Merged
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 @@ -46,7 +46,7 @@

const b = block('playground');
const fileUploadHandler: FileUploadHandler = async (file) => {
console.info('[Playground] Uploading file: ' + file.name);

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

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
await randomDelay(1000, 3000);
return {url: URL.createObjectURL(file)};
};
Expand All @@ -63,6 +63,7 @@
allowHTML?: boolean;
settingsVisible?: boolean;
initialEditor?: MarkdownEditorMode;
preserveEmptyRows?: boolean;
d3m1d0v marked this conversation as resolved.
Show resolved Hide resolved
breaks?: boolean;
linkify?: boolean;
linkifyTlds?: string | string[];
Expand Down Expand Up @@ -101,8 +102,8 @@
>;

logger.setLogger({
metrics: console.info,

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

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
action: (data) => console.info(`Action: ${data.action}`, data),

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

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
...console,
});

Expand All @@ -115,6 +116,7 @@
allowHTML,
breaks,
linkify,
preserveEmptyRows,
linkifyTlds,
sanitizeHtml,
prepareRawMarkup,
Expand Down Expand Up @@ -147,7 +149,7 @@
}, [mdRaw]);

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

Check warning on line 152 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 140 column 9
<SplitModePreview
getValue={getValue}
allowHTML={md.html}
Expand Down Expand Up @@ -219,6 +221,7 @@
experimental: {
...experimental,
directiveSyntax,
preserveEmptyRows: preserveEmptyRows,
},
prepareRawMarkup: prepareRawMarkup
? (value) => '**prepare raw markup**\n\n' + value
Expand Down Expand Up @@ -271,14 +274,14 @@
setEditorMode(mode);
}
const onToolbarAction = ({id, editorMode: type}: ToolbarActionData) => {
console.info(`The '${id}' action is performed in the ${type}-editor.`);

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

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
};
function onChangeSplitModeEnabled({splitModeEnabled}: {splitModeEnabled: boolean}) {
props.onChangeSplitModeEnabled?.(splitModeEnabled);
console.info(`Split mode enabled: ${splitModeEnabled}`);

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

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
}
function onChangeToolbarVisibility({visible}: {visible: boolean}) {
console.info('Toolbar visible: ' + visible);

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

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
}

mdEditor.on('cancel', onCancel);
Expand All @@ -298,7 +301,7 @@
mdEditor.off('change-split-mode-enabled', onChangeSplitModeEnabled);
mdEditor.off('change-toolbar-visibility', onChangeToolbarVisibility);
};
}, [mdEditor]);

Check warning on line 304 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 (
<div className={b()}>
Expand Down
16 changes: 15 additions & 1 deletion src/bundle/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import {EditorView as CMEditorView} from '@codemirror/view';
import {TextSelection} from 'prosemirror-state';
import {EditorView as PMEditorView} from 'prosemirror-view';

import {TransformFn} from 'src/core/markdown/ProseMirrorTransformer';

import {getAutocompleteConfig} from '../../src/markup/codemirror/autocomplete';
import type {CommonEditor, MarkupString} from '../common';
import {
type ActionStorage,
Expand Down Expand Up @@ -124,6 +127,7 @@ export type EditorOptions = Pick<
renderStorage: ReactRenderStorage;
preset: EditorPreset;
directiveSyntax: DirectiveSyntaxContext;
pmTransformers: TransformFn[];
};

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

readonly #preset: EditorPreset;
#extensions?: WysiwygEditorOptions['extensions'];
Expand Down Expand Up @@ -248,6 +254,7 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
mdPreset,
initialContent: this.#markup,
extensions: this.#extensions,
pmTransformers: this.#pmTransformers,
allowHTML: this.#mdOptions.html,
linkify: this.#mdOptions.linkify,
linkifyTlds: this.#mdOptions.linkifyTlds,
Expand Down Expand Up @@ -279,7 +286,12 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
extensions: this.#markupConfig.extensions,
disabledExtensions: this.#markupConfig.disabledExtensions,
keymaps: this.#markupConfig.keymaps,
yfmLangOptions: {languageData: this.#markupConfig.languageData},
preserveEmptyRows: this.#preserveEmptyRows,
yfmLangOptions: {
languageData: getAutocompleteConfig({
preserveEmptyRows: this.#preserveEmptyRows,
}).concat(this.#markupConfig?.languageData || []),
},
autocompletion: this.#markupConfig.autocompletion,
directiveSyntax: this.directiveSyntax,
receiver: this,
Expand Down Expand Up @@ -330,6 +342,7 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
this.#markup = initial.markup ?? '';

this.#preset = opts.preset ?? 'full';
this.#pmTransformers = opts.pmTransformers;
this.#mdOptions = md;
this.#extensions = wysiwygConfig.extensions;
this.#markupConfig = {...opts.markupConfig};
Expand All @@ -342,6 +355,7 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
);
this.#directiveSyntax = opts.directiveSyntax;
this.#enableNewImageSizeCalculation = Boolean(experimental.enableNewImageSizeCalculation);
this.#preserveEmptyRows = experimental.preserveEmptyRows || false;
this.#prepareRawMarkup = experimental.prepareRawMarkup;
this.#escapeConfig = wysiwygConfig.escapeConfig;
this.#beforeEditorModeChange = experimental.beforeEditorModeChange;
Expand Down
1 change: 1 addition & 0 deletions src/bundle/config/action-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const names = [
'heading4',
'heading5',
'heading6',
'emptyRow',
/** @deprecated use horizontalRule */
'horizontalrule',
'horizontalRule',
Expand Down
6 changes: 6 additions & 0 deletions src/bundle/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ export type MarkdownEditorExperimentalOptions = {
* Default value is 'disabled'.
*/
directiveSyntax?: DirectiveSyntaxOption;
/**
* If we need support for empty strings
*
* @default false
*/
preserveEmptyRows?: boolean;
};

export type MarkdownEditorMarkupConfig = {
Expand Down
9 changes: 9 additions & 0 deletions src/bundle/useMarkdownEditor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {useLayoutEffect, useMemo} from 'react';

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

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

const pmTransformers = getPMTransformers({
emptyRowTransformer: preserveEmptyRows,
});

const directiveSyntax = new DirectiveSyntaxContext(experimental.directiveSyntax);

const extensions: Extension = (builder) => {
Expand All @@ -59,6 +65,7 @@ export function useMarkdownEditor<T extends object = {}>(
editor.emit('submit', null);
return true;
},
preserveEmptyRows: preserveEmptyRows,
placeholderOptions: wysiwygConfig.placeholderOptions,
mdBreaks: breaks,
fileUploadHandler: uploadFile,
Expand All @@ -72,11 +79,13 @@ export function useMarkdownEditor<T extends object = {}>(
}
}
};

return new EditorImpl({
...props,
preset,
renderStorage,
directiveSyntax,
pmTransformers,
md: {
...md,
breaks,
Expand Down
2 changes: 2 additions & 0 deletions src/bundle/wysiwyg-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type BundlePresetOptions = ExtensionsOptions &
EditorModeKeymapOptions & {
preset: MarkdownEditorPreset;
mdBreaks?: boolean;
preserveEmptyRows?: boolean;
fileUploadHandler?: FileUploadHandler;
placeholderOptions?: WysiwygPlaceholderOptions;
/**
Expand Down Expand Up @@ -81,6 +82,7 @@ export const BundlePreset: ExtensionAuto<BundlePresetOptions> = (builder, opts)
? value()
: value ?? i18nPlaceholder('doc_empty');
},
preserveEmptyRows: opts.preserveEmptyRows,
...opts.baseSchema,
},
};
Expand Down
4 changes: 4 additions & 0 deletions src/core/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {CommonEditor, ContentHandler, MarkupString} from '../common';
import type {ActionsManager} from './ActionsManager';
import {WysiwygContentHandler} from './ContentHandler';
import {ExtensionsManager} from './ExtensionsManager';
import {TransformFn} from './markdown/ProseMirrorTransformer';
import type {ActionStorage} from './types/actions';
import type {Extension} from './types/extension';
import type {Parser} from './types/parser';
Expand All @@ -30,6 +31,7 @@ export type WysiwygEditorOptions = {
mdPreset?: PresetName;
allowHTML?: boolean;
linkify?: boolean;
pmTransformers?: TransformFn[];
linkifyTlds?: string | string[];
escapeConfig?: EscapeConfig;
/** Call on any state change (move cursor, change selection, etc...) */
Expand Down Expand Up @@ -74,6 +76,7 @@ export class WysiwygEditor implements CommonEditor, ActionStorage {
allowHTML,
mdPreset,
linkify,
pmTransformers,
linkifyTlds,
escapeConfig,
onChange,
Expand All @@ -92,6 +95,7 @@ export class WysiwygEditor implements CommonEditor, ActionStorage {
// "breaks" option only affects the renderer, but not the parser
mdOpts: {html: allowHTML, linkify, breaks: true, preset: mdPreset},
linkifyTlds,
pmTransformers,
});

const state = EditorState.create({
Expand Down
20 changes: 18 additions & 2 deletions src/core/ExtensionsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {ExtensionBuilder} from './ExtensionBuilder';
import {ParserTokensRegistry} from './ParserTokensRegistry';
import {SchemaSpecRegistry} from './SchemaSpecRegistry';
import {SerializerTokensRegistry} from './SerializerTokensRegistry';
import {TransformFn} from './markdown/ProseMirrorTransformer';
import type {ActionSpec} from './types/actions';
import type {
Extension,
Expand All @@ -24,6 +25,7 @@ type ExtensionsManagerParams = {
type ExtensionsManagerOptions = {
mdOpts?: MarkdownIt.Options & {preset?: PresetName};
linkifyTlds?: string | string[];
pmTransformers?: TransformFn[];
};

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

#pmTransformers: TransformFn[] = [];

#mdForMarkup: MarkdownIt;
#mdForText: MarkdownIt;
#extensions: Extension;
Expand All @@ -62,6 +66,10 @@ export class ExtensionsManager {
this.#mdForText.linkify.tlds(options.linkifyTlds, true);
}

if (options.pmTransformers) {
this.#pmTransformers = options.pmTransformers;
}

// TODO: add prefilled context
this.#builder = new ExtensionBuilder();
}
Expand Down Expand Up @@ -118,8 +126,16 @@ export class ExtensionsManager {
this.#deps = {
schema,
actions: new ActionsManager(),
markupParser: this.#parserRegistry.createParser(schema, this.#mdForMarkup),
textParser: this.#parserRegistry.createParser(schema, this.#mdForText),
markupParser: this.#parserRegistry.createParser(
schema,
this.#mdForMarkup,
this.#pmTransformers,
),
textParser: this.#parserRegistry.createParser(
schema,
this.#mdForText,
this.#pmTransformers,
),
serializer: this.#serializerRegistry.createSerializer(),
};
}
Expand Down
5 changes: 3 additions & 2 deletions src/core/ParserTokensRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type MarkdownIt from 'markdown-it';
import type {Schema} from 'prosemirror-model';

import {MarkdownParser} from './markdown/MarkdownParser';
import {TransformFn} from './markdown/ProseMirrorTransformer';
import type {Parser, ParserToken} from './types/parser';

export class ParserTokensRegistry {
Expand All @@ -12,7 +13,7 @@ export class ParserTokensRegistry {
return this;
}

createParser(schema: Schema, tokenizer: MarkdownIt): Parser {
return new MarkdownParser(schema, tokenizer, this.#tokens);
createParser(schema: Schema, tokenizer: MarkdownIt, pmTransformers: TransformFn[]): Parser {
return new MarkdownParser(schema, tokenizer, this.#tokens, pmTransformers);
}
}
37 changes: 21 additions & 16 deletions src/core/markdown/Markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,28 @@ import {MarkdownSerializer} from './MarkdownSerializer';

const {schema} = builder;
schema.nodes['hard_break'].spec.isBreak = true;
const parser: Parser = new MarkdownParser(schema, new MarkdownIt('commonmark'), {
paragraph: {type: 'block', name: 'paragraph'},
heading: {
type: 'block',
name: 'heading',
getAttrs: (tok) => ({level: Number(tok.tag.slice(1))}),
const parser: Parser = new MarkdownParser(
schema,
new MarkdownIt('commonmark'),
{
paragraph: {type: 'block', name: 'paragraph'},
heading: {
type: 'block',
name: 'heading',
getAttrs: (tok) => ({level: Number(tok.tag.slice(1))}),
},
list_item: {type: 'block', name: 'list_item'},
bullet_list: {type: 'block', name: 'bullet_list'},
ordered_list: {type: 'block', name: 'ordered_list'},
hardbreak: {type: 'node', name: 'hard_break'},
fence: {type: 'block', name: 'code_block', noCloseToken: true},

em: {type: 'mark', name: 'em'},
strong: {type: 'mark', name: 'strong'},
code_inline: {type: 'mark', name: 'code', noCloseToken: true},
},
list_item: {type: 'block', name: 'list_item'},
bullet_list: {type: 'block', name: 'bullet_list'},
ordered_list: {type: 'block', name: 'ordered_list'},
hardbreak: {type: 'node', name: 'hard_break'},
fence: {type: 'block', name: 'code_block', noCloseToken: true},

em: {type: 'mark', name: 'em'},
strong: {type: 'mark', name: 'strong'},
code_inline: {type: 'mark', name: 'code', noCloseToken: true},
});
[],
);
const serializer = new MarkdownSerializer(
{
text: ((state, node) => {
Expand Down
15 changes: 10 additions & 5 deletions src/core/markdown/MarkdownParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ import type {Parser} from '../types/parser';
import {MarkdownParser} from './MarkdownParser';

const md = MarkdownIt('commonmark', {html: false, breaks: true});
const testParser: Parser = new MarkdownParser(schema, md, {
blockquote: {name: 'blockquote', type: 'block', ignore: true},
paragraph: {type: 'block', name: 'paragraph'},
softbreak: {type: 'node', name: 'hard_break'},
});
const testParser: Parser = new MarkdownParser(
schema,
md,
{
blockquote: {name: 'blockquote', type: 'block', ignore: true},
paragraph: {type: 'block', name: 'paragraph'},
softbreak: {type: 'node', name: 'hard_break'},
},
[],
);

function parseWith(parser: Parser) {
return (text: string, node: Node) => {
Expand Down
17 changes: 14 additions & 3 deletions src/core/markdown/MarkdownParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {Mark, MarkType, Node, NodeType, Schema} from 'prosemirror-model';
import {logger} from '../../logger';
import type {Parser, ParserToken} from '../types/parser';

import {ProseMirrorTransformer, TransformFn} from './ProseMirrorTransformer';

type TokenAttrs = {[name: string]: unknown};

const openSuffix = '_open';
Expand All @@ -22,12 +24,19 @@ export class MarkdownParser implements Parser {
marks: readonly Mark[];
tokens: Record<string, ParserToken>;
tokenizer: MarkdownIt;

constructor(schema: Schema, tokenizer: MarkdownIt, tokens: Record<string, ParserToken>) {
pmTransformers: TransformFn[];

constructor(
schema: Schema,
tokenizer: MarkdownIt,
tokens: Record<string, ParserToken>,
pmTransformers: TransformFn[],
) {
this.schema = schema;
this.marks = Mark.none;
this.tokens = tokens;
this.tokenizer = tokenizer;
this.pmTransformers = pmTransformers;
}

validateLink(url: string): boolean {
Expand Down Expand Up @@ -69,7 +78,9 @@ export class MarkdownParser implements Parser {
doc = this.closeNode();
} while (this.stack.length);

return (doc || this.schema.topNodeType.createAndFill()) as Node;
const pmTransformer = new ProseMirrorTransformer(this.pmTransformers);

return doc ? pmTransformer.transform(doc) : this.schema.topNodeType.createAndFill()!;
} finally {
logger.metrics({component: 'parser', event: 'parse', duration: Date.now() - time});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {TransformFn} from './index';

export const transformEmptyParagraph: TransformFn = (node) => {
if (node.type !== 'paragraph') return;
if (node.content?.length !== 1) return;
if (node.content[0]?.type !== 'text') return;
if (node.content[0].text === String.fromCharCode(160)) delete node.content;
};
Loading
Loading