Knaito163 8 anni fa
parent
commit
b644504bd1

+ 11 - 1
CHANGES.md

@@ -1,11 +1,21 @@
 CHANGES
 CHANGES
 ========
 ========
 
 
-## 2.4.4-RC
+## 2.4.5-RC
 
 
+* 
+
+## 2.4.4
+
+* Feature: Autoformat Markdown Table
+* Feature: highlight.js Theme Selector
+* Fix: The bug of updating numbering list by codemirror
 * Fix: Template LangProcessor doesn't work
 * Fix: Template LangProcessor doesn't work
     * Introduced by 2.4.0
     * Introduced by 2.4.0
 * Support: Apply ESLint
 * Support: Apply ESLint
+* Support: Upgrade libs
+    * react, react-dom
+    * codemirror, react-codemirror2
 
 
 ## 2.4.3
 ## 2.4.3
 
 

+ 1 - 0
crowi-plus

@@ -0,0 +1 @@
+Subproject commit 3406c4e50a0ccf4e611ae4d7663aae7023a9e249

+ 10 - 0
lib/form/admin/customhighlightJsStyle.js

@@ -0,0 +1,10 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field
+  ;
+
+module.exports = form(
+  field('settingForm[customize:highlightJsStyle]'),
+  field('settingForm[customize:highlightJsStyleBorder]').trim().toBooleanStrict()
+);

+ 1 - 0
lib/form/index.js

@@ -27,6 +27,7 @@ module.exports = {
     custombehavior: require('./admin/custombehavior'),
     custombehavior: require('./admin/custombehavior'),
     customlayout: require('./admin/customlayout'),
     customlayout: require('./admin/customlayout'),
     customfeatures: require('./admin/customfeatures'),
     customfeatures: require('./admin/customfeatures'),
+    customhighlightJsStyle: require('./admin/customhighlightJsStyle'),
     userInvite: require('./admin/userInvite'),
     userInvite: require('./admin/userInvite'),
     slackIwhSetting: require('./admin/slackIwhSetting'),
     slackIwhSetting: require('./admin/slackIwhSetting'),
     slackSetting: require('./admin/slackSetting'),
     slackSetting: require('./admin/slackSetting'),

+ 39 - 42
lib/locales/en-US/translation.json

@@ -254,7 +254,7 @@
     "invalid": "Invalid"
     "invalid": "Invalid"
 
 
   },
   },
- "security_setting": {
+  "security_setting": {
     "Basic authentication": "Basic authentication",
     "Basic authentication": "Basic authentication",
     "Security settings": "Security settings",
     "Security settings": "Security settings",
     "Guest users access": "Guest users access",
     "Guest users access": "Guest users access",
@@ -269,8 +269,7 @@
     "only_those":" Only those whose e-mail address including the company address can register.",
     "only_those":" Only those whose e-mail address including the company address can register.",
     "insert_single":"Please insert single e-mail address per line.",
     "insert_single":"Please insert single e-mail address per line.",
     "Authentication mechanism settings":"Authentication mechanism settings"
     "Authentication mechanism settings":"Authentication mechanism settings"
-
- },
+  },
 
 
   "markdown_setting": {
   "markdown_setting": {
     "markdown_rendering": "You can change Markdown rendering settings.",
     "markdown_rendering": "You can change Markdown rendering settings.",
@@ -279,47 +278,45 @@
     "validate_comment": "Validate Line Break in the comment section",
     "validate_comment": "Validate Line Break in the comment section",
     "treat_comment": "Treat line breaking in the comment section as <code>&lt;br&gt;</code> in HTML",
     "treat_comment": "Treat line breaking in the comment section as <code>&lt;br&gt;</code> in HTML",
     "TBD": "(TBD: Markdown function in the comment section has not been implemented yet)"
     "TBD": "(TBD: Markdown function in the comment section has not been implemented yet)"
+  },
 
 
-     },
-
-     "customize_page": {
-       "Behavior": "Behavior",
-       "Layout": "Layout",
-        "Function": "Function",
-        "function_choose": "You can choose Valid/Invalid of the function",
-        "Timeline function": "Timeline function",
-        "subpage_display": "You can show the timeline of the subpages.",
-        "performance_decrease": "If there are many subpages, performance decreases while page loading.",
-        "list_page_display": "You can speed up list page display by invalidating.",
-        "tab_switch": "Save tab-switching in the browser",
-        "save_edit": "Save edit tab and history tab switching in the browser and make it object for forward/back command of the browser.",
-        "by_invalidating": "By invalidating, you can make page transition as the only object for forward/back command of the browser.",
-        "Custom CSS": "Custom CSS",
-        "write_CSS": "You can write CSS that is applied to whole system.",
-        "reflect_change": "You need to reload the page to reflect the change.",
-        "ctrl_space": "Ctrl+Space to Autocomplete",
-        "Custom script": "Custom script",
-        "write_java": "You can write Javascript that is applied to whole system."
-
-
- },
+  "customize_page": {
+    "Behavior": "Behavior",
+    "Layout": "Layout",
+    "Function": "Function",
+    "function_choose": "You can choose Valid/Invalid of the function",
+    "Timeline function": "Timeline function",
+    "Code Highlight": "Code Highlight",
+    "Theme": "Theme",
+    "subpage_display": "You can show the timeline of the subpages.",
+    "performance_decrease": "If there are many subpages, performance decreases while page loading.",
+    "list_page_display": "You can speed up list page display by invalidating.",
+    "tab_switch": "Save tab-switching in the browser",
+    "save_edit": "Save edit tab and history tab switching in the browser and make it object for forward/back command of the browser.",
+    "by_invalidating": "By invalidating, you can make page transition as the only object for forward/back command of the browser.",
+    "Custom CSS": "Custom CSS",
+    "write_CSS": "You can write CSS that is applied to whole system.",
+    "reflect_change": "You need to reload the page to reflect the change.",
+    "ctrl_space": "Ctrl+Space to Autocomplete",
+    "Custom script": "Custom script",
+    "write_java": "You can write Javascript that is applied to whole system."
+  },
 
 
   "user_management": {
   "user_management": {
-        "User management": "User management",
-        "invite_users": "Invite new users",
-        "external_account": "External account management",
-        "user_list": "List of users",
-        "Date created": "Date created",
-        "Last login": "Last login",
-        "Manage": "Manage",
-        "Edit menu": "Edit menu",
-        "Reissue password": "Reissue password",
-        "Status":"Status",
-        "Deactivate account":"Deactivate account",
-        "your_own":"You cannot deactivate your own account",
-        "Administrator menu":"Administrator menu",
-        "cannot_remove":"You cannot remove yourself from administrator"
-
+    "User management": "User management",
+    "invite_users": "Invite new users",
+    "external_account": "External account management",
+    "user_list": "List of users",
+    "Date created": "Date created",
+    "Last login": "Last login",
+    "Manage": "Manage",
+    "Edit menu": "Edit menu",
+    "Reissue password": "Reissue password",
+    "Status":"Status",
+    "Deactivate account":"Deactivate account",
+    "your_own":"You cannot deactivate your own account",
+    "Administrator menu":"Administrator menu",
+    "cannot_remove":"You cannot remove yourself from administrator"
+  }
 
 
-      }
 }
 }

+ 37 - 41
lib/locales/ja/translation.json

@@ -288,7 +288,6 @@
     "insert_single":"1行に1メールアドレス入力してください。",
     "insert_single":"1行に1メールアドレス入力してください。",
     "Authentication mechanism settings":"認証機構設定"
     "Authentication mechanism settings":"認証機構設定"
   },
   },
-
   "markdown_setting": {
   "markdown_setting": {
     "markdown_rendering": "Markdownレンダリングの設定を変更できます。",
     "markdown_rendering": "Markdownレンダリングの設定を変更できます。",
     "validate Line Break": "Line Break を有効にする",
     "validate Line Break": "Line Break を有効にする",
@@ -299,48 +298,45 @@
 
 
   },
   },
 
 
-     "customize_page": {
-       "Behavior": "挙動",
-       "Layout": "レイアウト",
-       "Function": "機能",
-       "function_choose": "機能の有効/無効を選択できます。",
-       "Timeline function": "タイムライン機能",
-       "subpage_display": "配下ページのタイムラインを表示できます。",
-       "performance_decrease": "配下ページが多い場合はページロード時のパフォーマンスが落ちます。",
-       "list_page_display": "無効化することでリストページの表示を高速化できます。",
-       "tab_switch":"タブ変更をブラウザ履歴に保存",
-       "save_edit": "編集タブやヒストリータブ等の切り替えをブラウザ履歴に保存し、ブラウザの戻る/進む操作の対象にします。",
-       "by_invalidating": "無効化することで、ページ遷移のみを戻る/進む操作の対象にすることができます。",
-       "Custom CSS": "カスタム CSS",
-        "write_CSS": " システム全体に適用されるCSSを記述できます。",
-        "reflect_change": "変更の反映はページの更新が必要です。",
-        "ctrl_space": "Ctrl+Space でコード補完",
-        "Custom script": "カスタムスクリプト",
-        "write_java": "システム全体に適用されるJavaScriptを記述できます。"
-
- },
-
-     "user_management": {
-        "User management": "ユーザー管理",
-        "invite_users": "新規ユーザーの招待",
-        "external_account": "外部アカウントの管理",
-        "user_list": "ユーザー一覧",
-        "Date created": "作成日",
-        "Last login": "最終ログイン",
-        "Manage": "操作",
-        "Edit menu": "編集メニュー",
-        "Reissue password": "パスワードの再発行",
-        "Status":"ステータス",
-        "Deactivate account":"アカウント停止",
-        "your_own":"自分自身のアカウントを停止することはできません",
-        "Administrator menu":"管理者メニュー",
-        "cannot_remove":"自分自身を管理者から外すことはできません"
-
-
-
+  "customize_page": {
+    "Behavior": "挙動",
+    "Layout": "レイアウト",
+    "Function": "機能",
+    "function_choose": "機能の有効/無効を選択できます。",
+    "Timeline function": "タイムライン機能",
+    "Code Highlight": "コードハイライト",
+    "Theme": "テーマ",
+    "subpage_display": "配下ページのタイムラインを表示できます。",
+    "performance_decrease": "配下ページが多い場合はページロード時のパフォーマンスが落ちます。",
+    "list_page_display": "無効化することでリストページの表示を高速化できます。",
+    "tab_switch": "タブ変更をブラウザ履歴に保存",
+    "save_edit": "編集タブやヒストリータブ等の切り替えをブラウザ履歴に保存し、ブラウザの戻る/進む操作の対象にします。",
+    "by_invalidating": "無効化することで、ページ遷移のみを戻る/進む操作の対象にすることができます。",
+    "Custom CSS": "カスタム CSS",
+    "write_CSS": " システム全体に適用されるCSSを記述できます。",
+    "reflect_change": "変更の反映はページの更新が必要です。",
+    "ctrl_space": "Ctrl+Space でコード補完",
+    "Custom script": "カスタムスクリプト",
+    "write_java": "システム全体に適用されるJavaScriptを記述できます。"
 
 
+  },
 
 
+  "user_management": {
+    "User management": "ユーザー管理",
+    "invite_users": "新規ユーザーの招待",
+    "external_account": "外部アカウントの管理",
+    "user_list": "ユーザー一覧",
+    "Date created": "作成日",
+    "Last login": "最終ログイン",
+    "Manage": "操作",
+    "Edit menu": "編集メニュー",
+    "Reissue password": "パスワードの再発行",
+    "Status": "ステータス",
+    "Deactivate account": "アカウント停止",
+    "your_own": "自分自身のアカウントを停止することはできません",
+    "Administrator menu": "管理者メニュー",
+    "cannot_remove": "自分自身を管理者から外すことはできません"
+  }
 
 
-      }
 }
 }
 
 

+ 111 - 133
lib/models/config.js

@@ -40,56 +40,57 @@ module.exports = function(crowi) {
   /**
   /**
    * default values when migrated from Official Crowi
    * default values when migrated from Official Crowi
    */
    */
-  function getDefaultCrowiConfigs()
-  {
+  function getDefaultCrowiConfigs() {
     return {
     return {
       //'app:installed'     : "0.0.0",
       //'app:installed'     : "0.0.0",
-      'app:title'         : 'Crowi',
-      'app:confidential'  : '',
-
-      'app:fileUpload'    : false,
-
-      'security:restrictGuestMode'      : 'Deny',
-
-      'security:registrationMode'      : 'Open',
-      'security:registrationWhiteList' : [],
-
-      'security:isEnabledPassport' : false,
-      'security:passport-ldap:isEnabled' : false,
-      'security:passport-ldap:serverUrl' : undefined,
-      'security:passport-ldap:isUserBind' : undefined,
-      'security:passport-ldap:bindDN' : undefined,
-      'security:passport-ldap:bindDNPassword' : undefined,
-      'security:passport-ldap:searchFilter' : undefined,
-      'security:passport-ldap:attrMapUsername' : undefined,
-      'security:passport-ldap:groupSearchBase' : undefined,
-      'security:passport-ldap:groupSearchFilter' : undefined,
-      'security:passport-ldap:groupDnProperty' : undefined,
-
-      'aws:bucket'          : 'crowi',
-      'aws:region'          : 'ap-northeast-1',
-      'aws:accessKeyId'     : '',
-      'aws:secretAccessKey' : '',
-
-      'mail:from'         : '',
-      'mail:smtpHost'     : '',
-      'mail:smtpPort'     : '',
-      'mail:smtpUser'     : '',
-      'mail:smtpPassword' : '',
-
-      'google:clientId'     : '',
-      'google:clientSecret' : '',
-
-      'plugin:isEnabledPlugins' : true,
-
-      'customize:css' : '',
-      'customize:script' : '',
-      'customize:header' : '',
-      'customize:title' : '',
-      'customize:behavior' : 'crowi',
-      'customize:layout' : 'crowi',
-      'customize:isEnabledTimeline' : true,
-      'customize:isSavedStatesOfTabChanges' : true,
+      'app:title': 'Crowi',
+      'app:confidential': '',
+
+      'app:fileUpload': false,
+
+      'security:restrictGuestMode': 'Deny',
+
+      'security:registrationMode': 'Open',
+      'security:registrationWhiteList': [],
+
+      'security:isEnabledPassport': false,
+      'security:passport-ldap:isEnabled': false,
+      'security:passport-ldap:serverUrl': undefined,
+      'security:passport-ldap:isUserBind': undefined,
+      'security:passport-ldap:bindDN': undefined,
+      'security:passport-ldap:bindDNPassword': undefined,
+      'security:passport-ldap:searchFilter': undefined,
+      'security:passport-ldap:attrMapUsername': undefined,
+      'security:passport-ldap:groupSearchBase': undefined,
+      'security:passport-ldap:groupSearchFilter': undefined,
+      'security:passport-ldap:groupDnProperty': undefined,
+
+      'aws:bucket': 'crowi',
+      'aws:region': 'ap-northeast-1',
+      'aws:accessKeyId': '',
+      'aws:secretAccessKey': '',
+
+      'mail:from': '',
+      'mail:smtpHost': '',
+      'mail:smtpPort': '',
+      'mail:smtpUser': '',
+      'mail:smtpPassword': '',
+
+      'google:clientId': '',
+      'google:clientSecret': '',
+
+      'plugin:isEnabledPlugins': true,
+
+      'customize:css': '',
+      'customize:script': '',
+      'customize:header': '',
+      'customize:title': '',
+      'customize:highlightJsStyle': 'github',
+      'customize:highlightJsStyleBorder': false,
+      'customize:behavior': 'crowi',
+      'customize:layout': 'crowi',
+      'customize:isEnabledTimeline': true,
+      'customize:isSavedStatesOfTabChanges': true,
     };
     };
   }
   }
 
 
@@ -97,7 +98,7 @@ module.exports = function(crowi) {
     return {
     return {
       'markdown:isEnabledLinebreaks': true,
       'markdown:isEnabledLinebreaks': true,
       'markdown:isEnabledLinebreaksInComments': true,
       'markdown:isEnabledLinebreaksInComments': true,
-    }
+    };
   }
   }
 
 
   function getValueForCrowiNS(config, key) {
   function getValueForCrowiNS(config, key) {
@@ -109,8 +110,7 @@ module.exports = function(crowi) {
     return config.crowi[key];
     return config.crowi[key];
   }
   }
 
 
-  configSchema.statics.getRestrictGuestModeLabels = function()
-  {
+  configSchema.statics.getRestrictGuestModeLabels = function() {
     var labels = {};
     var labels = {};
     labels[SECURITY_RESTRICT_GUEST_MODE_DENY]     = 'アカウントを持たないユーザーはアクセス不可';
     labels[SECURITY_RESTRICT_GUEST_MODE_DENY]     = 'アカウントを持たないユーザーはアクセス不可';
     labels[SECURITY_RESTRICT_GUEST_MODE_READONLY] = '閲覧のみ許可';
     labels[SECURITY_RESTRICT_GUEST_MODE_READONLY] = '閲覧のみ許可';
@@ -118,8 +118,7 @@ module.exports = function(crowi) {
     return labels;
     return labels;
   };
   };
 
 
-  configSchema.statics.getRegistrationModeLabels = function()
-  {
+  configSchema.statics.getRegistrationModeLabels = function() {
     var labels = {};
     var labels = {};
     labels[SECURITY_REGISTRATION_MODE_OPEN]       = '公開 (だれでも登録可能)';
     labels[SECURITY_REGISTRATION_MODE_OPEN]       = '公開 (だれでも登録可能)';
     labels[SECURITY_REGISTRATION_MODE_RESTRICTED] = '制限 (登録完了には管理者の承認が必要)';
     labels[SECURITY_REGISTRATION_MODE_RESTRICTED] = '制限 (登録完了には管理者の承認が必要)';
@@ -128,11 +127,10 @@ module.exports = function(crowi) {
     return labels;
     return labels;
   };
   };
 
 
-  configSchema.statics.updateConfigCache = function(ns, config)
-  {
+  configSchema.statics.updateConfigCache = function(ns, config) {
     var originalConfig = crowi.getConfig();
     var originalConfig = crowi.getConfig();
     var newNSConfig = originalConfig[ns] || {};
     var newNSConfig = originalConfig[ns] || {};
-    Object.keys(config).forEach(function (key) {
+    Object.keys(config).forEach(function(key) {
       if (config[key] || config[key] === '' || config[key] === false) {
       if (config[key] || config[key] === '' || config[key] === false) {
         newNSConfig[key] = config[key];
         newNSConfig[key] = config[key];
       }
       }
@@ -147,10 +145,9 @@ module.exports = function(crowi) {
   };
   };
 
 
   // Execute only once for installing application
   // Execute only once for installing application
-  configSchema.statics.applicationInstall = function(callback)
-  {
+  configSchema.statics.applicationInstall = function(callback) {
     var Config = this;
     var Config = this;
-    Config.count({ ns: 'crowi' }, function (err, count) {
+    Config.count({ ns: 'crowi' }, function(err, count) {
       if (count > 0) {
       if (count > 0) {
         return callback(new Error('Application already installed'), null);
         return callback(new Error('Application already installed'), null);
       }
       }
@@ -162,8 +159,7 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  configSchema.statics.setupCofigFormData = function(ns, config)
-  {
+  configSchema.statics.setupCofigFormData = function(ns, config) {
     var defaultConfig = {};
     var defaultConfig = {};
 
 
     // set Default Settings
     // set Default Settings
@@ -177,7 +173,7 @@ module.exports = function(crowi) {
     if (!defaultConfig[ns]) {
     if (!defaultConfig[ns]) {
       defaultConfig[ns] = {};
       defaultConfig[ns] = {};
     }
     }
-    Object.keys(config[ns] || {}).forEach(function (key) {
+    Object.keys(config[ns] || {}).forEach(function(key) {
       if (config[ns][key] !== undefined) {
       if (config[ns][key] !== undefined) {
         defaultConfig[key] = config[ns][key];
         defaultConfig[key] = config[ns][key];
       }
       }
@@ -186,47 +182,43 @@ module.exports = function(crowi) {
   };
   };
 
 
 
 
-  configSchema.statics.updateNamespaceByArray = function(ns, configs, callback)
-  {
+  configSchema.statics.updateNamespaceByArray = function(ns, configs, callback) {
     var Config = this;
     var Config = this;
     if (configs.length < 0) {
     if (configs.length < 0) {
       return callback(new Error('Argument #1 is not array.'), null);
       return callback(new Error('Argument #1 is not array.'), null);
     }
     }
 
 
-    Object.keys(configs).forEach(function (key) {
+    Object.keys(configs).forEach(function(key) {
       var value = configs[key];
       var value = configs[key];
 
 
       Config.findOneAndUpdate(
       Config.findOneAndUpdate(
         { ns: ns, key: key },
         { ns: ns, key: key },
         { ns: ns, key: key, value: JSON.stringify(value) },
         { ns: ns, key: key, value: JSON.stringify(value) },
         { upsert: true, },
         { upsert: true, },
-        function (err, config) {
+        function(err, config) {
           debug('Config.findAndUpdate', err, config);
           debug('Config.findAndUpdate', err, config);
-      });
+        });
     });
     });
 
 
     return callback(null, configs);
     return callback(null, configs);
   };
   };
 
 
-  configSchema.statics.findAndUpdate = function(ns, key, value, callback)
-  {
+  configSchema.statics.findAndUpdate = function(ns, key, value, callback) {
     var Config = this;
     var Config = this;
     Config.findOneAndUpdate(
     Config.findOneAndUpdate(
       { ns: ns, key: key },
       { ns: ns, key: key },
       { ns: ns, key: key, value: JSON.stringify(value) },
       { ns: ns, key: key, value: JSON.stringify(value) },
       { upsert: true, },
       { upsert: true, },
-      function (err, config) {
+      function(err, config) {
         debug('Config.findAndUpdate', err, config);
         debug('Config.findAndUpdate', err, config);
         callback(err, config);
         callback(err, config);
-    });
+      });
   };
   };
 
 
-  configSchema.statics.getConfig = function(callback)
-  {
+  configSchema.statics.getConfig = function(callback) {
   };
   };
 
 
-  configSchema.statics.loadAllConfig = function(callback)
-  {
+  configSchema.statics.loadAllConfig = function(callback) {
     var Config = this
     var Config = this
       , config = {};
       , config = {};
     config.crowi = {}; // crowi namespace
     config.crowi = {}; // crowi namespace
@@ -252,8 +244,7 @@ module.exports = function(crowi) {
       });
       });
   };
   };
 
 
-  configSchema.statics.isEnabledPassport = function(config)
-  {
+  configSchema.statics.isEnabledPassport = function(config) {
     // always true if crowi-plus installed cleanly
     // always true if crowi-plus installed cleanly
     if (Object.keys(config.crowi).length == 0) {
     if (Object.keys(config.crowi).length == 0) {
       return true;
       return true;
@@ -263,18 +254,16 @@ module.exports = function(crowi) {
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
   };
   };
 
 
-  configSchema.statics.isEnabledPassportLdap = function(config)
-  {
+  configSchema.statics.isEnabledPassportLdap = function(config) {
     const key = 'security:passport-ldap:isEnabled';
     const key = 'security:passport-ldap:isEnabled';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
   };
   };
 
 
-  configSchema.statics.isUploadable = function(config)
-  {
+  configSchema.statics.isUploadable = function(config) {
     var method = crowi.env.FILE_UPLOAD || 'aws';
     var method = crowi.env.FILE_UPLOAD || 'aws';
 
 
     if (method == 'aws' && (
     if (method == 'aws' && (
-        !config.crowi['aws:accessKeyId'] ||
+      !config.crowi['aws:accessKeyId'] ||
         !config.crowi['aws:secretAccessKey'] ||
         !config.crowi['aws:secretAccessKey'] ||
         !config.crowi['aws:region'] ||
         !config.crowi['aws:region'] ||
         !config.crowi['aws:bucket'])) {
         !config.crowi['aws:bucket'])) {
@@ -284,8 +273,7 @@ module.exports = function(crowi) {
     return method != 'none';
     return method != 'none';
   };
   };
 
 
-  configSchema.statics.isGuesstAllowedToRead = function(config)
-  {
+  configSchema.statics.isGuesstAllowedToRead = function(config) {
     // return false if undefined
     // return false if undefined
     if (undefined === config.crowi || undefined === config.crowi['security:restrictGuestMode']) {
     if (undefined === config.crowi || undefined === config.crowi['security:restrictGuestMode']) {
       return false;
       return false;
@@ -294,14 +282,12 @@ module.exports = function(crowi) {
     return SECURITY_RESTRICT_GUEST_MODE_READONLY === config.crowi['security:restrictGuestMode'];
     return SECURITY_RESTRICT_GUEST_MODE_READONLY === config.crowi['security:restrictGuestMode'];
   };
   };
 
 
-  configSchema.statics.isEnabledPlugins = function(config)
-  {
+  configSchema.statics.isEnabledPlugins = function(config) {
     const key = 'plugin:isEnabledPlugins';
     const key = 'plugin:isEnabledPlugins';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
   };
   };
 
 
-  configSchema.statics.isEnabledLinebreaks = function(config)
-  {
+  configSchema.statics.isEnabledLinebreaks = function(config) {
     const key = 'markdown:isEnabledLinebreaks';
     const key = 'markdown:isEnabledLinebreaks';
 
 
     // return default value if undefined
     // return default value if undefined
@@ -312,8 +298,7 @@ module.exports = function(crowi) {
     return config.markdown[key];
     return config.markdown[key];
   };
   };
 
 
-  configSchema.statics.isEnabledLinebreaksInComments = function(config)
-  {
+  configSchema.statics.isEnabledLinebreaksInComments = function(config) {
     const key = 'markdown:isEnabledLinebreaksInComments';
     const key = 'markdown:isEnabledLinebreaksInComments';
 
 
     // return default value if undefined
     // return default value if undefined
@@ -327,70 +312,69 @@ module.exports = function(crowi) {
   /**
   /**
    * initialize custom css strings
    * initialize custom css strings
    */
    */
-  configSchema.statics.initCustomCss = function(config)
-  {
+  configSchema.statics.initCustomCss = function(config) {
     const key = 'customize:css';
     const key = 'customize:css';
     const rawCss = getValueForCrowiNS(config, key);
     const rawCss = getValueForCrowiNS(config, key);
     // uglify and store
     // uglify and store
     this._customCss = uglifycss.processString(rawCss);
     this._customCss = uglifycss.processString(rawCss);
-  }
+  };
 
 
-  configSchema.statics.customCss = function(config)
-  {
+  configSchema.statics.customCss = function(config) {
     return this._customCss;
     return this._customCss;
-  }
+  };
 
 
-  configSchema.statics.initCustomScript = function(config)
-  {
+  configSchema.statics.initCustomScript = function(config) {
     const key = 'customize:script';
     const key = 'customize:script';
     const rawScript = getValueForCrowiNS(config, key);
     const rawScript = getValueForCrowiNS(config, key);
     // store as is
     // store as is
     this._customScript = rawScript;
     this._customScript = rawScript;
-  }
+  };
 
 
-  configSchema.statics.customScript = function(config)
-  {
+  configSchema.statics.customScript = function(config) {
     return this._customScript;
     return this._customScript;
-  }
+  };
 
 
-  configSchema.statics.customHeader = function(config)
-  {
+  configSchema.statics.customHeader = function(config) {
     const key = 'customize:header';
     const key = 'customize:header';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
-  }
+  };
 
 
-  configSchema.statics.customTitle = function(config)
-  {
+  configSchema.statics.customTitle = function(config) {
     const key = 'customize:title';
     const key = 'customize:title';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
-  }
+  };
 
 
-  configSchema.statics.behaviorType = function(config)
-  {
+  configSchema.statics.behaviorType = function(config) {
     const key = 'customize:behavior';
     const key = 'customize:behavior';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
-  }
+  };
 
 
-  configSchema.statics.layoutType = function(config)
-  {
+  configSchema.statics.layoutType = function(config) {
     const key = 'customize:layout';
     const key = 'customize:layout';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
-  }
+  };
 
 
-  configSchema.statics.isEnabledTimeline = function(config)
-  {
+  configSchema.statics.highlightJsStyle = function(config) {
+    const key = 'customize:highlightJsStyle';
+    return getValueForCrowiNS(config, key);
+  };
+
+  configSchema.statics.highlightJsStyleBorder = function(config) {
+    const key = 'customize:highlightJsStyleBorder';
+    return getValueForCrowiNS(config, key);
+  };
+
+  configSchema.statics.isEnabledTimeline = function(config) {
     const key = 'customize:isEnabledTimeline';
     const key = 'customize:isEnabledTimeline';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
   };
   };
 
 
-  configSchema.statics.isSavedStatesOfTabChanges = function(config)
-  {
+  configSchema.statics.isSavedStatesOfTabChanges = function(config) {
     const key = 'customize:isSavedStatesOfTabChanges';
     const key = 'customize:isSavedStatesOfTabChanges';
     return getValueForCrowiNS(config, key);
     return getValueForCrowiNS(config, key);
   };
   };
 
 
-  configSchema.statics.fileUploadEnabled = function(config)
-  {
+  configSchema.statics.fileUploadEnabled = function(config) {
     const Config = this;
     const Config = this;
 
 
     if (!Config.isUploadable(config)) {
     if (!Config.isUploadable(config)) {
@@ -401,13 +385,11 @@ module.exports = function(crowi) {
     return !!config.crowi['app:fileUpload'];
     return !!config.crowi['app:fileUpload'];
   };
   };
 
 
-  configSchema.statics.hasSlackConfig = function(config)
-  {
+  configSchema.statics.hasSlackConfig = function(config) {
     return Config.hasSlackWebClientConfig(config) || Config.hasSlackIwhUrl(config);
     return Config.hasSlackWebClientConfig(config) || Config.hasSlackIwhUrl(config);
   };
   };
 
 
-  configSchema.statics.hasSlackWebClientConfig = function(config)
-  {
+  configSchema.statics.hasSlackWebClientConfig = function(config) {
     if (!config.notification) {
     if (!config.notification) {
       return false;
       return false;
     }
     }
@@ -422,24 +404,21 @@ module.exports = function(crowi) {
   /**
   /**
    * for Slack Incoming Webhooks
    * for Slack Incoming Webhooks
    */
    */
-  configSchema.statics.hasSlackIwhUrl = function(config)
-  {
+  configSchema.statics.hasSlackIwhUrl = function(config) {
     if (!config.notification) {
     if (!config.notification) {
       return false;
       return false;
     }
     }
     return config.notification['slack:incomingWebhookUrl'];
     return config.notification['slack:incomingWebhookUrl'];
   };
   };
 
 
-  configSchema.statics.isIncomingWebhookPrioritized = function(config)
-  {
+  configSchema.statics.isIncomingWebhookPrioritized = function(config) {
     if (!config.notification) {
     if (!config.notification) {
       return false;
       return false;
     }
     }
     return config.notification['slack:isIncomingWebhookPrioritized'];
     return config.notification['slack:isIncomingWebhookPrioritized'];
   };
   };
 
 
-  configSchema.statics.hasSlackToken = function(config)
-  {
+  configSchema.statics.hasSlackToken = function(config) {
     if (!this.hasSlackWebClientConfig(config)) {
     if (!this.hasSlackWebClientConfig(config)) {
       return false;
       return false;
     }
     }
@@ -451,8 +430,7 @@ module.exports = function(crowi) {
     return true;
     return true;
   };
   };
 
 
-  configSchema.statics.getLocalconfig = function(config)
-  {
+  configSchema.statics.getLocalconfig = function(config) {
     const Config = this;
     const Config = this;
     const env = crowi.getEnv();
     const env = crowi.getEnv();
 
 
@@ -476,7 +454,7 @@ module.exports = function(crowi) {
     };
     };
 
 
     return local_config;
     return local_config;
-  }
+  };
 
 
   /*
   /*
   configSchema.statics.isInstalled = function(config)
   configSchema.statics.isInstalled = function(config)

+ 14 - 0
lib/routes/admin.js

@@ -130,8 +130,22 @@ module.exports = function(crowi, app) {
     var settingForm;
     var settingForm;
     settingForm = Config.setupCofigFormData('crowi', req.config);
     settingForm = Config.setupCofigFormData('crowi', req.config);
 
 
+    const highlightJsCssSelectorOptions = {
+      "github":           { name: '[Light] Github',         border: false },
+      "github-gist":      { name: '[Light] Github Gist',    border: true },
+      "atom-one-light":   { name: '[Light] Atom One Light', border: true },
+      "xcode":            { name: '[Light] Xcode',          border: true },
+      "vs":               { name: '[Light] Vs',             border: true },
+      "atom-one-dark":    { name: '[Dark] Atom One Dark',   border: false },
+      "hybrid":           { name: '[Dark] Hybrid',          border: false },
+      "monokai":          { name: '[Dark] Monokai',         border: false },
+      "tomorrow-night":   { name: '[Dark] Tomorrow Night',  border: false },
+      "vs2015":           { name: '[Dark] Vs 2015',         border: false },
+    }
+
     return res.render('admin/customize', {
     return res.render('admin/customize', {
       settingForm: settingForm,
       settingForm: settingForm,
+      highlightJsCssSelectorOptions: highlightJsCssSelectorOptions
     });
     });
   };
   };
 
 

+ 1 - 0
lib/routes/index.js

@@ -78,6 +78,7 @@ module.exports = function(crowi, app) {
   app.post('/_api/admin/customize/behavior' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.custombehavior, admin.api.customizeSetting);
   app.post('/_api/admin/customize/behavior' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.custombehavior, admin.api.customizeSetting);
   app.post('/_api/admin/customize/layout'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customlayout, admin.api.customizeSetting);
   app.post('/_api/admin/customize/layout'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customlayout, admin.api.customizeSetting);
   app.post('/_api/admin/customize/features' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customfeatures, admin.api.customizeSetting);
   app.post('/_api/admin/customize/features' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customfeatures, admin.api.customizeSetting);
+  app.post('/_api/admin/customize/highlightJsStyle' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customhighlightJsStyle, admin.api.customizeSetting);
 
 
   // search admin
   // search admin
   app.get('/admin/search'              , loginRequired(crowi, app) , middleware.adminRequired() , admin.search.index);
   app.get('/admin/search'              , loginRequired(crowi, app) , middleware.adminRequired() , admin.search.index);

+ 10 - 0
lib/util/swigFunctions.js

@@ -107,6 +107,16 @@ module.exports = function(crowi, app, req, locals) {
     return Config.layoutType(config);
     return Config.layoutType(config);
   }
   }
 
 
+  locals.highlightJsStyle = function() {
+    var config = crowi.getConfig()
+    return Config.highlightJsStyle(config);
+  }
+
+  locals.highlightJsStyleBorder = function() {
+    var config = crowi.getConfig()
+    return Config.highlightJsStyleBorder(config);
+  }
+
   locals.isEnabledTimeline = function() {
   locals.isEnabledTimeline = function() {
     var config = crowi.getConfig()
     var config = crowi.getConfig()
     return Config.isEnabledTimeline(config);
     return Config.isEnabledTimeline(config);

+ 77 - 7
lib/views/admin/customize.html

@@ -3,10 +3,9 @@
 {% block html_title %}{{ t('Customize') }} {% endblock %}
 {% block html_title %}{{ t('Customize') }} {% endblock %}
 
 
 {% block html_additional_headers %}
 {% block html_additional_headers %}
+  {% parent %}
   <!-- CodeMirror -->
   <!-- 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/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>
   <style>
     .CodeMirror {
     .CodeMirror {
       border: 1px solid #eee;
       border: 1px solid #eee;
@@ -14,7 +13,6 @@
   </style>
   </style>
 {% endblock %}
 {% endblock %}
 
 
-
 {% block content_head %}
 {% block content_head %}
 <div class="header-wrap">
 <div class="header-wrap">
   <header id="page-header">
   <header id="page-header">
@@ -143,7 +141,6 @@
       </fieldset>
       </fieldset>
       </form>
       </form>
 
 
-
       <form action="/_api/admin/customize/features" method="post" class="form-horizontal" id="customfeaturesSettingForm" role="form">
       <form action="/_api/admin/customize/features" method="post" class="form-horizontal" id="customfeaturesSettingForm" role="form">
       <fieldset>
       <fieldset>
       <legend>{{ t('customize_page.Function') }}</legend>
       <legend>{{ t('customize_page.Function') }}</legend>
@@ -204,6 +201,67 @@
       </fieldset>
       </fieldset>
       </form>
       </form>
 
 
+      <form action="/_api/admin/customize/highlightJsStyle" method="post" class="form-horizontal" id="cutomhighlightJsStyleSettingForm" role="form">
+        <fieldset>
+          <legend>{{ t('customize_page.Code Highlight') }}</legend>
+          <div class="form-group">
+            <label for="settingForm[customize:highlightJsStyle]" class="col-xs-3 control-label">{{ t('customize_page.Theme') }}</label>
+            <div class="col-xs-9">
+              <select class="form-control" name="settingForm[customize:highlightJsStyle]" onChange="selectHighlightJsStyle(event)">
+                {% for key in Object.keys(highlightJsCssSelectorOptions) %}
+                  <option value={{key}} {% if key == highlightJsStyle() %} selected {% endif %}>{{highlightJsCssSelectorOptions[key].name}}</option>
+                {% endfor %}
+              </select>
+            </div>
+          </div>
+
+          <div class="form-group">
+            <label for="settingForm[customize:highlightJsStyleBorder]" class="col-xs-3 control-label">(TBD) Border</label>
+            <div class="col-xs-9">
+              <div class="btn-group btn-toggle" data-toggle="buttons">
+                <label class="btn btn-default {% if settingForm['customize:highlightJsStyleBorder'] %}active{% endif %}" data-active-class="primary">
+                  <input name="settingForm[customize:highlightJsStyleBorder]" value="true" type="radio"
+                      {% if true === settingForm['customize:highlightJsStyleBorder'] %}checked{% endif %}> {{ t('Valid') }}
+                </label>
+                <label class="btn btn-default {% if !settingForm['customize:highlightJsStyleBorder'] %}active{% endif %}" data-active-class="primary">
+                  <input name="settingForm[customize:highlightJsStyleBorder]" value="false" type="radio"
+                      {% if !settingForm['customize:highlightJsStyleBorder'] %}checked{% endif %}> {{ t('Invalid') }}
+                </label>
+              </div>
+            </div>
+          </div>
+
+          <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@9.12.0/styles/{{ highlightJsStyle() }}.css" class="highlightJsCss">
+
+          <p class="help-block">
+            Examples:
+            <pre class="hljs"><code class="highlightjs-demo">function $initHighlight(block, cls) {
+  try {
+    if (cls.search(/\bno\-highlight\b/) != -1)
+      return process(block, true, 0x0F) +
+              ` class="${cls}"`;
+  } catch (e) {
+    /* handle exception */
+  }
+  for (var i = 0 / 2; i < classes.length; i++) {
+    if (checkCondition(classes[i]) === undefined)
+      console.log('undefined');
+  }
+}
+
+export  $initHighlight;</code></pre>
+          </p>
+
+          <div class="form-group">
+            <div class="col-xs-offset-5 col-xs-6">
+              <input type="hidden" name="_csrf" value="{{ csrf() }}">
+              <button type="submit" class="btn btn-primary">{{ t('Update') }}</button>
+            </div>
+          </div>
+
+        </fieldset>
+      </form>
+
       <form action="/_api/admin/customize/header" method="post" class="form-horizontal" id="cutomheaderSettingForm" role="form">
       <form action="/_api/admin/customize/header" method="post" class="form-horizontal" id="cutomheaderSettingForm" role="form">
       <fieldset>
       <fieldset>
         <legend>カスタムヘッダーHTML</legend>
         <legend>カスタムヘッダーHTML</legend>
@@ -215,7 +273,7 @@
 
 
         <p class="help-block">
         <p class="help-block">
           Examples:
           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 class="hljs"><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>
         </p>
 
 
         <div class="form-group">
         <div class="form-group">
@@ -337,7 +395,7 @@
         </p>
         </p>
         <p class="help-block">
         <p class="help-block">
           Examples:
           Examples:
-<pre><code>console.log($('.main-container'));
+<pre class="hljs"><code>console.log($('.main-container'));
 
 
 window.addEventListener('load', (event) => {
 window.addEventListener('load', (event) => {
   console.log('config: ', crowi.config);
   console.log('config: ', crowi.config);
@@ -371,7 +429,7 @@ window.addEventListener('load', (event) => {
   </div>
   </div>
 
 
   <script>
   <script>
-    $('#cutomcssSettingForm, #cutomscriptSettingForm, #cutomlayoutSettingForm, #cutombehaviorSettingForm, #customfeaturesSettingForm, #cutomheaderSettingForm, #customtitleSettingForm').each(function() {
+    $('#cutomcssSettingForm, #cutomscriptSettingForm, #cutomlayoutSettingForm, #cutombehaviorSettingForm, #customfeaturesSettingForm, #cutomheaderSettingForm, #cutomhighlightJsStyleSettingForm, #customtitleSettingForm').each(function() {
       $(this).submit(function()
       $(this).submit(function()
       {
       {
         function showMessage(formId, msg, status) {
         function showMessage(formId, msg, status) {
@@ -419,6 +477,18 @@ window.addEventListener('load', (event) => {
       });
       });
     });
     });
 
 
+    // init highlight.js
+    hljs.initHighlightingOnLoad()
+
+    function selectHighlightJsStyle(event) {
+      var highlightJsCssDOM = $(".highlightJsCss")[0]
+      // selected value
+      var val = event.target.value
+      // replace css url
+      // see https://regex101.com/r/gBNZYu/4
+      highlightJsCssDOM.href = highlightJsCssDOM.href.replace(/[^/]+\.css$/, `${val}.css`);
+    }
+
   </script>
   </script>
 
 
 </div>
 </div>

+ 1 - 1
lib/views/layout/layout.html

@@ -91,7 +91,7 @@ gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
   <!-- emojione -->
   <!-- emojione -->
   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css">
   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css">
   <!-- highlight.js -->
   <!-- highlight.js -->
-  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@9.12.0/styles/github.css">
+  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@9.12.0/styles/{{ highlightJsStyle() }}.css">
 
 
   {% block html_additional_headers %}{% endblock %}
   {% block html_additional_headers %}{% endblock %}
 
 

+ 0 - 2
lib/views/page_presentation.html

@@ -48,8 +48,6 @@ gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
 
 
     <!-- Google Fonts -->
     <!-- Google Fonts -->
     <link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
     <link href='https://fonts.googleapis.com/css?family=Lato:400,700' rel='stylesheet' type='text/css'>
-    <!-- highlight.js -->
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@9.12.0/styles/github.css">
 
 
     <style>
     <style>
       {{ customCss() }}
       {{ customCss() }}

+ 6 - 5
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "crowi-plus",
   "name": "crowi-plus",
-  "version": "2.4.4-RC",
+  "version": "2.4.5-RC",
   "description": "Enhanced Crowi",
   "description": "Enhanced Crowi",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -61,7 +61,7 @@
     "body-parser": "^1.18.2",
     "body-parser": "^1.18.2",
     "bootstrap-sass": "~3.3.6",
     "bootstrap-sass": "~3.3.6",
     "check-node-version": "^3.1.1",
     "check-node-version": "^3.1.1",
-    "codemirror": "^5.33.0",
+    "codemirror": "^5.36.0",
     "connect-flash": "~0.1.1",
     "connect-flash": "~0.1.1",
     "connect-redis": "^3.3.0",
     "connect-redis": "^3.3.0",
     "cookie-parser": "^1.4.3",
     "cookie-parser": "^1.4.3",
@@ -101,6 +101,7 @@
     "markdown-it-plantuml": "^1.0.0",
     "markdown-it-plantuml": "^1.0.0",
     "markdown-it-task-lists": "^2.1.0",
     "markdown-it-task-lists": "^2.1.0",
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.2",
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.2",
+    "markdown-table": "^1.1.1",
     "md5": "^2.2.1",
     "md5": "^2.2.1",
     "method-override": "^2.3.10",
     "method-override": "^2.3.10",
     "mkdirp": "~0.5.1",
     "mkdirp": "~0.5.1",
@@ -118,12 +119,12 @@
     "passport-local": "^1.0.0",
     "passport-local": "^1.0.0",
     "pino-clf": "^1.0.2",
     "pino-clf": "^1.0.2",
     "plantuml-encoder": "^1.2.5",
     "plantuml-encoder": "^1.2.5",
-    "react": "^16.0.0",
+    "react": "^16.2.0",
     "react-bootstrap": "^0.32.0",
     "react-bootstrap": "^0.32.0",
     "react-bootstrap-typeahead": "^2.0.2",
     "react-bootstrap-typeahead": "^2.0.2",
     "react-clipboard.js": "^1.1.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",
     "react-dropzone": "^4.2.7",
     "redis": "^2.7.1",
     "redis": "^2.7.1",
     "reveal.js": "^3.5.0",
     "reveal.js": "^3.5.0",

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

@@ -34,9 +34,12 @@ require('codemirror/theme/twilight.css');
 import Dropzone from 'react-dropzone';
 import Dropzone from 'react-dropzone';
 
 
 import pasteHelper from './PasteHelper';
 import pasteHelper from './PasteHelper';
-import markdownListHelper from './MarkdownListHelper';
 import emojiAutoCompleteHelper from './EmojiAutoCompleteHelper';
 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 {
 export default class Editor extends React.Component {
 
 
@@ -46,6 +49,12 @@ export default class Editor extends React.Component {
     // https://regex101.com/r/7BN2fR/2
     // https://regex101.com/r/7BN2fR/2
     this.indentAndMarkPattern = /^([ \t]*)(?:>|\-|\+|\*|\d+\.) /;
     this.indentAndMarkPattern = /^([ \t]*)(?:>|\-|\+|\*|\d+\.) /;
 
 
+    this.interceptorManager = new InterceptorManager();
+    this.interceptorManager.addInterceptors([
+      new MarkdownListInterceptor(),
+      new MarkdownTableInterceptor(),
+    ]);
+
     this.state = {
     this.state = {
       value: this.props.value,
       value: this.props.value,
       dropzoneActive: false,
       dropzoneActive: false,
@@ -57,6 +66,7 @@ export default class Editor extends React.Component {
     this.setScrollTopByLine = this.setScrollTopByLine.bind(this);
     this.setScrollTopByLine = this.setScrollTopByLine.bind(this);
     this.forceToFocus = this.forceToFocus.bind(this);
     this.forceToFocus = this.forceToFocus.bind(this);
     this.dispatchSave = this.dispatchSave.bind(this);
     this.dispatchSave = this.dispatchSave.bind(this);
+    this.handleEnterKey = this.handleEnterKey.bind(this);
 
 
     this.onScrollCursorIntoView = this.onScrollCursorIntoView.bind(this);
     this.onScrollCursorIntoView = this.onScrollCursorIntoView.bind(this);
     this.onPaste = this.onPaste.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) {
   onScrollCursorIntoView(editor, event) {
     if (this.props.onScrollCursorIntoView != null) {
     if (this.props.onScrollCursorIntoView != null) {
       const line = editor.getCursor().line;
       const line = editor.getCursor().line;
@@ -344,7 +374,7 @@ export default class Editor extends React.Component {
               highlightFormatting: true,
               highlightFormatting: true,
               // continuelist, indentlist
               // continuelist, indentlist
               extraKeys: {
               extraKeys: {
-                "Enter": markdownListHelper.newlineAndIndentContinueMarkdownList,
+                "Enter": this.handleEnterKey,
                 "Tab": "indentMore",
                 "Tab": "indentMore",
                 "Shift-Tab": "indentLess",
                 "Shift-Tab": "indentLess",
                 "Ctrl-Q": (cm) => { cm.foldCode(cm.getCursor()) },
                 "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';
 import * as codemirror from 'codemirror';
 
 
-class MarkdownListHelper {
+/**
+ * Utility for markdown list
+ */
+class MarkdownListUtil {
 
 
   constructor() {
   constructor() {
     // https://github.com/codemirror/CodeMirror/blob/c7853a989c77bb9f520c9c530cbe1497856e96fc/addon/edit/continuelist.js#L14
     // 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.indentAndMarkRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/;
     this.indentAndMarkOnlyRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
     this.indentAndMarkOnlyRE = /^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/;
 
 
-    this.newlineAndIndentContinueMarkdownList = this.newlineAndIndentContinueMarkdownList.bind(this);
     this.pasteText = this.pasteText.bind(this);
     this.pasteText = this.pasteText.bind(this);
 
 
     this.getBol = this.getBol.bind(this);
     this.getBol = this.getBol.bind(this);
     this.getEol = this.getEol.bind(this);
     this.getEol = this.getEol.bind(this);
     this.getStrFromBol = this.getStrFromBol.bind(this);
     this.getStrFromBol = this.getStrFromBol.bind(this);
     this.getStrToEol = this.getStrToEol.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();
     const curPos = editor.getCursor();
     return editor.getDoc().getRange(curPos, this.getEol(editor));
     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
 // singleton pattern
-const instance = new MarkdownListHelper();
+const instance = new MarkdownListUtil();
 Object.freeze(instance);
 Object.freeze(instance);
 export default 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 accepts from 'attr-accept'
 
 
-import markdownListHelper from './MarkdownListHelper';
+import markdownListUtil from './MarkdownListUtil';
 
 
 class PasteHelper {
 class PasteHelper {
 
 
@@ -21,7 +21,7 @@ class PasteHelper {
       return;
       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
   // Firefox versions prior to 53 return a bogus MIME type for every file drag, so dragovers with

+ 12 - 8
yarn.lock

@@ -1533,9 +1533,9 @@ code-point-at@^1.0.0:
   version "1.1.0"
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
   resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
 
 
-codemirror@^5.33.0:
-  version "5.33.0"
-  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.33.0.tgz#462ad9a6fe8d38b541a9536a3997e1ef93b40c6a"
+codemirror@^5.36.0:
+  version "5.36.0"
+  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.36.0.tgz#1172ad9dc298056c06e0b34e5ccd23825ca15b40"
 
 
 color-convert@^1.3.0, color-convert@^1.9.0:
 color-convert@^1.3.0, color-convert@^1.9.0:
   version "1.9.1"
   version "1.9.1"
@@ -4134,6 +4134,10 @@ markdown-it@^8.4.0:
     mdurl "^1.0.1"
     mdurl "^1.0.1"
     uc.micro "^1.0.3"
     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:
 marked-terminal@^1.6.2:
   version "1.7.0"
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.7.0.tgz#c8c460881c772c7604b64367007ee5f77f125904"
   resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.7.0.tgz#c8c460881c772c7604b64367007ee5f77f125904"
@@ -5546,11 +5550,11 @@ react-clipboard.js@^1.1.2:
     clipboard "^1.6.1"
     clipboard "^1.6.1"
     prop-types "^15.5.0"
     prop-types "^15.5.0"
 
 
-react-codemirror2@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-4.0.0.tgz#88a30bb082cb87755a80e057d4c7b577456c38f0"
+react-codemirror2@^4.2.1:
+  version "4.2.1"
+  resolved "https://registry.yarnpkg.com/react-codemirror2/-/react-codemirror2-4.2.1.tgz#4ad3c5c60ebbcb34880f961721b51527324ec021"
 
 
-react-dom@^16.0.0:
+react-dom@^16.2.0:
   version "16.2.0"
   version "16.2.0"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.2.0.tgz#69003178601c0ca19b709b33a83369fe6124c044"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.2.0.tgz#69003178601c0ca19b709b33a83369fe6124c044"
   dependencies:
   dependencies:
@@ -5604,7 +5608,7 @@ react-transition-group@^2.0.0, react-transition-group@^2.2.0:
     prop-types "^15.5.8"
     prop-types "^15.5.8"
     warning "^3.0.0"
     warning "^3.0.0"
 
 
-react@^16.0.0:
+react@^16.2.0:
   version "16.2.0"
   version "16.2.0"
   resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
   resolved "https://registry.yarnpkg.com/react/-/react-16.2.0.tgz#a31bd2dab89bff65d42134fa187f24d054c273ba"
   dependencies:
   dependencies: