Skip to content

Commit

Permalink
feat: added support for an empty string (#505)
Browse files Browse the repository at this point in the history
  • Loading branch information
PMAWorks authored Dec 23, 2024
1 parent cc026a3 commit a3f8b50
Show file tree
Hide file tree
Showing 27 changed files with 326 additions and 33 deletions.
3 changes: 3 additions & 0 deletions demo/components/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type PlaygroundProps = {
allowHTML?: boolean;
settingsVisible?: boolean;
initialEditor?: MarkdownEditorMode;
preserveEmptyRows?: boolean;
breaks?: boolean;
linkify?: boolean;
linkifyTlds?: string | string[];
Expand Down Expand Up @@ -115,6 +116,7 @@ export const Playground = React.memo<PlaygroundProps>((props) => {
allowHTML,
breaks,
linkify,
preserveEmptyRows,
linkifyTlds,
sanitizeHtml,
prepareRawMarkup,
Expand Down Expand Up @@ -219,6 +221,7 @@ export const Playground = React.memo<PlaygroundProps>((props) => {
experimental: {
...experimental,
directiveSyntax,
preserveEmptyRows: preserveEmptyRows,
},
prepareRawMarkup: prepareRawMarkup
? (value) => '**prepare raw markup**\n\n' + value
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

0 comments on commit a3f8b50

Please sign in to comment.