diff --git a/apps/editor/src/__test__/unit/convertor.spec.ts b/apps/editor/src/__test__/unit/convertor.spec.ts index 52702f2650..bef09ec536 100644 --- a/apps/editor/src/__test__/unit/convertor.spec.ts +++ b/apps/editor/src/__test__/unit/convertor.spec.ts @@ -2,7 +2,7 @@ import { source, oneLineTrim } from 'common-tags'; import { Context, MdNode, Parser, HTMLConvertorMap } from '@toast-ui/toastmark'; -import { Schema } from 'prosemirror-model'; +import { Node, Schema } from 'prosemirror-model'; import { createSpecs } from '@/wysiwyg/specCreator'; import Convertor from '@/convertors/convertor'; @@ -595,7 +595,20 @@ describe('Convertor', () => { | ![altText](imgUrl) **mixed** | `; - assertConverting(markdown, `${markdown}\n`); + const expected = source` + | thead | + | ----- | + | | + |
  1. ordered
| + | | + | | + |
  1. mixed
| + |
  1. mixed
| + | foobaz | + | ![altText](imgUrl) **mixed** | + `; + + assertConverting(markdown, `${expected}\n`); }); it('table with unmatched html list', () => { @@ -1061,4 +1074,28 @@ describe('Convertor', () => { assertConverting(markdown, markdown); }); }); + + it('should convert by using HTML tag when delimiter is not preceded an alphanumeric', () => { + const wwNodeJson = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + marks: [{ type: 'strong' }], + text: '"test"', + }, + { type: 'text', text: 'a' }, + ], + }, + ], + }; + const wwNode = Node.fromJSON(schema, wwNodeJson); + + const result = convertor.toMarkdownText(wwNode); + + expect(result).toBe(`"test"a`); + }); }); diff --git a/apps/editor/src/convertors/toMarkdown/toMdConvertorState.ts b/apps/editor/src/convertors/toMarkdown/toMdConvertorState.ts index 544a186f58..99991a348f 100644 --- a/apps/editor/src/convertors/toMarkdown/toMdConvertorState.ts +++ b/apps/editor/src/convertors/toMarkdown/toMdConvertorState.ts @@ -1,6 +1,6 @@ import { Node, Mark } from 'prosemirror-model'; -import { includes, escape, last } from '@/utils/common'; +import { includes, escape, last, isEndWithSpace, isStartWithSpace } from '@/utils/common'; import { WwNodeType, WwMarkType } from '@t/wysiwyg'; import { @@ -10,6 +10,7 @@ import { FirstDelimFn, InfoForPosSync, } from '@t/convertor'; +import { DEFAULT_TEXT_NOT_START_OR_END_WITH_SPACE } from '@/utils/constants'; export default class ToMdConvertorState { private readonly nodeTypeConvertors: ToMdNodeTypeConvertorMap; @@ -49,11 +50,27 @@ export default class ToMdConvertorState { return /(^|\n)$/.test(this.result); } + private isBetweenSpaces(parent: Node, index: number) { + const { content } = parent; + + const isFrontNodeEndWithSpace = + index === 0 || + isEndWithSpace(content.child(index - 1).text ?? DEFAULT_TEXT_NOT_START_OR_END_WITH_SPACE); + + const isRearNodeStartWithSpace = + index >= content.childCount - 1 || + isStartWithSpace(content.child(index + 1).text ?? DEFAULT_TEXT_NOT_START_OR_END_WITH_SPACE); + + return isFrontNodeEndWithSpace && isRearNodeStartWithSpace; + } + private markText(mark: Mark, entering: boolean, parent: Node, index: number) { const convertor = this.getMarkConvertor(mark); if (convertor) { - const { delim, rawHTML } = convertor({ node: mark, parent, index }, entering); + const betweenSpace = this.isBetweenSpaces(parent, entering ? index : index - 1); + + const { delim, rawHTML } = convertor({ node: mark, parent, index }, entering, betweenSpace); return (rawHTML as string) || (delim as string); } diff --git a/apps/editor/src/convertors/toMarkdown/toMdConvertors.ts b/apps/editor/src/convertors/toMarkdown/toMdConvertors.ts index 71066e7b18..8f1a190404 100644 --- a/apps/editor/src/convertors/toMarkdown/toMdConvertors.ts +++ b/apps/editor/src/convertors/toMarkdown/toMdConvertors.ts @@ -208,29 +208,44 @@ export const toMdConvertors: ToMdConvertorMap = { }; }, - strong({ node }, { entering }) { + strong({ node }, { entering }, betweenSpace) { const { rawHTML } = node.attrs; + let delim = '**'; + + if (!betweenSpace) { + delim = entering ? '' : ''; + } return { - delim: '**', + delim, rawHTML: entering ? getOpenRawHTML(rawHTML) : getCloseRawHTML(rawHTML), }; }, - emph({ node }, { entering }) { + emph({ node }, { entering }, betweenSpace) { const { rawHTML } = node.attrs; + let delim = '*'; + + if (!betweenSpace) { + delim = entering ? '' : ''; + } return { - delim: '*', + delim, rawHTML: entering ? getOpenRawHTML(rawHTML) : getCloseRawHTML(rawHTML), }; }, - strike({ node }, { entering }) { + strike({ node }, { entering }, betweenSpace) { const { rawHTML } = node.attrs; + let delim = '~~'; + + if (!betweenSpace) { + delim = entering ? '' : ''; + } return { - delim: '~~', + delim, rawHTML: entering ? getOpenRawHTML(rawHTML) : getCloseRawHTML(rawHTML), }; }, @@ -353,7 +368,7 @@ function createMarkTypeConvertors(convertors: ToMdConvertorMap) { const markTypes = Object.keys(markTypeOptions) as WwMarkType[]; markTypes.forEach((type) => { - markTypeConvertors[type] = (nodeInfo, entering) => { + markTypeConvertors[type] = (nodeInfo, entering, betweenSpace) => { const markOption = markTypeOptions[type]; const convertor = convertors[type]; @@ -362,7 +377,9 @@ function createMarkTypeConvertors(convertors: ToMdConvertorMap) { // When calling the converter without using `delim` and `rawHTML` values, // the converter is called without parameters. const runConvertor = convertor && nodeInfo && !isUndefined(entering); - const params = runConvertor ? convertor!(nodeInfo as MarkInfo, { entering }) : {}; + const params = runConvertor + ? convertor!(nodeInfo as MarkInfo, { entering }, betweenSpace) + : {}; return { ...params, ...markOption }; }; diff --git a/apps/editor/src/utils/common.ts b/apps/editor/src/utils/common.ts index 343eecc8d7..cd38e93083 100644 --- a/apps/editor/src/utils/common.ts +++ b/apps/editor/src/utils/common.ts @@ -257,3 +257,15 @@ export function assign(targetObj: Record, obj: Record export function getSortedNumPair(valueA: number, valueB: number) { return valueA > valueB ? [valueB, valueA] : [valueA, valueB]; } + +export function isStartWithSpace(text: string) { + const reStartWithSpace = /^\s(\S*)/g; + + return reStartWithSpace.test(text); +} + +export function isEndWithSpace(text: string) { + const reEndWithSpace = /(\S*)\s$/g; + + return reEndWithSpace.test(text); +} diff --git a/apps/editor/src/utils/constants.ts b/apps/editor/src/utils/constants.ts index 2d6cd54983..78d5adf33f 100644 --- a/apps/editor/src/utils/constants.ts +++ b/apps/editor/src/utils/constants.ts @@ -20,3 +20,5 @@ export const reBR = //i; export const reHTMLComment = /|/; export const ALTERNATIVE_TAG_FOR_BR = '

'; + +export const DEFAULT_TEXT_NOT_START_OR_END_WITH_SPACE = 'a'; diff --git a/apps/editor/types/convertor.d.ts b/apps/editor/types/convertor.d.ts index e8ef7d5fc0..d44db7d00d 100644 --- a/apps/editor/types/convertor.d.ts +++ b/apps/editor/types/convertor.d.ts @@ -111,7 +111,8 @@ export type ToMdNodeTypeConvertorMap = Partial ToMdConvertorReturnValues & ToMdMarkTypeOption; export type ToMdMarkTypeConvertorMap = Partial>; @@ -124,7 +125,8 @@ interface ToMdConvertorContext { type ToMdConvertor = ( nodeInfo: NodeInfo | MarkInfo, - context: ToMdConvertorContext + context: ToMdConvertorContext, + betweenSpace?: boolean ) => ToMdConvertorReturnValues; export type ToMdConvertorMap = Partial>;