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 (core): added support for an empty string #505

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions demo/components/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@

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

Check warning on line 50 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 Down Expand Up @@ -77,6 +77,7 @@
allowHTML?: boolean;
settingsVisible?: boolean;
initialEditor?: MarkdownEditorMode;
preserveEmptyRows?: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add preserveEmptyRows to the following line in Playground.tsx to enable rerendering in the demo.

breaks?: boolean;
linkify?: boolean;
linkifyTlds?: string | string[];
Expand Down Expand Up @@ -114,8 +115,8 @@
>;

logger.setLogger({
metrics: console.info,

Check warning on line 118 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 119 in demo/components/Playground.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
...console,
});

Expand All @@ -128,6 +129,7 @@
allowHTML,
breaks,
linkify,
preserveEmptyRows,
linkifyTlds,
sanitizeHtml,
prepareRawMarkup,
Expand Down Expand Up @@ -159,7 +161,7 @@
}, [mdRaw]);

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

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

Check warning on line 290 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 294 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 297 in demo/components/Playground.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
}

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

Check warning on line 317 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
8 changes: 7 additions & 1 deletion src/bundle/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {EditorView as CMEditorView} from '@codemirror/view';
import {TextSelection} from 'prosemirror-state';
import {EditorView as PMEditorView} from 'prosemirror-view';

import {getAutocompleteConfig} from '../../src/markup/codemirror/autocomplete';
import type {CommonEditor, MarkupString} from '../common';
import {
type ActionStorage,
Expand Down Expand Up @@ -248,6 +249,7 @@ export class EditorImpl extends SafeEventEmitter<EventMapInt> implements EditorI
mdPreset,
initialContent: this.#markup,
extensions: this.#extensions,
pmTransformers: this.#mdOptions.pmTransformers,
allowHTML: this.#mdOptions.html,
linkify: this.#mdOptions.linkify,
linkifyTlds: this.#mdOptions.linkifyTlds,
Expand Down Expand Up @@ -279,7 +281,11 @@ 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},
yfmLangOptions: {
languageData: getAutocompleteConfig({
preserveEmptyRows: this.#mdOptions.preserveEmptyRows,
}),
},
Comment on lines -282 to +288
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should concat languageData config from this.#markupConfig.languageData and result of call getAutocompleteConfig() together

autocompletion: this.#markupConfig.autocompletion,
directiveSyntax: this.directiveSyntax,
receiver: this,
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 @@ -14,6 +14,7 @@ const names = [
'heading4',
'heading5',
'heading6',
'emptyRow',
'bulletList',
'orderedList',
'liftListItem',
Expand Down
4 changes: 4 additions & 0 deletions src/bundle/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import type {ReactNode} from 'react';

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

import type {MarkupString} from '../common';
import type {EscapeConfig, Extension} from '../core';
import type {CreateCodemirrorParams, YfmLangOptions} from '../markup';
Expand Down Expand Up @@ -39,9 +41,11 @@ export type WysiwygPlaceholderOptions = {

export type MarkdownEditorMdOptions = {
html?: boolean;
preserveEmptyRows?: boolean;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this setting to experiment options

breaks?: boolean;
linkify?: boolean;
linkifyTlds?: string | string[];
pmTransformers?: TransformFn[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this field not needed anymore?

};

export type MarkdownEditorInitialOptions = {
Expand Down
8 changes: 8 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 = md.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 @@ -83,6 +90,7 @@ export function useMarkdownEditor<T extends object = {}>(
html: md.html ?? props.allowHTML,
linkify: md.linkify ?? props.linkify,
linkifyTlds: md.linkifyTlds ?? props.linkifyTlds,
pmTransformers,
},
initial: {
...initial,
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
5 changes: 4 additions & 1 deletion 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 @@ -90,7 +93,7 @@ export class WysiwygEditor implements CommonEditor, ActionStorage {
actions,
} = ExtensionsManager.process(extensions, {
// "breaks" option only affects the renderer, but not the parser
mdOpts: {html: allowHTML, linkify, breaks: true, preset: mdPreset},
mdOpts: {pmTransformers, html: allowHTML, linkify, breaks: true, preset: mdPreset},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mdOpts – its about markdown-it options. pmTransformers – he's not one of them. put pmTransformers similarly as linkifyTlds

linkifyTlds,
});

Expand Down
21 changes: 18 additions & 3 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 @@ -22,7 +23,7 @@ type ExtensionsManagerParams = {
};

type ExtensionsManagerOptions = {
mdOpts?: MarkdownIt.Options & {preset?: PresetName};
mdOpts?: MarkdownIt.Options & {preset?: PresetName; pmTransformers?: TransformFn[]};
linkifyTlds?: string | string[];
};

Expand All @@ -38,6 +39,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 +65,10 @@ export class ExtensionsManager {
this.#mdForText.linkify.tlds(options.linkifyTlds, true);
}

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

// TODO: add prefilled context
this.#builder = new ExtensionBuilder();
}
Expand Down Expand Up @@ -118,8 +125,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
8 changes: 8 additions & 0 deletions src/core/markdown/ProseMirrorTransformer/emptyRowParser.ts
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;
};
20 changes: 20 additions & 0 deletions src/core/markdown/ProseMirrorTransformer/getTransformers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// TODO: add a new method to the ExtensionBuilder
import {transformEmptyParagraph} from './emptyRowParser';

import {TransformFn} from '.';

type GetTransformersProps = {
emptyRowTransformer?: boolean;
};

type GetPMTransformersType = (config: GetTransformersProps) => TransformFn[];

export const getPMTransformers: GetPMTransformersType = ({emptyRowTransformer}) => {
const transformers = [];

if (emptyRowTransformer) {
transformers.push(transformEmptyParagraph);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add today: add a new method to the ExtensionBuilder

}

return transformers;
};
Loading
Loading