Parcourir la source

Merge pull request #312 from weseek/master

release v2.4.4
Yuki Takei il y a 8 ans
Parent
commit
7f5e90aa58

+ 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
-}

+ 13 - 1
CHANGES.md

@@ -1,7 +1,19 @@
 CHANGES
 ========
 
-## 2.4.3-RC
+## 2.4.4-RC
+
+* Feature: Autoformat Markdown Table
+* Feature: highlight.js Theme Selector
+* 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
 
 * Improvement: i18n in `/admin`
 * Improvement: Add `SESSION_NAME` environment variable

+ 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

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

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

@@ -254,7 +254,7 @@
     "invalid": "Invalid"
 
   },
- "security_setting": {
+  "security_setting": {
     "Basic authentication": "Basic authentication",
     "Security settings": "Security settings",
     "Guest users access": "Guest users access",
@@ -269,8 +269,7 @@
     "only_those":" Only those whose e-mail address including the company address can register.",
     "insert_single":"Please insert single e-mail address per line.",
     "Authentication mechanism settings":"Authentication mechanism settings"
-
- },
+  },
 
   "markdown_setting": {
     "markdown_rendering": "You can change Markdown rendering settings.",
@@ -279,47 +278,45 @@
     "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",
     "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",
-        "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メールアドレス入力してください。",
     "Authentication mechanism settings":"認証機構設定"
   },
-
   "markdown_setting": {
     "markdown_rendering": "Markdownレンダリングの設定を変更できます。",
     "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": "自分自身を管理者から外すことはできません"
+  }
 
-      }
 }
 

+ 14 - 0
lib/models/config.js

@@ -85,6 +85,8 @@ module.exports = function(crowi) {
       'customize:css' : '',
       'customize:script' : '',
       'customize:header' : '',
+      'customize:highlightJsStyle' : 'github',
+      'customize:highlightJsStyleBorder' : false,
       'customize:behavior' : 'crowi',
       'customize:layout' : 'crowi',
       'customize:isEnabledTimeline' : true,
@@ -370,6 +372,18 @@ module.exports = function(crowi) {
     return getValueForCrowiNS(config, key);
   }
 
+  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';

+ 14 - 0
lib/routes/admin.js

@@ -130,8 +130,22 @@ module.exports = function(crowi, app) {
     var settingForm;
     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', {
       settingForm: settingForm,
+      highlightJsCssSelectorOptions: highlightJsCssSelectorOptions
     });
   };
 

+ 1 - 0
lib/routes/index.js

@@ -77,6 +77,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/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/highlightJsStyle' , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customhighlightJsStyle, admin.api.customizeSetting);
 
   // search admin
   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);
   }
 
+  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() {
     var config = crowi.getConfig()
     return Config.isEnabledTimeline(config);

+ 77 - 7
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;
@@ -14,7 +13,6 @@
   </style>
 {% endblock %}
 
-
 {% block content_head %}
 <div class="header-wrap">
   <header id="page-header">
@@ -143,7 +141,6 @@
       </fieldset>
       </form>
 
-
       <form action="/_api/admin/customize/features" method="post" class="form-horizontal" id="customfeaturesSettingForm" role="form">
       <fieldset>
       <legend>{{ t('customize_page.Function') }}</legend>
@@ -204,6 +201,67 @@
       </fieldset>
       </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">
       <fieldset>
         <legend>カスタムヘッダーHTML</legend>
@@ -215,7 +273,7 @@
 
         <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 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>
 
         <div class="form-group">
@@ -301,7 +359,7 @@
         </p>
         <p class="help-block">
           Examples:
-<pre><code>console.log($('.main-container'));
+<pre class="hljs"><code>console.log($('.main-container'));
 
 window.addEventListener('load', (event) => {
   console.log('config: ', crowi.config);
@@ -335,7 +393,7 @@ window.addEventListener('load', (event) => {
   </div>
 
   <script>
-    $('#cutomcssSettingForm, #cutomscriptSettingForm, #cutomlayoutSettingForm, #cutombehaviorSettingForm, #customfeaturesSettingForm, #cutomheaderSettingForm').each(function() {
+    $('#cutomcssSettingForm, #cutomscriptSettingForm, #cutomlayoutSettingForm, #cutombehaviorSettingForm, #customfeaturesSettingForm, #cutomheaderSettingForm, #cutomhighlightJsStyleSettingForm').each(function() {
       $(this).submit(function()
       {
         function showMessage(formId, msg, status) {
@@ -383,6 +441,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>
 
 </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 -->
   <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/emojione@3.1.2/extras/css/emojione.min.css">
   <!-- 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 %}
 

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

+ 8 - 5
package.json

@@ -1,6 +1,6 @@
 {
   "name": "crowi-plus",
-  "version": "2.4.3-RC",
+  "version": "2.4.4-RC",
   "description": "Enhanced Crowi",
   "tags": [
     "wiki",
@@ -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",

+ 6 - 0
resource/js/app.js

@@ -82,6 +82,12 @@ if (isEnabledPlugins) {
 // configure renderer
 crowiRenderer.setup(crowi.config);
 
+// restore draft when the first time to edit
+const draft = crowi.findDraft(pagePath);
+if (!pageRevisionId && draft != null) {
+  markdown = draft;
+}
+
 /**
  * define components
  *  key: id of element

+ 0 - 12
resource/js/components/PageEditor.js

@@ -60,8 +60,6 @@ export default class PageEditor extends React.Component {
   }
 
   componentWillMount() {
-    // restore draft
-    this.restoreDraft();
     // initial rendering
     this.renderPreview(this.state.markdown);
   }
@@ -284,16 +282,6 @@ export default class PageEditor extends React.Component {
     scrollSyncHelper.scrollEditor(this.refs.editor, this.previewElement, offset);
   }
 
-  /*
-   * methods for draft
-   */
-  restoreDraft() {
-    // restore draft when the first time to edit
-    const draft = this.props.crowi.findDraft(this.props.pagePath);
-    if (!this.props.revisionId && draft != null) {
-      this.setState({markdown: draft});
-    }
-  }
   saveDraft() {
     // only when the first time to edit
     if (!this.state.revisionId) {

+ 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

+ 12 - 0
resource/js/legacy/crowi.js

@@ -472,6 +472,16 @@ $(function() {
 
   if (pageId) {
 
+    // for Crowi Template LangProcessor
+    $('.template-create-button', $('#revision-body')).on('click', function() {
+      var path = $(this).data('path');
+      var templateId = $(this).data('template');
+      var template = $('#' + templateId).html();
+
+      crowi.saveDraft(path, template);
+      top.location.href = `${path}#edit-form`;
+    });
+
     /*
      * transplanted to React components -- 2018.02.04 Yuki Takei
      *
@@ -806,6 +816,7 @@ $(function() {
   });
 });
 
+/*
 Crowi.getRevisionBodyContent = function() {
   return $('#revision-body-content').html();
 }
@@ -813,6 +824,7 @@ Crowi.getRevisionBodyContent = function() {
 Crowi.replaceRevisionBodyContent = function(html) {
   $('#revision-body-content').html(html);
 }
+*/
 
 Crowi.findHashFromUrl = function(url)
 {

+ 1 - 1
resource/js/util/LangProcessor/Template.js

@@ -59,7 +59,7 @@ export default class Template {
 
     const content = `
       <div class="page-template-builder">
-        <button class="template-create-button btn btn-default" data-template="${templateId}" data-path="${pageName}#edit-form">
+        <button class="template-create-button btn btn-default" data-template="${templateId}" data-path="${pageName}">
           <i class="fa fa-pencil"></i> ${pageName}
         </button>
         <pre><code id="${templateId}" class="lang-${lang}">${code}\n</code></pre>

Fichier diff supprimé car celui-ci est trop grand
+ 489 - 19
yarn.lock


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff