Repository: withspectrum/draft-js-markdown-plugin (js)

Issue:
Add option to only allow certain markdown features
We need an option that only allows people to use inline markdown. (for messages) That would probably include:

- `**bold**`
- `*italic*`
- `inline code`
- `[links](http://example.com)`

Pull request:
Whitelist markdown features via `config.features`
- [x] whitelist block features
- [x] whitelist inline features
- [x] update tests
- [x] update readme to document whitelisted features

fixes #21

Example:

```js
// this will only enable BOLD for inline and CODE
// as well as header-one for blocks
const features = {
  inline: ['BOLD'],
  block: ['CODE', 'header-one'],
}
const plugin = createMarkdownPlugin({ features })
```

Patch:
diff --git a/README.md b/README.md
index 5862823..fb131e4 100644
--- a/README.md
+++ b/README.md
@@ -69,6 +69,64 @@ const languages = {
 const markdownPlugin = createMarkdownPlugin({ languages })
 ```
 
+### `features`
+A list of enabled features, by default all features are turned on.
+
+```js
+features = {
+  block: Array<string>,
+  inline: Array<string>,
+}
+```
+
+````
+#### Example
+
+```
+// this will only enable BOLD for inline and CODE
+// as well as header-one for blocks
+const features = {
+  inline: ['BOLD'],
+  block: ['CODE', 'header-one'],
+}
+const plugin = createMarkdownPlugin({ features })
+```
+
+*Available Inline features*:
+
+```js
+[
+  'BOLD',
+  'ITALIC',
+  'CODE',
+  'STRIKETHROUGH',
+  'LINK',
+  'IMAGE'
+]
+```
+
+*Available Block features*:
+
+```js
+import { CHECKABLE_LIST_ITEM } from "draft-js-checkable-list-item"
+[
+  'CODE',
+  'header-one',
+  'header-two',
+  'header-three',
+  'header-four',
+  'header-five',
+  'header-six',
+  'ordered-list-item',
+  'unordered-list-item',
+  // CHECKABLE_LIST_ITEM is a constant from 'draft-js-checkable-list-item'
+  // see import statementabove
+  CHECKABLE_LIST_ITEM,
+  'blockquote',
+]
+```
+
+
 ## Usage
 
 ```js
diff --git a/package.json b/package.json
index be336ad..57d3fb2 100644
--- a/package.json
+++ b/package.json
@@ -95,8 +95,7 @@
   "peerDependencies": {
     "draft-js-plugins-editor": "~2.0.0-rc.1 || 2.0.0-rc2 || 2.0.0-rc1 || 2.0.0-beta12",
     "react": "^15.0.0",
-    "react-dom": "^15.0.0",
-    "react-portal": "^4.1.4"
+    "react-dom": "^15.0.0"
   },
   "contributors": [
     "Atsushi Nagase <a@ngs.io>"
diff --git a/src/constants.js b/src/constants.js
index b0de780..b5991af 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -1,3 +1,5 @@
+import { CHECKABLE_LIST_ITEM } from "draft-js-checkable-list-item";
+
 export const CODE_BLOCK_REGEX = /^```([\w-]+)?\s*$/;
 
 export const inlineMatchers = {
@@ -8,3 +10,26 @@ export const inlineMatchers = {
 };
 
 export const CODE_BLOCK_TYPE = "code-block";
+
+export const defaultInlineWhitelist = [
+  "BOLD",
+  "ITALIC",
+  "CODE",
+  "STRIKETHROUGH",
+  "LINK",
+  "IMAGE",
+];
+
+export const defaultBlockWhitelist = [
+  "CODE",
+  "header-one",
+  "header-two",
+  "header-three",
+  "header-four",
+  "header-five",
+  "header-six",
+  "ordered-list-item",
+  "unordered-list-item",
+  CHECKABLE_LIST_ITEM,
+  "blockquote",
+];
diff --git a/src/index.js b/src/index.js
index 3d4a294..58e2745 100755
--- a/src/index.js
+++ b/src/index.js
@@ -29,7 +29,12 @@ import changeCurrentBlockType from "./modifiers/changeCurrentBlockType";
 import createLinkDecorator from "./decorators/link";
 import createImageDecorator from "./decorators/image";
 import { replaceText } from "./utils";
-import { CODE_BLOCK_REGEX, CODE_BLOCK_TYPE } from "./constants";
+import {
+  CODE_BLOCK_REGEX,
+  CODE_BLOCK_TYPE,
+  defaultInlineWhitelist,
+  defaultBlockWhitelist,
+} from "./constants";
 
 const defaultLanguages = {
   bash: "Bash",
@@ -85,21 +90,35 @@ function inCodeBlock(editorState) {
   return false;
 }
 
-function checkCharacterForState(editorState, character) {
-  let newEditorState = handleBlockType(editorState, character);
-  if (editorState === newEditorState) {
+function checkCharacterForState(config, editorState, character) {
+  let newEditorState = handleBlockType(
+    config.features.block,
+    editorState,
+    character
+  );
+  if (
+    editorState === newEditorState &&
+    config.features.inline.includes("IMAGE")
+  ) {
     newEditorState = handleImage(editorState, character);
   }
-  if (editorState === newEditorState) {
+  if (
+    editorState === newEditorState &&
+    config.features.inline.includes("LINK")
+  ) {
     newEditorState = handleLink(editorState, character);
   }
   if (editorState === newEditorState) {
-    newEditorState = handleInlineStyle(editorState, character);
+    newEditorState = handleInlineStyle(
+      config.features.inline,
+      editorState,
+      character
+    );
   }
   return newEditorState;
 }
 
-function checkReturnForState(editorState, ev) {
+function checkReturnForState(config, editorState, ev) {
   let newEditorState = editorState;
   const contentState = editorState.getCurrentContent();
   const selection = editorState.getSelection();
@@ -123,7 +142,7 @@ function checkReturnForState(editorState, ev) {
   ) {
     // transform markdown (if we aren't in a codeblock that is)
     if (!inCodeBlock(editorState)) {
-      newEditorState = checkCharacterForState(newEditorState, "\n");
+      newEditorState = checkCharacterForState(config, newEditorState, "\n");
     }
     if (newEditorState === editorState) {
       newEditorState = insertEmptyBlock(newEditorState);
@@ -134,6 +153,7 @@ function checkReturnForState(editorState, ev) {
   if (
     newEditorState === editorState &&
     type !== "code-block" &&
+    config.features.block.includes("CODE") &&
     CODE_BLOCK_REGEX.test(text)
   ) {
     newEditorState = handleNewCodeBlock(editorState);
@@ -157,6 +177,10 @@ function checkReturnForState(editorState, ev) {
 const defaultConfig = {
   renderLanguageSelect: defaultRenderSelect,
   languages: defaultLanguages,
+  features: {
+    inline: defaultInlineWhitelist,
+    block: defaultBlockWhitelist,
+  },
 };
 
 const createMarkdownPlugin = (_config = {}) => {
@@ -165,6 +189,10 @@ const createMarkdownPlugin = (_config = {}) => {
   const config = {
     ...defaultConfig,
     ..._config,
+    features: {
+      ...defaultConfig.features,
+      ..._config.features,
+    },
   };
 
   return {
@@ -237,7 +265,7 @@ const createMarkdownPlugin = (_config = {}) => {
     handleReturn(ev, editorState, { setEditorState }) {
       if (inLink(editorState)) return "not-handled";
 
-      let newEditorState = checkReturnForState(editorState, ev);
+      let newEditorState = checkReturnForState(config, editorState, ev);
       const selection = newEditorState.getSelection();
 
       // exit code blocks
@@ -249,7 +277,7 @@ const createMarkdownPlugin = (_config = {}) => {
         return "handled";
       }
 
-      newEditorState = checkCharacterForState(newEditorState, "\n");
+      newEditorState = checkCharacterForState(config, newEditorState, "\n");
       let content = newEditorState.getCurrentContent();
 
       // if there are actually no changes but the editorState has a
@@ -283,7 +311,11 @@ const createMarkdownPlugin = (_config = {}) => {
       // If we're in a link - don't transform markdown
       if (inLink(editorState)) return "not-handled";
 
-      const newEditorState = checkCharacterForState(editorState, character);
+      const newEditorState = checkCharacterForState(
+        config,
+        editorState,
+        character
+      );
       if (editorState !== newEditorState) {
         setEditorState(newEditorState);
         return "handled";
@@ -334,11 +366,19 @@ const createMarkdownPlugin = (_config = {}) => {
             newEditorState,
             buffer.join("") + text[i]
           );
-          newEditorState = checkCharacterForState(newEditorState, text[i]);
+          newEditorState = checkCharacterForState(
+            config,
+            newEditorState,
+            text[i]
+          );
           buffer = [];
         } else if (text[i].charCodeAt(0) === 10) {
           newEditorState = replaceText(newEditorState, buffer.join(""));
-          const tmpEditorState = checkReturnForState(newEditorState, {});
+          const tmpEditorState = checkReturnForState(
+            config,
+            newEditorState,
+            {}
+          );
           if (newEditorState === tmpEditorState) {
             newEditorState = insertEmptyBlock(tmpEditorState);
           } else {
diff --git a/src/modifiers/handleBlockType.js b/src/modifiers/handleBlockType.js
index 5c358c5..36c70f3 100644
--- a/src/modifiers/handleBlockType.js
+++ b/src/modifiers/handleBlockType.js
@@ -10,17 +10,18 @@ const sharps = len => {
   return ret;
 };
 
-const blockTypes = [
-  null,
-  "header-one",
-  "header-two",
-  "header-three",
-  "header-four",
-  "header-five",
-  "header-six",
-];
+const blockTypes = {
+  "#": "header-one",
+  "##": "header-two",
+  "###": "header-three",
+  "####": "header-four",
+  "#####": "header-five",
+  "######": "header-six",
+};
+
+const headerReg = /^(#+)\s+/;
 
-const handleBlockType = (editorState, character) => {
+const handleBlockType = (whiteList, editorState, character) => {
   const currentSelection = editorState.getSelection();
   const key = currentSelection.getStartKey();
   const text = editorState
@@ -33,26 +34,32 @@ const handleBlockType = (editorState, character) => {
   );
   const blockType = RichUtils.getCurrentBlockType(editorState);
 
+  const headerMatch = line.match(headerReg);
+
   if (blockType === "unstyled" || blockType === "paragraph") {
-    for (let i = 1; i <= 6; i += 1) {
-      if (line.indexOf(`${sharps(i)} `) === 0) {
-        return changeCurrentBlockType(
-          editorState,
-          blockTypes[i],
-          line.replace(/^#+\s/, "")
-        );
-      }
+    if (
+      headerMatch !== null &&
+      blockTypes[headerMatch[1]] !== null &&
+      whiteList.includes(blockTypes[headerMatch[1]])
+    ) {
+      return changeCurrentBlockType(
+        editorState,
+        blockTypes[headerMatch[1]],
+        line.replace(/^#+\s/, "")
+      );
     }
+
     let matchArr = line.match(/^[*-] (.*)$/);
-    if (matchArr) {
+    if (matchArr && whiteList.includes("unordered-list-item")) {
       return changeCurrentBlockType(
         editorState,
         "unordered-list-item",
         matchArr[1]
       );
     }
+
     matchArr = line.match(/^[\d]\. (.*)$/);
-    if (matchArr) {
+    if (matchArr && whiteList.includes("ordered-list-item")) {
       return changeCurrentBlockType(
         editorState,
         "ordered-list-item",
@@ -63,7 +70,10 @@ const handleBlockType = (editorState, character) => {
     if (matchArr) {
       return changeCurrentBlockType(editorState, "blockquote", matchArr[1]);
     }
-  } else if (blockType === "unordered-list-item") {
+  } else if (
+    blockType === "unordered-list-item" &&
+    whiteList.includes(CHECKABLE_LIST_ITEM)
+  ) {
     let matchArr = line.match(/^\[([x ])] (.*)$/i);
     if (matchArr) {
       return changeCurrentBlockType(
diff --git a/src/modifiers/handleInlineStyle.js b/src/modifiers/handleInlineStyle.js
index add7f5a..5705f8f 100644
--- a/src/modifiers/handleInlineStyle.js
+++ b/src/modifiers/handleInlineStyle.js
@@ -2,26 +2,27 @@ import changeCurrentInlineStyle from "./changeCurrentInlineStyle";
 import { EditorState, Modifier } from "draft-js";
 import { inlineMatchers } from "../constants";
 
-const handleChange = (editorState, line, character) => {
+const handleChange = (editorState, line, whitelist) => {
   let newEditorState = editorState;
-  Object.keys(inlineMatchers).some(k => {
-    inlineMatchers[k].some(re => {
-      let matchArr;
-      do {
-        matchArr = re.exec(line);
-        if (matchArr) {
-          newEditorState = changeCurrentInlineStyle(
-            newEditorState,
-            matchArr,
-            k,
-            character
-          );
-        }
-      } while (matchArr);
+  Object.keys(inlineMatchers)
+    .filter(matcher => whitelist.includes(matcher))
+    .some(k => {
+      inlineMatchers[k].some(re => {
+        let matchArr;
+        do {
+          matchArr = re.exec(line);
+          if (matchArr) {
+            newEditorState = changeCurrentInlineStyle(
+              newEditorState,
+              matchArr,
+              k
+            );
+          }
+        } while (matchArr);
+        return newEditorState !== editorState;
+      });
       return newEditorState !== editorState;
     });
-    return newEditorState !== editorState;
-  });
   return newEditorState;
 };
 
@@ -36,16 +37,16 @@ const getLine = (editorState, anchorOffset) => {
     .slice(0, selection.getFocusOffset());
 };
 
-const handleInlineStyle = (editorState, character) => {
+const handleInlineStyle = (whitelist, editorState, character) => {
   let selection = editorState.getSelection();
   let line = getLine(editorState, selection.getAnchorOffset());
-  let newEditorState = handleChange(editorState, line, "");
+  let newEditorState = handleChange(editorState, line, whitelist);
   let lastEditorState = editorState;
 
   while (newEditorState !== lastEditorState) {
     lastEditorState = newEditorState;
     line = getLine(newEditorState, selection.getAnchorOffset());
-    newEditorState = handleChange(newEditorState, line, "");
+    newEditorState = handleChange(newEditorState, line, whitelist);
   }
 
   if (newEditorState !== editorState) {


Test patch:
diff --git a/src/__test__/plugin.test.js b/src/__test__/plugin.test.js
index f2ab9fb..d8e5871 100755
--- a/src/__test__/plugin.test.js
+++ b/src/__test__/plugin.test.js
@@ -4,6 +4,8 @@ import {
   CheckableListItemUtils,
 } from "draft-js-checkable-list-item";
 
+import { defaultInlineWhitelist, defaultBlockWhitelist } from "../constants";
+
 import { Map, List } from "immutable";
 import createMarkdownPlugin from "../";
 
@@ -451,12 +453,7 @@ describe("draft-js-markdown-plugin", () => {
             ],
           };
         });
-        [
-          "handleBlockType",
-          "handleImage",
-          "handleLink",
-          "handleInlineStyle",
-        ].forEach(modifier => {
+        ["handleImage", "handleLink"].forEach(modifier => {
           describe(modifier, () => {
             beforeEach(() => {
               createMarkdownPlugin.__Rewire__(modifier, modifierSpy); // eslint-disable-line no-underscore-dangle
@@ -467,6 +464,36 @@ describe("draft-js-markdown-plugin", () => {
             });
           });
         });
+        ["handleBlockType"].forEach(modifier => {
+          describe(modifier, () => {
+            beforeEach(() => {
+              createMarkdownPlugin.__Rewire__(modifier, modifierSpy); // eslint-disable-line no-underscore-dangle
+            });
+            it("returns handled", () => {
+              expect(subject()).toBe("handled");
+              expect(modifierSpy).toHaveBeenCalledWith(
+                defaultBlockWhitelist,
+                currentEditorState,
+                " "
+              );
+            });
+          });
+        });
+        ["handleInlineStyle"].forEach(modifier => {
+          describe(modifier, () => {
+            beforeEach(() => {
+              createMarkdownPlugin.__Rewire__(modifier, modifierSpy); // eslint-disable-line no-underscore-dangle
+            });
+            it("returns handled", () => {
+              expect(subject()).toBe("handled");
+              expect(modifierSpy).toHaveBeenCalledWith(
+                defaultInlineWhitelist,
+                currentEditorState,
+                " "
+              );
+            });
+          });
+        });
         describe("character is not a space", () => {
           beforeEach(() => {
             character = "x";
diff --git a/src/modifiers/__test__/handleBlockType-test.js b/src/modifiers/__test__/handleBlockType-test.js
index 1ca5165..474da01 100644
--- a/src/modifiers/__test__/handleBlockType-test.js
+++ b/src/modifiers/__test__/handleBlockType-test.js
@@ -1,5 +1,6 @@
 import Draft, { EditorState, SelectionState } from "draft-js";
 import handleBlockType from "../handleBlockType";
+import { defaultBlockWhitelist } from "../../constants";
 
 describe("handleBlockType", () => {
   describe("no markup", () => {
@@ -31,7 +32,11 @@ describe("handleBlockType", () => {
       selection
     );
     it("does not convert block type", () => {
-      const newEditorState = handleBlockType(editorState, " ");
+      const newEditorState = handleBlockType(
+        defaultBlockWhitelist,
+        editorState,
+        " "
+      );
       expect(newEditorState).toEqual(editorState);
       expect(Draft.convertToRaw(newEditorState.getCurrentContent())).toEqual(
         rawContentState
@@ -69,7 +74,11 @@ describe("handleBlockType", () => {
     );
 
     it("does not convert block type", () => {
-      const newEditorState = handleBlockType(editorState, " ");
+      const newEditorState = handleBlockType(
+        defaultBlockWhitelist,
+        editorState,
+        " "
+      );
       expect(newEditorState).toEqual(editorState);
       expect(Draft.convertToRaw(newEditorState.getCurrentContent())).toEqual(
         rawContentState
@@ -549,7 +558,11 @@ describe("handleBlockType", () => {
         selection
       );
       it("converts block type", () => {
-        const newEditorState = handleBlockType(editorState, character);
+        const newEditorState = handleBlockType(
+          defaultBlockWhitelist,
+          editorState,
+          character
+        );
         expect(newEditorState).not.toEqual(editorState);
         expect(Draft.convertToRaw(newEditorState.getCurrentContent())).toEqual(
           after
diff --git a/src/modifiers/__test__/handleInlineStyle-test.js b/src/modifiers/__test__/handleInlineStyle-test.js
index de16d62..ed26390 100644
--- a/src/modifiers/__test__/handleInlineStyle-test.js
+++ b/src/modifiers/__test__/handleInlineStyle-test.js
@@ -2,6 +2,7 @@
 
 import Draft, { EditorState, SelectionState } from "draft-js";
 import handleInlineStyle from "../handleInlineStyle";
+import { defaultInlineWhitelist } from "../../constants";
 
 describe("handleInlineStyle", () => {
   describe("no markup", () => {
@@ -33,7 +34,11 @@ describe("handleInlineStyle", () => {
       selection
     );
     it("does not convert block type", () => {
-      const newEditorState = handleInlineStyle(editorState, " ");
+      const newEditorState = handleInlineStyle(
+        defaultInlineWhitelist,
+        editorState,
+        " "
+      );
       expect(newEditorState).toEqual(editorState);
       expect(Draft.convertToRaw(newEditorState.getCurrentContent())).toEqual(
         rawContentState
@@ -600,7 +605,11 @@ describe("handleInlineStyle", () => {
       );
 
       it("does not convert markdown to style or block type if selection is at the wrong place", () => {
-        const newEditorState = handleInlineStyle(sameEditorState, character);
+        const newEditorState = handleInlineStyle(
+          defaultInlineWhitelist,
+          sameEditorState,
+          character
+        );
         expect(newEditorState).toEqual(sameEditorState);
         expect(Draft.convertToRaw(newEditorState.getCurrentContent())).toEqual(
           before
@@ -608,7 +617,11 @@ describe("handleInlineStyle", () => {
       });
 
       it("converts markdown to style or block type", () => {
-        const newEditorState = handleInlineStyle(editorState, character);
+        const newEditorState = handleInlineStyle(
+          defaultInlineWhitelist,
+          editorState,
+          character
+        );
         expect(newEditorState).not.toEqual(editorState);
         expect(Draft.convertToRaw(newEditorState.getCurrentContent())).toEqual(
           after