From 533d944775f1254a53ccbb4645eefb84f7f09620 Mon Sep 17 00:00:00 2001 From: Mikhail Pavlovich Date: Mon, 16 Dec 2024 17:42:44 +0300 Subject: [PATCH] fix PR --- demo/components/Playground.tsx | 6 +- src/bundle/Editor.ts | 2 +- src/bundle/types.ts | 2 +- src/bundle/useMarkdownEditor.ts | 8 +- src/bundle/wysiwyg-preset.ts | 4 +- .../ProseMirrorTransformer/getTransformers.ts | 1 + .../base/BaseSchema/BaseSchemaSpecs/index.ts | 17 ++- .../codemirror/autocomplete/directive.ts | 109 ------------------ src/markup/codemirror/autocomplete/index.ts | 11 +- src/markup/codemirror/yfm.ts | 102 +++++++++++++++- src/markup/commands/helpers.ts | 24 +--- 11 files changed, 136 insertions(+), 150 deletions(-) delete mode 100644 src/markup/codemirror/autocomplete/directive.ts diff --git a/demo/components/Playground.tsx b/demo/components/Playground.tsx index 0a84a6ef..56716cb8 100644 --- a/demo/components/Playground.tsx +++ b/demo/components/Playground.tsx @@ -76,7 +76,7 @@ export type PlaygroundProps = { allowHTML?: boolean; settingsVisible?: boolean; initialEditor?: MarkdownEditorMode; - allowEmptyRows?: boolean; + preserveEmptyRows?: boolean; breaks?: boolean; linkify?: boolean; linkifyTlds?: string | string[]; @@ -127,7 +127,7 @@ export const Playground = React.memo((props) => { allowHTML, breaks, linkify, - allowEmptyRows, + preserveEmptyRows, linkifyTlds, sanitizeHtml, prepareRawMarkup, @@ -192,7 +192,7 @@ export const Playground = React.memo((props) => { directiveSyntax, }, md: { - allowEmptyRows: allowEmptyRows, + preserveEmptyRows: preserveEmptyRows, }, prepareRawMarkup: prepareRawMarkup ? (value) => '**prepare raw markup**\n\n' + value diff --git a/src/bundle/Editor.ts b/src/bundle/Editor.ts index fbe788e2..1471b47c 100644 --- a/src/bundle/Editor.ts +++ b/src/bundle/Editor.ts @@ -283,7 +283,7 @@ export class EditorImpl extends SafeEventEmitter implements EditorI keymaps: this.#markupConfig.keymaps, yfmLangOptions: { languageData: getAutocompleteConfig({ - allowEmptyRows: this.#mdOptions.allowEmptyRows, + preserveEmptyRows: this.#mdOptions.preserveEmptyRows, }), }, autocompletion: this.#markupConfig.autocompletion, diff --git a/src/bundle/types.ts b/src/bundle/types.ts index 7b83983c..17e005db 100644 --- a/src/bundle/types.ts +++ b/src/bundle/types.ts @@ -30,7 +30,7 @@ export type ParseInsertedUrlAsImage = (text: string) => {imageUrl: string; title export type MarkdownEditorMdOptions = { html?: boolean; - allowEmptyRows?: boolean; + preserveEmptyRows?: boolean; breaks?: boolean; linkify?: boolean; linkifyTlds?: string | string[]; diff --git a/src/bundle/useMarkdownEditor.ts b/src/bundle/useMarkdownEditor.ts index 8eb90ca9..243004e8 100644 --- a/src/bundle/useMarkdownEditor.ts +++ b/src/bundle/useMarkdownEditor.ts @@ -34,7 +34,7 @@ export function useMarkdownEditor( } = props; const breaks = md.breaks ?? props.breaks; - const allowEmptyRows = md.allowEmptyRows; + const preserveEmptyRows = md.preserveEmptyRows; const preset: MarkdownEditorPreset = props.preset ?? 'full'; const renderStorage = new ReactRenderStorage(); const uploadFile = handlers.uploadFile ?? props.fileUploadHandler; @@ -44,7 +44,7 @@ export function useMarkdownEditor( const enableNewImageSizeCalculation = experimental.enableNewImageSizeCalculation; const pmTransformers = getPMTransformers({ - emptyRowTransformer: allowEmptyRows, + emptyRowTransformer: preserveEmptyRows, }); const directiveSyntax = new DirectiveSyntaxContext(experimental.directiveSyntax); @@ -65,7 +65,7 @@ export function useMarkdownEditor( editor.emit('submit', null); return true; }, - allowEmptyRows: allowEmptyRows, + preserveEmptyRows: preserveEmptyRows, mdBreaks: breaks, fileUploadHandler: uploadFile, needToSetDimensionsForUploadedImages, @@ -89,7 +89,7 @@ export function useMarkdownEditor( html: md.html ?? props.allowHTML, linkify: md.linkify ?? props.linkify, linkifyTlds: md.linkifyTlds ?? props.linkifyTlds, - pmTransformers: pmTransformers, + pmTransformers, }, initial: { ...initial, diff --git a/src/bundle/wysiwyg-preset.ts b/src/bundle/wysiwyg-preset.ts index 19e8f816..4be28611 100644 --- a/src/bundle/wysiwyg-preset.ts +++ b/src/bundle/wysiwyg-preset.ts @@ -26,7 +26,7 @@ export type BundlePresetOptions = ExtensionsOptions & EditorModeKeymapOptions & { preset: MarkdownEditorPreset; mdBreaks?: boolean; - allowEmptyRows?: boolean; + preserveEmptyRows?: boolean; fileUploadHandler?: FileUploadHandler; /** * If we need to set dimensions for uploaded images @@ -68,7 +68,7 @@ export const BundlePreset: ExtensionAuto = (builder, opts) !node.text && parent?.type.name === BaseNode.Doc && parent.childCount === 1; return isDocEmpty ? i18nPlaceholder('doc_empty') : null; }, - allowEmptyRows: opts.allowEmptyRows, + preserveEmptyRows: opts.preserveEmptyRows, ...opts.baseSchema, }, }; diff --git a/src/core/markdown/ProseMirrorTransformer/getTransformers.ts b/src/core/markdown/ProseMirrorTransformer/getTransformers.ts index 39c30f91..d9e05884 100644 --- a/src/core/markdown/ProseMirrorTransformer/getTransformers.ts +++ b/src/core/markdown/ProseMirrorTransformer/getTransformers.ts @@ -1,3 +1,4 @@ +// TODO: add a new method to the ExtensionBuilder import {transformEmptyParagraph} from './emptyRowParser'; import {TransformFn} from '.'; diff --git a/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts b/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts index 5798476b..3b332bbd 100644 --- a/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts +++ b/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts @@ -16,7 +16,7 @@ export const pType = nodeTypeFactory(BaseNode.Paragraph); export type BaseSchemaSpecsOptions = { // This cannot be passed through placeholder option of BehaviorPreset because BasePreset initializes first paragraphPlaceholder?: NonNullable['content']; - allowEmptyRows?: boolean; + preserveEmptyRows?: boolean; }; export const BaseSchemaSpecs: ExtensionAuto = (builder, opts) => { @@ -64,8 +64,19 @@ export const BaseSchemaSpecs: ExtensionAuto = (builder, }, fromMd: {tokenSpec: {name: BaseNode.Paragraph, type: 'block'}}, toMd: (state, node, parent) => { - if (opts.allowEmptyRows && !node.content.size) { - const isParentEmpty = parent.content.size / parent.content.childCount === 2; + if (opts.preserveEmptyRows && !node.content.size) { + let isParentEmpty = true; + + for (let index = 0; index < parent.content.childCount; index++) { + const parentChild = parent.content.child(index); + if ( + parentChild.content.size !== 0 || + parentChild.type.name !== 'paragraph' + ) { + isParentEmpty = false; + } + } + if (!isParentEmpty) { state.write(' \n\n'); } diff --git a/src/markup/codemirror/autocomplete/directive.ts b/src/markup/codemirror/autocomplete/directive.ts deleted file mode 100644 index 803cbd3b..00000000 --- a/src/markup/codemirror/autocomplete/directive.ts +++ /dev/null @@ -1,109 +0,0 @@ -import {Completion, CompletionContext, snippet} from '@codemirror/autocomplete'; - -import {capitalize} from '../../../lodash'; -import {DirectiveSyntaxFacet} from '../directive-facet'; - -export type YfmNoteType = 'info' | 'tip' | 'warning' | 'alert'; -export const yfmNoteTypes: readonly YfmNoteType[] = ['info', 'tip', 'warning', 'alert']; -export const yfmNoteSnippetTemplate = (type: YfmNoteType) => - `{% note ${type} %}\n\n#{}\n\n{% endnote %}\n\n` as const; -export const yfmNoteSnippets: Record> = { - info: snippet(yfmNoteSnippetTemplate('info')), - tip: snippet(yfmNoteSnippetTemplate('tip')), - warning: snippet(yfmNoteSnippetTemplate('warning')), - alert: snippet(yfmNoteSnippetTemplate('alert')), -}; - -export const yfmCutSnippetTemplate = '{% cut "#{title}" %}\n\n#{}\n\n{% endcut %}\n\n'; -export const yfmCutSnippet = snippet(yfmCutSnippetTemplate); - -export const yfmCutDirectiveSnippetTemplate = ':::cut [#{title}]\n#{}\n:::\n\n'; -export const yfmCutDirectiveSnippet = snippet(yfmCutDirectiveSnippetTemplate); - -export const directiveAutocomplete = { - autocomplete: (context: CompletionContext) => { - const directiveContext = context.state.facet(DirectiveSyntaxFacet); - - // TODO: add more actions and re-enable - // let word = context.matchBefore(/\/.*/); - // if (word) { - // return { - // from: word.from, - // options: [ - // ...yfmNoteTypes.map((type, index) => ({ - // label: `/yfm note ${type}`, - // displayLabel: `YFM Note ${capitalize(type)}`, - // type: 'text', - // apply: yfmNoteSnippets[type], - // boost: -index, - // })), - // { - // label: '/yfm cut', - // displayLabel: 'YFM Cut', - // type: 'text', - // apply: directiveFacet.shouldInsertDirectiveMarkup('yfmCut') - // ? yfmCutDirectiveSnippet - // : yfmCutSnippet, - // }, - // ], - // }; - // } - - const word = context.matchBefore(/^.*/); - - // if (allowEmptyRows && word?.text.startsWith('&')) { - // return { - // from: word.from, - // options: [ - // { - // label: ' ', - // displayLabel: i18n('snippet.text'), - // type: 'text', - // apply: emptyRowSnippet, - // }, - // ], - // }; - // } - - if (directiveContext.option !== 'only' && word?.text.startsWith('{%')) { - return { - from: word.from, - options: [ - ...yfmNoteTypes.map((type, index) => ({ - label: `{% note ${type}`, - displayLabel: capitalize(type), - type: 'text', - section: 'YFM Note', - apply: yfmNoteSnippets[type], - boost: -index, - })), - { - label: '{% cut', - displayLabel: 'YFM Cut', - type: 'text', - apply: directiveContext.shouldInsertDirectiveMarkup('yfmCut') - ? yfmCutDirectiveSnippet - : yfmCutSnippet, - }, - ], - }; - } - if (directiveContext.option !== 'disabled' && word?.text.startsWith(':')) { - const options: Completion[] = []; - - if (directiveContext.valueFor('yfmCut') !== 'disabled') { - options.push({ - label: ':::cut', - displayLabel: 'YFM Cut', - type: 'text', - apply: yfmCutDirectiveSnippet, - }); - } - - if (options.length) { - return {from: word.from, options}; - } - } - return null; - }, -}; diff --git a/src/markup/codemirror/autocomplete/index.ts b/src/markup/codemirror/autocomplete/index.ts index 0c40f5f9..d77b500e 100644 --- a/src/markup/codemirror/autocomplete/index.ts +++ b/src/markup/codemirror/autocomplete/index.ts @@ -1,18 +1,19 @@ -import {directiveAutocomplete} from './directive'; +import {mdAutocomplete} from '../yfm'; + import {emptyRowAutocomplete} from './emptyRow'; type GetAutocompleteConfig = { - allowEmptyRows?: boolean; + preserveEmptyRows?: boolean; }; -export const getAutocompleteConfig = ({allowEmptyRows}: GetAutocompleteConfig) => { +export const getAutocompleteConfig = ({preserveEmptyRows}: GetAutocompleteConfig) => { const autocompleteItems = []; - if (allowEmptyRows) { + if (preserveEmptyRows) { autocompleteItems.push(emptyRowAutocomplete); } - autocompleteItems.push(directiveAutocomplete); + autocompleteItems.push(mdAutocomplete); return autocompleteItems; }; diff --git a/src/markup/codemirror/yfm.ts b/src/markup/codemirror/yfm.ts index 2b7f6342..ccc6f15c 100644 --- a/src/markup/codemirror/yfm.ts +++ b/src/markup/codemirror/yfm.ts @@ -1,9 +1,13 @@ -import {CompletionSource} from '@codemirror/autocomplete'; +import {Completion, CompletionSource, snippet} from '@codemirror/autocomplete'; import {markdown, markdownLanguage} from '@codemirror/lang-markdown'; import type {Extension} from '@codemirror/state'; import {Tag, tags} from '@lezer/highlight'; import type {DelimiterType, MarkdownConfig} from '@lezer/markdown'; +import {capitalize} from '../../lodash'; + +import {DirectiveSyntaxFacet} from './directive-facet'; + export const customTags = { underline: Tag.define(), monospace: Tag.define(), @@ -60,6 +64,23 @@ const MarkedExtension = mdInlineFactory({ tag: customTags.marked, }); +export type YfmNoteType = 'info' | 'tip' | 'warning' | 'alert'; +export const yfmNoteTypes: readonly YfmNoteType[] = ['info', 'tip', 'warning', 'alert']; +export const yfmNoteSnippetTemplate = (type: YfmNoteType) => + `{% note ${type} %}\n\n#{}\n\n{% endnote %}\n\n` as const; +export const yfmNoteSnippets: Record> = { + info: snippet(yfmNoteSnippetTemplate('info')), + tip: snippet(yfmNoteSnippetTemplate('tip')), + warning: snippet(yfmNoteSnippetTemplate('warning')), + alert: snippet(yfmNoteSnippetTemplate('alert')), +}; + +export const yfmCutSnippetTemplate = '{% cut "#{title}" %}\n\n#{}\n\n{% endcut %}\n\n'; +export const yfmCutSnippet = snippet(yfmCutSnippetTemplate); + +export const yfmCutDirectiveSnippetTemplate = ':::cut [#{title}]\n#{}\n:::\n\n'; +export const yfmCutDirectiveSnippet = snippet(yfmCutDirectiveSnippetTemplate); + export interface LanguageData { autocomplete: CompletionSource; [key: string]: any; @@ -69,6 +90,79 @@ export interface YfmLangOptions { languageData?: LanguageData[]; } +export const mdAutocomplete: LanguageData = { + autocomplete: (context) => { + const directiveContext = context.state.facet(DirectiveSyntaxFacet); + + // TODO: add more actions and re-enable + // let word = context.matchBefore(/\/.*/); + // if (word) { + // return { + // from: word.from, + // options: [ + // ...yfmNoteTypes.map((type, index) => ({ + // label: `/yfm note ${type}`, + // displayLabel: `YFM Note ${capitalize(type)}`, + // type: 'text', + // apply: yfmNoteSnippets[type], + // boost: -index, + // })), + // { + // label: '/yfm cut', + // displayLabel: 'YFM Cut', + // type: 'text', + // apply: directiveFacet.shouldInsertDirectiveMarkup('yfmCut') + // ? yfmCutDirectiveSnippet + // : yfmCutSnippet, + // }, + // ], + // }; + // } + + const word = context.matchBefore(/^.*/); + if (directiveContext.option !== 'only' && word?.text.startsWith('{%')) { + return { + from: word.from, + options: [ + ...yfmNoteTypes.map((type, index) => ({ + label: `{% note ${type}`, + displayLabel: capitalize(type), + type: 'text', + section: 'YFM Note', + apply: yfmNoteSnippets[type], + boost: -index, + })), + { + label: '{% cut', + displayLabel: 'YFM Cut', + type: 'text', + apply: directiveContext.shouldInsertDirectiveMarkup('yfmCut') + ? yfmCutDirectiveSnippet + : yfmCutSnippet, + }, + ], + }; + } + if (directiveContext.option !== 'disabled' && word?.text.startsWith(':')) { + const options: Completion[] = []; + + if (directiveContext.valueFor('yfmCut') !== 'disabled') { + options.push({ + label: ':::cut', + displayLabel: 'YFM Cut', + type: 'text', + apply: yfmCutDirectiveSnippet, + }); + } + + if (options.length) { + return {from: word.from, options}; + } + } + return null; + }, +}; + export function yfmLang({languageData = []}: YfmLangOptions = {}): Extension { const mdSupport = markdown({ // defaultCodeLanguage: markdownLanguage, @@ -78,5 +172,9 @@ export function yfmLang({languageData = []}: YfmLangOptions = {}): Extension { extensions: [UnderlineExtension, MonospaceExtension, MarkedExtension], }); - return [mdSupport, languageData.map((item) => mdSupport.language.data.of(item))]; + return [ + mdSupport, + mdSupport.language.data.of(mdAutocomplete), + languageData.map((item) => mdSupport.language.data.of(item)), + ]; } diff --git a/src/markup/commands/helpers.ts b/src/markup/commands/helpers.ts index 1ab668dd..94629278 100644 --- a/src/markup/commands/helpers.ts +++ b/src/markup/commands/helpers.ts @@ -26,11 +26,7 @@ export function getBlockExtraLineBreaks( return {before: lineBreaksBefore, after: lineBreaksAfter}; } -export function replaceOrInsertAfter( - state: EditorState, - markup: string, - shouldGetBreaks?: boolean, -): TransactionSpec { +export function replaceOrInsertAfter(state: EditorState, markup: string): TransactionSpec { const selrange = state.selection.main; if (isFullLinesSelection(state.doc, selrange)) { const extraBreaks = getBlockExtraLineBreaks(state, { @@ -43,26 +39,14 @@ export function replaceOrInsertAfter( state.lineBreak.repeat(extraBreaks.after), ); } else { - let breaksAfter = 2; - - if (shouldGetBreaks) { - const extraBreaks = getBlockExtraLineBreaks(state, { - from: state.doc.lineAt(selrange.from), - to: state.doc.lineAt(selrange.to), - }); - - breaksAfter = extraBreaks.after; - } - - const insert = state.lineBreak.repeat(2) + markup + state.lineBreak.repeat(breaksAfter); - + const insert = state.lineBreak.repeat(2) + markup + state.lineBreak.repeat(2); const from = state.doc.lineAt(selrange.to).to; - const selAnchor = from + insert.length + breaksAfter; + const selAnchor = from + insert.length - 2; return {changes: {from, insert}, selection: {anchor: selAnchor}}; } } -export function isFullLinesSelection(doc: Text, range: SelectionRange): boolean { +function isFullLinesSelection(doc: Text, range: SelectionRange): boolean { const fromLine = doc.lineAt(range.from); const toLine = doc.lineAt(range.to); return range.from <= fromLine.from && range.to >= toLine.to;