Skip to content

Commit

Permalink
fix: <br> operation when converting editor mode, pasting data or ca…
Browse files Browse the repository at this point in the history
…lling API (#1807)

* fix: replace br with paragraph  for pasting the data with whitespace properly

* fix: replace br with empty paragraph when pasting data to wysiwyg

* fix: the result of getHTML() API should be same as the wysiwyg contents

* chore: add test case(setHTML(), getHTML())

* refactor: move br regExp to constants

* fix: editor cannot convert following br tag

* chore: add convertor test case(br tag)

* chore: fix wrong test case(setHTML API)
  • Loading branch information
js87zz authored Oct 6, 2021
1 parent 04d6f8c commit 844efe9
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 29 deletions.
41 changes: 40 additions & 1 deletion apps/editor/src/__test__/unit/convertor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ describe('Convertor', () => {
assertConverting(markdown, expected);
});

it('<br> with html inline node ', () => {
it('<br> with html inline node', () => {
const markdown = source`
foo
bar
Expand All @@ -452,6 +452,45 @@ describe('Convertor', () => {

assertConverting(markdown, expected);
});

it('<br> with following <br>', () => {
const markdown = source`
text1
<br>
text2<br>
<br>
text3
`;
const expected = source`
text1
text2
text3
`;

assertConverting(markdown, expected);
});

it('<br> in the middle of the paragraph', () => {
const markdown = source`
text1
<br>
te<br>xt2<br>
<br>
text3
`;
const expected = source`
text1
te
xt2
text3
`;

assertConverting(markdown, expected);
});
});

describe('convert inline html', () => {
Expand Down
79 changes: 77 additions & 2 deletions apps/editor/src/__test__/unit/editor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import '@/i18n/en-us';
import { oneLineTrim, stripIndents } from 'common-tags';
import { oneLineTrim, stripIndents, source } from 'common-tags';
import { Emitter } from '@t/event';
import { EditorOptions } from '@t/editor';
import type { OpenTagToken } from '@toast-ui/toastmark';
Expand Down Expand Up @@ -74,7 +74,7 @@ describe('editor', () => {
it('basic', () => {
editor.setMarkdown('# heading\n* bullet');

const result = stripIndents`
const result = oneLineTrim`
<h1>heading</h1>
<ul>
<li>
Expand All @@ -95,6 +95,28 @@ describe('editor', () => {

expect(spy).not.toHaveBeenCalled();
});

it('should be the same as wysiwyg contents', () => {
const input = source`
<p>first line</p>
<p>second line</p>
<p><br>\nthird line</p>
<p><br>\n<br>\nfourth line</p>
`;
const expected = oneLineTrim`
<p>first line</p>
<p>second line</p>
<p><br></p>
<p>third line</p>
<p><br></p>
<p><br></p>
<p>fourth line</p>
`;

editor.setHTML(input);

expect(editor.getHTML()).toBe(expected);
});
});

it('changeMode()', () => {
Expand Down Expand Up @@ -159,6 +181,59 @@ describe('editor', () => {
expect(mdEditor).toContainHTML('<div>a</div><div>b</div>');
expect(getPreviewHTML()).toBe('<p>a<br>b</p>');
});

it('should parse the br tag with the paragraph block to separate between blocks in wysiwyg', () => {
editor.setHTML(
'<h1>test title</h1><p><strong>test bold</strong><br><em>test italic</em><br>normal text</p>'
);
editor.changeMode('wysiwyg');

const expected = oneLineTrim`
<h1>test title</h1>
<p><strong>test bold</strong></p>
<p><em>test italic</em></p>
<p>normal text</p>
`;

expect(wwEditor).toContainHTML(expected);
});

it('should parse the br tag with the paragraph block to separate between blocks', () => {
const input = source`
<p>first line</p>
<p>second line</p>
<p><br>\nthird line</p>
<p><br>\n<br>\nfourth line</p>
`;
const expected = oneLineTrim`
<p>first line<br>second line</p>
<p>third line</p>
<p><br>fourth line</p>
`;

editor.setHTML(input);

expect(getPreviewHTML()).toBe(expected);
});

it('should be parsed with the same content when calling setHTML() with getHTML() API result', () => {
const input = source`
<p>first line</p>
<p>second line</p>
<p><br>\nthird line</p>
<p><br>\n<br>\nfourth line</p>
`;

editor.setHTML(input);

const mdEditorHTML = mdEditor.innerHTML;
const mdPreviewHTML = getPreviewHTML();

editor.setHTML(editor.getHTML());

expect(mdEditor).toContainHTML(mdEditorHTML);
expect(getPreviewHTML()).toBe(mdPreviewHTML);
});
});

it('reset()', () => {
Expand Down
20 changes: 17 additions & 3 deletions apps/editor/src/convertors/toWysiwyg/htmlToWwConvertors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
ToWwConvertorState,
} from '@t/convertor';
import { includes } from '@/utils/common';
import { CLOSE_TAG, reHTMLTag } from '@/utils/constants';
import { reHTMLTag } from '@/utils/constants';

export function getTextWithoutTrailingNewline(text: string) {
return text[text.length - 1] === '\n' ? text.slice(0, text.length - 1) : text;
Expand All @@ -30,6 +30,10 @@ export function isInlineNode({ type }: MdNode) {
return includes(['text', 'strong', 'emph', 'strike', 'image', 'link', 'code'], type);
}

function isSoftbreak(mdNode: MdNode | null) {
return mdNode?.type === 'softbreak';
}

function isListNode({ type, literal }: MdNode) {
const matched = type === 'htmlInline' && literal!.match(reHTMLTag);

Expand Down Expand Up @@ -162,12 +166,22 @@ const convertors: HTMLToWwConvertorMap = {
const { parent, prev, next } = node;

if (parent?.type === 'paragraph') {
if (prev && !(prev.type === 'htmlInline' && prev.literal!.match(CLOSE_TAG))) {
// should open a paragraph node when line text has only <br> tag
// ex) first line\n\n<br>\nfourth line
if (isSoftbreak(prev)) {
state.openNode(paragraph);
}

if (next) {
// should close a paragraph node when line text has only <br> tag
// ex) first line\n\n<br>\nfourth line
if (isSoftbreak(next)) {
state.closeNode();
// should close a paragraph node and open a paragraph node to separate between blocks
// when <br> tag is in the middle of the paragraph
// ex) first <br>line\nthird line
} else if (next) {
state.closeNode();
state.openNode(paragraph);
}
} else if (parent?.type === 'tableCell') {
if (prev && (isInlineNode(prev) || isCustomHTMLInlineNode(state, prev))) {
Expand Down
4 changes: 2 additions & 2 deletions apps/editor/src/convertors/toWysiwyg/toWwConvertors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ import { ToWwConvertorMap } from '@t/convertor';
import { createWidgetContent, getWidgetContent } from '@/widget/rules';
import { getChildrenHTML, getHTMLAttrsByHTMLString } from '@/wysiwyg/nodes/html';
import { includes } from '@/utils/common';
import { reHTMLTag } from '@/utils/constants';
import { reBR, reHTMLTag } from '@/utils/constants';

function isBRTag(node: MdNode) {
return node.type === 'htmlInline' && /<br ?\/?>/.test(node.literal!);
return node.type === 'htmlInline' && reBR.test(node.literal!);
}

function addRawHTMLAttributeToDOM(parent: Node) {
Expand Down
21 changes: 9 additions & 12 deletions apps/editor/src/editorCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ import { getHTMLRenderConvertors } from './markdown/htmlRenderConvertors';
* @param {autofocus} [options.autofocus=true] - automatically focus the editor on creation.
*/
class ToastUIEditorCore {
private initialHtml: string;
private initialHTML: string;

private toastMark: ToastMark;

Expand Down Expand Up @@ -118,7 +118,7 @@ class ToastUIEditorCore {
protected pluginInfo: PluginInfoResult;

constructor(options: EditorOptions) {
this.initialHtml = options.el.innerHTML;
this.initialHTML = options.el.innerHTML;
options.el.innerHTML = '';

this.options = extend(
Expand Down Expand Up @@ -256,7 +256,7 @@ class ToastUIEditorCore {
}

if (!this.options.initialValue) {
this.setHTML(this.initialHtml, false);
this.setHTML(this.initialHTML, false);
}

this.commandManager = new CommandManager(
Expand Down Expand Up @@ -481,18 +481,15 @@ class ToastUIEditorCore {
*/
getHTML() {
this.eventEmitter.holdEventInvoke(() => {
if (this.isWysiwygMode()) {
this.mdEditor.setMarkdown(this.convertor.toMarkdownText(this.wwEditor.getModel()));
if (this.isMarkdownMode()) {
const mdNode = this.toastMark.getRootNode();
const wwNode = this.convertor.toWysiwygModel(mdNode);

this.wwEditor.setModel(wwNode!);
}
});

const mdNode = this.toastMark.getRootNode();
const mdRenderer = this.preview.getRenderer();

return mdRenderer
.render(mdNode)
.replace(/\sdata-nodeid="\d{1,}"/g, '')
.trim();
return this.wwEditor.view.dom.innerHTML;
}

/**
Expand Down
3 changes: 3 additions & 0 deletions apps/editor/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ export const CLOSE_TAG = `</(${TAG_NAME})\\s*[>]`;
export const HTML_TAG = `(?:${OPEN_TAG}|${CLOSE_TAG})`;

export const reHTMLTag = new RegExp(`^${HTML_TAG}`, 'i');
export const reBR = /<br\s*\/*>/i;

export const ALTERNATIVE_TAG_FOR_BR = '</p><p>';
17 changes: 9 additions & 8 deletions apps/editor/src/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import hasClass from 'tui-code-snippet/domUtil/hasClass';
import addClass from 'tui-code-snippet/domUtil/addClass';
import removeClass from 'tui-code-snippet/domUtil/removeClass';
import matches from 'tui-code-snippet/domUtil/matches';
import { HTML_TAG, OPEN_TAG } from './constants';
import { ALTERNATIVE_TAG_FOR_BR, HTML_TAG, OPEN_TAG, reBR } from './constants';
import { isNil } from './common';

export function isPositionInBox(style: CSSStyleDeclaration, offsetX: number, offsetY: number) {
Expand Down Expand Up @@ -239,27 +239,28 @@ export function setAttributes(attributes: Record<string, any>, element: HTMLElem
}

export function replaceBRWithEmptyBlock(html: string) {
const reBr = /<br\s*\/*>/i;
// remove br in paragraph to compatible with markdown
let replacedHTML = html.replace(/<p><br\s*\/*><\/p>/gi, '<p></p>');
const reHTMLTag = new RegExp(HTML_TAG, 'ig');
const htmlTagMatched = html.match(reHTMLTag);
const htmlTagMatched = replacedHTML.match(reHTMLTag);

htmlTagMatched?.forEach((htmlTag, index) => {
if (reBr.test(htmlTag)) {
let alternativeTag = '';
if (reBR.test(htmlTag)) {
let alternativeTag = ALTERNATIVE_TAG_FOR_BR;

if (index) {
const prevTag = htmlTagMatched[index - 1];
const openTagMatched = prevTag.match(OPEN_TAG);

if (openTagMatched) {
if (openTagMatched && !/br/i.test(openTagMatched[1])) {
const [, tagName] = openTagMatched;

alternativeTag = `</${tagName}><${tagName}>`;
}
}
html = html.replace(reBr, alternativeTag);
replacedHTML = replacedHTML.replace(reBR, alternativeTag);
}
});

return html;
return replacedHTML;
}
3 changes: 2 additions & 1 deletion apps/editor/src/wysiwyg/clipboard/paste.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Schema, Node, Slice, Fragment, NodeType } from 'prosemirror-model';

import { isFromMso, convertMsoParagraphsToList } from '@/wysiwyg/clipboard/pasteMsoList';
import { getTableContentFromSlice } from '@/wysiwyg/helper/table';
import { ALTERNATIVE_TAG_FOR_BR } from '@/utils/constants';

const START_FRAGMENT_COMMENT = '<!--StartFragment-->';
const END_FRAGMENT_COMMENT = '<!--EndFragment-->';
Expand All @@ -14,7 +15,7 @@ function getContentBetweenFragmentComments(html: string) {
html = html.slice(startFragmentIndex + START_FRAGMENT_COMMENT.length, endFragmentIndex);
}

return html;
return html.replace(/<br[^>]*>/g, ALTERNATIVE_TAG_FOR_BR);
}

function convertMsoTableToCompletedTable(html: string) {
Expand Down

0 comments on commit 844efe9

Please sign in to comment.