Просмотр исходного кода

Merge branch 'master' into support/admin-top-bilingual

Yuki Takei 8 лет назад
Родитель
Сommit
3c23a1e2cc
44 измененных файлов с 883 добавлено и 187 удалено
  1. 31 1
      CHANGES.md
  2. 1 6
      LICENSE
  3. 80 0
      THIRD-PARTY-NOTICES.md
  4. 30 0
      bin/shrink-emojione-strategy.js
  5. 9 0
      config/webpack.common.js
  6. 9 0
      lib/form/admin/customheader.js
  7. 1 0
      lib/form/index.js
  8. 2 4
      lib/models/comment.js
  9. 7 0
      lib/models/config.js
  10. 9 7
      lib/models/page.js
  11. 0 5
      lib/routes/attachment.js
  12. 14 6
      lib/routes/comment.js
  13. 1 0
      lib/routes/index.js
  14. 6 8
      lib/util/i18nUserSettingDetector.js
  15. 6 2
      lib/util/middlewares.js
  16. 5 0
      lib/util/swigFunctions.js
  17. 37 1
      lib/views/admin/customize.html
  18. 1 1
      lib/views/crowi-plus/base/user_page_nosidebar.html
  19. 1 1
      lib/views/crowi-plus/page.html
  20. 13 0
      lib/views/layout/layout.html
  21. 12 0
      lib/views/page_presentation.html
  22. 8 5
      package.json
  23. 2 2
      resource/css/_user_crowi-plus.scss
  24. 1 1
      resource/css/_wiki.scss
  25. 1 1
      resource/css/_wiki_crowi-plus.scss
  26. 11 0
      resource/js/app.js
  27. 59 0
      resource/js/components/Admin/CustomHeaderEditor.js
  28. 97 19
      resource/js/components/PageEditor.js
  29. 35 6
      resource/js/components/PageEditor/Editor.js
  30. 12 21
      resource/js/components/PageEditor/EmojiAutoCompleteHelper.js
  31. 7 0
      resource/js/components/PageEditor/Preview.js
  32. 195 0
      resource/js/components/PageEditor/ScrollSyncHelper.js
  33. 1 1
      resource/js/components/SearchTypeahead.js
  34. 6 10
      resource/js/util/Crowi.js
  35. 4 1
      resource/js/util/GrowiRenderer.js
  36. 2 2
      resource/js/util/PreProcessor/CsvToTable.js
  37. 0 0
      resource/js/util/emojione/emoji_strategy_shrinked.json
  38. 5 6
      resource/js/util/interceptor/detach-code-blocks.js
  39. 11 1
      resource/js/util/markdown-it/emoji.js
  40. 21 31
      resource/js/util/markdown-it/header-line-number.js
  41. 14 16
      resource/js/util/markdown-it/header.js
  42. 5 6
      resource/js/util/markdown-it/plantuml.js
  43. 1 0
      resource/js/util/markdown-it/toc-and-anchor.js
  44. 110 16
      yarn.lock

+ 31 - 1
CHANGES.md

@@ -1,7 +1,35 @@
 CHANGES
 CHANGES
 ========
 ========
 
 
-## 2.4.0-RC
+## 2.4.3-RC
+
+* 
+
+
+## 2.4.2
+
+* Improvement: Ensure to set absolute url from root when attaching files when `FILE_UPLOAD=local`
+* Fix: Inline code blocks that includes doller sign are broken
+* Fix: Comment count is not updated when a comment of the page is deleted
+* Improvement: i18n in `/admin` (WIP)
+* Support: Upgrade libs
+    * googleapis
+    * markdown-it-plantuml
+
+## 2.4.1
+
+* Feature: Custom Header HTML
+* Improvement: Add highlight.js languages
+    * dockerfile, go, gradle, json, less, scss, typescript, yaml
+* Fix: Couldn't connect to PLANTUML_URI
+    * Introduced by 2.4.0
+* Fix: Couldn't render UML which includes CJK
+    * Introduced by 2.4.0
+* Support: Upgrade libs
+    * axios
+    * diff2html
+
+## 2.4.0
 
 
 * Feature: Support Footnotes
 * Feature: Support Footnotes
 * Feature: Support Task lists
 * Feature: Support Task lists
@@ -10,7 +38,9 @@ CHANGES
 * Feature: Enable to switch whether rendering MathJax in realtime or not
 * Feature: Enable to switch whether rendering MathJax in realtime or not
 * Improvement: Replace markdown parser with markdown-it
 * Improvement: Replace markdown parser with markdown-it
 * Improvement: Generate anchor of headers with header strings
 * Improvement: Generate anchor of headers with header strings
+* Improvement: Enhanced Scroll Sync on Markdown Editor/Preview
 * Improvement: Update `#revision-body` tab contents after saving with `Ctrl-S`
 * Improvement: Update `#revision-body` tab contents after saving with `Ctrl-S`
+* Fix: 500 Internal Server Error occures when basic-auth configuration is set
 
 
 ## 2.3.9
 ## 2.3.9
 
 

+ 1 - 6
LICENSE

@@ -1,6 +1,6 @@
 MIT License
 MIT License
 
 
-Copyright (c) 2017 WESEEK, Inc.
+Copyright (c) 2018 WESEEK, Inc.
 
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
 of this software and associated documentation files (the "Software"), to deal
@@ -19,8 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
 SOFTWARE.
-
-----
-
-This software is copied and modified from https://github.com/crowi/crowi,
-released under the MIT license, Copyright (c) 2013 Sotaro KARASAWA <sotaro.k@gmail.com>

+ 80 - 0
THIRD-PARTY-NOTICES.md

@@ -0,0 +1,80 @@
+crowi-plus uses third-party libraries or other resources that may  
+be distributed under licenses different than the crowi-plus software.
+
+In the event that we accidentally failed to list a required notice,  
+please bring it to our attention through any of the ways detailed here :
+
+    info@weseek.co.jp
+
+The attached notices are provided for information only.
+
+For any licenses that require disclosure of source, sources are available at  
+https://github.com/weseek/crowi-plus.
+
+
+1. crowi/crowi (https://github.com/crowi/crowi)
+2. Microsoft/vscode (https://github.com/Microsoft/vscode)
+
+
+
+License Notice for Crowi
+-------------------------
+
+https://github.com/crowi/crowi/blob/master/LICENSE
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2013 Sotaro KARASAWA <sotaro.k@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+
+License Notice for Visual Studio Code
+-------------------------------------
+
+https://github.com/Microsoft/vscode/blob/master/LICENSE.txt
+
+```
+MIT License
+
+Copyright (c) 2015 - present Microsoft Corporation
+
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+```

+ 30 - 0
bin/shrink-emojione-strategy.js

@@ -0,0 +1,30 @@
+/**
+ * the tool to shrink emojione/emoji_strategy.json and output
+ *
+ * @author Yuki Takei <yuki@weseek.co.jp>
+ */
+const fs = require('graceful-fs');
+const normalize = require('normalize-path');
+const helpers = require('../config/helpers');
+
+const OUT = helpers.root('tmp/emoji_strategy_shrinked.json');
+
+const emojiStrategy = require('emojione/emoji_strategy.json');
+const markdownItEmojiFull = require('markdown-it-emoji/lib/data/full.json');
+
+let shrinkedMap = {};
+for (let unicode in emojiStrategy) {
+  const data = emojiStrategy[unicode];
+  const shortname = data.shortname.replace(/\:/g, '');
+
+  // ignore if it isn't included in markdownItEmojiFull
+  if (markdownItEmojiFull[shortname] == null) {
+    continue;
+  }
+
+  // add
+  shrinkedMap[unicode] = data;
+}
+
+// write
+fs.writeFileSync(OUT, JSON.stringify(shrinkedMap));

+ 9 - 0
config/webpack.common.js

@@ -10,6 +10,7 @@ const helpers = require('./helpers');
  */
  */
 const AssetsPlugin = require('assets-webpack-plugin');
 const AssetsPlugin = require('assets-webpack-plugin');
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
 const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
+const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');;
 
 
 /*
 /*
  * Webpack configuration
  * Webpack configuration
@@ -47,6 +48,9 @@ module.exports = function (options) {
           exclude: /node_modules/,
           exclude: /node_modules/,
           use: [{
           use: [{
             loader: 'babel-loader?cacheDirectory',
             loader: 'babel-loader?cacheDirectory',
+            options: {
+              plugins: ['lodash'],
+            }
           }]
           }]
         },
         },
         {
         {
@@ -98,6 +102,11 @@ module.exports = function (options) {
         chunks: ['commons', 'plugin'],
         chunks: ['commons', 'plugin'],
       }),
       }),
 
 
+      new LodashModuleReplacementPlugin,
+
+      // ignore
+      new webpack.IgnorePlugin(/^\.\/lib\/deflate\.js/, /markdown-it-plantuml/),
+
       new webpack.ProvidePlugin({ // refs externals
       new webpack.ProvidePlugin({ // refs externals
         jQuery: "jquery",
         jQuery: "jquery",
         $: "jquery",
         $: "jquery",

+ 9 - 0
lib/form/admin/customheader.js

@@ -0,0 +1,9 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field
+  ;
+
+module.exports = form(
+  field('settingForm[customize:header]')
+);

+ 1 - 0
lib/form/index.js

@@ -22,6 +22,7 @@ module.exports = {
     markdown: require('./admin/markdown'),
     markdown: require('./admin/markdown'),
     customcss: require('./admin/customcss'),
     customcss: require('./admin/customcss'),
     customscript: require('./admin/customscript'),
     customscript: require('./admin/customscript'),
+    customheader: require('./admin/customheader'),
     custombehavior: require('./admin/custombehavior'),
     custombehavior: require('./admin/custombehavior'),
     customlayout: require('./admin/customlayout'),
     customlayout: require('./admin/customlayout'),
     customfeatures: require('./admin/customfeatures'),
     customfeatures: require('./admin/customfeatures'),

+ 2 - 4
lib/models/comment.js

@@ -122,10 +122,8 @@ module.exports = function(crowi) {
       , Comment = crowi.model('Comment')
       , Comment = crowi.model('Comment')
     ;
     ;
 
 
-    Comment.countCommentByPageId(savedComment.page)
-    .then(function(count) {
-      return Page.updateCommentCount(savedComment.page, count);
-    }).then(function(page) {
+    Page.updateCommentCount(savedComment.page)
+    .then(function(page) {
       debug('CommentCount Updated', page);
       debug('CommentCount Updated', page);
     }).catch(function() {
     }).catch(function() {
     });
     });

+ 7 - 0
lib/models/config.js

@@ -84,6 +84,7 @@ module.exports = function(crowi) {
 
 
       'customize:css' : '',
       'customize:css' : '',
       'customize:script' : '',
       'customize:script' : '',
+      'customize:header' : '',
       'customize:behavior' : 'crowi',
       'customize:behavior' : 'crowi',
       'customize:layout' : 'crowi',
       'customize:layout' : 'crowi',
       'customize:isEnabledTimeline' : true,
       'customize:isEnabledTimeline' : true,
@@ -351,6 +352,12 @@ module.exports = function(crowi) {
     return this._customScript;
     return this._customScript;
   }
   }
 
 
+  configSchema.statics.customHeader = function(config)
+  {
+    const key = 'customize:header';
+    return getValueForCrowiNS(config, key);
+  }
+
   configSchema.statics.behaviorType = function(config)
   configSchema.statics.behaviorType = function(config)
   {
   {
     const key = 'customize:behavior';
     const key = 'customize:behavior';

+ 9 - 7
lib/models/page.js

@@ -16,7 +16,8 @@ module.exports = function(crowi) {
 
 
     , pageEvent = crowi.event('page')
     , pageEvent = crowi.event('page')
 
 
-    , pageSchema;
+    , pageSchema
+    , Comment = crowi.model('Comment');
 
 
   function isPortalPath(path) {
   function isPortalPath(path) {
     if (path.match(/.*\/$/)) {
     if (path.match(/.*\/$/)) {
@@ -305,18 +306,19 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  pageSchema.statics.updateCommentCount = function (page, num)
+  pageSchema.statics.updateCommentCount = function (pageId)
   {
   {
     var self = this;
     var self = this;
-
-    return new Promise(function(resolve, reject) {
-      self.update({_id: page}, {commentCount: num}, {}, function(err, data) {
+    var Comment = crowi.model("Comment");
+    return Comment.countCommentByPageId(pageId)
+    .then(function(count) {
+      self.update({_id: pageId}, {commentCount: count}, {}, function(err, data) {
         if (err) {
         if (err) {
           debug('Update commentCount Error', err);
           debug('Update commentCount Error', err);
-          return reject(err);
+          throw err;
         }
         }
 
 
-        return resolve(data);
+        return data;
       });
       });
     });
     });
   };
   };

+ 0 - 5
lib/routes/attachment.js

@@ -145,11 +145,6 @@ module.exports = function(crowi, app) {
           var fileUrl = data.fileUrl;
           var fileUrl = data.fileUrl;
           var config = crowi.getConfig();
           var config = crowi.getConfig();
 
 
-          // isLocalUrl??
-          if (!fileUrl.match(/^https?/)) {
-            fileUrl = (config.crowi['app:url'] || '') + fileUrl;
-          }
-
           var result = {
           var result = {
             page: page.toObject(),
             page: page.toObject(),
             attachment: data.toObject(),
             attachment: data.toObject(),

+ 14 - 6
lib/routes/comment.js

@@ -82,15 +82,23 @@ module.exports = function(crowi, app) {
   api.remove = function(req, res){
   api.remove = function(req, res){
     var commentId = req.body.comment_id;
     var commentId = req.body.comment_id;
     if (!commentId) {
     if (!commentId) {
-      return res.json(ApiResponse.error(`'comment_id' is undefined`));
+      return Promise.resolve(res.json(ApiResponse.error(`'comment_id' is undefined`)));
     }
     }
 
 
-    return Comment.remove({_id: commentId})
-      .then(function() {
-        return res.json(ApiResponse.success({}));
-      }).catch(function(err) {
-        return res.json(ApiResponse.error(err));
+    return Comment.findById(commentId).exec()
+      .then(function(comment) {
+        return comment.remove()
+        .then(function() {
+           return Page.updateCommentCount(comment.page);
+        })
+        .then(function() {
+           return res.json(ApiResponse.success({})); 
+        });
+      })
+      .catch(function(err) {
+        return res.json(ApiResponse.error(err)); 
       });
       });
+
   };
   };
 
 
   return actions;
   return actions;

+ 1 - 0
lib/routes/index.js

@@ -73,6 +73,7 @@ module.exports = function(crowi, app) {
   app.get('/admin/customize'                , loginRequired(crowi, app) , middleware.adminRequired() , admin.customize.index);
   app.get('/admin/customize'                , loginRequired(crowi, app) , middleware.adminRequired() , admin.customize.index);
   app.post('/_api/admin/customize/css'      , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customcss, admin.api.customizeSetting);
   app.post('/_api/admin/customize/css'      , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customcss, admin.api.customizeSetting);
   app.post('/_api/admin/customize/script'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customscript, admin.api.customizeSetting);
   app.post('/_api/admin/customize/script'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customscript, admin.api.customizeSetting);
+  app.post('/_api/admin/customize/header'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.customheader, 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/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);

+ 6 - 8
lib/util/i18nUserSettingDetector.js

@@ -2,15 +2,13 @@ module.exports = {
   name: 'userSettingDetector',
   name: 'userSettingDetector',
 
 
   lookup: function(req, res, options) {
   lookup: function(req, res, options) {
-    var lang = null;
-
-    if (req.user) {
-      if ('lang' in req.user) {
-        lang = req.user.lang || null;
-      }
+    // return null if
+    //  1. user doesn't logged in
+    //  2. req.user is username/email string to login which is set by basic-auth-connect
+    if (req.user == null || !(req.user instanceof Object)) {
+      return null;
     }
     }
-
-    return lang;
+    return req.user.lang || null;
   },
   },
 
 
   cacheUserlanguage: function(req, res, lng, options) {
   cacheUserlanguage: function(req, res, lng, options) {

+ 6 - 2
lib/util/middlewares.js

@@ -182,7 +182,9 @@ exports.swigFilters = function(app, swig) {
 
 
 exports.adminRequired = function() {
 exports.adminRequired = function() {
   return function(req, res, next) {
   return function(req, res, next) {
-    if (req.user && '_id' in req.user) {
+    // check the user logged in
+    //  make sure that req.user isn't username/email string to login which is set by basic-auth-connect
+    if (req.user != null && (req.user instanceof Object) && '_id' in req.user) {
       if (req.user.admin) {
       if (req.user.admin) {
         next();
         next();
         return;
         return;
@@ -215,7 +217,9 @@ exports.loginRequired = function(crowi, app, isStrictly = true) {
       }
       }
     }
     }
 
 
-    if (req.user && '_id' in req.user) {
+    // check the user logged in
+    //  make sure that req.user isn't username/email string to login which is set by basic-auth-connect
+    if (req.user != null && (req.user instanceof Object) && '_id' in req.user) {
       if (req.user.status === User.STATUS_ACTIVE) {
       if (req.user.status === User.STATUS_ACTIVE) {
         // Active の人だけ先に進める
         // Active の人だけ先に進める
         return next();
         return next();

+ 5 - 0
lib/util/swigFunctions.js

@@ -92,6 +92,11 @@ module.exports = function(crowi, app, req, locals) {
     return Config.customScript();
     return Config.customScript();
   }
   }
 
 
+  locals.customHeader = function() {
+    var config = crowi.getConfig()
+    return Config.customHeader(config);
+  }
+
   locals.behaviorType = function() {
   locals.behaviorType = function() {
     var config = crowi.getConfig()
     var config = crowi.getConfig()
     return Config.behaviorType(config);
     return Config.behaviorType(config);

+ 37 - 1
lib/views/admin/customize.html

@@ -204,6 +204,42 @@
       </fieldset>
       </fieldset>
       </form>
       </form>
 
 
+      <form action="/_api/admin/customize/header" method="post" class="form-horizontal" id="cutomheaderSettingForm" role="form">
+      <fieldset>
+        <legend>カスタムヘッダーHTML</legend>
+
+        <p class="well">
+          システム全体に適用される HTML を記述できます。<code>&lt;header&gt;</code> タグ内の他の <code>&lt;script&gt;</code> タグ読み込み前に展開されます。<br>
+          変更の反映はページの更新が必要です。
+        </p>
+
+        <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>
+        </p>
+
+        <div class="form-group">
+          <div class="col-xs-12">
+            <div id="custom-header-editor"></div>
+            <input type="hidden" id="inputCustomHeader" name="settingForm[customize:header]" value="{{ settingForm['customize:header'] }}">
+          </div>
+          <div class="col-xs-12">
+            <p class="help-block text-right">
+              <i class="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
+              Ctrl+Space でコード補完
+            </p>
+          </div>
+        </div>
+
+        <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">更新</button>
+          </div>
+        </div>
+
+      </fieldset>
+      </form>
 
 
       <form action="/_api/admin/customize/css" method="post" class="form-horizontal" id="cutomcssSettingForm" role="form">
       <form action="/_api/admin/customize/css" method="post" class="form-horizontal" id="cutomcssSettingForm" role="form">
       <fieldset>
       <fieldset>
@@ -299,7 +335,7 @@ window.addEventListener('load', (event) => {
   </div>
   </div>
 
 
   <script>
   <script>
-    $('#cutomcssSettingForm, #cutomscriptSettingForm, #cutomlayoutSettingForm, #cutombehaviorSettingForm, #customfeaturesSettingForm').each(function() {
+    $('#cutomcssSettingForm, #cutomscriptSettingForm, #cutomlayoutSettingForm, #cutombehaviorSettingForm, #customfeaturesSettingForm, #cutomheaderSettingForm').each(function() {
       $(this).submit(function()
       $(this).submit(function()
       {
       {
         function showMessage(formId, msg, status) {
         function showMessage(formId, msg, status) {

+ 1 - 1
lib/views/crowi-plus/base/user_page_nosidebar.html

@@ -46,7 +46,7 @@
 
 
         {# relocate #revision-toc #}
         {# relocate #revision-toc #}
         <div class="col-lg-2 col-md-3 visible-lg visible-md">
         <div class="col-lg-2 col-md-3 visible-lg visible-md">
-          <div id="revision-toc" class="revision-toc" data-spy="affix" data-offset-top="50">
+          <div id="revision-toc" class="revision-toc" data-spy="affix" data-offset-top="54">
             <div id="revision-toc-content" class="revision-toc-content"></div>
             <div id="revision-toc-content" class="revision-toc-content"></div>
           </div>
           </div>
         </div> {# /.col- #}
         </div> {# /.col- #}

+ 1 - 1
lib/views/crowi-plus/page.html

@@ -34,7 +34,7 @@
 
 
       {# relocate #revision-toc #}
       {# relocate #revision-toc #}
       <div class="col-lg-2 col-md-3 visible-lg visible-md">
       <div class="col-lg-2 col-md-3 visible-lg visible-md">
-        <div id="revision-toc" class="revision-toc" data-spy="affix" data-offset-top="100">
+        <div id="revision-toc" class="revision-toc" data-spy="affix" data-offset-top="128">
           <div id="revision-toc-content" class="revision-toc-content"></div>
           <div id="revision-toc-content" class="revision-toc-content"></div>
         </div>
         </div>
       </div> {# /.col- #}
       </div> {# /.col- #}

+ 13 - 0
lib/views/layout/layout.html

@@ -22,6 +22,8 @@
   <link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96">
   <link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96">
   <link rel="icon" type="image/png" href="/android-chrome-192x192.png" sizes="192x192">
   <link rel="icon" type="image/png" href="/android-chrome-192x192.png" sizes="192x192">
 
 
+  {{ customHeader() }}
+
   <!-- polyfills for IE11 -->
   <!-- polyfills for IE11 -->
   <script>
   <script>
     var userAgent = window.navigator.userAgent.toLowerCase();
     var userAgent = window.navigator.userAgent.toLowerCase();
@@ -37,6 +39,17 @@
   <script src="https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.3.1"></script>
   <script src="https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.3.1"></script>
   <!-- highlight.js -->
   <!-- highlight.js -->
   <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.12.0/build/highlight.min.js"></script>
   <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.12.0/build/highlight.min.js"></script>
+  <script src="https://cdn.jsdelivr.net/combine/
+gh/highlightjs/cdn-release@9.12.0/build/languages/dockerfile.min.js,
+gh/highlightjs/cdn-release@9.12.0/build/languages/go.min.js,
+gh/highlightjs/cdn-release@9.12.0/build/languages/gradle.min.js,
+gh/highlightjs/cdn-release@9.12.0/build/languages/json.min.js,
+gh/highlightjs/cdn-release@9.12.0/build/languages/less.min.js,
+gh/highlightjs/cdn-release@9.12.0/build/languages/scss.min.js,
+gh/highlightjs/cdn-release@9.12.0/build/languages/typescript.min.js,
+gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
+" defer></script>
+
   {% if local_config.env.MATHJAX %}
   {% if local_config.env.MATHJAX %}
     <!-- Mathjax -->
     <!-- Mathjax -->
     <script type="text/x-mathjax-config" async>
     <script type="text/x-mathjax-config" async>

+ 12 - 0
lib/views/page_presentation.html

@@ -6,6 +6,8 @@
     <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
     <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
 
 
+    {{ customHeader() }}
+
     <!-- polyfills for IE11 -->
     <!-- polyfills for IE11 -->
     <script>
     <script>
       var userAgent = window.navigator.userAgent.toLowerCase();
       var userAgent = window.navigator.userAgent.toLowerCase();
@@ -21,6 +23,16 @@
     <script src="https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.3.1"></script>
     <script src="https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.3.1"></script>
     <!-- highlight.js -->
     <!-- highlight.js -->
     <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.12.0/build/highlight.min.js"></script>
     <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.12.0/build/highlight.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/combine/
+gh/highlightjs/cdn-release@9.12.0/build/languages/dockerfile.min.js,
+gh/highlightjs/cdn-release@9.12.0/build/languages/go.min.js,
+gh/highlightjs/cdn-release@9.12.0/build/languages/gradle.min.js,
+gh/highlightjs/cdn-release@9.12.0/build/languages/json.min.js,
+gh/highlightjs/cdn-release@9.12.0/build/languages/less.min.js,
+gh/highlightjs/cdn-release@9.12.0/build/languages/scss.min.js,
+gh/highlightjs/cdn-release@9.12.0/build/languages/typescript.min.js,
+gh/highlightjs/cdn-release@9.12.0/build/languages/yaml.min.js
+" defer></script>
 
 
     {% if env === 'development' %}
     {% if env === 'development' %}
       <script src="/dll/vendor.dll.js"></script>
       <script src="/dll/vendor.dll.js"></script>

+ 8 - 5
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "crowi-plus",
   "name": "crowi-plus",
-  "version": "2.4.0-RC",
+  "version": "2.4.3-RC",
   "description": "Enhanced Crowi",
   "description": "Enhanced Crowi",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -51,9 +51,10 @@
     "assets-webpack-plugin": "~3.5.1",
     "assets-webpack-plugin": "~3.5.1",
     "async": "^2.3.0",
     "async": "^2.3.0",
     "aws-sdk": "^2.88.0",
     "aws-sdk": "^2.88.0",
-    "axios": "^0.17.0",
+    "axios": "^0.18.0",
     "babel-core": "^6.25.0",
     "babel-core": "^6.25.0",
     "babel-loader": "^7.1.1",
     "babel-loader": "^7.1.1",
+    "babel-plugin-lodash": "^3.3.2",
     "babel-preset-env": "^1.6.0",
     "babel-preset-env": "^1.6.0",
     "babel-preset-react": "^6.24.1",
     "babel-preset-react": "^6.24.1",
     "basic-auth-connect": "~1.0.0",
     "basic-auth-connect": "~1.0.0",
@@ -72,7 +73,7 @@
     "date-fns": "^1.29.0",
     "date-fns": "^1.29.0",
     "debug": "^3.1.0",
     "debug": "^3.1.0",
     "diff": "^3.3.0",
     "diff": "^3.3.0",
-    "diff2html": "^2.3.0",
+    "diff2html": "^2.3.3",
     "elasticsearch": "^14.0.0",
     "elasticsearch": "^14.0.0",
     "entities": "^1.1.1",
     "entities": "^1.1.1",
     "env-cmd": "^7.0.0",
     "env-cmd": "^7.0.0",
@@ -83,7 +84,7 @@
     "express-session": "~1.15.0",
     "express-session": "~1.15.0",
     "express-webpack-assets": "^0.1.0",
     "express-webpack-assets": "^0.1.0",
     "file-loader": "^1.1.0",
     "file-loader": "^1.1.0",
-    "googleapis": "^26.0.0",
+    "googleapis": "^27.0.0",
     "graceful-fs": "^4.1.11",
     "graceful-fs": "^4.1.11",
     "i18next": "^10.0.1",
     "i18next": "^10.0.1",
     "i18next-express-middleware": "^1.0.5",
     "i18next-express-middleware": "^1.0.5",
@@ -91,12 +92,13 @@
     "i18next-sprintf-postprocessor": "^0.2.2",
     "i18next-sprintf-postprocessor": "^0.2.2",
     "jquery-ui": "^1.12.1",
     "jquery-ui": "^1.12.1",
     "jquery.cookie": "~1.4.1",
     "jquery.cookie": "~1.4.1",
+    "lodash-webpack-plugin": "^0.11.4",
     "markdown-it": "^8.4.0",
     "markdown-it": "^8.4.0",
     "markdown-it-emoji": "^1.4.0",
     "markdown-it-emoji": "^1.4.0",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-mathjax": "^2.0.0",
     "markdown-it-mathjax": "^2.0.0",
     "markdown-it-named-headers": "^0.0.4",
     "markdown-it-named-headers": "^0.0.4",
-    "markdown-it-plantuml": "^0.3.1",
+    "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",
     "md5": "^2.2.1",
     "md5": "^2.2.1",
@@ -115,6 +117,7 @@
     "passport-ldapauth": "^2.0.0",
     "passport-ldapauth": "^2.0.0",
     "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",
     "react": "^16.0.0",
     "react": "^16.0.0",
     "react-bootstrap": "^0.32.0",
     "react-bootstrap": "^0.32.0",
     "react-bootstrap-typeahead": "^2.0.2",
     "react-bootstrap-typeahead": "^2.0.2",

+ 2 - 2
resource/css/_user_crowi-plus.scss

@@ -7,12 +7,12 @@
 
 
     .revision-toc {
     .revision-toc {
       .revision-toc-content {
       .revision-toc-content {
-        margin-top: 25px;
+        margin-top: 0;
         background: none;
         background: none;
       }
       }
 
 
       &.affix {
       &.affix {
-        top: 10px;
+        top: 0;
       }
       }
     }
     }
 
 

+ 1 - 1
resource/css/_wiki.scss

@@ -34,7 +34,7 @@ div.body {
     background: #fcfcfc;
     background: #fcfcfc;
     padding: 10px;
     padding: 10px;
 
 
-    > ul {
+    > ul > li { // first level of li
       margin: 4px 4px 4px 15px;
       margin: 4px 4px 4px 15px;
       padding: 5px;
       padding: 5px;
     }
     }

+ 1 - 1
resource/css/_wiki_crowi-plus.scss

@@ -15,7 +15,7 @@
 
 
     // affix
     // affix
     &.affix {
     &.affix {
-      top: 30px;
+      top: 0;
     }
     }
   }
   }
 }
 }

+ 11 - 0
resource/js/app.js

@@ -25,6 +25,7 @@ import SearchTypeahead  from './components/SearchTypeahead';
 
 
 import CustomCssEditor  from './components/Admin/CustomCssEditor';
 import CustomCssEditor  from './components/Admin/CustomCssEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomScriptEditor from './components/Admin/CustomScriptEditor';
+import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 
 
 import * as entities from 'entities';
 import * as entities from 'entities';
 
 
@@ -193,6 +194,16 @@ if (customScriptEditorElem != null) {
     customScriptEditorElem
     customScriptEditorElem
   )
   )
 }
 }
+const customHeaderEditorElem = document.getElementById('custom-header-editor');
+if (customHeaderEditorElem != null) {
+  // get input[type=hidden] element
+  const customHeaderInputElem = document.getElementById('inputCustomHeader');
+
+  ReactDOM.render(
+    <CustomHeaderEditor inputElem={customHeaderInputElem} />,
+    customHeaderEditorElem
+  )
+}
 
 
 // うわーもうー
 // うわーもうー
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', function() {
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', function() {

+ 59 - 0
resource/js/components/Admin/CustomHeaderEditor.js

@@ -0,0 +1,59 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { UnControlled as CodeMirror } from 'react-codemirror2';
+require('codemirror/lib/codemirror.css');
+require('codemirror/addon/display/autorefresh');
+require('codemirror/addon/hint/show-hint');
+require('codemirror/addon/edit/matchbrackets');
+require('codemirror/addon/edit/closebrackets');
+require('codemirror/mode/htmlmixed/htmlmixed');
+require('codemirror/theme/eclipse.css');
+
+require('jquery-ui/ui/widgets/resizable');
+
+export default class CustomHeaderEditor extends React.Component {
+
+  constructor(props) {
+    super(props);
+  }
+
+  render() {
+    // get initial value from inputElem
+    const value = this.props.inputElem.value;
+
+    return (
+      <CodeMirror
+        value={value}
+        autoFocus={true}
+        options={{
+          mode: 'htmlmixed',
+          lineNumbers: true,
+          tabSize: 2,
+          indentUnit: 2,
+          theme: 'eclipse',
+          autoRefresh: true,
+          matchBrackets: true,
+          autoCloseBrackets: true,
+          extraKeys: {"Ctrl-Space": "autocomplete"},
+        }}
+        editorDidMount={(editor, next) => {
+          // resizable with jquery.ui
+          $(editor.getWrapperElement()).resizable({
+            resize: function() {
+              editor.setSize($(this).width(), $(this).height());
+            }
+          });
+        }}
+        onChange={(editor, data, value) => {
+          this.props.inputElem.value = value;
+        }}
+      />
+    )
+  }
+
+}
+
+CustomHeaderEditor.propTypes = {
+  inputElem: PropTypes.object.isRequired,
+};

+ 97 - 19
resource/js/components/PageEditor.js

@@ -9,6 +9,7 @@ import GrowiRenderer from '../util/GrowiRenderer';
 import { EditorOptions, PreviewOptions } from './PageEditor/OptionsSelector';
 import { EditorOptions, PreviewOptions } from './PageEditor/OptionsSelector';
 import Editor from './PageEditor/Editor';
 import Editor from './PageEditor/Editor';
 import Preview from './PageEditor/Preview';
 import Preview from './PageEditor/Preview';
+import scrollSyncHelper from './PageEditor/ScrollSyncHelper';
 
 
 export default class PageEditor extends React.Component {
 export default class PageEditor extends React.Component {
 
 
@@ -38,16 +39,24 @@ export default class PageEditor extends React.Component {
     this.onSave = this.onSave.bind(this);
     this.onSave = this.onSave.bind(this);
     this.onUpload = this.onUpload.bind(this);
     this.onUpload = this.onUpload.bind(this);
     this.onEditorScroll = this.onEditorScroll.bind(this);
     this.onEditorScroll = this.onEditorScroll.bind(this);
-    this.getMaxScrollTop = this.getMaxScrollTop.bind(this);
-    this.getScrollTop = this.getScrollTop.bind(this);
+    this.onEditorScrollCursorIntoView = this.onEditorScrollCursorIntoView.bind(this);
+    this.onPreviewScroll = this.onPreviewScroll.bind(this);
     this.saveDraft = this.saveDraft.bind(this);
     this.saveDraft = this.saveDraft.bind(this);
     this.clearDraft = this.clearDraft.bind(this);
     this.clearDraft = this.clearDraft.bind(this);
     this.pageSavedHandler = this.pageSavedHandler.bind(this);
     this.pageSavedHandler = this.pageSavedHandler.bind(this);
     this.apiErrorHandler = this.apiErrorHandler.bind(this);
     this.apiErrorHandler = this.apiErrorHandler.bind(this);
 
 
+    // for scrolling
+    this.lastScrolledDateWithCursor = null;
+    this.isOriginOfScrollSyncEditor = false;
+    this.isOriginOfScrollSyncEditor = false;
+
     // create throttled function
     // create throttled function
+    this.scrollPreviewByEditorLineWithThrottle = throttle(20, this.scrollPreviewByEditorLine);
+    this.scrollPreviewByCursorMovingWithThrottle = throttle(20, this.scrollPreviewByCursorMoving);
+    this.scrollEditorByPreviewScrollWithThrottle = throttle(20, this.scrollEditorByPreviewScroll);
     this.renderWithDebounce = debounce(50, throttle(100, this.renderPreview));
     this.renderWithDebounce = debounce(50, throttle(100, this.renderPreview));
-    this.saveDraftWithDebounce = debounce(300, this.saveDraft);
+    this.saveDraftWithDebounce = debounce(800, this.saveDraft);
   }
   }
 
 
   componentWillMount() {
   componentWillMount() {
@@ -67,6 +76,7 @@ export default class PageEditor extends React.Component {
    */
    */
   setCaretLine(line) {
   setCaretLine(line) {
     this.refs.editor.setCaretLine(line);
     this.refs.editor.setCaretLine(line);
+    scrollSyncHelper.scrollPreview(this.previewElement, line);
   }
   }
 
 
   /**
   /**
@@ -181,32 +191,99 @@ export default class PageEditor extends React.Component {
   /**
   /**
    * the scroll event handler from codemirror
    * the scroll event handler from codemirror
    * @param {any} data {left, top, width, height, clientWidth, clientHeight} object that represents the current scroll position, the size of the scrollable area, and the size of the visible area (minus scrollbars).
    * @param {any} data {left, top, width, height, clientWidth, clientHeight} object that represents the current scroll position, the size of the scrollable area, and the size of the visible area (minus scrollbars).
-   *    see https://codemirror.net/doc/manual.html#events
+   *                    And data.line is also available that is added by Editor component
+   * @see https://codemirror.net/doc/manual.html#events
    */
    */
   onEditorScroll(data) {
   onEditorScroll(data) {
-    const rate = data.top / (data.height - data.clientHeight)
-    const top = this.getScrollTop(this.previewElement, rate);
+    // prevent scrolling
+    //   if the elapsed time from last scroll with cursor is shorter than 40ms
+    const now = new Date();
+    if (now - this.lastScrolledDateWithCursor < 40) {
+      return;
+    }
 
 
-    this.previewElement.scrollTop = top;
+    this.scrollPreviewByEditorLineWithThrottle(data.line);
   }
   }
+
   /**
   /**
-   * transplanted from crowi-form.js -- 2018.01.21 Yuki Takei
-   * @param {*} dom
+   * the scroll event handler from codemirror
+   * @param {number} line
+   * @see https://codemirror.net/doc/manual.html#events
    */
    */
-  getMaxScrollTop(dom) {
-    var rect = dom.getBoundingClientRect();
-    return dom.scrollHeight - rect.height;
+  onEditorScrollCursorIntoView(line) {
+    // record date
+    this.lastScrolledDateWithCursor = new Date();
+    this.scrollPreviewByCursorMovingWithThrottle(line);
+  }
+
+  /**
+   * scroll Preview element by scroll event
+   * @param {number} line
+   */
+  scrollPreviewByEditorLine(line) {
+    if (this.previewElement == null) {
+      return;
+    }
+
+    // prevent circular invocation
+    if (this.isOriginOfScrollSyncPreview) {
+      this.isOriginOfScrollSyncPreview = false; // turn off the flag
+      return;
+    }
+
+    // turn on the flag
+    this.isOriginOfScrollSyncEditor = true;
+    scrollSyncHelper.scrollPreview(this.previewElement, line);
   };
   };
+
   /**
   /**
-   * transplanted from crowi-form.js -- 2018.01.21 Yuki Takei
-   * @param {*} dom
+   * scroll Preview element by cursor moving
+   * @param {number} line
    */
    */
-  getScrollTop(dom, rate) {
-    var maxScrollTop = this.getMaxScrollTop(dom);
-    var top = maxScrollTop * rate;
-    return top;
+  scrollPreviewByCursorMoving(line) {
+    if (this.previewElement == null) {
+      return;
+    }
+
+    // prevent circular invocation
+    if (this.isOriginOfScrollSyncPreview) {
+      this.isOriginOfScrollSyncPreview = false; // turn off the flag
+      return;
+    }
+
+    // turn on the flag
+    this.isOriginOfScrollSyncEditor = true;
+    scrollSyncHelper.scrollPreviewToRevealOverflowing(this.previewElement, line);
   };
   };
 
 
+  /**
+   * the scroll event handler from Preview component
+   * @param {number} offset
+   */
+  onPreviewScroll(offset) {
+    this.scrollEditorByPreviewScrollWithThrottle(offset);
+  }
+
+  /**
+   * scroll Editor component by scroll event of Preview component
+   * @param {number} offset
+   */
+  scrollEditorByPreviewScroll(offset) {
+    if (this.previewElement == null) {
+      return;
+    }
+
+    // prevent circular invocation
+    if (this.isOriginOfScrollSyncEditor) {
+      this.isOriginOfScrollSyncEditor = false;  // turn off the flag
+      return;
+    }
+
+    // turn on the flag
+    this.isOriginOfScrollSyncPreview = true;
+    scrollSyncHelper.scrollEditor(this.refs.editor, this.previewElement, offset);
+  }
+
   /*
   /*
    * methods for draft
    * methods for draft
    */
    */
@@ -287,7 +364,6 @@ export default class PageEditor extends React.Component {
       .then(() => interceptorManager.process('preRenderPreviewHtml', context))
       .then(() => interceptorManager.process('preRenderPreviewHtml', context))
       .then(() => {
       .then(() => {
         this.setState({ html: context.parsedHTML });
         this.setState({ html: context.parsedHTML });
-
         // set html to the hidden input (for submitting to save)
         // set html to the hidden input (for submitting to save)
         $('#form-body').val(this.state.markdown);
         $('#form-body').val(this.state.markdown);
       })
       })
@@ -305,6 +381,7 @@ export default class PageEditor extends React.Component {
               isUploadable={this.state.isUploadable}
               isUploadable={this.state.isUploadable}
               isUploadableFile={this.state.isUploadableFile}
               isUploadableFile={this.state.isUploadableFile}
               onScroll={this.onEditorScroll}
               onScroll={this.onEditorScroll}
+              onScrollCursorIntoView={this.onEditorScrollCursorIntoView}
               onChange={this.onMarkdownChanged}
               onChange={this.onMarkdownChanged}
               onSave={this.onSave}
               onSave={this.onSave}
               onUpload={this.onUpload}
               onUpload={this.onUpload}
@@ -316,6 +393,7 @@ export default class PageEditor extends React.Component {
               isMathJaxEnabled={this.state.isMathJaxEnabled}
               isMathJaxEnabled={this.state.isMathJaxEnabled}
               renderMathJaxOnInit={false}
               renderMathJaxOnInit={false}
               previewOptions={this.state.previewOptions}
               previewOptions={this.state.previewOptions}
+              onScroll={this.onPreviewScroll}
           />
           />
         </div>
         </div>
       </div>
       </div>

+ 35 - 6
resource/js/components/PageEditor/Editor.js

@@ -54,9 +54,11 @@ export default class Editor extends React.Component {
 
 
     this.getCodeMirror = this.getCodeMirror.bind(this);
     this.getCodeMirror = this.getCodeMirror.bind(this);
     this.setCaretLine = this.setCaretLine.bind(this);
     this.setCaretLine = this.setCaretLine.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.onScrollCursorIntoView = this.onScrollCursorIntoView.bind(this);
     this.onPaste = this.onPaste.bind(this);
     this.onPaste = this.onPaste.bind(this);
 
 
     this.onDragEnterForCM = this.onDragEnterForCM.bind(this);
     this.onDragEnterForCM = this.onDragEnterForCM.bind(this);
@@ -95,15 +97,30 @@ export default class Editor extends React.Component {
    * @param {string} number
    * @param {string} number
    */
    */
   setCaretLine(line) {
   setCaretLine(line) {
-    const editor = this.getCodeMirror();
+    if (isNaN(line)) {
+      return;
+    }
 
 
-    // scroll to the bottom for a moment
-    const lastLine = editor.getDoc().lastLine();
-    editor.scrollIntoView(lastLine);
+    const editor = this.getCodeMirror();
+    const linePosition = Math.max(0, line);
 
 
-    const linePosition = Math.max(0, line - 1);
-    editor.scrollIntoView(linePosition);
     editor.setCursor({line: linePosition});   // leave 'ch' field as null/undefined to indicate the end of line
     editor.setCursor({line: linePosition});   // leave 'ch' field as null/undefined to indicate the end of line
+    this.setScrollTopByLine(linePosition);
+  }
+
+  /**
+   * scroll
+   * @param {number} line
+   */
+  setScrollTopByLine(line) {
+    if (isNaN(line)) {
+      return;
+    }
+
+    const editor = this.getCodeMirror();
+    // get top position of the line
+    var top = editor.charCoords({line, ch: 0}, 'local').top;
+    editor.scrollTo(null, top);
   }
   }
 
 
   /**
   /**
@@ -143,6 +160,13 @@ export default class Editor extends React.Component {
     }
     }
   }
   }
 
 
+  onScrollCursorIntoView(editor, event) {
+    if (this.props.onScrollCursorIntoView != null) {
+      const line = editor.getCursor().line;
+      this.props.onScrollCursorIntoView(line);
+    }
+  }
+
   /**
   /**
    * CodeMirror paste event handler
    * CodeMirror paste event handler
    * see: https://codemirror.net/doc/manual.html#events
    * see: https://codemirror.net/doc/manual.html#events
@@ -296,6 +320,7 @@ export default class Editor extends React.Component {
             editorDidMount={(editor) => {
             editorDidMount={(editor) => {
               // add event handlers
               // add event handlers
               editor.on('paste', this.onPaste);
               editor.on('paste', this.onPaste);
+              editor.on('scrollCursorIntoView', this.onScrollCursorIntoView);
             }}
             }}
             value={this.state.value}
             value={this.state.value}
             options={{
             options={{
@@ -327,6 +352,9 @@ export default class Editor extends React.Component {
             }}
             }}
             onScroll={(editor, data) => {
             onScroll={(editor, data) => {
               if (this.props.onScroll != null) {
               if (this.props.onScroll != null) {
+                // add line data
+                const line = editor.lineAtHeight(data.top, 'local');
+                data.line = line;
                 this.props.onScroll(data);
                 this.props.onScroll(data);
               }
               }
             }}
             }}
@@ -363,6 +391,7 @@ Editor.propTypes = {
   isUploadableFile: PropTypes.bool,
   isUploadableFile: PropTypes.bool,
   onChange: PropTypes.func,
   onChange: PropTypes.func,
   onScroll: PropTypes.func,
   onScroll: PropTypes.func,
+  onScrollCursorIntoView: PropTypes.func,
   onSave: PropTypes.func,
   onSave: PropTypes.func,
   onUpload: PropTypes.func,
   onUpload: PropTypes.func,
 };
 };

+ 12 - 21
resource/js/components/PageEditor/EmojiAutoCompleteHelper.js

@@ -1,33 +1,23 @@
-import axios from 'axios';
+import emojiStrategy from '../../util/emojione/emoji_strategy_shrinked.json';
 
 
 class EmojiAutoCompleteHelper {
 class EmojiAutoCompleteHelper {
 
 
   constructor() {
   constructor() {
-    this.emojiStrategy = {};
     this.emojiShortnameImageMap = {}
     this.emojiShortnameImageMap = {}
 
 
-    this.initEmojiImageMap()
-      .then(() => {
-        Object.freeze(this);  // freeze after initializing data
-      })
-
     this.initEmojiImageMap = this.initEmojiImageMap.bind(this);
     this.initEmojiImageMap = this.initEmojiImageMap.bind(this);
     this.showHint = this.showHint.bind(this);
     this.showHint = this.showHint.bind(this);
+
+    this.initEmojiImageMap()
   }
   }
 
 
   initEmojiImageMap() {
   initEmojiImageMap() {
-    const emojiStrategyUrl = 'https://cdn.jsdelivr.net/npm/emojione@3.1.2/emoji_strategy.json';
-
-    return axios.get(emojiStrategyUrl)
-      .then((res) => {
-        this.emojiStrategy = res.data;
-        for (let unicode in this.emojiStrategy) {
-          const data = this.emojiStrategy[unicode];
-          const shortname = data.shortname;
-          // add image tag
-          this.emojiShortnameImageMap[shortname] = emojione.shortnameToImage(shortname);
-        }
-      });
+    for (let unicode in emojiStrategy) {
+      const data = emojiStrategy[unicode];
+      const shortname = data.shortname;
+      // add image tag
+      this.emojiShortnameImageMap[shortname] = emojione.shortnameToImage(shortname);
+    }
   }
   }
 
 
   /**
   /**
@@ -106,8 +96,8 @@ class EmojiAutoCompleteHelper {
     const countLen4 = () => { countLen3() + results4.length; }
     const countLen4 = () => { countLen3() + results4.length; }
     // TODO performance tune
     // TODO performance tune
     // when total length of all results is less than `maxLength`
     // when total length of all results is less than `maxLength`
-    for (let unicode in this.emojiStrategy) {
-      const data = this.emojiStrategy[unicode];
+    for (let unicode in emojiStrategy) {
+      const data = emojiStrategy[unicode];
 
 
       if (maxLength <= countLen1()) { break; }
       if (maxLength <= countLen1()) { break; }
       // prefix match to shortname
       // prefix match to shortname
@@ -144,4 +134,5 @@ class EmojiAutoCompleteHelper {
 
 
 // singleton pattern
 // singleton pattern
 const instance = new EmojiAutoCompleteHelper();
 const instance = new EmojiAutoCompleteHelper();
+Object.freeze(this);
 export default instance;
 export default instance;

+ 7 - 0
resource/js/components/PageEditor/Preview.js

@@ -20,7 +20,13 @@ export default class Preview extends React.Component {
     return (
     return (
       <div className="page-editor-preview-body"
       <div className="page-editor-preview-body"
           ref={(elm) => {
           ref={(elm) => {
+            this.previewElement = elm;
             this.props.inputRef(elm);
             this.props.inputRef(elm);
+          }}
+          onScroll={(event) => {
+            if (this.props.onScroll != null) {
+              this.props.onScroll(event.target.scrollTop);
+            }
           }}>
           }}>
 
 
         <RevisionBody
         <RevisionBody
@@ -38,4 +44,5 @@ Preview.propTypes = {
   isMathJaxEnabled: PropTypes.bool,
   isMathJaxEnabled: PropTypes.bool,
   renderMathJaxOnInit: PropTypes.bool,
   renderMathJaxOnInit: PropTypes.bool,
   previewOptions: PropTypes.instanceOf(PreviewOptions),
   previewOptions: PropTypes.instanceOf(PreviewOptions),
+  onScroll: PropTypes.func,
 };
 };

+ 195 - 0
resource/js/components/PageEditor/ScrollSyncHelper.js

@@ -0,0 +1,195 @@
+/**
+ * This class is copied from Microsoft/vscode repository
+ * @see https://github.com/Microsoft/vscode/blob/0532a3429a18688a0c086a4212e7e5b4888b2a48/extensions/markdown/media/main.js
+ */
+class ScrollSyncHelper {
+
+  /**
+	 * @typedef {{ element: Element, line: number }} CodeLineElement
+	 */
+
+  constructor() {
+  }
+
+  getCodeLineElements(parentElement) {
+    /** @type {CodeLineElement[]} */
+    let elements;
+    if (!elements) {
+      elements = Array.prototype.map.call(
+        parentElement.getElementsByClassName('code-line'),
+        element => {
+          const line = +element.getAttribute('data-line');
+          return { element, line }
+        })
+        .filter(x => !isNaN(x.line));
+    }
+    return elements;
+  }
+
+  /**
+	 * Find the html elements that map to a specific target line in the editor.
+	 *
+	 * If an exact match, returns a single element. If the line is between elements,
+	 * returns the element prior to and the element after the given line.
+	 *
+   * @param {Element} element
+	 * @param {number} targetLine
+	 *
+	 * @returns {{ previous: CodeLineElement, next?: CodeLineElement }}
+	 */
+	getElementsForSourceLine(element, targetLine) {
+		const lines = this.getCodeLineElements(element);
+		let previous = lines[0] || null;
+		for (const entry of lines) {
+			if (entry.line === targetLine) {
+				return { previous: entry, next: null };
+			} else if (entry.line > targetLine) {
+				return { previous, next: entry };
+			}
+			previous = entry;
+		}
+		return { previous };
+  }
+
+  /**
+	 * Find the html elements that are at a specific pixel offset on the page.
+	 *
+   * @param {Element} parentElement
+	 * @param {number} offset
+   *
+	 * @returns {{ previous: CodeLineElement, next?: CodeLineElement }}
+	 */
+	getLineElementsAtPageOffset(parentElement, offset) {
+		const lines = this.getCodeLineElements(parentElement);
+
+    const position = offset - parentElement.scrollTop + this.getParentElementOffset(parentElement);
+
+		let lo = -1;
+		let hi = lines.length - 1;
+		while (lo + 1 < hi) {
+			const mid = Math.floor((lo + hi) / 2);
+      const bounds = lines[mid].element.getBoundingClientRect();
+			if (bounds.top + bounds.height >= position) {
+				hi = mid;
+			} else {
+				lo = mid;
+			}
+    }
+
+		const hiElement = lines[hi];
+		if (hi >= 1 && hiElement.element.getBoundingClientRect().top > position) {
+			const loElement = lines[lo];
+			const bounds = loElement.element.getBoundingClientRect();
+      let previous = { element: loElement.element, line: loElement.line };
+      if (bounds.height > 0) {
+        previous.line += (position - bounds.top) / (bounds.height);
+      }
+      const next = { element: hiElement.element, line: hiElement.line, fractional: 0 };
+			return { previous, next };
+		}
+
+		const bounds = hiElement.element.getBoundingClientRect();
+    const previous = { element: hiElement.element, line: hiElement.line + (position - bounds.top) / (bounds.height) };
+		return { previous };
+  }
+
+  getEditorLineNumberForPageOffset(parentElement, offset) {
+    const { previous, next } = this.getLineElementsAtPageOffset(parentElement, offset);
+		if (previous) {
+			if (next) {
+        const betweenProgress = (offset - parentElement.scrollTop - previous.element.getBoundingClientRect().top) / (next.element.getBoundingClientRect().top - previous.element.getBoundingClientRect().top);
+				return previous.line + betweenProgress * (next.line - previous.line);
+			} else {
+				return previous.line;
+			}
+		}
+		return null;
+  }
+
+  /**
+   * return the sum of the offset position of parent element and paddingTop
+   * @param {Element} parentElement
+   */
+  getParentElementOffset(parentElement) {
+    const offsetY = parentElement.getBoundingClientRect().top;
+    // get paddingTop
+    const style = window.getComputedStyle(parentElement, null);
+    const paddingTop = +(style.paddingTop.replace('px', ''));
+
+		return offsetY + paddingTop;
+  }
+
+  /**
+	 * Attempt to scroll preview element for a source line in the editor.
+	 *
+   * @param {Element} previewElement
+	 * @param {number} line
+	 */
+	scrollPreview(previewElement, line) {
+		const { previous, next } = this.getElementsForSourceLine(previewElement, line);
+		if (previous) {
+			let scrollTo = 0;
+			if (next) {
+				// Between two elements. Go to percentage offset between them.
+				const betweenProgress = (line - previous.line) / (next.line - previous.line);
+				const elementOffset = next.element.getBoundingClientRect().top - previous.element.getBoundingClientRect().top;
+				scrollTo = previous.element.getBoundingClientRect().top + betweenProgress * elementOffset;
+			} else {
+				scrollTo = previous.element.getBoundingClientRect().top;
+      }
+
+      scrollTo -= this.getParentElementOffset(previewElement);
+
+      previewElement.scroll(0, previewElement.scrollTop + scrollTo);
+		}
+  }
+
+  /**
+	 * Attempt to reveal the element that is overflowing from previewElement.
+	 *
+   * @param {Element} previewElement
+	 * @param {number} line
+	 */
+  scrollPreviewToRevealOverflowing(previewElement, line) {
+		const { previous, next } = this.getElementsForSourceLine(previewElement, line);
+		if (previous) {
+      const parentElementOffset = this.getParentElementOffset(previewElement);
+      const prevElmTop = previous.element.getBoundingClientRect().top - parentElementOffset;
+      const prevElmBottom = previous.element.getBoundingClientRect().bottom - parentElementOffset;
+
+      let scrollTo = null;
+      if (prevElmTop < 0) {
+        // set the top of 'previous.element' to the top of 'previewElement'
+        scrollTo = previewElement.scrollTop + prevElmTop;
+      }
+      else if (prevElmBottom > previewElement.clientHeight) {
+        // set the bottom of 'previous.element' to the bottom of 'previewElement'
+        scrollTo = previewElement.scrollTop + prevElmBottom - previewElement.clientHeight + 20;
+      }
+
+      if (scrollTo == null) {
+        return;
+      }
+
+      previewElement.scroll(0, scrollTo);
+		}
+  }
+
+  /**
+   * Attempt to scroll Editor component for the offset of the element in the Preview component.
+   *
+   * @param {Editor} editor
+   * @param {Element} previewElement
+   * @param {number} offset
+   */
+  scrollEditor(editor, previewElement, offset) {
+    let line = this.getEditorLineNumberForPageOffset(previewElement, offset);
+    line = Math.floor(line);
+    editor.setScrollTopByLine(line);
+  }
+}
+
+// singleton pattern
+const instance = new ScrollSyncHelper();
+Object.freeze(instance);
+export default instance;

+ 1 - 1
resource/js/components/SearchTypeahead.js

@@ -1,6 +1,6 @@
-import {noop} from 'lodash';
 import React from 'react';
 import React from 'react';
 
 
+import { noop } from 'lodash/noop';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 
 
 import UserPicture from './User/UserPicture';
 import UserPicture from './User/UserPicture';

+ 6 - 10
resource/js/util/Crowi.js

@@ -5,12 +5,10 @@
 import axios from 'axios'
 import axios from 'axios'
 import InterceptorManager from '../../../lib/util/interceptor-manager';
 import InterceptorManager from '../../../lib/util/interceptor-manager';
 
 
-//// disable Detach/Restore interceptors
-//// because markdown-it handles emoji and Linker in code blocks well -- 2018.02.01 Yuki Takei
-// import {
-//   DetachCodeBlockInterceptor,
-//   RestoreCodeBlockInterceptor,
-// } from './interceptor/detach-code-blocks';
+import {
+  DetachCodeBlockInterceptor,
+  RestoreCodeBlockInterceptor,
+} from './interceptor/detach-code-blocks';
 
 
 export default class Crowi {
 export default class Crowi {
   constructor(context, window) {
   constructor(context, window) {
@@ -31,10 +29,8 @@ export default class Crowi {
 
 
     this.interceptorManager = new InterceptorManager();
     this.interceptorManager = new InterceptorManager();
     this.interceptorManager.addInterceptors([
     this.interceptorManager.addInterceptors([
-      //// disable Detach/Restore interceptors
-      //// because markdown-it handles emoji and Linker in code blocks well -- 2018.02.01 Yuki Takei
-      // new DetachCodeBlockInterceptor(this),
-      // new RestoreCodeBlockInterceptor(this),
+      new DetachCodeBlockInterceptor(this),
+      new RestoreCodeBlockInterceptor(this),
     ]);
     ]);
 
 
     // FIXME
     // FIXME

+ 4 - 1
resource/js/util/GrowiRenderer.js

@@ -152,12 +152,15 @@ export default class GrowiRenderer {
         return this.langProcessors[lang].process(code, langExt);
         return this.langProcessors[lang].process(code, langExt);
       }
       }
 
 
+      const citeTag = (langFn) ? `<cite>${langFn}</cite>` : '';
       if (hljs.getLanguage(lang)) {
       if (hljs.getLanguage(lang)) {
-        let citeTag = (langFn) ? `<cite>${langFn}</cite>` : '';
         try {
         try {
           return `<pre class="hljs">${citeTag}<code class="language-${lang}">${hljs.highlight(lang, code, true).value}</code></pre>`;
           return `<pre class="hljs">${citeTag}<code class="language-${lang}">${hljs.highlight(lang, code, true).value}</code></pre>`;
         } catch (__) {}
         } catch (__) {}
       }
       }
+      else {
+        return `<pre class="hljs">${citeTag}<code>${code}</code></pre>`;
+      }
     }
     }
 
 
     return '';
     return '';

+ 2 - 2
resource/js/util/PreProcessor/CsvToTable.js

@@ -3,8 +3,8 @@ import csvToMarkdownTable from 'csv-to-markdown-table';
 export default class CsvToTable {
 export default class CsvToTable {
   process(markdown) {
   process(markdown) {
 
 
-    // see: https://regex101.com/r/WR6IvX/2
-    return markdown.replace(/```(\S+)[\r\n]((.|[\r\n])*?)[\r\n]```/gm, (all, group1, group2) => {
+    // see: https://regex101.com/r/WR6IvX/3
+    return markdown.replace(/:::\s*(\S+)[\r\n]((.|[\r\n])*?)[\r\n]:::/gm, (all, group1, group2) => {
       switch (group1) {
       switch (group1) {
         case 'tsv':
         case 'tsv':
           return csvToMarkdownTable(group2, '\t');
           return csvToMarkdownTable(group2, '\t');

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
resource/js/util/emojione/emoji_strategy_shrinked.json


+ 5 - 6
resource/js/util/interceptor/detach-code-blocks.js

@@ -1,6 +1,3 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-
 import { BasicInterceptor } from 'crowi-pluginkit';
 import { BasicInterceptor } from 'crowi-pluginkit';
 
 
 
 
@@ -40,8 +37,8 @@ export class DetachCodeBlockInterceptor extends BasicInterceptor {
 
 
     context.dcbContextMap = {};
     context.dcbContextMap = {};
 
 
-    // see: https://regex101.com/r/8PAEcC/2
-    context.markdown = markdown.replace(/(```(.|[\r\n])*?```)|(`[^\r\n]*?`)/gm, (all) => {
+    // see: https://regex101.com/r/8PAEcC/3
+    context.markdown = markdown.replace(/((```|~~~)(.|[\r\n])*?(```|~~~))|(`[^\r\n]*?`)/gm, (all) => {
       // create ID
       // create ID
       const replaceId = 'dcb-' + this.createRandomStr(8);
       const replaceId = 'dcb-' + this.createRandomStr(8);
 
 
@@ -107,7 +104,9 @@ export class RestoreCodeBlockInterceptor extends BasicInterceptor {
       // get context object from context
       // get context object from context
       let dcbContext = context.dcbContextMap[replaceId];
       let dcbContext = context.dcbContextMap[replaceId];
 
 
-      context.markdown = context.markdown.replace(dcbContext.substituteContent, dcbContext.content);
+      // replace it with content by using getter function so that the doller sign does not work
+      // see: https://github.com/weseek/crowi-plus/issues/285
+      context.markdown = context.markdown.replace(dcbContext.substituteContent, () => { return dcbContext.content; });
     });
     });
 
 
     // resolve
     // resolve

+ 11 - 1
resource/js/util/markdown-it/emoji.js

@@ -1,3 +1,5 @@
+import emojiStrategy from '../emojione/emoji_strategy_shrinked.json';
+
 export default class EmojiConfigurer {
 export default class EmojiConfigurer {
 
 
   constructor(crowi) {
   constructor(crowi) {
@@ -5,7 +7,15 @@ export default class EmojiConfigurer {
   }
   }
 
 
   configure(md) {
   configure(md) {
-    md.use(require('markdown-it-emoji'));
+    const emojiShortnameUnicodeMap = {};
+
+    for (let unicode in emojiStrategy) {
+      const data = emojiStrategy[unicode];
+      const shortname = data.shortname.replace(/\:/g, '');
+      emojiShortnameUnicodeMap[shortname] = String.fromCharCode(unicode);
+    }
+
+    md.use(require('markdown-it-emoji'), {defs: emojiShortnameUnicodeMap});
 
 
     // integrate markdown-it-emoji and emojione
     // integrate markdown-it-emoji and emojione
     md.renderer.rules.emoji = (token, idx) => {
     md.renderer.rules.emoji = (token, idx) => {

+ 21 - 31
resource/js/util/markdown-it/header-line-number.js

@@ -2,43 +2,33 @@ export default class HeaderLineNumberConfigurer {
 
 
   constructor(crowi) {
   constructor(crowi) {
     this.crowi = crowi;
     this.crowi = crowi;
-
-    this.injectLineNumbers = this.injectLineNumbers.bind(this);
-    this.combineRules = this.combineRules.bind(this);
+    this.firstLine = 0;
   }
   }
 
 
   configure(md) {
   configure(md) {
-    const rules = md.renderer.rules;
-    const headingOpenOrg = rules.heading_open;
-    const paragraphOpenOrg = rules.paragraph_open;
-    // combine rule and set
-    rules.heading_open = this.combineRules(this.injectLineNumbers, headingOpenOrg);
-    rules.paragraph_open = this.combineRules(this.injectLineNumbers, paragraphOpenOrg);
+    for (const renderName of ['paragraph_open', 'heading_open', 'image', 'code_block', 'blockquote_open', 'list_item_open']) {
+      this.addLineNumberRenderer(md, renderName);
+    }
   }
   }
 
 
   /**
   /**
-   * Inject line numbers for sync scroll
-   * @see https://github.com/markdown-it/markdown-it/blob/e6f19eab4204122e85e4a342e0c1c8486ff40c2d/support/demo_template/index.js#L169
+   * Add line numbers for sync scroll
+   * @see https://github.com/Microsoft/vscode/blob/6e8d4d057bd1152d49a1e9780ec6db6363593855/extensions/markdown/src/markdownEngine.ts#L118
    */
    */
-  injectLineNumbers(tokens, idx, options, env, slf) {
-    var line;
-    if (tokens[idx].map && tokens[idx].level === 0) {
-      line = tokens[idx].map[0] + 1;    // add 1 to convert to line number
-      tokens[idx].attrJoin('class', 'line');
-      tokens[idx].attrSet('data-line', String(line));
-    }
-    return slf.renderToken(tokens, idx, options, env, slf);
-  }
+  addLineNumberRenderer(md, ruleName) {
+		const original = md.renderer.rules[ruleName];
+		md.renderer.rules[ruleName] = (tokens, idx, options, env, self) => {
+			const token = tokens[idx];
+			if (token.map && token.map.length) {
+				token.attrSet('data-line', this.firstLine + token.map[0]);
+				token.attrJoin('class', 'code-line');
+			}
 
 
-  combineRules(rule1, rule2) {
-    return (tokens, idx, options, env, slf) => {
-      if (rule1 != null) {
-        rule1(tokens, idx, options, env, slf);
-      }
-      if (rule2 != null) {
-        rule2(tokens, idx, options, env, slf);
-      }
-      return slf.renderToken(tokens, idx, options, env, slf);
-    }
-  }
+			if (original) {
+				return original(tokens, idx, options, env, self);
+			} else {
+				return self.renderToken(tokens, idx, options, env, self);
+			}
+		};
+}
 }
 }

+ 14 - 16
resource/js/util/markdown-it/header.js

@@ -4,14 +4,25 @@ export default class HeaderConfigurer {
     this.crowi = crowi;
     this.crowi = crowi;
 
 
     this.injectRevisionHeadClass = this.injectRevisionHeadClass.bind(this);
     this.injectRevisionHeadClass = this.injectRevisionHeadClass.bind(this);
-    this.combineRules = this.combineRules.bind(this);
   }
   }
 
 
   configure(md) {
   configure(md) {
     const rules = md.renderer.rules;
     const rules = md.renderer.rules;
-    const headingOpenOrg = rules.heading_open;
+    const original = rules.heading_open;
     // combine rule and set
     // combine rule and set
-    rules.heading_open = this.combineRules(this.injectRevisionHeadClass, headingOpenOrg);
+    // rules.heading_open = this.combineRules(this.injectRevisionHeadClass, headingOpenOrg);
+    rules.heading_open = (tokens, idx, options, env, self) => {
+      const token = tokens[idx];
+
+      // Inject 'revision-head' class
+			this.injectRevisionHeadClass(tokens, idx, options, env, self);
+
+			if (original) {
+				return original(tokens, idx, options, env, self);
+			} else {
+				return self.renderToken(tokens, idx, options, env, self);
+			}
+		};
   }
   }
 
 
   /**
   /**
@@ -21,18 +32,5 @@ export default class HeaderConfigurer {
     if (tokens[idx].map && tokens[idx].level === 0) {
     if (tokens[idx].map && tokens[idx].level === 0) {
       tokens[idx].attrJoin('class', 'revision-head');
       tokens[idx].attrJoin('class', 'revision-head');
     }
     }
-    return slf.renderToken(tokens, idx, options, env, slf);
-  }
-
-  combineRules(rule1, rule2) {
-    return (tokens, idx, options, env, slf) => {
-      if (rule1 != null) {
-        rule1(tokens, idx, options, env, slf);
-      }
-      if (rule2 != null) {
-        rule2(tokens, idx, options, env, slf);
-      }
-      return slf.renderToken(tokens, idx, options, env, slf);
-    }
   }
   }
 }
 }

+ 5 - 6
resource/js/util/markdown-it/plantuml.js

@@ -1,3 +1,4 @@
+import plantumlEncoder from 'plantuml-encoder';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
 export default class PlantUMLConfigurer {
 export default class PlantUMLConfigurer {
@@ -6,21 +7,19 @@ export default class PlantUMLConfigurer {
     this.crowi = crowi;
     this.crowi = crowi;
     const config = crowi.getConfig();
     const config = crowi.getConfig();
 
 
-    this.deflate = require('markdown-it-plantuml/lib/deflate.js');
-    this.serverUrl = config.env.PLANTUML_URI || 'http://plantuml.com';
+    this.serverUrl = config.env.PLANTUML_URI || 'http://plantuml.com/plantuml';
 
 
     this.generateSource = this.generateSource.bind(this);
     this.generateSource = this.generateSource.bind(this);
   }
   }
 
 
   configure(md) {
   configure(md) {
-    md.use(require('markdown-it-plantuml'), 'name', {
+    md.use(require('markdown-it-plantuml'), {
       generateSource: this.generateSource,
       generateSource: this.generateSource,
     });
     });
   }
   }
 
 
   generateSource(umlCode) {
   generateSource(umlCode) {
-    const zippedCode =
-      this.deflate.encode64(this.deflate.zip_deflate('@startuml\n' + umlCode + '\n@enduml', 9));
-    return urljoin(this.serverUrl, 'plantuml', 'svg' , zippedCode);
+    const zippedCode = plantumlEncoder.encode(`@startuml\n${umlCode}\n@enduml`);
+    return urljoin(this.serverUrl, 'svg' , zippedCode);
   }
   }
 }
 }

+ 1 - 0
resource/js/util/markdown-it/toc-and-anchor.js

@@ -9,6 +9,7 @@ export default class TocAndAnchorConfigurer {
 
 
   configure(md) {
   configure(md) {
     md.use(require('markdown-it-toc-and-anchor-with-slugid').default, {
     md.use(require('markdown-it-toc-and-anchor-with-slugid').default, {
+        tocLastLevel: 3,
         anchorLinkBefore: false,
         anchorLinkBefore: false,
         anchorLinkSymbol: '',
         anchorLinkSymbol: '',
         anchorLinkSymbolClassName: 'fa fa-link',
         anchorLinkSymbolClassName: 'fa fa-link',

+ 110 - 16
yarn.lock

@@ -376,6 +376,13 @@ axios@^0.17.0, axios@^0.17.1:
     follow-redirects "^1.2.5"
     follow-redirects "^1.2.5"
     is-buffer "^1.1.5"
     is-buffer "^1.1.5"
 
 
+axios@^0.18.0:
+  version "0.18.0"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102"
+  dependencies:
+    follow-redirects "^1.3.0"
+    is-buffer "^1.1.5"
+
 babel-code-frame@^6.26.0:
 babel-code-frame@^6.26.0:
   version "6.26.0"
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
   resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
@@ -487,6 +494,13 @@ babel-helper-hoist-variables@^6.24.1:
     babel-runtime "^6.22.0"
     babel-runtime "^6.22.0"
     babel-types "^6.24.1"
     babel-types "^6.24.1"
 
 
+babel-helper-module-imports@^7.0.0-beta.3:
+  version "7.0.0-beta.3"
+  resolved "https://registry.yarnpkg.com/babel-helper-module-imports/-/babel-helper-module-imports-7.0.0-beta.3.tgz#e15764e3af9c8e11810c09f78f498a2bdc71585a"
+  dependencies:
+    babel-types "7.0.0-beta.3"
+    lodash "^4.2.0"
+
 babel-helper-optimise-call-expression@^6.24.1:
 babel-helper-optimise-call-expression@^6.24.1:
   version "6.24.1"
   version "6.24.1"
   resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
   resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
@@ -550,6 +564,16 @@ babel-plugin-check-es2015-constants@^6.22.0:
   dependencies:
   dependencies:
     babel-runtime "^6.22.0"
     babel-runtime "^6.22.0"
 
 
+babel-plugin-lodash@^3.3.2:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/babel-plugin-lodash/-/babel-plugin-lodash-3.3.2.tgz#da3a5b49ba27447f54463f6c4fa81396ccdd463f"
+  dependencies:
+    babel-helper-module-imports "^7.0.0-beta.3"
+    babel-types "^6.26.0"
+    glob "^7.1.1"
+    lodash "^4.17.4"
+    require-package-name "^2.0.1"
+
 babel-plugin-syntax-async-functions@^6.8.0:
 babel-plugin-syntax-async-functions@^6.8.0:
   version "6.13.0"
   version "6.13.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
   resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
@@ -897,6 +921,14 @@ babel-traverse@^6.24.1, babel-traverse@^6.26.0:
     invariant "^2.2.2"
     invariant "^2.2.2"
     lodash "^4.17.4"
     lodash "^4.17.4"
 
 
+babel-types@7.0.0-beta.3:
+  version "7.0.0-beta.3"
+  resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-7.0.0-beta.3.tgz#cd927ca70e0ae8ab05f4aab83778cfb3e6eb20b4"
+  dependencies:
+    esutils "^2.0.2"
+    lodash "^4.2.0"
+    to-fast-properties "^2.0.0"
+
 babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0:
 babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0:
   version "6.26.0"
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
   resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
@@ -1887,9 +1919,9 @@ dicer@0.2.5:
     readable-stream "1.1.x"
     readable-stream "1.1.x"
     streamsearch "0.1.2"
     streamsearch "0.1.2"
 
 
-diff2html@^2.3.0:
-  version "2.3.2"
-  resolved "https://registry.yarnpkg.com/diff2html/-/diff2html-2.3.2.tgz#1c5864266d437148bc66fdd66d4ad750102d7fed"
+diff2html@^2.3.3:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/diff2html/-/diff2html-2.3.3.tgz#31bb815881c975634c7f3907a5e789341e1560bc"
   dependencies:
   dependencies:
     diff "^3.3.1"
     diff "^3.3.1"
     hogan.js "^3.0.2"
     hogan.js "^3.0.2"
@@ -2316,7 +2348,7 @@ express@^4.15.2, express@^4.16.1:
     utils-merge "1.0.1"
     utils-merge "1.0.1"
     vary "~1.1.2"
     vary "~1.1.2"
 
 
-extend@3, extend@~3.0.0, extend@~3.0.1:
+extend@3, extend@^3.0.1, extend@~3.0.0, extend@~3.0.1:
   version "3.0.1"
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
 
 
@@ -2456,6 +2488,12 @@ follow-redirects@^1.2.5:
   dependencies:
   dependencies:
     debug "^3.1.0"
     debug "^3.1.0"
 
 
+follow-redirects@^1.3.0:
+  version "1.4.1"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa"
+  dependencies:
+    debug "^3.1.0"
+
 for-in@^0.1.3:
 for-in@^0.1.3:
   version "0.1.8"
   version "0.1.8"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
@@ -2565,6 +2603,14 @@ gaze@^1.0.0:
   dependencies:
   dependencies:
     globule "^1.0.0"
     globule "^1.0.0"
 
 
+gcp-metadata@^0.6.0:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-0.6.1.tgz#62d54871fc6aeeac6a688e094abc886cb7aaacd0"
+  dependencies:
+    axios "^0.17.1"
+    extend "^3.0.1"
+    retry-axios "0.3.0"
+
 generate-function@^2.0.0:
 generate-function@^2.0.0:
   version "2.0.0"
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74"
   resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74"
@@ -2649,15 +2695,17 @@ good-listener@^1.2.2:
   dependencies:
   dependencies:
     delegate "^3.1.2"
     delegate "^3.1.2"
 
 
-google-auth-library@^1.1.0:
-  version "1.2.1"
-  resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-1.2.1.tgz#20eb9d585b1837a703712abdb787da4984982b64"
+google-auth-library@^1.3.1:
+  version "1.3.1"
+  resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-1.3.1.tgz#a0456166c9538ee062c51b8e70765a5a0e13e757"
   dependencies:
   dependencies:
-    axios "^0.17.1"
+    axios "^0.18.0"
+    gcp-metadata "^0.6.0"
     gtoken "^2.1.0"
     gtoken "^2.1.0"
     jws "^3.1.4"
     jws "^3.1.4"
     lodash.isstring "^4.0.1"
     lodash.isstring "^4.0.1"
     lru-cache "^4.1.1"
     lru-cache "^4.1.1"
+    retry-axios "^0.3.2"
 
 
 google-p12-pem@^1.0.0:
 google-p12-pem@^1.0.0:
   version "1.0.0"
   version "1.0.0"
@@ -2666,14 +2714,15 @@ google-p12-pem@^1.0.0:
     node-forge "^0.7.1"
     node-forge "^0.7.1"
     pify "^3.0.0"
     pify "^3.0.0"
 
 
-googleapis@^26.0.0:
-  version "26.0.1"
-  resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-26.0.1.tgz#e1efb43b00546b1ad8c055a83cf210d5422b7f42"
+googleapis@^27.0.0:
+  version "27.0.0"
+  resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-27.0.0.tgz#c210633b43e7047b65d33da40c489b6d8f9c02b8"
   dependencies:
   dependencies:
-    google-auth-library "^1.1.0"
+    google-auth-library "^1.3.1"
+    pify "^3.0.0"
     qs "^6.5.1"
     qs "^6.5.1"
     string-template "1.0.0"
     string-template "1.0.0"
-    uuid "^3.1.0"
+    uuid "^3.2.1"
 
 
 graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
 graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
   version "4.1.11"
   version "4.1.11"
@@ -3419,6 +3468,12 @@ locate-path@^2.0.0:
     p-locate "^2.0.0"
     p-locate "^2.0.0"
     path-exists "^3.0.0"
     path-exists "^3.0.0"
 
 
+lodash-webpack-plugin@^0.11.4:
+  version "0.11.4"
+  resolved "https://registry.yarnpkg.com/lodash-webpack-plugin/-/lodash-webpack-plugin-0.11.4.tgz#6c3ecba3d4b8d24b53940b63542715c5ed3c4ac5"
+  dependencies:
+    lodash "^4.17.4"
+
 lodash._arraycopy@^3.0.0:
 lodash._arraycopy@^3.0.0:
   version "3.0.0"
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1"
   resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1"
@@ -3610,6 +3665,10 @@ lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.4, l
   version "4.17.4"
   version "4.17.4"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
 
 
+lodash@^4.2.0:
+  version "4.17.5"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
+
 lolex@^1.6.0:
 lolex@^1.6.0:
   version "1.6.0"
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6"
   resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6"
@@ -3684,9 +3743,9 @@ markdown-it-named-headers@^0.0.4:
   dependencies:
   dependencies:
     string "^3.0.1"
     string "^3.0.1"
 
 
-markdown-it-plantuml@^0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/markdown-it-plantuml/-/markdown-it-plantuml-0.3.1.tgz#f338df4d691a5561364e65809b6812bcb3d8b047"
+markdown-it-plantuml@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/markdown-it-plantuml/-/markdown-it-plantuml-1.0.0.tgz#7b6a351a1d9275705c09626b02d873301e5899c2"
 
 
 markdown-it-task-lists@^2.1.0:
 markdown-it-task-lists@^2.1.0:
   version "2.1.0"
   version "2.1.0"
@@ -4396,6 +4455,10 @@ p-try@^1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
   resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
 
 
+pako@1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.3.tgz#5f515b0c6722e1982920ae8005eacb0b7ca73ccf"
+
 pako@~1.0.5:
 pako@~1.0.5:
   version "1.0.6"
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
   resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
@@ -4610,6 +4673,13 @@ pkginfo@^0.4.0:
   version "0.4.1"
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff"
   resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff"
 
 
+plantuml-encoder@^1.2.5:
+  version "1.2.5"
+  resolved "https://registry.yarnpkg.com/plantuml-encoder/-/plantuml-encoder-1.2.5.tgz#6b8e5b9e1a1dbd88b3fd9fb46f734eec7d44b548"
+  dependencies:
+    pako "1.0.3"
+    utf8-bytes "0.0.1"
+
 postcss-calc@^5.2.0:
 postcss-calc@^5.2.0:
   version "5.3.1"
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e"
   resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-5.3.1.tgz#77bae7ca928ad85716e2fda42f261bf7c1d65b5e"
@@ -5421,6 +5491,10 @@ require-main-filename@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
   resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
 
 
+require-package-name@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/require-package-name/-/require-package-name-2.0.1.tgz#c11e97276b65b8e2923f75dabf5fb2ef0c3841b9"
+
 require_optional@^1.0.1:
 require_optional@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e"
   resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e"
@@ -5438,6 +5512,14 @@ resolve@^1.0.0:
   dependencies:
   dependencies:
     path-parse "^1.0.5"
     path-parse "^1.0.5"
 
 
+retry-axios@0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-0.3.0.tgz#7858ad369872d6acaf05fd97b0490969c9c35ee2"
+
+retry-axios@^0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-0.3.2.tgz#5757c80f585b4cc4c4986aa2ffd47a60c6d35e13"
+
 retry@^0.9.0:
 retry@^0.9.0:
   version "0.9.0"
   version "0.9.0"
   resolved "https://registry.yarnpkg.com/retry/-/retry-0.9.0.tgz#6f697e50a0e4ddc8c8f7fb547a9b60dead43678d"
   resolved "https://registry.yarnpkg.com/retry/-/retry-0.9.0.tgz#6f697e50a0e4ddc8c8f7fb547a9b60dead43678d"
@@ -6024,6 +6106,10 @@ to-fast-properties@^1.0.3:
   version "1.0.3"
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
   resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
 
 
+to-fast-properties@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+
 toastr@^2.1.2:
 toastr@^2.1.2:
   version "2.1.4"
   version "2.1.4"
   resolved "https://registry.yarnpkg.com/toastr/-/toastr-2.1.4.tgz#8b43be64fb9d0c414871446f2db8e8ca4e95f181"
   resolved "https://registry.yarnpkg.com/toastr/-/toastr-2.1.4.tgz#8b43be64fb9d0c414871446f2db8e8ca4e95f181"
@@ -6209,6 +6295,10 @@ uslug@^1.0.4:
   dependencies:
   dependencies:
     unorm ">= 1.0.0"
     unorm ">= 1.0.0"
 
 
+utf8-bytes@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/utf8-bytes/-/utf8-bytes-0.0.1.tgz#116b025448c9b500081cdfbf1f4d6c6c37d8837d"
+
 util-deprecate@~1.0.1:
 util-deprecate@~1.0.1:
   version "1.0.2"
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -6227,6 +6317,10 @@ uuid@3.1.0, uuid@^3.0.0, uuid@^3.1.0:
   version "3.1.0"
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
 
 
+uuid@^3.2.1:
+  version "3.2.1"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
+
 uws@~0.14.4:
 uws@~0.14.4:
   version "0.14.5"
   version "0.14.5"
   resolved "https://registry.yarnpkg.com/uws/-/uws-0.14.5.tgz#67aaf33c46b2a587a5f6666d00f7691328f149dc"
   resolved "https://registry.yarnpkg.com/uws/-/uws-0.14.5.tgz#67aaf33c46b2a587a5f6666d00f7691328f149dc"

Некоторые файлы не были показаны из-за большого количества измененных файлов