Repository: nhn/tui.editor (ts)

Issue:
editor is still getting focused after autofocus=false
## Describe the bug
```js
<Editor autofocus={false}/>
```
Then also editor is getting focused after load
## To Reproduce
Steps to reproduce the behavior:
Clone this repo https://github.com/ats1999/editor-no-chakra-UI and run the example.

## Expected behaviour
Editor should not get focused

## Screenshots
If applicable, add screenshots to help explain your problem.

https://user-images.githubusercontent.com/54087826/138985006-ef62f51f-c222-4c82-a44a-4049a346fae2.mp4


## Desktop (please complete the following information):
 - OS: [e.g. iOS]
 - Browser [e.g. chrome, safari]
 - Version [e.g. 22]

## Smartphone (please complete the following information):
 - Device: [e.g. iPhone6]
 - OS: [e.g. iOS8.1]
 - Browser [e.g. stock browser, safari]
 - Version [e.g. 22]

## Additional context
Add any other context about the problem here.


Pull request description:
fix: Fixed autofocus={false} invalid (fix #1945)
<!-- EDIT TITLE PLEASE -->
<!-- It should be one of them
  <ISSUE TYPE>: Short Description (<CLOSING TYPE> #<ISSUE NUMBERS>)
  ex)
  feat: add new feature (close #111)
  fix: wrong behavior (fix #111)
  chore: change build tool (ref #111)
-->

<!-- SPECIFY A ISSUE TYPE AT HEAD
  feat: A new feature
  fix: A bug fix
  docs: Documentation only changes
  style: Changes that do not affect the meaning of the code (white-space, formatting etc)
  refactor: A code change that neither fixes a bug or adds a feature
  perf: A code change that improves performance
  test: Adding missing tests
  chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->

<!-- ADD CLOSING TYPE AND ISSUE NUMBER AT TAIL
  (<CLOSING TYPE> #<ISSUE NUMBERS>)
  close: resolve not a bug(feature, docs, etc) completely
  fix: resolve a bug completely
  ref: not fully resolved or related to
-->

### Please check if the PR fulfills these requirements
- [x] It's the right issue type on the title
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] The commit message follows our guidelines
- [ ] Tests for the changes have been added (for bug fixes/features)
- [ ] Docs have been added/updated (for bug fixes/features)
- [ ] It does not introduce a breaking change or has a description of the breaking change

### Description
Modify the issue in which the `autofocus` option is ignored in the markdown tab mode
* related issue(#1802)


---
Thank you for your contribution to TOAST UI product. 🎉 😘 ✨


Patch:
diff --git a/apps/editor/src/convertors/toMarkdown/toMdConvertors.ts b/apps/editor/src/convertors/toMarkdown/toMdConvertors.ts
index d53495c8..1a041813 100644
--- a/apps/editor/src/convertors/toMarkdown/toMdConvertors.ts
+++ b/apps/editor/src/convertors/toMarkdown/toMdConvertors.ts
@@ -4,7 +4,7 @@ import isUndefined from 'tui-code-snippet/type/isUndefined';
 
 import { nodeTypeWriters, write } from './toMdNodeTypeWriters';
 
-import { repeat, escape, quote } from '@/utils/common';
+import { repeat, escape, quote, escapeXml } from '@/utils/common';
 
 import {
   ToMdConvertorMap,
@@ -168,7 +168,7 @@ export const toMdConvertors: ToMdConvertorMap = {
   image({ node }) {
     const { attrs } = node;
     const altText = escape(attrs.altText || '');
-    const imageUrl = escape(attrs.imageUrl);
+    const imageUrl = escapeXml(attrs.imageUrl);
     const altAttr = altText ? ` alt="${altText}"` : '';
 
     return {
@@ -237,7 +237,7 @@ export const toMdConvertors: ToMdConvertorMap = {
 
   link({ node }, { entering }) {
     const { attrs } = node;
-    const linkUrl = escape(attrs.linkUrl);
+    const linkUrl = escapeXml(attrs.linkUrl);
     const { rawHTML } = attrs;
 
     if (entering) {
diff --git a/apps/editor/src/convertors/toWysiwyg/toWwConvertors.ts b/apps/editor/src/convertors/toWysiwyg/toWwConvertors.ts
index 5cfdc38b..738eafa9 100644
--- a/apps/editor/src/convertors/toWysiwyg/toWwConvertors.ts
+++ b/apps/editor/src/convertors/toWysiwyg/toWwConvertors.ts
@@ -233,35 +233,40 @@ const toWwConvertors: ToWwConvertorMap = {
   },
 
   tableCell(state, node, { entering }) {
-    if (!(node as TableCellMdNode).ignored) {
-      const hasParaNode = (childNode: MdNode | null) =>
-        childNode && (isInlineNode(childNode) || isCustomHTMLInlineNode(state, childNode));
+    const { tableHeadCell, tableBodyCell, paragraph } = state.schema.nodes;
+    const tablePart = node.parent!.parent!;
+    const cell = tablePart.type === 'tableHead' ? tableHeadCell : tableBodyCell;
+    const hasParaNode = (childNode: MdNode | null) =>
+      childNode && (isInlineNode(childNode) || isCustomHTMLInlineNode(state, childNode));
 
-      if (entering) {
-        const { tableHeadCell, tableBodyCell, paragraph } = state.schema.nodes;
-        const tablePart = node.parent!.parent!;
-        const cell = tablePart.type === 'tableHead' ? tableHeadCell : tableBodyCell;
+    if (entering) {
+      let attrs: Record<string, string | number> | null = null;
 
+      if (!(node as TableCellMdNode).ignored) {
         const table = tablePart.parent as TableMdNode;
-        const { align } = table.columns[(node as TableCellMdNode).startIdx];
-        const attrs: Record<string, string | number> = { ...(node as TableCellMdNode).attrs };
+        const { startIdx } = node as TableCellMdNode;
 
-        if (align) {
-          attrs.align = align;
-        }
+        if (table.columns.length > startIdx) {
+          const { align } = table.columns[startIdx];
 
-        state.openNode(cell, attrs);
+          attrs = (node as TableCellMdNode).attrs ?? {};
 
-        if (hasParaNode(node.firstChild)) {
-          state.openNode(paragraph);
-        }
-      } else {
-        if (hasParaNode(node.lastChild)) {
-          state.closeNode();
+          if (align) {
+            attrs.align = align;
+          }
         }
+      }
 
+      state.openNode(cell, attrs);
+
+      if (hasParaNode(node.firstChild)) {
+        state.openNode(paragraph);
+      }
+    } else {
+      if (hasParaNode(node.lastChild)) {
         state.closeNode();
       }
+      state.closeNode();
     }
   },
 
diff --git a/apps/editor/src/markdown/marks/listItem.ts b/apps/editor/src/markdown/marks/listItem.ts
index f63b28c7..dd98f492 100644
--- a/apps/editor/src/markdown/marks/listItem.ts
+++ b/apps/editor/src/markdown/marks/listItem.ts
@@ -24,8 +24,11 @@ import { getTextContent } from '../helper/query';
 
 type CommandType = 'bullet' | 'ordered' | 'task';
 
-function cannotBeListNode({ type }: MdNode) {
-  return type === 'codeBlock' || type === 'heading' || type.indexOf('table') !== -1;
+function cannotBeListNode({ type, sourcepos }: MdNode, line: number) {
+  // eslint-disable-next-line prefer-destructuring
+  const startLine = sourcepos![0][0];
+
+  return line <= startLine && (type === 'codeBlock' || type === 'heading' || type.match('table'));
 }
 
 interface RangeInfo {
@@ -135,7 +138,7 @@ export class ListItem extends Mark {
       for (let line = startLine; line <= endLine; line += 1) {
         const mdNode: MdNode = toastMark.findFirstNodeAtLine(line)!;
 
-        if (mdNode && cannotBeListNode(mdNode)) {
+        if (mdNode && cannotBeListNode(mdNode, line)) {
           break;
         }
 
diff --git a/apps/editor/src/markdown/mdEditor.ts b/apps/editor/src/markdown/mdEditor.ts
index a0cb9e18..32053cd4 100644
--- a/apps/editor/src/markdown/mdEditor.ts
+++ b/apps/editor/src/markdown/mdEditor.ts
@@ -73,15 +73,20 @@ export default class MdEditor extends EditorBase {
     this.commands = this.createCommands();
     this.specs.setContext({ ...this.context, view: this.view });
     this.createClipboard();
-    this.eventEmitter.listen('changePreviewTabWrite', () => this.toggleActive(true));
+    // To prevent unnecessary focus setting during initial rendering
+    this.eventEmitter.listen('changePreviewTabWrite', (isMarkdownTabMounted) =>
+      this.toggleActive(true, isMarkdownTabMounted)
+    );
     this.eventEmitter.listen('changePreviewTabPreview', () => this.toggleActive(false));
     this.initEvent();
   }
 
-  private toggleActive(active: boolean) {
+  private toggleActive(active: boolean, isMarkdownTabMounted?: boolean) {
     toggleClass(this.el!, 'active', active);
     if (active) {
-      this.focus();
+      if (!isMarkdownTabMounted) {
+        this.focus();
+      }
     } else {
       this.blur();
     }
diff --git a/apps/editor/src/ui/components/toolbar/toolbar.ts b/apps/editor/src/ui/components/toolbar/toolbar.ts
index 84a11e8d..eb895dcb 100644
--- a/apps/editor/src/ui/components/toolbar/toolbar.ts
+++ b/apps/editor/src/ui/components/toolbar/toolbar.ts
@@ -271,7 +271,7 @@ export class Toolbar extends Component<Props, State> {
 
   mounted() {
     if (this.props.previewStyle === 'tab') {
-      this.props.eventEmitter.emit('changePreviewTabWrite');
+      this.props.eventEmitter.emit('changePreviewTabWrite', true);
     }
     // classify toolbar and dropdown toolbar after DOM has been rendered
     this.setState(this.classifyToolbarItems());
diff --git a/apps/editor/src/utils/common.ts b/apps/editor/src/utils/common.ts
index d723350a..f5ef7b43 100644
--- a/apps/editor/src/utils/common.ts
+++ b/apps/editor/src/utils/common.ts
@@ -12,6 +12,31 @@ const reEscapeHTML = /<([a-zA-Z_][a-zA-Z0-9\-._]*)(\s|[^\\/>])*\/?>|<(\/)([a-zA-
 const reEscapeBackSlash = /\\[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~\\]/g;
 const reEscapePairedChars = /[*_~`]/g;
 
+const XMLSPECIAL = '[&<>"]';
+const reXmlSpecial = new RegExp(XMLSPECIAL, 'g');
+
+function replaceUnsafeChar(char: string) {
+  switch (char) {
+    case '&':
+      return '&amp;';
+    case '<':
+      return '&lt;';
+    case '>':
+      return '&gt;';
+    case '"':
+      return '&quot;';
+    default:
+      return char;
+  }
+}
+
+export function escapeXml(text: string) {
+  if (reXmlSpecial.test(text)) {
+    return text.replace(reXmlSpecial, replaceUnsafeChar);
+  }
+  return text;
+}
+
 export function sendHostName() {
   sendHostname('editor', 'UA-129966929-1');
 }
diff --git a/apps/editor/src/wysiwyg/marks/link.ts b/apps/editor/src/wysiwyg/marks/link.ts
index a7064ba1..5d7c7de6 100644
--- a/apps/editor/src/wysiwyg/marks/link.ts
+++ b/apps/editor/src/wysiwyg/marks/link.ts
@@ -2,7 +2,8 @@ import { Mark as ProsemirrorMark, DOMOutputSpecArray } from 'prosemirror-model';
 import { toggleMark } from 'prosemirror-commands';
 
 import Mark from '@/spec/mark';
-import { encodeMarkdownText, escapeMarkdownText } from '@/utils/encoder';
+import { escapeMarkdownText } from '@/utils/encoder';
+import { escapeXml } from '@/utils/common';
 import { sanitizeHTML } from '@/sanitizer/htmlSanitizer';
 import { createTextNode } from '@/helper/manipulation';
 import { getCustomAttrs, getDefaultCustomAttrs } from '@/wysiwyg/helper/node';
@@ -67,7 +68,7 @@ export class Link extends Mark {
 
       if (from && to && linkUrl) {
         const attrs = {
-          linkUrl: encodeMarkdownText(linkUrl),
+          linkUrl: escapeXml(linkUrl),
           linkText: escapeMarkdownText(linkText),
         };
         const mark = schema.mark('link', attrs);
diff --git a/apps/editor/src/wysiwyg/nodes/heading.ts b/apps/editor/src/wysiwyg/nodes/heading.ts
index f7b4889a..61ffb1db 100644
--- a/apps/editor/src/wysiwyg/nodes/heading.ts
+++ b/apps/editor/src/wysiwyg/nodes/heading.ts
@@ -48,7 +48,10 @@ export class Heading extends NodeSchema {
   }
 
   commands(): EditorCommand {
-    return (payload) => (state, dispatch) =>
-      setBlockType(state.schema.nodes.heading, payload)(state, dispatch);
+    return (payload) => (state, dispatch) => {
+      const nodeType = state.schema.nodes[payload!.level ? 'heading' : 'paragraph'];
+
+      return setBlockType(nodeType, payload)(state, dispatch);
+    };
   }
 }
diff --git a/apps/editor/src/wysiwyg/nodes/image.ts b/apps/editor/src/wysiwyg/nodes/image.ts
index bad1ce95..bcc67b62 100644
--- a/apps/editor/src/wysiwyg/nodes/image.ts
+++ b/apps/editor/src/wysiwyg/nodes/image.ts
@@ -1,7 +1,7 @@
 import { Node as ProsemirrorNode, DOMOutputSpecArray } from 'prosemirror-model';
 
 import NodeSchema from '@/spec/node';
-import { encodeMarkdownText } from '@/utils/encoder';
+import { escapeXml } from '@/utils/common';
 import { sanitizeHTML } from '@/sanitizer/htmlSanitizer';
 
 import { EditorCommand } from '@t/spec';
@@ -63,7 +63,7 @@ export class Image extends NodeSchema {
       }
 
       const node = schema.nodes.image.createAndFill({
-        imageUrl: encodeMarkdownText(imageUrl),
+        imageUrl: escapeXml(imageUrl),
         ...(altText && { altText }),
       });
 


Test patch:
diff --git a/apps/editor/src/__test__/unit/convertor.spec.ts b/apps/editor/src/__test__/unit/convertor.spec.ts
index f3060b17..de38c415 100644
--- a/apps/editor/src/__test__/unit/convertor.spec.ts
+++ b/apps/editor/src/__test__/unit/convertor.spec.ts
@@ -161,7 +161,7 @@ describe('Convertor', () => {
       const expected = source`
         ![](imgUrl)
         ![altText](imgUrl)
-        ![altText](img\\*Url)
+        ![altText](img*Url)
     	`;
 
       assertConverting(markdown, expected);
@@ -172,11 +172,13 @@ describe('Convertor', () => {
         [](url)foo
         [text](url)
         [text](ur*l)
+        [Editor](https://github.com/nhn_test/tui.editor)
     	`;
       const expected = source`
         foo
         [text](url)
-        [text](ur\\*l)
+        [text](ur*l)
+        [Editor](https://github.com/nhn_test/tui.editor)
     	`;
 
       assertConverting(markdown, expected);
@@ -284,6 +286,21 @@ describe('Convertor', () => {
       assertConverting(markdown, `${expected}\n`);
     });
 
+    it('should normalize wrong table syntax when converting', () => {
+      const markdown = source`
+        | col1 | col2 | col3 |
+        | --- | --- |
+        | cell1 | cell2 | cell3 |
+      `;
+      const expected = source`
+        | col1 | col2 | col3 |
+        | ---- | ---- | ---- |
+        | cell1 | cell2 |  |
+      `;
+
+      assertConverting(markdown, `${expected}\n`);
+    });
+
     it('task', () => {
       const markdown = source`
         * [ ] foo
diff --git a/apps/editor/src/__test__/unit/markdown/mdCommand.spec.ts b/apps/editor/src/__test__/unit/markdown/mdCommand.spec.ts
index aeb81cef..5b9b268f 100644
--- a/apps/editor/src/__test__/unit/markdown/mdCommand.spec.ts
+++ b/apps/editor/src/__test__/unit/markdown/mdCommand.spec.ts
@@ -535,6 +535,15 @@ describe('bulletList command', () => {
 
     expect(getPreviewHTML()).toBe(result);
   });
+
+  it('should add bullet list syntax to empty line after heading node', () => {
+    mde.setMarkdown('# heading\n');
+
+    mde.setSelection([2, 1], [2, 1]);
+    cmd.exec('bulletList');
+
+    expect(getTextContent(mde)).toBe('# heading\n* ');
+  });
 });
 
 describe('orderedList command', () => {
@@ -732,6 +741,15 @@ describe('orderedList command', () => {
 
     expect(getPreviewHTML()).toBe(result);
   });
+
+  it('should add ordered list syntax to empty line after heading node', () => {
+    mde.setMarkdown('# heading\n');
+
+    mde.setSelection([2, 1], [2, 1]);
+    cmd.exec('orderedList');
+
+    expect(getTextContent(mde)).toBe('# heading\n1. ');
+  });
 });
 
 describe('taskList command', () => {
diff --git a/apps/editor/src/__test__/unit/wysiwyg/wwCommand.spec.ts b/apps/editor/src/__test__/unit/wysiwyg/wwCommand.spec.ts
index 3b6bcb61..3766e8ca 100644
--- a/apps/editor/src/__test__/unit/wysiwyg/wwCommand.spec.ts
+++ b/apps/editor/src/__test__/unit/wysiwyg/wwCommand.spec.ts
@@ -85,6 +85,15 @@ describe('wysiwyg commands', () => {
 
       expect(wwe.getHTML()).toBe('<h6>foo</h6>');
     });
+
+    it('should change heading element to paragraph with level 0', () => {
+      setTextToEditor('foo');
+
+      cmd.exec('selectAll');
+      cmd.exec('heading', { level: 0 });
+
+      expect(wwe.getHTML()).toBe('<p>foo</p>');
+    });
   });
 
   describe('hr command', () => {