Procházet zdrojové kódy

Merge branch 'master' into imprv/enable-to-select-highlight.js-style

Yuki Takei před 8 roky
rodič
revize
7839eaa155

+ 63 - 0
.eslintrc.js

@@ -0,0 +1,63 @@
+module.exports = {
+  "env": {
+    "browser": true,
+    "commonjs": true,
+    "es6": true,
+    "node": true
+  },
+  "extends": "eslint:recommended",
+  "parserOptions": {
+    "ecmaFeatures": {
+      "experimentalObjectRestSpread": true,
+      "jsx": true
+    },
+    "sourceType": "module"
+  },
+  "plugins": [
+    "react"
+  ],
+  "rules": {
+    "brace-style": [
+      "error",
+      "stroustrup", { "allowSingleLine": true }
+    ],
+    "comma-spacing": [
+      "error",
+      { "before": false, "after": true }
+    ],
+    "func-call-spacing": [
+      "error",
+      "never"
+    ],
+    "indent": [
+      "error",
+      2
+    ],
+    "key-spacing": [
+      "error", { "beforeColon": false, "afterColon": true }
+    ],
+    "keyword-spacing": [
+      "error", {}
+    ],
+    "linebreak-style": [
+      "error",
+      "unix"
+    ],
+    "quotes": [
+      "error",
+      "single"
+    ],
+    "semi": [
+      "error",
+      "always"
+    ],
+    "space-before-blocks": [
+      "error",
+      "always"
+    ],
+    "space-before-function-paren": [
+      "error",
+      "never"
+    ]
+  }
+};

+ 0 - 14
.jshintrc

@@ -1,14 +0,0 @@
-{
-  "curly": true,
-  "eqnull": false,
-  "forin": true,
-  "freeze": true,
-  "immed": true,
-  "indent": 2,
-  "laxcomma": true,
-  "newcap": true,
-  "node": true,
-  "quotmark": "single",
-  "trailing": true,
-  "undef": true
-}

+ 6 - 0
CHANGES.md

@@ -3,8 +3,14 @@ CHANGES
 
 ## 2.4.4-RC
 
+* Feature: Autoformat Markdown Table
+* Fix: The bug of updating numbering list by codemirror
 * Fix: Template LangProcessor doesn't work
     * Introduced by 2.4.0
+* Support: Apply ESLint
+* Support: Upgrade libs
+    * react, react-dom
+    * codemirror, react-codemirror2
 
 ## 2.4.3
 

+ 2 - 3
lib/views/admin/customize.html

@@ -3,10 +3,9 @@
 {% block html_title %}{{ t('Customize') }} {% endblock %}
 
 {% block html_additional_headers %}
+  {% parent %}
   <!-- CodeMirror -->
-  <link rel="stylesheet" href="https://cdn.jsdelivr.net/g/codemirror@4.5.0(codemirror.css+addon/hint/show-hint.css)">
   <link rel="stylesheet" href="https://cdn.jsdelivr.net/jquery.ui/1.11.4/jquery-ui.min.css">
-  <link rel="stylesheet" href="https://cdn.jsdelivr.net/codemirror/4.5.0/theme/eclipse.css">
   <style>
     .CodeMirror {
       border: 1px solid #eee;
@@ -255,7 +254,7 @@ export  $initHighlight;</code></pre>
 
         <p class="help-block">
           Examples:
-          <pre><code>&lt;script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js" defer&gt;&lt;script&gt;</script></code></pre>
+          <pre><code>&lt;script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js" defer&gt;&lt;/script&gt;</code></pre>
         </p>
 
         <div class="form-group">

+ 7 - 4
package.json

@@ -61,7 +61,7 @@
     "body-parser": "^1.18.2",
     "bootstrap-sass": "~3.3.6",
     "check-node-version": "^3.1.1",
-    "codemirror": "^5.33.0",
+    "codemirror": "^5.36.0",
     "connect-flash": "~0.1.1",
     "connect-redis": "^3.3.0",
     "cookie-parser": "^1.4.3",
@@ -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",
@@ -118,12 +119,12 @@
     "passport-local": "^1.0.0",
     "pino-clf": "^1.0.2",
     "plantuml-encoder": "^1.2.5",
-    "react": "^16.0.0",
+    "react": "^16.2.0",
     "react-bootstrap": "^0.32.0",
     "react-bootstrap-typeahead": "^2.0.2",
     "react-clipboard.js": "^1.1.2",
-    "react-codemirror2": "^4.0.0",
-    "react-dom": "^16.0.0",
+    "react-codemirror2": "^4.2.1",
+    "react-dom": "^16.2.0",
     "react-dropzone": "^4.2.7",
     "redis": "^2.7.1",
     "reveal.js": "^3.5.0",
@@ -149,6 +150,8 @@
     "colors": "^1.1.2",
     "commander": "^2.11.0",
     "easy-livereload": "^1.2.0",
+    "eslint": "^4.18.2",
+    "eslint-plugin-react": "^7.7.0",
     "mocha": "^5.0.0",
     "morgan": "^1.8.2",
     "node-dev": "^3.1.3",

+ 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()) },

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

@@ -0,0 +1,45 @@
+import { BasicInterceptor } from 'crowi-pluginkit';
+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 - 21
resource/js/components/PageEditor/MarkdownListHelper.js → resource/js/components/PageEditor/MarkdownListUtil.js

@@ -1,6 +1,9 @@
 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 +11,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 +144,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

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 489 - 19
yarn.lock


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů