Explorar o código

Merge pull request #299 from weseek/feat/autoformat-markdown-for-table

Feat/autoformat markdown for table
Yuki Takei %!s(int64=8) %!d(string=hai) anos
pai
achega
499fb9dbe9

+ 1 - 0
package.json

@@ -101,6 +101,7 @@
     "markdown-it-plantuml": "^1.0.0",
     "markdown-it-task-lists": "^2.1.0",
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.2",
+    "markdown-table": "^1.1.1",
     "md5": "^2.2.1",
     "method-override": "^2.3.10",
     "mkdirp": "~0.5.1",

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

@@ -34,9 +34,12 @@ require('codemirror/theme/twilight.css');
 import Dropzone from 'react-dropzone';
 
 import pasteHelper from './PasteHelper';
-import markdownListHelper from './MarkdownListHelper';
 import emojiAutoCompleteHelper from './EmojiAutoCompleteHelper';
 
+import InterceptorManager from '../../../../lib/util/interceptor-manager';
+
+import MarkdownListInterceptor from './MarkdownListInterceptor';
+import MarkdownTableInterceptor from './MarkdownTableInterceptor';
 
 export default class Editor extends React.Component {
 
@@ -46,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 MarkdownListInterceptor(),
+      new MarkdownTableInterceptor(),
+    ]);
+
     this.state = {
       value: this.props.value,
       dropzoneActive: false,
@@ -57,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);
@@ -160,6 +170,26 @@ 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) {
+          codemirror.commands.newlineAndIndentContinueMarkdownList(editor);
+        }
+      });
+  }
+
   onScrollCursorIntoView(editor, event) {
     if (this.props.onScrollCursorIntoView != null) {
       const line = editor.getCursor().line;
@@ -344,7 +374,7 @@ export default class Editor extends React.Component {
               highlightFormatting: true,
               // continuelist, indentlist
               extraKeys: {
-                "Enter": markdownListHelper.newlineAndIndentContinueMarkdownList,
+                "Enter": this.handleEnterKey,
                 "Tab": "indentMore",
                 "Shift-Tab": "indentLess",
                 "Ctrl-Q": (cm) => { cm.foldCode(cm.getCursor()) },

+ 47 - 0
resource/js/components/PageEditor/MarkdownListInterceptor.js

@@ -0,0 +1,47 @@
+import { BasicInterceptor } from 'crowi-pluginkit';
+import * as codemirror from 'codemirror';
+
+import mlu from './MarkdownListUtil';
+
+export default class MarkdownListInterceptor extends BasicInterceptor {
+
+  constructor() {
+    super();
+  }
+
+  /**
+   * @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;
+
+    // get strings from current position to EOL(end of line) before break the line
+    const strToEol = mlu.getStrToEol(editor);
+    if (mlu.indentAndMarkRE.test(strToEol)) {
+      mlu.newlineWithoutIndent(editor, strToEol);
+
+      // report to manager that handling was done
+      context.handlers.push(this.className);
+    }
+
+    // resolve
+    return Promise.resolve(context);
+  }
+}

+ 16 - 23
resource/js/components/PageEditor/MarkdownListHelper.js → resource/js/components/PageEditor/MarkdownListUtil.js

@@ -1,6 +1,7 @@
-import * as codemirror from 'codemirror';
-
-class MarkdownListHelper {
+/**
+ * Utility for markdown list
+ */
+class MarkdownListUtil {
 
   constructor() {
     // https://github.com/codemirror/CodeMirror/blob/c7853a989c77bb9f520c9c530cbe1497856e96fc/addon/edit/continuelist.js#L14
@@ -8,31 +9,13 @@ class MarkdownListHelper {
     this.indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/;
     this.indentAndMarkOnlyRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
 
-    this.newlineAndIndentContinueMarkdownList = this.newlineAndIndentContinueMarkdownList.bind(this);
     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);
-  }
-
-  /**
-   * wrap codemirror.commands.newlineAndIndentContinueMarkdownList
-   * @param {any} editor An editor instance of CodeMirror
-   */
-  newlineAndIndentContinueMarkdownList(editor) {
-    // get strings from current position to EOL(end of line) before break the line
-    const strToEol = this.getStrToEol(editor);
-
-    if (this.indentAndMarkRE.test(strToEol)) {
-      codemirror.commands.newlineAndIndent(editor);
-      // replace the line with strToEol (abort auto indent)
-      editor.getDoc().replaceRange(strToEol, this.getBol(editor), this.getEol(editor));
-    }
-    else {
-      codemirror.commands.newlineAndIndentContinueMarkdownList(editor);
-    }
+    this.newlineWithoutIndent = this.newlineWithoutIndent.bind(this);
   }
 
   /**
@@ -159,9 +142,19 @@ class MarkdownListHelper {
     const curPos = editor.getCursor();
     return editor.getDoc().getRange(curPos, this.getEol(editor));
   }
+
+  /**
+   * insert newline without indent
+   */
+  newlineWithoutIndent(editor, strToEol) {
+    codemirror.commands.newlineAndIndent(editor);
+
+    // replace the line with strToEol (abort auto indent)
+    editor.getDoc().replaceRange(strToEol, this.getBol(editor), this.getEol(editor));
+  }
 }
 
 // singleton pattern
-const instance = new MarkdownListHelper();
+const instance = new MarkdownListUtil();
 Object.freeze(instance);
 export default instance;

+ 62 - 0
resource/js/components/PageEditor/MarkdownTableInterceptor.js

@@ -0,0 +1,62 @@
+import { BasicInterceptor } from 'crowi-pluginkit';
+
+import mtu from './MarkdownTableUtil';
+
+/**
+ * Interceptor for markdown table
+ */
+export default class MarkdownTableInterceptor extends BasicInterceptor {
+
+  constructor() {
+    super();
+  }
+
+  /**
+   * @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;
+
+    // get strings from BOL(beginning of line) to current position
+    const strFromBol = mtu.getStrFromBol(editor);
+
+    if (mtu.isEndOfLine(editor) && mtu.linePartOfTableRE.test(strFromBol)) {
+      // get lines all of table from current position to beginning of table
+      const strFromBot = mtu.getStrFromBot(editor);
+      let table = mtu.parseFromTableStringToMarkdownTable(strFromBot);
+
+      mtu.addRowToMarkdownTable(table);
+
+      const strToEot = mtu.getStrToEot(editor);
+      const tableBottom = mtu.parseFromTableStringToMarkdownTable(strToEot);
+      if (tableBottom.table.length > 0) {
+        table = mtu.mergeMarkdownTable([table, tableBottom]);
+      }
+
+      mtu.replaceMarkdownTableWithReformed(editor, table);
+
+      // report to manager that handling was done
+      context.handlers.push(this.className);
+    }
+
+    // resolve
+    return Promise.resolve(context);
+  }
+}

+ 209 - 0
resource/js/components/PageEditor/MarkdownTableUtil.js

@@ -0,0 +1,209 @@
+import markdown_table from 'markdown-table';
+
+/**
+ * Utility for markdown table
+ */
+class MarkdownTableUtil {
+
+  constructor() {
+    // 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.tableAlignmentLineNegRE = /^[^-:]*$/;  // it is need to check to ignore empty row which is matched above RE
+    this.linePartOfTableRE = /^\|[^\r\n]*|[^\r\n]*\|$|([^\|\r\n]+\|[^\|\r\n]*)+/; // own idea
+
+    this.getBot = this.getBot.bind(this);
+    this.getEot = this.getEot.bind(this);
+    this.getBol = this.getBol.bind(this);
+    this.getStrFromBot = this.getStrFromBot.bind(this);
+    this.getStrToEot = this.getStrToEot.bind(this);
+    this.getStrFromBol = this.getStrFromBol.bind(this);
+
+    this.parseFromTableStringToMarkdownTable = this.parseFromTableStringToMarkdownTable.bind(this);
+    this.replaceMarkdownTableWithReformed = this.replaceMarkdownTableWithReformed.bind(this);
+  }
+
+  /**
+   * return the postion of the BOT(beginning of table)
+   * (It is assumed that current line is a part of table)
+   */
+  getBot(editor) {
+    const firstLine = editor.getDoc().firstLine();
+    const curPos = editor.getCursor();
+    let line = curPos.line - 1;
+    for (; line >= firstLine; line--) {
+      const strLine = editor.getDoc().getLine(line);
+      if (!this.linePartOfTableRE.test(strLine)) {
+        break;
+      }
+    }
+    const botLine = Math.max(firstLine, line + 1);
+    return { line: botLine, ch: 0 };
+  }
+
+  /**
+   * return the postion of the EOT(end of table)
+   * (It is assumed that current line is a part of table)
+   */
+  getEot(editor) {
+    const lastLine = editor.getDoc().lastLine();
+    const curPos = editor.getCursor();
+    let line = curPos.line + 1;
+    for (; line <= lastLine; line++) {
+      const strLine = editor.getDoc().getLine(line);
+      if (!this.linePartOfTableRE.test(strLine)) {
+        break;
+      }
+    }
+    const eotLine = Math.min(line - 1, lastLine);
+    const lineLength = editor.getDoc().getLine(eotLine).length;
+    return { line: eotLine, ch: lineLength };
+  }
+
+  /**
+   * return the postion of the BOL(beginning of line)
+   */
+  getBol(editor) {
+    const curPos = editor.getCursor();
+    return { line: curPos.line, ch: 0 };
+  }
+
+  /**
+   * return strings from BOT(beginning of table) to current position
+   */
+  getStrFromBot(editor) {
+    const curPos = editor.getCursor();
+    return editor.getDoc().getRange(this.getBot(editor), curPos);
+  }
+
+  /**
+   * return strings from current position to EOT(end of table)
+   */
+  getStrToEot(editor) {
+    const curPos = editor.getCursor();
+    return editor.getDoc().getRange(curPos, this.getEot(editor));
+  }
+
+  /**
+   * return strings from BOL(beginning of line) to current position
+   */
+  getStrFromBol(editor) {
+    const curPos = editor.getCursor();
+    return editor.getDoc().getRange(this.getBol(editor), curPos);
+  }
+
+  /**
+   * returns markdown table whose described by 'markdown-table' format
+   *   ref. https://github.com/wooorm/markdown-table
+   * @param {string} lines all of table
+   */
+  parseFromTableStringToMarkdownTable(strMDTable) {
+    const arrMDTableLines = strMDTable.split(/(\r\n|\r|\n)/);
+    let contents = [];
+    let aligns = [];
+    for (let n = 0; n < arrMDTableLines.length; n++) {
+      const line = arrMDTableLines[n];
+
+      if (this.tableAlignmentLineRE.test(line) && !this.tableAlignmentLineNegRE.test(line)) {
+        // parse line which described alignment
+        const alignRuleRE = [
+          { align: 'c', regex: /^:-+:$/ },
+          { align: 'l', regex: /^:-+$/  },
+          { align: 'r', regex: /^-+:$/  },
+        ];
+        let lineText = "";
+        lineText = line.replace(/^\||\|$/g, ''); // strip off pipe charactor which is placed head of line and last of line.
+        lineText = lineText.replace(/\s*/g, '');
+        aligns = lineText.split(/\|/).map(col => {
+          const rule = alignRuleRE.find(rule => col.match(rule.regex));
+          return (rule != undefined) ? rule.align : '';
+        });
+      } else if (this.linePartOfTableRE.test(line)) {
+        // parse line whether header or body
+        let lineText = "";
+        lineText = line.replace(/\s*\|\s*/g, '|');
+        lineText = lineText.replace(/^\||\|$/g, ''); // strip off pipe charactor which is placed head of line and last of line.
+        const row = lineText.split(/\|/);
+        contents.push(row);
+      }
+    }
+    return (new MarkdownTable(contents, { align: aligns }));
+  }
+
+  /**
+   * return boolean value whether the current position of cursor is end of line
+   */
+  isEndOfLine(editor) {
+    const curPos = editor.getCursor();
+    return (curPos.ch == editor.getDoc().getLine(curPos.line).length);
+  }
+
+  /**
+   * add a row at the end
+   * (This function overwrite directory markdown table specified as argument.)
+   * @param {MarkdownTable} markdown table
+   */
+  addRowToMarkdownTable(mdtable) {
+    const numCol = mdtable.table.length > 0 ? mdtable.table[0].length : 1;
+    let newRow = [];
+    (new Array(numCol)).forEach(() => newRow.push('')); // create cols
+    mdtable.table.push(newRow);
+  }
+
+  /**
+   * returns markdown table that is merged all of markdown table in array
+   * (The merged markdown table options are used for the first markdown table.)
+   * @param {Array} array of markdown table
+   */
+  mergeMarkdownTable(mdtable_list) {
+    if (mdtable_list == undefined
+        || !(mdtable_list instanceof Array)) {
+      return undefined;
+    }
+
+    let newTable = [];
+    const options = mdtable_list[0].options; // use option of first markdown-table
+    mdtable_list.forEach((mdtable) => {
+      newTable = newTable.concat(mdtable.table)
+    });
+    return (new MarkdownTable(newTable, options));
+  }
+
+  /**
+   * replace markdown table which is reformed by markdown-table
+   * @param {MarkdownTable} markdown table
+   */
+  replaceMarkdownTableWithReformed(editor, table) {
+    const curPos = editor.getCursor();
+
+    // replace the lines to strTableLinesFormated
+    const strTableLinesFormated = table.toString();
+    editor.getDoc().replaceRange(strTableLinesFormated, this.getBot(editor), this.getEot(editor));
+
+    // set cursor to first column
+    editor.getDoc().setCursor(curPos.line + 1, 2);
+  }
+}
+
+/**
+ * markdown table class for markdown-table module
+ *   ref. https://github.com/wooorm/markdown-table
+ */
+class MarkdownTable {
+
+  constructor(table, options) {
+    this.table = table || [];
+    this.options = options || {};
+
+    this.toString = this.toString.bind(this);
+  }
+
+  toString() {
+    return markdown_table(this.table, this.options);
+  }
+}
+
+// singleton pattern
+const instance = new MarkdownTableUtil();
+Object.freeze(instance);
+export default instance;

+ 2 - 2
resource/js/components/PageEditor/PasteHelper.js

@@ -1,6 +1,6 @@
 import accepts from 'attr-accept'
 
-import markdownListHelper from './MarkdownListHelper';
+import markdownListUtil from './MarkdownListUtil';
 
 class PasteHelper {
 
@@ -21,7 +21,7 @@ class PasteHelper {
       return;
     }
 
-    markdownListHelper.pasteText(editor, event, text);
+    markdownListUtil.pasteText(editor, event, text);
   }
 
   // Firefox versions prior to 53 return a bogus MIME type for every file drag, so dragovers with

+ 4 - 0
yarn.lock

@@ -4134,6 +4134,10 @@ markdown-it@^8.4.0:
     mdurl "^1.0.1"
     uc.micro "^1.0.3"
 
+markdown-table@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-1.1.1.tgz#4b3dd3a133d1518b8ef0dbc709bf2a1b4824bc8c"
+
 marked-terminal@^1.6.2:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.7.0.tgz#c8c460881c772c7604b64367007ee5f77f125904"