Ryu Sato 8 лет назад
Родитель
Сommit
472b3d9c34

+ 0 - 29
resource/js/components/PageEditor/ContextBasedNewLineHandlerExecutor.js

@@ -1,29 +0,0 @@
-import * as codemirror from 'codemirror';
-
-import markdownListHelper from './MarkdownListHelper';
-import markdownTableHelper from './MarkdownTableHelper';
-
-/**
- * Selector for one new line handler
- */
-class ContextBasedNewLineHandlerExecutor {
-
-  /**
-   * select one handler from helper
-   * @param {any} editor An editor instance of CodeMirror
-   */
-  execNewLineHandler(editor) {
-    let newLineHelpers = [markdownTableHelper, markdownListHelper];
-    const helper = newLineHelpers.find( h => h.isMatchedContext(editor));
-    if (helper != undefined) {
-      helper.handleNewLine(editor);
-    } else {
-      codemirror.commands.newlineAndIndent(editor);
-    }
-  }
-}
-
-// singleton pattern
-const instance = new ContextBasedNewLineHandlerExecutor();
-Object.freeze(instance);
-export default instance;

+ 34 - 2
resource/js/components/PageEditor/Editor.js

@@ -34,9 +34,13 @@ require('codemirror/theme/twilight.css');
 import Dropzone from 'react-dropzone';
 
 import pasteHelper from './PasteHelper';
-import contextBasedNewLineHandlerExecutor from './ContextBasedNewLineHandlerExecutor';
 import emojiAutoCompleteHelper from './EmojiAutoCompleteHelper';
 
+import InterceptorManager from '../../../../lib/util/interceptor-manager';
+
+import AbortContinueMarkdownListInterceptor from '../../util/interceptor/AbortContinueMarkdownListInterceptor';
+import ReformMarkdownTableInterceptor from '../../util/interceptor/ReformMarkdownTableInterceptor';
+
 export default class Editor extends React.Component {
 
   constructor(props) {
@@ -45,6 +49,12 @@ export default class Editor extends React.Component {
     // https://regex101.com/r/7BN2fR/2
     this.indentAndMarkPattern = /^([ \t]*)(?:>|\-|\+|\*|\d+\.) /;
 
+    this.interceptorManager = new InterceptorManager();
+    this.interceptorManager.addInterceptors([
+      new AbortContinueMarkdownListInterceptor(),
+      new ReformMarkdownTableInterceptor(),
+    ]);
+
     this.state = {
       value: this.props.value,
       dropzoneActive: false,
@@ -56,6 +66,7 @@ export default class Editor extends React.Component {
     this.setScrollTopByLine = this.setScrollTopByLine.bind(this);
     this.forceToFocus = this.forceToFocus.bind(this);
     this.dispatchSave = this.dispatchSave.bind(this);
+    this.handleEnterKey = this.handleEnterKey.bind(this);
 
     this.onScrollCursorIntoView = this.onScrollCursorIntoView.bind(this);
     this.onPaste = this.onPaste.bind(this);
@@ -159,6 +170,27 @@ export default class Editor extends React.Component {
     }
   }
 
+  /**
+   * handle ENTER key
+   */
+  handleEnterKey() {
+
+    const editor = this.getCodeMirror();
+    var context = {
+      handlers: [],  // list of handlers which process enter key
+      editor: editor,
+    };
+
+    const interceptorManager = this.interceptorManager;
+    interceptorManager.process('preHandleEnter', context)
+      .then(() => {
+        if (context.handlers.length == 0) {
+          console.log('codemirror.commands.newlineAndIndentContinueMarkdownList(editor)');
+          codemirror.commands.newlineAndIndentContinueMarkdownList(editor);
+        }
+      });
+  }
+
   onScrollCursorIntoView(editor, event) {
     if (this.props.onScrollCursorIntoView != null) {
       const line = editor.getCursor().line;
@@ -343,7 +375,7 @@ export default class Editor extends React.Component {
               highlightFormatting: true,
               // continuelist, indentlist
               extraKeys: {
-                "Enter": contextBasedNewLineHandlerExecutor.execNewLineHandler,
+                "Enter": this.handleEnterKey,
                 "Tab": "indentMore",
                 "Shift-Tab": "indentLess",
                 "Ctrl-Q": (cm) => { cm.foldCode(cm.getCursor()) },

+ 1 - 31
resource/js/components/PageEditor/MarkdownListHelper.js

@@ -17,50 +17,20 @@ class MarkdownListHelper {
     this.getStrToEol = this.getStrToEol.bind(this);
   }
 
-  /**
-   * return whether context is matched by list
-   * @param {any} editor An editor instance of CodeMirror
-   */
-  isMatchedContext(editor) {
-    console.log('MarkdownListHelper.isMatchedContext');
-    // get strings from BOL(beginning of line) to current position
-    const strFromBol = this.getStrFromBol(editor);
-    const strToEol = this.getStrToEol(editor);
-    console.log('strToEol: ' + strToEol);
-    console.log('strFromBol: ' + strFromBol);
-    console.log('will return ' + (this.indentAndMarkRE.test(strToEol)
-                                 || this.indentAndMarkRE.test(strFromBol)
-                                 || this.indentAndMarkOnlyRE.test(strFromBol) ? 'true' : 'false'));
-    return this.indentAndMarkRE.test(strToEol)
-           || this.indentAndMarkRE.test(strFromBol)
-           || this.indentAndMarkOnlyRE.test(strFromBol);
-  }
-
-  /**
-   * handle new line
-   * @param {any} editor An editor instance of CodeMirror
-   */
-  handleNewLine(editor) {
-    console.log('MarkdownListHelper.handleNewLine');
-    this.newlineAndIndentContinueMarkdownList(editor);
-  }
-
   /**
    * wrap codemirror.commands.newlineAndIndentContinueMarkdownList
    * @param {any} editor An editor instance of CodeMirror
    */
   newlineAndIndentContinueMarkdownList(editor) {
-    console.log('MarkdownListHelper.newlineAndIndentContinueMarkdownList');
     // get strings from current position to EOL(end of line) before break the line
     const strToEol = this.getStrToEol(editor);
+
     if (this.indentAndMarkRE.test(strToEol)) {
-      console.log('MarkdownListHelper.newlineAndIndentContinueMarkdownList: abort auto indent');
       codemirror.commands.newlineAndIndent(editor);
       // replace the line with strToEol (abort auto indent)
       editor.getDoc().replaceRange(strToEol, this.getBol(editor), this.getEol(editor));
     }
     else {
-      console.log('MarkdownListHelper.newlineAndIndentContinueMarkdownList: will auto indent');
       codemirror.commands.newlineAndIndentContinueMarkdownList(editor);
     }
   }

+ 0 - 1
resource/js/components/PageEditor/PasteHelper.js

@@ -22,7 +22,6 @@ class PasteHelper {
     }
 
     markdownListHelper.pasteText(editor, event, text);
-    // [TODO] add markdownTableHelper
   }
 
   // Firefox versions prior to 53 return a bogus MIME type for every file drag, so dragovers with

+ 193 - 0
resource/js/util/interceptor/AbortContinueMarkdownListInterceptor.js

@@ -0,0 +1,193 @@
+import { BasicInterceptor } from 'crowi-pluginkit';
+
+import * as codemirror from 'codemirror';
+
+import markdownTable from 'markdown-table';
+
+/**
+ * The interceptor that reform markdown table
+ */
+export default class AbortContinueMarkdownListInterceptor extends BasicInterceptor {
+
+  constructor() {
+    super();
+
+    // https://github.com/codemirror/CodeMirror/blob/c7853a989c77bb9f520c9c530cbe1497856e96fc/addon/edit/continuelist.js#L14
+    // https://regex101.com/r/7BN2fR/5
+    this.indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/;
+    this.indentAndMarkOnlyRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
+
+    this.pasteText = this.pasteText.bind(this);
+
+    this.getBol = this.getBol.bind(this);
+    this.getEol = this.getEol.bind(this);
+    this.getStrFromBol = this.getStrFromBol.bind(this);
+    this.getStrToEol = this.getStrToEol.bind(this);
+  }
+
+  /**
+   * @inheritdoc
+   */
+  isInterceptWhen(contextName) {
+    return (
+      contextName === 'preHandleEnter'
+    );
+  }
+
+  /**
+   * return boolean value whether processable parallel
+   */
+  isProcessableParallel() {
+    return false;
+  }
+
+  /**
+   * @inheritdoc
+   */
+  process(contextName, ...args) {
+    const context = Object.assign(args[0]);   // clone
+    const editor = context.editor;
+
+    console.log('AbortContinueMarkdownListInterceptor.process');
+
+    // get strings from current position to EOL(end of line) before break the line
+    const strToEol = this.getStrToEol(editor);
+    if (this.indentAndMarkRE.test(strToEol)) {
+      console.log('AbortContinueMarkdownListInterceptor.newlineAndIndentContinueMarkdownList: abort auto indent');
+      codemirror.commands.newlineAndIndent(editor);
+      // replace the line with strToEol (abort auto indent)
+      editor.getDoc().replaceRange(strToEol, this.getBol(editor), this.getEol(editor));
+
+      // report to manager that handling was done
+      context.handlers.push(this.className);
+    }
+
+    // resolve
+    return Promise.resolve(context);
+  }
+
+  /**
+   * paste text
+   * @param {any} editor An editor instance of CodeMirror
+   * @param {any} event
+   * @param {string} text
+   */
+  pasteText(editor, event, text) {
+    // get strings from BOL(beginning of line) to current position
+    const strFromBol = this.getStrFromBol(editor);
+
+    const matched = strFromBol.match(this.indentAndMarkRE);
+    // when match indentAndMarkOnlyRE
+    // (this means the current position is the beginning of the list item)
+    if (this.indentAndMarkOnlyRE.test(strFromBol)) {
+      const adjusted = this.adjustPastedData(strFromBol, text);
+
+      // replace
+      if (adjusted != null) {
+        event.preventDefault();
+        editor.getDoc().replaceRange(adjusted, this.getBol(editor), editor.getCursor());
+      }
+    }
+  }
+
+  /**
+   * return adjusted pasted data by indentAndMark
+   *
+   * @param {string} indentAndMark
+   * @param {string} text
+   * @returns adjusted pasted data
+   *      returns null when adjustment is not necessary
+   */
+  adjustPastedData(indentAndMark, text) {
+    let adjusted = null;
+
+    // list data (starts with indent and mark)
+    if (text.match(this.indentAndMarkRE)) {
+      const indent = indentAndMark.match(this.indentAndMarkRE)[1];
+
+      // splice to an array of line
+      const lines = text.match(/[^\r\n]+/g);
+      // indent
+      const replacedLines = lines.map((line) => {
+        return indent + line;
+      })
+
+      adjusted = replacedLines.join('\n');
+    }
+    // listful data
+    else if (this.isListfulData(text)) {
+      // do nothing (return null)
+    }
+    // not listful data
+    else {
+      // append `indentAndMark` at the beginning of all lines (except the first line)
+      const replacedText = text.replace(/(\r\n|\r|\n)/g, "$1" + indentAndMark);
+      // append `indentAndMark` to the first line
+      adjusted = indentAndMark + replacedText;
+    }
+
+    return adjusted;
+  }
+
+  /**
+   * evaluate whether `text` is list like data or not
+   * @param {string} text
+   */
+  isListfulData(text) {
+    // return false if includes at least one blank line
+    // see https://stackoverflow.com/a/16369725
+    if (text.match(/^\s*[\r\n]/m) != null) {
+      return false;
+    }
+
+    const lines = text.match(/[^\r\n]+/g);
+    // count lines that starts with indent and mark
+    let isListful = false;
+    let count = 0;
+    lines.forEach((line) => {
+      if (line.match(this.indentAndMarkRE)) {
+        count++;
+      }
+      // ensure to be true if it is 50% or more
+      if (count >= lines.length / 2) {
+        isListful = true;
+        return;
+      }
+    });
+
+    return isListful;
+  }
+
+  /**
+   * return the postion of the BOL(beginning of line)
+   */
+  getBol(editor) {
+    const curPos = editor.getCursor();
+    return { line: curPos.line, ch: 0 };
+  }
+
+  /**
+   * return the postion of the EOL(end of line)
+   */
+  getEol(editor) {
+    const curPos = editor.getCursor();
+    const lineLength = editor.getDoc().getLine(curPos.line).length;
+    return { line: curPos.line, ch: lineLength };
+  }
+
+  /**
+   * return strings from BOL(beginning of line) to current position
+   */
+  getStrFromBol(editor) {
+    const curPos = editor.getCursor();
+    return editor.getDoc().getRange(this.getBol(editor), curPos);
+  }
+
+  /**
+   * return strings from current position to EOL(end of line)
+   */
+  getStrToEol(editor) {
+    const curPos = editor.getCursor();
+    return editor.getDoc().getRange(curPos, this.getEol(editor));
+  }
+}

+ 44 - 54
resource/js/components/PageEditor/MarkdownTableHelper.js → resource/js/util/interceptor/ReformMarkdownTableInterceptor.js

@@ -1,21 +1,20 @@
-import * as codemirror from 'codemirror';
+import { BasicInterceptor } from 'crowi-pluginkit';
 
 import markdownTable from 'markdown-table';
 
-class MarkdownTableHelper {
+/**
+ * The interceptor that reform markdown table
+ */
+export default class ReformMarkdownTableInterceptor extends BasicInterceptor {
 
   constructor() {
+    super();
+
     // https://github.com/markdown-it/markdown-it/blob/d29f421927e93e88daf75f22089a3e732e195bd2/lib/rules_block/table.js#L83
     // https://regex101.com/r/7BN2fR/7
     this.tableAlignmentLineRE = /^[-:|][-:|\s]*$/;
     this.linePartOfTableRE = /^\|[^\r\n]*|[^\r\n]*\|$|([^\|\r\n]+\|[^\|\r\n]*)+/; // own idea
 
-    this.isMatchedContext = this.isMatchedContext.bind(this);
-    this.handleNewLine = this.handleNewLine.bind(this);
-
-    this.newlineAndIndentContinueMarkdownTable = this.newlineAndIndentContinueMarkdownTable.bind(this);
-    this.pasteText = this.pasteText.bind(this);
-
     this.getBot = this.getBot.bind(this);
     this.getEot = this.getEot.bind(this);
     this.getBol = this.getBol.bind(this);
@@ -26,57 +25,53 @@ class MarkdownTableHelper {
   }
 
   /**
-   * return whether context is matched by table
-   * @param {any} editor An editor instance of CodeMirror
+   * @inheritdoc
    */
-  isMatchedContext(editor) {
-    console.log('MarkdownTableHelper.isMatchedContext');
-    // get strings from BOL(beginning of line) to current position
-    const strFromBol = this.getStrFromBol(editor);
-    console.log('strFromBol: ' + strFromBol);
-    console.log('will return ' + (this.linePartOfTableRE.test(strFromBol) ? 'true' : 'false'));
-    return this.linePartOfTableRE.test(strFromBol);
+  isInterceptWhen(contextName) {
+    return (
+      contextName === 'preHandleEnter'
+    );
   }
 
   /**
-   * handle new line
-   * @param {any} editor An editor instance of CodeMirror
+   * return boolean value whether processable parallel
    */
-  handleNewLine(editor) {
-    console.log('MarkdownTableHelper.handleNewLine');
-    this.newlineAndIndentContinueMarkdownTable(editor);
+  isProcessableParallel() {
+    return false;
   }
 
   /**
-   * insert new line with auto shaping format of Markdown table
-   * @param {any} editor An editor instance of CodeMirror
+   * @inheritdoc
    */
-  newlineAndIndentContinueMarkdownTable(editor) {
-    console.log('MarkdownTableHelper.newlineAndIndentContinueMarkdownTable');
-    if (!this.isMatchedContext(editor)) return;
-
-    // get lines all of table from current position to beginning of table
-    const strTableLines = this.getStrFromBot(editor);
-    console.log('strTableLines: ' + strTableLines);
-
-    const table = this.parseFromTableStringToJSON(editor, this.getBot(editor), editor.getCursor());
-    console.log('table: ' + JSON.stringify(table));
-    const strTableLinesFormated = table;
-    console.log('strTableLinesFormated: ' + strTableLinesFormated);
-
-    // replace the lines to strFormatedTableLines
-    editor.getDoc().replaceRange(strTableLinesFormated, this.getBot(editor), editor.getCursor());
-    codemirror.commands.newlineAndIndent(editor);
-  }
+  process(contextName, ...args) {
+    const context = Object.assign(args[0]);   // clone
+    const editor = context.editor;
 
-  /**
-   * paste text
-   * @param {any} editor An editor instance of CodeMirror
-   * @param {any} event
-   * @param {string} text
-   */
-  pasteText(editor, event, text) {
-    // [TODO] replace to formated table markdown
+    console.log('MarkdownTableHelper.process');
+
+    // get strings from BOL(beginning of line) to current position
+    const strFromBol = this.getStrFromBol(editor);
+
+    if (this.linePartOfTableRE.test(strFromBol)) {
+
+      // get lines all of table from current position to beginning of table
+      const strTableLines = this.getStrFromBot(editor);
+      console.log('strTableLines: ' + strTableLines);
+
+      const table = this.parseFromTableStringToJSON(editor, this.getBot(editor), editor.getCursor());
+      console.log('table: ' + JSON.stringify(table));
+      const strTableLinesFormated = table;
+      console.log('strTableLinesFormated: ' + strTableLinesFormated);
+
+      // replace the lines to strFormatedTableLines
+      editor.getDoc().replaceRange(strTableLinesFormated, this.getBot(editor), editor.getCursor());
+
+      // report to manager that handling was done
+      context.handlers.push(this.className);
+    }
+
+    // resolve
+    return Promise.resolve(context);
   }
 
   /**
@@ -190,8 +185,3 @@ class MarkdownTableHelper {
     return markdownTable(contents, { align: aligns } );
   }
 }
-
-// singleton pattern
-const instance = new MarkdownTableHelper();
-Object.freeze(instance);
-export default instance;