Pārlūkot izejas kodu

revert: to 08cd2eca, retry new-feat merge

Anankke 3 gadi atpakaļ
vecāks
revīzija
9a16dd52c1
100 mainītis faili ar 23259 papildinājumiem un 709 dzēšanām
  1. 117 66
      app/routes.php
  2. 0 70
      block-whoops-env.sh
  3. 2 3
      composer.json
  4. 381 208
      config/.config.example.php
  5. 500 0
      config/settings.json
  6. 0 5
      databases/fix-database.sql
  7. 0 34
      databases/migrations/20220411015707_product_table.php
  8. 0 25
      databases/migrations/20220411172052_drop_coupon_table.php
  9. 0 35
      databases/migrations/20220411172159_create_new_coupon_table.php
  10. 0 26
      databases/migrations/20220412045711_add_product_stock_field.php
  11. 0 39
      databases/migrations/20220412082403_product_order_table.php
  12. 0 26
      databases/migrations/20220413153512_add_two_fields_to_the_product_order_table.php
  13. 0 30
      databases/migrations/20220414005559_gift_card_table.php
  14. 0 33
      databases/migrations/20220416033504_work_order_table.php
  15. 0 53
      databases/migrations/20220416172613_delete_old_table.php
  16. 0 29
      databases/migrations/20220416175028_delete_user_table_field.php
  17. 0 25
      databases/migrations/20220417100701_drop_g_config_table.php
  18. 0 0
      db/migrations/20220109130532_create_stream_media_table.php
  19. 0 0
      db/seeds/readme.md
  20. 2 2
      phinx.php
  21. 1 0
      public/index.php
  22. 15 0
      public/theme/material/editor/.gitignore
  23. 22 0
      public/theme/material/editor/LICENSE
  24. 3169 0
      public/theme/material/editor/css/editormd.css
  25. 98 0
      public/theme/material/editor/css/editormd.logo.css
  26. 2 0
      public/theme/material/editor/css/editormd.logo.min.css
  27. 1 0
      public/theme/material/editor/css/editormd.min.css
  28. 2453 0
      public/theme/material/editor/css/editormd.preview.css
  29. 4 0
      public/theme/material/editor/css/editormd.preview.min.css
  30. 4667 0
      public/theme/material/editor/editormd.amd.js
  31. 1 0
      public/theme/material/editor/editormd.amd.min.js
  32. 4597 0
      public/theme/material/editor/editormd.js
  33. 0 0
      public/theme/material/editor/editormd.min.js
  34. BIN
      public/theme/material/editor/images/loading.gif
  35. BIN
      public/theme/material/editor/images/[email protected]
  36. BIN
      public/theme/material/editor/images/[email protected]
  37. 127 0
      public/theme/material/editor/languages/en.js
  38. 127 0
      public/theme/material/editor/languages/zh-tw.js
  39. 19 0
      public/theme/material/editor/lib/codemirror/LICENSE
  40. 183 0
      public/theme/material/editor/lib/codemirror/addon/comment/comment.js
  41. 85 0
      public/theme/material/editor/lib/codemirror/addon/comment/continuecomment.js
  42. 32 0
      public/theme/material/editor/lib/codemirror/addon/dialog/dialog.css
  43. 155 0
      public/theme/material/editor/lib/codemirror/addon/dialog/dialog.js
  44. 6 0
      public/theme/material/editor/lib/codemirror/addon/display/fullscreen.css
  45. 41 0
      public/theme/material/editor/lib/codemirror/addon/display/fullscreen.js
  46. 94 0
      public/theme/material/editor/lib/codemirror/addon/display/panel.js
  47. 58 0
      public/theme/material/editor/lib/codemirror/addon/display/placeholder.js
  48. 64 0
      public/theme/material/editor/lib/codemirror/addon/display/rulers.js
  49. 161 0
      public/theme/material/editor/lib/codemirror/addon/edit/closebrackets.js
  50. 166 0
      public/theme/material/editor/lib/codemirror/addon/edit/closetag.js
  51. 51 0
      public/theme/material/editor/lib/codemirror/addon/edit/continuelist.js
  52. 120 0
      public/theme/material/editor/lib/codemirror/addon/edit/matchbrackets.js
  53. 66 0
      public/theme/material/editor/lib/codemirror/addon/edit/matchtags.js
  54. 27 0
      public/theme/material/editor/lib/codemirror/addon/edit/trailingspace.js
  55. 105 0
      public/theme/material/editor/lib/codemirror/addon/fold/brace-fold.js
  56. 57 0
      public/theme/material/editor/lib/codemirror/addon/fold/comment-fold.js
  57. 149 0
      public/theme/material/editor/lib/codemirror/addon/fold/foldcode.js
  58. 20 0
      public/theme/material/editor/lib/codemirror/addon/fold/foldgutter.css
  59. 144 0
      public/theme/material/editor/lib/codemirror/addon/fold/foldgutter.js
  60. 44 0
      public/theme/material/editor/lib/codemirror/addon/fold/indent-fold.js
  61. 49 0
      public/theme/material/editor/lib/codemirror/addon/fold/markdown-fold.js
  62. 182 0
      public/theme/material/editor/lib/codemirror/addon/fold/xml-fold.js
  63. 41 0
      public/theme/material/editor/lib/codemirror/addon/hint/anyword-hint.js
  64. 56 0
      public/theme/material/editor/lib/codemirror/addon/hint/css-hint.js
  65. 348 0
      public/theme/material/editor/lib/codemirror/addon/hint/html-hint.js
  66. 146 0
      public/theme/material/editor/lib/codemirror/addon/hint/javascript-hint.js
  67. 38 0
      public/theme/material/editor/lib/codemirror/addon/hint/show-hint.css
  68. 394 0
      public/theme/material/editor/lib/codemirror/addon/hint/show-hint.js
  69. 240 0
      public/theme/material/editor/lib/codemirror/addon/hint/sql-hint.js
  70. 110 0
      public/theme/material/editor/lib/codemirror/addon/hint/xml-hint.js
  71. 41 0
      public/theme/material/editor/lib/codemirror/addon/lint/coffeescript-lint.js
  72. 35 0
      public/theme/material/editor/lib/codemirror/addon/lint/css-lint.js
  73. 136 0
      public/theme/material/editor/lib/codemirror/addon/lint/javascript-lint.js
  74. 31 0
      public/theme/material/editor/lib/codemirror/addon/lint/json-lint.js
  75. 73 0
      public/theme/material/editor/lib/codemirror/addon/lint/lint.css
  76. 205 0
      public/theme/material/editor/lib/codemirror/addon/lint/lint.js
  77. 28 0
      public/theme/material/editor/lib/codemirror/addon/lint/yaml-lint.js
  78. 112 0
      public/theme/material/editor/lib/codemirror/addon/merge/merge.css
  79. 735 0
      public/theme/material/editor/lib/codemirror/addon/merge/merge.js
  80. 64 0
      public/theme/material/editor/lib/codemirror/addon/mode/loadmode.js
  81. 118 0
      public/theme/material/editor/lib/codemirror/addon/mode/multiplex.js
  82. 33 0
      public/theme/material/editor/lib/codemirror/addon/mode/multiplex_test.js
  83. 85 0
      public/theme/material/editor/lib/codemirror/addon/mode/overlay.js
  84. 213 0
      public/theme/material/editor/lib/codemirror/addon/mode/simple.js
  85. 40 0
      public/theme/material/editor/lib/codemirror/addon/runmode/colorize.js
  86. 157 0
      public/theme/material/editor/lib/codemirror/addon/runmode/runmode-standalone.js
  87. 72 0
      public/theme/material/editor/lib/codemirror/addon/runmode/runmode.js
  88. 120 0
      public/theme/material/editor/lib/codemirror/addon/runmode/runmode.node.js
  89. 100 0
      public/theme/material/editor/lib/codemirror/addon/scroll/annotatescrollbar.js
  90. 46 0
      public/theme/material/editor/lib/codemirror/addon/scroll/scrollpastend.js
  91. 66 0
      public/theme/material/editor/lib/codemirror/addon/scroll/simplescrollbars.css
  92. 141 0
      public/theme/material/editor/lib/codemirror/addon/scroll/simplescrollbars.js
  93. 128 0
      public/theme/material/editor/lib/codemirror/addon/search/match-highlighter.js
  94. 8 0
      public/theme/material/editor/lib/codemirror/addon/search/matchesonscrollbar.css
  95. 95 0
      public/theme/material/editor/lib/codemirror/addon/search/matchesonscrollbar.js
  96. 164 0
      public/theme/material/editor/lib/codemirror/addon/search/search.js
  97. 189 0
      public/theme/material/editor/lib/codemirror/addon/search/searchcursor.js
  98. 71 0
      public/theme/material/editor/lib/codemirror/addon/selection/active-line.js
  99. 118 0
      public/theme/material/editor/lib/codemirror/addon/selection/mark-selection.js
  100. 98 0
      public/theme/material/editor/lib/codemirror/addon/selection/selection-pointer.js

+ 117 - 66
app/routes.php

@@ -1,4 +1,5 @@
 <?php
+
 declare(strict_types=1);
 
 use Slim\App as SlimApp;
@@ -13,7 +14,7 @@ return function (SlimApp $app) {
     $app->get('/tos',       App\Controllers\HomeController::class . ':tos');
     $app->get('/staff',     App\Controllers\HomeController::class . ':staff');
 
-    // Other
+    // other
     $app->post('/notify',               App\Controllers\HomeController::class . ':notify');
 
     // Telegram
@@ -21,50 +22,40 @@ return function (SlimApp $app) {
 
     // User Center
     $app->group('/user', function () {
-        // 用户中心首页
         $this->get('',                          App\Controllers\UserController::class . ':index');
         $this->get('/',                         App\Controllers\UserController::class . ':index');
+
         $this->post('/checkin',                 App\Controllers\UserController::class . ':doCheckin');
 
-        // 单页面
+        $this->get('/announcement',             App\Controllers\UserController::class . ':announcement');
         $this->get('/media',                    App\Controllers\UserController::class . ':media');
+
+        $this->get('/donate',                   App\Controllers\UserController::class . ':donate');
         $this->get('/profile',                  App\Controllers\UserController::class . ':profile');
-        $this->post('/kill',                    App\Controllers\UserController::class . ':handleKill');
+        $this->get('/invite',                   App\Controllers\UserController::class . ':invite');
         $this->get('/disable',                  App\Controllers\UserController::class . ':disable');
-        $this->get('/announcement',             App\Controllers\UserController::class . ':announcement');
-        $this->get('/subscribe_log',            App\Controllers\UserController::class . ':subscribe_log');
-
-        // 文档中心
-        $this->get('/docs/{client}',            App\Controllers\DocsController::class . ':index');
 
-        // 邀请系统
-        $this->get('/invite',                   App\Controllers\UserController::class . ':invite');
-        $this->put('/invite',                   App\Controllers\UserController::class . ':resetInviteURL');
+        $this->get('/node',                     App\Controllers\User\NodeController::class . ':user_node_page');
+        $this->get('/node/{id}/ajax',           App\Controllers\User\NodeController::class . ':user_node_ajax');
+        $this->get('/node/{id}',                App\Controllers\User\NodeController::class . ':user_node_info');
 
-        // 审计系统
         $this->get('/detect',                   App\Controllers\UserController::class . ':detect_index');
         $this->get('/detect/log',               App\Controllers\UserController::class . ':detect_log');
 
-        // 工单系统
+        $this->get('/shop',                     App\Controllers\UserController::class . ':shop');
+        $this->post('/coupon_check',            App\Controllers\UserController::class . ':CouponCheck');
+        $this->post('/buy',                     App\Controllers\UserController::class . ':buy');
+        $this->post('/buy_traffic_package',     App\Controllers\UserController::class . ':buy_traffic_package');
+
         $this->get('/ticket',                   App\Controllers\User\TicketController::class . ':ticket');
         $this->get('/ticket/create',            App\Controllers\User\TicketController::class . ':ticket_create');
         $this->post('/ticket',                  App\Controllers\User\TicketController::class . ':ticket_add');
         $this->get('/ticket/{id}/view',         App\Controllers\User\TicketController::class . ':ticket_view');
         $this->put('/ticket/{id}',              App\Controllers\User\TicketController::class . ':ticket_update');
 
-        // 新商店系统
-        $this->get('/product',                  App\Controllers\UserController::class . ':productIndex');
-        $this->get('/order',                    App\Controllers\UserController::class . ':orderIndex');
-        $this->get('/order/{no}',               App\Controllers\UserController::class . ':orderDetails');
-        $this->get('/order/status/{no}',        App\Controllers\UserController::class . ':orderStatus');
-        $this->post('/order',                   App\Controllers\UserController::class . ':createOrder');
-        $this->put('/order',                    App\Controllers\UserController::class . ':processOrder');
-        $this->post('/redeem',                  App\Controllers\UserController::class . ':redeemGiftCard');
-        $this->post('/coupon_check',            App\Controllers\UserController::class . ':couponCheck');
-
-        // 编辑页面
+        $this->post('/buy_invite',              App\Controllers\UserController::class . ':buyInvite');
+        $this->post('/custom_invite',           App\Controllers\UserController::class . ':customInvite');
         $this->get('/edit',                     App\Controllers\UserController::class . ':edit');
-        $this->get('/telegram_reset',           App\Controllers\UserController::class . ':telegram_reset');
         $this->post('/email',                   App\Controllers\UserController::class . ':updateEmail');
         $this->post('/username',                App\Controllers\UserController::class . ':updateUsername');
         $this->post('/password',                App\Controllers\UserController::class . ':updatePassword');
@@ -74,28 +65,54 @@ return function (SlimApp $app) {
         $this->post('/theme',                   App\Controllers\UserController::class . ':updateTheme');
         $this->post('/mail',                    App\Controllers\UserController::class . ':updateMail');
         $this->post('/sspwd',                   App\Controllers\UserController::class . ':updateSsPwd');
-        $this->post('/url_reset',               App\Controllers\UserController::class . ':resetURL');
-        $this->post('/gacheck',                 App\Controllers\UserController::class . ':gaCheck');
-        $this->post('/gaset',                   App\Controllers\UserController::class . ':gaSet');
-        $this->post('/gareset',                 App\Controllers\UserController::class . ':gaReset');
-        $this->post('/port',                    App\Controllers\UserController::class . ':resetPort');
-
-        // 节点列表
-        $this->get('/node',                     App\Controllers\User\NodeController::class . ':user_node_page');
-        $this->get('/server',                   App\Controllers\User\NodeController::class . ':serverList');
-        $this->get('/node/{id}/ajax',           App\Controllers\User\NodeController::class . ':user_node_ajax');
-        $this->get('/node/{id}',                App\Controllers\User\NodeController::class . ':user_node_info');
-
-        // 其他
+        $this->post('/method',                  App\Controllers\UserController::class . ':updateMethod');
+        $this->post('/hide',                    App\Controllers\UserController::class . ':updateHide');
+        $this->get('/sys',                      App\Controllers\UserController::class . ':sys');
+        $this->get('/trafficlog',               App\Controllers\UserController::class . ':trafficLog');
+        $this->get('/kill',                     App\Controllers\UserController::class . ':kill');
+        $this->post('/kill',                    App\Controllers\UserController::class . ':handleKill');
         $this->get('/logout',                   App\Controllers\UserController::class . ':logout');
         $this->get('/backtoadmin',              App\Controllers\UserController::class . ':backtoadmin');
-        $this->get('/getPcClient',              App\Controllers\UserController::class . ':getPcClient');
+        $this->get('/code',                     App\Controllers\UserController::class . ':code');
+
+        $this->get('/code_check',               App\Controllers\UserController::class . ':code_check');
+        $this->post('/code',                    App\Controllers\UserController::class . ':codepost');
+        $this->post('/gacheck',                 App\Controllers\UserController::class . ':GaCheck');
+        $this->post('/gaset',                   App\Controllers\UserController::class . ':GaSet');
+        $this->get('/gareset',                  App\Controllers\UserController::class . ':GaReset');
+        $this->get('/telegram_reset',           App\Controllers\UserController::class . ':telegram_reset');
+        $this->post('/resetport',               App\Controllers\UserController::class . ':ResetPort');
+        $this->post('/specifyport',             App\Controllers\UserController::class . ':SpecifyPort');
+        $this->post('/unblock',                 App\Controllers\UserController::class . ':Unblock');
+        $this->get('/bought',                   App\Controllers\UserController::class . ':bought');
+        $this->delete('/bought',                App\Controllers\UserController::class . ':deleteBoughtGet');
+        $this->get('/url_reset',                App\Controllers\UserController::class . ':resetURL');
+        $this->put('/invite',                   App\Controllers\UserController::class . ':resetInviteURL');
+
+        $this->get('/order',                    App\Controllers\UserController::class . ':user_order');
+        $this->get('/product',                  App\Controllers\UserController::class . ':product_index');
+
+        // 订阅记录
+        $this->get('/subscribe_log',            App\Controllers\UserController::class . ':subscribe_log');
+
+        // getUserAllURL
         $this->get('/getUserAllURL',            App\Controllers\UserController::class . ':getUserAllURL');
+
+        // getPcClient
+        $this->get('/getPcClient',              App\Controllers\UserController::class . ':getPcClient');
+
+        //Reconstructed Payment System
+        $this->post('/payment/purchase/{type}',        App\Services\Payment::class . ':purchase');
+        $this->get('/payment/purchase/{type}',         App\Services\Payment::class . ':purchase');
+        $this->get('/payment/return/{type}',           App\Services\Payment::class . ':returnHTML');
+
     })->add(new Auth());
 
-    $app->group('/payments', function () {
-        $this->get('/notify/{type}',            App\Services\Payment::class . ':notify');
-        $this->post('/notify/{type}',           App\Services\Payment::class . ':notify');
+    $app->group('/payment', function () {
+        $this->get('/notify/{type}',           App\Services\Payment::class . ':notify');
+        $this->post('/notify/{type}',   App\Services\Payment::class . ':notify');
+        $this->post('/status/{type}',          App\Services\Payment::class . ':getStatus');
+        // $this->post('/coinpay/notify',  App\Services\CoinPayment::class. ':notify');
     });
 
     // Auth
@@ -125,6 +142,7 @@ return function (SlimApp $app) {
         $this->get('',                          App\Controllers\AdminController::class . ':index');
         $this->get('/',                         App\Controllers\AdminController::class . ':index');
 
+        $this->get('/sys',                      App\Controllers\AdminController::class . ':sys');
         $this->get('/invite',                   App\Controllers\AdminController::class . ':invite');
         $this->post('/invite',                  App\Controllers\AdminController::class . ':addInvite');
         $this->post('/chginvite',               App\Controllers\AdminController::class . ':chgInvite');
@@ -141,29 +159,33 @@ return function (SlimApp $app) {
 
         // Ticket Mange
         $this->get('/ticket',                   App\Controllers\Admin\TicketController::class . ':index');
-        $this->get('/ticket/{id}/view',         App\Controllers\Admin\TicketController::class . ':read');
-        $this->put('/ticket/{id}',              App\Controllers\Admin\TicketController::class . ':addReply');
-        $this->put('/ticket/{id}/close',        App\Controllers\Admin\TicketController::class . ':closeTk');
-        $this->post('/ticket/ajax',             App\Controllers\Admin\TicketController::class . ':ajaxQuery');
-        $this->delete('/ticket/{id}',           App\Controllers\Admin\TicketController::class . ':delete');
+        $this->post('/ticket',                  App\Controllers\Admin\TicketController::class . ':add');
+        $this->get('/ticket/{id}/view',         App\Controllers\Admin\TicketController::class . ':show');
+        $this->put('/ticket/{id}',              App\Controllers\Admin\TicketController::class . ':update');
+        $this->post('/ticket/ajax',             App\Controllers\Admin\TicketController::class . ':ajax');
+
+        // Shop Mange
+        $this->get('/shop',                     App\Controllers\Admin\ShopController::class . ':index');
+        $this->post('/shop/ajax',               App\Controllers\Admin\ShopController::class . ':ajax_shop');
+        $this->get('/shop/create',              App\Controllers\Admin\ShopController::class . ':create');
+        $this->post('/shop',                    App\Controllers\Admin\ShopController::class . ':add');
+        $this->get('/shop/{id}/edit',           App\Controllers\Admin\ShopController::class . ':edit');
+        $this->put('/shop/{id}',                App\Controllers\Admin\ShopController::class . ':update');
+        $this->delete('/shop',                  App\Controllers\Admin\ShopController::class . ':deleteGet');
+
+        // Bought Mange
+        $this->get('/bought',                   App\Controllers\Admin\ShopController::class . ':bought');
+        $this->delete('/bought',                App\Controllers\Admin\ShopController::class . ':deleteBoughtGet');
+        $this->post('/bought/ajax',             App\Controllers\Admin\ShopController::class . ':ajax_bought');
 
         // Product
         $this->get('/product',                  App\Controllers\Admin\ProductController::class . ':index');
-        $this->get('/product/details/{id}',     App\Controllers\Admin\ProductController::class . ':get');
+        $this->get('/product/create',           App\Controllers\Admin\ProductController::class . ':create');
         $this->post('/product',                 App\Controllers\Admin\ProductController::class . ':save');
+        $this->get('/product/{id}/edit',        App\Controllers\Admin\ProductController::class . ':edit');
         $this->put('/product/{id}',             App\Controllers\Admin\ProductController::class . ':update');
         $this->delete('/product/{id}',          App\Controllers\Admin\ProductController::class . ':delete');
 
-        // Order
-        $this->get('/order',                    App\Controllers\Admin\OrderController::class . ':index');
-        $this->post('/order/ajax',              App\Controllers\Admin\OrderController::class . ':ajaxQuery');
-
-        // Gift Card
-        $this->get('/giftcard',                 App\Controllers\Admin\GiftCardController::class . ':index');
-        $this->post('/giftcard',                App\Controllers\Admin\GiftCardController::class . ':add');
-        $this->post('/giftcard/ajax',           App\Controllers\Admin\GiftCardController::class . ':ajaxQuery');
-        $this->delete('/giftcard/{id}',         App\Controllers\Admin\GiftCardController::class . ':delete');
-
         // Ann Mange
         $this->get('/announcement',             App\Controllers\Admin\AnnController::class . ':index');
         $this->get('/announcement/create',      App\Controllers\Admin\AnnController::class . ':create');
@@ -195,6 +217,14 @@ return function (SlimApp $app) {
         $this->post('/login/ajax',              App\Controllers\Admin\IpController::class . ':ajax_login');
         $this->post('/alive/ajax',              App\Controllers\Admin\IpController::class . ':ajax_alive');
 
+        // Code Mange
+        $this->get('/code',                     App\Controllers\Admin\CodeController::class . ':index');
+        $this->get('/code/create',              App\Controllers\Admin\CodeController::class . ':create');
+        $this->post('/code',                    App\Controllers\Admin\CodeController::class . ':add');
+        $this->get('/donate/create',            App\Controllers\Admin\CodeController::class . ':donate_create');
+        $this->post('/donate',                  App\Controllers\Admin\CodeController::class . ':donate_add');
+        $this->post('/code/ajax',               App\Controllers\Admin\CodeController::class . ':ajax_code');
+
         // User Mange
         $this->get('/user',                     App\Controllers\Admin\UserController::class . ':index');
         $this->get('/user/{id}/edit',           App\Controllers\Admin\UserController::class . ':edit');
@@ -205,15 +235,27 @@ return function (SlimApp $app) {
         $this->post('/user/create',             App\Controllers\Admin\UserController::class . ':createNewUser');
 
         // Coupon Mange
-        $this->get('/coupon',                   App\Controllers\Admin\CouponController::class . ':index');
-        $this->get('/coupon/details/{id}',      App\Controllers\Admin\CouponController::class . ':get');
-        $this->post('/coupon',                  App\Controllers\Admin\CouponController::class . ':save');
-        $this->put('/coupon/{id}',              App\Controllers\Admin\CouponController::class . ':update');
-        $this->delete('/coupon/{id}',           App\Controllers\Admin\CouponController::class . ':delete');
+        $this->get('/coupon',                   App\Controllers\AdminController::class . ':coupon');
+        $this->post('/coupon',                  App\Controllers\AdminController::class . ':addCoupon');
+        $this->post('/coupon/ajax',             App\Controllers\AdminController::class . ':ajax_coupon');
 
         // Subscribe Log Mange
         $this->get('/subscribe',                App\Controllers\Admin\SubscribeLogController::class . ':index');
-        $this->post('/subscribe/ajax',          App\Controllers\Admin\SubscribeLogController::class . ':subscribe_ajax');
+        $this->post('/subscribe/ajax',          App\Controllers\Admin\SubscribeLogController::class . ':ajax_subscribe_log');
+
+        // Detect Ban Mange
+        $this->get('/detect/ban',               App\Controllers\Admin\DetectBanLogController::class . ':index');
+        $this->post('/detect/ban/ajax',         App\Controllers\Admin\DetectBanLogController::class . ':ajax_log');
+
+        // 指定用户购买记录以及添加套餐
+        $this->get('/user/{id}/bought',         App\Controllers\Admin\UserLog\BoughtLogController::class . ':bought');
+        $this->post('/user/{id}/bought/ajax',   App\Controllers\Admin\UserLog\BoughtLogController::class . ':bought_ajax');
+        $this->delete('/user/bought',           App\Controllers\Admin\UserLog\BoughtLogController::class . ':bought_delete');
+        $this->post('/user/{id}/bought/buy',    App\Controllers\Admin\UserLog\BoughtLogController::class . ':bought_add');
+
+        // 指定用户充值记录
+        $this->get('/user/{id}/code',           App\Controllers\Admin\UserLog\CodeLogController::class . ':index');
+        $this->post('/user/{id}/code/ajax',     App\Controllers\Admin\UserLog\CodeLogController::class . ':ajax');
 
         // 指定用户订阅记录
         $this->get('/user/{id}/sublog',         App\Controllers\Admin\UserLog\SubLogController::class . ':index');
@@ -231,6 +273,16 @@ return function (SlimApp $app) {
         $this->get('/setting',                  App\Controllers\Admin\SettingController::class . ':index');
         $this->post('/setting',                 App\Controllers\Admin\SettingController::class . ':save');
         $this->post('/setting/email',           App\Controllers\Admin\SettingController::class . ':test');
+        $this->post('/setting/payment',         App\Controllers\Admin\SettingController::class . ':payment');
+
+        // Config Mange
+        $this->group('/config', function () {
+            $this->put('/update/{key}',         App\Controllers\Admin\GConfigController::class . ':update');
+            $this->get('/update/{key}/edit',    App\Controllers\Admin\GConfigController::class . ':edit');
+
+            $this->get('/telegram',             App\Controllers\Admin\GConfigController::class . ':telegram');
+            $this->post('/telegram/ajax',       App\Controllers\Admin\GConfigController::class . ':telegram_ajax');
+        });
     })->add(new Admin());
 
     if ($_ENV['enableAdminApi']){
@@ -257,7 +309,6 @@ return function (SlimApp $app) {
         $this->post('/nodes/config',        App\Controllers\Mod_Mu\NodeController::class . ':getConfig');
 
         $this->get('/users',                App\Controllers\Mod_Mu\UserController::class . ':index');
-        $this->get('/users/traffic',        App\Controllers\Mod_Mu\UserController::class . ':getTraffic');
         $this->post('/users/traffic',       App\Controllers\Mod_Mu\UserController::class . ':addTraffic');
         $this->post('/users/aliveip',       App\Controllers\Mod_Mu\UserController::class . ':addAliveIp');
         $this->post('/users/detectlog',     App\Controllers\Mod_Mu\UserController::class . ':addDetectLog');

+ 0 - 70
block-whoops-env.sh

@@ -1,70 +0,0 @@
-#!/bin/bash
-
-check()
-{
-    dir=$(pwd)
-
-    if [[ ! -e "${dir}/vendor/filp/whoops/src/Whoops/Handler/PrettyPageHandler.php" ]];then
-        echo -e "\033[31m Please execute [composer install] first. \033[0m"
-        exit
-    fi
-
-    file="${dir}/vendor/filp/whoops/src/Whoops/Handler/PrettyPageHandler.php"
-}
-
-getline()
-{
-    del_line=$(cat -n ${file} | grep "PHP_AUTH_PW" | awk '{print $1}')
-    sed -i "${del_line}d" $file
-
-    line=$(cat -n ${file} | grep "blacklist php provided auth based values" | awk '{print $1}')
-    line=$(expr ${line} + 2)
-}
-
-backup()
-{
-    cp -f ${file} ${dir}/vendor/filp/whoops/src/Whoops/Handler/PrettyPageHandler.php.bak
-}
-
-run()
-{
-    list='key baseUrl db_host db_database db_username db_password active_payments muKey muKeyList adminApiToken telegram_token telegram_request_token cloudflare_email cloudflare_key cloudflare_name sentry_dsn github_access_token pwdMethod salt'
-    for key in $list
-    do
-        sed -i "${line}i \ \ \ \ \ \ \ \ \$this->blacklist('_ENV', '${key}');" ${file}
-    done
-
-    masked_line=$(cat -n ${file} | grep "str_repeat" | awk '{print $1}')
-    masked_line=$(expr ${masked_line} + 2)
-
-    sed -i "${masked_line}i \ \ \ \ \ \ \ \ \ \ \ \ \}" ${file}
-    sed -i "${masked_line}i \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \$values[\$key] = '*';" ${file}
-    sed -i "${masked_line}i \ \ \ \ \ \ \ \ \ \ \ \ \if (is_array(\$superGlobal[\$key])) {" ${file}
-}
-
-tip()
-{
-    echo -e "\033[31m Important environment configuration has been blocked, but please note that even so, it is still very dangerous to open debug mode in the production environment, please close it in time. \033[0m"
-}
-
-recover()
-{
-    check
-    cp -f ${dir}/vendor/filp/whoops/src/Whoops/Handler/PrettyPageHandler.php.bak ${file}
-    echo -e "\033[32m Whoops has been restored to the original file. \033[0m"
-}
-
-main()
-{
-    check
-    getline
-    backup
-    run
-    tip
-}
-
-if [[ $1 != "recover" ]];then
-    main
-else
-    recover
-fi

+ 2 - 3
composer.json

@@ -16,6 +16,7 @@
         "league/omnipay": "^3",
         "lokielse/omnipay-alipay": "*",
         "mailgun/mailgun-php": "^2.6.0",
+        "paymentwall/paymentwall-php": "^2.1",
         "phpmailer/phpmailer": "~6.0",
         "ramsey/uuid": "^3.0@dev",
         "robmorgan/phinx": "^0.12.9",
@@ -24,12 +25,10 @@
         "slim/slim": "~3.0",
         "smarty/smarty": "3.*",
         "stripe/stripe-php": "^7.105",
-        "symfony/polyfill-mbstring": "^1.25",
-        "symfony/string": "^5.4",
         "symfony/yaml": "^4.4@dev",
         "telegram-bot/api": "^2.2",
         "voku/anti-xss": "^1.2",
-        "zeuxisoo/slim-whoops": "0.6.*"
+        "zeuxisoo/slim-whoops": "0.5.*"
     },
     "autoload": {
         "psr-4": {

+ 381 - 208
config/.config.example.php

@@ -1,223 +1,388 @@
 <?php
 
-/*
-    网站设置
-*/
-
-$_ENV['debug'] = false; // 生产环境需设为false
-$_ENV['appName'] = 'sspanel-uim'; // 站点名称
-$_ENV['key'] = '32150285b345c48aa3492f9212f61ca2'; // 修改为随机字符串
-$_ENV['baseUrl'] = 'https://domain.com';// 站点地址
-
-/*
-    数据库设置
-*/
-
-// db_host|db_socket 二选一,若设置 db_socket 则 db_host 会被忽略,不用请留空。若数据库在本机上推荐用 db_socket
-// db_host 例: localhost(可解析的主机名), 127.0.0.1(IP 地址), 10.0.0.2:4406(含端口)
-// db_socket 例:/var/run/mysqld/mysqld.sock(需使用绝对地址)
-
-$_ENV['db_host'] = 'localhost';
-$_ENV['db_database'] = '';
-$_ENV['db_username'] = '';
-$_ENV['db_password'] = '';
-
-$_ENV['db_socket'] = '';
-$_ENV['db_prefix'] = '';
-$_ENV['db_driver'] = 'mysql';
-$_ENV['db_charset'] = 'utf8mb4';
+//注释里请勿使用英文方括号、分号、单引号,否则迁移Config时会出错
+
+//config迁移附注(由开发者填写本次config迁移后需要注意的地方,站长勿动)
+//如需换行,直接换行即可,无需换行符
+//【新增/删除】config无需写入迁移附注
+$_ENV['config_migrate_notice'] =
+    '新增 enable_change_email 配置項
+新增 smtp_bbc 配置項
+';
+$_ENV['version'] = 2;    //仅当涉及【需要修改config以外的文件】时才需要+1,站长勿动
+
+
+//基本设置--------------------------------------------------------------------------------------------
+$_ENV['key']        = '1145141919810';                //!!! 瞎 jb 修改此key为随机字符串确保网站安全 !!!
+$_ENV['debug']      = false;                          //正式环境请确保为 false
+$_ENV['appName']    = 'SSPanel-UIM';                      //站点名称
+$_ENV['baseUrl']    = 'https://sspanel.host';               //站点地址
+$_ENV['muKey']      = 'NimaQu';                       //用于校验魔改后端请求,可以随意修改,但请保持前后端一致,否则节点不能工作!
+
+$_ENV['enableAdminApi'] = false;                      // 是否启用 Admin API, 如果不知道此项用途请保持为 false
+$_ENV['adminApiToken']  = 'ChangeMeToSafeKey';        // Admin API 的 Token, 请生成为高强度的 Token
+
+//数据库设置--------------------------------------------------------------------------------------------
+// db_host|db_socket 二选一,若设置 db_socket 则 db_host 会被忽略,不用请留空。若数据库在本机上推荐用 db_socket。
+// db_host 例: localhost(可解析的主机名), 127.0.0.1(IP 地址), 10.0.0.2:4406(含端口)
+// db_socket 例:/var/run/mysqld/mysqld.sock(需使用绝对地址)
+$_ENV['db_driver']    = 'mysql';
+$_ENV['db_host']      = '';
+$_ENV['db_socket']    = '';
+$_ENV['db_database']  = 'sspanel';           //数据库名
+$_ENV['db_username']  = 'root';              //数据库用户名
+$_ENV['db_password']  = 'sspanel';           //用户名对应的密码
+#高级
+$_ENV['db_charset']   = 'utf8mb4';
 $_ENV['db_collation'] = 'utf8mb4_unicode_ci';
+$_ENV['db_prefix']    = '';
 
-/*
-    支付设置
-*/
-
-$_ENV['active_payments'] = [
-    'alipay_f2f' => [
-        'name' => '支付宝',
-        'min' => '10',
-        'max' => '1000',
-        'enable' => false,
-        'f2f_pay_app_id'=> '',
-        'f2f_pay_pid'=> '',
-        'f2f_pay_public_key'=> '',
-        'f2f_pay_private_key'=> '',
-    ],
-    'universal' => [
-        'name' => '',
-        'min' => '10',
-        'max' => '1000',
-        'gateway' => '',
-        'sign_key' => '',
-        'enable' => false,
-    ],
-];
-
-/*
-    与 Web Api 有关的设置
-*/
-
-$_ENV['WebAPI'] = true;
-$_ENV['muKey'] = '3a7caa4b32ffb47e7bb2d0ec7d097110'; // 通信密钥
-$_ENV['muKeyList'] = []; // 多key列表
-$_ENV['checkNodeIp'] = true; // 是否验证节点ip
-
-$_ENV['enableAdminApi'] = false; // 是否启用 Admin API, 如果不知道此项用途请保持为 false
-$_ENV['adminApiToken']  = '7cb4ddeaea0a1a7a42f351f71a28124a'; // Admin API 的 Token, 请生成为高强度的 Token
-
-// 如下设置将使397,297号节点复用4号节点的流媒体解锁
+//流媒体解锁 如下设置将使397,297号节点复用4号节点的检测结果 使用时去掉注释符 //
 $_ENV['streaming_media_unlock_multiplexing'] = [
     //'397' => '4',
     //'297' => '4',
 ];
 
-/*
-    注册用户行为限制
-*/
-
-$_ENV['enable_kill'] = false; // 是否允许用户主动删除账户
-$_ENV['enable_change_email'] = false;  // 是否允许用户主动更改账户邮箱
-$_ENV['enable_checkin'] = true; // 是否允许用户签到
-$_ENV['enable_expired_checkin'] = true; // 是否允许过期用户签到
-$_ENV['checkinMin'] = 100; // 签到可获得的最低流量(MB)
-$_ENV['checkinMax'] = 300; // 签到可获得的最多流量(MB)
-$_ENV['enable_ticket'] = true; // 是否开启工单系统
-$_ENV['mail_ticket'] = true; // 是否开启工单邮件提醒
-$_ENV['enable_docs'] = true; // 是否开启文档系统
-$_ENV['gift_card_rebate'] = false; // 当用户兑换礼品卡添加余额时,是否执行返利
-
-/*
-    与邮件相关设置
-*/
-
-$_ENV['sendPageLimit'] = 50; // 发信分页数
-$_ENV['email_queue'] = true; // 邮件队列开关
-$_ENV['mail_filter'] = 0; // 0关闭; 1白名单模式; 2黑名单模式
-$_ENV['mail_filter_list'] = ['qq.com', 'vip.qq.com', 'foxmail.com'];
-$_ENV['notify_limit_mode'] = true; // false为关闭,per为按照百分比提醒,mb为按照固定剩余流量提醒
-$_ENV['notify_limit_value'] = 20; // 当上一项为per时,此处填写百分比;当上一项为mb时,此处填写流量
-
-/*
-    后端设置
-*/
-
-$_ENV['keep_connect'] = false; // 流量耗尽则限速1Mbps
-$_ENV['disconnect_time'] = 60; // 在用户超过套餐连接IP数后多久才会拒绝新连接
-
-$_ENV['min_port'] = 10000; // 0为不分配; 其他值时为分配起始端口
-$_ENV['max_port'] = 60000; // 0为不分配; 其他值时为分配终止端口
-
-$_ENV['v2ray_port'] = 443;
-$_ENV['v2ray_level'] = 0;
-$_ENV['v2ray_alter_id'] = 2;
-$_ENV['v2ray_protocol'] = 'HTTP/2 + TLS';
-
-/*
-    Telegram bot
-*/
-
-// 变更这些参数均需要执行 php xcat Tool setTelegram
-$_ENV['telegram_bot'] = ''; // 机器人用户名
-$_ENV['telegram_token'] = ''; // 机器人token
-$_ENV['telegram_chatid'] = ''; // 群组会话id
-$_ENV['enable_telegram'] = false; // 机器人开关
-$_ENV['use_new_telegram_bot'] = true; // 新版机器人开关
-$_ENV['telegram_group_quiet'] = false; // 是否在群组中回应
-$_ENV['telegram_request_token'] = '51d38e0819930dbdb808a5c3e65d08a9'; // 修改为随机字符串
-
-// 功能设置
-$_ENV['finance_public'] = false; // 财务报告是否向群公开
-$_ENV['enable_welcome_message'] = true; // 机器人发送欢迎消息
-$_ENV['enable_telegram_login'] = false; // 需配置并启用新版机器人开关
-$_ENV['allow_to_join_new_groups'] = true; // 允许 Bot 加入下方配置之外的群组
-$_ENV['group_id_allowed_to_join'] = []; // 允许加入的群组 ID
-$_ENV['telegram_admins'] = []; // 额外的 Telegram 管理员 ID
-$_ENV['delete_message_time'] = 180; // 0为关闭; 其他数值为在此时间后删除用户触发的 bot 回复
-$_ENV['delete_admin_message_time'] = 86400; // 0为关闭; 其他数值为在此时间后删除管理命令触发的 bot 回复
-$_ENV['enable_delete_user_cmd'] = false; // 删除用户触发的 bot 回复功能开关
-$_ENV['help_any_command'] = false; // 其他未知命令触发 /help 回复
-$_ENV['enable_user_email_group_show'] = false; // false时隐藏用户完整邮箱
-$_ENV['enable_detect_offline'] = false; // 节点掉线检测
-
-/*
-    订阅设置
-*/
-
-$_ENV['Subscribe'] = true; // 本站是否提供订阅功能
-$_ENV['subUrl'] = $_ENV['baseUrl'] . '/link/'; // 订阅地址,如需和站点名称相同,请不要修改
-$_ENV['mergeSub'] = true; // 合并订阅设置 可选项 false / true
-$_ENV['enable_sub_extend'] = true; // 是否开启订阅中默认显示流量剩余以及账户到期时间以及 sub_message 中的信息
-$_ENV['enable_forced_replacement'] = true; // 用户修改账户登录密码时,是否强制更换订阅地址
-$_ENV['sub_message'] = []; // 订阅中的营销信息,使用数组形式,将会添加在订阅列表的顶端,可用于为用户推送最新地址等信息,尽可能简短且数量不宜太多
-$_ENV['disable_sub_mu_port'] = false; // 将订阅中单端口的信息去除
-$_ENV['subscribeLog'] = true; // 是否记录用户订阅日志
-$_ENV['subscribeLog_show'] = true; // 是否允许用户查看订阅记录
-$_ENV['subscribeLog_keep_days'] = 7; // 订阅记录保留天数
-$_ENV['mu_port_migration'] = false; // 为后端直接下发偏移后的端口
-$_ENV['add_emoji_to_node_name'] = false; // 为部分订阅中默认添加 emoji
-$_ENV['add_appName_to_ss_uri'] = true; // 为 SS 节点名称中添加站点名
-$_ENV['subscribe_client'] = true; // 下载协议客户端时附带节点和订阅信息
-$_ENV['subscribe_client_url'] = ''; // 使用独立的服务器提供附带节点和订阅信息的协议客户端下载,为空表示不使用
-$_ENV['Clash_DefaultProfiles'] = 'default'; // Clash 默认配置方案
-$_ENV['Surge_DefaultProfiles'] = 'default'; // Surge 默认配置方案
-$_ENV['Surge2_DefaultProfiles'] = 'default'; // Surge2 默认配置方案
-$_ENV['Surfboard_DefaultProfiles']  = 'default'; // Surfboard 默认配置方案
-
-/*
-    注册设置
-*/
-
-$_ENV['random_group'] = '0'; // 注册时随机分配到的分组,英文半角逗号分隔
-$_ENV['enable_reg_im'] = true; // 注册时是否要求用户输入IM联系方式
-$_ENV['reg_invite_num'] = '100'; // 注册时默认的邀请码可用次数,开放注册模式下不扣减邀请码次数,仅在仅允许邀请注册的情况下扣减
-$_ENV['reg_money'] = 0; // 注册时默认的账户余额,可以设置一个数,然后引导用户在商店购买试用套餐
-$_ENV['reg_forbidden_ip'] = '127.0.0.0/8,::1/128'; // 注册时默认禁止访问IP列表,英文半角逗号分隔
-$_ENV['reg_forbidden_port'] = ''; // 注册时默认禁止访问端口列表,英文半角逗号分隔,支持端口段
-$_ENV['reg_obfs'] = 'plain'; // 注册时默认的混淆
-$_ENV['reg_method'] = 'rc4-md5'; // 注册时默认的加密
-$_ENV['reg_protocol'] = 'origin'; // 注册时默认的协议
-$_ENV['reg_obfs_param'] = 'world.taobao.com'; // 注册时默认的混淆参数
-$_ENV['reg_protocol_param'] = ''; // 注册时默认的协议参数
-$_ENV['mu_suffix'] = 'microsoft.com'; // 单端口多用户混淆参数后缀,可以随意修改,但请保持前后端一致
-$_ENV['mu_regex'] = '%5m%id.%suffix'; // 单端口多用户混淆参数表达式,%5m代表取用户特征 md5 的前五位,%id 代表用户id, %suffix 代表上面这个后缀
-
-/*
-    第三方服务
-*/
-
-// cloudflare.com
-$_ENV['cloudflare_enable'] = false; // 是否开启 Cloudflare 解析
-$_ENV['cloudflare_email'] = ''; // Cloudflare 邮箱地址
-$_ENV['cloudflare_key'] = ''; // Cloudflare API Key
-$_ENV['cloudflare_name'] = ''; // 域名
-
-// sentry.io
-$_ENV['sentry_dsn'] = '';
-
-// github.com
-$_ENV['github_access_token'] = '';
+//邮件设置--------------------------------------------------------------------------------------------
+$_ENV['sendPageLimit']      = 50;           //发信分页 解决大站发公告超时问题
+$_ENV['email_queue']        = true;         //如题,自动计划任务邮件使用队列 需要每分钟执行 php xcat Job SendMail
+$_ENV['mail_filter']        = 0;            //0: 关闭; 1: 白名单模式; 2; 黑名单模式;
+$_ENV['mail_filter_list']   = array("qq.com", "vip.qq.com", "foxmail.com");
+
+
+//备份设置--------------------------------------------------------------------------------------------
+$_ENV['auto_backup_email']  = '';                               //接收备份的邮箱
+$_ENV['auto_backup_password'] = '';                               //备份的压缩密码
+$_ENV['backup_notify']      = false;                            //备份通知到TG群中
+
+
+//用户注册设置-----------------------------------------------------------------------------------------
+$_ENV['free_user_reset_day']            = 0;                         //免费用戶的流量重置日,0为不重置
+$_ENV['free_user_reset_bandwidth']      = 0;                         //需要重置的免费流量,0为不重置
+$_ENV['random_group']              = '0';                       //注册时随机分组,注册时随机分配到的分组,多个分组请用英文半角逗号分隔
+
+$_ENV['enable_reg_im']             = false;                      //注册时是否要求用户输入IM联系方式
+
+$_ENV['reg_forbidden_ip']          = '127.0.0.0/8,::1/128';     //注册时默认禁止访问IP列表,半角英文逗号分割
+$_ENV['min_port']                  = 10000;                     //用户端口池最小值,如果该数值为0则用户在注册的时候不会被分配多用户端口,适合纯V2Ray/Trojan机场
+$_ENV['max_port']                  = 65535;                     //用户端口池最大值,如果该数值为0则用户在注册的时候不会被分配多用户端口,适合纯V2Ray/Trojan机场
+$_ENV['reg_forbidden_port']        = '';                        //注册时默认禁止访问端口列表,半角英文逗号分割,支持端口段
+
+$_ENV['mu_suffix']                 = 'microsoft.com';           //单端口多用户混淆参数后缀,可以随意修改,但请保持前后端一致
+$_ENV['mu_regex']                  = '%5m%id.%suffix';          //单端口多用户混淆参数表达式,%5m代表取用户特征 md5 的前五位,%id 代表用户id, %suffix 代表上面这个后缀。
+
+#邀请链接
+$_ENV['invite_price']              = -1;                        //用户购买邀请码所需要的价格,价格小于0时视为不开放购买
+$_ENV['custom_invite_price']       = -1;                        //用户定制邀请码所需要的价格,价格小于0时视为不开放购买
+
+
+//已注册用户设置---------------------------------------------------------------------------------------
+#基础
+$_ENV['enable_checkin']             = true;         //是否啓用簽到功能
+$_ENV['checkinMin']                 = 1;            //用户签到最少流量 单位MB
+$_ENV['checkinMax']                 = 50;           //用户签到最多流量
+
+$_ENV['auto_clean_uncheck_days']    = -1;           //自动清理多少天没签到的0级用户,小于等于0时关闭
+$_ENV['auto_clean_unused_days']     = -1;           //自动清理多少天没使用的0级用户,小于等于0时关闭
+$_ENV['auto_clean_min_money']       = 1;            //余额低于多少的0级用户可以被清理
+
+$_ENV['enable_bought_reset']        = true;         //购买时是否重置流量
+$_ENV['enable_bought_extend']       = true;         //购买时是否延长等级期限(同等级配套)
+
+$_ENV['port_price']                 = -1;           //用户随机重置端口所需要的价格,价格小于0时视为不开放购买
+$_ENV['port_price_specify']         = -1;           //用户指明钦定端口所需要的价格,价格小于0时视为不开放购买
+
+$_ENV['disconnect_time']        = 60;            //在用戶超过套餐连接IP数后多久才会拒绝新连接
+
+#高级
+$_ENV['class_expire_reset_traffic'] = 0;            //等级到期时重置为的流量值,单位GB,小于0时不重置
+$_ENV['account_expire_delete_days'] = -1;           //账户到期几天之后会删除账户,小于0时不删除
+
+$_ENV['enable_kill']                = true;         //是否允许用户注销账户
+$_ENV['enable_change_email']        = true;         //是否允许用户更改賬戶郵箱
+
+#用户流量余量不足邮件提醒
+$_ENV['notify_limit_mode']          = true;         //false为关闭,per为按照百分比提醒,mb为按照固定剩余流量提醒
+$_ENV['notify_limit_value']         = 20;           //当上一项为per时,此处填写百分比;当上一项为mb时,此处填写流量
+
+//订阅设置---------------------------------------------------------------------------------------
+$_ENV['Subscribe']                  = true;                         //本站是否提供订阅功能
+$_ENV['subUrl']                     = $_ENV['baseUrl'] . '/link/';  //订阅地址,如需和站点名称相同,请不要修改
+$_ENV['mergeSub']                   = true;                         //合并订阅设置 可选项 false / true
+$_ENV['enable_sub_extend']          = true;                         //是否开启订阅中默认显示流量剩余以及账户到期时间以及 sub_message 中的信息
+$_ENV['enable_forced_replacement']  = true;                         //用户修改账户登录密码时,是否强制更换订阅地址
+
+// 订阅中的营销信息
+// 使用数组形式,将会添加在订阅列表的顶端
+// 可用于为用户推送最新地址等信息,尽可能简短且数量不宜太多
+$_ENV['sub_message']                = [];
+$_ENV['disable_sub_mu_port']        = false;                        // 将订阅中单端口的信息去除
+$_ENV['subscribeLog']               = false;                        //是否记录用户订阅日志
+$_ENV['subscribeLog_show']          = true;                         //是否允许用户查看订阅记录
+$_ENV['subscribeLog_keep_days']     = 7;                            //订阅记录保留天数
+$_ENV['mu_port_migration']          = false;                        //为后端直接下发偏移后的端口
+$_ENV['add_emoji_to_node_name']     = false;                        //为部分订阅中默认添加 emoji
+$_ENV['add_appName_to_ss_uri']      = true;                         //为 SS 节点名称中添加站点名
+$_ENV['subscribe_client']           = true;                         //下载协议客户端时附带节点和订阅信息
+$_ENV['subscribe_client_url']       = '';                           //使用独立的服务器提供附带节点和订阅信息的协议客户端下载,为空表示不使用
+$_ENV['Clash_DefaultProfiles']      = 'default';                    //Clash 默认配置方案
+$_ENV['Surge_DefaultProfiles']      = 'default';                    //Surge 默认配置方案
+$_ENV['Surge2_DefaultProfiles']     = 'default';                    //Surge2 默认配置方案
+$_ENV['Surfboard_DefaultProfiles']  = 'default';                    //Surfboard 默认配置方案
+
+
+//审计自动封禁设置--------------------------------------------------------------------------------------------
+$_ENV['enable_auto_detect_ban']      = false;       // 审计自动封禁开关
+$_ENV['auto_detect_ban_numProcess']  = 300;         // 单次计划任务中审计记录的处理数量
+$_ENV['auto_detect_ban_allow_admin'] = true;        // 管理员不受审计限制
+$_ENV['auto_detect_ban_allow_users'] = [];          // 审计封禁的例外用户 ID
+
+// 审计封禁判断类型:
+//   - 1 = 仁慈模式,每触碰多少次封禁一次
+//   - 2 = 疯狂模式,累计触碰次数按阶梯进行不同时长的封禁
+$_ENV['auto_detect_ban_type']        = 1;
+$_ENV['auto_detect_ban_number']      = 30;             // 仁慈模式每次执行封禁所需的触发次数
+$_ENV['auto_detect_ban_time']        = 60;             // 仁慈模式每次封禁的时长 (分钟)
+
+// 疯狂模式阶梯
+// key 为触发次数
+//   - type:可选 time 按时间 或 kill 删号
+//   - time:时间,单位分钟
+$_ENV['auto_detect_ban'] = [
+    100 => [
+        'type' => 'time',
+        'time' => 120
+    ],
+    300 => [
+        'type' => 'time',
+        'time' => 720
+    ],
+    600 => [
+        'type' => 'time',
+        'time' => 4320
+    ],
+    1000 => [
+        'type' => 'kill',
+        'time' => 0
+    ]
+];
 
-/*
-    杂项
-*/
-
-$_ENV['authDriver'] = 'cookie'; // 不能更改
-$_ENV['pwdMethod'] = 'md5'; // md5,sha256,bcrypt,argon2i,argon2id
-$_ENV['salt'] = ''; // 加盐仅支持 md5,sha256
-$_ENV['tokenDriver'] = 'db';
-$_ENV['cacheDriver'] = 'cookie';
-$_ENV['sessionDriver'] = 'cookie';
-$_ENV['theme'] = 'material'; // 默认主题
-$_ENV['timeZone'] = 'PRC'; // PRC / UTC
-$_ENV['jump_delay'] = 1200;
-$_ENV['enable_login_bind_ip'] = true; // 是否将登陆线程和IP绑定
-$_ENV['cookie_expiration_time'] = 1; // cookie 过期时间
-$_ENV['php_user_group'] = 'www:www';
 
-/*
-    获取客户端地址
-*/
+//Bot 设置--------------------------------------------------------------------------------------------
+# Telegram bot
+$_ENV['enable_telegram']                    = false;        //是否开启 Telegram bot
+$_ENV['use_new_telegram_bot']               = true;         //是否使用新的 Telegram bot
+$_ENV['telegram_token']                     = '';           //Telegram bot,bot 的 token ,跟 father bot 申请
+$_ENV['telegram_chatid']                    = '';           //Telegram bot,群组会话 ID,把机器人拉进群里之后跟他 /ping 一下即可得到
+$_ENV['telegram_bot']                       = '_bot';       //Telegram 机器人账号
+$_ENV['telegram_group_quiet']               = false;        //Telegram 机器人在群组中不回应
+$_ENV['telegram_request_token']             = '';           //Telegram 机器人请求Key,随意设置,由大小写英文和数字组成,更新这个参数之后请 php xcat Tool setTelegram
+
+# 通用
+$_ENV['finance_public']                     = true;         //财务报告是否向群公开
+$_ENV['enable_welcome_message']             = true;         //机器人发送欢迎消息
+
+# Telegram BOT 其他选项
+$_ENV['allow_to_join_new_groups']           = true;         //允许 Bot 加入下方配置之外的群组
+$_ENV['group_id_allowed_to_join']           = [];           //允许加入的群组 ID,格式为 PHP 数组
+$_ENV['telegram_admins']                    = [];           //额外的 Telegram 管理员 ID,格式为 PHP 数组
+$_ENV['enable_not_admin_reply']             = true;         //非管理员操作管理员功能是否回复
+$_ENV['not_admin_reply_msg']                = '!';          //非管理员操作管理员功能的回复内容
+$_ENV['no_user_found']                      = '!';          //管理员操作时,找不到用户的回复
+$_ENV['no_search_value_provided']           = '!';          //管理员操作时,没有提供用户搜索值的回复
+$_ENV['data_method_not_found']              = '!';          //管理员操作时,修改数据的字段没有找到的回复
+$_ENV['delete_message_time']                = 180;          //在以下时间后删除用户命令触发的 bot 回复,单位:秒,删除时间可能会因为定时任务而有差异,为 0 代表不开启此功能
+$_ENV['delete_admin_message_time']          = 86400;        //在以下时间后删除管理命令触发的 bot 回复,单位:秒,删除时间可能会因为定时任务而有差异,为 0 代表不开启此功能
+$_ENV['enable_delete_user_cmd']             = false;        //自动删除群组中用户发送的命令,使用 delete_message_time 配置的时间,删除时间可能会因为定时任务而有差异
+$_ENV['help_any_command']                   = false;        //允许任意未知的命令触发 /help 的回复
+
+$_ENV['remark_user_search_email']           = ['邮箱'];                     //用户搜索字段 email 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_search_port']            = ['端口'];                     //用户搜索字段 port 的别名,可多个,格式为 PHP 数组
+
+$_ENV['remark_user_option_is_admin']        = ['管理员'];                   //用户搜索字段 is_admin 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_enable']          = ['用户启用'];                  //用户搜索字段 enable 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_money']           = ['金钱', '余额'];             //用户搜索字段 money 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_port']            = ['端口'];                     //用户搜索字段 port 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_transfer_enable'] = ['流量'];                     //用户搜索字段 transfer_enable 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_passwd']          = ['连接密码'];                 //用户搜索字段 passwd 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_method']          = ['加密'];                     //用户搜索字段 method 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_protocol']        = ['协议'];                     //用户搜索字段 protocol 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_protocol_param']  = ['协参', '协议参数'];         //用户搜索字段 protocol_param 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_obfs']            = ['混淆'];                     //用户搜索字段 obfs 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_obfs_param']      = ['混参', '混淆参数'];         //用户搜索字段 obfs_param 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_invite_num']      = ['邀请数量'];                 //用户搜索字段 invite_num 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_node_group']      = ['用户组', '用户分组'];       //用户搜索字段 node_group 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_class']           = ['等级'];                     //用户搜索字段 class 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_class_expire']    = ['等级过期时间'];             //用户搜索字段 class_expire 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_expire_in']       = ['账号过期时间'];             //用户搜索字段 expire_in 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_node_speedlimit'] = ['限速'];                    //用户搜索字段 node_speedlimit 的别名,可多个,格式为 PHP 数组
+$_ENV['remark_user_option_node_connector']  = ['连接数', '客户端'];         //用户搜索字段 node_connector 的别名,可多个,格式为 PHP 数组
+
+$_ENV['enable_user_email_group_show']       = false;                      //开启在群组搜寻用户信息时显示用户完整邮箱,关闭则会对邮箱中间内容打码,如 g****@gmail.com
+$_ENV['user_not_bind_reply']                = '您未绑定本站账号,您可以进入网站的 **资料编辑**,在右下方绑定您的账号.';                      //未绑定账户的回复
+$_ENV['telegram_general_pricing']           = '产品介绍.';                  //面向游客的产品介绍
+$_ENV['telegram_general_terms']             = '服务条款.';                  //面向游客的服务条款
+
+//社交登录设置
+#Telegram
+$_ENV['enable_telegram_login']              = false;   //开启这个设置前请先配置 Telegram bot 否则不会生效
+
+
+#工单系统设置
+$_ENV['enable_ticket']        = true;        //是否开启工单系统
+$_ENV['mail_ticket']          = true;        //是否开启工单邮件提醒
+
+# Server酱 用户提交新工单或者回复工单时用微信提醒机场主 https://sct.ftqq.com/
+$_ENV['useScFtqq']            = false;        //是否开启工单Server酱提醒
+$_ENV['ScFtqq_SCKEY']         = '';           //请填写您在Server酱获取的SCKEY  请仔细检查勿粘贴错
+
+
+#后台商品列表 销量统计
+$_ENV['sales_period']         = 30;             //统计指定周期内的销量,值为【expire/任意大于0的整数】
+
+#国旗
+$_ENV['enable_flag']          = true;            //启用该项之前务必先仔细阅读教程
+$_ENV['flag_regex']           = '/.*?(?=\s)/';   //从站点全名中匹配【国家/地区】的正则表达式(php版)
+
+#捐赠
+$_ENV['enable_donate']        = true;          //是否显示用户捐赠(所有收入将被公开)
+
+#iOS账户显示
+$_ENV['display_ios_class']    = -1;        //至少等级为多少的用户可以看见,小于0时关闭此功能
+$_ENV['display_ios_topup']    = 0;         //满足等级要求后,累计充值高于多少的用户可以看见
+$_ENV['ios_account']          = '';        //iOS账户
+$_ENV['ios_password']         = '';        //iOS密码
+
+#用户中心首页添加其他客户端的支持,可配合 subconverter 等 Api
+$_ENV['userCenterClient']     = [
+    'iOS'     => [
+        [
+            'name'           => 'Loon',
+            'support'        => 'SS/SSR/VMess',
+            'download_urls'  => [
+                [
+                    'name' => '本站下载',
+                    'url'  => 'https://google.com',
+                ],
+                [
+                    'name' => '官方下载',
+                    'url'  => 'https://baidu.com',
+                ]
+            ],
+            'tutorial_url'   => '/doc/#/iOS/Loon',
+            'description'    => '其他说明.',
+            'subscribe_urls' => [
+                [
+                    'name' => 'SS 订阅',
+                    'type' => 'href',
+                    'url'  => '%userUrl%?sub=2',
+                ],
+                [
+                    'name' => 'SSR 订阅',
+                    'type' => 'href',
+                    'url'  => '%userUrl%?sub=1',
+                ],
+                [
+                    'name' => 'V2Ray 订阅',
+                    'type' => 'copy',
+                    'url'  => '%userUrl%?sub=3',
+                ]
+            ]
+        ]
+    ],
+    'macOS'   => [],
+    'Linux'   => [],
+    'Router'  => [],
+    'Android' => [],
+    'Windows' => [
+        [
+            'name'           => 'Netch',
+            'support'        => 'SS/SSR/VMess',
+            'download_urls'  => [
+                [
+                    'name' => '官方下载',
+                    'url'  => 'https://github.com/NetchX/Netch/releases',
+                ]
+            ],
+            'tutorial_url'   => '/doc/#/Windows/Netch',
+            'description'    => '其他说明.',
+            'subscribe_urls' => [
+                [
+                    'name' => 'SS 订阅',
+                    'type' => 'href',
+                    'url'  => '%userUrl%?sub=2',
+                ],
+                [
+                    'name' => 'SSR 订阅',
+                    'type' => 'href',
+                    'url'  => '%userUrl%?sub=1',
+                ],
+                [
+                    'name' => 'V2Ray 订阅',
+                    'type' => 'copy',
+                    'url'  => '%userUrl%?sub=3',
+                ]
+            ]
+        ]
+    ]
+];
 
+//节点检测-----------------------------------------------------------------------------------------------
+#GFW检测,请通过crontab进行【开启/关闭】
+$_ENV['detect_gfw_interval']             = 3600;                                                               //检测间隔,单位:秒,低于推荐值会爆炸
+$_ENV['detect_gfw_port']                 = 22;                                                                 //所有节点服务器都打开的TCP端口,常用的为22(SSH端口)
+$_ENV['detect_gfw_url']                  = 'http://cn-sh-tcping.sspanel.org:8080/tcping?ip={ip}&port={port}'; //检测节点是否被gfw墙了的API的URL
+$_ENV['detect_gfw_judge']                = '$json_tcping[\'status\']=="true"';                                 //判断是否被墙的依据,json_tcping为上方URL返回的json数组
+$_ENV['detect_gfw_count']                = '3';                                                                //尝试次数
+
+#离线检测
+$_ENV['enable_detect_offline']           = true;
+#离线检测是否推送到Server酱 请配置好上文的Server配置
+$_ENV['enable_detect_offline_useScFtqq'] = false;
+
+//V2Ray相关设置------------------------------------------------------------------------------------------
+$_ENV['v2ray_port']     = 443;                  //V2Ray端口
+$_ENV['v2ray_protocol'] = 'HTTP/2 + TLS';       //V2Ray协议
+$_ENV['v2ray_alter_id'] = 32;
+$_ENV['v2ray_level']    = 0;
+
+//以下所有均为高级设置(一般用不上,不用改---------------------------------------------------------------------
+
+// 主站是否提供 WebAPI
+// - 为了安全性,推荐使用 WebAPI 模式对接节点并关闭公网数据库连接。
+// - 如果您全部节点使用数据库连接或者拥有独立的 WebAPI 站点或 Seed,则可设为 false。
+$_ENV['WebAPI']     = true;
+
+#杂项
+$_ENV['authDriver']             = 'cookie';            //不能更改此项
+$_ENV['pwdMethod']              = 'md5';               //密码加密 可选 md5, sha256, bcrypt, argon2i, argon2id(argon2i需要至少php7.2)
+$_ENV['salt']                   = '';                  //推荐配合 md5/sha256, bcrypt/argon2i/argon2id 会忽略此项
+$_ENV['sessionDriver']          = 'cookie';            //可选: cookie
+$_ENV['cacheDriver']            = 'cookie';            //可选: cookie
+$_ENV['tokenDriver']            = 'db';                //可选: db
+
+$_ENV['enable_login_bind_ip']   = false;        //是否将登陆线程和IP绑定
+$_ENV['rememberMeDuration']     = 7;           //登录时记住账号时长天数
+
+$_ENV['timeZone']               = 'PRC';                 //PRC 天朝时间  UTC 格林时间
+$_ENV['theme']                  = 'material';            //默认主题
+$_ENV['jump_delay']             = 1200;                  //跳转延时,单位ms,不建议太长
+
+$_ENV['checkNodeIp']            = true;                 //是否webapi验证节点ip
+$_ENV['muKeyList']              = [];                   //多 key 列表
+$_ENV['keep_connect']           = false;               // 流量耗尽用户限速至 1Mbps
+$_ENV['money_from_admin']       = false;            //是否开启管理员修改用户余额时创建充值记录
+
+#Cloudflare
+$_ENV['cloudflare_enable']      = false;         //是否开启 Cloudflare 解析
+$_ENV['cloudflare_email']       = '';            //Cloudflare 邮箱地址
+$_ENV['cloudflare_key']         = '';            //Cloudflare API Key
+$_ENV['cloudflare_name']        = '';            //域名
+
+#是否夹带统计代码,自己在 resources/views/{主题名} 下创建一个 analytics.tpl ,如果有必要就用 literal 界定符
+$_ENV['enable_analytics_code']  = false;
+
+#在套了CDN之后获取用户真实ip,如果您不知道这是什么,请不要乱动
 $_ENV['cdn_forwarded_ip'] = array('HTTP_X_FORWARDED_FOR', 'HTTP_ALI_CDN_REAL_IP', 'X-Real-IP', 'True-Client-Ip');
 foreach ($_ENV['cdn_forwarded_ip'] as $cdn_forwarded_ip) {
     if (isset($_SERVER[$cdn_forwarded_ip])) {
@@ -226,3 +391,11 @@ foreach ($_ENV['cdn_forwarded_ip'] as $cdn_forwarded_ip) {
         break;
     }
 }
+
+// https://sentry.io for production debugging
+$_ENV['sentry_dsn'] = '';
+
+// ClientDownload 命令解决 API 访问频率高而被限制使用的 Github access token
+$_ENV['github_access_token'] = '';
+
+$_ENV['php_user_group'] = 'www:www';

+ 500 - 0
config/settings.json

@@ -1,4 +1,84 @@
 [
+    {
+        "id": null,
+        "item": "payment_gateway",
+        "value": "[]",
+        "class": "pay",
+        "is_public": 1,
+        "type": "array",
+        "default": "[]",
+        "mark": "支付网关"
+    },
+    {
+        "id": null,
+        "item": "f2f_pay_app_id",
+        "value": "",
+        "class": "f2f",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "当面付AppID"
+    },
+    {
+        "id": null,
+        "item": "f2f_pay_pid",
+        "value": "",
+        "class": "f2f",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "当面付PID"
+    },
+    {
+        "id": null,
+        "item": "f2f_pay_public_key",
+        "value": "",
+        "class": "f2f",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "当面付公钥"
+    },
+    {
+        "id": null,
+        "item": "f2f_pay_private_key",
+        "value": "",
+        "class": "f2f",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "当面付私钥"
+    },
+    {
+        "id": null,
+        "item": "f2f_pay_notify_url",
+        "value": "",
+        "class": "f2f",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "当面付自定义回调地址"
+    },
+    {
+        "id": null,
+        "item": "vmq_gateway",
+        "value": "",
+        "class": "vmq",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "V免签网关"
+    },
+    {
+        "id": null,
+        "item": "vmq_key",
+        "value": "",
+        "class": "vmq",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "V免签密钥"
+    },
     {
         "id": null,
         "item": "smtp_host",
@@ -249,6 +329,86 @@
         "default": "",
         "mark": "aws密钥key"
     },
+    {
+        "id": null,
+        "item": "auto_backup_email",
+        "value": "",
+        "class": "backup",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "接收备份的邮箱"
+    },
+    {
+        "id": null,
+        "item": "auto_backup_password",
+        "value": "",
+        "class": "backup",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "备份的压缩密码"
+    },
+    {
+        "id": null,
+        "item": "payjs_mchid",
+        "value": "",
+        "class": "payjs",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "payjs_mchid"
+    },
+    {
+        "id": null,
+        "item": "payjs_key",
+        "value": "",
+        "class": "payjs",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "payjs_key"
+    },
+    {
+        "id": null,
+        "item": "enable_admin_contact",
+        "value": "0",
+        "class": "contact",
+        "is_public": 1,
+        "type": "bool",
+        "default": "0",
+        "mark": "是否显示站长联系方式"
+    },
+    {
+        "id": null,
+        "item": "admin_contact1",
+        "value": "qq",
+        "class": "contact",
+        "is_public": 1,
+        "type": "string",
+        "default": "qq",
+        "mark": "站长联系方式一"
+    },
+    {
+        "id": null,
+        "item": "admin_contact2",
+        "value": "mail",
+        "class": "contact",
+        "is_public": 1,
+        "type": "string",
+        "default": "mail",
+        "mark": "站长联系方式二"
+    },
+    {
+        "id": null,
+        "item": "admin_contact3",
+        "value": "telegram",
+        "class": "contact",
+        "is_public": 1,
+        "type": "string",
+        "default": "telegram",
+        "mark": "站长联系方式三"
+    },
     {
         "id": null,
         "item": "tawk_id",
@@ -299,6 +459,56 @@
         "default": "none",
         "mark": "客服系统开关"
     },
+    {
+        "id": null,
+        "item": "theadpay_url",
+        "value": "",
+        "class": "theadpay",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "theadpay_url"
+    },
+    {
+        "id": null,
+        "item": "theadpay_mchid",
+        "value": "",
+        "class": "theadpay",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "theadpay_mchid"
+    },
+    {
+        "id": null,
+        "item": "theadpay_key",
+        "value": "",
+        "class": "theadpay",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "theadpay_key"
+    },
+    {
+        "id": null,
+        "item": "coinpay_appid",
+        "value": "",
+        "class": "coinpay",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "CoinPay应用ID"
+    },
+    {
+        "id": null,
+        "item": "coinpay_secret",
+        "value": "",
+        "class": "coinpay",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "CoinPay验证密钥"
+    },
     {
         "id": null,
         "item": "user_center_bg",
@@ -339,6 +549,56 @@
         "default": "\/theme\/material\/css\/images\/bg\/amber.jpg",
         "mark": "管理中心背景图片地址"
     },
+    {
+        "id": null,
+        "item": "pmw_publickey",
+        "value": "",
+        "class": "pmw",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "pmw公钥"
+    },
+    {
+        "id": null,
+        "item": "pmw_privatekey",
+        "value": "",
+        "class": "pmw",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "pmw私钥"
+    },
+    {
+        "id": null,
+        "item": "pmw_widget",
+        "value": "m2_1",
+        "class": "pmw",
+        "is_public": 0,
+        "type": "string",
+        "default": "m2_1",
+        "mark": "pmw_widget"
+    },
+    {
+        "id": null,
+        "item": "pmw_height",
+        "value": "350px",
+        "class": "pmw",
+        "is_public": 0,
+        "type": "string",
+        "default": "350px",
+        "mark": "pmw_height"
+    },
+    {
+        "id": null,
+        "item": "auto_backup_notify",
+        "value": "0",
+        "class": "backup",
+        "is_public": 0,
+        "type": "bool",
+        "default": "0",
+        "mark": "备份是否通知到TG群中"
+    },
     {
         "id": null,
         "item": "reg_mode",
@@ -379,6 +639,156 @@
         "default": "5",
         "mark": "验证码有效期内单个ip可请求的发信次数"
     },
+    {
+        "id": null,
+        "item": "sign_up_for_free_traffic",
+        "value": "20",
+        "class": "register",
+        "is_public": 0,
+        "type": "int",
+        "default": "20",
+        "mark": "注册时赠送的流量(单位:GB)"
+    },
+    {
+        "id": null,
+        "item": "sign_up_for_free_time",
+        "value": "7",
+        "class": "register",
+        "is_public": 0,
+        "type": "int",
+        "default": "7",
+        "mark": "注册时赠送的时长(单位:天)"
+    },
+    {
+        "id": null,
+        "item": "connection_device_limit",
+        "value": "0",
+        "class": "register",
+        "is_public": 0,
+        "type": "int",
+        "default": "0",
+        "mark": "连接设备限制"
+    },
+    {
+        "id": null,
+        "item": "connection_rate_limit",
+        "value": "0",
+        "class": "register",
+        "is_public": 0,
+        "type": "int",
+        "default": "0",
+        "mark": "使用速率限制"
+    },
+    {
+        "id": null,
+        "item": "sign_up_for_class",
+        "value": "0",
+        "class": "register",
+        "is_public": 0,
+        "type": "int",
+        "default": "0",
+        "mark": "注册时设定的等级"
+    },
+    {
+        "id": null,
+        "item": "sign_up_for_class_time",
+        "value": "7",
+        "class": "register",
+        "is_public": 0,
+        "type": "int",
+        "default": "7",
+        "mark": "注册时设定的等级过期时间(单位:天)"
+    },
+    {
+        "id": null,
+        "item": "sign_up_for_method",
+        "value": "chacha20-ietf",
+        "class": "register",
+        "is_public": 0,
+        "type": "string",
+        "default": "chacha20-ietf",
+        "mark": "默认加密"
+    },
+    {
+        "id": null,
+        "item": "sign_up_for_protocol",
+        "value": "auth_aes128_sha1",
+        "class": "register",
+        "is_public": 0,
+        "type": "string",
+        "default": "auth_aes128_sha1",
+        "mark": "默认协议"
+    },
+    {
+        "id": null,
+        "item": "sign_up_for_obfs",
+        "value": "http_simple",
+        "class": "register",
+        "is_public": 0,
+        "type": "string",
+        "default": "http_simple",
+        "mark": "默认混淆"
+    },
+    {
+        "id": null,
+        "item": "sign_up_for_protocol_param",
+        "value": "",
+        "class": "register",
+        "is_public": 0,
+        "type": "string",
+        "default": "",
+        "mark": "默认协议参数"
+    },
+    {
+        "id": null,
+        "item": "sign_up_for_obfs_param",
+        "value": "www.jd.hk",
+        "class": "register",
+        "is_public": 0,
+        "type": "string",
+        "default": "www.jd.hk",
+        "mark": "默认混淆参数"
+    },
+    {
+        "id": null,
+        "item": "sign_up_for_daily_report",
+        "value": "0",
+        "class": "register",
+        "is_public": 0,
+        "type": "bool",
+        "default": "0",
+        "mark": "注册后是否默认接收每日用量邮件推送"
+    },
+    {
+        "id": null,
+        "item": "sign_up_for_invitation_codes",
+        "value": "10",
+        "class": "register",
+        "is_public": 0,
+        "type": "int",
+        "default": "10",
+        "mark": "初始邀请注册链接使用次数限制"
+    },
+    {
+        "id": null,
+        "item": "invitation_to_register_balance_reward",
+        "value": "1",
+        "class": "invite",
+        "is_public": 1,
+        "type": "int",
+        "default": "1",
+        "mark": "邀请注册余额奖励(单位:元)"
+    },
+    {
+        "id": null,
+        "item": "invitation_to_register_traffic_reward",
+        "value": "10",
+        "class": "invite",
+        "is_public": 1,
+        "type": "int",
+        "default": "10",
+        "mark": "邀请注册流量奖励(单位:GB)"
+    },
     {
         "id": null,
         "item": "invitation_mode",
@@ -438,5 +848,95 @@
         "type": "int",
         "default": "180",
         "mark": "返利时间范围限制(单位:天)"
+    },
+    {
+        "id": null,
+        "item": "stripe_currency",
+        "value": "HKD",
+        "class": "stripe",
+        "is_public": 0,
+        "type": "string",
+        "default": "HKD",
+        "mark": "货币代码"
+    },
+    {
+        "id": null,
+        "item": "stripe_sk",
+        "value": "stripe_sk",
+        "class": "stripe",
+        "is_public": 0,
+        "type": "string",
+        "default": "stripe_sk",
+        "mark": "stripe_sk"
+    },
+    {
+        "id": null,
+        "item": "stripe_pk",
+        "value": "stripe_pk",
+        "class": "stripe",
+        "is_public": 0,
+        "type": "string",
+        "default": "stripe_pk",
+        "mark": "stripe_pk"
+    },
+    {
+        "id": null,
+        "item": "stripe_webhook_key",
+        "value": "stripe_webhook_key",
+        "class": "stripe",
+        "is_public": 0,
+        "type": "string",
+        "default": "stripe_webhook_key",
+        "mark": "web_hook密钥"
+    },
+    {
+        "id": null,
+        "item": "stripe_min_recharge",
+        "value": "10",
+        "class": "stripe",
+        "is_public": 1,
+        "type": "int",
+        "default": "10",
+        "mark": "最低充值限额"
+    },
+    {
+        "id": null,
+        "item": "stripe_card",
+        "value": "0",
+        "class": "stripe",
+        "is_public": 0,
+        "type": "bool",
+        "default": "0",
+        "mark": "银行卡支付"
+    },
+    {
+        "id": null,
+        "item": "stripe_alipay",
+        "value": "0",
+        "class": "stripe",
+        "is_public": 0,
+        "type": "bool",
+        "default": "0",
+        "mark": "支付宝支付"
+    },
+    {
+        "id": null,
+        "item": "stripe_wechat",
+        "value": "0",
+        "class": "stripe",
+        "is_public": 0,
+        "type": "bool",
+        "default": "0",
+        "mark": "微信支付"
+    },
+    {
+        "id": null,
+        "item": "stripe_max_recharge",
+        "value": "1000",
+        "class": "stripe",
+        "is_public": 1,
+        "type": "int",
+        "default": "1000",
+        "mark": "最高充值限额"
     }
 ]

+ 0 - 5
databases/fix-database.sql

@@ -1,5 +0,0 @@
-ANALYZE TABLE `alive_ip`, `announcement`, `config`, `coupon`, `detect_list`, `detect_log`, `email_queue`, `email_verify`, `gift_card`, `link`, `login_ip`, `node`, `node_info`, `node_online_log`, `payback`, `phinxlog`, `product`, `product_order`, `stream_media`, `telegram_session`, `ticket`, `user`, `user_invite_code`, `user_password_reset`, `user_subscribe_log`, `user_token`, `work_order`;
-CHECK TABLE `alive_ip`, `announcement`, `config`, `coupon`, `detect_list`, `detect_log`, `email_queue`, `email_verify`, `gift_card`, `link`, `login_ip`, `node`, `node_info`, `node_online_log`, `payback`, `phinxlog`, `product`, `product_order`, `stream_media`, `telegram_session`, `ticket`, `user`, `user_invite_code`, `user_password_reset`, `user_subscribe_log`, `user_token`, `work_order`;
-CHECKSUM TABLE `alive_ip`, `announcement`, `config`, `coupon`, `detect_list`, `detect_log`, `email_queue`, `email_verify`, `gift_card`, `link`, `login_ip`, `node`, `node_info`, `node_online_log`, `payback`, `phinxlog`, `product`, `product_order`, `stream_media`, `telegram_session`, `ticket`, `user`, `user_invite_code`, `user_password_reset`, `user_subscribe_log`, `user_token`, `work_order`;
-OPTIMIZE TABLE `alive_ip`, `announcement`, `config`, `coupon`, `detect_list`, `detect_log`, `email_queue`, `email_verify`, `gift_card`, `link`, `login_ip`, `node`, `node_info`, `node_online_log`, `payback`, `phinxlog`, `product`, `product_order`, `stream_media`, `telegram_session`, `ticket`, `user`, `user_invite_code`, `user_password_reset`, `user_subscribe_log`, `user_token`, `work_order`;
-REPAIR TABLE `alive_ip`, `announcement`, `config`, `coupon`, `detect_list`, `detect_log`, `email_queue`, `email_verify`, `gift_card`, `link`, `login_ip`, `node`, `node_info`, `node_online_log`, `payback`, `phinxlog`, `product`, `product_order`, `stream_media`, `telegram_session`, `ticket`, `user`, `user_invite_code`, `user_password_reset`, `user_subscribe_log`, `user_token`, `work_order`;

+ 0 - 34
databases/migrations/20220411015707_product_table.php

@@ -1,34 +0,0 @@
-<?php
-declare (strict_types = 1);
-
-use Phinx\Migration\AbstractMigration;
-
-final class ProductTable extends AbstractMigration
-{
-    /**
-     * Change Method.
-     *
-     * Write your reversible migrations using this method.
-     *
-     * More information on writing migrations is available here:
-     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
-     *
-     * Remember to call "create()" or "update()" and NOT "save()" when working
-     * with the Table class.
-     */
-    public function change(): void
-    {
-        $table = $this->table('product');
-        $table->addColumn('type', 'text', array('comment' => '类型'))
-            ->addColumn('name', 'text', array('comment' => '名称'))
-            ->addColumn('price', 'integer', array('comment' => '售价'))
-            ->addColumn('content', 'text', array('comment' => '内容'))
-            ->addColumn('translate', 'text', array('comment' => '内容翻译'))
-            ->addColumn('html', 'text', array('comment' => '自定义代码'))
-            ->addColumn('limit', 'text', array('comment' => '购买限制', 'default' => null, 'null' => true))
-            ->addColumn('status', 'integer', array('comment' => '销售状态'))
-            ->addColumn('created_at', 'integer', array('comment' => '创建时间'))
-            ->addColumn('updated_at', 'integer', array('comment' => '更新时间'))
-            ->create();
-    }
-}

+ 0 - 25
databases/migrations/20220411172052_drop_coupon_table.php

@@ -1,25 +0,0 @@
-<?php
-declare (strict_types = 1);
-
-use Phinx\Migration\AbstractMigration;
-
-final class DropCouponTable extends AbstractMigration
-{
-    /**
-     * Change Method.
-     *
-     * Write your reversible migrations using this method.
-     *
-     * More information on writing migrations is available here:
-     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
-     *
-     * Remember to call "create()" or "update()" and NOT "save()" when working
-     * with the Table class.
-     */
-    public function change(): void
-    {
-        $this->table('coupon')
-            ->drop()
-            ->update();
-    }
-}

+ 0 - 35
databases/migrations/20220411172159_create_new_coupon_table.php

@@ -1,35 +0,0 @@
-<?php
-declare (strict_types = 1);
-
-use Phinx\Migration\AbstractMigration;
-
-final class CreateNewCouponTable extends AbstractMigration
-{
-    /**
-     * Change Method.
-     *
-     * Write your reversible migrations using this method.
-     *
-     * More information on writing migrations is available here:
-     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
-     *
-     * Remember to call "create()" or "update()" and NOT "save()" when working
-     * with the Table class.
-     */
-    public function change(): void
-    {
-        $table = $this->table('coupon');
-        $table->addColumn('coupon', 'text', array('comment' => '优惠码'))
-            ->addColumn('discount', 'text', array('comment' => '折扣额度'))
-            ->addColumn('time_limit', 'text', array('comment' => '时间限制'))
-            ->addColumn('product_limit', 'text', array('comment' => '商品使用范围限制'))
-            ->addColumn('user_limit', 'integer', array('comment' => '单用户使用最大次数'))
-            ->addColumn('total_limit', 'integer', array('comment' => '全部用户累计使用最大次数'))
-            ->addColumn('use_count', 'integer', array('comment' => '使用计数'))
-            ->addColumn('amount_count', 'text', array('comment' => '折扣金额计数'))
-            ->addColumn('created_at', 'integer', array('comment' => '创建时间'))
-            ->addColumn('updated_at', 'integer', array('comment' => '更新时间'))
-            ->addColumn('expired_at', 'integer', array('comment' => '过期时间'))
-            ->create();
-    }
-}

+ 0 - 26
databases/migrations/20220412045711_add_product_stock_field.php

@@ -1,26 +0,0 @@
-<?php
-declare (strict_types = 1);
-
-use Phinx\Migration\AbstractMigration;
-
-final class AddProductStockField extends AbstractMigration
-{
-    /**
-     * Change Method.
-     *
-     * Write your reversible migrations using this method.
-     *
-     * More information on writing migrations is available here:
-     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
-     *
-     * Remember to call "create()" or "update()" and NOT "save()" when working
-     * with the Table class.
-     */
-    public function change(): void
-    {
-        $table = $this->table('product');
-        $table->addColumn('sales', 'integer', array('comment' => '商品销售数', 'after' => 'content'))
-            ->addColumn('stock', 'integer', array('comment' => '商品库存', 'after' => 'content'))
-            ->update();
-    }
-}

+ 0 - 39
databases/migrations/20220412082403_product_order_table.php

@@ -1,39 +0,0 @@
-<?php
-declare (strict_types = 1);
-
-use Phinx\Migration\AbstractMigration;
-
-final class ProductOrderTable extends AbstractMigration
-{
-    /**
-     * Change Method.
-     *
-     * Write your reversible migrations using this method.
-     *
-     * More information on writing migrations is available here:
-     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
-     *
-     * Remember to call "create()" or "update()" and NOT "save()" when working
-     * with the Table class.
-     */
-    public function change(): void
-    {
-        $table = $this->table('product_order');
-        $table->addColumn('no', 'text', array('comment' => '订单号'))
-            ->addColumn('user_id', 'integer', array('comment' => '提交用户'))
-            ->addColumn('product_id', 'integer', array('comment' => '订单商品'))
-            ->addColumn('product_name', 'text', array('comment' => '商品名称'))
-            ->addColumn('product_type', 'text', array('comment' => '商品类型'))
-            ->addColumn('product_content', 'text', array('comment' => '商品内容'))
-            ->addColumn('product_price', 'integer', array('comment' => '商品售价'))
-            ->addColumn('order_coupon', 'text', array('comment' => '订单优惠码', 'default' => null, 'null' => true))
-            ->addColumn('order_price', 'integer', array('comment' => '订单金额'))
-            ->addColumn('order_status', 'text', array('comment' => '订单状态'))
-            ->addColumn('created_at', 'integer', array('comment' => '创建时间'))
-            ->addColumn('updated_at', 'integer', array('comment' => '更新时间'))
-            ->addColumn('expired_at', 'integer', array('comment' => '过期时间'))
-            ->addColumn('paid_at', 'integer', array('comment' => '支付时间'))
-            ->addColumn('paid_action', 'text', array('comment' => '支付后操作'))
-            ->create();
-    }
-}

+ 0 - 26
databases/migrations/20220413153512_add_two_fields_to_the_product_order_table.php

@@ -1,26 +0,0 @@
-<?php
-declare(strict_types=1);
-
-use Phinx\Migration\AbstractMigration;
-
-final class AddTwoFieldsToTheProductOrderTable extends AbstractMigration
-{
-    /**
-     * Change Method.
-     *
-     * Write your reversible migrations using this method.
-     *
-     * More information on writing migrations is available here:
-     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
-     *
-     * Remember to call "create()" or "update()" and NOT "save()" when working
-     * with the Table class.
-     */
-    public function change(): void
-    {
-        $table = $this->table('product_order');
-        $table->addColumn('order_payment', 'text', array('comment' => '订单支付方式', 'after' => 'order_price'))
-            ->addColumn('execute_status', 'integer', array('comment' => '订单处理状态', 'after' => 'paid_action'))
-            ->update();
-    }
-}

+ 0 - 30
databases/migrations/20220414005559_gift_card_table.php

@@ -1,30 +0,0 @@
-<?php
-declare(strict_types=1);
-
-use Phinx\Migration\AbstractMigration;
-
-final class GiftCardTable extends AbstractMigration
-{
-    /**
-     * Change Method.
-     *
-     * Write your reversible migrations using this method.
-     *
-     * More information on writing migrations is available here:
-     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
-     *
-     * Remember to call "create()" or "update()" and NOT "save()" when working
-     * with the Table class.
-     */
-    public function change(): void
-    {
-        $table = $this->table('gift_card');
-        $table->addColumn('card', 'text', array('comment' => '卡号'))
-            ->addColumn('balance', 'integer', array('comment' => '余额'))
-            ->addColumn('created_at', 'integer', array('comment' => '创建时间'))
-            ->addColumn('status', 'integer', array('comment' => '使用状态'))
-            ->addColumn('used_at', 'integer', array('comment' => '使用时间'))
-            ->addColumn('use_user', 'integer', array('comment' => '使用用户'))
-            ->create();
-    }
-}

+ 0 - 33
databases/migrations/20220416033504_work_order_table.php

@@ -1,33 +0,0 @@
-<?php
-declare(strict_types=1);
-
-use Phinx\Migration\AbstractMigration;
-
-final class WorkOrderTable extends AbstractMigration
-{
-    /**
-     * Change Method.
-     *
-     * Write your reversible migrations using this method.
-     *
-     * More information on writing migrations is available here:
-     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
-     *
-     * Remember to call "create()" or "update()" and NOT "save()" when working
-     * with the Table class.
-     */
-    public function change(): void
-    {
-        $table = $this->table('work_order');
-        $table->addColumn('tk_id', 'integer', array('comment' => '围绕主题'))
-            ->addColumn('is_topic', 'integer', array('comment' => '是否是主题帖'))
-            ->addColumn('title', 'text', array('comment' => '主题帖标题', 'default' => null, 'null' => true))
-            ->addColumn('content', 'text', array('comment' => '围绕主题帖的回复内容'))
-            ->addColumn('user_id', 'integer', array('comment' => '提交用户'))
-            ->addColumn('created_at', 'integer', array('comment' => '创建时间'))
-            ->addColumn('updated_at', 'integer', array('comment' => '更新时间'))
-            ->addColumn('closed_at', 'integer', array('comment' => '关闭时间', 'default' => null, 'null' => true))
-            ->addColumn('closed_by', 'text', array('comment' => '关闭人', 'default' => null, 'null' => true))
-            ->create();
-    }
-}

+ 0 - 53
databases/migrations/20220416172613_delete_old_table.php

@@ -1,53 +0,0 @@
-<?php
-declare(strict_types=1);
-
-use Phinx\Migration\AbstractMigration;
-
-final class DeleteOldTable extends AbstractMigration
-{
-    /**
-     * Change Method.
-     *
-     * Write your reversible migrations using this method.
-     *
-     * More information on writing migrations is available here:
-     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
-     *
-     * Remember to call "create()" or "update()" and NOT "save()" when working
-     * with the Table class.
-     */
-    public function change(): void
-    {
-        $this->table('blockip')
-        ->drop()
-        ->update();
-
-        $this->table('bought')
-        ->drop()
-        ->update();
-
-        $this->table('code')
-        ->drop()
-        ->update();
-
-        $this->table('detect_ban_log')
-        ->drop()
-        ->update();
-
-        $this->table('paylist')
-        ->drop()
-        ->update();
-
-        $this->table('shop')
-        ->drop()
-        ->update();
-
-        $this->table('unblockip')
-        ->drop()
-        ->update();
-
-        $this->table('user_hourly_usage')
-        ->drop()
-        ->update();
-    }
-}

+ 0 - 29
databases/migrations/20220416175028_delete_user_table_field.php

@@ -1,29 +0,0 @@
-<?php
-declare(strict_types=1);
-
-use Phinx\Migration\AbstractMigration;
-
-final class DeleteUserTableField extends AbstractMigration
-{
-    /**
-     * Change Method.
-     *
-     * Write your reversible migrations using this method.
-     *
-     * More information on writing migrations is available here:
-     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
-     *
-     * Remember to call "create()" or "update()" and NOT "save()" when working
-     * with the Table class.
-     */
-    public function up(): void
-    {
-        $table = $this->table('user');
-        $table->removeColumn('last_detect_ban_time')
-            ->removeColumn('all_detect_number')
-            ->removeColumn('is_hide')
-            ->removeColumn('auto_reset_day')
-            ->removeColumn('auto_reset_bandwidth')
-            ->save();
-    }
-}

+ 0 - 25
databases/migrations/20220417100701_drop_g_config_table.php

@@ -1,25 +0,0 @@
-<?php
-declare(strict_types=1);
-
-use Phinx\Migration\AbstractMigration;
-
-final class DropGConfigTable extends AbstractMigration
-{
-    /**
-     * Change Method.
-     *
-     * Write your reversible migrations using this method.
-     *
-     * More information on writing migrations is available here:
-     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
-     *
-     * Remember to call "create()" or "update()" and NOT "save()" when working
-     * with the Table class.
-     */
-    public function change(): void
-    {
-        $this->table('gconfig')
-        ->drop()
-        ->update();
-    }
-}

+ 0 - 0
databases/migrations/20220109130532_create_stream_media_table.php → db/migrations/20220109130532_create_stream_media_table.php


+ 0 - 0
databases/seeds/readme.md → db/seeds/readme.md


+ 2 - 2
phinx.php

@@ -5,8 +5,8 @@ include './config/.config.php';
 return
 [
     'paths' => [
-        'migrations' => '%%PHINX_CONFIG_DIR%%/databases/migrations',
-        'seeds' => '%%PHINX_CONFIG_DIR%%/databases/seeds'
+        'migrations' => '%%PHINX_CONFIG_DIR%%/db/migrations',
+        'seeds' => '%%PHINX_CONFIG_DIR%%/db/seeds'
     ],
     'environments' => [
         'default_migration_table' => 'phinxlog',

+ 1 - 0
public/index.php

@@ -11,6 +11,7 @@ declare(strict_types=1);
 
 require __DIR__ . '/../vendor/autoload.php';
 require __DIR__ . '/../config/.config.php';
+require __DIR__ . '/../config/appprofile.php';
 require __DIR__ . '/../app/predefine.php';
 require __DIR__ . '/../app/envload.php';
 

+ 15 - 0
public/theme/material/editor/.gitignore

@@ -0,0 +1,15 @@
+logs
+*.log
+*.pid
+*.seed
+node_modules/
+.sass-cache/
+research/
+test/
+backup/
+examples/uploads/**/*
+*.bat
+*.sh
+.project
+.url
+css/*.map

+ 22 - 0
public/theme/material/editor/LICENSE

@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 pandao
+
+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.
+

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 3169 - 0
public/theme/material/editor/css/editormd.css


+ 98 - 0
public/theme/material/editor/css/editormd.logo.css

@@ -0,0 +1,98 @@
+/*
+ * Editor.md
+ *
+ * @file        editormd.logo.css 
+ * @version     v1.5.0 
+ * @description Open source online markdown editor.
+ * @license     MIT License
+ * @author      Pandao
+ * {@link       https://github.com/pandao/editor.md}
+ * @updateTime  2015-06-09
+ */
+
+/*! prefixes.scss v0.1.0 | Author: Pandao | https://github.com/pandao/prefixes.scss | MIT license | Copyright (c) 2015 */
+@font-face {
+  font-family: 'editormd-logo';
+  src: url("../fonts/editormd-logo.eot?-5y8q6h");
+  src: url(".../fonts/editormd-logo.eot?#iefix-5y8q6h") format("embedded-opentype"), url("../fonts/editormd-logo.woff?-5y8q6h") format("woff"), url("../fonts/editormd-logo.ttf?-5y8q6h") format("truetype"), url("../fonts/editormd-logo.svg?-5y8q6h#icomoon") format("svg");
+  font-weight: normal;
+  font-style: normal;
+}
+.editormd-logo,
+.editormd-logo-1x,
+.editormd-logo-2x,
+.editormd-logo-3x,
+.editormd-logo-4x,
+.editormd-logo-5x,
+.editormd-logo-6x,
+.editormd-logo-7x,
+.editormd-logo-8x {
+  font-family: 'editormd-logo';
+  speak: none;
+  font-style: normal;
+  font-weight: normal;
+  font-variant: normal;
+  text-transform: none;
+  font-size: inherit;
+  line-height: 1;
+  display: inline-block;
+  text-rendering: auto;
+  vertical-align: inherit;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+.editormd-logo:before,
+.editormd-logo-1x:before,
+.editormd-logo-2x:before,
+.editormd-logo-3x:before,
+.editormd-logo-4x:before,
+.editormd-logo-5x:before,
+.editormd-logo-6x:before,
+.editormd-logo-7x:before,
+.editormd-logo-8x:before {
+  content: "\e1987";
+  /* 
+  HTML Entity &#xe1987; 
+  example: <span class="editormd-logo">&#xe1987;</span>
+  */
+}
+
+.editormd-logo-1x {
+  font-size: 1em;
+}
+
+.editormd-logo-lg {
+  font-size: 1.2em;
+}
+
+.editormd-logo-2x {
+  font-size: 2em;
+}
+
+.editormd-logo-3x {
+  font-size: 3em;
+}
+
+.editormd-logo-4x {
+  font-size: 4em;
+}
+
+.editormd-logo-5x {
+  font-size: 5em;
+}
+
+.editormd-logo-6x {
+  font-size: 6em;
+}
+
+.editormd-logo-7x {
+  font-size: 7em;
+}
+
+.editormd-logo-8x {
+  font-size: 8em;
+}
+
+.editormd-logo-color {
+  color: #2196F3;
+}

+ 2 - 0
public/theme/material/editor/css/editormd.logo.min.css

@@ -0,0 +1,2 @@
+/*! Editor.md v1.5.0 | editormd.logo.min.css | Open source online markdown editor. | MIT License | By: Pandao | https://github.com/pandao/editor.md | 2015-06-09 */
+/*! prefixes.scss v0.1.0 | Author: Pandao | https://github.com/pandao/prefixes.scss | MIT license | Copyright (c) 2015 */@font-face{font-family:editormd-logo;src:url(../fonts/editormd-logo.eot?-5y8q6h);src:url(.../fonts/editormd-logo.eot?#iefix-5y8q6h)format("embedded-opentype"),url(../fonts/editormd-logo.woff?-5y8q6h)format("woff"),url(../fonts/editormd-logo.ttf?-5y8q6h)format("truetype"),url(../fonts/editormd-logo.svg?-5y8q6h#icomoon)format("svg");font-weight:400;font-style:normal}.editormd-logo,.editormd-logo-1x,.editormd-logo-2x,.editormd-logo-3x,.editormd-logo-4x,.editormd-logo-5x,.editormd-logo-6x,.editormd-logo-7x,.editormd-logo-8x{font-family:editormd-logo;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;font-size:inherit;line-height:1;display:inline-block;text-rendering:auto;vertical-align:inherit;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.editormd-logo-1x:before,.editormd-logo-2x:before,.editormd-logo-3x:before,.editormd-logo-4x:before,.editormd-logo-5x:before,.editormd-logo-6x:before,.editormd-logo-7x:before,.editormd-logo-8x:before,.editormd-logo:before{content:"\e1987"}.editormd-logo-1x{font-size:1em}.editormd-logo-lg{font-size:1.2em}.editormd-logo-2x{font-size:2em}.editormd-logo-3x{font-size:3em}.editormd-logo-4x{font-size:4em}.editormd-logo-5x{font-size:5em}.editormd-logo-6x{font-size:6em}.editormd-logo-7x{font-size:7em}.editormd-logo-8x{font-size:8em}.editormd-logo-color{color:#2196F3}

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
public/theme/material/editor/css/editormd.min.css


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 2453 - 0
public/theme/material/editor/css/editormd.preview.css


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 4 - 0
public/theme/material/editor/css/editormd.preview.min.css


+ 4667 - 0
public/theme/material/editor/editormd.amd.js

@@ -0,0 +1,4667 @@
+/*
+ * Editor.md
+ *
+ * @file        editormd.amd.js 
+ * @version     v1.5.0 
+ * @description Open source online markdown editor.
+ * @license     MIT License
+ * @author      Pandao
+ * {@link       https://github.com/pandao/editor.md}
+ * @updateTime  2015-06-09
+ */
+
+;(function(factory) {
+    "use strict";
+    
+	// CommonJS/Node.js
+	if (typeof require === "function" && typeof exports === "object" && typeof module === "object")
+    { 
+        module.exports = factory;
+    }
+	else if (typeof define === "function")  // AMD/CMD/Sea.js
+	{
+        if (define.amd) // for Require.js
+        {
+            var cmModePath  = "codemirror/mode/";
+            var cmAddonPath = "codemirror/addon/";
+
+            var codeMirrorModules = [
+                "jquery", "marked", "prettify",
+                "katex", "raphael", "underscore", "flowchart",  "jqueryflowchart",  "sequenceDiagram",
+
+                "codemirror/lib/codemirror",
+                cmModePath + "css/css",
+                cmModePath + "sass/sass",
+                cmModePath + "shell/shell",
+                cmModePath + "sql/sql",
+                cmModePath + "clike/clike",
+                cmModePath + "php/php",
+                cmModePath + "xml/xml",
+                cmModePath + "markdown/markdown",
+                cmModePath + "javascript/javascript",
+                cmModePath + "htmlmixed/htmlmixed",
+                cmModePath + "gfm/gfm",
+                cmModePath + "http/http",
+                cmModePath + "go/go",
+                cmModePath + "dart/dart",
+                cmModePath + "coffeescript/coffeescript",
+                cmModePath + "nginx/nginx",
+                cmModePath + "python/python",
+                cmModePath + "perl/perl",
+                cmModePath + "lua/lua",
+                cmModePath + "r/r", 
+                cmModePath + "ruby/ruby", 
+                cmModePath + "rst/rst",
+                cmModePath + "smartymixed/smartymixed",
+                cmModePath + "vb/vb",
+                cmModePath + "vbscript/vbscript",
+                cmModePath + "velocity/velocity",
+                cmModePath + "xquery/xquery",
+                cmModePath + "yaml/yaml",
+                cmModePath + "erlang/erlang",
+                cmModePath + "jade/jade",
+
+                cmAddonPath + "edit/trailingspace", 
+                cmAddonPath + "dialog/dialog", 
+                cmAddonPath + "search/searchcursor", 
+                cmAddonPath + "search/search", 
+                cmAddonPath + "scroll/annotatescrollbar", 
+                cmAddonPath + "search/matchesonscrollbar", 
+                cmAddonPath + "display/placeholder", 
+                cmAddonPath + "edit/closetag", 
+                cmAddonPath + "fold/foldcode",
+                cmAddonPath + "fold/foldgutter",
+                cmAddonPath + "fold/indent-fold",
+                cmAddonPath + "fold/brace-fold",
+                cmAddonPath + "fold/xml-fold", 
+                cmAddonPath + "fold/markdown-fold",
+                cmAddonPath + "fold/comment-fold", 
+                cmAddonPath + "mode/overlay", 
+                cmAddonPath + "selection/active-line", 
+                cmAddonPath + "edit/closebrackets", 
+                cmAddonPath + "display/fullscreen",
+                cmAddonPath + "search/match-highlighter"
+            ];
+
+            define(codeMirrorModules, factory);
+        } 
+        else 
+        {
+		    define(["jquery"], factory);  // for Sea.js
+        }
+	} 
+	else
+	{ 
+        window.editormd = factory();
+	}
+    
+}(function() {    
+
+    if (typeof define == "function" && define.amd) {
+       $          = arguments[0];
+       marked     = arguments[1];
+       prettify   = arguments[2];
+       katex      = arguments[3];
+       Raphael    = arguments[4];
+       _          = arguments[5];
+       flowchart  = arguments[6];
+       CodeMirror = arguments[9];
+   }
+    
+    "use strict";
+    
+    var $ = (typeof (jQuery) !== "undefined") ? jQuery : Zepto;
+
+	if (typeof ($) === "undefined") {
+		return ;
+	}
+    
+    /**
+     * editormd
+     * 
+     * @param   {String} id           编辑器的ID
+     * @param   {Object} options      配置选项 Key/Value
+     * @returns {Object} editormd     返回editormd对象
+     */
+    
+    var editormd         = function (id, options) {
+        return new editormd.fn.init(id, options);
+    };
+    
+    editormd.title        = editormd.$name = "Editor.md";
+    editormd.version      = "1.5.0";
+    editormd.homePage     = "https://pandao.github.io/editor.md/";
+    editormd.classPrefix  = "editormd-";
+    
+    editormd.toolbarModes = {
+        full : [
+            "undo", "redo", "|", 
+            "bold", "del", "italic", "quote", "ucwords", "uppercase", "lowercase", "|", 
+            "h1", "h2", "h3", "h4", "h5", "h6", "|", 
+            "list-ul", "list-ol", "hr", "|",
+            "link", "reference-link", "image", "code", "preformatted-text", "code-block", "table", "datetime", "emoji", "html-entities", "pagebreak", "|",
+            "goto-line", "watch", "preview", "fullscreen", "clear", "search", "|",
+            "help", "info"
+        ],
+        simple : [
+            "undo", "redo", "|", 
+            "bold", "del", "italic", "quote", "uppercase", "lowercase", "|", 
+            "h1", "h2", "h3", "h4", "h5", "h6", "|", 
+            "list-ul", "list-ol", "hr", "|",
+            "watch", "preview", "fullscreen", "|",
+            "help", "info"
+        ],
+        mini : [
+            "undo", "redo", "|",
+            "watch", "preview", "|",
+            "help", "info"
+        ]
+    };
+    
+    editormd.defaults     = {
+        mode                 : "gfm",          //gfm or markdown
+        name                 : "",             // Form element name
+        value                : "",             // value for CodeMirror, if mode not gfm/markdown
+        theme                : "",             // Editor.md self themes, before v1.5.0 is CodeMirror theme, default empty
+        editorTheme          : "default",      // Editor area, this is CodeMirror theme at v1.5.0
+        previewTheme         : "",             // Preview area theme, default empty
+        markdown             : "",             // Markdown source code
+        appendMarkdown       : "",             // if in init textarea value not empty, append markdown to textarea
+        width                : "100%",
+        height               : "100%",
+        path                 : "./lib/",       // Dependents module file directory
+        pluginPath           : "",             // If this empty, default use settings.path + "../plugins/"
+        delay                : 300,            // Delay parse markdown to html, Uint : ms
+        autoLoadModules      : true,           // Automatic load dependent module files
+        watch                : true,
+        placeholder          : "Enjoy Markdown! coding now...",
+        gotoLine             : true,
+        codeFold             : false,
+        autoHeight           : false,
+		autoFocus            : true,
+        autoCloseTags        : true,
+        searchReplace        : true,
+        syncScrolling        : true,           // true | false | "single", default true
+        readOnly             : false,
+        tabSize              : 4,
+		indentUnit           : 4,
+        lineNumbers          : true,
+		lineWrapping         : true,
+		autoCloseBrackets    : true,
+		showTrailingSpace    : true,
+		matchBrackets        : true,
+		indentWithTabs       : true,
+		styleSelectedText    : true,
+        matchWordHighlight   : true,           // options: true, false, "onselected"
+        styleActiveLine      : true,           // Highlight the current line
+        dialogLockScreen     : true,
+        dialogShowMask       : true,
+        dialogDraggable      : true,
+        dialogMaskBgColor    : "#fff",
+        dialogMaskOpacity    : 0.1,
+        fontSize             : "13px",
+        saveHTMLToTextarea   : false,
+        disabledKeyMaps      : [],
+        
+        onload               : function() {},
+        onresize             : function() {},
+        onchange             : function() {},
+        onwatch              : null,
+        onunwatch            : null,
+        onpreviewing         : function() {},
+        onpreviewed          : function() {},
+        onfullscreen         : function() {},
+        onfullscreenExit     : function() {},
+        onscroll             : function() {},
+        onpreviewscroll      : function() {},
+        
+        imageUpload          : false,
+        imageFormats         : ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
+        imageUploadURL       : "",
+        crossDomainUpload    : false,
+        uploadCallbackURL    : "",
+        
+        toc                  : true,           // Table of contents
+        tocm                 : false,           // Using [TOCM], auto create ToC dropdown menu
+        tocTitle             : "",             // for ToC dropdown menu btn
+        tocDropdown          : false,
+        tocContainer         : "",
+        tocStartLevel        : 1,              // Said from H1 to create ToC
+        htmlDecode           : false,          // Open the HTML tag identification 
+        pageBreak            : true,           // Enable parse page break [========]
+        atLink               : true,           // for @link
+        emailLink            : true,           // for email address auto link
+        taskList             : false,          // Enable Github Flavored Markdown task lists
+        emoji                : true,          // :emoji: , Support Github emoji, Twitter Emoji (Twemoji);
+                                               // Support FontAwesome icon emoji :fa-xxx: > Using fontAwesome icon web fonts;
+                                               // Support Editor.md logo icon emoji :editormd-logo: :editormd-logo-1x: > 1~8x;
+        tex                  : false,          // TeX(LaTeX), based on KaTeX
+        flowChart            : false,          // flowChart.js only support IE9+
+        sequenceDiagram      : false,          // sequenceDiagram.js only support IE9+
+        previewCodeHighlight : true,
+                
+        toolbar              : true,           // show/hide toolbar
+        toolbarAutoFixed     : true,           // on window scroll auto fixed position
+        toolbarIcons         : "full",
+        toolbarTitles        : {},
+        toolbarHandlers      : {
+            ucwords : function() {
+                return editormd.toolbarHandlers.ucwords;
+            },
+            lowercase : function() {
+                return editormd.toolbarHandlers.lowercase;
+            }
+        },
+        toolbarCustomIcons   : {               // using html tag create toolbar icon, unused default <a> tag.
+            lowercase        : "<a href=\"javascript:;\" title=\"Lowercase\" unselectable=\"on\"><i class=\"fa\" name=\"lowercase\" style=\"font-size:24px;margin-top: -10px;\">a</i></a>",
+            "ucwords"        : "<a href=\"javascript:;\" title=\"ucwords\" unselectable=\"on\"><i class=\"fa\" name=\"ucwords\" style=\"font-size:20px;margin-top: -3px;\">Aa</i></a>"
+        }, 
+        toolbarIconsClass    : {
+            undo             : "fa-undo",
+            redo             : "fa-repeat",
+            bold             : "fa-bold",
+            del              : "fa-strikethrough",
+            italic           : "fa-italic",
+            quote            : "fa-quote-left",
+            uppercase        : "fa-font",
+            h1               : editormd.classPrefix + "bold",
+            h2               : editormd.classPrefix + "bold",
+            h3               : editormd.classPrefix + "bold",
+            h4               : editormd.classPrefix + "bold",
+            h5               : editormd.classPrefix + "bold",
+            h6               : editormd.classPrefix + "bold",
+            "list-ul"        : "fa-list-ul",
+            "list-ol"        : "fa-list-ol",
+            hr               : "fa-minus",
+            link             : "fa-link",
+            "reference-link" : "fa-anchor",
+            image            : "fa-picture-o",
+            code             : "fa-code",
+            "preformatted-text" : "fa-file-code-o",
+            "code-block"     : "fa-file-code-o",
+            table            : "fa-table",
+            datetime         : "fa-clock-o",
+            emoji            : "fa-smile-o",
+            "html-entities"  : "fa-copyright",
+            pagebreak        : "fa-newspaper-o",
+            "goto-line"      : "fa-terminal", // fa-crosshairs
+            watch            : "fa-eye-slash",
+            unwatch          : "fa-eye",
+            preview          : "fa-desktop",
+            search           : "fa-search",
+            fullscreen       : "fa-arrows-alt",
+            clear            : "fa-eraser",
+            help             : "fa-question-circle",
+            info             : "fa-info-circle"
+        },        
+        toolbarIconTexts     : {},
+        
+        lang : {
+            name        : "zh-cn",
+            description : "开源在线Markdown编辑器<br/>Open source online Markdown editor.",
+            tocTitle    : "目录",
+            toolbar     : {
+                undo             : "撤销(Ctrl+Z)",
+                redo             : "重做(Ctrl+Y)",
+                bold             : "粗体",
+                del              : "删除线",
+                italic           : "斜体",
+                quote            : "引用",
+                ucwords          : "将每个单词首字母转成大写",
+                uppercase        : "将所选转换成大写",
+                lowercase        : "将所选转换成小写",
+                h1               : "标题1",
+                h2               : "标题2",
+                h3               : "标题3",
+                h4               : "标题4",
+                h5               : "标题5",
+                h6               : "标题6",
+                "list-ul"        : "无序列表",
+                "list-ol"        : "有序列表",
+                hr               : "横线",
+                link             : "链接",
+                "reference-link" : "引用链接",
+                image            : "添加图片",
+                code             : "行内代码",
+                "preformatted-text" : "预格式文本 / 代码块(缩进风格)",
+                "code-block"     : "代码块(多语言风格)",
+                table            : "添加表格",
+                datetime         : "日期时间",
+                emoji            : "Emoji表情",
+                "html-entities"  : "HTML实体字符",
+                pagebreak        : "插入分页符",
+                "goto-line"      : "跳转到行",
+                watch            : "关闭实时预览",
+                unwatch          : "开启实时预览",
+                preview          : "全窗口预览HTML(按 Shift + ESC还原)",
+                fullscreen       : "全屏(按ESC还原)",
+                clear            : "清空",
+                search           : "搜索",
+                help             : "使用帮助",
+                info             : "关于" + editormd.title
+            },
+            buttons : {
+                enter  : "确定",
+                cancel : "取消",
+                close  : "关闭"
+            },
+            dialog : {
+                link : {
+                    title    : "添加链接",
+                    url      : "链接地址",
+                    urlTitle : "链接标题",
+                    urlEmpty : "错误:请填写链接地址。"
+                },
+                referenceLink : {
+                    title    : "添加引用链接",
+                    name     : "引用名称",
+                    url      : "链接地址",
+                    urlId    : "链接ID",
+                    urlTitle : "链接标题",
+                    nameEmpty: "错误:引用链接的名称不能为空。",
+                    idEmpty  : "错误:请填写引用链接的ID。",
+                    urlEmpty : "错误:请填写引用链接的URL地址。"
+                },
+                image : {
+                    title    : "添加图片",
+                    url      : "图片地址",
+                    link     : "图片链接",
+                    alt      : "图片描述",
+                    uploadButton     : "本地上传",
+                    imageURLEmpty    : "错误:图片地址不能为空。",
+                    uploadFileEmpty  : "错误:上传的图片不能为空。",
+                    formatNotAllowed : "错误:只允许上传图片文件,允许上传的图片文件格式有:"
+                },
+                preformattedText : {
+                    title             : "添加预格式文本或代码块", 
+                    emptyAlert        : "错误:请填写预格式文本或代码的内容。"
+                },
+                codeBlock : {
+                    title             : "添加代码块",                    
+                    selectLabel       : "代码语言:",
+                    selectDefaultText : "请选择代码语言",
+                    otherLanguage     : "其他语言",
+                    unselectedLanguageAlert : "错误:请选择代码所属的语言类型。",
+                    codeEmptyAlert    : "错误:请填写代码内容。"
+                },
+                htmlEntities : {
+                    title : "HTML 实体字符"
+                },
+                help : {
+                    title : "使用帮助"
+                }
+            }
+        }
+    };
+    
+    editormd.classNames  = {
+        tex : editormd.classPrefix + "tex"
+    };
+
+    editormd.dialogZindex = 99999;
+    
+    editormd.$katex       = null;
+    editormd.$marked      = null;
+    editormd.$CodeMirror  = null;
+    editormd.$prettyPrint = null;
+    
+    var timer, flowchartTimer;
+
+    editormd.prototype    = editormd.fn = {
+        state : {
+            watching   : false,
+            loaded     : false,
+            preview    : false,
+            fullscreen : false
+        },
+        
+        /**
+         * 构造函数/实例初始化
+         * Constructor / instance initialization
+         * 
+         * @param   {String}   id            编辑器的ID
+         * @param   {Object}   [options={}]  配置选项 Key/Value
+         * @returns {editormd}               返回editormd的实例对象
+         */
+        
+        init : function (id, options) {
+            
+            options              = options || {};
+            
+            if (typeof id === "object")
+            {
+                options = id;
+            }
+            
+            var _this            = this;
+            var classPrefix      = this.classPrefix  = editormd.classPrefix; 
+            var settings         = this.settings     = $.extend(true, editormd.defaults, options);
+            
+            id                   = (typeof id === "object") ? settings.id : id;
+            
+            var editor           = this.editor       = $("#" + id);
+            
+            this.id              = id;
+            this.lang            = settings.lang;
+            
+            var classNames       = this.classNames   = {
+                textarea : {
+                    html     : classPrefix + "html-textarea",
+                    markdown : classPrefix + "markdown-textarea"
+                }
+            };
+            
+            settings.pluginPath = (settings.pluginPath === "") ? settings.path + "../plugins/" : settings.pluginPath; 
+            
+            this.state.watching = (settings.watch) ? true : false;
+            
+            if ( !editor.hasClass("editormd") ) {
+                editor.addClass("editormd");
+            }
+            
+            editor.css({
+                width  : (typeof settings.width  === "number") ? settings.width  + "px" : settings.width,
+                height : (typeof settings.height === "number") ? settings.height + "px" : settings.height
+            });
+            
+            if (settings.autoHeight)
+            {
+                editor.css("height", "auto");
+            }
+                        
+            var markdownTextarea = this.markdownTextarea = editor.children("textarea");
+            
+            if (markdownTextarea.length < 1)
+            {
+                editor.append("<textarea></textarea>");
+                markdownTextarea = this.markdownTextarea = editor.children("textarea");
+            }
+            
+            markdownTextarea.addClass(classNames.textarea.markdown).attr("placeholder", settings.placeholder);
+            
+            if (typeof markdownTextarea.attr("name") === "undefined" || markdownTextarea.attr("name") === "")
+            {
+                markdownTextarea.attr("name", (settings.name !== "") ? settings.name : id + "-markdown-doc");
+            }
+            
+            var appendElements = [
+                (!settings.readOnly) ? "<a href=\"javascript:;\" class=\"fa fa-close " + classPrefix + "preview-close-btn\"></a>" : "",
+                ( (settings.saveHTMLToTextarea) ? "<textarea class=\"" + classNames.textarea.html + "\" name=\"" + id + "-html-code\"></textarea>" : "" ),
+                "<div class=\"" + classPrefix + "preview\"><div class=\"markdown-body " + classPrefix + "preview-container\"></div></div>",
+                "<div class=\"" + classPrefix + "container-mask\" style=\"display:block;\"></div>",
+                "<div class=\"" + classPrefix + "mask\"></div>"
+            ].join("\n");
+            
+            editor.append(appendElements).addClass(classPrefix + "vertical");
+            
+            if (settings.theme !== "") 
+            {
+                editor.addClass(classPrefix + "theme-" + settings.theme);
+            }
+            
+            this.mask          = editor.children("." + classPrefix + "mask");    
+            this.containerMask = editor.children("." + classPrefix  + "container-mask");
+            
+            if (settings.markdown !== "")
+            {
+                markdownTextarea.val(settings.markdown);
+            }
+            
+            if (settings.appendMarkdown !== "")
+            {
+                markdownTextarea.val(markdownTextarea.val() + settings.appendMarkdown);
+            }
+            
+            this.htmlTextarea     = editor.children("." + classNames.textarea.html);            
+            this.preview          = editor.children("." + classPrefix + "preview");
+            this.previewContainer = this.preview.children("." + classPrefix + "preview-container");
+            
+            if (settings.previewTheme !== "") 
+            {
+                this.preview.addClass(classPrefix + "preview-theme-" + settings.previewTheme);
+            }
+            
+            if (typeof define === "function" && define.amd)
+            {
+                if (typeof katex !== "undefined") 
+                {
+                    editormd.$katex = katex;
+                }
+                
+                if (settings.searchReplace && !settings.readOnly) 
+                {
+                    editormd.loadCSS(settings.path + "codemirror/addon/dialog/dialog");
+                    editormd.loadCSS(settings.path + "codemirror/addon/search/matchesonscrollbar");
+                }
+            }
+            
+            if ((typeof define === "function" && define.amd) || !settings.autoLoadModules)
+            {
+                if (typeof CodeMirror !== "undefined") {
+                    editormd.$CodeMirror = CodeMirror;
+                }
+                
+                if (typeof marked     !== "undefined") {
+                    editormd.$marked     = marked;
+                }
+                
+                this.setCodeMirror().setToolbar().loadedDisplay();
+            } 
+            else 
+            {
+                this.loadQueues();
+            }
+
+            return this;
+        },
+        
+        /**
+         * 所需组件加载队列
+         * Required components loading queue
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        loadQueues : function() {
+            var _this        = this;
+            var settings     = this.settings;
+            var loadPath     = settings.path;
+                                
+            var loadFlowChartOrSequenceDiagram = function() {
+                
+                if (editormd.isIE8) 
+                {
+                    _this.loadedDisplay();
+                    
+                    return ;
+                }
+
+                if (settings.flowChart || settings.sequenceDiagram) 
+                {
+                    editormd.loadScript(loadPath + "raphael.min", function() {
+
+                        editormd.loadScript(loadPath + "underscore.min", function() {  
+
+                            if (!settings.flowChart && settings.sequenceDiagram) 
+                            {
+                                editormd.loadScript(loadPath + "sequence-diagram.min", function() {
+                                    _this.loadedDisplay();
+                                });
+                            }
+                            else if (settings.flowChart && !settings.sequenceDiagram) 
+                            {      
+                                editormd.loadScript(loadPath + "flowchart.min", function() {  
+                                    editormd.loadScript(loadPath + "jquery.flowchart.min", function() {
+                                        _this.loadedDisplay();
+                                    });
+                                });
+                            }
+                            else if (settings.flowChart && settings.sequenceDiagram) 
+                            {  
+                                editormd.loadScript(loadPath + "flowchart.min", function() {  
+                                    editormd.loadScript(loadPath + "jquery.flowchart.min", function() {
+                                        editormd.loadScript(loadPath + "sequence-diagram.min", function() {
+                                            _this.loadedDisplay();
+                                        });
+                                    });
+                                });
+                            }
+                        });
+
+                    });
+                } 
+                else
+                {
+                    _this.loadedDisplay();
+                }
+            }; 
+
+            editormd.loadCSS(loadPath + "codemirror/codemirror.min");
+            
+            if (settings.searchReplace && !settings.readOnly)
+            {
+                editormd.loadCSS(loadPath + "codemirror/addon/dialog/dialog");
+                editormd.loadCSS(loadPath + "codemirror/addon/search/matchesonscrollbar");
+            }
+            
+            if (settings.codeFold)
+            {
+                editormd.loadCSS(loadPath + "codemirror/addon/fold/foldgutter");            
+            }
+            
+            editormd.loadScript(loadPath + "codemirror/codemirror.min", function() {
+                editormd.$CodeMirror = CodeMirror;
+                
+                editormd.loadScript(loadPath + "codemirror/modes.min", function() {
+                    
+                    editormd.loadScript(loadPath + "codemirror/addons.min", function() {
+                        
+                        _this.setCodeMirror();
+                        
+                        if (settings.mode !== "gfm" && settings.mode !== "markdown") 
+                        {
+                            _this.loadedDisplay();
+                            
+                            return false;
+                        }
+                        
+                        _this.setToolbar();
+
+                        editormd.loadScript(loadPath + "marked.min", function() {
+
+                            editormd.$marked = marked;
+                                
+                            if (settings.previewCodeHighlight) 
+                            {
+                                editormd.loadScript(loadPath + "prettify.min", function() {
+                                    loadFlowChartOrSequenceDiagram();
+                                });
+                            } 
+                            else
+                            {                  
+                                loadFlowChartOrSequenceDiagram();
+                            }
+                        });
+                        
+                    });
+                    
+                });
+                
+            });
+
+            return this;
+        },
+        
+        /**
+         * 设置 Editor.md 的整体主题,主要是工具栏
+         * Setting Editor.md theme
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setTheme : function(theme) {
+            var editor      = this.editor;
+            var oldTheme    = this.settings.theme;
+            var themePrefix = this.classPrefix + "theme-";
+            
+            editor.removeClass(themePrefix + oldTheme).addClass(themePrefix + theme);
+            
+            this.settings.theme = theme;
+            
+            return this;
+        },
+        
+        /**
+         * 设置 CodeMirror(编辑区)的主题
+         * Setting CodeMirror (Editor area) theme
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setEditorTheme : function(theme) {  
+            var settings   = this.settings;  
+            settings.editorTheme = theme;  
+            
+            if (theme !== "default")
+            {
+                editormd.loadCSS(settings.path + "codemirror/theme/" + settings.editorTheme);
+            }
+            
+            this.cm.setOption("theme", theme);
+            
+            return this;
+        },
+        
+        /**
+         * setEditorTheme() 的别名
+         * setEditorTheme() alias
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setCodeMirrorTheme : function (theme) {            
+            this.setEditorTheme(theme);
+            
+            return this;
+        },
+        
+        /**
+         * 设置 Editor.md 的主题
+         * Setting Editor.md theme
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setPreviewTheme : function(theme) {  
+            var preview     = this.preview;
+            var oldTheme    = this.settings.previewTheme;
+            var themePrefix = this.classPrefix + "preview-theme-";
+            
+            preview.removeClass(themePrefix + oldTheme).addClass(themePrefix + theme);
+            
+            this.settings.previewTheme = theme;
+            
+            return this;
+        },
+        
+        /**
+         * 配置和初始化CodeMirror组件
+         * CodeMirror initialization
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setCodeMirror : function() { 
+            var settings         = this.settings;
+            var editor           = this.editor;
+            
+            if (settings.editorTheme !== "default")
+            {
+                editormd.loadCSS(settings.path + "codemirror/theme/" + settings.editorTheme);
+            }
+            
+            var codeMirrorConfig = {
+                mode                      : settings.mode,
+                theme                     : settings.editorTheme,
+                tabSize                   : settings.tabSize,
+                dragDrop                  : false,
+                autofocus                 : settings.autoFocus,
+                autoCloseTags             : settings.autoCloseTags,
+                readOnly                  : (settings.readOnly) ? "nocursor" : false,
+                indentUnit                : settings.indentUnit,
+                lineNumbers               : settings.lineNumbers,
+                lineWrapping              : settings.lineWrapping,
+                extraKeys                 : {
+                                                "Ctrl-Q": function(cm) { 
+                                                    cm.foldCode(cm.getCursor()); 
+                                                }
+                                            },
+                foldGutter                : settings.codeFold,
+                gutters                   : ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
+                matchBrackets             : settings.matchBrackets,
+                indentWithTabs            : settings.indentWithTabs,
+                styleActiveLine           : settings.styleActiveLine,
+                styleSelectedText         : settings.styleSelectedText,
+                autoCloseBrackets         : settings.autoCloseBrackets,
+                showTrailingSpace         : settings.showTrailingSpace,
+                highlightSelectionMatches : ( (!settings.matchWordHighlight) ? false : { showToken: (settings.matchWordHighlight === "onselected") ? false : /\w/ } )
+            };
+            
+            this.codeEditor = this.cm        = editormd.$CodeMirror.fromTextArea(this.markdownTextarea[0], codeMirrorConfig);
+            this.codeMirror = this.cmElement = editor.children(".CodeMirror");
+            
+            if (settings.value !== "")
+            {
+                this.cm.setValue(settings.value);
+            }
+
+            this.codeMirror.css({
+                fontSize : settings.fontSize,
+                width    : (!settings.watch) ? "100%" : "50%"
+            });
+            
+            if (settings.autoHeight)
+            {
+                this.codeMirror.css("height", "auto");
+                this.cm.setOption("viewportMargin", Infinity);
+            }
+            
+            if (!settings.lineNumbers)
+            {
+                this.codeMirror.find(".CodeMirror-gutters").css("border-right", "none");
+            }
+
+            return this;
+        },
+        
+        /**
+         * 获取CodeMirror的配置选项
+         * Get CodeMirror setting options
+         * 
+         * @returns {Mixed}                  return CodeMirror setting option value
+         */
+        
+        getCodeMirrorOption : function(key) {            
+            return this.cm.getOption(key);
+        },
+        
+        /**
+         * 配置和重配置CodeMirror的选项
+         * CodeMirror setting options / resettings
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setCodeMirrorOption : function(key, value) {
+            
+            this.cm.setOption(key, value);
+            
+            return this;
+        },
+        
+        /**
+         * 添加 CodeMirror 键盘快捷键
+         * Add CodeMirror keyboard shortcuts key map
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        addKeyMap : function(map, bottom) {
+            this.cm.addKeyMap(map, bottom);
+            
+            return this;
+        },
+        
+        /**
+         * 移除 CodeMirror 键盘快捷键
+         * Remove CodeMirror keyboard shortcuts key map
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        removeKeyMap : function(map) {
+            this.cm.removeKeyMap(map);
+            
+            return this;
+        },
+        
+        /**
+         * 跳转到指定的行
+         * Goto CodeMirror line
+         * 
+         * @param   {String|Intiger}   line      line number or "first"|"last"
+         * @returns {editormd}                   返回editormd的实例对象
+         */
+        
+        gotoLine : function (line) {
+            
+            var settings = this.settings;
+            
+            if (!settings.gotoLine)
+            {
+                return this;
+            }
+            
+            var cm       = this.cm;
+            var editor   = this.editor;
+            var count    = cm.lineCount();
+            var preview  = this.preview;
+            
+            if (typeof line === "string")
+            {
+                if(line === "last")
+                {
+                    line = count;
+                }
+            
+                if (line === "first")
+                {
+                    line = 1;
+                }
+            }
+            
+            if (typeof line !== "number") 
+            {  
+                alert("Error: The line number must be an integer.");
+                return this;
+            }
+            
+            line  = parseInt(line) - 1;
+            
+            if (line > count)
+            {
+                alert("Error: The line number range 1-" + count);
+                
+                return this;
+            }
+            
+            cm.setCursor( {line : line, ch : 0} );
+            
+            var scrollInfo   = cm.getScrollInfo();
+            var clientHeight = scrollInfo.clientHeight; 
+            var coords       = cm.charCoords({line : line, ch : 0}, "local");
+            
+            cm.scrollTo(null, (coords.top + coords.bottom - clientHeight) / 2);
+            
+            if (settings.watch)
+            {            
+                var cmScroll  = this.codeMirror.find(".CodeMirror-scroll")[0];
+                var height    = $(cmScroll).height(); 
+                var scrollTop = cmScroll.scrollTop;         
+                var percent   = (scrollTop / cmScroll.scrollHeight);
+
+                if (scrollTop === 0)
+                {
+                    preview.scrollTop(0);
+                } 
+                else if (scrollTop + height >= cmScroll.scrollHeight - 16)
+                { 
+                    preview.scrollTop(preview[0].scrollHeight);                    
+                } 
+                else
+                {                    
+                    preview.scrollTop(preview[0].scrollHeight * percent);
+                }
+            }
+
+            cm.focus();
+            
+            return this;
+        },
+        
+        /**
+         * 扩展当前实例对象,可同时设置多个或者只设置一个
+         * Extend editormd instance object, can mutil setting.
+         * 
+         * @returns {editormd}                  this(editormd instance object.)
+         */
+        
+        extend : function() {
+            if (typeof arguments[1] !== "undefined")
+            {
+                if (typeof arguments[1] === "function")
+                {
+                    arguments[1] = $.proxy(arguments[1], this);
+                }
+
+                this[arguments[0]] = arguments[1];
+            }
+            
+            if (typeof arguments[0] === "object" && typeof arguments[0].length === "undefined")
+            {
+                $.extend(true, this, arguments[0]);
+            }
+
+            return this;
+        },
+        
+        /**
+         * 设置或扩展当前实例对象,单个设置
+         * Extend editormd instance object, one by one
+         * 
+         * @param   {String|Object}   key       option key
+         * @param   {String|Object}   value     option value
+         * @returns {editormd}                  this(editormd instance object.)
+         */
+        
+        set : function (key, value) {
+            
+            if (typeof value !== "undefined" && typeof value === "function")
+            {
+                value = $.proxy(value, this);
+            }
+            
+            this[key] = value;
+
+            return this;
+        },
+        
+        /**
+         * 重新配置
+         * Resetting editor options
+         * 
+         * @param   {String|Object}   key       option key
+         * @param   {String|Object}   value     option value
+         * @returns {editormd}                  this(editormd instance object.)
+         */
+        
+        config : function(key, value) {
+            var settings = this.settings;
+            
+            if (typeof key === "object")
+            {
+                settings = $.extend(true, settings, key);
+            }
+            
+            if (typeof key === "string")
+            {
+                settings[key] = value;
+            }
+            
+            this.settings = settings;
+            this.recreate();
+            
+            return this;
+        },
+        
+        /**
+         * 注册事件处理方法
+         * Bind editor event handle
+         * 
+         * @param   {String}     eventType      event type
+         * @param   {Function}   callback       回调函数
+         * @returns {editormd}                  this(editormd instance object.)
+         */
+        
+        on : function(eventType, callback) {
+            var settings = this.settings;
+            
+            if (typeof settings["on" + eventType] !== "undefined") 
+            {                
+                settings["on" + eventType] = $.proxy(callback, this);      
+            }
+
+            return this;
+        },
+        
+        /**
+         * 解除事件处理方法
+         * Unbind editor event handle
+         * 
+         * @param   {String}   eventType          event type
+         * @returns {editormd}                    this(editormd instance object.)
+         */
+        
+        off : function(eventType) {
+            var settings = this.settings;
+            
+            if (typeof settings["on" + eventType] !== "undefined") 
+            {
+                settings["on" + eventType] = function(){};
+            }
+            
+            return this;
+        },
+        
+        /**
+         * 显示工具栏
+         * Display toolbar
+         * 
+         * @param   {Function} [callback=function(){}] 回调函数
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        showToolbar : function(callback) {
+            var settings = this.settings;
+            
+            if(settings.readOnly) {
+                return this;
+            }
+            
+            if (settings.toolbar && (this.toolbar.length < 1 || this.toolbar.find("." + this.classPrefix + "menu").html() === "") )
+            {
+                this.setToolbar();
+            }
+            
+            settings.toolbar = true; 
+            
+            this.toolbar.show();
+            this.resize();
+            
+            $.proxy(callback || function(){}, this)();
+
+            return this;
+        },
+        
+        /**
+         * 隐藏工具栏
+         * Hide toolbar
+         * 
+         * @param   {Function} [callback=function(){}] 回调函数
+         * @returns {editormd}                         this(editormd instance object.)
+         */
+        
+        hideToolbar : function(callback) { 
+            var settings = this.settings;
+            
+            settings.toolbar = false;  
+            this.toolbar.hide();
+            this.resize();
+            
+            $.proxy(callback || function(){}, this)();
+
+            return this;
+        },
+        
+        /**
+         * 页面滚动时工具栏的固定定位
+         * Set toolbar in window scroll auto fixed position
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setToolbarAutoFixed : function(fixed) {
+            
+            var state    = this.state;
+            var editor   = this.editor;
+            var toolbar  = this.toolbar;
+            var settings = this.settings;
+            
+            if (typeof fixed !== "undefined")
+            {
+                settings.toolbarAutoFixed = fixed;
+            }
+            
+            var autoFixedHandle = function(){
+                var $window = $(window);
+                var top     = $window.scrollTop();
+                
+                if (!settings.toolbarAutoFixed)
+                {
+                    return false;
+                }
+
+                if (top - editor.offset().top > 10 && top < editor.height())
+                {
+                    toolbar.css({
+                        position : "fixed",
+                        width    : editor.width() + "px",
+                        left     : ($window.width() - editor.width()) / 2 + "px"
+                    });
+                }
+                else
+                {
+                    toolbar.css({
+                        position : "absolute",
+                        width    : "100%",
+                        left     : 0
+                    });
+                }
+            };
+            
+            if (!state.fullscreen && !state.preview && settings.toolbar && settings.toolbarAutoFixed)
+            {
+                $(window).bind("scroll", autoFixedHandle);
+            }
+
+            return this;
+        },
+        
+        /**
+         * 配置和初始化工具栏
+         * Set toolbar and Initialization
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setToolbar : function() {
+            var settings    = this.settings;  
+            
+            if(settings.readOnly) {
+                return this;
+            }
+            
+            var editor      = this.editor;
+            var preview     = this.preview;
+            var classPrefix = this.classPrefix;
+            
+            var toolbar     = this.toolbar = editor.children("." + classPrefix + "toolbar");
+            
+            if (settings.toolbar && toolbar.length < 1)
+            {            
+                var toolbarHTML = "<div class=\"" + classPrefix + "toolbar\"><div class=\"" + classPrefix + "toolbar-container\"><ul class=\"" + classPrefix + "menu\"></ul></div></div>";
+                
+                editor.append(toolbarHTML);
+                toolbar = this.toolbar = editor.children("." + classPrefix + "toolbar");
+            }
+            
+            if (!settings.toolbar) 
+            {
+                toolbar.hide();
+                
+                return this;
+            }
+            
+            toolbar.show();
+            
+            var icons       = (typeof settings.toolbarIcons === "function") ? settings.toolbarIcons() 
+                            : ((typeof settings.toolbarIcons === "string")  ? editormd.toolbarModes[settings.toolbarIcons] : settings.toolbarIcons);
+            
+            var toolbarMenu = toolbar.find("." + this.classPrefix + "menu"), menu = "";
+            var pullRight   = false;
+            
+            for (var i = 0, len = icons.length; i < len; i++)
+            {
+                var name = icons[i];
+
+                if (name === "||") 
+                { 
+                    pullRight = true;
+                } 
+                else if (name === "|")
+                {
+                    menu += "<li class=\"divider\" unselectable=\"on\">|</li>";
+                }
+                else
+                {
+                    var isHeader = (/h(\d)/.test(name));
+                    var index    = name;
+                    
+                    if (name === "watch" && !settings.watch) {
+                        index = "unwatch";
+                    }
+                    
+                    var title     = settings.lang.toolbar[index];
+                    var iconTexts = settings.toolbarIconTexts[index];
+                    var iconClass = settings.toolbarIconsClass[index];
+                    
+                    title     = (typeof title     === "undefined") ? "" : title;
+                    iconTexts = (typeof iconTexts === "undefined") ? "" : iconTexts;
+                    iconClass = (typeof iconClass === "undefined") ? "" : iconClass;
+
+                    var menuItem = pullRight ? "<li class=\"pull-right\">" : "<li>";
+                    
+                    if (typeof settings.toolbarCustomIcons[name] !== "undefined" && typeof settings.toolbarCustomIcons[name] !== "function")
+                    {
+                        menuItem += settings.toolbarCustomIcons[name];
+                    }
+                    else 
+                    {
+                        menuItem += "<a href=\"javascript:;\" title=\"" + title + "\" unselectable=\"on\">";
+                        menuItem += "<i class=\"fa " + iconClass + "\" name=\""+name+"\" unselectable=\"on\">"+((isHeader) ? name.toUpperCase() : ( (iconClass === "") ? iconTexts : "") ) + "</i>";
+                        menuItem += "</a>";
+                    }
+
+                    menuItem += "</li>";
+
+                    menu = pullRight ? menuItem + menu : menu + menuItem;
+                }
+            }
+
+            toolbarMenu.html(menu);
+            
+            toolbarMenu.find("[title=\"Lowercase\"]").attr("title", settings.lang.toolbar.lowercase);
+            toolbarMenu.find("[title=\"ucwords\"]").attr("title", settings.lang.toolbar.ucwords);
+            
+            this.setToolbarHandler();
+            this.setToolbarAutoFixed();
+
+            return this;
+        },
+        
+        /**
+         * 工具栏图标事件处理对象序列
+         * Get toolbar icons event handlers
+         * 
+         * @param   {Object}   cm    CodeMirror的实例对象
+         * @param   {String}   name  要获取的事件处理器名称
+         * @returns {Object}         返回处理对象序列
+         */
+            
+        dialogLockScreen : function() {
+            $.proxy(editormd.dialogLockScreen, this)();
+            
+            return this;
+        },
+
+        dialogShowMask : function(dialog) {
+            $.proxy(editormd.dialogShowMask, this)(dialog);
+            
+            return this;
+        },
+        
+        getToolbarHandles : function(name) {  
+            var toolbarHandlers = this.toolbarHandlers = editormd.toolbarHandlers;
+            
+            return (name && typeof toolbarIconHandlers[name] !== "undefined") ? toolbarHandlers[name] : toolbarHandlers;
+        },
+        
+        /**
+         * 工具栏图标事件处理器
+         * Bind toolbar icons event handle
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setToolbarHandler : function() {
+            var _this               = this;
+            var settings            = this.settings;
+            
+            if (!settings.toolbar || settings.readOnly) {
+                return this;
+            }
+            
+            var toolbar             = this.toolbar;
+            var cm                  = this.cm;
+            var classPrefix         = this.classPrefix;           
+            var toolbarIcons        = this.toolbarIcons = toolbar.find("." + classPrefix + "menu > li > a");  
+            var toolbarIconHandlers = this.getToolbarHandles();  
+                
+            toolbarIcons.bind(editormd.mouseOrTouch("click", "touchend"), function(event) {
+
+                var icon                = $(this).children(".fa");
+                var name                = icon.attr("name");
+                var cursor              = cm.getCursor();
+                var selection           = cm.getSelection();
+
+                if (name === "") {
+                    return ;
+                }
+                
+                _this.activeIcon = icon;
+
+                if (typeof toolbarIconHandlers[name] !== "undefined") 
+                {
+                    $.proxy(toolbarIconHandlers[name], _this)(cm);
+                }
+                else 
+                {
+                    if (typeof settings.toolbarHandlers[name] !== "undefined") 
+                    {
+                        $.proxy(settings.toolbarHandlers[name], _this)(cm, icon, cursor, selection);
+                    }
+                }
+                
+                if (name !== "link" && name !== "reference-link" && name !== "image" && name !== "code-block" && 
+                    name !== "preformatted-text" && name !== "watch" && name !== "preview" && name !== "search" && name !== "fullscreen" && name !== "info") 
+                {
+                    cm.focus();
+                }
+
+                return false;
+
+            });
+
+            return this;
+        },
+        
+        /**
+         * 动态创建对话框
+         * Creating custom dialogs
+         * 
+         * @param   {Object} options  配置项键值对 Key/Value
+         * @returns {dialog}          返回创建的dialog的jQuery实例对象
+         */
+        
+        createDialog : function(options) {            
+            return $.proxy(editormd.createDialog, this)(options);
+        },
+        
+        /**
+         * 创建关于Editor.md的对话框
+         * Create about Editor.md dialog
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        createInfoDialog : function() {
+            var _this        = this;
+			var editor       = this.editor;
+            var classPrefix  = this.classPrefix;  
+            
+            var infoDialogHTML = [
+                "<div class=\"" + classPrefix + "dialog " + classPrefix + "dialog-info\" style=\"\">",
+                "<div class=\"" + classPrefix + "dialog-container\">",
+                "<h1><i class=\"editormd-logo editormd-logo-lg editormd-logo-color\"></i> " + editormd.title + "<small>v" + editormd.version + "</small></h1>",
+                "<p>" + this.lang.description + "</p>",
+                "<p style=\"margin: 10px 0 20px 0;\"><a href=\"" + editormd.homePage + "\" target=\"_blank\">" + editormd.homePage + " <i class=\"fa fa-external-link\"></i></a></p>",
+                "<p style=\"font-size: 0.85em;\">Copyright &copy; 2015 <a href=\"https://github.com/pandao\" target=\"_blank\" class=\"hover-link\">Pandao</a>, The <a href=\"https://github.com/pandao/editor.md/blob/master/LICENSE\" target=\"_blank\" class=\"hover-link\">MIT</a> License.</p>",
+                "</div>",
+                "<a href=\"javascript:;\" class=\"fa fa-close " + classPrefix + "dialog-close\"></a>",
+                "</div>"
+            ].join("\n");
+
+            editor.append(infoDialogHTML);
+            
+            var infoDialog  = this.infoDialog = editor.children("." + classPrefix + "dialog-info");
+
+            infoDialog.find("." + classPrefix + "dialog-close").bind(editormd.mouseOrTouch("click", "touchend"), function() {
+                _this.hideInfoDialog();
+            });
+            
+            infoDialog.css("border", (editormd.isIE8) ? "1px solid #ddd" : "").css("z-index", editormd.dialogZindex).show();
+            
+            this.infoDialogPosition();
+
+            return this;
+        },
+        
+        /**
+         * 关于Editor.md对话居中定位
+         * Editor.md dialog position handle
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        infoDialogPosition : function() {
+            var infoDialog = this.infoDialog;
+            
+			var _infoDialogPosition = function() {
+				infoDialog.css({
+					top  : ($(window).height() - infoDialog.height()) / 2 + "px",
+					left : ($(window).width()  - infoDialog.width()) / 2  + "px"
+				});
+			};
+
+			_infoDialogPosition();
+
+			$(window).resize(_infoDialogPosition);
+            
+            return this;
+        },
+        
+        /**
+         * 显示关于Editor.md
+         * Display about Editor.md dialog
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        showInfoDialog : function() {
+
+            $("html,body").css("overflow-x", "hidden");
+            
+            var _this       = this;
+			var editor      = this.editor;
+            var settings    = this.settings;         
+			var infoDialog  = this.infoDialog = editor.children("." + this.classPrefix + "dialog-info");
+            
+            if (infoDialog.length < 1)
+            {
+                this.createInfoDialog();
+            }
+            
+            this.lockScreen(true);
+            
+            this.mask.css({
+						opacity         : settings.dialogMaskOpacity,
+						backgroundColor : settings.dialogMaskBgColor
+					}).show();
+
+			infoDialog.css("z-index", editormd.dialogZindex).show();
+
+			this.infoDialogPosition();
+
+            return this;
+        },
+        
+        /**
+         * 隐藏关于Editor.md
+         * Hide about Editor.md dialog
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        hideInfoDialog : function() {            
+            $("html,body").css("overflow-x", "");
+            this.infoDialog.hide();
+            this.mask.hide();
+            this.lockScreen(false);
+
+            return this;
+        },
+        
+        /**
+         * 锁屏
+         * lock screen
+         * 
+         * @param   {Boolean}    lock    Boolean 布尔值,是否锁屏
+         * @returns {editormd}           返回editormd的实例对象
+         */
+        
+        lockScreen : function(lock) {
+            editormd.lockScreen(lock);
+            this.resize();
+
+            return this;
+        },
+        
+        /**
+         * 编辑器界面重建,用于动态语言包或模块加载等
+         * Recreate editor
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        recreate : function() {
+            var _this            = this;
+            var editor           = this.editor;
+            var settings         = this.settings;
+            
+            this.codeMirror.remove();
+            
+            this.setCodeMirror();
+
+            if (!settings.readOnly) 
+            {
+                if (editor.find(".editormd-dialog").length > 0) {
+                    editor.find(".editormd-dialog").remove();
+                }
+                
+                if (settings.toolbar) 
+                {  
+                    this.getToolbarHandles();                  
+                    this.setToolbar();
+                }
+            }
+            
+            this.loadedDisplay(true);
+
+            return this;
+        },
+        
+        /**
+         * 高亮预览HTML的pre代码部分
+         * highlight of preview codes
+         * 
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        previewCodeHighlight : function() {    
+            var settings         = this.settings;
+            var previewContainer = this.previewContainer;
+            
+            if (settings.previewCodeHighlight) 
+            {
+                previewContainer.find("pre").addClass("prettyprint linenums");
+                
+                if (typeof prettyPrint !== "undefined")
+                {                    
+                    prettyPrint();
+                }
+            }
+
+            return this;
+        },
+        
+        /**
+         * 解析TeX(KaTeX)科学公式
+         * TeX(KaTeX) Renderer
+         * 
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        katexRender : function() {
+            
+            if (timer === null)
+            {
+                return this;
+            }
+            
+            this.previewContainer.find("." + editormd.classNames.tex).each(function(){
+                var tex  = $(this);
+                editormd.$katex.render(tex.text(), tex[0]);
+                
+                tex.find(".katex").css("font-size", "1.6em");
+            });   
+
+            return this;
+        },
+        
+        /**
+         * 解析和渲染流程图及时序图
+         * FlowChart and SequenceDiagram Renderer
+         * 
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        flowChartAndSequenceDiagramRender : function() {
+            var $this            = this;
+            var settings         = this.settings;
+            var previewContainer = this.previewContainer;
+            
+            if (editormd.isIE8) {
+                return this;
+            }
+
+            if (settings.flowChart) {
+                if (flowchartTimer === null) {
+                    return this;
+                }
+                
+                previewContainer.find(".flowchart").flowChart(); 
+            }
+
+            if (settings.sequenceDiagram) {
+                previewContainer.find(".sequence-diagram").sequenceDiagram({theme: "simple"});
+            }
+                    
+            var preview    = $this.preview;
+            var codeMirror = $this.codeMirror;
+            var codeView   = codeMirror.find(".CodeMirror-scroll");
+
+            var height    = codeView.height();
+            var scrollTop = codeView.scrollTop();                    
+            var percent   = (scrollTop / codeView[0].scrollHeight);
+            var tocHeight = 0;
+
+            preview.find(".markdown-toc-list").each(function(){
+                tocHeight += $(this).height();
+            });
+
+            var tocMenuHeight = preview.find(".editormd-toc-menu").height(); 
+            tocMenuHeight = (!tocMenuHeight) ? 0 : tocMenuHeight;
+
+            if (scrollTop === 0) 
+            {
+                preview.scrollTop(0);
+            } 
+            else if (scrollTop + height >= codeView[0].scrollHeight - 16)
+            { 
+                preview.scrollTop(preview[0].scrollHeight);                        
+            } 
+            else
+            {                  
+                preview.scrollTop((preview[0].scrollHeight + tocHeight + tocMenuHeight) * percent);
+            }
+
+            return this;
+        },
+        
+        /**
+         * 注册键盘快捷键处理
+         * Register CodeMirror keyMaps (keyboard shortcuts).
+         * 
+         * @param   {Object}    keyMap      KeyMap key/value {"(Ctrl/Shift/Alt)-Key" : function(){}}
+         * @returns {editormd}              return this
+         */
+        
+        registerKeyMaps : function(keyMap) {
+            
+            var _this           = this;
+            var cm              = this.cm;
+            var settings        = this.settings;
+            var toolbarHandlers = editormd.toolbarHandlers;
+            var disabledKeyMaps = settings.disabledKeyMaps;
+            
+            keyMap              = keyMap || null;
+            
+            if (keyMap)
+            {
+                for (var i in keyMap)
+                {
+                    if ($.inArray(i, disabledKeyMaps) < 0)
+                    {
+                        var map = {};
+                        map[i]  = keyMap[i];
+
+                        cm.addKeyMap(keyMap);
+                    }
+                }
+            }
+            else
+            {
+                for (var k in editormd.keyMaps)
+                {
+                    var _keyMap = editormd.keyMaps[k];
+                    var handle = (typeof _keyMap === "string") ? $.proxy(toolbarHandlers[_keyMap], _this) : $.proxy(_keyMap, _this);
+                    
+                    if ($.inArray(k, ["F9", "F10", "F11"]) < 0 && $.inArray(k, disabledKeyMaps) < 0)
+                    {
+                        var _map = {};
+                        _map[k] = handle;
+
+                        cm.addKeyMap(_map);
+                    }
+                }
+                
+                $(window).keydown(function(event) {
+                    
+                    var keymaps = {
+                        "120" : "F9",
+                        "121" : "F10",
+                        "122" : "F11"
+                    };
+                    
+                    if ( $.inArray(keymaps[event.keyCode], disabledKeyMaps) < 0 )
+                    {
+                        switch (event.keyCode)
+                        {
+                            case 120:
+                                    $.proxy(toolbarHandlers["watch"], _this)();
+                                    return false;
+                                break;
+                                
+                            case 121:
+                                    $.proxy(toolbarHandlers["preview"], _this)();
+                                    return false;
+                                break;
+                                
+                            case 122:
+                                    $.proxy(toolbarHandlers["fullscreen"], _this)();                        
+                                    return false;
+                                break;
+                                
+                            default:
+                                break;
+                        }
+                    }
+                });
+            }
+
+            return this;
+        },
+        
+        /**
+         * 绑定同步滚动
+         * 
+         * @returns {editormd} return this
+         */
+        
+        bindScrollEvent : function() {
+            
+            var _this            = this;
+            var preview          = this.preview;
+            var settings         = this.settings;
+            var codeMirror       = this.codeMirror;
+            var mouseOrTouch     = editormd.mouseOrTouch;
+            
+            if (!settings.syncScrolling) {
+                return this;
+            }
+                
+            var cmBindScroll = function() {    
+                codeMirror.find(".CodeMirror-scroll").bind(mouseOrTouch("scroll", "touchmove"), function(event) {
+                    var height    = $(this).height();
+                    var scrollTop = $(this).scrollTop();                    
+                    var percent   = (scrollTop / $(this)[0].scrollHeight);
+                    
+                    var tocHeight = 0;
+                    
+                    preview.find(".markdown-toc-list").each(function(){
+                        tocHeight += $(this).height();
+                    });
+                    
+                    var tocMenuHeight = preview.find(".editormd-toc-menu").height();
+                    tocMenuHeight = (!tocMenuHeight) ? 0 : tocMenuHeight;
+
+                    if (scrollTop === 0) 
+                    {
+                        preview.scrollTop(0);
+                    } 
+                    else if (scrollTop + height >= $(this)[0].scrollHeight - 16)
+                    { 
+                        preview.scrollTop(preview[0].scrollHeight);                        
+                    } 
+                    else
+                    {
+                        preview.scrollTop((preview[0].scrollHeight  + tocHeight + tocMenuHeight) * percent);
+                    }
+                    
+                    $.proxy(settings.onscroll, _this)(event);
+                });
+            };
+
+            var cmUnbindScroll = function() {
+                codeMirror.find(".CodeMirror-scroll").unbind(mouseOrTouch("scroll", "touchmove"));
+            };
+
+            var previewBindScroll = function() {
+                
+                preview.bind(mouseOrTouch("scroll", "touchmove"), function(event) {
+                    var height    = $(this).height();
+                    var scrollTop = $(this).scrollTop();         
+                    var percent   = (scrollTop / $(this)[0].scrollHeight);
+                    var codeView  = codeMirror.find(".CodeMirror-scroll");
+
+                    if(scrollTop === 0) 
+                    {
+                        codeView.scrollTop(0);
+                    }
+                    else if (scrollTop + height >= $(this)[0].scrollHeight)
+                    {
+                        codeView.scrollTop(codeView[0].scrollHeight);                        
+                    }
+                    else 
+                    {
+                        codeView.scrollTop(codeView[0].scrollHeight * percent);
+                    }
+                    
+                    $.proxy(settings.onpreviewscroll, _this)(event);
+                });
+
+            };
+
+            var previewUnbindScroll = function() {
+                preview.unbind(mouseOrTouch("scroll", "touchmove"));
+            }; 
+
+			codeMirror.bind({
+				mouseover  : cmBindScroll,
+				mouseout   : cmUnbindScroll,
+				touchstart : cmBindScroll,
+				touchend   : cmUnbindScroll
+			});
+            
+            if (settings.syncScrolling === "single") {
+                return this;
+            }
+            
+			preview.bind({
+				mouseover  : previewBindScroll,
+				mouseout   : previewUnbindScroll,
+				touchstart : previewBindScroll,
+				touchend   : previewUnbindScroll
+			});
+
+            return this;
+        },
+        
+        bindChangeEvent : function() {
+            
+            var _this            = this;
+            var cm               = this.cm;
+            var settings         = this.settings;
+            
+            if (!settings.syncScrolling) {
+                return this;
+            }
+            
+            cm.on("change", function(_cm, changeObj) {
+                
+                if (settings.watch)
+                {
+                    _this.previewContainer.css("padding", settings.autoHeight ? "20px 20px 50px 40px" : "20px");
+                }
+                
+                timer = setTimeout(function() {
+                    clearTimeout(timer);
+                    _this.save();
+                    timer = null;
+                }, settings.delay);
+            });
+
+            return this;
+        },
+        
+        /**
+         * 加载队列完成之后的显示处理
+         * Display handle of the module queues loaded after.
+         * 
+         * @param   {Boolean}   recreate   是否为重建编辑器
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        loadedDisplay : function(recreate) {
+            
+            recreate             = recreate || false;
+            
+            var _this            = this;
+            var editor           = this.editor;
+            var preview          = this.preview;
+            var settings         = this.settings;
+            
+            this.containerMask.hide();
+            
+            this.save();
+            
+            if (settings.watch) {
+                preview.show();
+            }
+            
+            editor.data("oldWidth", editor.width()).data("oldHeight", editor.height()); // 为了兼容Zepto
+            
+            this.resize();
+            this.registerKeyMaps();
+            
+            $(window).resize(function(){
+                _this.resize();
+            });
+            
+            this.bindScrollEvent().bindChangeEvent();
+            
+            if (!recreate)
+            {
+                $.proxy(settings.onload, this)();
+            }
+            
+            this.state.loaded = true;
+
+            return this;
+        },
+        
+        /**
+         * 设置编辑器的宽度
+         * Set editor width
+         * 
+         * @param   {Number|String} width  编辑器宽度值
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        width : function(width) {
+                
+            this.editor.css("width", (typeof width === "number") ? width  + "px" : width);            
+            this.resize();
+            
+            return this;
+        },
+        
+        /**
+         * 设置编辑器的高度
+         * Set editor height
+         * 
+         * @param   {Number|String} height  编辑器高度值
+         * @returns {editormd}              返回editormd的实例对象
+         */
+        
+        height : function(height) {
+                
+            this.editor.css("height", (typeof height === "number")  ? height  + "px" : height);            
+            this.resize();
+            
+            return this;
+        },
+        
+        /**
+         * 调整编辑器的尺寸和布局
+         * Resize editor layout
+         * 
+         * @param   {Number|String} [width=null]  编辑器宽度值
+         * @param   {Number|String} [height=null] 编辑器高度值
+         * @returns {editormd}                    返回editormd的实例对象
+         */
+        
+        resize : function(width, height) {
+            
+            width  = width  || null;
+            height = height || null;
+            
+            var state      = this.state;
+            var editor     = this.editor;
+            var preview    = this.preview;
+            var toolbar    = this.toolbar;
+            var settings   = this.settings;
+            var codeMirror = this.codeMirror;
+            
+            if (width)
+            {
+                editor.css("width", (typeof width  === "number") ? width  + "px" : width);
+            }
+            
+            if (settings.autoHeight && !state.fullscreen && !state.preview)
+            {
+                editor.css("height", "auto");
+                codeMirror.css("height", "auto");
+            } 
+            else 
+            {
+                if (height) 
+                {
+                    editor.css("height", (typeof height === "number") ? height + "px" : height);
+                }
+                
+                if (state.fullscreen)
+                {
+                    editor.height($(window).height());
+                }
+
+                if (settings.toolbar && !settings.readOnly) 
+                {
+                    codeMirror.css("margin-top", toolbar.height() + 1).height(editor.height() - toolbar.height());
+                } 
+                else
+                {
+                    codeMirror.css("margin-top", 0).height(editor.height());
+                }
+            }
+            
+            if(settings.watch) 
+            {
+                codeMirror.width(editor.width() / 2);
+                preview.width((!state.preview) ? editor.width() / 2 : editor.width());
+                
+                this.previewContainer.css("padding", settings.autoHeight ? "20px 20px 50px 40px" : "20px");
+                
+                if (settings.toolbar && !settings.readOnly) 
+                {
+                    preview.css("top", toolbar.height() + 1);
+                } 
+                else 
+                {
+                    preview.css("top", 0);
+                }
+                
+                if (settings.autoHeight && !state.fullscreen && !state.preview)
+                {
+                    preview.height("");
+                }
+                else
+                {                
+                    var previewHeight = (settings.toolbar && !settings.readOnly) ? editor.height() - toolbar.height() : editor.height();
+                    
+                    preview.height(previewHeight);
+                }
+            } 
+            else 
+            {
+                codeMirror.width(editor.width());
+                preview.hide();
+            }
+            
+            if (state.loaded) 
+            {
+                $.proxy(settings.onresize, this)();
+            }
+
+            return this;
+        },
+        
+        /**
+         * 解析和保存Markdown代码
+         * Parse & Saving Markdown source code
+         * 
+         * @returns {editormd}     返回editormd的实例对象
+         */
+        
+        save : function() {
+            
+            if (timer === null)
+            {
+                return this;
+            }
+            
+            var _this            = this;
+            var state            = this.state;
+            var settings         = this.settings;
+            var cm               = this.cm;            
+            var cmValue          = cm.getValue();
+            var previewContainer = this.previewContainer;
+
+            if (settings.mode !== "gfm" && settings.mode !== "markdown") 
+            {
+                this.markdownTextarea.val(cmValue);
+                
+                return this;
+            }
+            
+            var marked          = editormd.$marked;
+            var markdownToC     = this.markdownToC = [];            
+            var rendererOptions = this.markedRendererOptions = {  
+                toc                  : settings.toc,
+                tocm                 : settings.tocm,
+                tocStartLevel        : settings.tocStartLevel,
+                pageBreak            : settings.pageBreak,
+                taskList             : settings.taskList,
+                emoji                : settings.emoji,
+                tex                  : settings.tex,
+                atLink               : settings.atLink,           // for @link
+                emailLink            : settings.emailLink,        // for mail address auto link
+                flowChart            : settings.flowChart,
+                sequenceDiagram      : settings.sequenceDiagram,
+                previewCodeHighlight : settings.previewCodeHighlight,
+            };
+            
+            var markedOptions = this.markedOptions = {
+                renderer    : editormd.markedRenderer(markdownToC, rendererOptions),
+                gfm         : true,
+                tables      : true,
+                breaks      : true,
+                pedantic    : false,
+                sanitize    : (settings.htmlDecode) ? false : true,  // 关闭忽略HTML标签,即开启识别HTML标签,默认为false
+                smartLists  : true,
+                smartypants : true
+            };
+            
+            marked.setOptions(markedOptions);
+                    
+            var newMarkdownDoc = editormd.$marked(cmValue, markedOptions);
+            
+            //console.info("cmValue", cmValue, newMarkdownDoc);
+            
+            newMarkdownDoc = editormd.filterHTMLTags(newMarkdownDoc, settings.htmlDecode);
+            
+            //console.error("cmValue", cmValue, newMarkdownDoc);
+            
+            this.markdownTextarea.text(cmValue);
+            
+            cm.save();
+            
+            if (settings.saveHTMLToTextarea) 
+            {
+                this.htmlTextarea.text(newMarkdownDoc);
+            }
+            
+            if(settings.watch || (!settings.watch && state.preview))
+            {
+                previewContainer.html(newMarkdownDoc);
+
+                this.previewCodeHighlight();
+                
+                if (settings.toc) 
+                {
+                    var tocContainer = (settings.tocContainer === "") ? previewContainer : $(settings.tocContainer);
+                    var tocMenu      = tocContainer.find("." + this.classPrefix + "toc-menu");
+                    
+                    tocContainer.attr("previewContainer", (settings.tocContainer === "") ? "true" : "false");
+                    
+                    if (settings.tocContainer !== "" && tocMenu.length > 0)
+                    {
+                        tocMenu.remove();
+                    }
+                    
+                    editormd.markdownToCRenderer(markdownToC, tocContainer, settings.tocDropdown, settings.tocStartLevel);
+            
+                    if (settings.tocDropdown || tocContainer.find("." + this.classPrefix + "toc-menu").length > 0)
+                    {
+                        editormd.tocDropdownMenu(tocContainer, (settings.tocTitle !== "") ? settings.tocTitle : this.lang.tocTitle);
+                    }
+            
+                    if (settings.tocContainer !== "")
+                    {
+                        previewContainer.find(".markdown-toc").css("border", "none");
+                    }
+                }
+                
+                if (settings.tex)
+                {
+                    if (!editormd.kaTeXLoaded && settings.autoLoadModules) 
+                    {
+                        editormd.loadKaTeX(function() {
+                            editormd.$katex = katex;
+                            editormd.kaTeXLoaded = true;
+                            _this.katexRender();
+                        });
+                    } 
+                    else 
+                    {
+                        editormd.$katex = katex;
+                        this.katexRender();
+                    }
+                }                
+                
+                if (settings.flowChart || settings.sequenceDiagram)
+                {
+                    flowchartTimer = setTimeout(function(){
+                        clearTimeout(flowchartTimer);
+                        _this.flowChartAndSequenceDiagramRender();
+                        flowchartTimer = null;
+                    }, 10);
+                }
+
+                if (state.loaded) 
+                {
+                    $.proxy(settings.onchange, this)();
+                }
+            }
+
+            return this;
+        },
+        
+        /**
+         * 聚焦光标位置
+         * Focusing the cursor position
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        focus : function() {
+            this.cm.focus();
+
+            return this;
+        },
+        
+        /**
+         * 设置光标的位置
+         * Set cursor position
+         * 
+         * @param   {Object}    cursor 要设置的光标位置键值对象,例:{line:1, ch:0}
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        setCursor : function(cursor) {
+            this.cm.setCursor(cursor);
+
+            return this;
+        },
+        
+        /**
+         * 获取当前光标的位置
+         * Get the current position of the cursor
+         * 
+         * @returns {Cursor}         返回一个光标Cursor对象
+         */
+        
+        getCursor : function() {
+            return this.cm.getCursor();
+        },
+        
+        /**
+         * 设置光标选中的范围
+         * Set cursor selected ranges
+         * 
+         * @param   {Object}    from   开始位置的光标键值对象,例:{line:1, ch:0}
+         * @param   {Object}    to     结束位置的光标键值对象,例:{line:1, ch:0}
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        setSelection : function(from, to) {
+        
+            this.cm.setSelection(from, to);
+        
+            return this;
+        },
+        
+        /**
+         * 获取光标选中的文本
+         * Get the texts from cursor selected
+         * 
+         * @returns {String}         返回选中文本的字符串形式
+         */
+        
+        getSelection : function() {
+            return this.cm.getSelection();
+        },
+        
+        /**
+         * 设置光标选中的文本范围
+         * Set the cursor selection ranges
+         * 
+         * @param   {Array}    ranges  cursor selection ranges array
+         * @returns {Array}            return this
+         */
+        
+        setSelections : function(ranges) {
+            this.cm.setSelections(ranges);
+            
+            return this;
+        },
+        
+        /**
+         * 获取光标选中的文本范围
+         * Get the cursor selection ranges
+         * 
+         * @returns {Array}         return selection ranges array
+         */
+        
+        getSelections : function() {
+            return this.cm.getSelections();
+        },
+        
+        /**
+         * 替换当前光标选中的文本或在当前光标处插入新字符
+         * Replace the text at the current cursor selected or insert a new character at the current cursor position
+         * 
+         * @param   {String}    value  要插入的字符值
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        replaceSelection : function(value) {
+            this.cm.replaceSelection(value);
+
+            return this;
+        },
+        
+        /**
+         * 在当前光标处插入新字符
+         * Insert a new character at the current cursor position
+         *
+         * 同replaceSelection()方法
+         * With the replaceSelection() method
+         * 
+         * @param   {String}    value  要插入的字符值
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        insertValue : function(value) {
+            this.replaceSelection(value);
+
+            return this;
+        },
+        
+        /**
+         * 追加markdown
+         * append Markdown to editor
+         * 
+         * @param   {String}    md     要追加的markdown源文档
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        appendMarkdown : function(md) {
+            var settings = this.settings;
+            var cm       = this.cm;
+            
+            cm.setValue(cm.getValue() + md);
+            
+            return this;
+        },
+        
+        /**
+         * 设置和传入编辑器的markdown源文档
+         * Set Markdown source document
+         * 
+         * @param   {String}    md     要传入的markdown源文档
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        setMarkdown : function(md) {
+            this.cm.setValue(md || this.settings.markdown);
+            
+            return this;
+        },
+        
+        /**
+         * 获取编辑器的markdown源文档
+         * Set Editor.md markdown/CodeMirror value
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        getMarkdown : function() {
+            return this.cm.getValue();
+        },
+        
+        /**
+         * 获取编辑器的源文档
+         * Get CodeMirror value
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        getValue : function() {
+            return this.cm.getValue();
+        },
+        
+        /**
+         * 设置编辑器的源文档
+         * Set CodeMirror value
+         * 
+         * @param   {String}     value   set code/value/string/text
+         * @returns {editormd}           返回editormd的实例对象
+         */
+        
+        setValue : function(value) {
+            this.cm.setValue(value);
+            
+            return this;
+        },
+        
+        /**
+         * 清空编辑器
+         * Empty CodeMirror editor container
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        clear : function() {
+            this.cm.setValue("");
+            
+            return this;
+        },
+        
+        /**
+         * 获取解析后存放在Textarea的HTML源码
+         * Get parsed html code from Textarea
+         * 
+         * @returns {String}               返回HTML源码
+         */
+        
+        getHTML : function() {
+            if (!this.settings.saveHTMLToTextarea)
+            {
+                alert("Error: settings.saveHTMLToTextarea == false");
+
+                return false;
+            }
+            
+            return this.htmlTextarea.val();
+        },
+        
+        /**
+         * getHTML()的别名
+         * getHTML (alias)
+         * 
+         * @returns {String}           Return html code 返回HTML源码
+         */
+        
+        getTextareaSavedHTML : function() {
+            return this.getHTML();
+        },
+        
+        /**
+         * 获取预览窗口的HTML源码
+         * Get html from preview container
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        getPreviewedHTML : function() {
+            if (!this.settings.watch)
+            {
+                alert("Error: settings.watch == false");
+
+                return false;
+            }
+            
+            return this.previewContainer.html();
+        },
+        
+        /**
+         * 开启实时预览
+         * Enable real-time watching
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        watch : function(callback) {     
+            var settings        = this.settings;
+            
+            if ($.inArray(settings.mode, ["gfm", "markdown"]) < 0)
+            {
+                return this;
+            }
+            
+            this.state.watching = settings.watch = true;
+            this.preview.show();
+            
+            if (this.toolbar)
+            {
+                var watchIcon   = settings.toolbarIconsClass.watch;
+                var unWatchIcon = settings.toolbarIconsClass.unwatch;
+                
+                var icon        = this.toolbar.find(".fa[name=watch]");
+                icon.parent().attr("title", settings.lang.toolbar.watch);
+                icon.removeClass(unWatchIcon).addClass(watchIcon);
+            }
+            
+            this.codeMirror.css("border-right", "1px solid #ddd").width(this.editor.width() / 2); 
+            
+            timer = 0;
+            
+            this.save().resize();
+            
+            if (!settings.onwatch)
+            {
+                settings.onwatch = callback || function() {};
+            }
+            
+            $.proxy(settings.onwatch, this)();
+            
+            return this;
+        },
+        
+        /**
+         * 关闭实时预览
+         * Disable real-time watching
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        unwatch : function(callback) {
+            var settings        = this.settings;
+            this.state.watching = settings.watch = false;
+            this.preview.hide();
+            
+            if (this.toolbar) 
+            {
+                var watchIcon   = settings.toolbarIconsClass.watch;
+                var unWatchIcon = settings.toolbarIconsClass.unwatch;
+                
+                var icon    = this.toolbar.find(".fa[name=watch]");
+                icon.parent().attr("title", settings.lang.toolbar.unwatch);
+                icon.removeClass(watchIcon).addClass(unWatchIcon);
+            }
+            
+            this.codeMirror.css("border-right", "none").width(this.editor.width());
+            
+            this.resize();
+            
+            if (!settings.onunwatch)
+            {
+                settings.onunwatch = callback || function() {};
+            }
+            
+            $.proxy(settings.onunwatch, this)();
+            
+            return this;
+        },
+        
+        /**
+         * 显示编辑器
+         * Show editor
+         * 
+         * @param   {Function} [callback=function()] 回调函数
+         * @returns {editormd}                       返回editormd的实例对象
+         */
+        
+        show : function(callback) {
+            callback  = callback || function() {};
+            
+            var _this = this;
+            this.editor.show(0, function() {
+                $.proxy(callback, _this)();
+            });
+            
+            return this;
+        },
+        
+        /**
+         * 隐藏编辑器
+         * Hide editor
+         * 
+         * @param   {Function} [callback=function()] 回调函数
+         * @returns {editormd}                       返回editormd的实例对象
+         */
+        
+        hide : function(callback) {
+            callback  = callback || function() {};
+            
+            var _this = this;
+            this.editor.hide(0, function() {
+                $.proxy(callback, _this)();
+            });
+            
+            return this;
+        },
+        
+        /**
+         * 隐藏编辑器部分,只预览HTML
+         * Enter preview html state
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        previewing : function() {
+            
+            var _this            = this;
+            var editor           = this.editor;
+            var preview          = this.preview;
+            var toolbar          = this.toolbar;
+            var settings         = this.settings;
+            var codeMirror       = this.codeMirror;
+            var previewContainer = this.previewContainer;
+            
+            if ($.inArray(settings.mode, ["gfm", "markdown"]) < 0) {
+                return this;
+            }
+            
+            if (settings.toolbar && toolbar) {
+                toolbar.toggle();
+                toolbar.find(".fa[name=preview]").toggleClass("active");
+            }
+            
+            codeMirror.toggle();
+            
+            var escHandle = function(event) {
+                if (event.shiftKey && event.keyCode === 27) {
+                    _this.previewed();
+                }
+            };
+
+            if (codeMirror.css("display") === "none") // 为了兼容Zepto,而不使用codeMirror.is(":hidden")
+            {
+                this.state.preview = true;
+
+                if (this.state.fullscreen) {
+                    preview.css("background", "#fff");
+                }
+                
+                editor.find("." + this.classPrefix + "preview-close-btn").show().bind(editormd.mouseOrTouch("click", "touchend"), function(){
+                    _this.previewed();
+                });
+            
+                if (!settings.watch)
+                {
+                    this.save();
+                } 
+                else 
+                {
+                    previewContainer.css("padding", "");
+                }
+                
+                previewContainer.addClass(this.classPrefix + "preview-active");
+
+                preview.show().css({
+                    position  : "",
+                    top       : 0,
+                    width     : editor.width(),
+                    height    : (settings.autoHeight && !this.state.fullscreen) ? "auto" : editor.height()
+                });
+                
+                if (this.state.loaded)
+                {
+                    $.proxy(settings.onpreviewing, this)();
+                }
+
+                $(window).bind("keyup", escHandle);
+            } 
+            else 
+            {
+                $(window).unbind("keyup", escHandle);
+                this.previewed();
+            }
+        },
+        
+        /**
+         * 显示编辑器部分,退出只预览HTML
+         * Exit preview html state
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        previewed : function() {
+            
+            var editor           = this.editor;
+            var preview          = this.preview;
+            var toolbar          = this.toolbar;
+            var settings         = this.settings;
+            var previewContainer = this.previewContainer;
+            var previewCloseBtn  = editor.find("." + this.classPrefix + "preview-close-btn");
+
+            this.state.preview   = false;
+            
+            this.codeMirror.show();
+            
+            if (settings.toolbar) {
+                toolbar.show();
+            }
+            
+            preview[(settings.watch) ? "show" : "hide"]();
+            
+            previewCloseBtn.hide().unbind(editormd.mouseOrTouch("click", "touchend"));
+                
+            previewContainer.removeClass(this.classPrefix + "preview-active");
+                
+            if (settings.watch)
+            {
+                previewContainer.css("padding", "20px");
+            }
+            
+            preview.css({ 
+                background : null,
+                position   : "absolute",
+                width      : editor.width() / 2,
+                height     : (settings.autoHeight && !this.state.fullscreen) ? "auto" : editor.height() - toolbar.height(),
+                top        : (settings.toolbar)    ? toolbar.height() : 0
+            });
+
+            if (this.state.loaded)
+            {
+                $.proxy(settings.onpreviewed, this)();
+            }
+            
+            return this;
+        },
+        
+        /**
+         * 编辑器全屏显示
+         * Fullscreen show
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        fullscreen : function() {
+            
+            var _this            = this;
+            var state            = this.state;
+            var editor           = this.editor;
+            var preview          = this.preview;
+            var toolbar          = this.toolbar;
+            var settings         = this.settings;
+            var fullscreenClass  = this.classPrefix + "fullscreen";
+            
+            if (toolbar) {
+                toolbar.find(".fa[name=fullscreen]").parent().toggleClass("active"); 
+            }
+            
+            var escHandle = function(event) {
+                if (!event.shiftKey && event.keyCode === 27) 
+                {
+                    if (state.fullscreen)
+                    {
+                        _this.fullscreenExit();
+                    }
+                }
+            };
+
+            if (!editor.hasClass(fullscreenClass)) 
+            {
+                state.fullscreen = true;
+
+                $("html,body").css("overflow", "hidden");
+                
+                editor.css({
+                    width    : $(window).width(),
+                    height   : $(window).height()
+                }).addClass(fullscreenClass);
+
+                this.resize();
+    
+                $.proxy(settings.onfullscreen, this)();
+
+                $(window).bind("keyup", escHandle);
+            }
+            else
+            {           
+                $(window).unbind("keyup", escHandle); 
+                this.fullscreenExit();
+            }
+
+            return this;
+        },
+        
+        /**
+         * 编辑器退出全屏显示
+         * Exit fullscreen state
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        fullscreenExit : function() {
+            
+            var editor            = this.editor;
+            var settings          = this.settings;
+            var toolbar           = this.toolbar;
+            var fullscreenClass   = this.classPrefix + "fullscreen";  
+            
+            this.state.fullscreen = false;
+            
+            if (toolbar) {
+                toolbar.find(".fa[name=fullscreen]").parent().removeClass("active"); 
+            }
+
+            $("html,body").css("overflow", "");
+
+            editor.css({
+                width    : editor.data("oldWidth"),
+                height   : editor.data("oldHeight")
+            }).removeClass(fullscreenClass);
+
+            this.resize();
+            
+            $.proxy(settings.onfullscreenExit, this)();
+
+            return this;
+        },
+        
+        /**
+         * 加载并执行插件
+         * Load and execute the plugin
+         * 
+         * @param   {String}     name    plugin name / function name
+         * @param   {String}     path    plugin load path
+         * @returns {editormd}           返回editormd的实例对象
+         */
+        
+        executePlugin : function(name, path) {
+            
+            var _this    = this;
+            var cm       = this.cm;
+            var settings = this.settings;
+            
+            path = settings.pluginPath + path;
+            
+            if (typeof define === "function") 
+            {            
+                if (typeof this[name] === "undefined")
+                {
+                    alert("Error: " + name + " plugin is not found, you are not load this plugin.");
+                    
+                    return this;
+                }
+                
+                this[name](cm);
+                
+                return this;
+            }
+            
+            if ($.inArray(path, editormd.loadFiles.plugin) < 0)
+            {
+                editormd.loadPlugin(path, function() {
+                    editormd.loadPlugins[name] = _this[name];
+                    _this[name](cm);
+                });
+            }
+            else
+            {
+                $.proxy(editormd.loadPlugins[name], this)(cm);
+            }
+            
+            return this;
+        },
+                
+        /**
+         * 搜索替换
+         * Search & replace
+         * 
+         * @param   {String}     command    CodeMirror serach commands, "find, fintNext, fintPrev, clearSearch, replace, replaceAll"
+         * @returns {editormd}              return this
+         */
+        
+        search : function(command) {
+            var settings = this.settings;
+            
+            if (!settings.searchReplace)
+            {
+                alert("Error: settings.searchReplace == false");
+                return this;
+            }
+            
+            if (!settings.readOnly)
+            {
+                this.cm.execCommand(command || "find");
+            }
+            
+            return this;
+        },
+        
+        searchReplace : function() {            
+            this.search("replace");
+            
+            return this;
+        },
+        
+        searchReplaceAll : function() {          
+            this.search("replaceAll");
+            
+            return this;
+        }
+    };
+    
+    editormd.fn.init.prototype = editormd.fn; 
+   
+    /**
+     * 锁屏
+     * lock screen when dialog opening
+     * 
+     * @returns {void}
+     */
+
+    editormd.dialogLockScreen = function() {
+        var settings = this.settings || {dialogLockScreen : true};
+        
+        if (settings.dialogLockScreen) 
+        {            
+            $("html,body").css("overflow", "hidden");
+            this.resize();
+        }
+    };
+   
+    /**
+     * 显示透明背景层
+     * Display mask layer when dialog opening
+     * 
+     * @param   {Object}     dialog    dialog jQuery object
+     * @returns {void}
+     */
+    
+    editormd.dialogShowMask = function(dialog) {
+        var editor   = this.editor;
+        var settings = this.settings || {dialogShowMask : true};
+        
+        dialog.css({
+            top  : ($(window).height() - dialog.height()) / 2 + "px",
+            left : ($(window).width()  - dialog.width())  / 2 + "px"
+        });
+
+        if (settings.dialogShowMask) {
+            editor.children("." + this.classPrefix + "mask").css("z-index", parseInt(dialog.css("z-index")) - 1).show();
+        }
+    };
+
+    editormd.toolbarHandlers = {
+        undo : function() {
+            this.cm.undo();
+        },
+        
+        redo : function() {
+            this.cm.redo();
+        },
+        
+        bold : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("**" + selection + "**");
+
+            if(selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 2);
+            }
+        },
+        
+        del : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("~~" + selection + "~~");
+
+            if(selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 2);
+            }
+        },
+
+        italic : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("*" + selection + "*");
+
+            if(selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+
+        quote : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (cursor.ch !== 0)
+            {
+                cm.setCursor(cursor.line, 0);
+                cm.replaceSelection("> " + selection);
+                cm.setCursor(cursor.line, cursor.ch + 2);
+            }
+            else
+            {
+                cm.replaceSelection("> " + selection);
+            }
+
+            //cm.replaceSelection("> " + selection);
+            //cm.setCursor(cursor.line, (selection === "") ? cursor.ch + 2 : cursor.ch + selection.length + 2);
+        },
+        
+        ucfirst : function() {
+            var cm         = this.cm;
+            var selection  = cm.getSelection();
+            var selections = cm.listSelections();
+
+            cm.replaceSelection(editormd.firstUpperCase(selection));
+            cm.setSelections(selections);
+        },
+        
+        ucwords : function() {
+            var cm         = this.cm;
+            var selection  = cm.getSelection();
+            var selections = cm.listSelections();
+
+            cm.replaceSelection(editormd.wordsFirstUpperCase(selection));
+            cm.setSelections(selections);
+        },
+        
+        uppercase : function() {
+            var cm         = this.cm;
+            var selection  = cm.getSelection();
+            var selections = cm.listSelections();
+
+            cm.replaceSelection(selection.toUpperCase());
+            cm.setSelections(selections);
+        },
+        
+        lowercase : function() {
+            var cm         = this.cm;
+            var cursor     = cm.getCursor();
+            var selection  = cm.getSelection();
+            var selections = cm.listSelections();
+            
+            cm.replaceSelection(selection.toLowerCase());
+            cm.setSelections(selections);
+        },
+
+        h1 : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (cursor.ch !== 0)
+            {
+                cm.setCursor(cursor.line, 0);
+                cm.replaceSelection("# " + selection);
+                cm.setCursor(cursor.line, cursor.ch + 2);
+            }
+            else
+            {
+                cm.replaceSelection("# " + selection);
+            }
+        },
+
+        h2 : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (cursor.ch !== 0)
+            {
+                cm.setCursor(cursor.line, 0);
+                cm.replaceSelection("## " + selection);
+                cm.setCursor(cursor.line, cursor.ch + 3);
+            }
+            else
+            {
+                cm.replaceSelection("## " + selection);
+            }
+        },
+
+        h3 : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (cursor.ch !== 0)
+            {
+                cm.setCursor(cursor.line, 0);
+                cm.replaceSelection("### " + selection);
+                cm.setCursor(cursor.line, cursor.ch + 4);
+            }
+            else
+            {
+                cm.replaceSelection("### " + selection);
+            }
+        },
+
+        h4 : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (cursor.ch !== 0)
+            {
+                cm.setCursor(cursor.line, 0);
+                cm.replaceSelection("#### " + selection);
+                cm.setCursor(cursor.line, cursor.ch + 5);
+            }
+            else
+            {
+                cm.replaceSelection("#### " + selection);
+            }
+        },
+
+        h5 : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (cursor.ch !== 0)
+            {
+                cm.setCursor(cursor.line, 0);
+                cm.replaceSelection("##### " + selection);
+                cm.setCursor(cursor.line, cursor.ch + 6);
+            }
+            else
+            {
+                cm.replaceSelection("##### " + selection);
+            }
+        },
+
+        h6 : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (cursor.ch !== 0)
+            {
+                cm.setCursor(cursor.line, 0);
+                cm.replaceSelection("###### " + selection);
+                cm.setCursor(cursor.line, cursor.ch + 7);
+            }
+            else
+            {
+                cm.replaceSelection("###### " + selection);
+            }
+        },
+
+        "list-ul" : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (selection === "") 
+            {
+                cm.replaceSelection("- " + selection);
+            } 
+            else 
+            {
+                var selectionText = selection.split("\n");
+
+                for (var i = 0, len = selectionText.length; i < len; i++) 
+                {
+                    selectionText[i] = (selectionText[i] === "") ? "" : "- " + selectionText[i];
+                }
+
+                cm.replaceSelection(selectionText.join("\n"));
+            }
+        },
+
+        "list-ol" : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if(selection === "") 
+            {
+                cm.replaceSelection("1. " + selection);
+            }
+            else
+            {
+                var selectionText = selection.split("\n");
+
+                for (var i = 0, len = selectionText.length; i < len; i++) 
+                {
+                    selectionText[i] = (selectionText[i] === "") ? "" : (i+1) + ". " + selectionText[i];
+                }
+
+                cm.replaceSelection(selectionText.join("\n"));
+            }
+        },
+
+        hr : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection(((cursor.ch !== 0) ? "\n\n" : "\n") + "------------\n\n");
+        },
+
+        tex : function() {
+            if (!this.settings.tex)
+            {
+                alert("settings.tex === false");
+                return this;
+            }
+            
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("$$" + selection + "$$");
+
+            if(selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 2);
+            }
+        },
+
+        link : function() {
+            this.executePlugin("linkDialog", "link-dialog/link-dialog");
+        },
+
+        "reference-link" : function() {
+            this.executePlugin("referenceLinkDialog", "reference-link-dialog/reference-link-dialog");
+        },
+
+        pagebreak : function() {
+            if (!this.settings.pageBreak)
+            {
+                alert("settings.pageBreak === false");
+                return this;
+            }
+            
+            var cm        = this.cm;
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("\r\n[========]\r\n");
+        },
+
+        image : function() {
+            this.executePlugin("imageDialog", "image-dialog/image-dialog");
+        },
+        
+        code : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("`" + selection + "`");
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+
+        "code-block" : function() {
+            this.executePlugin("codeBlockDialog", "code-block-dialog/code-block-dialog");            
+        },
+
+        "preformatted-text" : function() {
+            this.executePlugin("preformattedTextDialog", "preformatted-text-dialog/preformatted-text-dialog");
+        },
+        
+        table : function() {
+            this.executePlugin("tableDialog", "table-dialog/table-dialog");         
+        },
+        
+        datetime : function() {
+            var cm        = this.cm;
+            var selection = cm.getSelection();
+            var date      = new Date();
+            var langName  = this.settings.lang.name;
+            var datefmt   = editormd.dateFormat() + " " + editormd.dateFormat((langName === "zh-cn" || langName === "zh-tw") ? "cn-week-day" : "week-day");
+
+            cm.replaceSelection(datefmt);
+        },
+        
+        emoji : function() {
+            this.executePlugin("emojiDialog", "emoji-dialog/emoji-dialog");
+        },
+                
+        "html-entities" : function() {
+            this.executePlugin("htmlEntitiesDialog", "html-entities-dialog/html-entities-dialog");
+        },
+                
+        "goto-line" : function() {
+            this.executePlugin("gotoLineDialog", "goto-line-dialog/goto-line-dialog");
+        },
+
+        watch : function() {    
+            this[this.settings.watch ? "unwatch" : "watch"]();
+        },
+
+        preview : function() {
+            this.previewing();
+        },
+
+        fullscreen : function() {
+            this.fullscreen();
+        },
+
+        clear : function() {
+            this.clear();
+        },
+        
+        search : function() {
+            this.search();
+        },
+
+        help : function() {
+            this.executePlugin("helpDialog", "help-dialog/help-dialog");
+        },
+
+        info : function() {
+            this.showInfoDialog();
+        }
+    };
+    
+    editormd.keyMaps = {
+        "Ctrl-1"       : "h1",
+        "Ctrl-2"       : "h2",
+        "Ctrl-3"       : "h3",
+        "Ctrl-4"       : "h4",
+        "Ctrl-5"       : "h5",
+        "Ctrl-6"       : "h6",
+        "Ctrl-B"       : "bold",  // if this is string ==  editormd.toolbarHandlers.xxxx
+        "Ctrl-D"       : "datetime",
+        
+        "Ctrl-E"       : function() { // emoji
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            if (!this.settings.emoji)
+            {
+                alert("Error: settings.emoji == false");
+                return ;
+            }
+
+            cm.replaceSelection(":" + selection + ":");
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+        "Ctrl-Alt-G"   : "goto-line",
+        "Ctrl-H"       : "hr",
+        "Ctrl-I"       : "italic",
+        "Ctrl-K"       : "code",
+        
+        "Ctrl-L"        : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            var title = (selection === "") ? "" : " \""+selection+"\"";
+
+            cm.replaceSelection("[" + selection + "]("+title+")");
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+        "Ctrl-U"         : "list-ul",
+        
+        "Shift-Ctrl-A"   : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            if (!this.settings.atLink)
+            {
+                alert("Error: settings.atLink == false");
+                return ;
+            }
+
+            cm.replaceSelection("@" + selection);
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+        
+        "Shift-Ctrl-C"     : "code",
+        "Shift-Ctrl-Q"     : "quote",
+        "Shift-Ctrl-S"     : "del",
+        "Shift-Ctrl-K"     : "tex",  // KaTeX
+        
+        "Shift-Alt-C"      : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            cm.replaceSelection(["```", selection, "```"].join("\n"));
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 3);
+            } 
+        },
+        
+        "Shift-Ctrl-Alt-C" : "code-block",
+        "Shift-Ctrl-H"     : "html-entities",
+        "Shift-Alt-H"      : "help",
+        "Shift-Ctrl-E"     : "emoji",
+        "Shift-Ctrl-U"     : "uppercase",
+        "Shift-Alt-U"      : "ucwords",
+        "Shift-Ctrl-Alt-U" : "ucfirst",
+        "Shift-Alt-L"      : "lowercase",
+        
+        "Shift-Ctrl-I"     : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            var title = (selection === "") ? "" : " \""+selection+"\"";
+
+            cm.replaceSelection("![" + selection + "]("+title+")");
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 4);
+            }
+        },
+        
+        "Shift-Ctrl-Alt-I" : "image",
+        "Shift-Ctrl-L"     : "link",
+        "Shift-Ctrl-O"     : "list-ol",
+        "Shift-Ctrl-P"     : "preformatted-text",
+        "Shift-Ctrl-T"     : "table",
+        "Shift-Alt-P"      : "pagebreak",
+        "F9"               : "watch",
+        "F10"              : "preview",
+        "F11"              : "fullscreen",
+    };
+    
+    /**
+     * 清除字符串两边的空格
+     * Clear the space of strings both sides.
+     * 
+     * @param   {String}    str            string
+     * @returns {String}                   trimed string    
+     */
+    
+    var trim = function(str) {
+        return (!String.prototype.trim) ? str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "") : str.trim();
+    };
+    
+    editormd.trim = trim;
+    
+    /**
+     * 所有单词首字母大写
+     * Words first to uppercase
+     * 
+     * @param   {String}    str            string
+     * @returns {String}                   string
+     */
+    
+    var ucwords = function (str) {
+        return str.toLowerCase().replace(/\b(\w)|\s(\w)/g, function($1) {  
+            return $1.toUpperCase();
+        });
+    };
+    
+    editormd.ucwords = editormd.wordsFirstUpperCase = ucwords;
+    
+    /**
+     * 字符串首字母大写
+     * Only string first char to uppercase
+     * 
+     * @param   {String}    str            string
+     * @returns {String}                   string
+     */
+    
+    var firstUpperCase = function(str) {        
+        return str.toLowerCase().replace(/\b(\w)/, function($1){
+            return $1.toUpperCase();
+        });
+    };
+    
+    var ucfirst = firstUpperCase;
+    
+    editormd.firstUpperCase = editormd.ucfirst = firstUpperCase;
+    
+    editormd.urls = {
+        atLinkBase : "https://github.com/"
+    };
+    
+    editormd.regexs = {
+        atLink        : /@(\w+)/g,
+        email         : /(\w+)@(\w+)\.(\w+)\.?(\w+)?/g,
+        emailLink     : /(mailto:)?([\w\.\_]+)@(\w+)\.(\w+)\.?(\w+)?/g,
+        emoji         : /:([\w\+-]+):/g,
+        emojiDatetime : /(\d{2}:\d{2}:\d{2})/g,
+        twemoji       : /:(tw-([\w]+)-?(\w+)?):/g,
+        fontAwesome   : /:(fa-([\w]+)(-(\w+)){0,}):/g,
+        editormdLogo  : /:(editormd-logo-?(\w+)?):/g,
+        pageBreak     : /^\[[=]{8,}\]$/
+    };
+
+    // Emoji graphics files url path
+    editormd.emoji     = {
+        path  : "http://www.emoji-cheat-sheet.com/graphics/emojis/",
+        ext   : ".png"
+    };
+
+    // Twitter Emoji (Twemoji)  graphics files url path    
+    editormd.twemoji = {
+        path : "http://twemoji.maxcdn.com/36x36/",
+        ext  : ".png"
+    };
+
+    /**
+     * 自定义marked的解析器
+     * Custom Marked renderer rules
+     * 
+     * @param   {Array}    markdownToC     传入用于接收TOC的数组
+     * @returns {Renderer} markedRenderer  返回marked的Renderer自定义对象
+     */
+
+    editormd.markedRenderer = function(markdownToC, options) {
+        var defaults = {
+            toc                  : true,           // Table of contents
+            tocm                 : false,
+            tocStartLevel        : 1,              // Said from H1 to create ToC  
+            pageBreak            : true,
+            atLink               : true,           // for @link
+            emailLink            : true,           // for mail address auto link
+            taskList             : false,          // Enable Github Flavored Markdown task lists
+            emoji                : true,          // :emoji: , Support Twemoji, fontAwesome, Editor.md logo emojis.
+            tex                  : false,          // TeX(LaTeX), based on KaTeX
+            flowChart            : false,          // flowChart.js only support IE9+
+            sequenceDiagram      : false,          // sequenceDiagram.js only support IE9+
+        };
+        
+        var settings        = $.extend(defaults, options || {});    
+        var marked          = editormd.$marked;
+        var markedRenderer  = new marked.Renderer();
+        markdownToC         = markdownToC || [];        
+            
+        var regexs          = editormd.regexs;
+        var atLinkReg       = regexs.atLink;
+        var emojiReg        = regexs.emoji;
+        var emailReg        = regexs.email;
+        var emailLinkReg    = regexs.emailLink;
+        var twemojiReg      = regexs.twemoji;
+        var faIconReg       = regexs.fontAwesome;
+        var editormdLogoReg = regexs.editormdLogo;
+        var pageBreakReg    = regexs.pageBreak;
+
+        markedRenderer.emoji = function(text) {
+            
+            text = text.replace(editormd.regexs.emojiDatetime, function($1) {           
+                return $1.replace(/:/g, "&#58;");
+            });
+            
+            var matchs = text.match(emojiReg);
+
+            if (!matchs || !settings.emoji) {
+                return text;
+            }
+
+            for (var i = 0, len = matchs.length; i < len; i++)
+            {            
+                if (matchs[i] === ":+1:") {
+                    matchs[i] = ":\\+1:";
+                }
+
+                text = text.replace(new RegExp(matchs[i]), function($1, $2){
+                    var faMatchs = $1.match(faIconReg);
+                    var name     = $1.replace(/:/g, "");
+
+                    if (faMatchs)
+                    {                        
+                        for (var fa = 0, len1 = faMatchs.length; fa < len1; fa++)
+                        {
+                            var faName = faMatchs[fa].replace(/:/g, "");
+                            
+                            return "<i class=\"fa " + faName + " fa-emoji\" title=\"" + faName.replace("fa-", "") + "\"></i>";
+                        }
+                    }
+                    else
+                    {
+                        var emdlogoMathcs = $1.match(editormdLogoReg);
+                        var twemojiMatchs = $1.match(twemojiReg);
+
+                        if (emdlogoMathcs)                                        
+                        {                            
+                            for (var x = 0, len2 = emdlogoMathcs.length; x < len2; x++)
+                            {
+                                var logoName = emdlogoMathcs[x].replace(/:/g, "");
+                                return "<i class=\"" + logoName + "\" title=\"Editor.md logo (" + logoName + ")\"></i>";
+                            }
+                        }
+                        else if (twemojiMatchs) 
+                        {
+                            for (var t = 0, len3 = twemojiMatchs.length; t < len3; t++)
+                            {
+                                var twe = twemojiMatchs[t].replace(/:/g, "").replace("tw-", "");
+                                return "<img src=\"" + editormd.twemoji.path + twe + editormd.twemoji.ext + "\" title=\"twemoji-" + twe + "\" alt=\"twemoji-" + twe + "\" class=\"emoji twemoji\" />";
+                            }
+                        }
+                        else
+                        {
+                            var src = (name === "+1") ? "plus1" : name;
+                            src     = (src === "black_large_square") ? "black_square" : src;
+                            src     = (src === "moon") ? "waxing_gibbous_moon" : src;
+
+                            return "<img src=\"" + editormd.emoji.path + src + editormd.emoji.ext + "\" class=\"emoji\" title=\"&#58;" + name + "&#58;\" alt=\"&#58;" + name + "&#58;\" />";
+                        }
+                    }
+                });
+            }
+
+            return text;
+        };
+
+        markedRenderer.atLink = function(text) {
+
+            if (atLinkReg.test(text))
+            { 
+                if (settings.atLink) 
+                {
+                    text = text.replace(emailReg, function($1, $2, $3, $4) {
+                        return $1.replace(/@/g, "_#_&#64;_#_");
+                    });
+
+                    text = text.replace(atLinkReg, function($1, $2) {
+                        return "<a href=\"" + editormd.urls.atLinkBase + "" + $2 + "\" title=\"&#64;" + $2 + "\" class=\"at-link\">" + $1 + "</a>";
+                    }).replace(/_#_&#64;_#_/g, "@");
+                }
+                
+                if (settings.emailLink)
+                {
+                    text = text.replace(emailLinkReg, function($1, $2, $3, $4, $5) {
+                        return (!$2 && $.inArray($5, "jpg|jpeg|png|gif|webp|ico|icon|pdf".split("|")) < 0) ? "<a href=\"mailto:" + $1 + "\">"+$1+"</a>" : $1;
+                    });
+                }
+
+                return text;
+            }
+
+            return text;
+        };
+                
+        markedRenderer.link = function (href, title, text) {
+
+            if (this.options.sanitize) {
+                try {
+                    var prot = decodeURIComponent(unescape(href)).replace(/[^\w:]/g,"").toLowerCase();
+                } catch(e) {
+                    return "";
+                }
+
+                if (prot.indexOf("javascript:") === 0) {
+                    return "";
+                }
+            }
+
+            var out = "<a href=\"" + href + "\"";
+            
+            if (atLinkReg.test(title) || atLinkReg.test(text))
+            {
+                if (title)
+                {
+                    out += " title=\"" + title.replace(/@/g, "&#64;");
+                }
+                
+                return out + "\">" + text.replace(/@/g, "&#64;") + "</a>";
+            }
+
+            if (title) {
+                out += " title=\"" + title + "\"";
+            }
+
+            out += ">" + text + "</a>";
+
+            return out;
+        };
+        
+        markedRenderer.heading = function(text, level, raw) {
+                    
+            var linkText       = text;
+            var hasLinkReg     = /\s*\<a\s*href\=\"(.*)\"\s*([^\>]*)\>(.*)\<\/a\>\s*/;
+            var getLinkTextReg = /\s*\<a\s*([^\>]+)\>([^\>]*)\<\/a\>\s*/g;
+
+            if (hasLinkReg.test(text)) 
+            {
+                var tempText = [];
+                text         = text.split(/\<a\s*([^\>]+)\>([^\>]*)\<\/a\>/);
+
+                for (var i = 0, len = text.length; i < len; i++)
+                {
+                    tempText.push(text[i].replace(/\s*href\=\"(.*)\"\s*/g, ""));
+                }
+
+                text = tempText.join(" ");
+            }
+            
+            text = trim(text);
+            
+            var escapedText    = text.toLowerCase().replace(/[^\w]+/g, "-");
+            var toc = {
+                text  : text,
+                level : level,
+                slug  : escapedText
+            };
+            
+            var isChinese = /^[\u4e00-\u9fa5]+$/.test(text);
+            var id        = (isChinese) ? escape(text).replace(/\%/g, "") : text.toLowerCase().replace(/[^\w]+/g, "-");
+
+            markdownToC.push(toc);
+            
+            var headingHTML = "<h" + level + " id=\"h"+ level + "-" + this.options.headerPrefix + id +"\">";
+            
+            headingHTML    += "<a name=\"" + text + "\" class=\"reference-link\"></a>";
+            headingHTML    += "<span class=\"header-link octicon octicon-link\"></span>";
+            headingHTML    += (hasLinkReg) ? this.atLink(this.emoji(linkText)) : this.atLink(this.emoji(text));
+            headingHTML    += "</h" + level + ">";
+
+            return headingHTML;
+        };
+        
+        markedRenderer.pageBreak = function(text) {
+            if (pageBreakReg.test(text) && settings.pageBreak)
+            {
+                text = "<hr style=\"page-break-after:always;\" class=\"page-break editormd-page-break\" />";
+            }
+            
+            return text;
+        };
+
+        markedRenderer.paragraph = function(text) {
+            var isTeXInline     = /\$\$(.*)\$\$/g.test(text);
+            var isTeXLine       = /^\$\$(.*)\$\$$/.test(text);
+            var isTeXAddClass   = (isTeXLine)     ? " class=\"" + editormd.classNames.tex + "\"" : "";
+            var isToC           = (settings.tocm) ? /^(\[TOC\]|\[TOCM\])$/.test(text) : /^\[TOC\]$/.test(text);
+            var isToCMenu       = /^\[TOCM\]$/.test(text);
+            
+            if (!isTeXLine && isTeXInline) 
+            {
+                text = text.replace(/(\$\$([^\$]*)\$\$)+/g, function($1, $2) {
+                    return "<span class=\"" + editormd.classNames.tex + "\">" + $2.replace(/\$/g, "") + "</span>";
+                });
+            } 
+            else 
+            {
+                text = (isTeXLine) ? text.replace(/\$/g, "") : text;
+            }
+            
+            var tocHTML = "<div class=\"markdown-toc editormd-markdown-toc\">" + text + "</div>";
+            
+            return (isToC) ? ( (isToCMenu) ? "<div class=\"editormd-toc-menu\">" + tocHTML + "</div><br/>" : tocHTML )
+                           : ( (pageBreakReg.test(text)) ? this.pageBreak(text) : "<p" + isTeXAddClass + ">" + this.atLink(this.emoji(text)) + "</p>\n" );
+        };
+
+        markedRenderer.code = function (code, lang, escaped) { 
+
+            if (lang === "seq" || lang === "sequence")
+            {
+                return "<div class=\"sequence-diagram\">" + code + "</div>";
+            } 
+            else if ( lang === "flow")
+            {
+                return "<div class=\"flowchart\">" + code + "</div>";
+            } 
+            else if ( lang === "math" || lang === "latex" || lang === "katex")
+            {
+                return "<p class=\"" + editormd.classNames.tex + "\">" + code + "</p>";
+            } 
+            else 
+            {
+
+                return marked.Renderer.prototype.code.apply(this, arguments);
+            }
+        };
+
+        markedRenderer.tablecell = function(content, flags) {
+            var type = (flags.header) ? "th" : "td";
+            var tag  = (flags.align)  ? "<" + type +" style=\"text-align:" + flags.align + "\">" : "<" + type + ">";
+            
+            return tag + this.atLink(this.emoji(content)) + "</" + type + ">\n";
+        };
+
+        markedRenderer.listitem = function(text) {
+            if (settings.taskList && /^\s*\[[x\s]\]\s*/.test(text)) 
+            {
+                text = text.replace(/^\s*\[\s\]\s*/, "<input type=\"checkbox\" class=\"task-list-item-checkbox\" /> ")
+                           .replace(/^\s*\[x\]\s*/,  "<input type=\"checkbox\" class=\"task-list-item-checkbox\" checked disabled /> ");
+
+                return "<li style=\"list-style: none;\">" + this.atLink(this.emoji(text)) + "</li>";
+            }
+            else 
+            {
+                return "<li>" + this.atLink(this.emoji(text)) + "</li>";
+            }
+        };
+        
+        return markedRenderer;
+    };
+    
+    /**
+     *
+     * 生成TOC(Table of Contents)
+     * Creating ToC (Table of Contents)
+     * 
+     * @param   {Array}    toc             从marked获取的TOC数组列表
+     * @param   {Element}  container       插入TOC的容器元素
+     * @param   {Integer}  startLevel      Hx 起始层级
+     * @returns {Object}   tocContainer    返回ToC列表容器层的jQuery对象元素
+     */
+    
+    editormd.markdownToCRenderer = function(toc, container, tocDropdown, startLevel) {
+        
+        var html        = "";    
+        var lastLevel   = 0;
+        var classPrefix = this.classPrefix;
+        
+        startLevel      = startLevel  || 1;
+        
+        for (var i = 0, len = toc.length; i < len; i++) 
+        {
+            var text  = toc[i].text;
+            var level = toc[i].level;
+            
+            if (level < startLevel) {
+                continue;
+            }
+            
+            if (level > lastLevel) 
+            {
+                html += "";
+            }
+            else if (level < lastLevel) 
+            {
+                html += (new Array(lastLevel - level + 2)).join("</ul></li>");
+            } 
+            else 
+            {
+                html += "</ul></li>";
+            }
+
+            html += "<li><a class=\"toc-level-" + level + "\" href=\"#" + text + "\" level=\"" + level + "\">" + text + "</a><ul>";
+            lastLevel = level;
+        }
+        
+        var tocContainer = container.find(".markdown-toc");
+        
+        if ((tocContainer.length < 1 && container.attr("previewContainer") === "false"))
+        {
+            var tocHTML = "<div class=\"markdown-toc " + classPrefix + "markdown-toc\"></div>";
+            
+            tocHTML = (tocDropdown) ? "<div class=\"" + classPrefix + "toc-menu\">" + tocHTML + "</div>" : tocHTML;
+            
+            container.html(tocHTML);
+            
+            tocContainer = container.find(".markdown-toc");
+        }
+        
+        if (tocDropdown)
+        {
+            tocContainer.wrap("<div class=\"" + classPrefix + "toc-menu\"></div><br/>");
+        }
+        
+        tocContainer.html("<ul class=\"markdown-toc-list\"></ul>").children(".markdown-toc-list").html(html.replace(/\r?\n?\<ul\>\<\/ul\>/g, ""));
+        
+        return tocContainer;
+    };
+    
+    /**
+     *
+     * 生成TOC下拉菜单
+     * Creating ToC dropdown menu
+     * 
+     * @param   {Object}   container       插入TOC的容器jQuery对象元素
+     * @param   {String}   tocTitle        ToC title
+     * @returns {Object}                   return toc-menu object
+     */
+    
+    editormd.tocDropdownMenu = function(container, tocTitle) {
+        
+        tocTitle      = tocTitle || "Table of Contents";
+        
+        var zindex    = 400;
+        var tocMenus  = container.find("." + this.classPrefix + "toc-menu");
+
+        tocMenus.each(function() {
+            var $this  = $(this);
+            var toc    = $this.children(".markdown-toc");
+            var icon   = "<i class=\"fa fa-angle-down\"></i>";
+            var btn    = "<a href=\"javascript:;\" class=\"toc-menu-btn\">" + icon + tocTitle + "</a>";
+            var menu   = toc.children("ul");            
+            var list   = menu.find("li");
+            
+            toc.append(btn);
+            
+            list.first().before("<li><h1>" + tocTitle + " " + icon + "</h1></li>");
+            
+            $this.mouseover(function(){
+                menu.show();
+
+                list.each(function(){
+                    var li = $(this);
+                    var ul = li.children("ul");
+
+                    if (ul.html() === "")
+                    {
+                        ul.remove();
+                    }
+
+                    if (ul.length > 0 && ul.html() !== "")
+                    {
+                        var firstA = li.children("a").first();
+
+                        if (firstA.children(".fa").length < 1)
+                        {
+                            firstA.append( $(icon).css({ float:"right", paddingTop:"4px" }) );
+                        }
+                    }
+
+                    li.mouseover(function(){
+                        ul.css("z-index", zindex).show();
+                        zindex += 1;
+                    }).mouseleave(function(){
+                        ul.hide();
+                    });
+                });
+            }).mouseleave(function(){
+                menu.hide();
+            }); 
+        });       
+        
+        return tocMenus;
+    };
+    
+    /**
+     * 简单地过滤指定的HTML标签
+     * Filter custom html tags
+     * 
+     * @param   {String}   html          要过滤HTML
+     * @param   {String}   filters       要过滤的标签
+     * @returns {String}   html          返回过滤的HTML
+     */
+    
+    editormd.filterHTMLTags = function(html, filters) {
+        
+        if (typeof html !== "string") {
+            html = new String(html);
+        }
+            
+        if (typeof filters !== "string") {
+            return html;
+        }
+
+        var expression = filters.split("|");
+        var filterTags = expression[0].split(",");
+        var attrs      = expression[1];
+
+        for (var i = 0, len = filterTags.length; i < len; i++)
+        {
+            var tag = filterTags[i];
+
+            html = html.replace(new RegExp("\<\s*" + tag + "\s*([^\>]*)\>([^\>]*)\<\s*\/" + tag + "\s*\>", "igm"), "");
+        }
+        
+        //return html;
+
+        if (typeof attrs !== "undefined")
+        {
+            var htmlTagRegex = /\<(\w+)\s*([^\>]*)\>([^\>]*)\<\/(\w+)\>/ig;
+
+            if (attrs === "*")
+            {
+                html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) {
+                    return "<" + $2 + ">" + $4 + "</" + $5 + ">";
+                });         
+            }
+            else if (attrs === "on*")
+            {
+                html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) {
+                    var el = $("<" + $2 + ">" + $4 + "</" + $5 + ">");
+                    var _attrs = $($1)[0].attributes;
+                    var $attrs = {};
+                    
+                    $.each(_attrs, function(i, e) {
+                        if (e.nodeName !== '"') $attrs[e.nodeName] = e.nodeValue;
+                    });
+                    
+                    $.each($attrs, function(i) {                        
+                        if (i.indexOf("on") === 0) {
+                            delete $attrs[i];
+                        }
+                    });
+                    
+                    el.attr($attrs);
+                    
+                    var text = (typeof el[1] !== "undefined") ? $(el[1]).text() : "";
+
+                    return el[0].outerHTML + text;
+                });
+            }
+            else
+            {
+                html = html.replace(htmlTagRegex, function($1, $2, $3, $4) {
+                    var filterAttrs = attrs.split(",");
+                    var el = $($1);
+                    el.html($4);
+
+                    $.each(filterAttrs, function(i) {
+                        el.attr(filterAttrs[i], null);
+                    });
+
+                    return el[0].outerHTML;
+                });
+            }
+        }
+        
+        return html;
+    };
+    
+    /**
+     * 将Markdown文档解析为HTML用于前台显示
+     * Parse Markdown to HTML for Font-end preview.
+     * 
+     * @param   {String}   id            用于显示HTML的对象ID
+     * @param   {Object}   [options={}]  配置选项,可选
+     * @returns {Object}   div           返回jQuery对象元素
+     */
+    
+    editormd.markdownToHTML = function(id, options) {
+        var defaults = {
+            gfm                  : true,
+            toc                  : true,
+            tocm                 : false,
+            tocStartLevel        : 1,
+            tocTitle             : "目录",
+            tocDropdown          : false,
+            tocContainer         : "",
+            markdown             : "",
+            markdownSourceCode   : false,
+            htmlDecode           : false,
+            autoLoadKaTeX        : true,
+            pageBreak            : true,
+            atLink               : true,    // for @link
+            emailLink            : true,    // for mail address auto link
+            tex                  : false,
+            taskList             : false,   // Github Flavored Markdown task lists
+            emoji                : true,
+            flowChart            : false,
+            sequenceDiagram      : false,
+            previewCodeHighlight : true
+        };
+        
+        editormd.$marked  = marked;
+
+        var div           = $("#" + id);
+        var settings      = div.settings = $.extend(true, defaults, options || {});
+        var saveTo        = div.find("textarea");
+        
+        if (saveTo.length < 1)
+        {
+            div.append("<textarea></textarea>");
+            saveTo        = div.find("textarea");
+        }        
+        
+        var markdownDoc   = (settings.markdown === "") ? saveTo.val() : settings.markdown; 
+        var markdownToC   = [];
+
+        var rendererOptions = {  
+            toc                  : settings.toc,
+            tocm                 : settings.tocm,
+            tocStartLevel        : settings.tocStartLevel,
+            taskList             : settings.taskList,
+            emoji                : settings.emoji,
+            tex                  : settings.tex,
+            pageBreak            : settings.pageBreak,
+            atLink               : settings.atLink,           // for @link
+            emailLink            : settings.emailLink,        // for mail address auto link
+            flowChart            : settings.flowChart,
+            sequenceDiagram      : settings.sequenceDiagram,
+            previewCodeHighlight : settings.previewCodeHighlight,
+        };
+
+        var markedOptions = {
+            renderer    : editormd.markedRenderer(markdownToC, rendererOptions),
+            gfm         : settings.gfm,
+            tables      : true,
+            breaks      : true,
+            pedantic    : false,
+            sanitize    : (settings.htmlDecode) ? false : true, // 是否忽略HTML标签,即是否开启HTML标签解析,为了安全性,默认不开启
+            smartLists  : true,
+            smartypants : true
+        };
+        
+		markdownDoc = new String(markdownDoc);
+        
+        var markdownParsed = marked(markdownDoc, markedOptions);
+        
+        markdownParsed = editormd.filterHTMLTags(markdownParsed, settings.htmlDecode);
+        
+        if (settings.markdownSourceCode) {
+            saveTo.text(markdownDoc);
+        } else {
+            saveTo.remove();
+        }
+        
+        div.addClass("markdown-body " + this.classPrefix + "html-preview").append(markdownParsed);
+        
+        var tocContainer = (settings.tocContainer !== "") ? $(settings.tocContainer) : div;
+        
+        if (settings.tocContainer !== "")
+        {
+            tocContainer.attr("previewContainer", false);
+        }
+         
+        if (settings.toc) 
+        {
+            div.tocContainer = this.markdownToCRenderer(markdownToC, tocContainer, settings.tocDropdown, settings.tocStartLevel);
+            
+            if (settings.tocDropdown || div.find("." + this.classPrefix + "toc-menu").length > 0)
+            {
+                this.tocDropdownMenu(div, settings.tocTitle);
+            }
+            
+            if (settings.tocContainer !== "")
+            {
+                div.find(".editormd-toc-menu, .editormd-markdown-toc").remove();
+            }
+        }
+            
+        if (settings.previewCodeHighlight) 
+        {
+            div.find("pre").addClass("prettyprint linenums");
+            prettyPrint();
+        }
+        
+        if (!editormd.isIE8) 
+        {
+            if (settings.flowChart) {
+                div.find(".flowchart").flowChart(); 
+            }
+
+            if (settings.sequenceDiagram) {
+                div.find(".sequence-diagram").sequenceDiagram({theme: "simple"});
+            }
+        }
+
+        if (settings.tex)
+        {
+            var katexHandle = function() {
+                div.find("." + editormd.classNames.tex).each(function(){
+                    var tex  = $(this);                    
+                    katex.render(tex.html().replace(/&lt;/g, "<").replace(/&gt;/g, ">"), tex[0]);                    
+                    tex.find(".katex").css("font-size", "1.6em");
+                });
+            };
+            
+            if (settings.autoLoadKaTeX && !editormd.$katex && !editormd.kaTeXLoaded)
+            {
+                this.loadKaTeX(function() {
+                    editormd.$katex      = katex;
+                    editormd.kaTeXLoaded = true;
+                    katexHandle();
+                });
+            }
+            else
+            {
+                katexHandle();
+            }
+        }
+        
+        div.getMarkdown = function() {            
+            return saveTo.val();
+        };
+        
+        return div;
+    };
+    
+    // Editor.md themes, change toolbar themes etc.
+    // added @1.5.0
+    editormd.themes        = ["default", "dark"];
+    
+    // Preview area themes
+    // added @1.5.0
+    editormd.previewThemes = ["default", "dark"];
+    
+    // CodeMirror / editor area themes
+    // @1.5.0 rename -> editorThemes, old version -> themes
+    editormd.editorThemes = [
+        "default", "3024-day", "3024-night",
+        "ambiance", "ambiance-mobile",
+        "base16-dark", "base16-light", "blackboard",
+        "cobalt",
+        "eclipse", "elegant", "erlang-dark",
+        "lesser-dark",
+        "mbo", "mdn-like", "midnight", "monokai",
+        "neat", "neo", "night",
+        "paraiso-dark", "paraiso-light", "pastel-on-dark",
+        "rubyblue",
+        "solarized",
+        "the-matrix", "tomorrow-night-eighties", "twilight",
+        "vibrant-ink",
+        "xq-dark", "xq-light"
+    ];
+
+    editormd.loadPlugins = {};
+    
+    editormd.loadFiles = {
+        js     : [],
+        css    : [],
+        plugin : []
+    };
+    
+    /**
+     * 动态加载Editor.md插件,但不立即执行
+     * Load editor.md plugins
+     * 
+     * @param {String}   fileName              插件文件路径
+     * @param {Function} [callback=function()] 加载成功后执行的回调函数
+     * @param {String}   [into="head"]         嵌入页面的位置
+     */
+    
+    editormd.loadPlugin = function(fileName, callback, into) {
+        callback   = callback || function() {};
+        
+        this.loadScript(fileName, function() {
+            editormd.loadFiles.plugin.push(fileName);
+            callback();
+        }, into);
+    };
+    
+    /**
+     * 动态加载CSS文件的方法
+     * Load css file method
+     * 
+     * @param {String}   fileName              CSS文件名
+     * @param {Function} [callback=function()] 加载成功后执行的回调函数
+     * @param {String}   [into="head"]         嵌入页面的位置
+     */
+    
+    editormd.loadCSS   = function(fileName, callback, into) {
+        into       = into     || "head";        
+        callback   = callback || function() {};
+        
+        var css    = document.createElement("link");
+        css.type   = "text/css";
+        css.rel    = "stylesheet";
+        css.onload = css.onreadystatechange = function() {
+            editormd.loadFiles.css.push(fileName);
+            callback();
+        };
+
+        css.href   = fileName + ".css";
+
+        if(into === "head") {
+            document.getElementsByTagName("head")[0].appendChild(css);
+        } else {
+            document.body.appendChild(css);
+        }
+    };
+    
+    editormd.isIE    = (navigator.appName == "Microsoft Internet Explorer");
+    editormd.isIE8   = (editormd.isIE && navigator.appVersion.match(/8./i) == "8.");
+
+    /**
+     * 动态加载JS文件的方法
+     * Load javascript file method
+     * 
+     * @param {String}   fileName              JS文件名
+     * @param {Function} [callback=function()] 加载成功后执行的回调函数
+     * @param {String}   [into="head"]         嵌入页面的位置
+     */
+
+    editormd.loadScript = function(fileName, callback, into) {
+        
+        into          = into     || "head";
+        callback      = callback || function() {};
+        
+        var script    = null; 
+        script        = document.createElement("script");
+        script.id     = fileName.replace(/[\./]+/g, "-");
+        script.type   = "text/javascript";        
+        script.src    = fileName + ".js";
+        
+        if (editormd.isIE8) 
+        {            
+            script.onreadystatechange = function() {
+                if(script.readyState) 
+                {
+                    if (script.readyState === "loaded" || script.readyState === "complete") 
+                    {
+                        script.onreadystatechange = null; 
+                        editormd.loadFiles.js.push(fileName);
+                        callback();
+                    }
+                } 
+            };
+        }
+        else
+        {
+            script.onload = function() {
+                editormd.loadFiles.js.push(fileName);
+                callback();
+            };
+        }
+
+        if (into === "head") {
+            document.getElementsByTagName("head")[0].appendChild(script);
+        } else {
+            document.body.appendChild(script);
+        }
+    };
+    
+    // 使用国外的CDN,加载速度有时会很慢,或者自定义URL
+    // You can custom KaTeX load url.
+    editormd.katexURL  = {
+        css : "//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min",
+        js  : "//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min"
+    };
+    
+    editormd.kaTeXLoaded = false;
+    
+    /**
+     * 加载KaTeX文件
+     * load KaTeX files
+     * 
+     * @param {Function} [callback=function()]  加载成功后执行的回调函数
+     */
+    
+    editormd.loadKaTeX = function (callback) {
+        editormd.loadCSS(editormd.katexURL.css, function(){
+            editormd.loadScript(editormd.katexURL.js, callback || function(){});
+        });
+    };
+        
+    /**
+     * 锁屏
+     * lock screen
+     * 
+     * @param   {Boolean}   lock   Boolean 布尔值,是否锁屏
+     * @returns {void}
+     */
+    
+    editormd.lockScreen = function(lock) {
+        $("html,body").css("overflow", (lock) ? "hidden" : "");
+    };
+        
+    /**
+     * 动态创建对话框
+     * Creating custom dialogs
+     * 
+     * @param   {Object} options 配置项键值对 Key/Value
+     * @returns {dialog} 返回创建的dialog的jQuery实例对象
+     */
+
+    editormd.createDialog = function(options) {
+        var defaults = {
+            name : "",
+            width : 420,
+            height: 240,
+            title : "",
+            drag  : true,
+            closed : true,
+            content : "",
+            mask : true,
+            maskStyle : {
+                backgroundColor : "#fff",
+                opacity : 0.1
+            },
+            lockScreen : true,
+            footer : true,
+            buttons : false
+        };
+
+        options          = $.extend(true, defaults, options);
+        
+        var $this        = this;
+        var editor       = this.editor;
+        var classPrefix  = editormd.classPrefix;
+        var guid         = (new Date()).getTime();
+        var dialogName   = ( (options.name === "") ? classPrefix + "dialog-" + guid : options.name);
+        var mouseOrTouch = editormd.mouseOrTouch;
+
+        var html         = "<div class=\"" + classPrefix + "dialog " + dialogName + "\">";
+
+        if (options.title !== "")
+        {
+            html += "<div class=\"" + classPrefix + "dialog-header\"" + ( (options.drag) ? " style=\"cursor: move;\"" : "" ) + ">";
+            html += "<strong class=\"" + classPrefix + "dialog-title\">" + options.title + "</strong>";
+            html += "</div>";
+        }
+
+        if (options.closed)
+        {
+            html += "<a href=\"javascript:;\" class=\"fa fa-close " + classPrefix + "dialog-close\"></a>";
+        }
+
+        html += "<div class=\"" + classPrefix + "dialog-container\">" + options.content;                    
+
+        if (options.footer || typeof options.footer === "string") 
+        {
+            html += "<div class=\"" + classPrefix + "dialog-footer\">" + ( (typeof options.footer === "boolean") ? "" : options.footer) + "</div>";
+        }
+
+        html += "</div>";
+
+        html += "<div class=\"" + classPrefix + "dialog-mask " + classPrefix + "dialog-mask-bg\"></div>";
+        html += "<div class=\"" + classPrefix + "dialog-mask " + classPrefix + "dialog-mask-con\"></div>";
+        html += "</div>";
+
+        editor.append(html);
+
+        var dialog = editor.find("." + dialogName);
+
+        dialog.lockScreen = function(lock) {
+            if (options.lockScreen)
+            {                
+                $("html,body").css("overflow", (lock) ? "hidden" : "");
+                $this.resize();
+            }
+
+            return dialog;
+        };
+
+        dialog.showMask = function() {
+            if (options.mask)
+            {
+                editor.find("." + classPrefix + "mask").css(options.maskStyle).css("z-index", editormd.dialogZindex - 1).show();
+            }
+            return dialog;
+        };
+
+        dialog.hideMask = function() {
+            if (options.mask)
+            {
+                editor.find("." + classPrefix + "mask").hide();
+            }
+
+            return dialog;
+        };
+
+        dialog.loading = function(show) {                        
+            var loading = dialog.find("." + classPrefix + "dialog-mask");
+            loading[(show) ? "show" : "hide"]();
+
+            return dialog;
+        };
+
+        dialog.lockScreen(true).showMask();
+
+        dialog.show().css({
+            zIndex : editormd.dialogZindex,
+            border : (editormd.isIE8) ? "1px solid #ddd" : "",
+            width  : (typeof options.width  === "number") ? options.width + "px"  : options.width,
+            height : (typeof options.height === "number") ? options.height + "px" : options.height
+        });
+
+        var dialogPosition = function(){
+            dialog.css({
+                top    : ($(window).height() - dialog.height()) / 2 + "px",
+                left   : ($(window).width() - dialog.width()) / 2 + "px"
+            });
+        };
+
+        dialogPosition();
+
+        $(window).resize(dialogPosition);
+
+        dialog.children("." + classPrefix + "dialog-close").bind(mouseOrTouch("click", "touchend"), function() {
+            dialog.hide().lockScreen(false).hideMask();
+        });
+
+        if (typeof options.buttons === "object")
+        {
+            var footer = dialog.footer = dialog.find("." + classPrefix + "dialog-footer");
+
+            for (var key in options.buttons)
+            {
+                var btn = options.buttons[key];
+                var btnClassName = classPrefix + key + "-btn";
+
+                footer.append("<button class=\"" + classPrefix + "btn " + btnClassName + "\">" + btn[0] + "</button>");
+                btn[1] = $.proxy(btn[1], dialog);
+                footer.children("." + btnClassName).bind(mouseOrTouch("click", "touchend"), btn[1]);
+            }
+        }
+
+        if (options.title !== "" && options.drag)
+        {                        
+            var posX, posY;
+            var dialogHeader = dialog.children("." + classPrefix + "dialog-header");
+
+            if (!options.mask) {
+                dialogHeader.bind(mouseOrTouch("click", "touchend"), function(){
+                    editormd.dialogZindex += 2;
+                    dialog.css("z-index", editormd.dialogZindex);
+                });
+            }
+
+            dialogHeader.mousedown(function(e) {
+                e = e || window.event;  //IE
+                posX = e.clientX - parseInt(dialog[0].style.left);
+                posY = e.clientY - parseInt(dialog[0].style.top);
+
+                document.onmousemove = moveAction;                   
+            });
+
+            var userCanSelect = function (obj) {
+                obj.removeClass(classPrefix + "user-unselect").off("selectstart");
+            };
+
+            var userUnselect = function (obj) {
+                obj.addClass(classPrefix + "user-unselect").on("selectstart", function(event) { // selectstart for IE                        
+                    return false;
+                });
+            };
+
+            var moveAction = function (e) {
+                e = e || window.event;  //IE
+
+                var left, top, nowLeft = parseInt(dialog[0].style.left), nowTop = parseInt(dialog[0].style.top);
+
+                if( nowLeft >= 0 ) {
+                    if( nowLeft + dialog.width() <= $(window).width()) {
+                        left = e.clientX - posX;
+                    } else {	
+                        left = $(window).width() - dialog.width();
+                        document.onmousemove = null;
+                    }
+                } else {
+                    left = 0;
+                    document.onmousemove = null;
+                }
+
+                if( nowTop >= 0 ) {
+                    top = e.clientY - posY;
+                } else {
+                    top = 0;
+                    document.onmousemove = null;
+                }
+
+
+                document.onselectstart = function() {
+                    return false;
+                };
+
+                userUnselect($("body"));
+                userUnselect(dialog);
+                dialog[0].style.left = left + "px";
+                dialog[0].style.top  = top + "px";
+            };
+
+            document.onmouseup = function() {                            
+                userCanSelect($("body"));
+                userCanSelect(dialog);
+
+                document.onselectstart = null;         
+                document.onmousemove = null;
+            };
+
+            dialogHeader.touchDraggable = function() {
+                var offset = null;
+                var start  = function(e) {
+                    var orig = e.originalEvent; 
+                    var pos  = $(this).parent().position();
+
+                    offset = {
+                        x : orig.changedTouches[0].pageX - pos.left,
+                        y : orig.changedTouches[0].pageY - pos.top
+                    };
+                };
+
+                var move = function(e) {
+                    e.preventDefault();
+                    var orig = e.originalEvent;
+
+                    $(this).parent().css({
+                        top  : orig.changedTouches[0].pageY - offset.y,
+                        left : orig.changedTouches[0].pageX - offset.x
+                    });
+                };
+
+                this.bind("touchstart", start).bind("touchmove", move);
+            };
+
+            dialogHeader.touchDraggable();
+        }
+
+        editormd.dialogZindex += 2;
+
+        return dialog;
+    };
+    
+    /**
+     * 鼠标和触摸事件的判断/选择方法
+     * MouseEvent or TouchEvent type switch
+     * 
+     * @param   {String} [mouseEventType="click"]    供选择的鼠标事件
+     * @param   {String} [touchEventType="touchend"] 供选择的触摸事件
+     * @returns {String} EventType                   返回事件类型名称
+     */
+    
+    editormd.mouseOrTouch = function(mouseEventType, touchEventType) {
+        mouseEventType = mouseEventType || "click";
+        touchEventType = touchEventType || "touchend";
+        
+        var eventType  = mouseEventType;
+
+        try {
+            document.createEvent("TouchEvent");
+            eventType = touchEventType;
+        } catch(e) {}
+
+        return eventType;
+    };
+    
+    /**
+     * 日期时间的格式化方法
+     * Datetime format method
+     * 
+     * @param   {String}   [format=""]  日期时间的格式,类似PHP的格式
+     * @returns {String}   datefmt      返回格式化后的日期时间字符串
+     */
+    
+    editormd.dateFormat = function(format) {                
+        format      = format || "";
+
+        var addZero = function(d) {
+            return (d < 10) ? "0" + d : d;
+        };
+
+        var date    = new Date(); 
+        var year    = date.getFullYear();
+        var year2   = year.toString().slice(2, 4);
+        var month   = addZero(date.getMonth() + 1);
+        var day     = addZero(date.getDate());
+        var weekDay = date.getDay();
+        var hour    = addZero(date.getHours());
+        var min     = addZero(date.getMinutes());
+        var second  = addZero(date.getSeconds());
+        var ms      = addZero(date.getMilliseconds()); 
+        var datefmt = "";
+
+        var ymd     = year2 + "-" + month + "-" + day;
+        var fymd    = year  + "-" + month + "-" + day;
+        var hms     = hour  + ":" + min   + ":" + second;
+
+        switch (format) 
+        {
+            case "UNIX Time" :
+                    datefmt = date.getTime();
+                break;
+
+            case "UTC" :
+                    datefmt = date.toUTCString();
+                break;	
+
+            case "yy" :
+                    datefmt = year2;
+                break;	
+
+            case "year" :
+            case "yyyy" :
+                    datefmt = year;
+                break;
+
+            case "month" :
+            case "mm" :
+                    datefmt = month;
+                break;                        
+
+            case "cn-week-day" :
+            case "cn-wd" :
+                    var cnWeekDays = ["日", "一", "二", "三", "四", "五", "六"];
+                    datefmt = "星期" + cnWeekDays[weekDay];
+                break;
+
+            case "week-day" :
+            case "wd" :
+                    var weekDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
+                    datefmt = weekDays[weekDay];
+                break;
+
+            case "day" :
+            case "dd" :
+                    datefmt = day;
+                break;
+
+            case "hour" :
+            case "hh" :
+                    datefmt = hour;
+                break;
+
+            case "min" :
+            case "ii" :
+                    datefmt = min;
+                break;
+
+            case "second" :
+            case "ss" :
+                    datefmt = second;
+                break;
+
+            case "ms" :
+                    datefmt = ms;
+                break;
+
+            case "yy-mm-dd" :
+                    datefmt = ymd;
+                break;
+
+            case "yyyy-mm-dd" :
+                    datefmt = fymd;
+                break;
+
+            case "yyyy-mm-dd h:i:s ms" :
+            case "full + ms" : 
+                    datefmt = fymd + " " + hms + " " + ms;
+                break;
+
+            case "full" :
+            case "yyyy-mm-dd h:i:s" :
+                default:
+                    datefmt = fymd + " " + hms;
+                break;
+        }
+
+        return datefmt;
+    };
+
+    return editormd;
+
+}));

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 1 - 0
public/theme/material/editor/editormd.amd.min.js


+ 4597 - 0
public/theme/material/editor/editormd.js

@@ -0,0 +1,4597 @@
+/*
+ * Editor.md
+ *
+ * @file        editormd.js 
+ * @version     v1.5.0 
+ * @description Open source online markdown editor.
+ * @license     MIT License
+ * @author      Pandao
+ * {@link       https://github.com/pandao/editor.md}
+ * @updateTime  2015-06-09
+ */
+
+;(function(factory) {
+    "use strict";
+    
+	// CommonJS/Node.js
+	if (typeof require === "function" && typeof exports === "object" && typeof module === "object")
+    { 
+        module.exports = factory;
+    }
+	else if (typeof define === "function")  // AMD/CMD/Sea.js
+	{
+        if (define.amd) // for Require.js
+        {
+            /* Require.js define replace */
+        } 
+        else 
+        {
+		    define(["jquery"], factory);  // for Sea.js
+        }
+	} 
+	else
+	{ 
+        window.editormd = factory();
+	}
+    
+}(function() {    
+
+    /* Require.js assignment replace */
+    
+    "use strict";
+    
+    var $ = (typeof (jQuery) !== "undefined") ? jQuery : Zepto;
+
+	if (typeof ($) === "undefined") {
+		return ;
+	}
+    
+    /**
+     * editormd
+     * 
+     * @param   {String} id           编辑器的ID
+     * @param   {Object} options      配置选项 Key/Value
+     * @returns {Object} editormd     返回editormd对象
+     */
+    
+    var editormd         = function (id, options) {
+        return new editormd.fn.init(id, options);
+    };
+    
+    editormd.title        = editormd.$name = "Editor.md";
+    editormd.version      = "1.5.0";
+    editormd.homePage     = "https://pandao.github.io/editor.md/";
+    editormd.classPrefix  = "editormd-";
+    
+    editormd.toolbarModes = {
+        full : [
+            "undo", "redo", "|", 
+            "bold", "del", "italic", "quote", "ucwords", "uppercase", "lowercase", "|", 
+            "h1", "h2", "h3", "h4", "h5", "h6", "|", 
+            "list-ul", "list-ol", "hr", "|",
+            "link", "reference-link", "image", "code", "preformatted-text", "code-block", "table", "datetime", "emoji", "html-entities", "pagebreak", "|",
+            "goto-line", "watch", "preview", "fullscreen", "clear", "search", "|",
+            "help", "info"
+        ],
+        simple : [
+            "undo", "redo", "|", 
+            "bold", "del", "italic", "quote", "uppercase", "lowercase", "|", 
+            "h1", "h2", "h3", "h4", "h5", "h6", "|", 
+            "list-ul", "list-ol", "hr", "|",
+            "watch", "preview", "fullscreen", "|",
+            "help", "info"
+        ],
+        mini : [
+            "undo", "redo", "|",
+            "watch", "preview", "|",
+            "help", "info"
+        ]
+    };
+    
+    editormd.defaults     = {
+        mode                 : "gfm",          //gfm or markdown
+        name                 : "",             // Form element name
+        value                : "",             // value for CodeMirror, if mode not gfm/markdown
+        theme                : "",             // Editor.md self themes, before v1.5.0 is CodeMirror theme, default empty
+        editorTheme          : "default",      // Editor area, this is CodeMirror theme at v1.5.0
+        previewTheme         : "",             // Preview area theme, default empty
+        markdown             : "",             // Markdown source code
+        appendMarkdown       : "",             // if in init textarea value not empty, append markdown to textarea
+        width                : "100%",
+        height               : "100%",
+        path                 : "./lib/",       // Dependents module file directory
+        pluginPath           : "",             // If this empty, default use settings.path + "../plugins/"
+        delay                : 300,            // Delay parse markdown to html, Uint : ms
+        autoLoadModules      : true,           // Automatic load dependent module files
+        watch                : true,
+        placeholder          : "Enjoy Markdown! coding now...",
+        gotoLine             : true,
+        codeFold             : false,
+        autoHeight           : false,
+		autoFocus            : true,
+        autoCloseTags        : true,
+        searchReplace        : true,
+        syncScrolling        : true,           // true | false | "single", default true
+        readOnly             : false,
+        tabSize              : 4,
+		indentUnit           : 4,
+        lineNumbers          : true,
+		lineWrapping         : true,
+		autoCloseBrackets    : true,
+		showTrailingSpace    : true,
+		matchBrackets        : true,
+		indentWithTabs       : true,
+		styleSelectedText    : true,
+        matchWordHighlight   : true,           // options: true, false, "onselected"
+        styleActiveLine      : true,           // Highlight the current line
+        dialogLockScreen     : true,
+        dialogShowMask       : true,
+        dialogDraggable      : true,
+        dialogMaskBgColor    : "#fff",
+        dialogMaskOpacity    : 0.1,
+        fontSize             : "13px",
+        saveHTMLToTextarea   : false,
+        disabledKeyMaps      : [],
+        
+        onload               : function() {},
+        onresize             : function() {},
+        onchange             : function() {},
+        onwatch              : null,
+        onunwatch            : null,
+        onpreviewing         : function() {},
+        onpreviewed          : function() {},
+        onfullscreen         : function() {},
+        onfullscreenExit     : function() {},
+        onscroll             : function() {},
+        onpreviewscroll      : function() {},
+        
+        imageUpload          : false,
+        imageFormats         : ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
+        imageUploadURL       : "",
+        crossDomainUpload    : false,
+        uploadCallbackURL    : "",
+        
+        toc                  : true,           // Table of contents
+        tocm                 : false,           // Using [TOCM], auto create ToC dropdown menu
+        tocTitle             : "",             // for ToC dropdown menu btn
+        tocDropdown          : false,
+        tocContainer         : "",
+        tocStartLevel        : 1,              // Said from H1 to create ToC
+        htmlDecode           : false,          // Open the HTML tag identification 
+        pageBreak            : true,           // Enable parse page break [========]
+        atLink               : true,           // for @link
+        emailLink            : true,           // for email address auto link
+        taskList             : false,          // Enable Github Flavored Markdown task lists
+        emoji                : true,          // :emoji: , Support Github emoji, Twitter Emoji (Twemoji);
+                                               // Support FontAwesome icon emoji :fa-xxx: > Using fontAwesome icon web fonts;
+                                               // Support Editor.md logo icon emoji :editormd-logo: :editormd-logo-1x: > 1~8x;
+        tex                  : false,          // TeX(LaTeX), based on KaTeX
+        flowChart            : false,          // flowChart.js only support IE9+
+        sequenceDiagram      : false,          // sequenceDiagram.js only support IE9+
+        previewCodeHighlight : true,
+                
+        toolbar              : true,           // show/hide toolbar
+        toolbarAutoFixed     : true,           // on window scroll auto fixed position
+        toolbarIcons         : "full",
+        toolbarTitles        : {},
+        toolbarHandlers      : {
+            ucwords : function() {
+                return editormd.toolbarHandlers.ucwords;
+            },
+            lowercase : function() {
+                return editormd.toolbarHandlers.lowercase;
+            }
+        },
+        toolbarCustomIcons   : {               // using html tag create toolbar icon, unused default <a> tag.
+            lowercase        : "<a href=\"javascript:;\" title=\"Lowercase\" unselectable=\"on\"><i class=\"fa\" name=\"lowercase\" style=\"font-size:24px;margin-top: -10px;\">a</i></a>",
+            "ucwords"        : "<a href=\"javascript:;\" title=\"ucwords\" unselectable=\"on\"><i class=\"fa\" name=\"ucwords\" style=\"font-size:20px;margin-top: -3px;\">Aa</i></a>"
+        }, 
+        toolbarIconsClass    : {
+            undo             : "fa-undo",
+            redo             : "fa-repeat",
+            bold             : "fa-bold",
+            del              : "fa-strikethrough",
+            italic           : "fa-italic",
+            quote            : "fa-quote-left",
+            uppercase        : "fa-font",
+            h1               : editormd.classPrefix + "bold",
+            h2               : editormd.classPrefix + "bold",
+            h3               : editormd.classPrefix + "bold",
+            h4               : editormd.classPrefix + "bold",
+            h5               : editormd.classPrefix + "bold",
+            h6               : editormd.classPrefix + "bold",
+            "list-ul"        : "fa-list-ul",
+            "list-ol"        : "fa-list-ol",
+            hr               : "fa-minus",
+            link             : "fa-link",
+            "reference-link" : "fa-anchor",
+            image            : "fa-picture-o",
+            code             : "fa-code",
+            "preformatted-text" : "fa-file-code-o",
+            "code-block"     : "fa-file-code-o",
+            table            : "fa-table",
+            datetime         : "fa-clock-o",
+            emoji            : "fa-smile-o",
+            "html-entities"  : "fa-copyright",
+            pagebreak        : "fa-newspaper-o",
+            "goto-line"      : "fa-terminal", // fa-crosshairs
+            watch            : "fa-eye-slash",
+            unwatch          : "fa-eye",
+            preview          : "fa-desktop",
+            search           : "fa-search",
+            fullscreen       : "fa-arrows-alt",
+            clear            : "fa-eraser",
+            help             : "fa-question-circle",
+            info             : "fa-info-circle"
+        },        
+        toolbarIconTexts     : {},
+        
+        lang : {
+            name        : "zh-cn",
+            description : "开源在线Markdown编辑器<br/>Open source online Markdown editor.",
+            tocTitle    : "目录",
+            toolbar     : {
+                undo             : "撤销(Ctrl+Z)",
+                redo             : "重做(Ctrl+Y)",
+                bold             : "粗体",
+                del              : "删除线",
+                italic           : "斜体",
+                quote            : "引用",
+                ucwords          : "将每个单词首字母转成大写",
+                uppercase        : "将所选转换成大写",
+                lowercase        : "将所选转换成小写",
+                h1               : "标题1",
+                h2               : "标题2",
+                h3               : "标题3",
+                h4               : "标题4",
+                h5               : "标题5",
+                h6               : "标题6",
+                "list-ul"        : "无序列表",
+                "list-ol"        : "有序列表",
+                hr               : "横线",
+                link             : "链接",
+                "reference-link" : "引用链接",
+                image            : "添加图片",
+                code             : "行内代码",
+                "preformatted-text" : "预格式文本 / 代码块(缩进风格)",
+                "code-block"     : "代码块(多语言风格)",
+                table            : "添加表格",
+                datetime         : "日期时间",
+                emoji            : "Emoji表情",
+                "html-entities"  : "HTML实体字符",
+                pagebreak        : "插入分页符",
+                "goto-line"      : "跳转到行",
+                watch            : "关闭实时预览",
+                unwatch          : "开启实时预览",
+                preview          : "全窗口预览HTML(按 Shift + ESC还原)",
+                fullscreen       : "全屏(按ESC还原)",
+                clear            : "清空",
+                search           : "搜索",
+                help             : "使用帮助",
+                info             : "关于" + editormd.title
+            },
+            buttons : {
+                enter  : "确定",
+                cancel : "取消",
+                close  : "关闭"
+            },
+            dialog : {
+                link : {
+                    title    : "添加链接",
+                    url      : "链接地址",
+                    urlTitle : "链接标题",
+                    urlEmpty : "错误:请填写链接地址。"
+                },
+                referenceLink : {
+                    title    : "添加引用链接",
+                    name     : "引用名称",
+                    url      : "链接地址",
+                    urlId    : "链接ID",
+                    urlTitle : "链接标题",
+                    nameEmpty: "错误:引用链接的名称不能为空。",
+                    idEmpty  : "错误:请填写引用链接的ID。",
+                    urlEmpty : "错误:请填写引用链接的URL地址。"
+                },
+                image : {
+                    title    : "添加图片",
+                    url      : "图片地址",
+                    link     : "图片链接",
+                    alt      : "图片描述",
+                    uploadButton     : "本地上传",
+                    imageURLEmpty    : "错误:图片地址不能为空。",
+                    uploadFileEmpty  : "错误:上传的图片不能为空。",
+                    formatNotAllowed : "错误:只允许上传图片文件,允许上传的图片文件格式有:"
+                },
+                preformattedText : {
+                    title             : "添加预格式文本或代码块", 
+                    emptyAlert        : "错误:请填写预格式文本或代码的内容。"
+                },
+                codeBlock : {
+                    title             : "添加代码块",                    
+                    selectLabel       : "代码语言:",
+                    selectDefaultText : "请选择代码语言",
+                    otherLanguage     : "其他语言",
+                    unselectedLanguageAlert : "错误:请选择代码所属的语言类型。",
+                    codeEmptyAlert    : "错误:请填写代码内容。"
+                },
+                htmlEntities : {
+                    title : "HTML 实体字符"
+                },
+                help : {
+                    title : "使用帮助"
+                }
+            }
+        }
+    };
+    
+    editormd.classNames  = {
+        tex : editormd.classPrefix + "tex"
+    };
+
+    editormd.dialogZindex = 99999;
+    
+    editormd.$katex       = null;
+    editormd.$marked      = null;
+    editormd.$CodeMirror  = null;
+    editormd.$prettyPrint = null;
+    
+    var timer, flowchartTimer;
+
+    editormd.prototype    = editormd.fn = {
+        state : {
+            watching   : false,
+            loaded     : false,
+            preview    : false,
+            fullscreen : false
+        },
+        
+        /**
+         * 构造函数/实例初始化
+         * Constructor / instance initialization
+         * 
+         * @param   {String}   id            编辑器的ID
+         * @param   {Object}   [options={}]  配置选项 Key/Value
+         * @returns {editormd}               返回editormd的实例对象
+         */
+        
+        init : function (id, options) {
+            
+            options              = options || {};
+            
+            if (typeof id === "object")
+            {
+                options = id;
+            }
+            
+            var _this            = this;
+            var classPrefix      = this.classPrefix  = editormd.classPrefix; 
+            var settings         = this.settings     = $.extend(true, editormd.defaults, options);
+            
+            id                   = (typeof id === "object") ? settings.id : id;
+            
+            var editor           = this.editor       = $("#" + id);
+            
+            this.id              = id;
+            this.lang            = settings.lang;
+            
+            var classNames       = this.classNames   = {
+                textarea : {
+                    html     : classPrefix + "html-textarea",
+                    markdown : classPrefix + "markdown-textarea"
+                }
+            };
+            
+            settings.pluginPath = (settings.pluginPath === "") ? settings.path + "../plugins/" : settings.pluginPath; 
+            
+            this.state.watching = (settings.watch) ? true : false;
+            
+            if ( !editor.hasClass("editormd") ) {
+                editor.addClass("editormd");
+            }
+            
+            editor.css({
+                width  : (typeof settings.width  === "number") ? settings.width  + "px" : settings.width,
+                height : (typeof settings.height === "number") ? settings.height + "px" : settings.height
+            });
+            
+            if (settings.autoHeight)
+            {
+                editor.css("height", "auto");
+            }
+                        
+            var markdownTextarea = this.markdownTextarea = editor.children("textarea");
+            
+            if (markdownTextarea.length < 1)
+            {
+                editor.append("<textarea></textarea>");
+                markdownTextarea = this.markdownTextarea = editor.children("textarea");
+            }
+            
+            markdownTextarea.addClass(classNames.textarea.markdown).attr("placeholder", settings.placeholder);
+            
+            if (typeof markdownTextarea.attr("name") === "undefined" || markdownTextarea.attr("name") === "")
+            {
+                markdownTextarea.attr("name", (settings.name !== "") ? settings.name : id + "-markdown-doc");
+            }
+            
+            var appendElements = [
+                (!settings.readOnly) ? "<a href=\"javascript:;\" class=\"fa fa-close " + classPrefix + "preview-close-btn\"></a>" : "",
+                ( (settings.saveHTMLToTextarea) ? "<textarea class=\"" + classNames.textarea.html + "\" name=\"" + id + "-html-code\"></textarea>" : "" ),
+                "<div class=\"" + classPrefix + "preview\"><div class=\"markdown-body " + classPrefix + "preview-container\"></div></div>",
+                "<div class=\"" + classPrefix + "container-mask\" style=\"display:block;\"></div>",
+                "<div class=\"" + classPrefix + "mask\"></div>"
+            ].join("\n");
+            
+            editor.append(appendElements).addClass(classPrefix + "vertical");
+            
+            if (settings.theme !== "") 
+            {
+                editor.addClass(classPrefix + "theme-" + settings.theme);
+            }
+            
+            this.mask          = editor.children("." + classPrefix + "mask");    
+            this.containerMask = editor.children("." + classPrefix  + "container-mask");
+            
+            if (settings.markdown !== "")
+            {
+                markdownTextarea.val(settings.markdown);
+            }
+            
+            if (settings.appendMarkdown !== "")
+            {
+                markdownTextarea.val(markdownTextarea.val() + settings.appendMarkdown);
+            }
+            
+            this.htmlTextarea     = editor.children("." + classNames.textarea.html);            
+            this.preview          = editor.children("." + classPrefix + "preview");
+            this.previewContainer = this.preview.children("." + classPrefix + "preview-container");
+            
+            if (settings.previewTheme !== "") 
+            {
+                this.preview.addClass(classPrefix + "preview-theme-" + settings.previewTheme);
+            }
+            
+            if (typeof define === "function" && define.amd)
+            {
+                if (typeof katex !== "undefined") 
+                {
+                    editormd.$katex = katex;
+                }
+                
+                if (settings.searchReplace && !settings.readOnly) 
+                {
+                    editormd.loadCSS(settings.path + "codemirror/addon/dialog/dialog");
+                    editormd.loadCSS(settings.path + "codemirror/addon/search/matchesonscrollbar");
+                }
+            }
+            
+            if ((typeof define === "function" && define.amd) || !settings.autoLoadModules)
+            {
+                if (typeof CodeMirror !== "undefined") {
+                    editormd.$CodeMirror = CodeMirror;
+                }
+                
+                if (typeof marked     !== "undefined") {
+                    editormd.$marked     = marked;
+                }
+                
+                this.setCodeMirror().setToolbar().loadedDisplay();
+            } 
+            else 
+            {
+                this.loadQueues();
+            }
+
+            return this;
+        },
+        
+        /**
+         * 所需组件加载队列
+         * Required components loading queue
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        loadQueues : function() {
+            var _this        = this;
+            var settings     = this.settings;
+            var loadPath     = settings.path;
+                                
+            var loadFlowChartOrSequenceDiagram = function() {
+                
+                if (editormd.isIE8) 
+                {
+                    _this.loadedDisplay();
+                    
+                    return ;
+                }
+
+                if (settings.flowChart || settings.sequenceDiagram) 
+                {
+                    editormd.loadScript(loadPath + "raphael.min", function() {
+
+                        editormd.loadScript(loadPath + "underscore.min", function() {  
+
+                            if (!settings.flowChart && settings.sequenceDiagram) 
+                            {
+                                editormd.loadScript(loadPath + "sequence-diagram.min", function() {
+                                    _this.loadedDisplay();
+                                });
+                            }
+                            else if (settings.flowChart && !settings.sequenceDiagram) 
+                            {      
+                                editormd.loadScript(loadPath + "flowchart.min", function() {  
+                                    editormd.loadScript(loadPath + "jquery.flowchart.min", function() {
+                                        _this.loadedDisplay();
+                                    });
+                                });
+                            }
+                            else if (settings.flowChart && settings.sequenceDiagram) 
+                            {  
+                                editormd.loadScript(loadPath + "flowchart.min", function() {  
+                                    editormd.loadScript(loadPath + "jquery.flowchart.min", function() {
+                                        editormd.loadScript(loadPath + "sequence-diagram.min", function() {
+                                            _this.loadedDisplay();
+                                        });
+                                    });
+                                });
+                            }
+                        });
+
+                    });
+                } 
+                else
+                {
+                    _this.loadedDisplay();
+                }
+            }; 
+
+            editormd.loadCSS(loadPath + "codemirror/codemirror.min");
+            
+            if (settings.searchReplace && !settings.readOnly)
+            {
+                editormd.loadCSS(loadPath + "codemirror/addon/dialog/dialog");
+                editormd.loadCSS(loadPath + "codemirror/addon/search/matchesonscrollbar");
+            }
+            
+            if (settings.codeFold)
+            {
+                editormd.loadCSS(loadPath + "codemirror/addon/fold/foldgutter");            
+            }
+            
+            editormd.loadScript(loadPath + "codemirror/codemirror.min", function() {
+                editormd.$CodeMirror = CodeMirror;
+                
+                editormd.loadScript(loadPath + "codemirror/modes.min", function() {
+                    
+                    editormd.loadScript(loadPath + "codemirror/addons.min", function() {
+                        
+                        _this.setCodeMirror();
+                        
+                        if (settings.mode !== "gfm" && settings.mode !== "markdown") 
+                        {
+                            _this.loadedDisplay();
+                            
+                            return false;
+                        }
+                        
+                        _this.setToolbar();
+
+                        editormd.loadScript(loadPath + "marked.min", function() {
+
+                            editormd.$marked = marked;
+                                
+                            if (settings.previewCodeHighlight) 
+                            {
+                                editormd.loadScript(loadPath + "prettify.min", function() {
+                                    loadFlowChartOrSequenceDiagram();
+                                });
+                            } 
+                            else
+                            {                  
+                                loadFlowChartOrSequenceDiagram();
+                            }
+                        });
+                        
+                    });
+                    
+                });
+                
+            });
+
+            return this;
+        },
+        
+        /**
+         * 设置 Editor.md 的整体主题,主要是工具栏
+         * Setting Editor.md theme
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setTheme : function(theme) {
+            var editor      = this.editor;
+            var oldTheme    = this.settings.theme;
+            var themePrefix = this.classPrefix + "theme-";
+            
+            editor.removeClass(themePrefix + oldTheme).addClass(themePrefix + theme);
+            
+            this.settings.theme = theme;
+            
+            return this;
+        },
+        
+        /**
+         * 设置 CodeMirror(编辑区)的主题
+         * Setting CodeMirror (Editor area) theme
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setEditorTheme : function(theme) {  
+            var settings   = this.settings;  
+            settings.editorTheme = theme;  
+            
+            if (theme !== "default")
+            {
+                editormd.loadCSS(settings.path + "codemirror/theme/" + settings.editorTheme);
+            }
+            
+            this.cm.setOption("theme", theme);
+            
+            return this;
+        },
+        
+        /**
+         * setEditorTheme() 的别名
+         * setEditorTheme() alias
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setCodeMirrorTheme : function (theme) {            
+            this.setEditorTheme(theme);
+            
+            return this;
+        },
+        
+        /**
+         * 设置 Editor.md 的主题
+         * Setting Editor.md theme
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setPreviewTheme : function(theme) {  
+            var preview     = this.preview;
+            var oldTheme    = this.settings.previewTheme;
+            var themePrefix = this.classPrefix + "preview-theme-";
+            
+            preview.removeClass(themePrefix + oldTheme).addClass(themePrefix + theme);
+            
+            this.settings.previewTheme = theme;
+            
+            return this;
+        },
+        
+        /**
+         * 配置和初始化CodeMirror组件
+         * CodeMirror initialization
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setCodeMirror : function() { 
+            var settings         = this.settings;
+            var editor           = this.editor;
+            
+            if (settings.editorTheme !== "default")
+            {
+                editormd.loadCSS(settings.path + "codemirror/theme/" + settings.editorTheme);
+            }
+            
+            var codeMirrorConfig = {
+                mode                      : settings.mode,
+                theme                     : settings.editorTheme,
+                tabSize                   : settings.tabSize,
+                dragDrop                  : false,
+                autofocus                 : settings.autoFocus,
+                autoCloseTags             : settings.autoCloseTags,
+                readOnly                  : (settings.readOnly) ? "nocursor" : false,
+                indentUnit                : settings.indentUnit,
+                lineNumbers               : settings.lineNumbers,
+                lineWrapping              : settings.lineWrapping,
+                extraKeys                 : {
+                                                "Ctrl-Q": function(cm) { 
+                                                    cm.foldCode(cm.getCursor()); 
+                                                }
+                                            },
+                foldGutter                : settings.codeFold,
+                gutters                   : ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
+                matchBrackets             : settings.matchBrackets,
+                indentWithTabs            : settings.indentWithTabs,
+                styleActiveLine           : settings.styleActiveLine,
+                styleSelectedText         : settings.styleSelectedText,
+                autoCloseBrackets         : settings.autoCloseBrackets,
+                showTrailingSpace         : settings.showTrailingSpace,
+                highlightSelectionMatches : ( (!settings.matchWordHighlight) ? false : { showToken: (settings.matchWordHighlight === "onselected") ? false : /\w/ } )
+            };
+            
+            this.codeEditor = this.cm        = editormd.$CodeMirror.fromTextArea(this.markdownTextarea[0], codeMirrorConfig);
+            this.codeMirror = this.cmElement = editor.children(".CodeMirror");
+            
+            if (settings.value !== "")
+            {
+                this.cm.setValue(settings.value);
+            }
+
+            this.codeMirror.css({
+                fontSize : settings.fontSize,
+                width    : (!settings.watch) ? "100%" : "50%"
+            });
+            
+            if (settings.autoHeight)
+            {
+                this.codeMirror.css("height", "auto");
+                this.cm.setOption("viewportMargin", Infinity);
+            }
+            
+            if (!settings.lineNumbers)
+            {
+                this.codeMirror.find(".CodeMirror-gutters").css("border-right", "none");
+            }
+
+            return this;
+        },
+        
+        /**
+         * 获取CodeMirror的配置选项
+         * Get CodeMirror setting options
+         * 
+         * @returns {Mixed}                  return CodeMirror setting option value
+         */
+        
+        getCodeMirrorOption : function(key) {            
+            return this.cm.getOption(key);
+        },
+        
+        /**
+         * 配置和重配置CodeMirror的选项
+         * CodeMirror setting options / resettings
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setCodeMirrorOption : function(key, value) {
+            
+            this.cm.setOption(key, value);
+            
+            return this;
+        },
+        
+        /**
+         * 添加 CodeMirror 键盘快捷键
+         * Add CodeMirror keyboard shortcuts key map
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        addKeyMap : function(map, bottom) {
+            this.cm.addKeyMap(map, bottom);
+            
+            return this;
+        },
+        
+        /**
+         * 移除 CodeMirror 键盘快捷键
+         * Remove CodeMirror keyboard shortcuts key map
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        removeKeyMap : function(map) {
+            this.cm.removeKeyMap(map);
+            
+            return this;
+        },
+        
+        /**
+         * 跳转到指定的行
+         * Goto CodeMirror line
+         * 
+         * @param   {String|Intiger}   line      line number or "first"|"last"
+         * @returns {editormd}                   返回editormd的实例对象
+         */
+        
+        gotoLine : function (line) {
+            
+            var settings = this.settings;
+            
+            if (!settings.gotoLine)
+            {
+                return this;
+            }
+            
+            var cm       = this.cm;
+            var editor   = this.editor;
+            var count    = cm.lineCount();
+            var preview  = this.preview;
+            
+            if (typeof line === "string")
+            {
+                if(line === "last")
+                {
+                    line = count;
+                }
+            
+                if (line === "first")
+                {
+                    line = 1;
+                }
+            }
+            
+            if (typeof line !== "number") 
+            {  
+                alert("Error: The line number must be an integer.");
+                return this;
+            }
+            
+            line  = parseInt(line) - 1;
+            
+            if (line > count)
+            {
+                alert("Error: The line number range 1-" + count);
+                
+                return this;
+            }
+            
+            cm.setCursor( {line : line, ch : 0} );
+            
+            var scrollInfo   = cm.getScrollInfo();
+            var clientHeight = scrollInfo.clientHeight; 
+            var coords       = cm.charCoords({line : line, ch : 0}, "local");
+            
+            cm.scrollTo(null, (coords.top + coords.bottom - clientHeight) / 2);
+            
+            if (settings.watch)
+            {            
+                var cmScroll  = this.codeMirror.find(".CodeMirror-scroll")[0];
+                var height    = $(cmScroll).height(); 
+                var scrollTop = cmScroll.scrollTop;         
+                var percent   = (scrollTop / cmScroll.scrollHeight);
+
+                if (scrollTop === 0)
+                {
+                    preview.scrollTop(0);
+                } 
+                else if (scrollTop + height >= cmScroll.scrollHeight - 16)
+                { 
+                    preview.scrollTop(preview[0].scrollHeight);                    
+                } 
+                else
+                {                    
+                    preview.scrollTop(preview[0].scrollHeight * percent);
+                }
+            }
+
+            cm.focus();
+            
+            return this;
+        },
+        
+        /**
+         * 扩展当前实例对象,可同时设置多个或者只设置一个
+         * Extend editormd instance object, can mutil setting.
+         * 
+         * @returns {editormd}                  this(editormd instance object.)
+         */
+        
+        extend : function() {
+            if (typeof arguments[1] !== "undefined")
+            {
+                if (typeof arguments[1] === "function")
+                {
+                    arguments[1] = $.proxy(arguments[1], this);
+                }
+
+                this[arguments[0]] = arguments[1];
+            }
+            
+            if (typeof arguments[0] === "object" && typeof arguments[0].length === "undefined")
+            {
+                $.extend(true, this, arguments[0]);
+            }
+
+            return this;
+        },
+        
+        /**
+         * 设置或扩展当前实例对象,单个设置
+         * Extend editormd instance object, one by one
+         * 
+         * @param   {String|Object}   key       option key
+         * @param   {String|Object}   value     option value
+         * @returns {editormd}                  this(editormd instance object.)
+         */
+        
+        set : function (key, value) {
+            
+            if (typeof value !== "undefined" && typeof value === "function")
+            {
+                value = $.proxy(value, this);
+            }
+            
+            this[key] = value;
+
+            return this;
+        },
+        
+        /**
+         * 重新配置
+         * Resetting editor options
+         * 
+         * @param   {String|Object}   key       option key
+         * @param   {String|Object}   value     option value
+         * @returns {editormd}                  this(editormd instance object.)
+         */
+        
+        config : function(key, value) {
+            var settings = this.settings;
+            
+            if (typeof key === "object")
+            {
+                settings = $.extend(true, settings, key);
+            }
+            
+            if (typeof key === "string")
+            {
+                settings[key] = value;
+            }
+            
+            this.settings = settings;
+            this.recreate();
+            
+            return this;
+        },
+        
+        /**
+         * 注册事件处理方法
+         * Bind editor event handle
+         * 
+         * @param   {String}     eventType      event type
+         * @param   {Function}   callback       回调函数
+         * @returns {editormd}                  this(editormd instance object.)
+         */
+        
+        on : function(eventType, callback) {
+            var settings = this.settings;
+            
+            if (typeof settings["on" + eventType] !== "undefined") 
+            {                
+                settings["on" + eventType] = $.proxy(callback, this);      
+            }
+
+            return this;
+        },
+        
+        /**
+         * 解除事件处理方法
+         * Unbind editor event handle
+         * 
+         * @param   {String}   eventType          event type
+         * @returns {editormd}                    this(editormd instance object.)
+         */
+        
+        off : function(eventType) {
+            var settings = this.settings;
+            
+            if (typeof settings["on" + eventType] !== "undefined") 
+            {
+                settings["on" + eventType] = function(){};
+            }
+            
+            return this;
+        },
+        
+        /**
+         * 显示工具栏
+         * Display toolbar
+         * 
+         * @param   {Function} [callback=function(){}] 回调函数
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        showToolbar : function(callback) {
+            var settings = this.settings;
+            
+            if(settings.readOnly) {
+                return this;
+            }
+            
+            if (settings.toolbar && (this.toolbar.length < 1 || this.toolbar.find("." + this.classPrefix + "menu").html() === "") )
+            {
+                this.setToolbar();
+            }
+            
+            settings.toolbar = true; 
+            
+            this.toolbar.show();
+            this.resize();
+            
+            $.proxy(callback || function(){}, this)();
+
+            return this;
+        },
+        
+        /**
+         * 隐藏工具栏
+         * Hide toolbar
+         * 
+         * @param   {Function} [callback=function(){}] 回调函数
+         * @returns {editormd}                         this(editormd instance object.)
+         */
+        
+        hideToolbar : function(callback) { 
+            var settings = this.settings;
+            
+            settings.toolbar = false;  
+            this.toolbar.hide();
+            this.resize();
+            
+            $.proxy(callback || function(){}, this)();
+
+            return this;
+        },
+        
+        /**
+         * 页面滚动时工具栏的固定定位
+         * Set toolbar in window scroll auto fixed position
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setToolbarAutoFixed : function(fixed) {
+            
+            var state    = this.state;
+            var editor   = this.editor;
+            var toolbar  = this.toolbar;
+            var settings = this.settings;
+            
+            if (typeof fixed !== "undefined")
+            {
+                settings.toolbarAutoFixed = fixed;
+            }
+            
+            var autoFixedHandle = function(){
+                var $window = $(window);
+                var top     = $window.scrollTop();
+                
+                if (!settings.toolbarAutoFixed)
+                {
+                    return false;
+                }
+
+                if (top - editor.offset().top > 10 && top < editor.height())
+                {
+                    toolbar.css({
+                        position : "fixed",
+                        width    : editor.width() + "px",
+                        left     : ($window.width() - editor.width()) / 2 + "px"
+                    });
+                }
+                else
+                {
+                    toolbar.css({
+                        position : "absolute",
+                        width    : "100%",
+                        left     : 0
+                    });
+                }
+            };
+            
+            if (!state.fullscreen && !state.preview && settings.toolbar && settings.toolbarAutoFixed)
+            {
+                $(window).bind("scroll", autoFixedHandle);
+            }
+
+            return this;
+        },
+        
+        /**
+         * 配置和初始化工具栏
+         * Set toolbar and Initialization
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setToolbar : function() {
+            var settings    = this.settings;  
+            
+            if(settings.readOnly) {
+                return this;
+            }
+            
+            var editor      = this.editor;
+            var preview     = this.preview;
+            var classPrefix = this.classPrefix;
+            
+            var toolbar     = this.toolbar = editor.children("." + classPrefix + "toolbar");
+            
+            if (settings.toolbar && toolbar.length < 1)
+            {            
+                var toolbarHTML = "<div class=\"" + classPrefix + "toolbar\"><div class=\"" + classPrefix + "toolbar-container\"><ul class=\"" + classPrefix + "menu\"></ul></div></div>";
+                
+                editor.append(toolbarHTML);
+                toolbar = this.toolbar = editor.children("." + classPrefix + "toolbar");
+            }
+            
+            if (!settings.toolbar) 
+            {
+                toolbar.hide();
+                
+                return this;
+            }
+            
+            toolbar.show();
+            
+            var icons       = (typeof settings.toolbarIcons === "function") ? settings.toolbarIcons() 
+                            : ((typeof settings.toolbarIcons === "string")  ? editormd.toolbarModes[settings.toolbarIcons] : settings.toolbarIcons);
+            
+            var toolbarMenu = toolbar.find("." + this.classPrefix + "menu"), menu = "";
+            var pullRight   = false;
+            
+            for (var i = 0, len = icons.length; i < len; i++)
+            {
+                var name = icons[i];
+
+                if (name === "||") 
+                { 
+                    pullRight = true;
+                } 
+                else if (name === "|")
+                {
+                    menu += "<li class=\"divider\" unselectable=\"on\">|</li>";
+                }
+                else
+                {
+                    var isHeader = (/h(\d)/.test(name));
+                    var index    = name;
+                    
+                    if (name === "watch" && !settings.watch) {
+                        index = "unwatch";
+                    }
+                    
+                    var title     = settings.lang.toolbar[index];
+                    var iconTexts = settings.toolbarIconTexts[index];
+                    var iconClass = settings.toolbarIconsClass[index];
+                    
+                    title     = (typeof title     === "undefined") ? "" : title;
+                    iconTexts = (typeof iconTexts === "undefined") ? "" : iconTexts;
+                    iconClass = (typeof iconClass === "undefined") ? "" : iconClass;
+
+                    var menuItem = pullRight ? "<li class=\"pull-right\">" : "<li>";
+                    
+                    if (typeof settings.toolbarCustomIcons[name] !== "undefined" && typeof settings.toolbarCustomIcons[name] !== "function")
+                    {
+                        menuItem += settings.toolbarCustomIcons[name];
+                    }
+                    else 
+                    {
+                        menuItem += "<a href=\"javascript:;\" title=\"" + title + "\" unselectable=\"on\">";
+                        menuItem += "<i class=\"fa " + iconClass + "\" name=\""+name+"\" unselectable=\"on\">"+((isHeader) ? name.toUpperCase() : ( (iconClass === "") ? iconTexts : "") ) + "</i>";
+                        menuItem += "</a>";
+                    }
+
+                    menuItem += "</li>";
+
+                    menu = pullRight ? menuItem + menu : menu + menuItem;
+                }
+            }
+
+            toolbarMenu.html(menu);
+            
+            toolbarMenu.find("[title=\"Lowercase\"]").attr("title", settings.lang.toolbar.lowercase);
+            toolbarMenu.find("[title=\"ucwords\"]").attr("title", settings.lang.toolbar.ucwords);
+            
+            this.setToolbarHandler();
+            this.setToolbarAutoFixed();
+
+            return this;
+        },
+        
+        /**
+         * 工具栏图标事件处理对象序列
+         * Get toolbar icons event handlers
+         * 
+         * @param   {Object}   cm    CodeMirror的实例对象
+         * @param   {String}   name  要获取的事件处理器名称
+         * @returns {Object}         返回处理对象序列
+         */
+            
+        dialogLockScreen : function() {
+            $.proxy(editormd.dialogLockScreen, this)();
+            
+            return this;
+        },
+
+        dialogShowMask : function(dialog) {
+            $.proxy(editormd.dialogShowMask, this)(dialog);
+            
+            return this;
+        },
+        
+        getToolbarHandles : function(name) {  
+            var toolbarHandlers = this.toolbarHandlers = editormd.toolbarHandlers;
+            
+            return (name && typeof toolbarIconHandlers[name] !== "undefined") ? toolbarHandlers[name] : toolbarHandlers;
+        },
+        
+        /**
+         * 工具栏图标事件处理器
+         * Bind toolbar icons event handle
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        setToolbarHandler : function() {
+            var _this               = this;
+            var settings            = this.settings;
+            
+            if (!settings.toolbar || settings.readOnly) {
+                return this;
+            }
+            
+            var toolbar             = this.toolbar;
+            var cm                  = this.cm;
+            var classPrefix         = this.classPrefix;           
+            var toolbarIcons        = this.toolbarIcons = toolbar.find("." + classPrefix + "menu > li > a");  
+            var toolbarIconHandlers = this.getToolbarHandles();  
+                
+            toolbarIcons.bind(editormd.mouseOrTouch("click", "touchend"), function(event) {
+
+                var icon                = $(this).children(".fa");
+                var name                = icon.attr("name");
+                var cursor              = cm.getCursor();
+                var selection           = cm.getSelection();
+
+                if (name === "") {
+                    return ;
+                }
+                
+                _this.activeIcon = icon;
+
+                if (typeof toolbarIconHandlers[name] !== "undefined") 
+                {
+                    $.proxy(toolbarIconHandlers[name], _this)(cm);
+                }
+                else 
+                {
+                    if (typeof settings.toolbarHandlers[name] !== "undefined") 
+                    {
+                        $.proxy(settings.toolbarHandlers[name], _this)(cm, icon, cursor, selection);
+                    }
+                }
+                
+                if (name !== "link" && name !== "reference-link" && name !== "image" && name !== "code-block" && 
+                    name !== "preformatted-text" && name !== "watch" && name !== "preview" && name !== "search" && name !== "fullscreen" && name !== "info") 
+                {
+                    cm.focus();
+                }
+
+                return false;
+
+            });
+
+            return this;
+        },
+        
+        /**
+         * 动态创建对话框
+         * Creating custom dialogs
+         * 
+         * @param   {Object} options  配置项键值对 Key/Value
+         * @returns {dialog}          返回创建的dialog的jQuery实例对象
+         */
+        
+        createDialog : function(options) {            
+            return $.proxy(editormd.createDialog, this)(options);
+        },
+        
+        /**
+         * 创建关于Editor.md的对话框
+         * Create about Editor.md dialog
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        createInfoDialog : function() {
+            var _this        = this;
+			var editor       = this.editor;
+            var classPrefix  = this.classPrefix;  
+            
+            var infoDialogHTML = [
+                "<div class=\"" + classPrefix + "dialog " + classPrefix + "dialog-info\" style=\"\">",
+                "<div class=\"" + classPrefix + "dialog-container\">",
+                "<h1><i class=\"editormd-logo editormd-logo-lg editormd-logo-color\"></i> " + editormd.title + "<small>v" + editormd.version + "</small></h1>",
+                "<p>" + this.lang.description + "</p>",
+                "<p style=\"margin: 10px 0 20px 0;\"><a href=\"" + editormd.homePage + "\" target=\"_blank\">" + editormd.homePage + " <i class=\"fa fa-external-link\"></i></a></p>",
+                "<p style=\"font-size: 0.85em;\">Copyright &copy; 2015 <a href=\"https://github.com/pandao\" target=\"_blank\" class=\"hover-link\">Pandao</a>, The <a href=\"https://github.com/pandao/editor.md/blob/master/LICENSE\" target=\"_blank\" class=\"hover-link\">MIT</a> License.</p>",
+                "</div>",
+                "<a href=\"javascript:;\" class=\"fa fa-close " + classPrefix + "dialog-close\"></a>",
+                "</div>"
+            ].join("\n");
+
+            editor.append(infoDialogHTML);
+            
+            var infoDialog  = this.infoDialog = editor.children("." + classPrefix + "dialog-info");
+
+            infoDialog.find("." + classPrefix + "dialog-close").bind(editormd.mouseOrTouch("click", "touchend"), function() {
+                _this.hideInfoDialog();
+            });
+            
+            infoDialog.css("border", (editormd.isIE8) ? "1px solid #ddd" : "").css("z-index", editormd.dialogZindex).show();
+            
+            this.infoDialogPosition();
+
+            return this;
+        },
+        
+        /**
+         * 关于Editor.md对话居中定位
+         * Editor.md dialog position handle
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        infoDialogPosition : function() {
+            var infoDialog = this.infoDialog;
+            
+			var _infoDialogPosition = function() {
+				infoDialog.css({
+					top  : ($(window).height() - infoDialog.height()) / 2 + "px",
+					left : ($(window).width()  - infoDialog.width()) / 2  + "px"
+				});
+			};
+
+			_infoDialogPosition();
+
+			$(window).resize(_infoDialogPosition);
+            
+            return this;
+        },
+        
+        /**
+         * 显示关于Editor.md
+         * Display about Editor.md dialog
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        showInfoDialog : function() {
+
+            $("html,body").css("overflow-x", "hidden");
+            
+            var _this       = this;
+			var editor      = this.editor;
+            var settings    = this.settings;         
+			var infoDialog  = this.infoDialog = editor.children("." + this.classPrefix + "dialog-info");
+            
+            if (infoDialog.length < 1)
+            {
+                this.createInfoDialog();
+            }
+            
+            this.lockScreen(true);
+            
+            this.mask.css({
+						opacity         : settings.dialogMaskOpacity,
+						backgroundColor : settings.dialogMaskBgColor
+					}).show();
+
+			infoDialog.css("z-index", editormd.dialogZindex).show();
+
+			this.infoDialogPosition();
+
+            return this;
+        },
+        
+        /**
+         * 隐藏关于Editor.md
+         * Hide about Editor.md dialog
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        hideInfoDialog : function() {            
+            $("html,body").css("overflow-x", "");
+            this.infoDialog.hide();
+            this.mask.hide();
+            this.lockScreen(false);
+
+            return this;
+        },
+        
+        /**
+         * 锁屏
+         * lock screen
+         * 
+         * @param   {Boolean}    lock    Boolean 布尔值,是否锁屏
+         * @returns {editormd}           返回editormd的实例对象
+         */
+        
+        lockScreen : function(lock) {
+            editormd.lockScreen(lock);
+            this.resize();
+
+            return this;
+        },
+        
+        /**
+         * 编辑器界面重建,用于动态语言包或模块加载等
+         * Recreate editor
+         * 
+         * @returns {editormd}  返回editormd的实例对象
+         */
+        
+        recreate : function() {
+            var _this            = this;
+            var editor           = this.editor;
+            var settings         = this.settings;
+            
+            this.codeMirror.remove();
+            
+            this.setCodeMirror();
+
+            if (!settings.readOnly) 
+            {
+                if (editor.find(".editormd-dialog").length > 0) {
+                    editor.find(".editormd-dialog").remove();
+                }
+                
+                if (settings.toolbar) 
+                {  
+                    this.getToolbarHandles();                  
+                    this.setToolbar();
+                }
+            }
+            
+            this.loadedDisplay(true);
+
+            return this;
+        },
+        
+        /**
+         * 高亮预览HTML的pre代码部分
+         * highlight of preview codes
+         * 
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        previewCodeHighlight : function() {    
+            var settings         = this.settings;
+            var previewContainer = this.previewContainer;
+            
+            if (settings.previewCodeHighlight) 
+            {
+                previewContainer.find("pre").addClass("prettyprint linenums");
+                
+                if (typeof prettyPrint !== "undefined")
+                {                    
+                    prettyPrint();
+                }
+            }
+
+            return this;
+        },
+        
+        /**
+         * 解析TeX(KaTeX)科学公式
+         * TeX(KaTeX) Renderer
+         * 
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        katexRender : function() {
+            
+            if (timer === null)
+            {
+                return this;
+            }
+            
+            this.previewContainer.find("." + editormd.classNames.tex).each(function(){
+                var tex  = $(this);
+                editormd.$katex.render(tex.text(), tex[0]);
+                
+                tex.find(".katex").css("font-size", "1.6em");
+            });   
+
+            return this;
+        },
+        
+        /**
+         * 解析和渲染流程图及时序图
+         * FlowChart and SequenceDiagram Renderer
+         * 
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        flowChartAndSequenceDiagramRender : function() {
+            var $this            = this;
+            var settings         = this.settings;
+            var previewContainer = this.previewContainer;
+            
+            if (editormd.isIE8) {
+                return this;
+            }
+
+            if (settings.flowChart) {
+                if (flowchartTimer === null) {
+                    return this;
+                }
+                
+                previewContainer.find(".flowchart").flowChart(); 
+            }
+
+            if (settings.sequenceDiagram) {
+                previewContainer.find(".sequence-diagram").sequenceDiagram({theme: "simple"});
+            }
+                    
+            var preview    = $this.preview;
+            var codeMirror = $this.codeMirror;
+            var codeView   = codeMirror.find(".CodeMirror-scroll");
+
+            var height    = codeView.height();
+            var scrollTop = codeView.scrollTop();                    
+            var percent   = (scrollTop / codeView[0].scrollHeight);
+            var tocHeight = 0;
+
+            preview.find(".markdown-toc-list").each(function(){
+                tocHeight += $(this).height();
+            });
+
+            var tocMenuHeight = preview.find(".editormd-toc-menu").height(); 
+            tocMenuHeight = (!tocMenuHeight) ? 0 : tocMenuHeight;
+
+            if (scrollTop === 0) 
+            {
+                preview.scrollTop(0);
+            } 
+            else if (scrollTop + height >= codeView[0].scrollHeight - 16)
+            { 
+                preview.scrollTop(preview[0].scrollHeight);                        
+            } 
+            else
+            {                  
+                preview.scrollTop((preview[0].scrollHeight + tocHeight + tocMenuHeight) * percent);
+            }
+
+            return this;
+        },
+        
+        /**
+         * 注册键盘快捷键处理
+         * Register CodeMirror keyMaps (keyboard shortcuts).
+         * 
+         * @param   {Object}    keyMap      KeyMap key/value {"(Ctrl/Shift/Alt)-Key" : function(){}}
+         * @returns {editormd}              return this
+         */
+        
+        registerKeyMaps : function(keyMap) {
+            
+            var _this           = this;
+            var cm              = this.cm;
+            var settings        = this.settings;
+            var toolbarHandlers = editormd.toolbarHandlers;
+            var disabledKeyMaps = settings.disabledKeyMaps;
+            
+            keyMap              = keyMap || null;
+            
+            if (keyMap)
+            {
+                for (var i in keyMap)
+                {
+                    if ($.inArray(i, disabledKeyMaps) < 0)
+                    {
+                        var map = {};
+                        map[i]  = keyMap[i];
+
+                        cm.addKeyMap(keyMap);
+                    }
+                }
+            }
+            else
+            {
+                for (var k in editormd.keyMaps)
+                {
+                    var _keyMap = editormd.keyMaps[k];
+                    var handle = (typeof _keyMap === "string") ? $.proxy(toolbarHandlers[_keyMap], _this) : $.proxy(_keyMap, _this);
+                    
+                    if ($.inArray(k, ["F9", "F10", "F11"]) < 0 && $.inArray(k, disabledKeyMaps) < 0)
+                    {
+                        var _map = {};
+                        _map[k] = handle;
+
+                        cm.addKeyMap(_map);
+                    }
+                }
+                
+                $(window).keydown(function(event) {
+                    
+                    var keymaps = {
+                        "120" : "F9",
+                        "121" : "F10",
+                        "122" : "F11"
+                    };
+                    
+                    if ( $.inArray(keymaps[event.keyCode], disabledKeyMaps) < 0 )
+                    {
+                        switch (event.keyCode)
+                        {
+                            case 120:
+                                    $.proxy(toolbarHandlers["watch"], _this)();
+                                    return false;
+                                break;
+                                
+                            case 121:
+                                    $.proxy(toolbarHandlers["preview"], _this)();
+                                    return false;
+                                break;
+                                
+                            case 122:
+                                    $.proxy(toolbarHandlers["fullscreen"], _this)();                        
+                                    return false;
+                                break;
+                                
+                            default:
+                                break;
+                        }
+                    }
+                });
+            }
+
+            return this;
+        },
+        
+        /**
+         * 绑定同步滚动
+         * 
+         * @returns {editormd} return this
+         */
+        
+        bindScrollEvent : function() {
+            
+            var _this            = this;
+            var preview          = this.preview;
+            var settings         = this.settings;
+            var codeMirror       = this.codeMirror;
+            var mouseOrTouch     = editormd.mouseOrTouch;
+            
+            if (!settings.syncScrolling) {
+                return this;
+            }
+                
+            var cmBindScroll = function() {    
+                codeMirror.find(".CodeMirror-scroll").bind(mouseOrTouch("scroll", "touchmove"), function(event) {
+                    var height    = $(this).height();
+                    var scrollTop = $(this).scrollTop();                    
+                    var percent   = (scrollTop / $(this)[0].scrollHeight);
+                    
+                    var tocHeight = 0;
+                    
+                    preview.find(".markdown-toc-list").each(function(){
+                        tocHeight += $(this).height();
+                    });
+                    
+                    var tocMenuHeight = preview.find(".editormd-toc-menu").height();
+                    tocMenuHeight = (!tocMenuHeight) ? 0 : tocMenuHeight;
+
+                    if (scrollTop === 0) 
+                    {
+                        preview.scrollTop(0);
+                    } 
+                    else if (scrollTop + height >= $(this)[0].scrollHeight - 16)
+                    { 
+                        preview.scrollTop(preview[0].scrollHeight);                        
+                    } 
+                    else
+                    {
+                        preview.scrollTop((preview[0].scrollHeight  + tocHeight + tocMenuHeight) * percent);
+                    }
+                    
+                    $.proxy(settings.onscroll, _this)(event);
+                });
+            };
+
+            var cmUnbindScroll = function() {
+                codeMirror.find(".CodeMirror-scroll").unbind(mouseOrTouch("scroll", "touchmove"));
+            };
+
+            var previewBindScroll = function() {
+                
+                preview.bind(mouseOrTouch("scroll", "touchmove"), function(event) {
+                    var height    = $(this).height();
+                    var scrollTop = $(this).scrollTop();         
+                    var percent   = (scrollTop / $(this)[0].scrollHeight);
+                    var codeView  = codeMirror.find(".CodeMirror-scroll");
+
+                    if(scrollTop === 0) 
+                    {
+                        codeView.scrollTop(0);
+                    }
+                    else if (scrollTop + height >= $(this)[0].scrollHeight)
+                    {
+                        codeView.scrollTop(codeView[0].scrollHeight);                        
+                    }
+                    else 
+                    {
+                        codeView.scrollTop(codeView[0].scrollHeight * percent);
+                    }
+                    
+                    $.proxy(settings.onpreviewscroll, _this)(event);
+                });
+
+            };
+
+            var previewUnbindScroll = function() {
+                preview.unbind(mouseOrTouch("scroll", "touchmove"));
+            }; 
+
+			codeMirror.bind({
+				mouseover  : cmBindScroll,
+				mouseout   : cmUnbindScroll,
+				touchstart : cmBindScroll,
+				touchend   : cmUnbindScroll
+			});
+            
+            if (settings.syncScrolling === "single") {
+                return this;
+            }
+            
+			preview.bind({
+				mouseover  : previewBindScroll,
+				mouseout   : previewUnbindScroll,
+				touchstart : previewBindScroll,
+				touchend   : previewUnbindScroll
+			});
+
+            return this;
+        },
+        
+        bindChangeEvent : function() {
+            
+            var _this            = this;
+            var cm               = this.cm;
+            var settings         = this.settings;
+            
+            if (!settings.syncScrolling) {
+                return this;
+            }
+            
+            cm.on("change", function(_cm, changeObj) {
+                
+                if (settings.watch)
+                {
+                    _this.previewContainer.css("padding", settings.autoHeight ? "20px 20px 50px 40px" : "20px");
+                }
+                
+                timer = setTimeout(function() {
+                    clearTimeout(timer);
+                    _this.save();
+                    timer = null;
+                }, settings.delay);
+            });
+
+            return this;
+        },
+        
+        /**
+         * 加载队列完成之后的显示处理
+         * Display handle of the module queues loaded after.
+         * 
+         * @param   {Boolean}   recreate   是否为重建编辑器
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        loadedDisplay : function(recreate) {
+            
+            recreate             = recreate || false;
+            
+            var _this            = this;
+            var editor           = this.editor;
+            var preview          = this.preview;
+            var settings         = this.settings;
+            
+            this.containerMask.hide();
+            
+            this.save();
+            
+            if (settings.watch) {
+                preview.show();
+            }
+            
+            editor.data("oldWidth", editor.width()).data("oldHeight", editor.height()); // 为了兼容Zepto
+            
+            this.resize();
+            this.registerKeyMaps();
+            
+            $(window).resize(function(){
+                _this.resize();
+            });
+            
+            this.bindScrollEvent().bindChangeEvent();
+            
+            if (!recreate)
+            {
+                $.proxy(settings.onload, this)();
+            }
+            
+            this.state.loaded = true;
+
+            return this;
+        },
+        
+        /**
+         * 设置编辑器的宽度
+         * Set editor width
+         * 
+         * @param   {Number|String} width  编辑器宽度值
+         * @returns {editormd}             返回editormd的实例对象
+         */
+        
+        width : function(width) {
+                
+            this.editor.css("width", (typeof width === "number") ? width  + "px" : width);            
+            this.resize();
+            
+            return this;
+        },
+        
+        /**
+         * 设置编辑器的高度
+         * Set editor height
+         * 
+         * @param   {Number|String} height  编辑器高度值
+         * @returns {editormd}              返回editormd的实例对象
+         */
+        
+        height : function(height) {
+                
+            this.editor.css("height", (typeof height === "number")  ? height  + "px" : height);            
+            this.resize();
+            
+            return this;
+        },
+        
+        /**
+         * 调整编辑器的尺寸和布局
+         * Resize editor layout
+         * 
+         * @param   {Number|String} [width=null]  编辑器宽度值
+         * @param   {Number|String} [height=null] 编辑器高度值
+         * @returns {editormd}                    返回editormd的实例对象
+         */
+        
+        resize : function(width, height) {
+            
+            width  = width  || null;
+            height = height || null;
+            
+            var state      = this.state;
+            var editor     = this.editor;
+            var preview    = this.preview;
+            var toolbar    = this.toolbar;
+            var settings   = this.settings;
+            var codeMirror = this.codeMirror;
+            
+            if (width)
+            {
+                editor.css("width", (typeof width  === "number") ? width  + "px" : width);
+            }
+            
+            if (settings.autoHeight && !state.fullscreen && !state.preview)
+            {
+                editor.css("height", "auto");
+                codeMirror.css("height", "auto");
+            } 
+            else 
+            {
+                if (height) 
+                {
+                    editor.css("height", (typeof height === "number") ? height + "px" : height);
+                }
+                
+                if (state.fullscreen)
+                {
+                    editor.height($(window).height());
+                }
+
+                if (settings.toolbar && !settings.readOnly) 
+                {
+                    codeMirror.css("margin-top", toolbar.height() + 1).height(editor.height() - toolbar.height());
+                } 
+                else
+                {
+                    codeMirror.css("margin-top", 0).height(editor.height());
+                }
+            }
+            
+            if(settings.watch) 
+            {
+                codeMirror.width(editor.width() / 2);
+                preview.width((!state.preview) ? editor.width() / 2 : editor.width());
+                
+                this.previewContainer.css("padding", settings.autoHeight ? "20px 20px 50px 40px" : "20px");
+                
+                if (settings.toolbar && !settings.readOnly) 
+                {
+                    preview.css("top", toolbar.height() + 1);
+                } 
+                else 
+                {
+                    preview.css("top", 0);
+                }
+                
+                if (settings.autoHeight && !state.fullscreen && !state.preview)
+                {
+                    preview.height("");
+                }
+                else
+                {                
+                    var previewHeight = (settings.toolbar && !settings.readOnly) ? editor.height() - toolbar.height() : editor.height();
+                    
+                    preview.height(previewHeight);
+                }
+            } 
+            else 
+            {
+                codeMirror.width(editor.width());
+                preview.hide();
+            }
+            
+            if (state.loaded) 
+            {
+                $.proxy(settings.onresize, this)();
+            }
+
+            return this;
+        },
+        
+        /**
+         * 解析和保存Markdown代码
+         * Parse & Saving Markdown source code
+         * 
+         * @returns {editormd}     返回editormd的实例对象
+         */
+        
+        save : function() {
+            
+            if (timer === null)
+            {
+                return this;
+            }
+            
+            var _this            = this;
+            var state            = this.state;
+            var settings         = this.settings;
+            var cm               = this.cm;            
+            var cmValue          = cm.getValue();
+            var previewContainer = this.previewContainer;
+
+            if (settings.mode !== "gfm" && settings.mode !== "markdown") 
+            {
+                this.markdownTextarea.val(cmValue);
+                
+                return this;
+            }
+            
+            var marked          = editormd.$marked;
+            var markdownToC     = this.markdownToC = [];            
+            var rendererOptions = this.markedRendererOptions = {  
+                toc                  : settings.toc,
+                tocm                 : settings.tocm,
+                tocStartLevel        : settings.tocStartLevel,
+                pageBreak            : settings.pageBreak,
+                taskList             : settings.taskList,
+                emoji                : settings.emoji,
+                tex                  : settings.tex,
+                atLink               : settings.atLink,           // for @link
+                emailLink            : settings.emailLink,        // for mail address auto link
+                flowChart            : settings.flowChart,
+                sequenceDiagram      : settings.sequenceDiagram,
+                previewCodeHighlight : settings.previewCodeHighlight,
+            };
+            
+            var markedOptions = this.markedOptions = {
+                renderer    : editormd.markedRenderer(markdownToC, rendererOptions),
+                gfm         : true,
+                tables      : true,
+                breaks      : true,
+                pedantic    : false,
+                sanitize    : (settings.htmlDecode) ? false : true,  // 关闭忽略HTML标签,即开启识别HTML标签,默认为false
+                smartLists  : true,
+                smartypants : true
+            };
+            
+            marked.setOptions(markedOptions);
+                    
+            var newMarkdownDoc = editormd.$marked(cmValue, markedOptions);
+            
+            //console.info("cmValue", cmValue, newMarkdownDoc);
+            
+            newMarkdownDoc = editormd.filterHTMLTags(newMarkdownDoc, settings.htmlDecode);
+            
+            //console.error("cmValue", cmValue, newMarkdownDoc);
+            
+            this.markdownTextarea.text(cmValue);
+            
+            cm.save();
+            
+            if (settings.saveHTMLToTextarea) 
+            {
+                this.htmlTextarea.text(newMarkdownDoc);
+            }
+            
+            if(settings.watch || (!settings.watch && state.preview))
+            {
+                previewContainer.html(newMarkdownDoc);
+
+                this.previewCodeHighlight();
+                
+                if (settings.toc) 
+                {
+                    var tocContainer = (settings.tocContainer === "") ? previewContainer : $(settings.tocContainer);
+                    var tocMenu      = tocContainer.find("." + this.classPrefix + "toc-menu");
+                    
+                    tocContainer.attr("previewContainer", (settings.tocContainer === "") ? "true" : "false");
+                    
+                    if (settings.tocContainer !== "" && tocMenu.length > 0)
+                    {
+                        tocMenu.remove();
+                    }
+                    
+                    editormd.markdownToCRenderer(markdownToC, tocContainer, settings.tocDropdown, settings.tocStartLevel);
+            
+                    if (settings.tocDropdown || tocContainer.find("." + this.classPrefix + "toc-menu").length > 0)
+                    {
+                        editormd.tocDropdownMenu(tocContainer, (settings.tocTitle !== "") ? settings.tocTitle : this.lang.tocTitle);
+                    }
+            
+                    if (settings.tocContainer !== "")
+                    {
+                        previewContainer.find(".markdown-toc").css("border", "none");
+                    }
+                }
+                
+                if (settings.tex)
+                {
+                    if (!editormd.kaTeXLoaded && settings.autoLoadModules) 
+                    {
+                        editormd.loadKaTeX(function() {
+                            editormd.$katex = katex;
+                            editormd.kaTeXLoaded = true;
+                            _this.katexRender();
+                        });
+                    } 
+                    else 
+                    {
+                        editormd.$katex = katex;
+                        this.katexRender();
+                    }
+                }                
+                
+                if (settings.flowChart || settings.sequenceDiagram)
+                {
+                    flowchartTimer = setTimeout(function(){
+                        clearTimeout(flowchartTimer);
+                        _this.flowChartAndSequenceDiagramRender();
+                        flowchartTimer = null;
+                    }, 10);
+                }
+
+                if (state.loaded) 
+                {
+                    $.proxy(settings.onchange, this)();
+                }
+            }
+
+            return this;
+        },
+        
+        /**
+         * 聚焦光标位置
+         * Focusing the cursor position
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        focus : function() {
+            this.cm.focus();
+
+            return this;
+        },
+        
+        /**
+         * 设置光标的位置
+         * Set cursor position
+         * 
+         * @param   {Object}    cursor 要设置的光标位置键值对象,例:{line:1, ch:0}
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        setCursor : function(cursor) {
+            this.cm.setCursor(cursor);
+
+            return this;
+        },
+        
+        /**
+         * 获取当前光标的位置
+         * Get the current position of the cursor
+         * 
+         * @returns {Cursor}         返回一个光标Cursor对象
+         */
+        
+        getCursor : function() {
+            return this.cm.getCursor();
+        },
+        
+        /**
+         * 设置光标选中的范围
+         * Set cursor selected ranges
+         * 
+         * @param   {Object}    from   开始位置的光标键值对象,例:{line:1, ch:0}
+         * @param   {Object}    to     结束位置的光标键值对象,例:{line:1, ch:0}
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        setSelection : function(from, to) {
+        
+            this.cm.setSelection(from, to);
+        
+            return this;
+        },
+        
+        /**
+         * 获取光标选中的文本
+         * Get the texts from cursor selected
+         * 
+         * @returns {String}         返回选中文本的字符串形式
+         */
+        
+        getSelection : function() {
+            return this.cm.getSelection();
+        },
+        
+        /**
+         * 设置光标选中的文本范围
+         * Set the cursor selection ranges
+         * 
+         * @param   {Array}    ranges  cursor selection ranges array
+         * @returns {Array}            return this
+         */
+        
+        setSelections : function(ranges) {
+            this.cm.setSelections(ranges);
+            
+            return this;
+        },
+        
+        /**
+         * 获取光标选中的文本范围
+         * Get the cursor selection ranges
+         * 
+         * @returns {Array}         return selection ranges array
+         */
+        
+        getSelections : function() {
+            return this.cm.getSelections();
+        },
+        
+        /**
+         * 替换当前光标选中的文本或在当前光标处插入新字符
+         * Replace the text at the current cursor selected or insert a new character at the current cursor position
+         * 
+         * @param   {String}    value  要插入的字符值
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        replaceSelection : function(value) {
+            this.cm.replaceSelection(value);
+
+            return this;
+        },
+        
+        /**
+         * 在当前光标处插入新字符
+         * Insert a new character at the current cursor position
+         *
+         * 同replaceSelection()方法
+         * With the replaceSelection() method
+         * 
+         * @param   {String}    value  要插入的字符值
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        insertValue : function(value) {
+            this.replaceSelection(value);
+
+            return this;
+        },
+        
+        /**
+         * 追加markdown
+         * append Markdown to editor
+         * 
+         * @param   {String}    md     要追加的markdown源文档
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        appendMarkdown : function(md) {
+            var settings = this.settings;
+            var cm       = this.cm;
+            
+            cm.setValue(cm.getValue() + md);
+            
+            return this;
+        },
+        
+        /**
+         * 设置和传入编辑器的markdown源文档
+         * Set Markdown source document
+         * 
+         * @param   {String}    md     要传入的markdown源文档
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        setMarkdown : function(md) {
+            this.cm.setValue(md || this.settings.markdown);
+            
+            return this;
+        },
+        
+        /**
+         * 获取编辑器的markdown源文档
+         * Set Editor.md markdown/CodeMirror value
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        getMarkdown : function() {
+            return this.cm.getValue();
+        },
+        
+        /**
+         * 获取编辑器的源文档
+         * Get CodeMirror value
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        getValue : function() {
+            return this.cm.getValue();
+        },
+        
+        /**
+         * 设置编辑器的源文档
+         * Set CodeMirror value
+         * 
+         * @param   {String}     value   set code/value/string/text
+         * @returns {editormd}           返回editormd的实例对象
+         */
+        
+        setValue : function(value) {
+            this.cm.setValue(value);
+            
+            return this;
+        },
+        
+        /**
+         * 清空编辑器
+         * Empty CodeMirror editor container
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        clear : function() {
+            this.cm.setValue("");
+            
+            return this;
+        },
+        
+        /**
+         * 获取解析后存放在Textarea的HTML源码
+         * Get parsed html code from Textarea
+         * 
+         * @returns {String}               返回HTML源码
+         */
+        
+        getHTML : function() {
+            if (!this.settings.saveHTMLToTextarea)
+            {
+                alert("Error: settings.saveHTMLToTextarea == false");
+
+                return false;
+            }
+            
+            return this.htmlTextarea.val();
+        },
+        
+        /**
+         * getHTML()的别名
+         * getHTML (alias)
+         * 
+         * @returns {String}           Return html code 返回HTML源码
+         */
+        
+        getTextareaSavedHTML : function() {
+            return this.getHTML();
+        },
+        
+        /**
+         * 获取预览窗口的HTML源码
+         * Get html from preview container
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        getPreviewedHTML : function() {
+            if (!this.settings.watch)
+            {
+                alert("Error: settings.watch == false");
+
+                return false;
+            }
+            
+            return this.previewContainer.html();
+        },
+        
+        /**
+         * 开启实时预览
+         * Enable real-time watching
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        watch : function(callback) {     
+            var settings        = this.settings;
+            
+            if ($.inArray(settings.mode, ["gfm", "markdown"]) < 0)
+            {
+                return this;
+            }
+            
+            this.state.watching = settings.watch = true;
+            this.preview.show();
+            
+            if (this.toolbar)
+            {
+                var watchIcon   = settings.toolbarIconsClass.watch;
+                var unWatchIcon = settings.toolbarIconsClass.unwatch;
+                
+                var icon        = this.toolbar.find(".fa[name=watch]");
+                icon.parent().attr("title", settings.lang.toolbar.watch);
+                icon.removeClass(unWatchIcon).addClass(watchIcon);
+            }
+            
+            this.codeMirror.css("border-right", "1px solid #ddd").width(this.editor.width() / 2); 
+            
+            timer = 0;
+            
+            this.save().resize();
+            
+            if (!settings.onwatch)
+            {
+                settings.onwatch = callback || function() {};
+            }
+            
+            $.proxy(settings.onwatch, this)();
+            
+            return this;
+        },
+        
+        /**
+         * 关闭实时预览
+         * Disable real-time watching
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        unwatch : function(callback) {
+            var settings        = this.settings;
+            this.state.watching = settings.watch = false;
+            this.preview.hide();
+            
+            if (this.toolbar) 
+            {
+                var watchIcon   = settings.toolbarIconsClass.watch;
+                var unWatchIcon = settings.toolbarIconsClass.unwatch;
+                
+                var icon    = this.toolbar.find(".fa[name=watch]");
+                icon.parent().attr("title", settings.lang.toolbar.unwatch);
+                icon.removeClass(watchIcon).addClass(unWatchIcon);
+            }
+            
+            this.codeMirror.css("border-right", "none").width(this.editor.width());
+            
+            this.resize();
+            
+            if (!settings.onunwatch)
+            {
+                settings.onunwatch = callback || function() {};
+            }
+            
+            $.proxy(settings.onunwatch, this)();
+            
+            return this;
+        },
+        
+        /**
+         * 显示编辑器
+         * Show editor
+         * 
+         * @param   {Function} [callback=function()] 回调函数
+         * @returns {editormd}                       返回editormd的实例对象
+         */
+        
+        show : function(callback) {
+            callback  = callback || function() {};
+            
+            var _this = this;
+            this.editor.show(0, function() {
+                $.proxy(callback, _this)();
+            });
+            
+            return this;
+        },
+        
+        /**
+         * 隐藏编辑器
+         * Hide editor
+         * 
+         * @param   {Function} [callback=function()] 回调函数
+         * @returns {editormd}                       返回editormd的实例对象
+         */
+        
+        hide : function(callback) {
+            callback  = callback || function() {};
+            
+            var _this = this;
+            this.editor.hide(0, function() {
+                $.proxy(callback, _this)();
+            });
+            
+            return this;
+        },
+        
+        /**
+         * 隐藏编辑器部分,只预览HTML
+         * Enter preview html state
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        previewing : function() {
+            
+            var _this            = this;
+            var editor           = this.editor;
+            var preview          = this.preview;
+            var toolbar          = this.toolbar;
+            var settings         = this.settings;
+            var codeMirror       = this.codeMirror;
+            var previewContainer = this.previewContainer;
+            
+            if ($.inArray(settings.mode, ["gfm", "markdown"]) < 0) {
+                return this;
+            }
+            
+            if (settings.toolbar && toolbar) {
+                toolbar.toggle();
+                toolbar.find(".fa[name=preview]").toggleClass("active");
+            }
+            
+            codeMirror.toggle();
+            
+            var escHandle = function(event) {
+                if (event.shiftKey && event.keyCode === 27) {
+                    _this.previewed();
+                }
+            };
+
+            if (codeMirror.css("display") === "none") // 为了兼容Zepto,而不使用codeMirror.is(":hidden")
+            {
+                this.state.preview = true;
+
+                if (this.state.fullscreen) {
+                    preview.css("background", "#fff");
+                }
+                
+                editor.find("." + this.classPrefix + "preview-close-btn").show().bind(editormd.mouseOrTouch("click", "touchend"), function(){
+                    _this.previewed();
+                });
+            
+                if (!settings.watch)
+                {
+                    this.save();
+                } 
+                else 
+                {
+                    previewContainer.css("padding", "");
+                }
+                
+                previewContainer.addClass(this.classPrefix + "preview-active");
+
+                preview.show().css({
+                    position  : "",
+                    top       : 0,
+                    width     : editor.width(),
+                    height    : (settings.autoHeight && !this.state.fullscreen) ? "auto" : editor.height()
+                });
+                
+                if (this.state.loaded)
+                {
+                    $.proxy(settings.onpreviewing, this)();
+                }
+
+                $(window).bind("keyup", escHandle);
+            } 
+            else 
+            {
+                $(window).unbind("keyup", escHandle);
+                this.previewed();
+            }
+        },
+        
+        /**
+         * 显示编辑器部分,退出只预览HTML
+         * Exit preview html state
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        previewed : function() {
+            
+            var editor           = this.editor;
+            var preview          = this.preview;
+            var toolbar          = this.toolbar;
+            var settings         = this.settings;
+            var previewContainer = this.previewContainer;
+            var previewCloseBtn  = editor.find("." + this.classPrefix + "preview-close-btn");
+
+            this.state.preview   = false;
+            
+            this.codeMirror.show();
+            
+            if (settings.toolbar) {
+                toolbar.show();
+            }
+            
+            preview[(settings.watch) ? "show" : "hide"]();
+            
+            previewCloseBtn.hide().unbind(editormd.mouseOrTouch("click", "touchend"));
+                
+            previewContainer.removeClass(this.classPrefix + "preview-active");
+                
+            if (settings.watch)
+            {
+                previewContainer.css("padding", "20px");
+            }
+            
+            preview.css({ 
+                background : null,
+                position   : "absolute",
+                width      : editor.width() / 2,
+                height     : (settings.autoHeight && !this.state.fullscreen) ? "auto" : editor.height() - toolbar.height(),
+                top        : (settings.toolbar)    ? toolbar.height() : 0
+            });
+
+            if (this.state.loaded)
+            {
+                $.proxy(settings.onpreviewed, this)();
+            }
+            
+            return this;
+        },
+        
+        /**
+         * 编辑器全屏显示
+         * Fullscreen show
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        fullscreen : function() {
+            
+            var _this            = this;
+            var state            = this.state;
+            var editor           = this.editor;
+            var preview          = this.preview;
+            var toolbar          = this.toolbar;
+            var settings         = this.settings;
+            var fullscreenClass  = this.classPrefix + "fullscreen";
+            
+            if (toolbar) {
+                toolbar.find(".fa[name=fullscreen]").parent().toggleClass("active"); 
+            }
+            
+            var escHandle = function(event) {
+                if (!event.shiftKey && event.keyCode === 27) 
+                {
+                    if (state.fullscreen)
+                    {
+                        _this.fullscreenExit();
+                    }
+                }
+            };
+
+            if (!editor.hasClass(fullscreenClass)) 
+            {
+                state.fullscreen = true;
+
+                $("html,body").css("overflow", "hidden");
+                
+                editor.css({
+                    width    : $(window).width(),
+                    height   : $(window).height()
+                }).addClass(fullscreenClass);
+
+                this.resize();
+    
+                $.proxy(settings.onfullscreen, this)();
+
+                $(window).bind("keyup", escHandle);
+            }
+            else
+            {           
+                $(window).unbind("keyup", escHandle); 
+                this.fullscreenExit();
+            }
+
+            return this;
+        },
+        
+        /**
+         * 编辑器退出全屏显示
+         * Exit fullscreen state
+         * 
+         * @returns {editormd}         返回editormd的实例对象
+         */
+        
+        fullscreenExit : function() {
+            
+            var editor            = this.editor;
+            var settings          = this.settings;
+            var toolbar           = this.toolbar;
+            var fullscreenClass   = this.classPrefix + "fullscreen";  
+            
+            this.state.fullscreen = false;
+            
+            if (toolbar) {
+                toolbar.find(".fa[name=fullscreen]").parent().removeClass("active"); 
+            }
+
+            $("html,body").css("overflow", "");
+
+            editor.css({
+                width    : editor.data("oldWidth"),
+                height   : editor.data("oldHeight")
+            }).removeClass(fullscreenClass);
+
+            this.resize();
+            
+            $.proxy(settings.onfullscreenExit, this)();
+
+            return this;
+        },
+        
+        /**
+         * 加载并执行插件
+         * Load and execute the plugin
+         * 
+         * @param   {String}     name    plugin name / function name
+         * @param   {String}     path    plugin load path
+         * @returns {editormd}           返回editormd的实例对象
+         */
+        
+        executePlugin : function(name, path) {
+            
+            var _this    = this;
+            var cm       = this.cm;
+            var settings = this.settings;
+            
+            path = settings.pluginPath + path;
+            
+            if (typeof define === "function") 
+            {            
+                if (typeof this[name] === "undefined")
+                {
+                    alert("Error: " + name + " plugin is not found, you are not load this plugin.");
+                    
+                    return this;
+                }
+                
+                this[name](cm);
+                
+                return this;
+            }
+            
+            if ($.inArray(path, editormd.loadFiles.plugin) < 0)
+            {
+                editormd.loadPlugin(path, function() {
+                    editormd.loadPlugins[name] = _this[name];
+                    _this[name](cm);
+                });
+            }
+            else
+            {
+                $.proxy(editormd.loadPlugins[name], this)(cm);
+            }
+            
+            return this;
+        },
+                
+        /**
+         * 搜索替换
+         * Search & replace
+         * 
+         * @param   {String}     command    CodeMirror serach commands, "find, fintNext, fintPrev, clearSearch, replace, replaceAll"
+         * @returns {editormd}              return this
+         */
+        
+        search : function(command) {
+            var settings = this.settings;
+            
+            if (!settings.searchReplace)
+            {
+                alert("Error: settings.searchReplace == false");
+                return this;
+            }
+            
+            if (!settings.readOnly)
+            {
+                this.cm.execCommand(command || "find");
+            }
+            
+            return this;
+        },
+        
+        searchReplace : function() {            
+            this.search("replace");
+            
+            return this;
+        },
+        
+        searchReplaceAll : function() {          
+            this.search("replaceAll");
+            
+            return this;
+        }
+    };
+    
+    editormd.fn.init.prototype = editormd.fn; 
+   
+    /**
+     * 锁屏
+     * lock screen when dialog opening
+     * 
+     * @returns {void}
+     */
+
+    editormd.dialogLockScreen = function() {
+        var settings = this.settings || {dialogLockScreen : true};
+        
+        if (settings.dialogLockScreen) 
+        {            
+            $("html,body").css("overflow", "hidden");
+            this.resize();
+        }
+    };
+   
+    /**
+     * 显示透明背景层
+     * Display mask layer when dialog opening
+     * 
+     * @param   {Object}     dialog    dialog jQuery object
+     * @returns {void}
+     */
+    
+    editormd.dialogShowMask = function(dialog) {
+        var editor   = this.editor;
+        var settings = this.settings || {dialogShowMask : true};
+        
+        dialog.css({
+            top  : ($(window).height() - dialog.height()) / 2 + "px",
+            left : ($(window).width()  - dialog.width())  / 2 + "px"
+        });
+
+        if (settings.dialogShowMask) {
+            editor.children("." + this.classPrefix + "mask").css("z-index", parseInt(dialog.css("z-index")) - 1).show();
+        }
+    };
+
+    editormd.toolbarHandlers = {
+        undo : function() {
+            this.cm.undo();
+        },
+        
+        redo : function() {
+            this.cm.redo();
+        },
+        
+        bold : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("**" + selection + "**");
+
+            if(selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 2);
+            }
+        },
+        
+        del : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("~~" + selection + "~~");
+
+            if(selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 2);
+            }
+        },
+
+        italic : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("*" + selection + "*");
+
+            if(selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+
+        quote : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (cursor.ch !== 0)
+            {
+                cm.setCursor(cursor.line, 0);
+                cm.replaceSelection("> " + selection);
+                cm.setCursor(cursor.line, cursor.ch + 2);
+            }
+            else
+            {
+                cm.replaceSelection("> " + selection);
+            }
+
+            //cm.replaceSelection("> " + selection);
+            //cm.setCursor(cursor.line, (selection === "") ? cursor.ch + 2 : cursor.ch + selection.length + 2);
+        },
+        
+        ucfirst : function() {
+            var cm         = this.cm;
+            var selection  = cm.getSelection();
+            var selections = cm.listSelections();
+
+            cm.replaceSelection(editormd.firstUpperCase(selection));
+            cm.setSelections(selections);
+        },
+        
+        ucwords : function() {
+            var cm         = this.cm;
+            var selection  = cm.getSelection();
+            var selections = cm.listSelections();
+
+            cm.replaceSelection(editormd.wordsFirstUpperCase(selection));
+            cm.setSelections(selections);
+        },
+        
+        uppercase : function() {
+            var cm         = this.cm;
+            var selection  = cm.getSelection();
+            var selections = cm.listSelections();
+
+            cm.replaceSelection(selection.toUpperCase());
+            cm.setSelections(selections);
+        },
+        
+        lowercase : function() {
+            var cm         = this.cm;
+            var cursor     = cm.getCursor();
+            var selection  = cm.getSelection();
+            var selections = cm.listSelections();
+            
+            cm.replaceSelection(selection.toLowerCase());
+            cm.setSelections(selections);
+        },
+
+        h1 : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (cursor.ch !== 0)
+            {
+                cm.setCursor(cursor.line, 0);
+                cm.replaceSelection("# " + selection);
+                cm.setCursor(cursor.line, cursor.ch + 2);
+            }
+            else
+            {
+                cm.replaceSelection("# " + selection);
+            }
+        },
+
+        h2 : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (cursor.ch !== 0)
+            {
+                cm.setCursor(cursor.line, 0);
+                cm.replaceSelection("## " + selection);
+                cm.setCursor(cursor.line, cursor.ch + 3);
+            }
+            else
+            {
+                cm.replaceSelection("## " + selection);
+            }
+        },
+
+        h3 : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (cursor.ch !== 0)
+            {
+                cm.setCursor(cursor.line, 0);
+                cm.replaceSelection("### " + selection);
+                cm.setCursor(cursor.line, cursor.ch + 4);
+            }
+            else
+            {
+                cm.replaceSelection("### " + selection);
+            }
+        },
+
+        h4 : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (cursor.ch !== 0)
+            {
+                cm.setCursor(cursor.line, 0);
+                cm.replaceSelection("#### " + selection);
+                cm.setCursor(cursor.line, cursor.ch + 5);
+            }
+            else
+            {
+                cm.replaceSelection("#### " + selection);
+            }
+        },
+
+        h5 : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (cursor.ch !== 0)
+            {
+                cm.setCursor(cursor.line, 0);
+                cm.replaceSelection("##### " + selection);
+                cm.setCursor(cursor.line, cursor.ch + 6);
+            }
+            else
+            {
+                cm.replaceSelection("##### " + selection);
+            }
+        },
+
+        h6 : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (cursor.ch !== 0)
+            {
+                cm.setCursor(cursor.line, 0);
+                cm.replaceSelection("###### " + selection);
+                cm.setCursor(cursor.line, cursor.ch + 7);
+            }
+            else
+            {
+                cm.replaceSelection("###### " + selection);
+            }
+        },
+
+        "list-ul" : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if (selection === "") 
+            {
+                cm.replaceSelection("- " + selection);
+            } 
+            else 
+            {
+                var selectionText = selection.split("\n");
+
+                for (var i = 0, len = selectionText.length; i < len; i++) 
+                {
+                    selectionText[i] = (selectionText[i] === "") ? "" : "- " + selectionText[i];
+                }
+
+                cm.replaceSelection(selectionText.join("\n"));
+            }
+        },
+
+        "list-ol" : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            if(selection === "") 
+            {
+                cm.replaceSelection("1. " + selection);
+            }
+            else
+            {
+                var selectionText = selection.split("\n");
+
+                for (var i = 0, len = selectionText.length; i < len; i++) 
+                {
+                    selectionText[i] = (selectionText[i] === "") ? "" : (i+1) + ". " + selectionText[i];
+                }
+
+                cm.replaceSelection(selectionText.join("\n"));
+            }
+        },
+
+        hr : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection(((cursor.ch !== 0) ? "\n\n" : "\n") + "------------\n\n");
+        },
+
+        tex : function() {
+            if (!this.settings.tex)
+            {
+                alert("settings.tex === false");
+                return this;
+            }
+            
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("$$" + selection + "$$");
+
+            if(selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 2);
+            }
+        },
+
+        link : function() {
+            this.executePlugin("linkDialog", "link-dialog/link-dialog");
+        },
+
+        "reference-link" : function() {
+            this.executePlugin("referenceLinkDialog", "reference-link-dialog/reference-link-dialog");
+        },
+
+        pagebreak : function() {
+            if (!this.settings.pageBreak)
+            {
+                alert("settings.pageBreak === false");
+                return this;
+            }
+            
+            var cm        = this.cm;
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("\r\n[========]\r\n");
+        },
+
+        image : function() {
+            this.executePlugin("imageDialog", "image-dialog/image-dialog");
+        },
+        
+        code : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+
+            cm.replaceSelection("`" + selection + "`");
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+
+        "code-block" : function() {
+            this.executePlugin("codeBlockDialog", "code-block-dialog/code-block-dialog");            
+        },
+
+        "preformatted-text" : function() {
+            this.executePlugin("preformattedTextDialog", "preformatted-text-dialog/preformatted-text-dialog");
+        },
+        
+        table : function() {
+            this.executePlugin("tableDialog", "table-dialog/table-dialog");         
+        },
+        
+        datetime : function() {
+            var cm        = this.cm;
+            var selection = cm.getSelection();
+            var date      = new Date();
+            var langName  = this.settings.lang.name;
+            var datefmt   = editormd.dateFormat() + " " + editormd.dateFormat((langName === "zh-cn" || langName === "zh-tw") ? "cn-week-day" : "week-day");
+
+            cm.replaceSelection(datefmt);
+        },
+        
+        emoji : function() {
+            this.executePlugin("emojiDialog", "emoji-dialog/emoji-dialog");
+        },
+                
+        "html-entities" : function() {
+            this.executePlugin("htmlEntitiesDialog", "html-entities-dialog/html-entities-dialog");
+        },
+                
+        "goto-line" : function() {
+            this.executePlugin("gotoLineDialog", "goto-line-dialog/goto-line-dialog");
+        },
+
+        watch : function() {    
+            this[this.settings.watch ? "unwatch" : "watch"]();
+        },
+
+        preview : function() {
+            this.previewing();
+        },
+
+        fullscreen : function() {
+            this.fullscreen();
+        },
+
+        clear : function() {
+            this.clear();
+        },
+        
+        search : function() {
+            this.search();
+        },
+
+        help : function() {
+            this.executePlugin("helpDialog", "help-dialog/help-dialog");
+        },
+
+        info : function() {
+            this.showInfoDialog();
+        }
+    };
+    
+    editormd.keyMaps = {
+        "Ctrl-1"       : "h1",
+        "Ctrl-2"       : "h2",
+        "Ctrl-3"       : "h3",
+        "Ctrl-4"       : "h4",
+        "Ctrl-5"       : "h5",
+        "Ctrl-6"       : "h6",
+        "Ctrl-B"       : "bold",  // if this is string ==  editormd.toolbarHandlers.xxxx
+        "Ctrl-D"       : "datetime",
+        
+        "Ctrl-E"       : function() { // emoji
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            if (!this.settings.emoji)
+            {
+                alert("Error: settings.emoji == false");
+                return ;
+            }
+
+            cm.replaceSelection(":" + selection + ":");
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+        "Ctrl-Alt-G"   : "goto-line",
+        "Ctrl-H"       : "hr",
+        "Ctrl-I"       : "italic",
+        "Ctrl-K"       : "code",
+        
+        "Ctrl-L"        : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            var title = (selection === "") ? "" : " \""+selection+"\"";
+
+            cm.replaceSelection("[" + selection + "]("+title+")");
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+        "Ctrl-U"         : "list-ul",
+        
+        "Shift-Ctrl-A"   : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            if (!this.settings.atLink)
+            {
+                alert("Error: settings.atLink == false");
+                return ;
+            }
+
+            cm.replaceSelection("@" + selection);
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 1);
+            }
+        },
+        
+        "Shift-Ctrl-C"     : "code",
+        "Shift-Ctrl-Q"     : "quote",
+        "Shift-Ctrl-S"     : "del",
+        "Shift-Ctrl-K"     : "tex",  // KaTeX
+        
+        "Shift-Alt-C"      : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            cm.replaceSelection(["```", selection, "```"].join("\n"));
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 3);
+            } 
+        },
+        
+        "Shift-Ctrl-Alt-C" : "code-block",
+        "Shift-Ctrl-H"     : "html-entities",
+        "Shift-Alt-H"      : "help",
+        "Shift-Ctrl-E"     : "emoji",
+        "Shift-Ctrl-U"     : "uppercase",
+        "Shift-Alt-U"      : "ucwords",
+        "Shift-Ctrl-Alt-U" : "ucfirst",
+        "Shift-Alt-L"      : "lowercase",
+        
+        "Shift-Ctrl-I"     : function() {
+            var cm        = this.cm;
+            var cursor    = cm.getCursor();
+            var selection = cm.getSelection();
+            
+            var title = (selection === "") ? "" : " \""+selection+"\"";
+
+            cm.replaceSelection("![" + selection + "]("+title+")");
+
+            if (selection === "") {
+                cm.setCursor(cursor.line, cursor.ch + 4);
+            }
+        },
+        
+        "Shift-Ctrl-Alt-I" : "image",
+        "Shift-Ctrl-L"     : "link",
+        "Shift-Ctrl-O"     : "list-ol",
+        "Shift-Ctrl-P"     : "preformatted-text",
+        "Shift-Ctrl-T"     : "table",
+        "Shift-Alt-P"      : "pagebreak",
+        "F9"               : "watch",
+        "F10"              : "preview",
+        "F11"              : "fullscreen",
+    };
+    
+    /**
+     * 清除字符串两边的空格
+     * Clear the space of strings both sides.
+     * 
+     * @param   {String}    str            string
+     * @returns {String}                   trimed string    
+     */
+    
+    var trim = function(str) {
+        return (!String.prototype.trim) ? str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "") : str.trim();
+    };
+    
+    editormd.trim = trim;
+    
+    /**
+     * 所有单词首字母大写
+     * Words first to uppercase
+     * 
+     * @param   {String}    str            string
+     * @returns {String}                   string
+     */
+    
+    var ucwords = function (str) {
+        return str.toLowerCase().replace(/\b(\w)|\s(\w)/g, function($1) {  
+            return $1.toUpperCase();
+        });
+    };
+    
+    editormd.ucwords = editormd.wordsFirstUpperCase = ucwords;
+    
+    /**
+     * 字符串首字母大写
+     * Only string first char to uppercase
+     * 
+     * @param   {String}    str            string
+     * @returns {String}                   string
+     */
+    
+    var firstUpperCase = function(str) {        
+        return str.toLowerCase().replace(/\b(\w)/, function($1){
+            return $1.toUpperCase();
+        });
+    };
+    
+    var ucfirst = firstUpperCase;
+    
+    editormd.firstUpperCase = editormd.ucfirst = firstUpperCase;
+    
+    editormd.urls = {
+        atLinkBase : "https://github.com/"
+    };
+    
+    editormd.regexs = {
+        atLink        : /@(\w+)/g,
+        email         : /(\w+)@(\w+)\.(\w+)\.?(\w+)?/g,
+        emailLink     : /(mailto:)?([\w\.\_]+)@(\w+)\.(\w+)\.?(\w+)?/g,
+        emoji         : /:([\w\+-]+):/g,
+        emojiDatetime : /(\d{2}:\d{2}:\d{2})/g,
+        twemoji       : /:(tw-([\w]+)-?(\w+)?):/g,
+        fontAwesome   : /:(fa-([\w]+)(-(\w+)){0,}):/g,
+        editormdLogo  : /:(editormd-logo-?(\w+)?):/g,
+        pageBreak     : /^\[[=]{8,}\]$/
+    };
+
+    // Emoji graphics files url path
+    editormd.emoji     = {
+        path  : "https://cdn.jsdelivr.net/npm/[email protected]/graphics/emojis/",
+        ext   : ".png"
+    };
+
+    // Twitter Emoji (Twemoji)  graphics files url path    
+    editormd.twemoji = {
+        path : "https://i1.wp.com/twemoji.maxcdn.com/36x36/",
+        ext  : ".png"
+    };
+
+    /**
+     * 自定义marked的解析器
+     * Custom Marked renderer rules
+     * 
+     * @param   {Array}    markdownToC     传入用于接收TOC的数组
+     * @returns {Renderer} markedRenderer  返回marked的Renderer自定义对象
+     */
+
+    editormd.markedRenderer = function(markdownToC, options) {
+        var defaults = {
+            toc                  : true,           // Table of contents
+            tocm                 : false,
+            tocStartLevel        : 1,              // Said from H1 to create ToC  
+            pageBreak            : true,
+            atLink               : true,           // for @link
+            emailLink            : true,           // for mail address auto link
+            taskList             : false,          // Enable Github Flavored Markdown task lists
+            emoji                : true,          // :emoji: , Support Twemoji, fontAwesome, Editor.md logo emojis.
+            tex                  : false,          // TeX(LaTeX), based on KaTeX
+            flowChart            : false,          // flowChart.js only support IE9+
+            sequenceDiagram      : false,          // sequenceDiagram.js only support IE9+
+        };
+        
+        var settings        = $.extend(defaults, options || {});    
+        var marked          = editormd.$marked;
+        var markedRenderer  = new marked.Renderer();
+        markdownToC         = markdownToC || [];        
+            
+        var regexs          = editormd.regexs;
+        var atLinkReg       = regexs.atLink;
+        var emojiReg        = regexs.emoji;
+        var emailReg        = regexs.email;
+        var emailLinkReg    = regexs.emailLink;
+        var twemojiReg      = regexs.twemoji;
+        var faIconReg       = regexs.fontAwesome;
+        var editormdLogoReg = regexs.editormdLogo;
+        var pageBreakReg    = regexs.pageBreak;
+
+        markedRenderer.emoji = function(text) {
+            
+            text = text.replace(editormd.regexs.emojiDatetime, function($1) {           
+                return $1.replace(/:/g, "&#58;");
+            });
+            
+            var matchs = text.match(emojiReg);
+
+            if (!matchs || !settings.emoji) {
+                return text;
+            }
+
+            for (var i = 0, len = matchs.length; i < len; i++)
+            {            
+                if (matchs[i] === ":+1:") {
+                    matchs[i] = ":\\+1:";
+                }
+
+                text = text.replace(new RegExp(matchs[i]), function($1, $2){
+                    var faMatchs = $1.match(faIconReg);
+                    var name     = $1.replace(/:/g, "");
+
+                    if (faMatchs)
+                    {                        
+                        for (var fa = 0, len1 = faMatchs.length; fa < len1; fa++)
+                        {
+                            var faName = faMatchs[fa].replace(/:/g, "");
+                            
+                            return "<i class=\"fa " + faName + " fa-emoji\" title=\"" + faName.replace("fa-", "") + "\"></i>";
+                        }
+                    }
+                    else
+                    {
+                        var emdlogoMathcs = $1.match(editormdLogoReg);
+                        var twemojiMatchs = $1.match(twemojiReg);
+
+                        if (emdlogoMathcs)                                        
+                        {                            
+                            for (var x = 0, len2 = emdlogoMathcs.length; x < len2; x++)
+                            {
+                                var logoName = emdlogoMathcs[x].replace(/:/g, "");
+                                return "<i class=\"" + logoName + "\" title=\"Editor.md logo (" + logoName + ")\"></i>";
+                            }
+                        }
+                        else if (twemojiMatchs) 
+                        {
+                            for (var t = 0, len3 = twemojiMatchs.length; t < len3; t++)
+                            {
+                                var twe = twemojiMatchs[t].replace(/:/g, "").replace("tw-", "");
+                                return "<img src=\"" + editormd.twemoji.path + twe + editormd.twemoji.ext + "\" title=\"twemoji-" + twe + "\" alt=\"twemoji-" + twe + "\" class=\"emoji twemoji\" />";
+                            }
+                        }
+                        else
+                        {
+                            var src = (name === "+1") ? "plus1" : name;
+                            src     = (src === "black_large_square") ? "black_square" : src;
+                            src     = (src === "moon") ? "waxing_gibbous_moon" : src;
+
+                            return "<img src=\"" + editormd.emoji.path + src + editormd.emoji.ext + "\" class=\"emoji\" title=\"&#58;" + name + "&#58;\" alt=\"&#58;" + name + "&#58;\" />";
+                        }
+                    }
+                });
+            }
+
+            return text;
+        };
+
+        markedRenderer.atLink = function(text) {
+
+            if (atLinkReg.test(text))
+            { 
+                if (settings.atLink) 
+                {
+                    text = text.replace(emailReg, function($1, $2, $3, $4) {
+                        return $1.replace(/@/g, "_#_&#64;_#_");
+                    });
+
+                    text = text.replace(atLinkReg, function($1, $2) {
+                        return "<a href=\"" + editormd.urls.atLinkBase + "" + $2 + "\" title=\"&#64;" + $2 + "\" class=\"at-link\">" + $1 + "</a>";
+                    }).replace(/_#_&#64;_#_/g, "@");
+                }
+                
+                if (settings.emailLink)
+                {
+                    text = text.replace(emailLinkReg, function($1, $2, $3, $4, $5) {
+                        return (!$2 && $.inArray($5, "jpg|jpeg|png|gif|webp|ico|icon|pdf".split("|")) < 0) ? "<a href=\"mailto:" + $1 + "\">"+$1+"</a>" : $1;
+                    });
+                }
+
+                return text;
+            }
+
+            return text;
+        };
+                
+        markedRenderer.link = function (href, title, text) {
+
+            if (this.options.sanitize) {
+                try {
+                    var prot = decodeURIComponent(unescape(href)).replace(/[^\w:]/g,"").toLowerCase();
+                } catch(e) {
+                    return "";
+                }
+
+                if (prot.indexOf("javascript:") === 0) {
+                    return "";
+                }
+            }
+
+            var out = "<a href=\"" + href + "\"";
+            
+            if (atLinkReg.test(title) || atLinkReg.test(text))
+            {
+                if (title)
+                {
+                    out += " title=\"" + title.replace(/@/g, "&#64;");
+                }
+                
+                return out + "\">" + text.replace(/@/g, "&#64;") + "</a>";
+            }
+
+            if (title) {
+                out += " title=\"" + title + "\"";
+            }
+
+            out += ">" + text + "</a>";
+
+            return out;
+        };
+        
+        markedRenderer.heading = function(text, level, raw) {
+                    
+            var linkText       = text;
+            var hasLinkReg     = /\s*\<a\s*href\=\"(.*)\"\s*([^\>]*)\>(.*)\<\/a\>\s*/;
+            var getLinkTextReg = /\s*\<a\s*([^\>]+)\>([^\>]*)\<\/a\>\s*/g;
+
+            if (hasLinkReg.test(text)) 
+            {
+                var tempText = [];
+                text         = text.split(/\<a\s*([^\>]+)\>([^\>]*)\<\/a\>/);
+
+                for (var i = 0, len = text.length; i < len; i++)
+                {
+                    tempText.push(text[i].replace(/\s*href\=\"(.*)\"\s*/g, ""));
+                }
+
+                text = tempText.join(" ");
+            }
+            
+            text = trim(text);
+            
+            var escapedText    = text.toLowerCase().replace(/[^\w]+/g, "-");
+            var toc = {
+                text  : text,
+                level : level,
+                slug  : escapedText
+            };
+            
+            var isChinese = /^[\u4e00-\u9fa5]+$/.test(text);
+            var id        = (isChinese) ? escape(text).replace(/\%/g, "") : text.toLowerCase().replace(/[^\w]+/g, "-");
+
+            markdownToC.push(toc);
+            
+            var headingHTML = "<h" + level + " id=\"h"+ level + "-" + this.options.headerPrefix + id +"\">";
+            
+            headingHTML    += "<a name=\"" + text + "\" class=\"reference-link\"></a>";
+            headingHTML    += "<span class=\"header-link octicon octicon-link\"></span>";
+            headingHTML    += (hasLinkReg) ? this.atLink(this.emoji(linkText)) : this.atLink(this.emoji(text));
+            headingHTML    += "</h" + level + ">";
+
+            return headingHTML;
+        };
+        
+        markedRenderer.pageBreak = function(text) {
+            if (pageBreakReg.test(text) && settings.pageBreak)
+            {
+                text = "<hr style=\"page-break-after:always;\" class=\"page-break editormd-page-break\" />";
+            }
+            
+            return text;
+        };
+
+        markedRenderer.paragraph = function(text) {
+            var isTeXInline     = /\$\$(.*)\$\$/g.test(text);
+            var isTeXLine       = /^\$\$(.*)\$\$$/.test(text);
+            var isTeXAddClass   = (isTeXLine)     ? " class=\"" + editormd.classNames.tex + "\"" : "";
+            var isToC           = (settings.tocm) ? /^(\[TOC\]|\[TOCM\])$/.test(text) : /^\[TOC\]$/.test(text);
+            var isToCMenu       = /^\[TOCM\]$/.test(text);
+            
+            if (!isTeXLine && isTeXInline) 
+            {
+                text = text.replace(/(\$\$([^\$]*)\$\$)+/g, function($1, $2) {
+                    return "<span class=\"" + editormd.classNames.tex + "\">" + $2.replace(/\$/g, "") + "</span>";
+                });
+            } 
+            else 
+            {
+                text = (isTeXLine) ? text.replace(/\$/g, "") : text;
+            }
+            
+            var tocHTML = "<div class=\"markdown-toc editormd-markdown-toc\">" + text + "</div>";
+            
+            return (isToC) ? ( (isToCMenu) ? "<div class=\"editormd-toc-menu\">" + tocHTML + "</div><br/>" : tocHTML )
+                           : ( (pageBreakReg.test(text)) ? this.pageBreak(text) : "<p" + isTeXAddClass + ">" + this.atLink(this.emoji(text)) + "</p>\n" );
+        };
+
+        markedRenderer.code = function (code, lang, escaped) { 
+
+            if (lang === "seq" || lang === "sequence")
+            {
+                return "<div class=\"sequence-diagram\">" + code + "</div>";
+            } 
+            else if ( lang === "flow")
+            {
+                return "<div class=\"flowchart\">" + code + "</div>";
+            } 
+            else if ( lang === "math" || lang === "latex" || lang === "katex")
+            {
+                return "<p class=\"" + editormd.classNames.tex + "\">" + code + "</p>";
+            } 
+            else 
+            {
+
+                return marked.Renderer.prototype.code.apply(this, arguments);
+            }
+        };
+
+        markedRenderer.tablecell = function(content, flags) {
+            var type = (flags.header) ? "th" : "td";
+            var tag  = (flags.align)  ? "<" + type +" style=\"text-align:" + flags.align + "\">" : "<" + type + ">";
+            
+            return tag + this.atLink(this.emoji(content)) + "</" + type + ">\n";
+        };
+
+        markedRenderer.listitem = function(text) {
+            if (settings.taskList && /^\s*\[[x\s]\]\s*/.test(text)) 
+            {
+                text = text.replace(/^\s*\[\s\]\s*/, "<input type=\"checkbox\" class=\"task-list-item-checkbox\" /> ")
+                           .replace(/^\s*\[x\]\s*/,  "<input type=\"checkbox\" class=\"task-list-item-checkbox\" checked disabled /> ");
+
+                return "<li style=\"list-style: none;\">" + this.atLink(this.emoji(text)) + "</li>";
+            }
+            else 
+            {
+                return "<li>" + this.atLink(this.emoji(text)) + "</li>";
+            }
+        };
+        
+        return markedRenderer;
+    };
+    
+    /**
+     *
+     * 生成TOC(Table of Contents)
+     * Creating ToC (Table of Contents)
+     * 
+     * @param   {Array}    toc             从marked获取的TOC数组列表
+     * @param   {Element}  container       插入TOC的容器元素
+     * @param   {Integer}  startLevel      Hx 起始层级
+     * @returns {Object}   tocContainer    返回ToC列表容器层的jQuery对象元素
+     */
+    
+    editormd.markdownToCRenderer = function(toc, container, tocDropdown, startLevel) {
+        
+        var html        = "";    
+        var lastLevel   = 0;
+        var classPrefix = this.classPrefix;
+        
+        startLevel      = startLevel  || 1;
+        
+        for (var i = 0, len = toc.length; i < len; i++) 
+        {
+            var text  = toc[i].text;
+            var level = toc[i].level;
+            
+            if (level < startLevel) {
+                continue;
+            }
+            
+            if (level > lastLevel) 
+            {
+                html += "";
+            }
+            else if (level < lastLevel) 
+            {
+                html += (new Array(lastLevel - level + 2)).join("</ul></li>");
+            } 
+            else 
+            {
+                html += "</ul></li>";
+            }
+
+            html += "<li><a class=\"toc-level-" + level + "\" href=\"#" + text + "\" level=\"" + level + "\">" + text + "</a><ul>";
+            lastLevel = level;
+        }
+        
+        var tocContainer = container.find(".markdown-toc");
+        
+        if ((tocContainer.length < 1 && container.attr("previewContainer") === "false"))
+        {
+            var tocHTML = "<div class=\"markdown-toc " + classPrefix + "markdown-toc\"></div>";
+            
+            tocHTML = (tocDropdown) ? "<div class=\"" + classPrefix + "toc-menu\">" + tocHTML + "</div>" : tocHTML;
+            
+            container.html(tocHTML);
+            
+            tocContainer = container.find(".markdown-toc");
+        }
+        
+        if (tocDropdown)
+        {
+            tocContainer.wrap("<div class=\"" + classPrefix + "toc-menu\"></div><br/>");
+        }
+        
+        tocContainer.html("<ul class=\"markdown-toc-list\"></ul>").children(".markdown-toc-list").html(html.replace(/\r?\n?\<ul\>\<\/ul\>/g, ""));
+        
+        return tocContainer;
+    };
+    
+    /**
+     *
+     * 生成TOC下拉菜单
+     * Creating ToC dropdown menu
+     * 
+     * @param   {Object}   container       插入TOC的容器jQuery对象元素
+     * @param   {String}   tocTitle        ToC title
+     * @returns {Object}                   return toc-menu object
+     */
+    
+    editormd.tocDropdownMenu = function(container, tocTitle) {
+        
+        tocTitle      = tocTitle || "Table of Contents";
+        
+        var zindex    = 400;
+        var tocMenus  = container.find("." + this.classPrefix + "toc-menu");
+
+        tocMenus.each(function() {
+            var $this  = $(this);
+            var toc    = $this.children(".markdown-toc");
+            var icon   = "<i class=\"fa fa-angle-down\"></i>";
+            var btn    = "<a href=\"javascript:;\" class=\"toc-menu-btn\">" + icon + tocTitle + "</a>";
+            var menu   = toc.children("ul");            
+            var list   = menu.find("li");
+            
+            toc.append(btn);
+            
+            list.first().before("<li><h1>" + tocTitle + " " + icon + "</h1></li>");
+            
+            $this.mouseover(function(){
+                menu.show();
+
+                list.each(function(){
+                    var li = $(this);
+                    var ul = li.children("ul");
+
+                    if (ul.html() === "")
+                    {
+                        ul.remove();
+                    }
+
+                    if (ul.length > 0 && ul.html() !== "")
+                    {
+                        var firstA = li.children("a").first();
+
+                        if (firstA.children(".fa").length < 1)
+                        {
+                            firstA.append( $(icon).css({ float:"right", paddingTop:"4px" }) );
+                        }
+                    }
+
+                    li.mouseover(function(){
+                        ul.css("z-index", zindex).show();
+                        zindex += 1;
+                    }).mouseleave(function(){
+                        ul.hide();
+                    });
+                });
+            }).mouseleave(function(){
+                menu.hide();
+            }); 
+        });       
+        
+        return tocMenus;
+    };
+    
+    /**
+     * 简单地过滤指定的HTML标签
+     * Filter custom html tags
+     * 
+     * @param   {String}   html          要过滤HTML
+     * @param   {String}   filters       要过滤的标签
+     * @returns {String}   html          返回过滤的HTML
+     */
+    
+    editormd.filterHTMLTags = function(html, filters) {
+        
+        if (typeof html !== "string") {
+            html = new String(html);
+        }
+            
+        if (typeof filters !== "string") {
+            return html;
+        }
+
+        var expression = filters.split("|");
+        var filterTags = expression[0].split(",");
+        var attrs      = expression[1];
+
+        for (var i = 0, len = filterTags.length; i < len; i++)
+        {
+            var tag = filterTags[i];
+
+            html = html.replace(new RegExp("\<\s*" + tag + "\s*([^\>]*)\>([^\>]*)\<\s*\/" + tag + "\s*\>", "igm"), "");
+        }
+        
+        //return html;
+
+        if (typeof attrs !== "undefined")
+        {
+            var htmlTagRegex = /\<(\w+)\s*([^\>]*)\>([^\>]*)\<\/(\w+)\>/ig;
+
+            if (attrs === "*")
+            {
+                html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) {
+                    return "<" + $2 + ">" + $4 + "</" + $5 + ">";
+                });         
+            }
+            else if (attrs === "on*")
+            {
+                html = html.replace(htmlTagRegex, function($1, $2, $3, $4, $5) {
+                    var el = $("<" + $2 + ">" + $4 + "</" + $5 + ">");
+                    var _attrs = $($1)[0].attributes;
+                    var $attrs = {};
+                    
+                    $.each(_attrs, function(i, e) {
+                        if (e.nodeName !== '"') $attrs[e.nodeName] = e.nodeValue;
+                    });
+                    
+                    $.each($attrs, function(i) {                        
+                        if (i.indexOf("on") === 0) {
+                            delete $attrs[i];
+                        }
+                    });
+                    
+                    el.attr($attrs);
+                    
+                    var text = (typeof el[1] !== "undefined") ? $(el[1]).text() : "";
+
+                    return el[0].outerHTML + text;
+                });
+            }
+            else
+            {
+                html = html.replace(htmlTagRegex, function($1, $2, $3, $4) {
+                    var filterAttrs = attrs.split(",");
+                    var el = $($1);
+                    el.html($4);
+
+                    $.each(filterAttrs, function(i) {
+                        el.attr(filterAttrs[i], null);
+                    });
+
+                    return el[0].outerHTML;
+                });
+            }
+        }
+        
+        return html;
+    };
+    
+    /**
+     * 将Markdown文档解析为HTML用于前台显示
+     * Parse Markdown to HTML for Font-end preview.
+     * 
+     * @param   {String}   id            用于显示HTML的对象ID
+     * @param   {Object}   [options={}]  配置选项,可选
+     * @returns {Object}   div           返回jQuery对象元素
+     */
+    
+    editormd.markdownToHTML = function(id, options) {
+        var defaults = {
+            gfm                  : true,
+            toc                  : true,
+            tocm                 : false,
+            tocStartLevel        : 1,
+            tocTitle             : "目录",
+            tocDropdown          : false,
+            tocContainer         : "",
+            markdown             : "",
+            markdownSourceCode   : false,
+            htmlDecode           : false,
+            autoLoadKaTeX        : true,
+            pageBreak            : true,
+            atLink               : true,    // for @link
+            emailLink            : true,    // for mail address auto link
+            tex                  : false,
+            taskList             : false,   // Github Flavored Markdown task lists
+            emoji                : true,
+            flowChart            : false,
+            sequenceDiagram      : false,
+            previewCodeHighlight : true
+        };
+        
+        editormd.$marked  = marked;
+
+        var div           = $("#" + id);
+        var settings      = div.settings = $.extend(true, defaults, options || {});
+        var saveTo        = div.find("textarea");
+        
+        if (saveTo.length < 1)
+        {
+            div.append("<textarea></textarea>");
+            saveTo        = div.find("textarea");
+        }        
+        
+        var markdownDoc   = (settings.markdown === "") ? saveTo.val() : settings.markdown; 
+        var markdownToC   = [];
+
+        var rendererOptions = {  
+            toc                  : settings.toc,
+            tocm                 : settings.tocm,
+            tocStartLevel        : settings.tocStartLevel,
+            taskList             : settings.taskList,
+            emoji                : settings.emoji,
+            tex                  : settings.tex,
+            pageBreak            : settings.pageBreak,
+            atLink               : settings.atLink,           // for @link
+            emailLink            : settings.emailLink,        // for mail address auto link
+            flowChart            : settings.flowChart,
+            sequenceDiagram      : settings.sequenceDiagram,
+            previewCodeHighlight : settings.previewCodeHighlight,
+        };
+
+        var markedOptions = {
+            renderer    : editormd.markedRenderer(markdownToC, rendererOptions),
+            gfm         : settings.gfm,
+            tables      : true,
+            breaks      : true,
+            pedantic    : false,
+            sanitize    : (settings.htmlDecode) ? false : true, // 是否忽略HTML标签,即是否开启HTML标签解析,为了安全性,默认不开启
+            smartLists  : true,
+            smartypants : true
+        };
+        
+		markdownDoc = new String(markdownDoc);
+        
+        var markdownParsed = marked(markdownDoc, markedOptions);
+        
+        markdownParsed = editormd.filterHTMLTags(markdownParsed, settings.htmlDecode);
+        
+        if (settings.markdownSourceCode) {
+            saveTo.text(markdownDoc);
+        } else {
+            saveTo.remove();
+        }
+        
+        div.addClass("markdown-body " + this.classPrefix + "html-preview").append(markdownParsed);
+        
+        var tocContainer = (settings.tocContainer !== "") ? $(settings.tocContainer) : div;
+        
+        if (settings.tocContainer !== "")
+        {
+            tocContainer.attr("previewContainer", false);
+        }
+         
+        if (settings.toc) 
+        {
+            div.tocContainer = this.markdownToCRenderer(markdownToC, tocContainer, settings.tocDropdown, settings.tocStartLevel);
+            
+            if (settings.tocDropdown || div.find("." + this.classPrefix + "toc-menu").length > 0)
+            {
+                this.tocDropdownMenu(div, settings.tocTitle);
+            }
+            
+            if (settings.tocContainer !== "")
+            {
+                div.find(".editormd-toc-menu, .editormd-markdown-toc").remove();
+            }
+        }
+            
+        if (settings.previewCodeHighlight) 
+        {
+            div.find("pre").addClass("prettyprint linenums");
+            prettyPrint();
+        }
+        
+        if (!editormd.isIE8) 
+        {
+            if (settings.flowChart) {
+                div.find(".flowchart").flowChart(); 
+            }
+
+            if (settings.sequenceDiagram) {
+                div.find(".sequence-diagram").sequenceDiagram({theme: "simple"});
+            }
+        }
+
+        if (settings.tex)
+        {
+            var katexHandle = function() {
+                div.find("." + editormd.classNames.tex).each(function(){
+                    var tex  = $(this);                    
+                    katex.render(tex.html().replace(/&lt;/g, "<").replace(/&gt;/g, ">"), tex[0]);                    
+                    tex.find(".katex").css("font-size", "1.6em");
+                });
+            };
+            
+            if (settings.autoLoadKaTeX && !editormd.$katex && !editormd.kaTeXLoaded)
+            {
+                this.loadKaTeX(function() {
+                    editormd.$katex      = katex;
+                    editormd.kaTeXLoaded = true;
+                    katexHandle();
+                });
+            }
+            else
+            {
+                katexHandle();
+            }
+        }
+        
+        div.getMarkdown = function() {            
+            return saveTo.val();
+        };
+        
+        return div;
+    };
+    
+    // Editor.md themes, change toolbar themes etc.
+    // added @1.5.0
+    editormd.themes        = ["default", "dark"];
+    
+    // Preview area themes
+    // added @1.5.0
+    editormd.previewThemes = ["default", "dark"];
+    
+    // CodeMirror / editor area themes
+    // @1.5.0 rename -> editorThemes, old version -> themes
+    editormd.editorThemes = [
+        "default", "3024-day", "3024-night",
+        "ambiance", "ambiance-mobile",
+        "base16-dark", "base16-light", "blackboard",
+        "cobalt",
+        "eclipse", "elegant", "erlang-dark",
+        "lesser-dark",
+        "mbo", "mdn-like", "midnight", "monokai",
+        "neat", "neo", "night",
+        "paraiso-dark", "paraiso-light", "pastel-on-dark",
+        "rubyblue",
+        "solarized",
+        "the-matrix", "tomorrow-night-eighties", "twilight",
+        "vibrant-ink",
+        "xq-dark", "xq-light"
+    ];
+
+    editormd.loadPlugins = {};
+    
+    editormd.loadFiles = {
+        js     : [],
+        css    : [],
+        plugin : []
+    };
+    
+    /**
+     * 动态加载Editor.md插件,但不立即执行
+     * Load editor.md plugins
+     * 
+     * @param {String}   fileName              插件文件路径
+     * @param {Function} [callback=function()] 加载成功后执行的回调函数
+     * @param {String}   [into="head"]         嵌入页面的位置
+     */
+    
+    editormd.loadPlugin = function(fileName, callback, into) {
+        callback   = callback || function() {};
+        
+        this.loadScript(fileName, function() {
+            editormd.loadFiles.plugin.push(fileName);
+            callback();
+        }, into);
+    };
+    
+    /**
+     * 动态加载CSS文件的方法
+     * Load css file method
+     * 
+     * @param {String}   fileName              CSS文件名
+     * @param {Function} [callback=function()] 加载成功后执行的回调函数
+     * @param {String}   [into="head"]         嵌入页面的位置
+     */
+    
+    editormd.loadCSS   = function(fileName, callback, into) {
+        into       = into     || "head";        
+        callback   = callback || function() {};
+        
+        var css    = document.createElement("link");
+        css.type   = "text/css";
+        css.rel    = "stylesheet";
+        css.onload = css.onreadystatechange = function() {
+            editormd.loadFiles.css.push(fileName);
+            callback();
+        };
+
+        css.href   = fileName + ".css";
+
+        if(into === "head") {
+            document.getElementsByTagName("head")[0].appendChild(css);
+        } else {
+            document.body.appendChild(css);
+        }
+    };
+    
+    editormd.isIE    = (navigator.appName == "Microsoft Internet Explorer");
+    editormd.isIE8   = (editormd.isIE && navigator.appVersion.match(/8./i) == "8.");
+
+    /**
+     * 动态加载JS文件的方法
+     * Load javascript file method
+     * 
+     * @param {String}   fileName              JS文件名
+     * @param {Function} [callback=function()] 加载成功后执行的回调函数
+     * @param {String}   [into="head"]         嵌入页面的位置
+     */
+
+    editormd.loadScript = function(fileName, callback, into) {
+        
+        into          = into     || "head";
+        callback      = callback || function() {};
+        
+        var script    = null; 
+        script        = document.createElement("script");
+        script.id     = fileName.replace(/[\./]+/g, "-");
+        script.type   = "text/javascript";        
+        script.src    = fileName + ".js";
+        
+        if (editormd.isIE8) 
+        {            
+            script.onreadystatechange = function() {
+                if(script.readyState) 
+                {
+                    if (script.readyState === "loaded" || script.readyState === "complete") 
+                    {
+                        script.onreadystatechange = null; 
+                        editormd.loadFiles.js.push(fileName);
+                        callback();
+                    }
+                } 
+            };
+        }
+        else
+        {
+            script.onload = function() {
+                editormd.loadFiles.js.push(fileName);
+                callback();
+            };
+        }
+
+        if (into === "head") {
+            document.getElementsByTagName("head")[0].appendChild(script);
+        } else {
+            document.body.appendChild(script);
+        }
+    };
+    
+    // 使用国外的CDN,加载速度有时会很慢,或者自定义URL
+    // You can custom KaTeX load url.
+    editormd.katexURL  = {
+        css : "//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min",
+        js  : "//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min"
+    };
+    
+    editormd.kaTeXLoaded = false;
+    
+    /**
+     * 加载KaTeX文件
+     * load KaTeX files
+     * 
+     * @param {Function} [callback=function()]  加载成功后执行的回调函数
+     */
+    
+    editormd.loadKaTeX = function (callback) {
+        editormd.loadCSS(editormd.katexURL.css, function(){
+            editormd.loadScript(editormd.katexURL.js, callback || function(){});
+        });
+    };
+        
+    /**
+     * 锁屏
+     * lock screen
+     * 
+     * @param   {Boolean}   lock   Boolean 布尔值,是否锁屏
+     * @returns {void}
+     */
+    
+    editormd.lockScreen = function(lock) {
+        $("html,body").css("overflow", (lock) ? "hidden" : "");
+    };
+        
+    /**
+     * 动态创建对话框
+     * Creating custom dialogs
+     * 
+     * @param   {Object} options 配置项键值对 Key/Value
+     * @returns {dialog} 返回创建的dialog的jQuery实例对象
+     */
+
+    editormd.createDialog = function(options) {
+        var defaults = {
+            name : "",
+            width : 420,
+            height: 240,
+            title : "",
+            drag  : true,
+            closed : true,
+            content : "",
+            mask : true,
+            maskStyle : {
+                backgroundColor : "#fff",
+                opacity : 0.1
+            },
+            lockScreen : true,
+            footer : true,
+            buttons : false
+        };
+
+        options          = $.extend(true, defaults, options);
+        
+        var $this        = this;
+        var editor       = this.editor;
+        var classPrefix  = editormd.classPrefix;
+        var guid         = (new Date()).getTime();
+        var dialogName   = ( (options.name === "") ? classPrefix + "dialog-" + guid : options.name);
+        var mouseOrTouch = editormd.mouseOrTouch;
+
+        var html         = "<div class=\"" + classPrefix + "dialog " + dialogName + "\">";
+
+        if (options.title !== "")
+        {
+            html += "<div class=\"" + classPrefix + "dialog-header\"" + ( (options.drag) ? " style=\"cursor: move;\"" : "" ) + ">";
+            html += "<strong class=\"" + classPrefix + "dialog-title\">" + options.title + "</strong>";
+            html += "</div>";
+        }
+
+        if (options.closed)
+        {
+            html += "<a href=\"javascript:;\" class=\"fa fa-close " + classPrefix + "dialog-close\"></a>";
+        }
+
+        html += "<div class=\"" + classPrefix + "dialog-container\">" + options.content;                    
+
+        if (options.footer || typeof options.footer === "string") 
+        {
+            html += "<div class=\"" + classPrefix + "dialog-footer\">" + ( (typeof options.footer === "boolean") ? "" : options.footer) + "</div>";
+        }
+
+        html += "</div>";
+
+        html += "<div class=\"" + classPrefix + "dialog-mask " + classPrefix + "dialog-mask-bg\"></div>";
+        html += "<div class=\"" + classPrefix + "dialog-mask " + classPrefix + "dialog-mask-con\"></div>";
+        html += "</div>";
+
+        editor.append(html);
+
+        var dialog = editor.find("." + dialogName);
+
+        dialog.lockScreen = function(lock) {
+            if (options.lockScreen)
+            {                
+                $("html,body").css("overflow", (lock) ? "hidden" : "");
+                $this.resize();
+            }
+
+            return dialog;
+        };
+
+        dialog.showMask = function() {
+            if (options.mask)
+            {
+                editor.find("." + classPrefix + "mask").css(options.maskStyle).css("z-index", editormd.dialogZindex - 1).show();
+            }
+            return dialog;
+        };
+
+        dialog.hideMask = function() {
+            if (options.mask)
+            {
+                editor.find("." + classPrefix + "mask").hide();
+            }
+
+            return dialog;
+        };
+
+        dialog.loading = function(show) {                        
+            var loading = dialog.find("." + classPrefix + "dialog-mask");
+            loading[(show) ? "show" : "hide"]();
+
+            return dialog;
+        };
+
+        dialog.lockScreen(true).showMask();
+
+        dialog.show().css({
+            zIndex : editormd.dialogZindex,
+            border : (editormd.isIE8) ? "1px solid #ddd" : "",
+            width  : (typeof options.width  === "number") ? options.width + "px"  : options.width,
+            height : (typeof options.height === "number") ? options.height + "px" : options.height
+        });
+
+        var dialogPosition = function(){
+            dialog.css({
+                top    : ($(window).height() - dialog.height()) / 2 + "px",
+                left   : ($(window).width() - dialog.width()) / 2 + "px"
+            });
+        };
+
+        dialogPosition();
+
+        $(window).resize(dialogPosition);
+
+        dialog.children("." + classPrefix + "dialog-close").bind(mouseOrTouch("click", "touchend"), function() {
+            dialog.hide().lockScreen(false).hideMask();
+        });
+
+        if (typeof options.buttons === "object")
+        {
+            var footer = dialog.footer = dialog.find("." + classPrefix + "dialog-footer");
+
+            for (var key in options.buttons)
+            {
+                var btn = options.buttons[key];
+                var btnClassName = classPrefix + key + "-btn";
+
+                footer.append("<button class=\"" + classPrefix + "btn " + btnClassName + "\">" + btn[0] + "</button>");
+                btn[1] = $.proxy(btn[1], dialog);
+                footer.children("." + btnClassName).bind(mouseOrTouch("click", "touchend"), btn[1]);
+            }
+        }
+
+        if (options.title !== "" && options.drag)
+        {                        
+            var posX, posY;
+            var dialogHeader = dialog.children("." + classPrefix + "dialog-header");
+
+            if (!options.mask) {
+                dialogHeader.bind(mouseOrTouch("click", "touchend"), function(){
+                    editormd.dialogZindex += 2;
+                    dialog.css("z-index", editormd.dialogZindex);
+                });
+            }
+
+            dialogHeader.mousedown(function(e) {
+                e = e || window.event;  //IE
+                posX = e.clientX - parseInt(dialog[0].style.left);
+                posY = e.clientY - parseInt(dialog[0].style.top);
+
+                document.onmousemove = moveAction;                   
+            });
+
+            var userCanSelect = function (obj) {
+                obj.removeClass(classPrefix + "user-unselect").off("selectstart");
+            };
+
+            var userUnselect = function (obj) {
+                obj.addClass(classPrefix + "user-unselect").on("selectstart", function(event) { // selectstart for IE                        
+                    return false;
+                });
+            };
+
+            var moveAction = function (e) {
+                e = e || window.event;  //IE
+
+                var left, top, nowLeft = parseInt(dialog[0].style.left), nowTop = parseInt(dialog[0].style.top);
+
+                if( nowLeft >= 0 ) {
+                    if( nowLeft + dialog.width() <= $(window).width()) {
+                        left = e.clientX - posX;
+                    } else {	
+                        left = $(window).width() - dialog.width();
+                        document.onmousemove = null;
+                    }
+                } else {
+                    left = 0;
+                    document.onmousemove = null;
+                }
+
+                if( nowTop >= 0 ) {
+                    top = e.clientY - posY;
+                } else {
+                    top = 0;
+                    document.onmousemove = null;
+                }
+
+
+                document.onselectstart = function() {
+                    return false;
+                };
+
+                userUnselect($("body"));
+                userUnselect(dialog);
+                dialog[0].style.left = left + "px";
+                dialog[0].style.top  = top + "px";
+            };
+
+            document.onmouseup = function() {                            
+                userCanSelect($("body"));
+                userCanSelect(dialog);
+
+                document.onselectstart = null;         
+                document.onmousemove = null;
+            };
+
+            dialogHeader.touchDraggable = function() {
+                var offset = null;
+                var start  = function(e) {
+                    var orig = e.originalEvent; 
+                    var pos  = $(this).parent().position();
+
+                    offset = {
+                        x : orig.changedTouches[0].pageX - pos.left,
+                        y : orig.changedTouches[0].pageY - pos.top
+                    };
+                };
+
+                var move = function(e) {
+                    e.preventDefault();
+                    var orig = e.originalEvent;
+
+                    $(this).parent().css({
+                        top  : orig.changedTouches[0].pageY - offset.y,
+                        left : orig.changedTouches[0].pageX - offset.x
+                    });
+                };
+
+                this.bind("touchstart", start).bind("touchmove", move);
+            };
+
+            dialogHeader.touchDraggable();
+        }
+
+        editormd.dialogZindex += 2;
+
+        return dialog;
+    };
+    
+    /**
+     * 鼠标和触摸事件的判断/选择方法
+     * MouseEvent or TouchEvent type switch
+     * 
+     * @param   {String} [mouseEventType="click"]    供选择的鼠标事件
+     * @param   {String} [touchEventType="touchend"] 供选择的触摸事件
+     * @returns {String} EventType                   返回事件类型名称
+     */
+    
+    editormd.mouseOrTouch = function(mouseEventType, touchEventType) {
+        mouseEventType = mouseEventType || "click";
+        touchEventType = touchEventType || "touchend";
+        
+        var eventType  = mouseEventType;
+
+        try {
+            document.createEvent("TouchEvent");
+            eventType = touchEventType;
+        } catch(e) {}
+
+        return eventType;
+    };
+    
+    /**
+     * 日期时间的格式化方法
+     * Datetime format method
+     * 
+     * @param   {String}   [format=""]  日期时间的格式,类似PHP的格式
+     * @returns {String}   datefmt      返回格式化后的日期时间字符串
+     */
+    
+    editormd.dateFormat = function(format) {                
+        format      = format || "";
+
+        var addZero = function(d) {
+            return (d < 10) ? "0" + d : d;
+        };
+
+        var date    = new Date(); 
+        var year    = date.getFullYear();
+        var year2   = year.toString().slice(2, 4);
+        var month   = addZero(date.getMonth() + 1);
+        var day     = addZero(date.getDate());
+        var weekDay = date.getDay();
+        var hour    = addZero(date.getHours());
+        var min     = addZero(date.getMinutes());
+        var second  = addZero(date.getSeconds());
+        var ms      = addZero(date.getMilliseconds()); 
+        var datefmt = "";
+
+        var ymd     = year2 + "-" + month + "-" + day;
+        var fymd    = year  + "-" + month + "-" + day;
+        var hms     = hour  + ":" + min   + ":" + second;
+
+        switch (format) 
+        {
+            case "UNIX Time" :
+                    datefmt = date.getTime();
+                break;
+
+            case "UTC" :
+                    datefmt = date.toUTCString();
+                break;	
+
+            case "yy" :
+                    datefmt = year2;
+                break;	
+
+            case "year" :
+            case "yyyy" :
+                    datefmt = year;
+                break;
+
+            case "month" :
+            case "mm" :
+                    datefmt = month;
+                break;                        
+
+            case "cn-week-day" :
+            case "cn-wd" :
+                    var cnWeekDays = ["日", "一", "二", "三", "四", "五", "六"];
+                    datefmt = "星期" + cnWeekDays[weekDay];
+                break;
+
+            case "week-day" :
+            case "wd" :
+                    var weekDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
+                    datefmt = weekDays[weekDay];
+                break;
+
+            case "day" :
+            case "dd" :
+                    datefmt = day;
+                break;
+
+            case "hour" :
+            case "hh" :
+                    datefmt = hour;
+                break;
+
+            case "min" :
+            case "ii" :
+                    datefmt = min;
+                break;
+
+            case "second" :
+            case "ss" :
+                    datefmt = second;
+                break;
+
+            case "ms" :
+                    datefmt = ms;
+                break;
+
+            case "yy-mm-dd" :
+                    datefmt = ymd;
+                break;
+
+            case "yyyy-mm-dd" :
+                    datefmt = fymd;
+                break;
+
+            case "yyyy-mm-dd h:i:s ms" :
+            case "full + ms" : 
+                    datefmt = fymd + " " + hms + " " + ms;
+                break;
+
+            case "full" :
+            case "yyyy-mm-dd h:i:s" :
+                default:
+                    datefmt = fymd + " " + hms;
+                break;
+        }
+
+        return datefmt;
+    };
+
+    return editormd;
+
+}));

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 0
public/theme/material/editor/editormd.min.js


BIN
public/theme/material/editor/images/loading.gif


BIN
public/theme/material/editor/images/[email protected]


BIN
public/theme/material/editor/images/[email protected]


+ 127 - 0
public/theme/material/editor/languages/en.js

@@ -0,0 +1,127 @@
+(function(){
+    var factory = function (exports) {
+        var lang = {
+            name : "en",
+            description : "Open source online Markdown editor.",
+            tocTitle    : "Table of Contents",
+            toolbar : {
+                undo             : "Undo(Ctrl+Z)",
+                redo             : "Redo(Ctrl+Y)",
+                bold             : "Bold",
+                del              : "Strikethrough",
+                italic           : "Italic",
+                quote            : "Block quote",
+                ucwords          : "Words first letter convert to uppercase",
+                uppercase        : "Selection text convert to uppercase",
+                lowercase        : "Selection text convert to lowercase",
+                h1               : "Heading 1",
+                h2               : "Heading 2",
+                h3               : "Heading 3",
+                h4               : "Heading 4",
+                h5               : "Heading 5",
+                h6               : "Heading 6",
+                "list-ul"        : "Unordered list",
+                "list-ol"        : "Ordered list",
+                hr               : "Horizontal rule",
+                link             : "Link",
+                "reference-link" : "Reference link",
+                image            : "Image",
+                code             : "Code inline",
+                "preformatted-text" : "Preformatted text / Code block (Tab indent)",
+                "code-block"     : "Code block (Multi-languages)",
+                table            : "Tables",
+                datetime         : "Datetime",
+                emoji            : "Emoji",
+                "html-entities"  : "HTML Entities",
+                pagebreak        : "Page break",
+                watch            : "Unwatch",
+                unwatch          : "Watch",
+                preview          : "HTML Preview (Press Shift + ESC exit)",
+                fullscreen       : "Fullscreen (Press ESC exit)",
+                clear            : "Clear",
+                search           : "Search",
+                help             : "Help",
+                info             : "About " + exports.title
+            },
+            buttons : {
+                enter  : "Enter",
+                cancel : "Cancel",
+                close  : "Close"
+            },
+            dialog : {
+                link : {
+                    title    : "Link",
+                    url      : "Address",
+                    urlTitle : "Title",
+                    urlEmpty : "Error: Please fill in the link address."
+                },
+                referenceLink : {
+                    title    : "Reference link",
+                    name     : "Name",
+                    url      : "Address",
+                    urlId    : "ID",
+                    urlTitle : "Title",
+                    nameEmpty: "Error: Reference name can't be empty.",
+                    idEmpty  : "Error: Please fill in reference link id.",
+                    urlEmpty : "Error: Please fill in reference link url address."
+                },
+                image : {
+                    title    : "Image",
+                    url      : "Address",
+                    link     : "Link",
+                    alt      : "Title",
+                    uploadButton     : "Upload",
+                    imageURLEmpty    : "Error: picture url address can't be empty.",
+                    uploadFileEmpty  : "Error: upload pictures cannot be empty!",
+                    formatNotAllowed : "Error: only allows to upload pictures file, upload allowed image file format:"
+                },
+                preformattedText : {
+                    title             : "Preformatted text / Codes", 
+                    emptyAlert        : "Error: Please fill in the Preformatted text or content of the codes."
+                },
+                codeBlock : {
+                    title             : "Code block",         
+                    selectLabel       : "Languages: ",
+                    selectDefaultText : "select a code language...",
+                    otherLanguage     : "Other languages",
+                    unselectedLanguageAlert : "Error: Please select the code language.",
+                    codeEmptyAlert    : "Error: Please fill in the code content."
+                },
+                htmlEntities : {
+                    title : "HTML Entities"
+                },
+                help : {
+                    title : "Help"
+                }
+            }
+        };
+        
+        exports.defaults.lang = lang;
+    };
+    
+	// CommonJS/Node.js
+	if (typeof require === "function" && typeof exports === "object" && typeof module === "object")
+    { 
+        module.exports = factory;
+    }
+	else if (typeof define === "function")  // AMD/CMD/Sea.js
+    {
+		if (define.amd) { // for Require.js
+
+			define(["editormd"], function(editormd) {
+                factory(editormd);
+            });
+
+		} else { // for Sea.js
+			define(function(require) {
+                var editormd = require("../editormd");
+                factory(editormd);
+            });
+		}
+	} 
+	else
+	{
+        factory(window.editormd);
+	}
+    
+})();

+ 127 - 0
public/theme/material/editor/languages/zh-tw.js

@@ -0,0 +1,127 @@
+(function(){
+    var factory = function (exports) {
+        var lang = {
+            name : "zh-tw",
+            description : "開源在線Markdown編輯器<br/>Open source online Markdown editor.",
+            tocTitle    : "目錄",
+            toolbar     : {
+                undo             : "撤銷(Ctrl+Z)",
+                redo             : "重做(Ctrl+Y)",
+                bold             : "粗體",
+                del              : "刪除線",
+                italic           : "斜體",
+                quote            : "引用",
+                ucwords          : "將所選的每個單詞首字母轉成大寫",
+                uppercase        : "將所選文本轉成大寫",
+                lowercase        : "將所選文本轉成小寫",
+                h1               : "標題1",
+                h2               : "標題2",
+                h3               : "標題3",
+                h4               : "標題4",
+                h5               : "標題5",
+                h6               : "標題6",
+                "list-ul"        : "無序列表",
+                "list-ol"        : "有序列表",
+                hr               : "横线",
+                link             : "链接",
+                "reference-link" : "引用鏈接",
+                image            : "圖片",
+                code             : "行內代碼",
+                "preformatted-text" : "預格式文本 / 代碼塊(縮進風格)",
+                "code-block"     : "代碼塊(多語言風格)",
+                table            : "添加表格",
+                datetime         : "日期時間",
+                emoji            : "Emoji 表情",
+                "html-entities"  : "HTML 實體字符",
+                pagebreak        : "插入分頁符",
+                watch            : "關閉實時預覽",
+                unwatch          : "開啟實時預覽",
+                preview          : "全窗口預覽HTML(按 Shift + ESC 退出)",
+                fullscreen       : "全屏(按 ESC 退出)",
+                clear            : "清空",
+                search           : "搜尋",
+                help             : "使用幫助",
+                info             : "關於" + exports.title
+            },
+            buttons : {
+                enter  : "確定",
+                cancel : "取消",
+                close  : "關閉"
+            },
+            dialog : {
+                link   : {
+                    title    : "添加鏈接",
+                    url      : "鏈接地址",
+                    urlTitle : "鏈接標題",
+                    urlEmpty : "錯誤:請填寫鏈接地址。"
+                },
+                referenceLink : {
+                    title    : "添加引用鏈接",
+                    name     : "引用名稱",
+                    url      : "鏈接地址",
+                    urlId    : "鏈接ID",
+                    urlTitle : "鏈接標題",
+                    nameEmpty: "錯誤:引用鏈接的名稱不能為空。",
+                    idEmpty  : "錯誤:請填寫引用鏈接的ID。",
+                    urlEmpty : "錯誤:請填寫引用鏈接的URL地址。"
+                },
+                image  : {
+                    title    : "添加圖片",
+                    url      : "圖片地址",
+                    link     : "圖片鏈接",
+                    alt      : "圖片描述",
+                    uploadButton     : "本地上傳",
+                    imageURLEmpty    : "錯誤:圖片地址不能為空。",
+                    uploadFileEmpty  : "錯誤:上傳的圖片不能為空!",
+                    formatNotAllowed : "錯誤:只允許上傳圖片文件,允許上傳的圖片文件格式有:"
+                },
+                preformattedText : {
+                    title             : "添加預格式文本或代碼塊", 
+                    emptyAlert        : "錯誤:請填寫預格式文本或代碼的內容。"
+                },
+                codeBlock : {
+                    title             : "添加代碼塊",                 
+                    selectLabel       : "代碼語言:",
+                    selectDefaultText : "請語言代碼語言",
+                    otherLanguage     : "其他語言",
+                    unselectedLanguageAlert : "錯誤:請選擇代碼所屬的語言類型。",
+                    codeEmptyAlert    : "錯誤:請填寫代碼內容。"
+                },
+                htmlEntities : {
+                    title : "HTML實體字符"
+                },
+                help : {
+                    title : "使用幫助"
+                }
+            }
+        };
+        
+        exports.defaults.lang = lang;
+    };
+    
+	// CommonJS/Node.js
+	if (typeof require === "function" && typeof exports === "object" && typeof module === "object")
+    { 
+        module.exports = factory;
+    }
+	else if (typeof define === "function")  // AMD/CMD/Sea.js
+    {
+		if (define.amd) { // for Require.js
+
+			define(["editormd"], function(editormd) {
+                factory(editormd);
+            });
+
+		} else { // for Sea.js
+			define(function(require) {
+                var editormd = require("../editormd");
+                factory(editormd);
+            });
+		}
+	} 
+	else
+	{
+        factory(window.editormd);
+	}
+    
+})();

+ 19 - 0
public/theme/material/editor/lib/codemirror/LICENSE

@@ -0,0 +1,19 @@
+Copyright (C) 2014 by Marijn Haverbeke <[email protected]> and others
+
+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.

+ 183 - 0
public/theme/material/editor/lib/codemirror/addon/comment/comment.js

@@ -0,0 +1,183 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  var noOptions = {};
+  var nonWS = /[^\s\u00a0]/;
+  var Pos = CodeMirror.Pos;
+
+  function firstNonWS(str) {
+    var found = str.search(nonWS);
+    return found == -1 ? 0 : found;
+  }
+
+  CodeMirror.commands.toggleComment = function(cm) {
+    var minLine = Infinity, ranges = cm.listSelections(), mode = null;
+    for (var i = ranges.length - 1; i >= 0; i--) {
+      var from = ranges[i].from(), to = ranges[i].to();
+      if (from.line >= minLine) continue;
+      if (to.line >= minLine) to = Pos(minLine, 0);
+      minLine = from.line;
+      if (mode == null) {
+        if (cm.uncomment(from, to)) mode = "un";
+        else { cm.lineComment(from, to); mode = "line"; }
+      } else if (mode == "un") {
+        cm.uncomment(from, to);
+      } else {
+        cm.lineComment(from, to);
+      }
+    }
+  };
+
+  CodeMirror.defineExtension("lineComment", function(from, to, options) {
+    if (!options) options = noOptions;
+    var self = this, mode = self.getModeAt(from);
+    var commentString = options.lineComment || mode.lineComment;
+    if (!commentString) {
+      if (options.blockCommentStart || mode.blockCommentStart) {
+        options.fullLines = true;
+        self.blockComment(from, to, options);
+      }
+      return;
+    }
+    var firstLine = self.getLine(from.line);
+    if (firstLine == null) return;
+    var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1);
+    var pad = options.padding == null ? " " : options.padding;
+    var blankLines = options.commentBlankLines || from.line == to.line;
+
+    self.operation(function() {
+      if (options.indent) {
+        var baseString = firstLine.slice(0, firstNonWS(firstLine));
+        for (var i = from.line; i < end; ++i) {
+          var line = self.getLine(i), cut = baseString.length;
+          if (!blankLines && !nonWS.test(line)) continue;
+          if (line.slice(0, cut) != baseString) cut = firstNonWS(line);
+          self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut));
+        }
+      } else {
+        for (var i = from.line; i < end; ++i) {
+          if (blankLines || nonWS.test(self.getLine(i)))
+            self.replaceRange(commentString + pad, Pos(i, 0));
+        }
+      }
+    });
+  });
+
+  CodeMirror.defineExtension("blockComment", function(from, to, options) {
+    if (!options) options = noOptions;
+    var self = this, mode = self.getModeAt(from);
+    var startString = options.blockCommentStart || mode.blockCommentStart;
+    var endString = options.blockCommentEnd || mode.blockCommentEnd;
+    if (!startString || !endString) {
+      if ((options.lineComment || mode.lineComment) && options.fullLines != false)
+        self.lineComment(from, to, options);
+      return;
+    }
+
+    var end = Math.min(to.line, self.lastLine());
+    if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
+
+    var pad = options.padding == null ? " " : options.padding;
+    if (from.line > end) return;
+
+    self.operation(function() {
+      if (options.fullLines != false) {
+        var lastLineHasText = nonWS.test(self.getLine(end));
+        self.replaceRange(pad + endString, Pos(end));
+        self.replaceRange(startString + pad, Pos(from.line, 0));
+        var lead = options.blockCommentLead || mode.blockCommentLead;
+        if (lead != null) for (var i = from.line + 1; i <= end; ++i)
+          if (i != end || lastLineHasText)
+            self.replaceRange(lead + pad, Pos(i, 0));
+      } else {
+        self.replaceRange(endString, to);
+        self.replaceRange(startString, from);
+      }
+    });
+  });
+
+  CodeMirror.defineExtension("uncomment", function(from, to, options) {
+    if (!options) options = noOptions;
+    var self = this, mode = self.getModeAt(from);
+    var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end);
+
+    // Try finding line comments
+    var lineString = options.lineComment || mode.lineComment, lines = [];
+    var pad = options.padding == null ? " " : options.padding, didSomething;
+    lineComment: {
+      if (!lineString) break lineComment;
+      for (var i = start; i <= end; ++i) {
+        var line = self.getLine(i);
+        var found = line.indexOf(lineString);
+        if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
+        if (found == -1 && (i != end || i == start) && nonWS.test(line)) break lineComment;
+        if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
+        lines.push(line);
+      }
+      self.operation(function() {
+        for (var i = start; i <= end; ++i) {
+          var line = lines[i - start];
+          var pos = line.indexOf(lineString), endPos = pos + lineString.length;
+          if (pos < 0) continue;
+          if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length;
+          didSomething = true;
+          self.replaceRange("", Pos(i, pos), Pos(i, endPos));
+        }
+      });
+      if (didSomething) return true;
+    }
+
+    // Try block comments
+    var startString = options.blockCommentStart || mode.blockCommentStart;
+    var endString = options.blockCommentEnd || mode.blockCommentEnd;
+    if (!startString || !endString) return false;
+    var lead = options.blockCommentLead || mode.blockCommentLead;
+    var startLine = self.getLine(start), endLine = end == start ? startLine : self.getLine(end);
+    var open = startLine.indexOf(startString), close = endLine.lastIndexOf(endString);
+    if (close == -1 && start != end) {
+      endLine = self.getLine(--end);
+      close = endLine.lastIndexOf(endString);
+    }
+    if (open == -1 || close == -1 ||
+        !/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) ||
+        !/comment/.test(self.getTokenTypeAt(Pos(end, close + 1))))
+      return false;
+
+    // Avoid killing block comments completely outside the selection.
+    // Positions of the last startString before the start of the selection, and the first endString after it.
+    var lastStart = startLine.lastIndexOf(startString, from.ch);
+    var firstEnd = lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length);
+    if (lastStart != -1 && firstEnd != -1 && firstEnd + endString.length != from.ch) return false;
+    // Positions of the first endString after the end of the selection, and the last startString before it.
+    firstEnd = endLine.indexOf(endString, to.ch);
+    var almostLastStart = endLine.slice(to.ch).lastIndexOf(startString, firstEnd - to.ch);
+    lastStart = (firstEnd == -1 || almostLastStart == -1) ? -1 : to.ch + almostLastStart;
+    if (firstEnd != -1 && lastStart != -1 && lastStart != to.ch) return false;
+
+    self.operation(function() {
+      self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
+                        Pos(end, close + endString.length));
+      var openEnd = open + startString.length;
+      if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length;
+      self.replaceRange("", Pos(start, open), Pos(start, openEnd));
+      if (lead) for (var i = start + 1; i <= end; ++i) {
+        var line = self.getLine(i), found = line.indexOf(lead);
+        if (found == -1 || nonWS.test(line.slice(0, found))) continue;
+        var foundEnd = found + lead.length;
+        if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length;
+        self.replaceRange("", Pos(i, found), Pos(i, foundEnd));
+      }
+    });
+    return true;
+  });
+});

+ 85 - 0
public/theme/material/editor/lib/codemirror/addon/comment/continuecomment.js

@@ -0,0 +1,85 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  var modes = ["clike", "css", "javascript"];
+
+  for (var i = 0; i < modes.length; ++i)
+    CodeMirror.extendMode(modes[i], {blockCommentContinue: " * "});
+
+  function continueComment(cm) {
+    if (cm.getOption("disableInput")) return CodeMirror.Pass;
+    var ranges = cm.listSelections(), mode, inserts = [];
+    for (var i = 0; i < ranges.length; i++) {
+      var pos = ranges[i].head, token = cm.getTokenAt(pos);
+      if (token.type != "comment") return CodeMirror.Pass;
+      var modeHere = CodeMirror.innerMode(cm.getMode(), token.state).mode;
+      if (!mode) mode = modeHere;
+      else if (mode != modeHere) return CodeMirror.Pass;
+
+      var insert = null;
+      if (mode.blockCommentStart && mode.blockCommentContinue) {
+        var end = token.string.indexOf(mode.blockCommentEnd);
+        var full = cm.getRange(CodeMirror.Pos(pos.line, 0), CodeMirror.Pos(pos.line, token.end)), found;
+        if (end != -1 && end == token.string.length - mode.blockCommentEnd.length && pos.ch >= end) {
+          // Comment ended, don't continue it
+        } else if (token.string.indexOf(mode.blockCommentStart) == 0) {
+          insert = full.slice(0, token.start);
+          if (!/^\s*$/.test(insert)) {
+            insert = "";
+            for (var j = 0; j < token.start; ++j) insert += " ";
+          }
+        } else if ((found = full.indexOf(mode.blockCommentContinue)) != -1 &&
+                   found + mode.blockCommentContinue.length > token.start &&
+                   /^\s*$/.test(full.slice(0, found))) {
+          insert = full.slice(0, found);
+        }
+        if (insert != null) insert += mode.blockCommentContinue;
+      }
+      if (insert == null && mode.lineComment && continueLineCommentEnabled(cm)) {
+        var line = cm.getLine(pos.line), found = line.indexOf(mode.lineComment);
+        if (found > -1) {
+          insert = line.slice(0, found);
+          if (/\S/.test(insert)) insert = null;
+          else insert += mode.lineComment + line.slice(found + mode.lineComment.length).match(/^\s*/)[0];
+        }
+      }
+      if (insert == null) return CodeMirror.Pass;
+      inserts[i] = "\n" + insert;
+    }
+
+    cm.operation(function() {
+      for (var i = ranges.length - 1; i >= 0; i--)
+        cm.replaceRange(inserts[i], ranges[i].from(), ranges[i].to(), "+insert");
+    });
+  }
+
+  function continueLineCommentEnabled(cm) {
+    var opt = cm.getOption("continueComments");
+    if (opt && typeof opt == "object")
+      return opt.continueLineComment !== false;
+    return true;
+  }
+
+  CodeMirror.defineOption("continueComments", null, function(cm, val, prev) {
+    if (prev && prev != CodeMirror.Init)
+      cm.removeKeyMap("continueComment");
+    if (val) {
+      var key = "Enter";
+      if (typeof val == "string")
+        key = val;
+      else if (typeof val == "object" && val.key)
+        key = val.key;
+      var map = {name: "continueComment"};
+      map[key] = continueComment;
+      cm.addKeyMap(map);
+    }
+  });
+});

+ 32 - 0
public/theme/material/editor/lib/codemirror/addon/dialog/dialog.css

@@ -0,0 +1,32 @@
+.CodeMirror-dialog {
+  position: absolute;
+  left: 0; right: 0;
+  background: white;
+  z-index: 15;
+  padding: .1em .8em;
+  overflow: hidden;
+  color: #333;
+}
+
+.CodeMirror-dialog-top {
+  border-bottom: 1px solid #eee;
+  top: 0;
+}
+
+.CodeMirror-dialog-bottom {
+  border-top: 1px solid #eee;
+  bottom: 0;
+}
+
+.CodeMirror-dialog input {
+  border: none;
+  outline: none;
+  background: transparent;
+  width: 20em;
+  color: inherit;
+  font-family: monospace;
+}
+
+.CodeMirror-dialog button {
+  font-size: 70%;
+}

+ 155 - 0
public/theme/material/editor/lib/codemirror/addon/dialog/dialog.js

@@ -0,0 +1,155 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Open simple dialogs on top of an editor. Relies on dialog.css.
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  function dialogDiv(cm, template, bottom) {
+    var wrap = cm.getWrapperElement();
+    var dialog;
+    dialog = wrap.appendChild(document.createElement("div"));
+    if (bottom)
+      dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom";
+    else
+      dialog.className = "CodeMirror-dialog CodeMirror-dialog-top";
+
+    if (typeof template == "string") {
+      dialog.innerHTML = template;
+    } else { // Assuming it's a detached DOM element.
+      dialog.appendChild(template);
+    }
+    return dialog;
+  }
+
+  function closeNotification(cm, newVal) {
+    if (cm.state.currentNotificationClose)
+      cm.state.currentNotificationClose();
+    cm.state.currentNotificationClose = newVal;
+  }
+
+  CodeMirror.defineExtension("openDialog", function(template, callback, options) {
+    if (!options) options = {};
+
+    closeNotification(this, null);
+
+    var dialog = dialogDiv(this, template, options.bottom);
+    var closed = false, me = this;
+    function close(newVal) {
+      if (typeof newVal == 'string') {
+        inp.value = newVal;
+      } else {
+        if (closed) return;
+        closed = true;
+        dialog.parentNode.removeChild(dialog);
+        me.focus();
+
+        if (options.onClose) options.onClose(dialog);
+      }
+    }
+
+    var inp = dialog.getElementsByTagName("input")[0], button;
+    if (inp) {
+      if (options.value) {
+        inp.value = options.value;
+        inp.select();
+      }
+
+      if (options.onInput)
+        CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);});
+      if (options.onKeyUp)
+        CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);});
+
+      CodeMirror.on(inp, "keydown", function(e) {
+        if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
+        if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) {
+          inp.blur();
+          CodeMirror.e_stop(e);
+          close();
+        }
+        if (e.keyCode == 13) callback(inp.value, e);
+      });
+
+      if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
+
+      inp.focus();
+    } else if (button = dialog.getElementsByTagName("button")[0]) {
+      CodeMirror.on(button, "click", function() {
+        close();
+        me.focus();
+      });
+
+      if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close);
+
+      button.focus();
+    }
+    return close;
+  });
+
+  CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
+    closeNotification(this, null);
+    var dialog = dialogDiv(this, template, options && options.bottom);
+    var buttons = dialog.getElementsByTagName("button");
+    var closed = false, me = this, blurring = 1;
+    function close() {
+      if (closed) return;
+      closed = true;
+      dialog.parentNode.removeChild(dialog);
+      me.focus();
+    }
+    buttons[0].focus();
+    for (var i = 0; i < buttons.length; ++i) {
+      var b = buttons[i];
+      (function(callback) {
+        CodeMirror.on(b, "click", function(e) {
+          CodeMirror.e_preventDefault(e);
+          close();
+          if (callback) callback(me);
+        });
+      })(callbacks[i]);
+      CodeMirror.on(b, "blur", function() {
+        --blurring;
+        setTimeout(function() { if (blurring <= 0) close(); }, 200);
+      });
+      CodeMirror.on(b, "focus", function() { ++blurring; });
+    }
+  });
+
+  /*
+   * openNotification
+   * Opens a notification, that can be closed with an optional timer
+   * (default 5000ms timer) and always closes on click.
+   *
+   * If a notification is opened while another is opened, it will close the
+   * currently opened one and open the new one immediately.
+   */
+  CodeMirror.defineExtension("openNotification", function(template, options) {
+    closeNotification(this, close);
+    var dialog = dialogDiv(this, template, options && options.bottom);
+    var closed = false, doneTimer;
+    var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000;
+
+    function close() {
+      if (closed) return;
+      closed = true;
+      clearTimeout(doneTimer);
+      dialog.parentNode.removeChild(dialog);
+    }
+
+    CodeMirror.on(dialog, 'click', function(e) {
+      CodeMirror.e_preventDefault(e);
+      close();
+    });
+
+    if (duration)
+      doneTimer = setTimeout(close, duration);
+
+    return close;
+  });
+});

+ 6 - 0
public/theme/material/editor/lib/codemirror/addon/display/fullscreen.css

@@ -0,0 +1,6 @@
+.CodeMirror-fullscreen {
+  position: fixed;
+  top: 0; left: 0; right: 0; bottom: 0;
+  height: auto;
+  z-index: 9;
+}

+ 41 - 0
public/theme/material/editor/lib/codemirror/addon/display/fullscreen.js

@@ -0,0 +1,41 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  CodeMirror.defineOption("fullScreen", false, function(cm, val, old) {
+    if (old == CodeMirror.Init) old = false;
+    if (!old == !val) return;
+    if (val) setFullscreen(cm);
+    else setNormal(cm);
+  });
+
+  function setFullscreen(cm) {
+    var wrap = cm.getWrapperElement();
+    cm.state.fullScreenRestore = {scrollTop: window.pageYOffset, scrollLeft: window.pageXOffset,
+                                  width: wrap.style.width, height: wrap.style.height};
+    wrap.style.width = "";
+    wrap.style.height = "auto";
+    wrap.className += " CodeMirror-fullscreen";
+    document.documentElement.style.overflow = "hidden";
+    cm.refresh();
+  }
+
+  function setNormal(cm) {
+    var wrap = cm.getWrapperElement();
+    wrap.className = wrap.className.replace(/\s*CodeMirror-fullscreen\b/, "");
+    document.documentElement.style.overflow = "";
+    var info = cm.state.fullScreenRestore;
+    wrap.style.width = info.width; wrap.style.height = info.height;
+    window.scrollTo(info.scrollLeft, info.scrollTop);
+    cm.refresh();
+  }
+});

+ 94 - 0
public/theme/material/editor/lib/codemirror/addon/display/panel.js

@@ -0,0 +1,94 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  CodeMirror.defineExtension("addPanel", function(node, options) {
+    if (!this.state.panels) initPanels(this);
+
+    var info = this.state.panels;
+    if (options && options.position == "bottom")
+      info.wrapper.appendChild(node);
+    else
+      info.wrapper.insertBefore(node, info.wrapper.firstChild);
+    var height = (options && options.height) || node.offsetHeight;
+    this._setSize(null, info.heightLeft -= height);
+    info.panels++;
+    return new Panel(this, node, options, height);
+  });
+
+  function Panel(cm, node, options, height) {
+    this.cm = cm;
+    this.node = node;
+    this.options = options;
+    this.height = height;
+    this.cleared = false;
+  }
+
+  Panel.prototype.clear = function() {
+    if (this.cleared) return;
+    this.cleared = true;
+    var info = this.cm.state.panels;
+    this.cm._setSize(null, info.heightLeft += this.height);
+    info.wrapper.removeChild(this.node);
+    if (--info.panels == 0) removePanels(this.cm);
+  };
+
+  Panel.prototype.changed = function(height) {
+    var newHeight = height == null ? this.node.offsetHeight : height;
+    var info = this.cm.state.panels;
+    this.cm._setSize(null, info.height += (newHeight - this.height));
+    this.height = newHeight;
+  };
+
+  function initPanels(cm) {
+    var wrap = cm.getWrapperElement();
+    var style = window.getComputedStyle ? window.getComputedStyle(wrap) : wrap.currentStyle;
+    var height = parseInt(style.height);
+    var info = cm.state.panels = {
+      setHeight: wrap.style.height,
+      heightLeft: height,
+      panels: 0,
+      wrapper: document.createElement("div")
+    };
+    wrap.parentNode.insertBefore(info.wrapper, wrap);
+    var hasFocus = cm.hasFocus();
+    info.wrapper.appendChild(wrap);
+    if (hasFocus) cm.focus();
+
+    cm._setSize = cm.setSize;
+    if (height != null) cm.setSize = function(width, newHeight) {
+      if (newHeight == null) return this._setSize(width, newHeight);
+      info.setHeight = newHeight;
+      if (typeof newHeight != "number") {
+        var px = /^(\d+\.?\d*)px$/.exec(newHeight);
+        if (px) {
+          newHeight = Number(px[1]);
+        } else {
+          info.wrapper.style.height = newHeight;
+          newHeight = info.wrapper.offsetHeight;
+          info.wrapper.style.height = "";
+        }
+      }
+      cm._setSize(width, info.heightLeft += (newHeight - height));
+      height = newHeight;
+    };
+  }
+
+  function removePanels(cm) {
+    var info = cm.state.panels;
+    cm.state.panels = null;
+
+    var wrap = cm.getWrapperElement();
+    info.wrapper.parentNode.replaceChild(wrap, info.wrapper);
+    wrap.style.height = info.setHeight;
+    cm.setSize = cm._setSize;
+    cm.setSize();
+  }
+});

+ 58 - 0
public/theme/material/editor/lib/codemirror/addon/display/placeholder.js

@@ -0,0 +1,58 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  CodeMirror.defineOption("placeholder", "", function(cm, val, old) {
+    var prev = old && old != CodeMirror.Init;
+    if (val && !prev) {
+      cm.on("blur", onBlur);
+      cm.on("change", onChange);
+      onChange(cm);
+    } else if (!val && prev) {
+      cm.off("blur", onBlur);
+      cm.off("change", onChange);
+      clearPlaceholder(cm);
+      var wrapper = cm.getWrapperElement();
+      wrapper.className = wrapper.className.replace(" CodeMirror-empty", "");
+    }
+
+    if (val && !cm.hasFocus()) onBlur(cm);
+  });
+
+  function clearPlaceholder(cm) {
+    if (cm.state.placeholder) {
+      cm.state.placeholder.parentNode.removeChild(cm.state.placeholder);
+      cm.state.placeholder = null;
+    }
+  }
+  function setPlaceholder(cm) {
+    clearPlaceholder(cm);
+    var elt = cm.state.placeholder = document.createElement("pre");
+    elt.style.cssText = "height: 0; overflow: visible";
+    elt.className = "CodeMirror-placeholder";
+    elt.appendChild(document.createTextNode(cm.getOption("placeholder")));
+    cm.display.lineSpace.insertBefore(elt, cm.display.lineSpace.firstChild);
+  }
+
+  function onBlur(cm) {
+    if (isEmpty(cm)) setPlaceholder(cm);
+  }
+  function onChange(cm) {
+    var wrapper = cm.getWrapperElement(), empty = isEmpty(cm);
+    wrapper.className = wrapper.className.replace(" CodeMirror-empty", "") + (empty ? " CodeMirror-empty" : "");
+
+    if (empty) setPlaceholder(cm);
+    else clearPlaceholder(cm);
+  }
+
+  function isEmpty(cm) {
+    return (cm.lineCount() === 1) && (cm.getLine(0) === "");
+  }
+});

+ 64 - 0
public/theme/material/editor/lib/codemirror/addon/display/rulers.js

@@ -0,0 +1,64 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  CodeMirror.defineOption("rulers", false, function(cm, val, old) {
+    if (old && old != CodeMirror.Init) {
+      clearRulers(cm);
+      cm.off("refresh", refreshRulers);
+    }
+    if (val && val.length) {
+      setRulers(cm);
+      cm.on("refresh", refreshRulers);
+    }
+  });
+
+  function clearRulers(cm) {
+    for (var i = cm.display.lineSpace.childNodes.length - 1; i >= 0; i--) {
+      var node = cm.display.lineSpace.childNodes[i];
+      if (/(^|\s)CodeMirror-ruler($|\s)/.test(node.className))
+        node.parentNode.removeChild(node);
+    }
+  }
+
+  function setRulers(cm) {
+    var val = cm.getOption("rulers");
+    var cw = cm.defaultCharWidth();
+    var left = cm.charCoords(CodeMirror.Pos(cm.firstLine(), 0), "div").left;
+    var minH = cm.display.scroller.offsetHeight + 30;
+    for (var i = 0; i < val.length; i++) {
+      var elt = document.createElement("div");
+      elt.className = "CodeMirror-ruler";
+      var col, cls = null, conf = val[i];
+      if (typeof conf == "number") {
+        col = conf;
+      } else {
+        col = conf.column;
+        if (conf.className) elt.className += " " + conf.className;
+        if (conf.color) elt.style.borderColor = conf.color;
+        if (conf.lineStyle) elt.style.borderLeftStyle = conf.lineStyle;
+        if (conf.width) elt.style.borderLeftWidth = conf.width;
+        cls = val[i].className;
+      }
+      elt.style.left = (left + col * cw) + "px";
+      elt.style.top = "-50px";
+      elt.style.bottom = "-20px";
+      elt.style.minHeight = minH + "px";
+      cm.display.lineSpace.insertBefore(elt, cm.display.cursorDiv);
+    }
+  }
+
+  function refreshRulers(cm) {
+    clearRulers(cm);
+    setRulers(cm);
+  }
+});

+ 161 - 0
public/theme/material/editor/lib/codemirror/addon/edit/closebrackets.js

@@ -0,0 +1,161 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  var DEFAULT_BRACKETS = "()[]{}''\"\"";
+  var DEFAULT_TRIPLES = "'\"";
+  var DEFAULT_EXPLODE_ON_ENTER = "[]{}";
+  var SPACE_CHAR_REGEX = /\s/;
+
+  var Pos = CodeMirror.Pos;
+
+  CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) {
+    if (old != CodeMirror.Init && old)
+      cm.removeKeyMap("autoCloseBrackets");
+    if (!val) return;
+    var pairs = DEFAULT_BRACKETS, triples = DEFAULT_TRIPLES, explode = DEFAULT_EXPLODE_ON_ENTER;
+    if (typeof val == "string") pairs = val;
+    else if (typeof val == "object") {
+      if (val.pairs != null) pairs = val.pairs;
+      if (val.triples != null) triples = val.triples;
+      if (val.explode != null) explode = val.explode;
+    }
+    var map = buildKeymap(pairs, triples);
+    if (explode) map.Enter = buildExplodeHandler(explode);
+    cm.addKeyMap(map);
+  });
+
+  function charsAround(cm, pos) {
+    var str = cm.getRange(Pos(pos.line, pos.ch - 1),
+                          Pos(pos.line, pos.ch + 1));
+    return str.length == 2 ? str : null;
+  }
+
+  // Project the token type that will exists after the given char is
+  // typed, and use it to determine whether it would cause the start
+  // of a string token.
+  function enteringString(cm, pos, ch) {
+    var line = cm.getLine(pos.line);
+    var token = cm.getTokenAt(pos);
+    if (/\bstring2?\b/.test(token.type)) return false;
+    var stream = new CodeMirror.StringStream(line.slice(0, pos.ch) + ch + line.slice(pos.ch), 4);
+    stream.pos = stream.start = token.start;
+    for (;;) {
+      var type1 = cm.getMode().token(stream, token.state);
+      if (stream.pos >= pos.ch + 1) return /\bstring2?\b/.test(type1);
+      stream.start = stream.pos;
+    }
+  }
+
+  function buildKeymap(pairs, triples) {
+    var map = {
+      name : "autoCloseBrackets",
+      Backspace: function(cm) {
+        if (cm.getOption("disableInput")) return CodeMirror.Pass;
+        var ranges = cm.listSelections();
+        for (var i = 0; i < ranges.length; i++) {
+          if (!ranges[i].empty()) return CodeMirror.Pass;
+          var around = charsAround(cm, ranges[i].head);
+          if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
+        }
+        for (var i = ranges.length - 1; i >= 0; i--) {
+          var cur = ranges[i].head;
+          cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1));
+        }
+      }
+    };
+    var closingBrackets = "";
+    for (var i = 0; i < pairs.length; i += 2) (function(left, right) {
+      closingBrackets += right;
+      map["'" + left + "'"] = function(cm) {
+        if (cm.getOption("disableInput")) return CodeMirror.Pass;
+        var ranges = cm.listSelections(), type, next;
+        for (var i = 0; i < ranges.length; i++) {
+          var range = ranges[i], cur = range.head, curType;
+          var next = cm.getRange(cur, Pos(cur.line, cur.ch + 1));
+          if (!range.empty()) {
+            curType = "surround";
+          } else if (left == right && next == right) {
+            if (cm.getRange(cur, Pos(cur.line, cur.ch + 3)) == left + left + left)
+              curType = "skipThree";
+            else
+              curType = "skip";
+          } else if (left == right && cur.ch > 1 && triples.indexOf(left) >= 0 &&
+                     cm.getRange(Pos(cur.line, cur.ch - 2), cur) == left + left &&
+                     (cur.ch <= 2 || cm.getRange(Pos(cur.line, cur.ch - 3), Pos(cur.line, cur.ch - 2)) != left)) {
+            curType = "addFour";
+          } else if (left == '"' || left == "'") {
+            if (!CodeMirror.isWordChar(next) && enteringString(cm, cur, left)) curType = "both";
+            else return CodeMirror.Pass;
+          } else if (cm.getLine(cur.line).length == cur.ch || closingBrackets.indexOf(next) >= 0 || SPACE_CHAR_REGEX.test(next)) {
+            curType = "both";
+          } else {
+            return CodeMirror.Pass;
+          }
+          if (!type) type = curType;
+          else if (type != curType) return CodeMirror.Pass;
+        }
+
+        cm.operation(function() {
+          if (type == "skip") {
+            cm.execCommand("goCharRight");
+          } else if (type == "skipThree") {
+            for (var i = 0; i < 3; i++)
+              cm.execCommand("goCharRight");
+          } else if (type == "surround") {
+            var sels = cm.getSelections();
+            for (var i = 0; i < sels.length; i++)
+              sels[i] = left + sels[i] + right;
+            cm.replaceSelections(sels, "around");
+          } else if (type == "both") {
+            cm.replaceSelection(left + right, null);
+            cm.execCommand("goCharLeft");
+          } else if (type == "addFour") {
+            cm.replaceSelection(left + left + left + left, "before");
+            cm.execCommand("goCharRight");
+          }
+        });
+      };
+      if (left != right) map["'" + right + "'"] = function(cm) {
+        var ranges = cm.listSelections();
+        for (var i = 0; i < ranges.length; i++) {
+          var range = ranges[i];
+          if (!range.empty() ||
+              cm.getRange(range.head, Pos(range.head.line, range.head.ch + 1)) != right)
+            return CodeMirror.Pass;
+        }
+        cm.execCommand("goCharRight");
+      };
+    })(pairs.charAt(i), pairs.charAt(i + 1));
+    return map;
+  }
+
+  function buildExplodeHandler(pairs) {
+    return function(cm) {
+      if (cm.getOption("disableInput")) return CodeMirror.Pass;
+      var ranges = cm.listSelections();
+      for (var i = 0; i < ranges.length; i++) {
+        if (!ranges[i].empty()) return CodeMirror.Pass;
+        var around = charsAround(cm, ranges[i].head);
+        if (!around || pairs.indexOf(around) % 2 != 0) return CodeMirror.Pass;
+      }
+      cm.operation(function() {
+        cm.replaceSelection("\n\n", null);
+        cm.execCommand("goCharLeft");
+        ranges = cm.listSelections();
+        for (var i = 0; i < ranges.length; i++) {
+          var line = ranges[i].head.line;
+          cm.indentLine(line, null, true);
+          cm.indentLine(line + 1, null, true);
+        }
+      });
+    };
+  }
+});

+ 166 - 0
public/theme/material/editor/lib/codemirror/addon/edit/closetag.js

@@ -0,0 +1,166 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+/**
+ * Tag-closer extension for CodeMirror.
+ *
+ * This extension adds an "autoCloseTags" option that can be set to
+ * either true to get the default behavior, or an object to further
+ * configure its behavior.
+ *
+ * These are supported options:
+ *
+ * `whenClosing` (default true)
+ *   Whether to autoclose when the '/' of a closing tag is typed.
+ * `whenOpening` (default true)
+ *   Whether to autoclose the tag when the final '>' of an opening
+ *   tag is typed.
+ * `dontCloseTags` (default is empty tags for HTML, none for XML)
+ *   An array of tag names that should not be autoclosed.
+ * `indentTags` (default is block tags for HTML, none for XML)
+ *   An array of tag names that should, when opened, cause a
+ *   blank line to be added inside the tag, and the blank line and
+ *   closing line to be indented.
+ *
+ * See demos/closetag.html for a usage example.
+ */
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"), require("../fold/xml-fold"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror", "../fold/xml-fold"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) {
+    if (old != CodeMirror.Init && old)
+      cm.removeKeyMap("autoCloseTags");
+    if (!val) return;
+    var map = {name: "autoCloseTags"};
+    if (typeof val != "object" || val.whenClosing)
+      map["'/'"] = function(cm) { return autoCloseSlash(cm); };
+    if (typeof val != "object" || val.whenOpening)
+      map["'>'"] = function(cm) { return autoCloseGT(cm); };
+    cm.addKeyMap(map);
+  });
+
+  var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param",
+                       "source", "track", "wbr"];
+  var htmlIndent = ["applet", "blockquote", "body", "button", "div", "dl", "fieldset", "form", "frameset", "h1", "h2", "h3", "h4",
+                    "h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"];
+
+  function autoCloseGT(cm) {
+    if (cm.getOption("disableInput")) return CodeMirror.Pass;
+    var ranges = cm.listSelections(), replacements = [];
+    for (var i = 0; i < ranges.length; i++) {
+      if (!ranges[i].empty()) return CodeMirror.Pass;
+      var pos = ranges[i].head, tok = cm.getTokenAt(pos);
+      var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
+      if (inner.mode.name != "xml" || !state.tagName) return CodeMirror.Pass;
+
+      var opt = cm.getOption("autoCloseTags"), html = inner.mode.configuration == "html";
+      var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose);
+      var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent);
+
+      var tagName = state.tagName;
+      if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch);
+      var lowerTagName = tagName.toLowerCase();
+      // Don't process the '>' at the end of an end-tag or self-closing tag
+      if (!tagName ||
+          tok.type == "string" && (tok.end != pos.ch || !/[\"\']/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1) ||
+          tok.type == "tag" && state.type == "closeTag" ||
+          tok.string.indexOf("/") == (tok.string.length - 1) || // match something like <someTagName />
+          dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 ||
+          closingTagExists(cm, tagName, pos, state, true))
+        return CodeMirror.Pass;
+
+      var indent = indentTags && indexOf(indentTags, lowerTagName) > -1;
+      replacements[i] = {indent: indent,
+                         text: ">" + (indent ? "\n\n" : "") + "</" + tagName + ">",
+                         newPos: indent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1)};
+    }
+
+    for (var i = ranges.length - 1; i >= 0; i--) {
+      var info = replacements[i];
+      cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, "+insert");
+      var sel = cm.listSelections().slice(0);
+      sel[i] = {head: info.newPos, anchor: info.newPos};
+      cm.setSelections(sel);
+      if (info.indent) {
+        cm.indentLine(info.newPos.line, null, true);
+        cm.indentLine(info.newPos.line + 1, null, true);
+      }
+    }
+  }
+
+  function autoCloseCurrent(cm, typingSlash) {
+    var ranges = cm.listSelections(), replacements = [];
+    var head = typingSlash ? "/" : "</";
+    for (var i = 0; i < ranges.length; i++) {
+      if (!ranges[i].empty()) return CodeMirror.Pass;
+      var pos = ranges[i].head, tok = cm.getTokenAt(pos);
+      var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state;
+      if (typingSlash && (tok.type == "string" || tok.string.charAt(0) != "<" ||
+                          tok.start != pos.ch - 1))
+        return CodeMirror.Pass;
+      // Kludge to get around the fact that we are not in XML mode
+      // when completing in JS/CSS snippet in htmlmixed mode. Does not
+      // work for other XML embedded languages (there is no general
+      // way to go from a mixed mode to its current XML state).
+      if (inner.mode.name != "xml") {
+        if (cm.getMode().name == "htmlmixed" && inner.mode.name == "javascript")
+          replacements[i] = head + "script>";
+        else if (cm.getMode().name == "htmlmixed" && inner.mode.name == "css")
+          replacements[i] = head + "style>";
+        else
+          return CodeMirror.Pass;
+      } else {
+        if (!state.context || !state.context.tagName ||
+            closingTagExists(cm, state.context.tagName, pos, state))
+          return CodeMirror.Pass;
+        replacements[i] = head + state.context.tagName + ">";
+      }
+    }
+    cm.replaceSelections(replacements);
+    ranges = cm.listSelections();
+    for (var i = 0; i < ranges.length; i++)
+      if (i == ranges.length - 1 || ranges[i].head.line < ranges[i + 1].head.line)
+        cm.indentLine(ranges[i].head.line);
+  }
+
+  function autoCloseSlash(cm) {
+    if (cm.getOption("disableInput")) return CodeMirror.Pass;
+    return autoCloseCurrent(cm, true);
+  }
+
+  CodeMirror.commands.closeTag = function(cm) { return autoCloseCurrent(cm); };
+
+  function indexOf(collection, elt) {
+    if (collection.indexOf) return collection.indexOf(elt);
+    for (var i = 0, e = collection.length; i < e; ++i)
+      if (collection[i] == elt) return i;
+    return -1;
+  }
+
+  // If xml-fold is loaded, we use its functionality to try and verify
+  // whether a given tag is actually unclosed.
+  function closingTagExists(cm, tagName, pos, state, newTag) {
+    if (!CodeMirror.scanForClosingTag) return false;
+    var end = Math.min(cm.lastLine() + 1, pos.line + 500);
+    var nextClose = CodeMirror.scanForClosingTag(cm, pos, null, end);
+    if (!nextClose || nextClose.tag != tagName) return false;
+    var cx = state.context;
+    // If the immediate wrapping context contains onCx instances of
+    // the same tag, a closing tag only exists if there are at least
+    // that many closing tags of that type following.
+    for (var onCx = newTag ? 1 : 0; cx && cx.tagName == tagName; cx = cx.prev) ++onCx;
+    pos = nextClose.to;
+    for (var i = 1; i < onCx; i++) {
+      var next = CodeMirror.scanForClosingTag(cm, pos, null, end);
+      if (!next || next.tag != tagName) return false;
+      pos = next.to;
+    }
+    return true;
+  }
+});

+ 51 - 0
public/theme/material/editor/lib/codemirror/addon/edit/continuelist.js

@@ -0,0 +1,51 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  var listRE = /^(\s*)(>[> ]*|[*+-]\s|(\d+)\.)(\s*)/,
+      emptyListRE = /^(\s*)(>[> ]*|[*+-]|(\d+)\.)(\s*)$/,
+      unorderedListRE = /[*+-]\s/;
+
+  CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) {
+    if (cm.getOption("disableInput")) return CodeMirror.Pass;
+    var ranges = cm.listSelections(), replacements = [];
+    for (var i = 0; i < ranges.length; i++) {
+      var pos = ranges[i].head, match;
+      var eolState = cm.getStateAfter(pos.line);
+      var inList = eolState.list !== false;
+      var inQuote = eolState.quote !== false;
+
+      if (!ranges[i].empty() || (!inList && !inQuote) || !(match = cm.getLine(pos.line).match(listRE))) {
+        cm.execCommand("newlineAndIndent");
+        return;
+      }
+      if (cm.getLine(pos.line).match(emptyListRE)) {
+        cm.replaceRange("", {
+          line: pos.line, ch: 0
+        }, {
+          line: pos.line, ch: pos.ch + 1
+        });
+        replacements[i] = "\n";
+
+      } else {
+        var indent = match[1], after = match[4];
+        var bullet = unorderedListRE.test(match[2]) || match[2].indexOf(">") >= 0
+          ? match[2]
+          : (parseInt(match[3], 10) + 1) + ".";
+
+        replacements[i] = "\n" + indent + bullet + after;
+      }
+    }
+
+    cm.replaceSelections(replacements);
+  };
+});

+ 120 - 0
public/theme/material/editor/lib/codemirror/addon/edit/matchbrackets.js

@@ -0,0 +1,120 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  var ie_lt8 = /MSIE \d/.test(navigator.userAgent) &&
+    (document.documentMode == null || document.documentMode < 8);
+
+  var Pos = CodeMirror.Pos;
+
+  var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"};
+
+  function findMatchingBracket(cm, where, strict, config) {
+    var line = cm.getLineHandle(where.line), pos = where.ch - 1;
+    var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)];
+    if (!match) return null;
+    var dir = match.charAt(1) == ">" ? 1 : -1;
+    if (strict && (dir > 0) != (pos == where.ch)) return null;
+    var style = cm.getTokenTypeAt(Pos(where.line, pos + 1));
+
+    var found = scanForBracket(cm, Pos(where.line, pos + (dir > 0 ? 1 : 0)), dir, style || null, config);
+    if (found == null) return null;
+    return {from: Pos(where.line, pos), to: found && found.pos,
+            match: found && found.ch == match.charAt(0), forward: dir > 0};
+  }
+
+  // bracketRegex is used to specify which type of bracket to scan
+  // should be a regexp, e.g. /[[\]]/
+  //
+  // Note: If "where" is on an open bracket, then this bracket is ignored.
+  //
+  // Returns false when no bracket was found, null when it reached
+  // maxScanLines and gave up
+  function scanForBracket(cm, where, dir, style, config) {
+    var maxScanLen = (config && config.maxScanLineLength) || 10000;
+    var maxScanLines = (config && config.maxScanLines) || 1000;
+
+    var stack = [];
+    var re = config && config.bracketRegex ? config.bracketRegex : /[(){}[\]]/;
+    var lineEnd = dir > 0 ? Math.min(where.line + maxScanLines, cm.lastLine() + 1)
+                          : Math.max(cm.firstLine() - 1, where.line - maxScanLines);
+    for (var lineNo = where.line; lineNo != lineEnd; lineNo += dir) {
+      var line = cm.getLine(lineNo);
+      if (!line) continue;
+      var pos = dir > 0 ? 0 : line.length - 1, end = dir > 0 ? line.length : -1;
+      if (line.length > maxScanLen) continue;
+      if (lineNo == where.line) pos = where.ch - (dir < 0 ? 1 : 0);
+      for (; pos != end; pos += dir) {
+        var ch = line.charAt(pos);
+        if (re.test(ch) && (style === undefined || cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style)) {
+          var match = matching[ch];
+          if ((match.charAt(1) == ">") == (dir > 0)) stack.push(ch);
+          else if (!stack.length) return {pos: Pos(lineNo, pos), ch: ch};
+          else stack.pop();
+        }
+      }
+    }
+    return lineNo - dir == (dir > 0 ? cm.lastLine() : cm.firstLine()) ? false : null;
+  }
+
+  function matchBrackets(cm, autoclear, config) {
+    // Disable brace matching in long lines, since it'll cause hugely slow updates
+    var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000;
+    var marks = [], ranges = cm.listSelections();
+    for (var i = 0; i < ranges.length; i++) {
+      var match = ranges[i].empty() && findMatchingBracket(cm, ranges[i].head, false, config);
+      if (match && cm.getLine(match.from.line).length <= maxHighlightLen) {
+        var style = match.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
+        marks.push(cm.markText(match.from, Pos(match.from.line, match.from.ch + 1), {className: style}));
+        if (match.to && cm.getLine(match.to.line).length <= maxHighlightLen)
+          marks.push(cm.markText(match.to, Pos(match.to.line, match.to.ch + 1), {className: style}));
+      }
+    }
+
+    if (marks.length) {
+      // Kludge to work around the IE bug from issue #1193, where text
+      // input stops going to the textare whever this fires.
+      if (ie_lt8 && cm.state.focused) cm.focus();
+
+      var clear = function() {
+        cm.operation(function() {
+          for (var i = 0; i < marks.length; i++) marks[i].clear();
+        });
+      };
+      if (autoclear) setTimeout(clear, 800);
+      else return clear;
+    }
+  }
+
+  var currentlyHighlighted = null;
+  function doMatchBrackets(cm) {
+    cm.operation(function() {
+      if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
+      currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
+    });
+  }
+
+  CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
+    if (old && old != CodeMirror.Init)
+      cm.off("cursorActivity", doMatchBrackets);
+    if (val) {
+      cm.state.matchBrackets = typeof val == "object" ? val : {};
+      cm.on("cursorActivity", doMatchBrackets);
+    }
+  });
+
+  CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);});
+  CodeMirror.defineExtension("findMatchingBracket", function(pos, strict, config){
+    return findMatchingBracket(this, pos, strict, config);
+  });
+  CodeMirror.defineExtension("scanForBracket", function(pos, dir, style, config){
+    return scanForBracket(this, pos, dir, style, config);
+  });
+});

+ 66 - 0
public/theme/material/editor/lib/codemirror/addon/edit/matchtags.js

@@ -0,0 +1,66 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"), require("../fold/xml-fold"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror", "../fold/xml-fold"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  CodeMirror.defineOption("matchTags", false, function(cm, val, old) {
+    if (old && old != CodeMirror.Init) {
+      cm.off("cursorActivity", doMatchTags);
+      cm.off("viewportChange", maybeUpdateMatch);
+      clear(cm);
+    }
+    if (val) {
+      cm.state.matchBothTags = typeof val == "object" && val.bothTags;
+      cm.on("cursorActivity", doMatchTags);
+      cm.on("viewportChange", maybeUpdateMatch);
+      doMatchTags(cm);
+    }
+  });
+
+  function clear(cm) {
+    if (cm.state.tagHit) cm.state.tagHit.clear();
+    if (cm.state.tagOther) cm.state.tagOther.clear();
+    cm.state.tagHit = cm.state.tagOther = null;
+  }
+
+  function doMatchTags(cm) {
+    cm.state.failedTagMatch = false;
+    cm.operation(function() {
+      clear(cm);
+      if (cm.somethingSelected()) return;
+      var cur = cm.getCursor(), range = cm.getViewport();
+      range.from = Math.min(range.from, cur.line); range.to = Math.max(cur.line + 1, range.to);
+      var match = CodeMirror.findMatchingTag(cm, cur, range);
+      if (!match) return;
+      if (cm.state.matchBothTags) {
+        var hit = match.at == "open" ? match.open : match.close;
+        if (hit) cm.state.tagHit = cm.markText(hit.from, hit.to, {className: "CodeMirror-matchingtag"});
+      }
+      var other = match.at == "close" ? match.open : match.close;
+      if (other)
+        cm.state.tagOther = cm.markText(other.from, other.to, {className: "CodeMirror-matchingtag"});
+      else
+        cm.state.failedTagMatch = true;
+    });
+  }
+
+  function maybeUpdateMatch(cm) {
+    if (cm.state.failedTagMatch) doMatchTags(cm);
+  }
+
+  CodeMirror.commands.toMatchingTag = function(cm) {
+    var found = CodeMirror.findMatchingTag(cm, cm.getCursor());
+    if (found) {
+      var other = found.at == "close" ? found.open : found.close;
+      if (other) cm.extendSelection(other.to, other.from);
+    }
+  };
+});

+ 27 - 0
public/theme/material/editor/lib/codemirror/addon/edit/trailingspace.js

@@ -0,0 +1,27 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  CodeMirror.defineOption("showTrailingSpace", false, function(cm, val, prev) {
+    if (prev == CodeMirror.Init) prev = false;
+    if (prev && !val)
+      cm.removeOverlay("trailingspace");
+    else if (!prev && val)
+      cm.addOverlay({
+        token: function(stream) {
+          for (var l = stream.string.length, i = l; i && /\s/.test(stream.string.charAt(i - 1)); --i) {}
+          if (i > stream.pos) { stream.pos = i; return null; }
+          stream.pos = l;
+          return "trailingspace";
+        },
+        name: "trailingspace"
+      });
+  });
+});

+ 105 - 0
public/theme/material/editor/lib/codemirror/addon/fold/brace-fold.js

@@ -0,0 +1,105 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerHelper("fold", "brace", function(cm, start) {
+  var line = start.line, lineText = cm.getLine(line);
+  var startCh, tokenType;
+
+  function findOpening(openCh) {
+    for (var at = start.ch, pass = 0;;) {
+      var found = at <= 0 ? -1 : lineText.lastIndexOf(openCh, at - 1);
+      if (found == -1) {
+        if (pass == 1) break;
+        pass = 1;
+        at = lineText.length;
+        continue;
+      }
+      if (pass == 1 && found < start.ch) break;
+      tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1));
+      if (!/^(comment|string)/.test(tokenType)) return found + 1;
+      at = found - 1;
+    }
+  }
+
+  var startToken = "{", endToken = "}", startCh = findOpening("{");
+  if (startCh == null) {
+    startToken = "[", endToken = "]";
+    startCh = findOpening("[");
+  }
+
+  if (startCh == null) return;
+  var count = 1, lastLine = cm.lastLine(), end, endCh;
+  outer: for (var i = line; i <= lastLine; ++i) {
+    var text = cm.getLine(i), pos = i == line ? startCh : 0;
+    for (;;) {
+      var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos);
+      if (nextOpen < 0) nextOpen = text.length;
+      if (nextClose < 0) nextClose = text.length;
+      pos = Math.min(nextOpen, nextClose);
+      if (pos == text.length) break;
+      if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == tokenType) {
+        if (pos == nextOpen) ++count;
+        else if (!--count) { end = i; endCh = pos; break outer; }
+      }
+      ++pos;
+    }
+  }
+  if (end == null || line == end && endCh == startCh) return;
+  return {from: CodeMirror.Pos(line, startCh),
+          to: CodeMirror.Pos(end, endCh)};
+});
+
+CodeMirror.registerHelper("fold", "import", function(cm, start) {
+  function hasImport(line) {
+    if (line < cm.firstLine() || line > cm.lastLine()) return null;
+    var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
+    if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
+    if (start.type != "keyword" || start.string != "import") return null;
+    // Now find closing semicolon, return its position
+    for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) {
+      var text = cm.getLine(i), semi = text.indexOf(";");
+      if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)};
+    }
+  }
+
+  var start = start.line, has = hasImport(start), prev;
+  if (!has || hasImport(start - 1) || ((prev = hasImport(start - 2)) && prev.end.line == start - 1))
+    return null;
+  for (var end = has.end;;) {
+    var next = hasImport(end.line + 1);
+    if (next == null) break;
+    end = next.end;
+  }
+  return {from: cm.clipPos(CodeMirror.Pos(start, has.startCh + 1)), to: end};
+});
+
+CodeMirror.registerHelper("fold", "include", function(cm, start) {
+  function hasInclude(line) {
+    if (line < cm.firstLine() || line > cm.lastLine()) return null;
+    var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
+    if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
+    if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8;
+  }
+
+  var start = start.line, has = hasInclude(start);
+  if (has == null || hasInclude(start - 1) != null) return null;
+  for (var end = start;;) {
+    var next = hasInclude(end + 1);
+    if (next == null) break;
+    ++end;
+  }
+  return {from: CodeMirror.Pos(start, has + 1),
+          to: cm.clipPos(CodeMirror.Pos(end))};
+});
+
+});

+ 57 - 0
public/theme/material/editor/lib/codemirror/addon/fold/comment-fold.js

@@ -0,0 +1,57 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerGlobalHelper("fold", "comment", function(mode) {
+  return mode.blockCommentStart && mode.blockCommentEnd;
+}, function(cm, start) {
+  var mode = cm.getModeAt(start), startToken = mode.blockCommentStart, endToken = mode.blockCommentEnd;
+  if (!startToken || !endToken) return;
+  var line = start.line, lineText = cm.getLine(line);
+
+  var startCh;
+  for (var at = start.ch, pass = 0;;) {
+    var found = at <= 0 ? -1 : lineText.lastIndexOf(startToken, at - 1);
+    if (found == -1) {
+      if (pass == 1) return;
+      pass = 1;
+      at = lineText.length;
+      continue;
+    }
+    if (pass == 1 && found < start.ch) return;
+    if (/comment/.test(cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1)))) {
+      startCh = found + startToken.length;
+      break;
+    }
+    at = found - 1;
+  }
+
+  var depth = 1, lastLine = cm.lastLine(), end, endCh;
+  outer: for (var i = line; i <= lastLine; ++i) {
+    var text = cm.getLine(i), pos = i == line ? startCh : 0;
+    for (;;) {
+      var nextOpen = text.indexOf(startToken, pos), nextClose = text.indexOf(endToken, pos);
+      if (nextOpen < 0) nextOpen = text.length;
+      if (nextClose < 0) nextClose = text.length;
+      pos = Math.min(nextOpen, nextClose);
+      if (pos == text.length) break;
+      if (pos == nextOpen) ++depth;
+      else if (!--depth) { end = i; endCh = pos; break outer; }
+      ++pos;
+    }
+  }
+  if (end == null || line == end && endCh == startCh) return;
+  return {from: CodeMirror.Pos(line, startCh),
+          to: CodeMirror.Pos(end, endCh)};
+});
+
+});

+ 149 - 0
public/theme/material/editor/lib/codemirror/addon/fold/foldcode.js

@@ -0,0 +1,149 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  function doFold(cm, pos, options, force) {
+    if (options && options.call) {
+      var finder = options;
+      options = null;
+    } else {
+      var finder = getOption(cm, options, "rangeFinder");
+    }
+    if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0);
+    var minSize = getOption(cm, options, "minFoldSize");
+
+    function getRange(allowFolded) {
+      var range = finder(cm, pos);
+      if (!range || range.to.line - range.from.line < minSize) return null;
+      var marks = cm.findMarksAt(range.from);
+      for (var i = 0; i < marks.length; ++i) {
+        if (marks[i].__isFold && force !== "fold") {
+          if (!allowFolded) return null;
+          range.cleared = true;
+          marks[i].clear();
+        }
+      }
+      return range;
+    }
+
+    var range = getRange(true);
+    if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) {
+      pos = CodeMirror.Pos(pos.line - 1, 0);
+      range = getRange(false);
+    }
+    if (!range || range.cleared || force === "unfold") return;
+
+    var myWidget = makeWidget(cm, options);
+    CodeMirror.on(myWidget, "mousedown", function(e) {
+      myRange.clear();
+      CodeMirror.e_preventDefault(e);
+    });
+    var myRange = cm.markText(range.from, range.to, {
+      replacedWith: myWidget,
+      clearOnEnter: true,
+      __isFold: true
+    });
+    myRange.on("clear", function(from, to) {
+      CodeMirror.signal(cm, "unfold", cm, from, to);
+    });
+    CodeMirror.signal(cm, "fold", cm, range.from, range.to);
+  }
+
+  function makeWidget(cm, options) {
+    var widget = getOption(cm, options, "widget");
+    if (typeof widget == "string") {
+      var text = document.createTextNode(widget);
+      widget = document.createElement("span");
+      widget.appendChild(text);
+      widget.className = "CodeMirror-foldmarker";
+    }
+    return widget;
+  }
+
+  // Clumsy backwards-compatible interface
+  CodeMirror.newFoldFunction = function(rangeFinder, widget) {
+    return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); };
+  };
+
+  // New-style interface
+  CodeMirror.defineExtension("foldCode", function(pos, options, force) {
+    doFold(this, pos, options, force);
+  });
+
+  CodeMirror.defineExtension("isFolded", function(pos) {
+    var marks = this.findMarksAt(pos);
+    for (var i = 0; i < marks.length; ++i)
+      if (marks[i].__isFold) return true;
+  });
+
+  CodeMirror.commands.toggleFold = function(cm) {
+    cm.foldCode(cm.getCursor());
+  };
+  CodeMirror.commands.fold = function(cm) {
+    cm.foldCode(cm.getCursor(), null, "fold");
+  };
+  CodeMirror.commands.unfold = function(cm) {
+    cm.foldCode(cm.getCursor(), null, "unfold");
+  };
+  CodeMirror.commands.foldAll = function(cm) {
+    cm.operation(function() {
+      for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
+        cm.foldCode(CodeMirror.Pos(i, 0), null, "fold");
+    });
+  };
+  CodeMirror.commands.unfoldAll = function(cm) {
+    cm.operation(function() {
+      for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
+        cm.foldCode(CodeMirror.Pos(i, 0), null, "unfold");
+    });
+  };
+
+  CodeMirror.registerHelper("fold", "combine", function() {
+    var funcs = Array.prototype.slice.call(arguments, 0);
+    return function(cm, start) {
+      for (var i = 0; i < funcs.length; ++i) {
+        var found = funcs[i](cm, start);
+        if (found) return found;
+      }
+    };
+  });
+
+  CodeMirror.registerHelper("fold", "auto", function(cm, start) {
+    var helpers = cm.getHelpers(start, "fold");
+    for (var i = 0; i < helpers.length; i++) {
+      var cur = helpers[i](cm, start);
+      if (cur) return cur;
+    }
+  });
+
+  var defaultOptions = {
+    rangeFinder: CodeMirror.fold.auto,
+    widget: "\u2194",
+    minFoldSize: 0,
+    scanUp: false
+  };
+
+  CodeMirror.defineOption("foldOptions", null);
+
+  function getOption(cm, options, name) {
+    if (options && options[name] !== undefined)
+      return options[name];
+    var editorOptions = cm.options.foldOptions;
+    if (editorOptions && editorOptions[name] !== undefined)
+      return editorOptions[name];
+    return defaultOptions[name];
+  }
+
+  CodeMirror.defineExtension("foldOption", function(options, name) {
+    return getOption(this, options, name);
+  });
+});

+ 20 - 0
public/theme/material/editor/lib/codemirror/addon/fold/foldgutter.css

@@ -0,0 +1,20 @@
+.CodeMirror-foldmarker {
+  color: blue;
+  text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px;
+  font-family: arial;
+  line-height: .3;
+  cursor: pointer;
+}
+.CodeMirror-foldgutter {
+  width: .7em;
+}
+.CodeMirror-foldgutter-open,
+.CodeMirror-foldgutter-folded {
+  cursor: pointer;
+}
+.CodeMirror-foldgutter-open:after {
+  content: "\25BE";
+}
+.CodeMirror-foldgutter-folded:after {
+  content: "\25B8";
+}

+ 144 - 0
public/theme/material/editor/lib/codemirror/addon/fold/foldgutter.js

@@ -0,0 +1,144 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"), require("./foldcode"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror", "./foldcode"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  CodeMirror.defineOption("foldGutter", false, function(cm, val, old) {
+    if (old && old != CodeMirror.Init) {
+      cm.clearGutter(cm.state.foldGutter.options.gutter);
+      cm.state.foldGutter = null;
+      cm.off("gutterClick", onGutterClick);
+      cm.off("change", onChange);
+      cm.off("viewportChange", onViewportChange);
+      cm.off("fold", onFold);
+      cm.off("unfold", onFold);
+      cm.off("swapDoc", updateInViewport);
+    }
+    if (val) {
+      cm.state.foldGutter = new State(parseOptions(val));
+      updateInViewport(cm);
+      cm.on("gutterClick", onGutterClick);
+      cm.on("change", onChange);
+      cm.on("viewportChange", onViewportChange);
+      cm.on("fold", onFold);
+      cm.on("unfold", onFold);
+      cm.on("swapDoc", updateInViewport);
+    }
+  });
+
+  var Pos = CodeMirror.Pos;
+
+  function State(options) {
+    this.options = options;
+    this.from = this.to = 0;
+  }
+
+  function parseOptions(opts) {
+    if (opts === true) opts = {};
+    if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter";
+    if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open";
+    if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded";
+    return opts;
+  }
+
+  function isFolded(cm, line) {
+    var marks = cm.findMarksAt(Pos(line));
+    for (var i = 0; i < marks.length; ++i)
+      if (marks[i].__isFold && marks[i].find().from.line == line) return true;
+  }
+
+  function marker(spec) {
+    if (typeof spec == "string") {
+      var elt = document.createElement("div");
+      elt.className = spec + " CodeMirror-guttermarker-subtle";
+      return elt;
+    } else {
+      return spec.cloneNode(true);
+    }
+  }
+
+  function updateFoldInfo(cm, from, to) {
+    var opts = cm.state.foldGutter.options, cur = from;
+    var minSize = cm.foldOption(opts, "minFoldSize");
+    var func = cm.foldOption(opts, "rangeFinder");
+    cm.eachLine(from, to, function(line) {
+      var mark = null;
+      if (isFolded(cm, cur)) {
+        mark = marker(opts.indicatorFolded);
+      } else {
+        var pos = Pos(cur, 0);
+        var range = func && func(cm, pos);
+        if (range && range.to.line - range.from.line >= minSize)
+          mark = marker(opts.indicatorOpen);
+      }
+      cm.setGutterMarker(line, opts.gutter, mark);
+      ++cur;
+    });
+  }
+
+  function updateInViewport(cm) {
+    var vp = cm.getViewport(), state = cm.state.foldGutter;
+    if (!state) return;
+    cm.operation(function() {
+      updateFoldInfo(cm, vp.from, vp.to);
+    });
+    state.from = vp.from; state.to = vp.to;
+  }
+
+  function onGutterClick(cm, line, gutter) {
+    var state = cm.state.foldGutter;
+    if (!state) return;
+    var opts = state.options;
+    if (gutter != opts.gutter) return;
+    cm.foldCode(Pos(line, 0), opts.rangeFinder);
+  }
+
+  function onChange(cm) {
+    var state = cm.state.foldGutter;
+    if (!state) return;
+    var opts = state.options;
+    state.from = state.to = 0;
+    clearTimeout(state.changeUpdate);
+    state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600);
+  }
+
+  function onViewportChange(cm) {
+    var state = cm.state.foldGutter;
+    if (!state) return;
+    var opts = state.options;
+    clearTimeout(state.changeUpdate);
+    state.changeUpdate = setTimeout(function() {
+      var vp = cm.getViewport();
+      if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {
+        updateInViewport(cm);
+      } else {
+        cm.operation(function() {
+          if (vp.from < state.from) {
+            updateFoldInfo(cm, vp.from, state.from);
+            state.from = vp.from;
+          }
+          if (vp.to > state.to) {
+            updateFoldInfo(cm, state.to, vp.to);
+            state.to = vp.to;
+          }
+        });
+      }
+    }, opts.updateViewportTimeSpan || 400);
+  }
+
+  function onFold(cm, from) {
+    var state = cm.state.foldGutter;
+    if (!state) return;
+    var line = from.line;
+    if (line >= state.from && line < state.to)
+      updateFoldInfo(cm, line, line + 1);
+  }
+});

+ 44 - 0
public/theme/material/editor/lib/codemirror/addon/fold/indent-fold.js

@@ -0,0 +1,44 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerHelper("fold", "indent", function(cm, start) {
+  var tabSize = cm.getOption("tabSize"), firstLine = cm.getLine(start.line);
+  if (!/\S/.test(firstLine)) return;
+  var getIndent = function(line) {
+    return CodeMirror.countColumn(line, null, tabSize);
+  };
+  var myIndent = getIndent(firstLine);
+  var lastLineInFold = null;
+  // Go through lines until we find a line that definitely doesn't belong in
+  // the block we're folding, or to the end.
+  for (var i = start.line + 1, end = cm.lastLine(); i <= end; ++i) {
+    var curLine = cm.getLine(i);
+    var curIndent = getIndent(curLine);
+    if (curIndent > myIndent) {
+      // Lines with a greater indent are considered part of the block.
+      lastLineInFold = i;
+    } else if (!/\S/.test(curLine)) {
+      // Empty lines might be breaks within the block we're trying to fold.
+    } else {
+      // A non-empty line at an indent equal to or less than ours marks the
+      // start of another block.
+      break;
+    }
+  }
+  if (lastLineInFold) return {
+    from: CodeMirror.Pos(start.line, firstLine.length),
+    to: CodeMirror.Pos(lastLineInFold, cm.getLine(lastLineInFold).length)
+  };
+});
+
+});

+ 49 - 0
public/theme/material/editor/lib/codemirror/addon/fold/markdown-fold.js

@@ -0,0 +1,49 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerHelper("fold", "markdown", function(cm, start) {
+  var maxDepth = 100;
+
+  function isHeader(lineNo) {
+    var tokentype = cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0));
+    return tokentype && /\bheader\b/.test(tokentype);
+  }
+
+  function headerLevel(lineNo, line, nextLine) {
+    var match = line && line.match(/^#+/);
+    if (match && isHeader(lineNo)) return match[0].length;
+    match = nextLine && nextLine.match(/^[=\-]+\s*$/);
+    if (match && isHeader(lineNo + 1)) return nextLine[0] == "=" ? 1 : 2;
+    return maxDepth;
+  }
+
+  var firstLine = cm.getLine(start.line), nextLine = cm.getLine(start.line + 1);
+  var level = headerLevel(start.line, firstLine, nextLine);
+  if (level === maxDepth) return undefined;
+
+  var lastLineNo = cm.lastLine();
+  var end = start.line, nextNextLine = cm.getLine(end + 2);
+  while (end < lastLineNo) {
+    if (headerLevel(end + 1, nextLine, nextNextLine) <= level) break;
+    ++end;
+    nextLine = nextNextLine;
+    nextNextLine = cm.getLine(end + 2);
+  }
+
+  return {
+    from: CodeMirror.Pos(start.line, firstLine.length),
+    to: CodeMirror.Pos(end, cm.getLine(end).length)
+  };
+});
+
+});

+ 182 - 0
public/theme/material/editor/lib/codemirror/addon/fold/xml-fold.js

@@ -0,0 +1,182 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  var Pos = CodeMirror.Pos;
+  function cmp(a, b) { return a.line - b.line || a.ch - b.ch; }
+
+  var nameStartChar = "A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD";
+  var nameChar = nameStartChar + "\-\:\.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040";
+  var xmlTagStart = new RegExp("<(/?)([" + nameStartChar + "][" + nameChar + "]*)", "g");
+
+  function Iter(cm, line, ch, range) {
+    this.line = line; this.ch = ch;
+    this.cm = cm; this.text = cm.getLine(line);
+    this.min = range ? range.from : cm.firstLine();
+    this.max = range ? range.to - 1 : cm.lastLine();
+  }
+
+  function tagAt(iter, ch) {
+    var type = iter.cm.getTokenTypeAt(Pos(iter.line, ch));
+    return type && /\btag\b/.test(type);
+  }
+
+  function nextLine(iter) {
+    if (iter.line >= iter.max) return;
+    iter.ch = 0;
+    iter.text = iter.cm.getLine(++iter.line);
+    return true;
+  }
+  function prevLine(iter) {
+    if (iter.line <= iter.min) return;
+    iter.text = iter.cm.getLine(--iter.line);
+    iter.ch = iter.text.length;
+    return true;
+  }
+
+  function toTagEnd(iter) {
+    for (;;) {
+      var gt = iter.text.indexOf(">", iter.ch);
+      if (gt == -1) { if (nextLine(iter)) continue; else return; }
+      if (!tagAt(iter, gt + 1)) { iter.ch = gt + 1; continue; }
+      var lastSlash = iter.text.lastIndexOf("/", gt);
+      var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt));
+      iter.ch = gt + 1;
+      return selfClose ? "selfClose" : "regular";
+    }
+  }
+  function toTagStart(iter) {
+    for (;;) {
+      var lt = iter.ch ? iter.text.lastIndexOf("<", iter.ch - 1) : -1;
+      if (lt == -1) { if (prevLine(iter)) continue; else return; }
+      if (!tagAt(iter, lt + 1)) { iter.ch = lt; continue; }
+      xmlTagStart.lastIndex = lt;
+      iter.ch = lt;
+      var match = xmlTagStart.exec(iter.text);
+      if (match && match.index == lt) return match;
+    }
+  }
+
+  function toNextTag(iter) {
+    for (;;) {
+      xmlTagStart.lastIndex = iter.ch;
+      var found = xmlTagStart.exec(iter.text);
+      if (!found) { if (nextLine(iter)) continue; else return; }
+      if (!tagAt(iter, found.index + 1)) { iter.ch = found.index + 1; continue; }
+      iter.ch = found.index + found[0].length;
+      return found;
+    }
+  }
+  function toPrevTag(iter) {
+    for (;;) {
+      var gt = iter.ch ? iter.text.lastIndexOf(">", iter.ch - 1) : -1;
+      if (gt == -1) { if (prevLine(iter)) continue; else return; }
+      if (!tagAt(iter, gt + 1)) { iter.ch = gt; continue; }
+      var lastSlash = iter.text.lastIndexOf("/", gt);
+      var selfClose = lastSlash > -1 && !/\S/.test(iter.text.slice(lastSlash + 1, gt));
+      iter.ch = gt + 1;
+      return selfClose ? "selfClose" : "regular";
+    }
+  }
+
+  function findMatchingClose(iter, tag) {
+    var stack = [];
+    for (;;) {
+      var next = toNextTag(iter), end, startLine = iter.line, startCh = iter.ch - (next ? next[0].length : 0);
+      if (!next || !(end = toTagEnd(iter))) return;
+      if (end == "selfClose") continue;
+      if (next[1]) { // closing tag
+        for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == next[2]) {
+          stack.length = i;
+          break;
+        }
+        if (i < 0 && (!tag || tag == next[2])) return {
+          tag: next[2],
+          from: Pos(startLine, startCh),
+          to: Pos(iter.line, iter.ch)
+        };
+      } else { // opening tag
+        stack.push(next[2]);
+      }
+    }
+  }
+  function findMatchingOpen(iter, tag) {
+    var stack = [];
+    for (;;) {
+      var prev = toPrevTag(iter);
+      if (!prev) return;
+      if (prev == "selfClose") { toTagStart(iter); continue; }
+      var endLine = iter.line, endCh = iter.ch;
+      var start = toTagStart(iter);
+      if (!start) return;
+      if (start[1]) { // closing tag
+        stack.push(start[2]);
+      } else { // opening tag
+        for (var i = stack.length - 1; i >= 0; --i) if (stack[i] == start[2]) {
+          stack.length = i;
+          break;
+        }
+        if (i < 0 && (!tag || tag == start[2])) return {
+          tag: start[2],
+          from: Pos(iter.line, iter.ch),
+          to: Pos(endLine, endCh)
+        };
+      }
+    }
+  }
+
+  CodeMirror.registerHelper("fold", "xml", function(cm, start) {
+    var iter = new Iter(cm, start.line, 0);
+    for (;;) {
+      var openTag = toNextTag(iter), end;
+      if (!openTag || iter.line != start.line || !(end = toTagEnd(iter))) return;
+      if (!openTag[1] && end != "selfClose") {
+        var start = Pos(iter.line, iter.ch);
+        var close = findMatchingClose(iter, openTag[2]);
+        return close && {from: start, to: close.from};
+      }
+    }
+  });
+  CodeMirror.findMatchingTag = function(cm, pos, range) {
+    var iter = new Iter(cm, pos.line, pos.ch, range);
+    if (iter.text.indexOf(">") == -1 && iter.text.indexOf("<") == -1) return;
+    var end = toTagEnd(iter), to = end && Pos(iter.line, iter.ch);
+    var start = end && toTagStart(iter);
+    if (!end || !start || cmp(iter, pos) > 0) return;
+    var here = {from: Pos(iter.line, iter.ch), to: to, tag: start[2]};
+    if (end == "selfClose") return {open: here, close: null, at: "open"};
+
+    if (start[1]) { // closing tag
+      return {open: findMatchingOpen(iter, start[2]), close: here, at: "close"};
+    } else { // opening tag
+      iter = new Iter(cm, to.line, to.ch, range);
+      return {open: here, close: findMatchingClose(iter, start[2]), at: "open"};
+    }
+  };
+
+  CodeMirror.findEnclosingTag = function(cm, pos, range) {
+    var iter = new Iter(cm, pos.line, pos.ch, range);
+    for (;;) {
+      var open = findMatchingOpen(iter);
+      if (!open) break;
+      var forward = new Iter(cm, pos.line, pos.ch, range);
+      var close = findMatchingClose(forward, open.tag);
+      if (close) return {open: open, close: close};
+    }
+  };
+
+  // Used by addon/edit/closetag.js
+  CodeMirror.scanForClosingTag = function(cm, pos, name, end) {
+    var iter = new Iter(cm, pos.line, pos.ch, end ? {from: 0, to: end} : null);
+    return findMatchingClose(iter, name);
+  };
+});

+ 41 - 0
public/theme/material/editor/lib/codemirror/addon/hint/anyword-hint.js

@@ -0,0 +1,41 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  var WORD = /[\w$]+/, RANGE = 500;
+
+  CodeMirror.registerHelper("hint", "anyword", function(editor, options) {
+    var word = options && options.word || WORD;
+    var range = options && options.range || RANGE;
+    var cur = editor.getCursor(), curLine = editor.getLine(cur.line);
+    var end = cur.ch, start = end;
+    while (start && word.test(curLine.charAt(start - 1))) --start;
+    var curWord = start != end && curLine.slice(start, end);
+
+    var list = [], seen = {};
+    var re = new RegExp(word.source, "g");
+    for (var dir = -1; dir <= 1; dir += 2) {
+      var line = cur.line, endLine = Math.min(Math.max(line + dir * range, editor.firstLine()), editor.lastLine()) + dir;
+      for (; line != endLine; line += dir) {
+        var text = editor.getLine(line), m;
+        while (m = re.exec(text)) {
+          if (line == cur.line && m[0] === curWord) continue;
+          if ((!curWord || m[0].lastIndexOf(curWord, 0) == 0) && !Object.prototype.hasOwnProperty.call(seen, m[0])) {
+            seen[m[0]] = true;
+            list.push(m[0]);
+          }
+        }
+      }
+    }
+    return {list: list, from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end)};
+  });
+});

+ 56 - 0
public/theme/material/editor/lib/codemirror/addon/hint/css-hint.js

@@ -0,0 +1,56 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"), require("../../mode/css/css"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror", "../../mode/css/css"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  var pseudoClasses = {link: 1, visited: 1, active: 1, hover: 1, focus: 1,
+                       "first-letter": 1, "first-line": 1, "first-child": 1,
+                       before: 1, after: 1, lang: 1};
+
+  CodeMirror.registerHelper("hint", "css", function(cm) {
+    var cur = cm.getCursor(), token = cm.getTokenAt(cur);
+    var inner = CodeMirror.innerMode(cm.getMode(), token.state);
+    if (inner.mode.name != "css") return;
+
+    var start = token.start, end = cur.ch, word = token.string.slice(0, end - start);
+    if (/[^\w$_-]/.test(word)) {
+      word = ""; start = end = cur.ch;
+    }
+
+    var spec = CodeMirror.resolveMode("text/css");
+
+    var result = [];
+    function add(keywords) {
+      for (var name in keywords)
+        if (!word || name.lastIndexOf(word, 0) == 0)
+          result.push(name);
+    }
+
+    var st = inner.state.state;
+    if (st == "pseudo" || token.type == "variable-3") {
+      add(pseudoClasses);
+    } else if (st == "block" || st == "maybeprop") {
+      add(spec.propertyKeywords);
+    } else if (st == "prop" || st == "parens" || st == "at" || st == "params") {
+      add(spec.valueKeywords);
+      add(spec.colorKeywords);
+    } else if (st == "media" || st == "media_parens") {
+      add(spec.mediaTypes);
+      add(spec.mediaFeatures);
+    }
+
+    if (result.length) return {
+      list: result,
+      from: CodeMirror.Pos(cur.line, start),
+      to: CodeMirror.Pos(cur.line, end)
+    };
+  });
+});

+ 348 - 0
public/theme/material/editor/lib/codemirror/addon/hint/html-hint.js

@@ -0,0 +1,348 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"), require("./xml-hint"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror", "./xml-hint"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  var langs = "ab aa af ak sq am ar an hy as av ae ay az bm ba eu be bn bh bi bs br bg my ca ch ce ny zh cv kw co cr hr cs da dv nl dz en eo et ee fo fj fi fr ff gl ka de el gn gu ht ha he hz hi ho hu ia id ie ga ig ik io is it iu ja jv kl kn kr ks kk km ki rw ky kv kg ko ku kj la lb lg li ln lo lt lu lv gv mk mg ms ml mt mi mr mh mn na nv nb nd ne ng nn no ii nr oc oj cu om or os pa pi fa pl ps pt qu rm rn ro ru sa sc sd se sm sg sr gd sn si sk sl so st es su sw ss sv ta te tg th ti bo tk tl tn to tr ts tt tw ty ug uk ur uz ve vi vo wa cy wo fy xh yi yo za zu".split(" ");
+  var targets = ["_blank", "_self", "_top", "_parent"];
+  var charsets = ["ascii", "utf-8", "utf-16", "latin1", "latin1"];
+  var methods = ["get", "post", "put", "delete"];
+  var encs = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"];
+  var media = ["all", "screen", "print", "embossed", "braille", "handheld", "print", "projection", "screen", "tty", "tv", "speech",
+               "3d-glasses", "resolution [>][<][=] [X]", "device-aspect-ratio: X/Y", "orientation:portrait",
+               "orientation:landscape", "device-height: [X]", "device-width: [X]"];
+  var s = { attrs: {} }; // Simple tag, reused for a whole lot of tags
+
+  var data = {
+    a: {
+      attrs: {
+        href: null, ping: null, type: null,
+        media: media,
+        target: targets,
+        hreflang: langs
+      }
+    },
+    abbr: s,
+    acronym: s,
+    address: s,
+    applet: s,
+    area: {
+      attrs: {
+        alt: null, coords: null, href: null, target: null, ping: null,
+        media: media, hreflang: langs, type: null,
+        shape: ["default", "rect", "circle", "poly"]
+      }
+    },
+    article: s,
+    aside: s,
+    audio: {
+      attrs: {
+        src: null, mediagroup: null,
+        crossorigin: ["anonymous", "use-credentials"],
+        preload: ["none", "metadata", "auto"],
+        autoplay: ["", "autoplay"],
+        loop: ["", "loop"],
+        controls: ["", "controls"]
+      }
+    },
+    b: s,
+    base: { attrs: { href: null, target: targets } },
+    basefont: s,
+    bdi: s,
+    bdo: s,
+    big: s,
+    blockquote: { attrs: { cite: null } },
+    body: s,
+    br: s,
+    button: {
+      attrs: {
+        form: null, formaction: null, name: null, value: null,
+        autofocus: ["", "autofocus"],
+        disabled: ["", "autofocus"],
+        formenctype: encs,
+        formmethod: methods,
+        formnovalidate: ["", "novalidate"],
+        formtarget: targets,
+        type: ["submit", "reset", "button"]
+      }
+    },
+    canvas: { attrs: { width: null, height: null } },
+    caption: s,
+    center: s,
+    cite: s,
+    code: s,
+    col: { attrs: { span: null } },
+    colgroup: { attrs: { span: null } },
+    command: {
+      attrs: {
+        type: ["command", "checkbox", "radio"],
+        label: null, icon: null, radiogroup: null, command: null, title: null,
+        disabled: ["", "disabled"],
+        checked: ["", "checked"]
+      }
+    },
+    data: { attrs: { value: null } },
+    datagrid: { attrs: { disabled: ["", "disabled"], multiple: ["", "multiple"] } },
+    datalist: { attrs: { data: null } },
+    dd: s,
+    del: { attrs: { cite: null, datetime: null } },
+    details: { attrs: { open: ["", "open"] } },
+    dfn: s,
+    dir: s,
+    div: s,
+    dl: s,
+    dt: s,
+    em: s,
+    embed: { attrs: { src: null, type: null, width: null, height: null } },
+    eventsource: { attrs: { src: null } },
+    fieldset: { attrs: { disabled: ["", "disabled"], form: null, name: null } },
+    figcaption: s,
+    figure: s,
+    font: s,
+    footer: s,
+    form: {
+      attrs: {
+        action: null, name: null,
+        "accept-charset": charsets,
+        autocomplete: ["on", "off"],
+        enctype: encs,
+        method: methods,
+        novalidate: ["", "novalidate"],
+        target: targets
+      }
+    },
+    frame: s,
+    frameset: s,
+    h1: s, h2: s, h3: s, h4: s, h5: s, h6: s,
+    head: {
+      attrs: {},
+      children: ["title", "base", "link", "style", "meta", "script", "noscript", "command"]
+    },
+    header: s,
+    hgroup: s,
+    hr: s,
+    html: {
+      attrs: { manifest: null },
+      children: ["head", "body"]
+    },
+    i: s,
+    iframe: {
+      attrs: {
+        src: null, srcdoc: null, name: null, width: null, height: null,
+        sandbox: ["allow-top-navigation", "allow-same-origin", "allow-forms", "allow-scripts"],
+        seamless: ["", "seamless"]
+      }
+    },
+    img: {
+      attrs: {
+        alt: null, src: null, ismap: null, usemap: null, width: null, height: null,
+        crossorigin: ["anonymous", "use-credentials"]
+      }
+    },
+    input: {
+      attrs: {
+        alt: null, dirname: null, form: null, formaction: null,
+        height: null, list: null, max: null, maxlength: null, min: null,
+        name: null, pattern: null, placeholder: null, size: null, src: null,
+        step: null, value: null, width: null,
+        accept: ["audio/*", "video/*", "image/*"],
+        autocomplete: ["on", "off"],
+        autofocus: ["", "autofocus"],
+        checked: ["", "checked"],
+        disabled: ["", "disabled"],
+        formenctype: encs,
+        formmethod: methods,
+        formnovalidate: ["", "novalidate"],
+        formtarget: targets,
+        multiple: ["", "multiple"],
+        readonly: ["", "readonly"],
+        required: ["", "required"],
+        type: ["hidden", "text", "search", "tel", "url", "email", "password", "datetime", "date", "month",
+               "week", "time", "datetime-local", "number", "range", "color", "checkbox", "radio",
+               "file", "submit", "image", "reset", "button"]
+      }
+    },
+    ins: { attrs: { cite: null, datetime: null } },
+    kbd: s,
+    keygen: {
+      attrs: {
+        challenge: null, form: null, name: null,
+        autofocus: ["", "autofocus"],
+        disabled: ["", "disabled"],
+        keytype: ["RSA"]
+      }
+    },
+    label: { attrs: { "for": null, form: null } },
+    legend: s,
+    li: { attrs: { value: null } },
+    link: {
+      attrs: {
+        href: null, type: null,
+        hreflang: langs,
+        media: media,
+        sizes: ["all", "16x16", "16x16 32x32", "16x16 32x32 64x64"]
+      }
+    },
+    map: { attrs: { name: null } },
+    mark: s,
+    menu: { attrs: { label: null, type: ["list", "context", "toolbar"] } },
+    meta: {
+      attrs: {
+        content: null,
+        charset: charsets,
+        name: ["viewport", "application-name", "author", "description", "generator", "keywords"],
+        "http-equiv": ["content-language", "content-type", "default-style", "refresh"]
+      }
+    },
+    meter: { attrs: { value: null, min: null, low: null, high: null, max: null, optimum: null } },
+    nav: s,
+    noframes: s,
+    noscript: s,
+    object: {
+      attrs: {
+        data: null, type: null, name: null, usemap: null, form: null, width: null, height: null,
+        typemustmatch: ["", "typemustmatch"]
+      }
+    },
+    ol: { attrs: { reversed: ["", "reversed"], start: null, type: ["1", "a", "A", "i", "I"] } },
+    optgroup: { attrs: { disabled: ["", "disabled"], label: null } },
+    option: { attrs: { disabled: ["", "disabled"], label: null, selected: ["", "selected"], value: null } },
+    output: { attrs: { "for": null, form: null, name: null } },
+    p: s,
+    param: { attrs: { name: null, value: null } },
+    pre: s,
+    progress: { attrs: { value: null, max: null } },
+    q: { attrs: { cite: null } },
+    rp: s,
+    rt: s,
+    ruby: s,
+    s: s,
+    samp: s,
+    script: {
+      attrs: {
+        type: ["text/javascript"],
+        src: null,
+        async: ["", "async"],
+        defer: ["", "defer"],
+        charset: charsets
+      }
+    },
+    section: s,
+    select: {
+      attrs: {
+        form: null, name: null, size: null,
+        autofocus: ["", "autofocus"],
+        disabled: ["", "disabled"],
+        multiple: ["", "multiple"]
+      }
+    },
+    small: s,
+    source: { attrs: { src: null, type: null, media: null } },
+    span: s,
+    strike: s,
+    strong: s,
+    style: {
+      attrs: {
+        type: ["text/css"],
+        media: media,
+        scoped: null
+      }
+    },
+    sub: s,
+    summary: s,
+    sup: s,
+    table: s,
+    tbody: s,
+    td: { attrs: { colspan: null, rowspan: null, headers: null } },
+    textarea: {
+      attrs: {
+        dirname: null, form: null, maxlength: null, name: null, placeholder: null,
+        rows: null, cols: null,
+        autofocus: ["", "autofocus"],
+        disabled: ["", "disabled"],
+        readonly: ["", "readonly"],
+        required: ["", "required"],
+        wrap: ["soft", "hard"]
+      }
+    },
+    tfoot: s,
+    th: { attrs: { colspan: null, rowspan: null, headers: null, scope: ["row", "col", "rowgroup", "colgroup"] } },
+    thead: s,
+    time: { attrs: { datetime: null } },
+    title: s,
+    tr: s,
+    track: {
+      attrs: {
+        src: null, label: null, "default": null,
+        kind: ["subtitles", "captions", "descriptions", "chapters", "metadata"],
+        srclang: langs
+      }
+    },
+    tt: s,
+    u: s,
+    ul: s,
+    "var": s,
+    video: {
+      attrs: {
+        src: null, poster: null, width: null, height: null,
+        crossorigin: ["anonymous", "use-credentials"],
+        preload: ["auto", "metadata", "none"],
+        autoplay: ["", "autoplay"],
+        mediagroup: ["movie"],
+        muted: ["", "muted"],
+        controls: ["", "controls"]
+      }
+    },
+    wbr: s
+  };
+
+  var globalAttrs = {
+    accesskey: ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
+    "class": null,
+    contenteditable: ["true", "false"],
+    contextmenu: null,
+    dir: ["ltr", "rtl", "auto"],
+    draggable: ["true", "false", "auto"],
+    dropzone: ["copy", "move", "link", "string:", "file:"],
+    hidden: ["hidden"],
+    id: null,
+    inert: ["inert"],
+    itemid: null,
+    itemprop: null,
+    itemref: null,
+    itemscope: ["itemscope"],
+    itemtype: null,
+    lang: ["en", "es"],
+    spellcheck: ["true", "false"],
+    style: null,
+    tabindex: ["1", "2", "3", "4", "5", "6", "7", "8", "9"],
+    title: null,
+    translate: ["yes", "no"],
+    onclick: null,
+    rel: ["stylesheet", "alternate", "author", "bookmark", "help", "license", "next", "nofollow", "noreferrer", "prefetch", "prev", "search", "tag"]
+  };
+  function populate(obj) {
+    for (var attr in globalAttrs) if (globalAttrs.hasOwnProperty(attr))
+      obj.attrs[attr] = globalAttrs[attr];
+  }
+
+  populate(s);
+  for (var tag in data) if (data.hasOwnProperty(tag) && data[tag] != s)
+    populate(data[tag]);
+
+  CodeMirror.htmlSchema = data;
+  function htmlHint(cm, options) {
+    var local = {schemaInfo: data};
+    if (options) for (var opt in options) local[opt] = options[opt];
+    return CodeMirror.hint.xml(cm, local);
+  }
+  CodeMirror.registerHelper("hint", "html", htmlHint);
+});

+ 146 - 0
public/theme/material/editor/lib/codemirror/addon/hint/javascript-hint.js

@@ -0,0 +1,146 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  var Pos = CodeMirror.Pos;
+
+  function forEach(arr, f) {
+    for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]);
+  }
+
+  function arrayContains(arr, item) {
+    if (!Array.prototype.indexOf) {
+      var i = arr.length;
+      while (i--) {
+        if (arr[i] === item) {
+          return true;
+        }
+      }
+      return false;
+    }
+    return arr.indexOf(item) != -1;
+  }
+
+  function scriptHint(editor, keywords, getToken, options) {
+    // Find the token at the cursor
+    var cur = editor.getCursor(), token = getToken(editor, cur);
+    if (/\b(?:string|comment)\b/.test(token.type)) return;
+    token.state = CodeMirror.innerMode(editor.getMode(), token.state).state;
+
+    // If it's not a 'word-style' token, ignore the token.
+    if (!/^[\w$_]*$/.test(token.string)) {
+      token = {start: cur.ch, end: cur.ch, string: "", state: token.state,
+               type: token.string == "." ? "property" : null};
+    } else if (token.end > cur.ch) {
+      token.end = cur.ch;
+      token.string = token.string.slice(0, cur.ch - token.start);
+    }
+
+    var tprop = token;
+    // If it is a property, find out what it is a property of.
+    while (tprop.type == "property") {
+      tprop = getToken(editor, Pos(cur.line, tprop.start));
+      if (tprop.string != ".") return;
+      tprop = getToken(editor, Pos(cur.line, tprop.start));
+      if (!context) var context = [];
+      context.push(tprop);
+    }
+    return {list: getCompletions(token, context, keywords, options),
+            from: Pos(cur.line, token.start),
+            to: Pos(cur.line, token.end)};
+  }
+
+  function javascriptHint(editor, options) {
+    return scriptHint(editor, javascriptKeywords,
+                      function (e, cur) {return e.getTokenAt(cur);},
+                      options);
+  };
+  CodeMirror.registerHelper("hint", "javascript", javascriptHint);
+
+  function getCoffeeScriptToken(editor, cur) {
+  // This getToken, it is for coffeescript, imitates the behavior of
+  // getTokenAt method in javascript.js, that is, returning "property"
+  // type and treat "." as indepenent token.
+    var token = editor.getTokenAt(cur);
+    if (cur.ch == token.start + 1 && token.string.charAt(0) == '.') {
+      token.end = token.start;
+      token.string = '.';
+      token.type = "property";
+    }
+    else if (/^\.[\w$_]*$/.test(token.string)) {
+      token.type = "property";
+      token.start++;
+      token.string = token.string.replace(/\./, '');
+    }
+    return token;
+  }
+
+  function coffeescriptHint(editor, options) {
+    return scriptHint(editor, coffeescriptKeywords, getCoffeeScriptToken, options);
+  }
+  CodeMirror.registerHelper("hint", "coffeescript", coffeescriptHint);
+
+  var stringProps = ("charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight " +
+                     "toUpperCase toLowerCase split concat match replace search").split(" ");
+  var arrayProps = ("length concat join splice push pop shift unshift slice reverse sort indexOf " +
+                    "lastIndexOf every some filter forEach map reduce reduceRight ").split(" ");
+  var funcProps = "prototype apply call bind".split(" ");
+  var javascriptKeywords = ("break case catch continue debugger default delete do else false finally for function " +
+                  "if in instanceof new null return switch throw true try typeof var void while with").split(" ");
+  var coffeescriptKeywords = ("and break catch class continue delete do else extends false finally for " +
+                  "if in instanceof isnt new no not null of off on or return switch then throw true try typeof until void while with yes").split(" ");
+
+  function getCompletions(token, context, keywords, options) {
+    var found = [], start = token.string, global = options && options.globalScope || window;
+    function maybeAdd(str) {
+      if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) found.push(str);
+    }
+    function gatherCompletions(obj) {
+      if (typeof obj == "string") forEach(stringProps, maybeAdd);
+      else if (obj instanceof Array) forEach(arrayProps, maybeAdd);
+      else if (obj instanceof Function) forEach(funcProps, maybeAdd);
+      for (var name in obj) maybeAdd(name);
+    }
+
+    if (context && context.length) {
+      // If this is a property, see if it belongs to some object we can
+      // find in the current environment.
+      var obj = context.pop(), base;
+      if (obj.type && obj.type.indexOf("variable") === 0) {
+        if (options && options.additionalContext)
+          base = options.additionalContext[obj.string];
+        if (!options || options.useGlobalScope !== false)
+          base = base || global[obj.string];
+      } else if (obj.type == "string") {
+        base = "";
+      } else if (obj.type == "atom") {
+        base = 1;
+      } else if (obj.type == "function") {
+        if (global.jQuery != null && (obj.string == '$' || obj.string == 'jQuery') &&
+            (typeof global.jQuery == 'function'))
+          base = global.jQuery();
+        else if (global._ != null && (obj.string == '_') && (typeof global._ == 'function'))
+          base = global._();
+      }
+      while (base != null && context.length)
+        base = base[context.pop().string];
+      if (base != null) gatherCompletions(base);
+    } else {
+      // If not, just look in the global object and any local scope
+      // (reading into JS mode internals to get at the local and global variables)
+      for (var v = token.state.localVars; v; v = v.next) maybeAdd(v.name);
+      for (var v = token.state.globalVars; v; v = v.next) maybeAdd(v.name);
+      if (!options || options.useGlobalScope !== false)
+        gatherCompletions(global);
+      forEach(keywords, maybeAdd);
+    }
+    return found;
+  }
+});

+ 38 - 0
public/theme/material/editor/lib/codemirror/addon/hint/show-hint.css

@@ -0,0 +1,38 @@
+.CodeMirror-hints {
+  position: absolute;
+  z-index: 10;
+  overflow: hidden;
+  list-style: none;
+
+  margin: 0;
+  padding: 2px;
+
+  -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
+  -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2);
+  box-shadow: 2px 3px 5px rgba(0,0,0,.2);
+  border-radius: 3px;
+  border: 1px solid silver;
+
+  background: white;
+  font-size: 90%;
+  font-family: monospace;
+
+  max-height: 20em;
+  overflow-y: auto;
+}
+
+.CodeMirror-hint {
+  margin: 0;
+  padding: 0 4px;
+  border-radius: 2px;
+  max-width: 19em;
+  overflow: hidden;
+  white-space: pre;
+  color: black;
+  cursor: pointer;
+}
+
+li.CodeMirror-hint-active {
+  background: #08f;
+  color: white;
+}

+ 394 - 0
public/theme/material/editor/lib/codemirror/addon/hint/show-hint.js

@@ -0,0 +1,394 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  var HINT_ELEMENT_CLASS        = "CodeMirror-hint";
+  var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active";
+
+  // This is the old interface, kept around for now to stay
+  // backwards-compatible.
+  CodeMirror.showHint = function(cm, getHints, options) {
+    if (!getHints) return cm.showHint(options);
+    if (options && options.async) getHints.async = true;
+    var newOpts = {hint: getHints};
+    if (options) for (var prop in options) newOpts[prop] = options[prop];
+    return cm.showHint(newOpts);
+  };
+
+  var asyncRunID = 0;
+  function retrieveHints(getter, cm, options, then) {
+    if (getter.async) {
+      var id = ++asyncRunID;
+      getter(cm, function(hints) {
+        if (asyncRunID == id) then(hints);
+      }, options);
+    } else {
+      then(getter(cm, options));
+    }
+  }
+
+  CodeMirror.defineExtension("showHint", function(options) {
+    // We want a single cursor position.
+    if (this.listSelections().length > 1 || this.somethingSelected()) return;
+
+    if (this.state.completionActive) this.state.completionActive.close();
+    var completion = this.state.completionActive = new Completion(this, options);
+    var getHints = completion.options.hint;
+    if (!getHints) return;
+
+    CodeMirror.signal(this, "startCompletion", this);
+    return retrieveHints(getHints, this, completion.options, function(hints) { completion.showHints(hints); });
+  });
+
+  function Completion(cm, options) {
+    this.cm = cm;
+    this.options = this.buildOptions(options);
+    this.widget = this.onClose = null;
+  }
+
+  Completion.prototype = {
+    close: function() {
+      if (!this.active()) return;
+      this.cm.state.completionActive = null;
+
+      if (this.widget) this.widget.close();
+      if (this.onClose) this.onClose();
+      CodeMirror.signal(this.cm, "endCompletion", this.cm);
+    },
+
+    active: function() {
+      return this.cm.state.completionActive == this;
+    },
+
+    pick: function(data, i) {
+      var completion = data.list[i];
+      if (completion.hint) completion.hint(this.cm, data, completion);
+      else this.cm.replaceRange(getText(completion), completion.from || data.from,
+                                completion.to || data.to, "complete");
+      CodeMirror.signal(data, "pick", completion);
+      this.close();
+    },
+
+    showHints: function(data) {
+      if (!data || !data.list.length || !this.active()) return this.close();
+
+      if (this.options.completeSingle && data.list.length == 1)
+        this.pick(data, 0);
+      else
+        this.showWidget(data);
+    },
+
+    showWidget: function(data) {
+      this.widget = new Widget(this, data);
+      CodeMirror.signal(data, "shown");
+
+      var debounce = 0, completion = this, finished;
+      var closeOn = this.options.closeCharacters;
+      var startPos = this.cm.getCursor(), startLen = this.cm.getLine(startPos.line).length;
+
+      var requestAnimationFrame = window.requestAnimationFrame || function(fn) {
+        return setTimeout(fn, 1000/60);
+      };
+      var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout;
+
+      function done() {
+        if (finished) return;
+        finished = true;
+        completion.close();
+        completion.cm.off("cursorActivity", activity);
+        if (data) CodeMirror.signal(data, "close");
+      }
+
+      function update() {
+        if (finished) return;
+        CodeMirror.signal(data, "update");
+        retrieveHints(completion.options.hint, completion.cm, completion.options, finishUpdate);
+      }
+      function finishUpdate(data_) {
+        data = data_;
+        if (finished) return;
+        if (!data || !data.list.length) return done();
+        if (completion.widget) completion.widget.close();
+        completion.widget = new Widget(completion, data);
+      }
+
+      function clearDebounce() {
+        if (debounce) {
+          cancelAnimationFrame(debounce);
+          debounce = 0;
+        }
+      }
+
+      function activity() {
+        clearDebounce();
+        var pos = completion.cm.getCursor(), line = completion.cm.getLine(pos.line);
+        if (pos.line != startPos.line || line.length - pos.ch != startLen - startPos.ch ||
+            pos.ch < startPos.ch || completion.cm.somethingSelected() ||
+            (pos.ch && closeOn.test(line.charAt(pos.ch - 1)))) {
+          completion.close();
+        } else {
+          debounce = requestAnimationFrame(update);
+          if (completion.widget) completion.widget.close();
+        }
+      }
+      this.cm.on("cursorActivity", activity);
+      this.onClose = done;
+    },
+
+    buildOptions: function(options) {
+      var editor = this.cm.options.hintOptions;
+      var out = {};
+      for (var prop in defaultOptions) out[prop] = defaultOptions[prop];
+      if (editor) for (var prop in editor)
+        if (editor[prop] !== undefined) out[prop] = editor[prop];
+      if (options) for (var prop in options)
+        if (options[prop] !== undefined) out[prop] = options[prop];
+      return out;
+    }
+  };
+
+  function getText(completion) {
+    if (typeof completion == "string") return completion;
+    else return completion.text;
+  }
+
+  function buildKeyMap(completion, handle) {
+    var baseMap = {
+      Up: function() {handle.moveFocus(-1);},
+      Down: function() {handle.moveFocus(1);},
+      PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);},
+      PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);},
+      Home: function() {handle.setFocus(0);},
+      End: function() {handle.setFocus(handle.length - 1);},
+      Enter: handle.pick,
+      Tab: handle.pick,
+      Esc: handle.close
+    };
+    var custom = completion.options.customKeys;
+    var ourMap = custom ? {} : baseMap;
+    function addBinding(key, val) {
+      var bound;
+      if (typeof val != "string")
+        bound = function(cm) { return val(cm, handle); };
+      // This mechanism is deprecated
+      else if (baseMap.hasOwnProperty(val))
+        bound = baseMap[val];
+      else
+        bound = val;
+      ourMap[key] = bound;
+    }
+    if (custom)
+      for (var key in custom) if (custom.hasOwnProperty(key))
+        addBinding(key, custom[key]);
+    var extra = completion.options.extraKeys;
+    if (extra)
+      for (var key in extra) if (extra.hasOwnProperty(key))
+        addBinding(key, extra[key]);
+    return ourMap;
+  }
+
+  function getHintElement(hintsElement, el) {
+    while (el && el != hintsElement) {
+      if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el;
+      el = el.parentNode;
+    }
+  }
+
+  function Widget(completion, data) {
+    this.completion = completion;
+    this.data = data;
+    var widget = this, cm = completion.cm;
+
+    var hints = this.hints = document.createElement("ul");
+    hints.className = "CodeMirror-hints";
+    this.selectedHint = data.selectedHint || 0;
+
+    var completions = data.list;
+    for (var i = 0; i < completions.length; ++i) {
+      var elt = hints.appendChild(document.createElement("li")), cur = completions[i];
+      var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS);
+      if (cur.className != null) className = cur.className + " " + className;
+      elt.className = className;
+      if (cur.render) cur.render(elt, data, cur);
+      else elt.appendChild(document.createTextNode(cur.displayText || getText(cur)));
+      elt.hintId = i;
+    }
+
+    var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null);
+    var left = pos.left, top = pos.bottom, below = true;
+    hints.style.left = left + "px";
+    hints.style.top = top + "px";
+    // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor.
+    var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth);
+    var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
+    (completion.options.container || document.body).appendChild(hints);
+    var box = hints.getBoundingClientRect(), overlapY = box.bottom - winH;
+    if (overlapY > 0) {
+      var height = box.bottom - box.top, curTop = pos.top - (pos.bottom - box.top);
+      if (curTop - height > 0) { // Fits above cursor
+        hints.style.top = (top = pos.top - height) + "px";
+        below = false;
+      } else if (height > winH) {
+        hints.style.height = (winH - 5) + "px";
+        hints.style.top = (top = pos.bottom - box.top) + "px";
+        var cursor = cm.getCursor();
+        if (data.from.ch != cursor.ch) {
+          pos = cm.cursorCoords(cursor);
+          hints.style.left = (left = pos.left) + "px";
+          box = hints.getBoundingClientRect();
+        }
+      }
+    }
+    var overlapX = box.right - winW;
+    if (overlapX > 0) {
+      if (box.right - box.left > winW) {
+        hints.style.width = (winW - 5) + "px";
+        overlapX -= (box.right - box.left) - winW;
+      }
+      hints.style.left = (left = pos.left - overlapX) + "px";
+    }
+
+    cm.addKeyMap(this.keyMap = buildKeyMap(completion, {
+      moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); },
+      setFocus: function(n) { widget.changeActive(n); },
+      menuSize: function() { return widget.screenAmount(); },
+      length: completions.length,
+      close: function() { completion.close(); },
+      pick: function() { widget.pick(); },
+      data: data
+    }));
+
+    if (completion.options.closeOnUnfocus) {
+      var closingOnBlur;
+      cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); });
+      cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); });
+    }
+
+    var startScroll = cm.getScrollInfo();
+    cm.on("scroll", this.onScroll = function() {
+      var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect();
+      var newTop = top + startScroll.top - curScroll.top;
+      var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop);
+      if (!below) point += hints.offsetHeight;
+      if (point <= editor.top || point >= editor.bottom) return completion.close();
+      hints.style.top = newTop + "px";
+      hints.style.left = (left + startScroll.left - curScroll.left) + "px";
+    });
+
+    CodeMirror.on(hints, "dblclick", function(e) {
+      var t = getHintElement(hints, e.target || e.srcElement);
+      if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();}
+    });
+
+    CodeMirror.on(hints, "click", function(e) {
+      var t = getHintElement(hints, e.target || e.srcElement);
+      if (t && t.hintId != null) {
+        widget.changeActive(t.hintId);
+        if (completion.options.completeOnSingleClick) widget.pick();
+      }
+    });
+
+    CodeMirror.on(hints, "mousedown", function() {
+      setTimeout(function(){cm.focus();}, 20);
+    });
+
+    CodeMirror.signal(data, "select", completions[0], hints.firstChild);
+    return true;
+  }
+
+  Widget.prototype = {
+    close: function() {
+      if (this.completion.widget != this) return;
+      this.completion.widget = null;
+      this.hints.parentNode.removeChild(this.hints);
+      this.completion.cm.removeKeyMap(this.keyMap);
+
+      var cm = this.completion.cm;
+      if (this.completion.options.closeOnUnfocus) {
+        cm.off("blur", this.onBlur);
+        cm.off("focus", this.onFocus);
+      }
+      cm.off("scroll", this.onScroll);
+    },
+
+    pick: function() {
+      this.completion.pick(this.data, this.selectedHint);
+    },
+
+    changeActive: function(i, avoidWrap) {
+      if (i >= this.data.list.length)
+        i = avoidWrap ? this.data.list.length - 1 : 0;
+      else if (i < 0)
+        i = avoidWrap ? 0  : this.data.list.length - 1;
+      if (this.selectedHint == i) return;
+      var node = this.hints.childNodes[this.selectedHint];
+      node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, "");
+      node = this.hints.childNodes[this.selectedHint = i];
+      node.className += " " + ACTIVE_HINT_ELEMENT_CLASS;
+      if (node.offsetTop < this.hints.scrollTop)
+        this.hints.scrollTop = node.offsetTop - 3;
+      else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight)
+        this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3;
+      CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node);
+    },
+
+    screenAmount: function() {
+      return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1;
+    }
+  };
+
+  CodeMirror.registerHelper("hint", "auto", function(cm, options) {
+    var helpers = cm.getHelpers(cm.getCursor(), "hint"), words;
+    if (helpers.length) {
+      for (var i = 0; i < helpers.length; i++) {
+        var cur = helpers[i](cm, options);
+        if (cur && cur.list.length) return cur;
+      }
+    } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) {
+      if (words) return CodeMirror.hint.fromList(cm, {words: words});
+    } else if (CodeMirror.hint.anyword) {
+      return CodeMirror.hint.anyword(cm, options);
+    }
+  });
+
+  CodeMirror.registerHelper("hint", "fromList", function(cm, options) {
+    var cur = cm.getCursor(), token = cm.getTokenAt(cur);
+    var found = [];
+    for (var i = 0; i < options.words.length; i++) {
+      var word = options.words[i];
+      if (word.slice(0, token.string.length) == token.string)
+        found.push(word);
+    }
+
+    if (found.length) return {
+      list: found,
+      from: CodeMirror.Pos(cur.line, token.start),
+            to: CodeMirror.Pos(cur.line, token.end)
+    };
+  });
+
+  CodeMirror.commands.autocomplete = CodeMirror.showHint;
+
+  var defaultOptions = {
+    hint: CodeMirror.hint.auto,
+    completeSingle: true,
+    alignWithWord: true,
+    closeCharacters: /[\s()\[\]{};:>,]/,
+    closeOnUnfocus: true,
+    completeOnSingleClick: false,
+    container: null,
+    customKeys: null,
+    extraKeys: null
+  };
+
+  CodeMirror.defineOption("hintOptions", null);
+});

+ 240 - 0
public/theme/material/editor/lib/codemirror/addon/hint/sql-hint.js

@@ -0,0 +1,240 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"), require("../../mode/sql/sql"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror", "../../mode/sql/sql"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  var tables;
+  var defaultTable;
+  var keywords;
+  var CONS = {
+    QUERY_DIV: ";",
+    ALIAS_KEYWORD: "AS"
+  };
+  var Pos = CodeMirror.Pos;
+
+  function getKeywords(editor) {
+    var mode = editor.doc.modeOption;
+    if (mode === "sql") mode = "text/x-sql";
+    return CodeMirror.resolveMode(mode).keywords;
+  }
+
+  function getText(item) {
+    return typeof item == "string" ? item : item.text;
+  }
+
+  function getItem(list, item) {
+    if (!list.slice) return list[item];
+    for (var i = list.length - 1; i >= 0; i--) if (getText(list[i]) == item)
+      return list[i];
+  }
+
+  function shallowClone(object) {
+    var result = {};
+    for (var key in object) if (object.hasOwnProperty(key))
+      result[key] = object[key];
+    return result;
+  }
+
+  function match(string, word) {
+    var len = string.length;
+    var sub = getText(word).substr(0, len);
+    return string.toUpperCase() === sub.toUpperCase();
+  }
+
+  function addMatches(result, search, wordlist, formatter) {
+    for (var word in wordlist) {
+      if (!wordlist.hasOwnProperty(word)) continue;
+      if (Array.isArray(wordlist)) {
+        word = wordlist[word];
+      }
+      if (match(search, word)) {
+        result.push(formatter(word));
+      }
+    }
+  }
+
+  function cleanName(name) {
+    // Get rid name from backticks(`) and preceding dot(.)
+    if (name.charAt(0) == ".") {
+      name = name.substr(1);
+    }
+    return name.replace(/`/g, "");
+  }
+
+  function insertBackticks(name) {
+    var nameParts = getText(name).split(".");
+    for (var i = 0; i < nameParts.length; i++)
+      nameParts[i] = "`" + nameParts[i] + "`";
+    var escaped = nameParts.join(".");
+    if (typeof name == "string") return escaped;
+    name = shallowClone(name);
+    name.text = escaped;
+    return name;
+  }
+
+  function nameCompletion(cur, token, result, editor) {
+    // Try to complete table, colunm names and return start position of completion
+    var useBacktick = false;
+    var nameParts = [];
+    var start = token.start;
+    var cont = true;
+    while (cont) {
+      cont = (token.string.charAt(0) == ".");
+      useBacktick = useBacktick || (token.string.charAt(0) == "`");
+
+      start = token.start;
+      nameParts.unshift(cleanName(token.string));
+
+      token = editor.getTokenAt(Pos(cur.line, token.start));
+      if (token.string == ".") {
+        cont = true;
+        token = editor.getTokenAt(Pos(cur.line, token.start));
+      }
+    }
+
+    // Try to complete table names
+    var string = nameParts.join(".");
+    addMatches(result, string, tables, function(w) {
+      return useBacktick ? insertBackticks(w) : w;
+    });
+
+    // Try to complete columns from defaultTable
+    addMatches(result, string, defaultTable, function(w) {
+      return useBacktick ? insertBackticks(w) : w;
+    });
+
+    // Try to complete columns
+    string = nameParts.pop();
+    var table = nameParts.join(".");
+
+    // Check if table is available. If not, find table by Alias
+    if (!getItem(tables, table))
+      table = findTableByAlias(table, editor);
+
+    var columns = getItem(tables, table);
+    if (columns && Array.isArray(tables) && columns.columns)
+      columns = columns.columns;
+
+    if (columns) {
+      addMatches(result, string, columns, function(w) {
+        if (typeof w == "string") {
+          w = table + "." + w;
+        } else {
+          w = shallowClone(w);
+          w.text = table + "." + w.text;
+        }
+        return useBacktick ? insertBackticks(w) : w;
+      });
+    }
+
+    return start;
+  }
+
+  function eachWord(lineText, f) {
+    if (!lineText) return;
+    var excepted = /[,;]/g;
+    var words = lineText.split(" ");
+    for (var i = 0; i < words.length; i++) {
+      f(words[i]?words[i].replace(excepted, '') : '');
+    }
+  }
+
+  function convertCurToNumber(cur) {
+    // max characters of a line is 999,999.
+    return cur.line + cur.ch / Math.pow(10, 6);
+  }
+
+  function convertNumberToCur(num) {
+    return Pos(Math.floor(num), +num.toString().split('.').pop());
+  }
+
+  function findTableByAlias(alias, editor) {
+    var doc = editor.doc;
+    var fullQuery = doc.getValue();
+    var aliasUpperCase = alias.toUpperCase();
+    var previousWord = "";
+    var table = "";
+    var separator = [];
+    var validRange = {
+      start: Pos(0, 0),
+      end: Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).length)
+    };
+
+    //add separator
+    var indexOfSeparator = fullQuery.indexOf(CONS.QUERY_DIV);
+    while(indexOfSeparator != -1) {
+      separator.push(doc.posFromIndex(indexOfSeparator));
+      indexOfSeparator = fullQuery.indexOf(CONS.QUERY_DIV, indexOfSeparator+1);
+    }
+    separator.unshift(Pos(0, 0));
+    separator.push(Pos(editor.lastLine(), editor.getLineHandle(editor.lastLine()).text.length));
+
+    //find valid range
+    var prevItem = 0;
+    var current = convertCurToNumber(editor.getCursor());
+    for (var i=0; i< separator.length; i++) {
+      var _v = convertCurToNumber(separator[i]);
+      if (current > prevItem && current <= _v) {
+        validRange = { start: convertNumberToCur(prevItem), end: convertNumberToCur(_v) };
+        break;
+      }
+      prevItem = _v;
+    }
+
+    var query = doc.getRange(validRange.start, validRange.end, false);
+
+    for (var i = 0; i < query.length; i++) {
+      var lineText = query[i];
+      eachWord(lineText, function(word) {
+        var wordUpperCase = word.toUpperCase();
+        if (wordUpperCase === aliasUpperCase && getItem(tables, previousWord))
+          table = previousWord;
+        if (wordUpperCase !== CONS.ALIAS_KEYWORD)
+          previousWord = word;
+      });
+      if (table) break;
+    }
+    return table;
+  }
+
+  CodeMirror.registerHelper("hint", "sql", function(editor, options) {
+    tables = (options && options.tables) || {};
+    var defaultTableName = options && options.defaultTable;
+    defaultTable = (defaultTableName && getItem(tables, defaultTableName)) || [];
+    keywords = keywords || getKeywords(editor);
+
+    var cur = editor.getCursor();
+    var result = [];
+    var token = editor.getTokenAt(cur), start, end, search;
+    if (token.end > cur.ch) {
+      token.end = cur.ch;
+      token.string = token.string.slice(0, cur.ch - token.start);
+    }
+
+    if (token.string.match(/^[.`\w@]\w*$/)) {
+      search = token.string;
+      start = token.start;
+      end = token.end;
+    } else {
+      start = end = cur.ch;
+      search = "";
+    }
+    if (search.charAt(0) == "." || search.charAt(0) == "`") {
+      start = nameCompletion(cur, token, result, editor);
+    } else {
+      addMatches(result, search, tables, function(w) {return w;});
+      addMatches(result, search, defaultTable, function(w) {return w;});
+      addMatches(result, search, keywords, function(w) {return w.toUpperCase();});
+    }
+
+    return {list: result, from: Pos(cur.line, start), to: Pos(cur.line, end)};
+  });
+});

+ 110 - 0
public/theme/material/editor/lib/codemirror/addon/hint/xml-hint.js

@@ -0,0 +1,110 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  var Pos = CodeMirror.Pos;
+
+  function getHints(cm, options) {
+    var tags = options && options.schemaInfo;
+    var quote = (options && options.quoteChar) || '"';
+    if (!tags) return;
+    var cur = cm.getCursor(), token = cm.getTokenAt(cur);
+    if (token.end > cur.ch) {
+      token.end = cur.ch;
+      token.string = token.string.slice(0, cur.ch - token.start);
+    }
+    var inner = CodeMirror.innerMode(cm.getMode(), token.state);
+    if (inner.mode.name != "xml") return;
+    var result = [], replaceToken = false, prefix;
+    var tag = /\btag\b/.test(token.type) && !/>$/.test(token.string);
+    var tagName = tag && /^\w/.test(token.string), tagStart;
+
+    if (tagName) {
+      var before = cm.getLine(cur.line).slice(Math.max(0, token.start - 2), token.start);
+      var tagType = /<\/$/.test(before) ? "close" : /<$/.test(before) ? "open" : null;
+      if (tagType) tagStart = token.start - (tagType == "close" ? 2 : 1);
+    } else if (tag && token.string == "<") {
+      tagType = "open";
+    } else if (tag && token.string == "</") {
+      tagType = "close";
+    }
+
+    if (!tag && !inner.state.tagName || tagType) {
+      if (tagName)
+        prefix = token.string;
+      replaceToken = tagType;
+      var cx = inner.state.context, curTag = cx && tags[cx.tagName];
+      var childList = cx ? curTag && curTag.children : tags["!top"];
+      if (childList && tagType != "close") {
+        for (var i = 0; i < childList.length; ++i) if (!prefix || childList[i].lastIndexOf(prefix, 0) == 0)
+          result.push("<" + childList[i]);
+      } else if (tagType != "close") {
+        for (var name in tags)
+          if (tags.hasOwnProperty(name) && name != "!top" && name != "!attrs" && (!prefix || name.lastIndexOf(prefix, 0) == 0))
+            result.push("<" + name);
+      }
+      if (cx && (!prefix || tagType == "close" && cx.tagName.lastIndexOf(prefix, 0) == 0))
+        result.push("</" + cx.tagName + ">");
+    } else {
+      // Attribute completion
+      var curTag = tags[inner.state.tagName], attrs = curTag && curTag.attrs;
+      var globalAttrs = tags["!attrs"];
+      if (!attrs && !globalAttrs) return;
+      if (!attrs) {
+        attrs = globalAttrs;
+      } else if (globalAttrs) { // Combine tag-local and global attributes
+        var set = {};
+        for (var nm in globalAttrs) if (globalAttrs.hasOwnProperty(nm)) set[nm] = globalAttrs[nm];
+        for (var nm in attrs) if (attrs.hasOwnProperty(nm)) set[nm] = attrs[nm];
+        attrs = set;
+      }
+      if (token.type == "string" || token.string == "=") { // A value
+        var before = cm.getRange(Pos(cur.line, Math.max(0, cur.ch - 60)),
+                                 Pos(cur.line, token.type == "string" ? token.start : token.end));
+        var atName = before.match(/([^\s\u00a0=<>\"\']+)=$/), atValues;
+        if (!atName || !attrs.hasOwnProperty(atName[1]) || !(atValues = attrs[atName[1]])) return;
+        if (typeof atValues == 'function') atValues = atValues.call(this, cm); // Functions can be used to supply values for autocomplete widget
+        if (token.type == "string") {
+          prefix = token.string;
+          var n = 0;
+          if (/['"]/.test(token.string.charAt(0))) {
+            quote = token.string.charAt(0);
+            prefix = token.string.slice(1);
+            n++;
+          }
+          var len = token.string.length;
+          if (/['"]/.test(token.string.charAt(len - 1))) {
+            quote = token.string.charAt(len - 1);
+            prefix = token.string.substr(n, len - 2);
+          }
+          replaceToken = true;
+        }
+        for (var i = 0; i < atValues.length; ++i) if (!prefix || atValues[i].lastIndexOf(prefix, 0) == 0)
+          result.push(quote + atValues[i] + quote);
+      } else { // An attribute name
+        if (token.type == "attribute") {
+          prefix = token.string;
+          replaceToken = true;
+        }
+        for (var attr in attrs) if (attrs.hasOwnProperty(attr) && (!prefix || attr.lastIndexOf(prefix, 0) == 0))
+          result.push(attr);
+      }
+    }
+    return {
+      list: result,
+      from: replaceToken ? Pos(cur.line, tagStart == null ? token.start : tagStart) : cur,
+      to: replaceToken ? Pos(cur.line, token.end) : cur
+    };
+  }
+
+  CodeMirror.registerHelper("hint", "xml", getHints);
+});

+ 41 - 0
public/theme/material/editor/lib/codemirror/addon/lint/coffeescript-lint.js

@@ -0,0 +1,41 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Depends on coffeelint.js from http://www.coffeelint.org/js/coffeelint.js
+
+// declare global: coffeelint
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerHelper("lint", "coffeescript", function(text) {
+  var found = [];
+  var parseError = function(err) {
+    var loc = err.lineNumber;
+    found.push({from: CodeMirror.Pos(loc-1, 0),
+                to: CodeMirror.Pos(loc, 0),
+                severity: err.level,
+                message: err.message});
+  };
+  try {
+    var res = coffeelint.lint(text);
+    for(var i = 0; i < res.length; i++) {
+      parseError(res[i]);
+    }
+  } catch(e) {
+    found.push({from: CodeMirror.Pos(e.location.first_line, 0),
+                to: CodeMirror.Pos(e.location.last_line, e.location.last_column),
+                severity: 'error',
+                message: e.message});
+  }
+  return found;
+});
+
+});

+ 35 - 0
public/theme/material/editor/lib/codemirror/addon/lint/css-lint.js

@@ -0,0 +1,35 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Depends on csslint.js from https://github.com/stubbornella/csslint
+
+// declare global: CSSLint
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerHelper("lint", "css", function(text) {
+  var found = [];
+  if (!window.CSSLint) return found;
+  var results = CSSLint.verify(text), messages = results.messages, message = null;
+  for ( var i = 0; i < messages.length; i++) {
+    message = messages[i];
+    var startLine = message.line -1, endLine = message.line -1, startCol = message.col -1, endCol = message.col;
+    found.push({
+      from: CodeMirror.Pos(startLine, startCol),
+      to: CodeMirror.Pos(endLine, endCol),
+      message: message.message,
+      severity : message.type
+    });
+  }
+  return found;
+});
+
+});

+ 136 - 0
public/theme/material/editor/lib/codemirror/addon/lint/javascript-lint.js

@@ -0,0 +1,136 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+  // declare global: JSHINT
+
+  var bogus = [ "Dangerous comment" ];
+
+  var warnings = [ [ "Expected '{'",
+                     "Statement body should be inside '{ }' braces." ] ];
+
+  var errors = [ "Missing semicolon", "Extra comma", "Missing property name",
+                 "Unmatched ", " and instead saw", " is not defined",
+                 "Unclosed string", "Stopping, unable to continue" ];
+
+  function validator(text, options) {
+    if (!window.JSHINT) return [];
+    JSHINT(text, options);
+    var errors = JSHINT.data().errors, result = [];
+    if (errors) parseErrors(errors, result);
+    return result;
+  }
+
+  CodeMirror.registerHelper("lint", "javascript", validator);
+
+  function cleanup(error) {
+    // All problems are warnings by default
+    fixWith(error, warnings, "warning", true);
+    fixWith(error, errors, "error");
+
+    return isBogus(error) ? null : error;
+  }
+
+  function fixWith(error, fixes, severity, force) {
+    var description, fix, find, replace, found;
+
+    description = error.description;
+
+    for ( var i = 0; i < fixes.length; i++) {
+      fix = fixes[i];
+      find = (typeof fix === "string" ? fix : fix[0]);
+      replace = (typeof fix === "string" ? null : fix[1]);
+      found = description.indexOf(find) !== -1;
+
+      if (force || found) {
+        error.severity = severity;
+      }
+      if (found && replace) {
+        error.description = replace;
+      }
+    }
+  }
+
+  function isBogus(error) {
+    var description = error.description;
+    for ( var i = 0; i < bogus.length; i++) {
+      if (description.indexOf(bogus[i]) !== -1) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  function parseErrors(errors, output) {
+    for ( var i = 0; i < errors.length; i++) {
+      var error = errors[i];
+      if (error) {
+        var linetabpositions, index;
+
+        linetabpositions = [];
+
+        // This next block is to fix a problem in jshint. Jshint
+        // replaces
+        // all tabs with spaces then performs some checks. The error
+        // positions (character/space) are then reported incorrectly,
+        // not taking the replacement step into account. Here we look
+        // at the evidence line and try to adjust the character position
+        // to the correct value.
+        if (error.evidence) {
+          // Tab positions are computed once per line and cached
+          var tabpositions = linetabpositions[error.line];
+          if (!tabpositions) {
+            var evidence = error.evidence;
+            tabpositions = [];
+            // ugggh phantomjs does not like this
+            // forEachChar(evidence, function(item, index) {
+            Array.prototype.forEach.call(evidence, function(item,
+                                                            index) {
+              if (item === '\t') {
+                // First col is 1 (not 0) to match error
+                // positions
+                tabpositions.push(index + 1);
+              }
+            });
+            linetabpositions[error.line] = tabpositions;
+          }
+          if (tabpositions.length > 0) {
+            var pos = error.character;
+            tabpositions.forEach(function(tabposition) {
+              if (pos > tabposition) pos -= 1;
+            });
+            error.character = pos;
+          }
+        }
+
+        var start = error.character - 1, end = start + 1;
+        if (error.evidence) {
+          index = error.evidence.substring(start).search(/.\b/);
+          if (index > -1) {
+            end += index;
+          }
+        }
+
+        // Convert to format expected by validation service
+        error.description = error.reason;// + "(jshint)";
+        error.start = error.character;
+        error.end = end;
+        error = cleanup(error);
+
+        if (error)
+          output.push({message: error.description,
+                       severity: error.severity,
+                       from: CodeMirror.Pos(error.line - 1, start),
+                       to: CodeMirror.Pos(error.line - 1, end)});
+      }
+    }
+  }
+});

+ 31 - 0
public/theme/material/editor/lib/codemirror/addon/lint/json-lint.js

@@ -0,0 +1,31 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Depends on jsonlint.js from https://github.com/zaach/jsonlint
+
+// declare global: jsonlint
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.registerHelper("lint", "json", function(text) {
+  var found = [];
+  jsonlint.parseError = function(str, hash) {
+    var loc = hash.loc;
+    found.push({from: CodeMirror.Pos(loc.first_line - 1, loc.first_column),
+                to: CodeMirror.Pos(loc.last_line - 1, loc.last_column),
+                message: str});
+  };
+  try { jsonlint.parse(text); }
+  catch(e) {}
+  return found;
+});
+
+});

+ 73 - 0
public/theme/material/editor/lib/codemirror/addon/lint/lint.css

@@ -0,0 +1,73 @@
+/* The lint marker gutter */
+.CodeMirror-lint-markers {
+  width: 16px;
+}
+
+.CodeMirror-lint-tooltip {
+  background-color: infobackground;
+  border: 1px solid black;
+  border-radius: 4px 4px 4px 4px;
+  color: infotext;
+  font-family: monospace;
+  font-size: 10pt;
+  overflow: hidden;
+  padding: 2px 5px;
+  position: fixed;
+  white-space: pre;
+  white-space: pre-wrap;
+  z-index: 100;
+  max-width: 600px;
+  opacity: 0;
+  transition: opacity .4s;
+  -moz-transition: opacity .4s;
+  -webkit-transition: opacity .4s;
+  -o-transition: opacity .4s;
+  -ms-transition: opacity .4s;
+}
+
+.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning {
+  background-position: left bottom;
+  background-repeat: repeat-x;
+}
+
+.CodeMirror-lint-mark-error {
+  background-image:
+  url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==")
+  ;
+}
+
+.CodeMirror-lint-mark-warning {
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
+}
+
+.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning {
+  background-position: center center;
+  background-repeat: no-repeat;
+  cursor: pointer;
+  display: inline-block;
+  height: 16px;
+  width: 16px;
+  vertical-align: middle;
+  position: relative;
+}
+
+.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning {
+  padding-left: 18px;
+  background-position: top left;
+  background-repeat: no-repeat;
+}
+
+.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
+}
+
+.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
+}
+
+.CodeMirror-lint-marker-multiple {
+  background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
+  background-repeat: no-repeat;
+  background-position: right bottom;
+  width: 100%; height: 100%;
+}

+ 205 - 0
public/theme/material/editor/lib/codemirror/addon/lint/lint.js

@@ -0,0 +1,205 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+  var GUTTER_ID = "CodeMirror-lint-markers";
+
+  function showTooltip(e, content) {
+    var tt = document.createElement("div");
+    tt.className = "CodeMirror-lint-tooltip";
+    tt.appendChild(content.cloneNode(true));
+    document.body.appendChild(tt);
+
+    function position(e) {
+      if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position);
+      tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px";
+      tt.style.left = (e.clientX + 5) + "px";
+    }
+    CodeMirror.on(document, "mousemove", position);
+    position(e);
+    if (tt.style.opacity != null) tt.style.opacity = 1;
+    return tt;
+  }
+  function rm(elt) {
+    if (elt.parentNode) elt.parentNode.removeChild(elt);
+  }
+  function hideTooltip(tt) {
+    if (!tt.parentNode) return;
+    if (tt.style.opacity == null) rm(tt);
+    tt.style.opacity = 0;
+    setTimeout(function() { rm(tt); }, 600);
+  }
+
+  function showTooltipFor(e, content, node) {
+    var tooltip = showTooltip(e, content);
+    function hide() {
+      CodeMirror.off(node, "mouseout", hide);
+      if (tooltip) { hideTooltip(tooltip); tooltip = null; }
+    }
+    var poll = setInterval(function() {
+      if (tooltip) for (var n = node;; n = n.parentNode) {
+        if (n && n.nodeType == 11) n = n.host;
+        if (n == document.body) return;
+        if (!n) { hide(); break; }
+      }
+      if (!tooltip) return clearInterval(poll);
+    }, 400);
+    CodeMirror.on(node, "mouseout", hide);
+  }
+
+  function LintState(cm, options, hasGutter) {
+    this.marked = [];
+    this.options = options;
+    this.timeout = null;
+    this.hasGutter = hasGutter;
+    this.onMouseOver = function(e) { onMouseOver(cm, e); };
+  }
+
+  function parseOptions(cm, options) {
+    if (options instanceof Function) return {getAnnotations: options};
+    if (!options || options === true) options = {};
+    if (!options.getAnnotations) options.getAnnotations = cm.getHelper(CodeMirror.Pos(0, 0), "lint");
+    if (!options.getAnnotations) throw new Error("Required option 'getAnnotations' missing (lint addon)");
+    return options;
+  }
+
+  function clearMarks(cm) {
+    var state = cm.state.lint;
+    if (state.hasGutter) cm.clearGutter(GUTTER_ID);
+    for (var i = 0; i < state.marked.length; ++i)
+      state.marked[i].clear();
+    state.marked.length = 0;
+  }
+
+  function makeMarker(labels, severity, multiple, tooltips) {
+    var marker = document.createElement("div"), inner = marker;
+    marker.className = "CodeMirror-lint-marker-" + severity;
+    if (multiple) {
+      inner = marker.appendChild(document.createElement("div"));
+      inner.className = "CodeMirror-lint-marker-multiple";
+    }
+
+    if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) {
+      showTooltipFor(e, labels, inner);
+    });
+
+    return marker;
+  }
+
+  function getMaxSeverity(a, b) {
+    if (a == "error") return a;
+    else return b;
+  }
+
+  function groupByLine(annotations) {
+    var lines = [];
+    for (var i = 0; i < annotations.length; ++i) {
+      var ann = annotations[i], line = ann.from.line;
+      (lines[line] || (lines[line] = [])).push(ann);
+    }
+    return lines;
+  }
+
+  function annotationTooltip(ann) {
+    var severity = ann.severity;
+    if (!severity) severity = "error";
+    var tip = document.createElement("div");
+    tip.className = "CodeMirror-lint-message-" + severity;
+    tip.appendChild(document.createTextNode(ann.message));
+    return tip;
+  }
+
+  function startLinting(cm) {
+    var state = cm.state.lint, options = state.options;
+    var passOptions = options.options || options; // Support deprecated passing of `options` property in options
+    if (options.async || options.getAnnotations.async)
+      options.getAnnotations(cm.getValue(), updateLinting, passOptions, cm);
+    else
+      updateLinting(cm, options.getAnnotations(cm.getValue(), passOptions, cm));
+  }
+
+  function updateLinting(cm, annotationsNotSorted) {
+    clearMarks(cm);
+    var state = cm.state.lint, options = state.options;
+
+    var annotations = groupByLine(annotationsNotSorted);
+
+    for (var line = 0; line < annotations.length; ++line) {
+      var anns = annotations[line];
+      if (!anns) continue;
+
+      var maxSeverity = null;
+      var tipLabel = state.hasGutter && document.createDocumentFragment();
+
+      for (var i = 0; i < anns.length; ++i) {
+        var ann = anns[i];
+        var severity = ann.severity;
+        if (!severity) severity = "error";
+        maxSeverity = getMaxSeverity(maxSeverity, severity);
+
+        if (options.formatAnnotation) ann = options.formatAnnotation(ann);
+        if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));
+
+        if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, {
+          className: "CodeMirror-lint-mark-" + severity,
+          __annotation: ann
+        }));
+      }
+
+      if (state.hasGutter)
+        cm.setGutterMarker(line, GUTTER_ID, makeMarker(tipLabel, maxSeverity, anns.length > 1,
+                                                       state.options.tooltips));
+    }
+    if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm);
+  }
+
+  function onChange(cm) {
+    var state = cm.state.lint;
+    clearTimeout(state.timeout);
+    state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500);
+  }
+
+  function popupSpanTooltip(ann, e) {
+    var target = e.target || e.srcElement;
+    showTooltipFor(e, annotationTooltip(ann), target);
+  }
+
+  function onMouseOver(cm, e) {
+    var target = e.target || e.srcElement;
+    if (!/\bCodeMirror-lint-mark-/.test(target.className)) return;
+    var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2;
+    var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client"));
+    for (var i = 0; i < spans.length; ++i) {
+      var ann = spans[i].__annotation;
+      if (ann) return popupSpanTooltip(ann, e);
+    }
+  }
+
+  CodeMirror.defineOption("lint", false, function(cm, val, old) {
+    if (old && old != CodeMirror.Init) {
+      clearMarks(cm);
+      cm.off("change", onChange);
+      CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver);
+      delete cm.state.lint;
+    }
+
+    if (val) {
+      var gutters = cm.getOption("gutters"), hasLintGutter = false;
+      for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true;
+      var state = cm.state.lint = new LintState(cm, parseOptions(cm, val), hasLintGutter);
+      cm.on("change", onChange);
+      if (state.options.tooltips != false)
+        CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver);
+
+      startLinting(cm);
+    }
+  });
+});

+ 28 - 0
public/theme/material/editor/lib/codemirror/addon/lint/yaml-lint.js

@@ -0,0 +1,28 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+// Depends on js-yaml.js from https://github.com/nodeca/js-yaml
+
+// declare global: jsyaml
+
+CodeMirror.registerHelper("lint", "yaml", function(text) {
+  var found = [];
+  try { jsyaml.load(text); }
+  catch(e) {
+      var loc = e.mark;
+      found.push({ from: CodeMirror.Pos(loc.line, loc.column), to: CodeMirror.Pos(loc.line, loc.column), message: e.message });
+  }
+  return found;
+});
+
+});

+ 112 - 0
public/theme/material/editor/lib/codemirror/addon/merge/merge.css

@@ -0,0 +1,112 @@
+.CodeMirror-merge {
+  position: relative;
+  border: 1px solid #ddd;
+  white-space: pre;
+}
+
+.CodeMirror-merge, .CodeMirror-merge .CodeMirror {
+  height: 350px;
+}
+
+.CodeMirror-merge-2pane .CodeMirror-merge-pane { width: 47%; }
+.CodeMirror-merge-2pane .CodeMirror-merge-gap { width: 6%; }
+.CodeMirror-merge-3pane .CodeMirror-merge-pane { width: 31%; }
+.CodeMirror-merge-3pane .CodeMirror-merge-gap { width: 3.5%; }
+
+.CodeMirror-merge-pane {
+  display: inline-block;
+  white-space: normal;
+  vertical-align: top;
+}
+.CodeMirror-merge-pane-rightmost {
+  position: absolute;
+  right: 0px;
+  z-index: 1;
+}
+
+.CodeMirror-merge-gap {
+  z-index: 2;
+  display: inline-block;
+  height: 100%;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+  overflow: hidden;
+  border-left: 1px solid #ddd;
+  border-right: 1px solid #ddd;
+  position: relative;
+  background: #f8f8f8;
+}
+
+.CodeMirror-merge-scrolllock-wrap {
+  position: absolute;
+  bottom: 0; left: 50%;
+}
+.CodeMirror-merge-scrolllock {
+  position: relative;
+  left: -50%;
+  cursor: pointer;
+  color: #555;
+  line-height: 1;
+}
+
+.CodeMirror-merge-copybuttons-left, .CodeMirror-merge-copybuttons-right {
+  position: absolute;
+  left: 0; top: 0;
+  right: 0; bottom: 0;
+  line-height: 1;
+}
+
+.CodeMirror-merge-copy {
+  position: absolute;
+  cursor: pointer;
+  color: #44c;
+}
+
+.CodeMirror-merge-copy-reverse {
+  position: absolute;
+  cursor: pointer;
+  color: #44c;
+}
+
+.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy { left: 2px; }
+.CodeMirror-merge-copybuttons-right .CodeMirror-merge-copy { right: 2px; }
+
+.CodeMirror-merge-r-inserted, .CodeMirror-merge-l-inserted {
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVQI12MwuCXy3+CWyH8GBgYGJgYkAABZbAQ9ELXurwAAAABJRU5ErkJggg==);
+  background-position: bottom left;
+  background-repeat: repeat-x;
+}
+
+.CodeMirror-merge-r-deleted, .CodeMirror-merge-l-deleted {
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVQI12M4Kyb2/6yY2H8GBgYGJgYkAABURgPz6Ks7wQAAAABJRU5ErkJggg==);
+  background-position: bottom left;
+  background-repeat: repeat-x;
+}
+
+.CodeMirror-merge-r-chunk { background: #ffffe0; }
+.CodeMirror-merge-r-chunk-start { border-top: 1px solid #ee8; }
+.CodeMirror-merge-r-chunk-end { border-bottom: 1px solid #ee8; }
+.CodeMirror-merge-r-connect { fill: #ffffe0; stroke: #ee8; stroke-width: 1px; }
+
+.CodeMirror-merge-l-chunk { background: #eef; }
+.CodeMirror-merge-l-chunk-start { border-top: 1px solid #88e; }
+.CodeMirror-merge-l-chunk-end { border-bottom: 1px solid #88e; }
+.CodeMirror-merge-l-connect { fill: #eef; stroke: #88e; stroke-width: 1px; }
+
+.CodeMirror-merge-l-chunk.CodeMirror-merge-r-chunk { background: #dfd; }
+.CodeMirror-merge-l-chunk-start.CodeMirror-merge-r-chunk-start { border-top: 1px solid #4e4; }
+.CodeMirror-merge-l-chunk-end.CodeMirror-merge-r-chunk-end { border-bottom: 1px solid #4e4; }
+
+.CodeMirror-merge-collapsed-widget:before {
+  content: "(...)";
+}
+.CodeMirror-merge-collapsed-widget {
+  cursor: pointer;
+  color: #88b;
+  background: #eef;
+  border: 1px solid #ddf;
+  font-size: 90%;
+  padding: 0 3px;
+  border-radius: 4px;
+}
+.CodeMirror-merge-collapsed-line .CodeMirror-gutter-elt { display: none; }

+ 735 - 0
public/theme/material/editor/lib/codemirror/addon/merge/merge.js

@@ -0,0 +1,735 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// declare global: diff_match_patch, DIFF_INSERT, DIFF_DELETE, DIFF_EQUAL
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"), require("diff_match_patch"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror", "diff_match_patch"], mod);
+  else // Plain browser env
+    mod(CodeMirror, diff_match_patch);
+})(function(CodeMirror, diff_match_patch) {
+  "use strict";
+  var Pos = CodeMirror.Pos;
+  var svgNS = "http://www.w3.org/2000/svg";
+
+  function DiffView(mv, type) {
+    this.mv = mv;
+    this.type = type;
+    this.classes = type == "left"
+      ? {chunk: "CodeMirror-merge-l-chunk",
+         start: "CodeMirror-merge-l-chunk-start",
+         end: "CodeMirror-merge-l-chunk-end",
+         insert: "CodeMirror-merge-l-inserted",
+         del: "CodeMirror-merge-l-deleted",
+         connect: "CodeMirror-merge-l-connect"}
+      : {chunk: "CodeMirror-merge-r-chunk",
+         start: "CodeMirror-merge-r-chunk-start",
+         end: "CodeMirror-merge-r-chunk-end",
+         insert: "CodeMirror-merge-r-inserted",
+         del: "CodeMirror-merge-r-deleted",
+         connect: "CodeMirror-merge-r-connect"};
+  }
+
+  DiffView.prototype = {
+    constructor: DiffView,
+    init: function(pane, orig, options) {
+      this.edit = this.mv.edit;
+      this.orig = CodeMirror(pane, copyObj({value: orig, readOnly: !this.mv.options.allowEditingOriginals}, copyObj(options)));
+
+      this.diff = getDiff(asString(orig), asString(options.value));
+      this.chunks = getChunks(this.diff);
+      this.diffOutOfDate = this.dealigned = false;
+
+      this.showDifferences = options.showDifferences !== false;
+      this.forceUpdate = registerUpdate(this);
+      setScrollLock(this, true, false);
+      registerScroll(this);
+    },
+    setShowDifferences: function(val) {
+      val = val !== false;
+      if (val != this.showDifferences) {
+        this.showDifferences = val;
+        this.forceUpdate("full");
+      }
+    }
+  };
+
+  function ensureDiff(dv) {
+    if (dv.diffOutOfDate) {
+      dv.diff = getDiff(dv.orig.getValue(), dv.edit.getValue());
+      dv.chunks = getChunks(dv.diff);
+      dv.diffOutOfDate = false;
+      CodeMirror.signal(dv.edit, "updateDiff", dv.diff);
+    }
+  }
+
+  var updating = false;
+  function registerUpdate(dv) {
+    var edit = {from: 0, to: 0, marked: []};
+    var orig = {from: 0, to: 0, marked: []};
+    var debounceChange, updatingFast = false;
+    function update(mode) {
+      updating = true;
+      updatingFast = false;
+      if (mode == "full") {
+        if (dv.svg) clear(dv.svg);
+        if (dv.copyButtons) clear(dv.copyButtons);
+        clearMarks(dv.edit, edit.marked, dv.classes);
+        clearMarks(dv.orig, orig.marked, dv.classes);
+        edit.from = edit.to = orig.from = orig.to = 0;
+      }
+      ensureDiff(dv);
+      if (dv.showDifferences) {
+        updateMarks(dv.edit, dv.diff, edit, DIFF_INSERT, dv.classes);
+        updateMarks(dv.orig, dv.diff, orig, DIFF_DELETE, dv.classes);
+      }
+      makeConnections(dv);
+
+      if (dv.mv.options.connect == "align")
+        alignChunks(dv);
+      updating = false;
+    }
+    function setDealign(fast) {
+      if (updating) return;
+      dv.dealigned = true;
+      set(fast);
+    }
+    function set(fast) {
+      if (updating || updatingFast) return;
+      clearTimeout(debounceChange);
+      if (fast === true) updatingFast = true;
+      debounceChange = setTimeout(update, fast === true ? 20 : 250);
+    }
+    function change(_cm, change) {
+      if (!dv.diffOutOfDate) {
+        dv.diffOutOfDate = true;
+        edit.from = edit.to = orig.from = orig.to = 0;
+      }
+      // Update faster when a line was added/removed
+      setDealign(change.text.length - 1 != change.to.line - change.from.line);
+    }
+    dv.edit.on("change", change);
+    dv.orig.on("change", change);
+    dv.edit.on("markerAdded", setDealign);
+    dv.edit.on("markerCleared", setDealign);
+    dv.orig.on("markerAdded", setDealign);
+    dv.orig.on("markerCleared", setDealign);
+    dv.edit.on("viewportChange", function() { set(false); });
+    dv.orig.on("viewportChange", function() { set(false); });
+    update();
+    return update;
+  }
+
+  function registerScroll(dv) {
+    dv.edit.on("scroll", function() {
+      syncScroll(dv, DIFF_INSERT) && makeConnections(dv);
+    });
+    dv.orig.on("scroll", function() {
+      syncScroll(dv, DIFF_DELETE) && makeConnections(dv);
+    });
+  }
+
+  function syncScroll(dv, type) {
+    // Change handler will do a refresh after a timeout when diff is out of date
+    if (dv.diffOutOfDate) return false;
+    if (!dv.lockScroll) return true;
+    var editor, other, now = +new Date;
+    if (type == DIFF_INSERT) { editor = dv.edit; other = dv.orig; }
+    else { editor = dv.orig; other = dv.edit; }
+    // Don't take action if the position of this editor was recently set
+    // (to prevent feedback loops)
+    if (editor.state.scrollSetBy == dv && (editor.state.scrollSetAt || 0) + 50 > now) return false;
+
+    var sInfo = editor.getScrollInfo();
+    if (dv.mv.options.connect == "align") {
+      targetPos = sInfo.top;
+    } else {
+      var halfScreen = .5 * sInfo.clientHeight, midY = sInfo.top + halfScreen;
+      var mid = editor.lineAtHeight(midY, "local");
+      var around = chunkBoundariesAround(dv.chunks, mid, type == DIFF_INSERT);
+      var off = getOffsets(editor, type == DIFF_INSERT ? around.edit : around.orig);
+      var offOther = getOffsets(other, type == DIFF_INSERT ? around.orig : around.edit);
+      var ratio = (midY - off.top) / (off.bot - off.top);
+      var targetPos = (offOther.top - halfScreen) + ratio * (offOther.bot - offOther.top);
+
+      var botDist, mix;
+      // Some careful tweaking to make sure no space is left out of view
+      // when scrolling to top or bottom.
+      if (targetPos > sInfo.top && (mix = sInfo.top / halfScreen) < 1) {
+        targetPos = targetPos * mix + sInfo.top * (1 - mix);
+      } else if ((botDist = sInfo.height - sInfo.clientHeight - sInfo.top) < halfScreen) {
+        var otherInfo = other.getScrollInfo();
+        var botDistOther = otherInfo.height - otherInfo.clientHeight - targetPos;
+        if (botDistOther > botDist && (mix = botDist / halfScreen) < 1)
+          targetPos = targetPos * mix + (otherInfo.height - otherInfo.clientHeight - botDist) * (1 - mix);
+      }
+    }
+
+    other.scrollTo(sInfo.left, targetPos);
+    other.state.scrollSetAt = now;
+    other.state.scrollSetBy = dv;
+    return true;
+  }
+
+  function getOffsets(editor, around) {
+    var bot = around.after;
+    if (bot == null) bot = editor.lastLine() + 1;
+    return {top: editor.heightAtLine(around.before || 0, "local"),
+            bot: editor.heightAtLine(bot, "local")};
+  }
+
+  function setScrollLock(dv, val, action) {
+    dv.lockScroll = val;
+    if (val && action != false) syncScroll(dv, DIFF_INSERT) && makeConnections(dv);
+    dv.lockButton.innerHTML = val ? "\u21db\u21da" : "\u21db&nbsp;&nbsp;\u21da";
+  }
+
+  // Updating the marks for editor content
+
+  function clearMarks(editor, arr, classes) {
+    for (var i = 0; i < arr.length; ++i) {
+      var mark = arr[i];
+      if (mark instanceof CodeMirror.TextMarker) {
+        mark.clear();
+      } else if (mark.parent) {
+        editor.removeLineClass(mark, "background", classes.chunk);
+        editor.removeLineClass(mark, "background", classes.start);
+        editor.removeLineClass(mark, "background", classes.end);
+      }
+    }
+    arr.length = 0;
+  }
+
+  // FIXME maybe add a margin around viewport to prevent too many updates
+  function updateMarks(editor, diff, state, type, classes) {
+    var vp = editor.getViewport();
+    editor.operation(function() {
+      if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {
+        clearMarks(editor, state.marked, classes);
+        markChanges(editor, diff, type, state.marked, vp.from, vp.to, classes);
+        state.from = vp.from; state.to = vp.to;
+      } else {
+        if (vp.from < state.from) {
+          markChanges(editor, diff, type, state.marked, vp.from, state.from, classes);
+          state.from = vp.from;
+        }
+        if (vp.to > state.to) {
+          markChanges(editor, diff, type, state.marked, state.to, vp.to, classes);
+          state.to = vp.to;
+        }
+      }
+    });
+  }
+
+  function markChanges(editor, diff, type, marks, from, to, classes) {
+    var pos = Pos(0, 0);
+    var top = Pos(from, 0), bot = editor.clipPos(Pos(to - 1));
+    var cls = type == DIFF_DELETE ? classes.del : classes.insert;
+    function markChunk(start, end) {
+      var bfrom = Math.max(from, start), bto = Math.min(to, end);
+      for (var i = bfrom; i < bto; ++i) {
+        var line = editor.addLineClass(i, "background", classes.chunk);
+        if (i == start) editor.addLineClass(line, "background", classes.start);
+        if (i == end - 1) editor.addLineClass(line, "background", classes.end);
+        marks.push(line);
+      }
+      // When the chunk is empty, make sure a horizontal line shows up
+      if (start == end && bfrom == end && bto == end) {
+        if (bfrom)
+          marks.push(editor.addLineClass(bfrom - 1, "background", classes.end));
+        else
+          marks.push(editor.addLineClass(bfrom, "background", classes.start));
+      }
+    }
+
+    var chunkStart = 0;
+    for (var i = 0; i < diff.length; ++i) {
+      var part = diff[i], tp = part[0], str = part[1];
+      if (tp == DIFF_EQUAL) {
+        var cleanFrom = pos.line + (startOfLineClean(diff, i) ? 0 : 1);
+        moveOver(pos, str);
+        var cleanTo = pos.line + (endOfLineClean(diff, i) ? 1 : 0);
+        if (cleanTo > cleanFrom) {
+          if (i) markChunk(chunkStart, cleanFrom);
+          chunkStart = cleanTo;
+        }
+      } else {
+        if (tp == type) {
+          var end = moveOver(pos, str, true);
+          var a = posMax(top, pos), b = posMin(bot, end);
+          if (!posEq(a, b))
+            marks.push(editor.markText(a, b, {className: cls}));
+          pos = end;
+        }
+      }
+    }
+    if (chunkStart <= pos.line) markChunk(chunkStart, pos.line + 1);
+  }
+
+  // Updating the gap between editor and original
+
+  function makeConnections(dv) {
+    if (!dv.showDifferences) return;
+
+    if (dv.svg) {
+      clear(dv.svg);
+      var w = dv.gap.offsetWidth;
+      attrs(dv.svg, "width", w, "height", dv.gap.offsetHeight);
+    }
+    if (dv.copyButtons) clear(dv.copyButtons);
+
+    var vpEdit = dv.edit.getViewport(), vpOrig = dv.orig.getViewport();
+    var sTopEdit = dv.edit.getScrollInfo().top, sTopOrig = dv.orig.getScrollInfo().top;
+    for (var i = 0; i < dv.chunks.length; i++) {
+      var ch = dv.chunks[i];
+      if (ch.editFrom <= vpEdit.to && ch.editTo >= vpEdit.from &&
+          ch.origFrom <= vpOrig.to && ch.origTo >= vpOrig.from)
+        drawConnectorsForChunk(dv, ch, sTopOrig, sTopEdit, w);
+    }
+  }
+
+  function getMatchingOrigLine(editLine, chunks) {
+    var editStart = 0, origStart = 0;
+    for (var i = 0; i < chunks.length; i++) {
+      var chunk = chunks[i];
+      if (chunk.editTo > editLine && chunk.editFrom <= editLine) return null;
+      if (chunk.editFrom > editLine) break;
+      editStart = chunk.editTo;
+      origStart = chunk.origTo;
+    }
+    return origStart + (editLine - editStart);
+  }
+
+  function findAlignedLines(dv, other) {
+    var linesToAlign = [];
+    for (var i = 0; i < dv.chunks.length; i++) {
+      var chunk = dv.chunks[i];
+      linesToAlign.push([chunk.origTo, chunk.editTo, other ? getMatchingOrigLine(chunk.editTo, other.chunks) : null]);
+    }
+    if (other) {
+      for (var i = 0; i < other.chunks.length; i++) {
+        var chunk = other.chunks[i];
+        for (var j = 0; j < linesToAlign.length; j++) {
+          var align = linesToAlign[j];
+          if (align[1] == chunk.editTo) {
+            j = -1;
+            break;
+          } else if (align[1] > chunk.editTo) {
+            break;
+          }
+        }
+        if (j > -1)
+          linesToAlign.splice(j - 1, 0, [getMatchingOrigLine(chunk.editTo, dv.chunks), chunk.editTo, chunk.origTo]);
+      }
+    }
+    return linesToAlign;
+  }
+
+  function alignChunks(dv, force) {
+    if (!dv.dealigned && !force) return;
+    if (!dv.orig.curOp) return dv.orig.operation(function() {
+      alignChunks(dv, force);
+    });
+
+    dv.dealigned = false;
+    var other = dv.mv.left == dv ? dv.mv.right : dv.mv.left;
+    if (other) {
+      ensureDiff(other);
+      other.dealigned = false;
+    }
+    var linesToAlign = findAlignedLines(dv, other);
+
+    // Clear old aligners
+    var aligners = dv.mv.aligners;
+    for (var i = 0; i < aligners.length; i++)
+      aligners[i].clear();
+    aligners.length = 0;
+
+    var cm = [dv.orig, dv.edit], scroll = [];
+    if (other) cm.push(other.orig);
+    for (var i = 0; i < cm.length; i++)
+      scroll.push(cm[i].getScrollInfo().top);
+
+    for (var ln = 0; ln < linesToAlign.length; ln++)
+      alignLines(cm, linesToAlign[ln], aligners);
+
+    for (var i = 0; i < cm.length; i++)
+      cm[i].scrollTo(null, scroll[i]);
+  }
+
+  function alignLines(cm, lines, aligners) {
+    var maxOffset = 0, offset = [];
+    for (var i = 0; i < cm.length; i++) if (lines[i] != null) {
+      var off = cm[i].heightAtLine(lines[i], "local");
+      offset[i] = off;
+      maxOffset = Math.max(maxOffset, off);
+    }
+    for (var i = 0; i < cm.length; i++) if (lines[i] != null) {
+      var diff = maxOffset - offset[i];
+      if (diff > 1)
+        aligners.push(padAbove(cm[i], lines[i], diff));
+    }
+  }
+
+  function padAbove(cm, line, size) {
+    var above = true;
+    if (line > cm.lastLine()) {
+      line--;
+      above = false;
+    }
+    var elt = document.createElement("div");
+    elt.className = "CodeMirror-merge-spacer";
+    elt.style.height = size + "px"; elt.style.minWidth = "1px";
+    return cm.addLineWidget(line, elt, {height: size, above: above});
+  }
+
+  function drawConnectorsForChunk(dv, chunk, sTopOrig, sTopEdit, w) {
+    var flip = dv.type == "left";
+    var top = dv.orig.heightAtLine(chunk.origFrom, "local") - sTopOrig;
+    if (dv.svg) {
+      var topLpx = top;
+      var topRpx = dv.edit.heightAtLine(chunk.editFrom, "local") - sTopEdit;
+      if (flip) { var tmp = topLpx; topLpx = topRpx; topRpx = tmp; }
+      var botLpx = dv.orig.heightAtLine(chunk.origTo, "local") - sTopOrig;
+      var botRpx = dv.edit.heightAtLine(chunk.editTo, "local") - sTopEdit;
+      if (flip) { var tmp = botLpx; botLpx = botRpx; botRpx = tmp; }
+      var curveTop = " C " + w/2 + " " + topRpx + " " + w/2 + " " + topLpx + " " + (w + 2) + " " + topLpx;
+      var curveBot = " C " + w/2 + " " + botLpx + " " + w/2 + " " + botRpx + " -1 " + botRpx;
+      attrs(dv.svg.appendChild(document.createElementNS(svgNS, "path")),
+            "d", "M -1 " + topRpx + curveTop + " L " + (w + 2) + " " + botLpx + curveBot + " z",
+            "class", dv.classes.connect);
+    }
+    if (dv.copyButtons) {
+      var copy = dv.copyButtons.appendChild(elt("div", dv.type == "left" ? "\u21dd" : "\u21dc",
+                                                "CodeMirror-merge-copy"));
+      var editOriginals = dv.mv.options.allowEditingOriginals;
+      copy.title = editOriginals ? "Push to left" : "Revert chunk";
+      copy.chunk = chunk;
+      copy.style.top = top + "px";
+
+      if (editOriginals) {
+        var topReverse = dv.orig.heightAtLine(chunk.editFrom, "local") - sTopEdit;
+        var copyReverse = dv.copyButtons.appendChild(elt("div", dv.type == "right" ? "\u21dd" : "\u21dc",
+                                                         "CodeMirror-merge-copy-reverse"));
+        copyReverse.title = "Push to right";
+        copyReverse.chunk = {editFrom: chunk.origFrom, editTo: chunk.origTo,
+                             origFrom: chunk.editFrom, origTo: chunk.editTo};
+        copyReverse.style.top = topReverse + "px";
+        dv.type == "right" ? copyReverse.style.left = "2px" : copyReverse.style.right = "2px";
+      }
+    }
+  }
+
+  function copyChunk(dv, to, from, chunk) {
+    if (dv.diffOutOfDate) return;
+    to.replaceRange(from.getRange(Pos(chunk.origFrom, 0), Pos(chunk.origTo, 0)),
+                         Pos(chunk.editFrom, 0), Pos(chunk.editTo, 0));
+  }
+
+  // Merge view, containing 0, 1, or 2 diff views.
+
+  var MergeView = CodeMirror.MergeView = function(node, options) {
+    if (!(this instanceof MergeView)) return new MergeView(node, options);
+
+    this.options = options;
+    var origLeft = options.origLeft, origRight = options.origRight == null ? options.orig : options.origRight;
+
+    var hasLeft = origLeft != null, hasRight = origRight != null;
+    var panes = 1 + (hasLeft ? 1 : 0) + (hasRight ? 1 : 0);
+    var wrap = [], left = this.left = null, right = this.right = null;
+    var self = this;
+
+    if (hasLeft) {
+      left = this.left = new DiffView(this, "left");
+      var leftPane = elt("div", null, "CodeMirror-merge-pane");
+      wrap.push(leftPane);
+      wrap.push(buildGap(left));
+    }
+
+    var editPane = elt("div", null, "CodeMirror-merge-pane");
+    wrap.push(editPane);
+
+    if (hasRight) {
+      right = this.right = new DiffView(this, "right");
+      wrap.push(buildGap(right));
+      var rightPane = elt("div", null, "CodeMirror-merge-pane");
+      wrap.push(rightPane);
+    }
+
+    (hasRight ? rightPane : editPane).className += " CodeMirror-merge-pane-rightmost";
+
+    wrap.push(elt("div", null, null, "height: 0; clear: both;"));
+
+    var wrapElt = this.wrap = node.appendChild(elt("div", wrap, "CodeMirror-merge CodeMirror-merge-" + panes + "pane"));
+    this.edit = CodeMirror(editPane, copyObj(options));
+
+    if (left) left.init(leftPane, origLeft, options);
+    if (right) right.init(rightPane, origRight, options);
+
+    if (options.collapseIdentical) {
+      updating = true;
+      this.editor().operation(function() {
+        collapseIdenticalStretches(self, options.collapseIdentical);
+      });
+      updating = false;
+    }
+    if (options.connect == "align") {
+      this.aligners = [];
+      alignChunks(this.left || this.right, true);
+    }
+
+    var onResize = function() {
+      if (left) makeConnections(left);
+      if (right) makeConnections(right);
+    };
+    CodeMirror.on(window, "resize", onResize);
+    var resizeInterval = setInterval(function() {
+      for (var p = wrapElt.parentNode; p && p != document.body; p = p.parentNode) {}
+      if (!p) { clearInterval(resizeInterval); CodeMirror.off(window, "resize", onResize); }
+    }, 5000);
+  };
+
+  function buildGap(dv) {
+    var lock = dv.lockButton = elt("div", null, "CodeMirror-merge-scrolllock");
+    lock.title = "Toggle locked scrolling";
+    var lockWrap = elt("div", [lock], "CodeMirror-merge-scrolllock-wrap");
+    CodeMirror.on(lock, "click", function() { setScrollLock(dv, !dv.lockScroll); });
+    var gapElts = [lockWrap];
+    if (dv.mv.options.revertButtons !== false) {
+      dv.copyButtons = elt("div", null, "CodeMirror-merge-copybuttons-" + dv.type);
+      CodeMirror.on(dv.copyButtons, "click", function(e) {
+        var node = e.target || e.srcElement;
+        if (!node.chunk) return;
+        if (node.className == "CodeMirror-merge-copy-reverse") {
+          copyChunk(dv, dv.orig, dv.edit, node.chunk);
+          return;
+        }
+        copyChunk(dv, dv.edit, dv.orig, node.chunk);
+      });
+      gapElts.unshift(dv.copyButtons);
+    }
+    if (dv.mv.options.connect != "align") {
+      var svg = document.createElementNS && document.createElementNS(svgNS, "svg");
+      if (svg && !svg.createSVGRect) svg = null;
+      dv.svg = svg;
+      if (svg) gapElts.push(svg);
+    }
+
+    return dv.gap = elt("div", gapElts, "CodeMirror-merge-gap");
+  }
+
+  MergeView.prototype = {
+    constuctor: MergeView,
+    editor: function() { return this.edit; },
+    rightOriginal: function() { return this.right && this.right.orig; },
+    leftOriginal: function() { return this.left && this.left.orig; },
+    setShowDifferences: function(val) {
+      if (this.right) this.right.setShowDifferences(val);
+      if (this.left) this.left.setShowDifferences(val);
+    },
+    rightChunks: function() {
+      if (this.right) { ensureDiff(this.right); return this.right.chunks; }
+    },
+    leftChunks: function() {
+      if (this.left) { ensureDiff(this.left); return this.left.chunks; }
+    }
+  };
+
+  function asString(obj) {
+    if (typeof obj == "string") return obj;
+    else return obj.getValue();
+  }
+
+  // Operations on diffs
+
+  var dmp = new diff_match_patch();
+  function getDiff(a, b) {
+    var diff = dmp.diff_main(a, b);
+    dmp.diff_cleanupSemantic(diff);
+    // The library sometimes leaves in empty parts, which confuse the algorithm
+    for (var i = 0; i < diff.length; ++i) {
+      var part = diff[i];
+      if (!part[1]) {
+        diff.splice(i--, 1);
+      } else if (i && diff[i - 1][0] == part[0]) {
+        diff.splice(i--, 1);
+        diff[i][1] += part[1];
+      }
+    }
+    return diff;
+  }
+
+  function getChunks(diff) {
+    var chunks = [];
+    var startEdit = 0, startOrig = 0;
+    var edit = Pos(0, 0), orig = Pos(0, 0);
+    for (var i = 0; i < diff.length; ++i) {
+      var part = diff[i], tp = part[0];
+      if (tp == DIFF_EQUAL) {
+        var startOff = startOfLineClean(diff, i) ? 0 : 1;
+        var cleanFromEdit = edit.line + startOff, cleanFromOrig = orig.line + startOff;
+        moveOver(edit, part[1], null, orig);
+        var endOff = endOfLineClean(diff, i) ? 1 : 0;
+        var cleanToEdit = edit.line + endOff, cleanToOrig = orig.line + endOff;
+        if (cleanToEdit > cleanFromEdit) {
+          if (i) chunks.push({origFrom: startOrig, origTo: cleanFromOrig,
+                              editFrom: startEdit, editTo: cleanFromEdit});
+          startEdit = cleanToEdit; startOrig = cleanToOrig;
+        }
+      } else {
+        moveOver(tp == DIFF_INSERT ? edit : orig, part[1]);
+      }
+    }
+    if (startEdit <= edit.line || startOrig <= orig.line)
+      chunks.push({origFrom: startOrig, origTo: orig.line + 1,
+                   editFrom: startEdit, editTo: edit.line + 1});
+    return chunks;
+  }
+
+  function endOfLineClean(diff, i) {
+    if (i == diff.length - 1) return true;
+    var next = diff[i + 1][1];
+    if (next.length == 1 || next.charCodeAt(0) != 10) return false;
+    if (i == diff.length - 2) return true;
+    next = diff[i + 2][1];
+    return next.length > 1 && next.charCodeAt(0) == 10;
+  }
+
+  function startOfLineClean(diff, i) {
+    if (i == 0) return true;
+    var last = diff[i - 1][1];
+    if (last.charCodeAt(last.length - 1) != 10) return false;
+    if (i == 1) return true;
+    last = diff[i - 2][1];
+    return last.charCodeAt(last.length - 1) == 10;
+  }
+
+  function chunkBoundariesAround(chunks, n, nInEdit) {
+    var beforeE, afterE, beforeO, afterO;
+    for (var i = 0; i < chunks.length; i++) {
+      var chunk = chunks[i];
+      var fromLocal = nInEdit ? chunk.editFrom : chunk.origFrom;
+      var toLocal = nInEdit ? chunk.editTo : chunk.origTo;
+      if (afterE == null) {
+        if (fromLocal > n) { afterE = chunk.editFrom; afterO = chunk.origFrom; }
+        else if (toLocal > n) { afterE = chunk.editTo; afterO = chunk.origTo; }
+      }
+      if (toLocal <= n) { beforeE = chunk.editTo; beforeO = chunk.origTo; }
+      else if (fromLocal <= n) { beforeE = chunk.editFrom; beforeO = chunk.origFrom; }
+    }
+    return {edit: {before: beforeE, after: afterE}, orig: {before: beforeO, after: afterO}};
+  }
+
+  function collapseSingle(cm, from, to) {
+    cm.addLineClass(from, "wrap", "CodeMirror-merge-collapsed-line");
+    var widget = document.createElement("span");
+    widget.className = "CodeMirror-merge-collapsed-widget";
+    widget.title = "Identical text collapsed. Click to expand.";
+    var mark = cm.markText(Pos(from, 0), Pos(to - 1), {
+      inclusiveLeft: true,
+      inclusiveRight: true,
+      replacedWith: widget,
+      clearOnEnter: true
+    });
+    function clear() {
+      mark.clear();
+      cm.removeLineClass(from, "wrap", "CodeMirror-merge-collapsed-line");
+    }
+    widget.addEventListener("click", clear);
+    return {mark: mark, clear: clear};
+  }
+
+  function collapseStretch(size, editors) {
+    var marks = [];
+    function clear() {
+      for (var i = 0; i < marks.length; i++) marks[i].clear();
+    }
+    for (var i = 0; i < editors.length; i++) {
+      var editor = editors[i];
+      var mark = collapseSingle(editor.cm, editor.line, editor.line + size);
+      marks.push(mark);
+      mark.mark.on("clear", clear);
+    }
+    return marks[0].mark;
+  }
+
+  function unclearNearChunks(dv, margin, off, clear) {
+    for (var i = 0; i < dv.chunks.length; i++) {
+      var chunk = dv.chunks[i];
+      for (var l = chunk.editFrom - margin; l < chunk.editTo + margin; l++) {
+        var pos = l + off;
+        if (pos >= 0 && pos < clear.length) clear[pos] = false;
+      }
+    }
+  }
+
+  function collapseIdenticalStretches(mv, margin) {
+    if (typeof margin != "number") margin = 2;
+    var clear = [], edit = mv.editor(), off = edit.firstLine();
+    for (var l = off, e = edit.lastLine(); l <= e; l++) clear.push(true);
+    if (mv.left) unclearNearChunks(mv.left, margin, off, clear);
+    if (mv.right) unclearNearChunks(mv.right, margin, off, clear);
+
+    for (var i = 0; i < clear.length; i++) {
+      if (clear[i]) {
+        var line = i + off;
+        for (var size = 1; i < clear.length - 1 && clear[i + 1]; i++, size++) {}
+        if (size > margin) {
+          var editors = [{line: line, cm: edit}];
+          if (mv.left) editors.push({line: getMatchingOrigLine(line, mv.left.chunks), cm: mv.left.orig});
+          if (mv.right) editors.push({line: getMatchingOrigLine(line, mv.right.chunks), cm: mv.right.orig});
+          var mark = collapseStretch(size, editors);
+          if (mv.options.onCollapse) mv.options.onCollapse(mv, line, size, mark);
+        }
+      }
+    }
+  }
+
+  // General utilities
+
+  function elt(tag, content, className, style) {
+    var e = document.createElement(tag);
+    if (className) e.className = className;
+    if (style) e.style.cssText = style;
+    if (typeof content == "string") e.appendChild(document.createTextNode(content));
+    else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]);
+    return e;
+  }
+
+  function clear(node) {
+    for (var count = node.childNodes.length; count > 0; --count)
+      node.removeChild(node.firstChild);
+  }
+
+  function attrs(elt) {
+    for (var i = 1; i < arguments.length; i += 2)
+      elt.setAttribute(arguments[i], arguments[i+1]);
+  }
+
+  function copyObj(obj, target) {
+    if (!target) target = {};
+    for (var prop in obj) if (obj.hasOwnProperty(prop)) target[prop] = obj[prop];
+    return target;
+  }
+
+  function moveOver(pos, str, copy, other) {
+    var out = copy ? Pos(pos.line, pos.ch) : pos, at = 0;
+    for (;;) {
+      var nl = str.indexOf("\n", at);
+      if (nl == -1) break;
+      ++out.line;
+      if (other) ++other.line;
+      at = nl + 1;
+    }
+    out.ch = (at ? 0 : out.ch) + (str.length - at);
+    if (other) other.ch = (at ? 0 : other.ch) + (str.length - at);
+    return out;
+  }
+
+  function posMin(a, b) { return (a.line - b.line || a.ch - b.ch) < 0 ? a : b; }
+  function posMax(a, b) { return (a.line - b.line || a.ch - b.ch) > 0 ? a : b; }
+  function posEq(a, b) { return a.line == b.line && a.ch == b.ch; }
+});

+ 64 - 0
public/theme/material/editor/lib/codemirror/addon/mode/loadmode.js

@@ -0,0 +1,64 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"), "cjs");
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], function(CM) { mod(CM, "amd"); });
+  else // Plain browser env
+    mod(CodeMirror, "plain");
+})(function(CodeMirror, env) {
+  if (!CodeMirror.modeURL) CodeMirror.modeURL = "../mode/%N/%N.js";
+
+  var loading = {};
+  function splitCallback(cont, n) {
+    var countDown = n;
+    return function() { if (--countDown == 0) cont(); };
+  }
+  function ensureDeps(mode, cont) {
+    var deps = CodeMirror.modes[mode].dependencies;
+    if (!deps) return cont();
+    var missing = [];
+    for (var i = 0; i < deps.length; ++i) {
+      if (!CodeMirror.modes.hasOwnProperty(deps[i]))
+        missing.push(deps[i]);
+    }
+    if (!missing.length) return cont();
+    var split = splitCallback(cont, missing.length);
+    for (var i = 0; i < missing.length; ++i)
+      CodeMirror.requireMode(missing[i], split);
+  }
+
+  CodeMirror.requireMode = function(mode, cont) {
+    if (typeof mode != "string") mode = mode.name;
+    if (CodeMirror.modes.hasOwnProperty(mode)) return ensureDeps(mode, cont);
+    if (loading.hasOwnProperty(mode)) return loading[mode].push(cont);
+
+    var file = CodeMirror.modeURL.replace(/%N/g, mode);
+    if (env == "plain") {
+      var script = document.createElement("script");
+      script.src = file;
+      var others = document.getElementsByTagName("script")[0];
+      var list = loading[mode] = [cont];
+      CodeMirror.on(script, "load", function() {
+        ensureDeps(mode, function() {
+          for (var i = 0; i < list.length; ++i) list[i]();
+        });
+      });
+      others.parentNode.insertBefore(script, others);
+    } else if (env == "cjs") {
+      require(file);
+      cont();
+    } else if (env == "amd") {
+      requirejs([file], cont);
+    }
+  };
+
+  CodeMirror.autoLoadMode = function(instance, mode) {
+    if (!CodeMirror.modes.hasOwnProperty(mode))
+      CodeMirror.requireMode(mode, function() {
+        instance.setOption("mode", instance.getOption("mode"));
+      });
+  };
+});

+ 118 - 0
public/theme/material/editor/lib/codemirror/addon/mode/multiplex.js

@@ -0,0 +1,118 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.multiplexingMode = function(outer /*, others */) {
+  // Others should be {open, close, mode [, delimStyle] [, innerStyle]} objects
+  var others = Array.prototype.slice.call(arguments, 1);
+  var n_others = others.length;
+
+  function indexOf(string, pattern, from) {
+    if (typeof pattern == "string") return string.indexOf(pattern, from);
+    var m = pattern.exec(from ? string.slice(from) : string);
+    return m ? m.index + from : -1;
+  }
+
+  return {
+    startState: function() {
+      return {
+        outer: CodeMirror.startState(outer),
+        innerActive: null,
+        inner: null
+      };
+    },
+
+    copyState: function(state) {
+      return {
+        outer: CodeMirror.copyState(outer, state.outer),
+        innerActive: state.innerActive,
+        inner: state.innerActive && CodeMirror.copyState(state.innerActive.mode, state.inner)
+      };
+    },
+
+    token: function(stream, state) {
+      if (!state.innerActive) {
+        var cutOff = Infinity, oldContent = stream.string;
+        for (var i = 0; i < n_others; ++i) {
+          var other = others[i];
+          var found = indexOf(oldContent, other.open, stream.pos);
+          if (found == stream.pos) {
+            stream.match(other.open);
+            state.innerActive = other;
+            state.inner = CodeMirror.startState(other.mode, outer.indent ? outer.indent(state.outer, "") : 0);
+            return other.delimStyle;
+          } else if (found != -1 && found < cutOff) {
+            cutOff = found;
+          }
+        }
+        if (cutOff != Infinity) stream.string = oldContent.slice(0, cutOff);
+        var outerToken = outer.token(stream, state.outer);
+        if (cutOff != Infinity) stream.string = oldContent;
+        return outerToken;
+      } else {
+        var curInner = state.innerActive, oldContent = stream.string;
+        if (!curInner.close && stream.sol()) {
+          state.innerActive = state.inner = null;
+          return this.token(stream, state);
+        }
+        var found = curInner.close ? indexOf(oldContent, curInner.close, stream.pos) : -1;
+        if (found == stream.pos) {
+          stream.match(curInner.close);
+          state.innerActive = state.inner = null;
+          return curInner.delimStyle;
+        }
+        if (found > -1) stream.string = oldContent.slice(0, found);
+        var innerToken = curInner.mode.token(stream, state.inner);
+        if (found > -1) stream.string = oldContent;
+
+        if (curInner.innerStyle) {
+          if (innerToken) innerToken = innerToken + ' ' + curInner.innerStyle;
+          else innerToken = curInner.innerStyle;
+        }
+
+        return innerToken;
+      }
+    },
+
+    indent: function(state, textAfter) {
+      var mode = state.innerActive ? state.innerActive.mode : outer;
+      if (!mode.indent) return CodeMirror.Pass;
+      return mode.indent(state.innerActive ? state.inner : state.outer, textAfter);
+    },
+
+    blankLine: function(state) {
+      var mode = state.innerActive ? state.innerActive.mode : outer;
+      if (mode.blankLine) {
+        mode.blankLine(state.innerActive ? state.inner : state.outer);
+      }
+      if (!state.innerActive) {
+        for (var i = 0; i < n_others; ++i) {
+          var other = others[i];
+          if (other.open === "\n") {
+            state.innerActive = other;
+            state.inner = CodeMirror.startState(other.mode, mode.indent ? mode.indent(state.outer, "") : 0);
+          }
+        }
+      } else if (state.innerActive.close === "\n") {
+        state.innerActive = state.inner = null;
+      }
+    },
+
+    electricChars: outer.electricChars,
+
+    innerMode: function(state) {
+      return state.inner ? {state: state.inner, mode: state.innerActive.mode} : {state: state.outer, mode: outer};
+    }
+  };
+};
+
+});

+ 33 - 0
public/theme/material/editor/lib/codemirror/addon/mode/multiplex_test.js

@@ -0,0 +1,33 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function() {
+  CodeMirror.defineMode("markdown_with_stex", function(){
+    var inner = CodeMirror.getMode({}, "stex");
+    var outer = CodeMirror.getMode({}, "markdown");
+
+    var innerOptions = {
+      open: '$',
+      close: '$',
+      mode: inner,
+      delimStyle: 'delim',
+      innerStyle: 'inner'
+    };
+
+    return CodeMirror.multiplexingMode(outer, innerOptions);
+  });
+
+  var mode = CodeMirror.getMode({}, "markdown_with_stex");
+
+  function MT(name) {
+    test.mode(
+      name,
+      mode,
+      Array.prototype.slice.call(arguments, 1),
+      'multiplexing');
+  }
+
+  MT(
+    "stexInsideMarkdown",
+    "[strong **Equation:**] [delim $][inner&tag \\pi][delim $]");
+})();

+ 85 - 0
public/theme/material/editor/lib/codemirror/addon/mode/overlay.js

@@ -0,0 +1,85 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Utility function that allows modes to be combined. The mode given
+// as the base argument takes care of most of the normal mode
+// functionality, but a second (typically simple) mode is used, which
+// can override the style of text. Both modes get to parse all of the
+// text, but when both assign a non-null style to a piece of code, the
+// overlay wins, unless the combine argument was true and not overridden,
+// or state.overlay.combineTokens was true, in which case the styles are
+// combined.
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.overlayMode = function(base, overlay, combine) {
+  return {
+    startState: function() {
+      return {
+        base: CodeMirror.startState(base),
+        overlay: CodeMirror.startState(overlay),
+        basePos: 0, baseCur: null,
+        overlayPos: 0, overlayCur: null,
+        streamSeen: null
+      };
+    },
+    copyState: function(state) {
+      return {
+        base: CodeMirror.copyState(base, state.base),
+        overlay: CodeMirror.copyState(overlay, state.overlay),
+        basePos: state.basePos, baseCur: null,
+        overlayPos: state.overlayPos, overlayCur: null
+      };
+    },
+
+    token: function(stream, state) {
+      if (stream != state.streamSeen ||
+          Math.min(state.basePos, state.overlayPos) < stream.start) {
+        state.streamSeen = stream;
+        state.basePos = state.overlayPos = stream.start;
+      }
+
+      if (stream.start == state.basePos) {
+        state.baseCur = base.token(stream, state.base);
+        state.basePos = stream.pos;
+      }
+      if (stream.start == state.overlayPos) {
+        stream.pos = stream.start;
+        state.overlayCur = overlay.token(stream, state.overlay);
+        state.overlayPos = stream.pos;
+      }
+      stream.pos = Math.min(state.basePos, state.overlayPos);
+
+      // state.overlay.combineTokens always takes precedence over combine,
+      // unless set to null
+      if (state.overlayCur == null) return state.baseCur;
+      else if (state.baseCur != null &&
+               state.overlay.combineTokens ||
+               combine && state.overlay.combineTokens == null)
+        return state.baseCur + " " + state.overlayCur;
+      else return state.overlayCur;
+    },
+
+    indent: base.indent && function(state, textAfter) {
+      return base.indent(state.base, textAfter);
+    },
+    electricChars: base.electricChars,
+
+    innerMode: function(state) { return {state: state.base, mode: base}; },
+
+    blankLine: function(state) {
+      if (base.blankLine) base.blankLine(state.base);
+      if (overlay.blankLine) overlay.blankLine(state.overlay);
+    }
+  };
+};
+
+});

+ 213 - 0
public/theme/material/editor/lib/codemirror/addon/mode/simple.js

@@ -0,0 +1,213 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  CodeMirror.defineSimpleMode = function(name, states) {
+    CodeMirror.defineMode(name, function(config) {
+      return CodeMirror.simpleMode(config, states);
+    });
+  };
+
+  CodeMirror.simpleMode = function(config, states) {
+    ensureState(states, "start");
+    var states_ = {}, meta = states.meta || {}, hasIndentation = false;
+    for (var state in states) if (state != meta && states.hasOwnProperty(state)) {
+      var list = states_[state] = [], orig = states[state];
+      for (var i = 0; i < orig.length; i++) {
+        var data = orig[i];
+        list.push(new Rule(data, states));
+        if (data.indent || data.dedent) hasIndentation = true;
+      }
+    }
+    var mode = {
+      startState: function() {
+        return {state: "start", pending: null,
+                local: null, localState: null,
+                indent: hasIndentation ? [] : null};
+      },
+      copyState: function(state) {
+        var s = {state: state.state, pending: state.pending,
+                 local: state.local, localState: null,
+                 indent: state.indent && state.indent.slice(0)};
+        if (state.localState)
+          s.localState = CodeMirror.copyState(state.local.mode, state.localState);
+        if (state.stack)
+          s.stack = state.stack.slice(0);
+        for (var pers = state.persistentStates; pers; pers = pers.next)
+          s.persistentStates = {mode: pers.mode,
+                                spec: pers.spec,
+                                state: pers.state == state.localState ? s.localState : CodeMirror.copyState(pers.mode, pers.state),
+                                next: s.persistentStates};
+        return s;
+      },
+      token: tokenFunction(states_, config),
+      innerMode: function(state) { return state.local && {mode: state.local.mode, state: state.localState}; },
+      indent: indentFunction(states_, meta)
+    };
+    if (meta) for (var prop in meta) if (meta.hasOwnProperty(prop))
+      mode[prop] = meta[prop];
+    return mode;
+  };
+
+  function ensureState(states, name) {
+    if (!states.hasOwnProperty(name))
+      throw new Error("Undefined state " + name + "in simple mode");
+  }
+
+  function toRegex(val, caret) {
+    if (!val) return /(?:)/;
+    var flags = "";
+    if (val instanceof RegExp) {
+      if (val.ignoreCase) flags = "i";
+      val = val.source;
+    } else {
+      val = String(val);
+    }
+    return new RegExp((caret === false ? "" : "^") + "(?:" + val + ")", flags);
+  }
+
+  function asToken(val) {
+    if (!val) return null;
+    if (typeof val == "string") return val.replace(/\./g, " ");
+    var result = [];
+    for (var i = 0; i < val.length; i++)
+      result.push(val[i] && val[i].replace(/\./g, " "));
+    return result;
+  }
+
+  function Rule(data, states) {
+    if (data.next || data.push) ensureState(states, data.next || data.push);
+    this.regex = toRegex(data.regex);
+    this.token = asToken(data.token);
+    this.data = data;
+  }
+
+  function tokenFunction(states, config) {
+    return function(stream, state) {
+      if (state.pending) {
+        var pend = state.pending.shift();
+        if (state.pending.length == 0) state.pending = null;
+        stream.pos += pend.text.length;
+        return pend.token;
+      }
+
+      if (state.local) {
+        if (state.local.end && stream.match(state.local.end)) {
+          var tok = state.local.endToken || null;
+          state.local = state.localState = null;
+          return tok;
+        } else {
+          var tok = state.local.mode.token(stream, state.localState), m;
+          if (state.local.endScan && (m = state.local.endScan.exec(stream.current())))
+            stream.pos = stream.start + m.index;
+          return tok;
+        }
+      }
+
+      var curState = states[state.state];
+      for (var i = 0; i < curState.length; i++) {
+        var rule = curState[i];
+        var matches = (!rule.data.sol || stream.sol()) && stream.match(rule.regex);
+        if (matches) {
+          if (rule.data.next) {
+            state.state = rule.data.next;
+          } else if (rule.data.push) {
+            (state.stack || (state.stack = [])).push(state.state);
+            state.state = rule.data.push;
+          } else if (rule.data.pop && state.stack && state.stack.length) {
+            state.state = state.stack.pop();
+          }
+
+          if (rule.data.mode)
+            enterLocalMode(config, state, rule.data.mode, rule.token);
+          if (rule.data.indent)
+            state.indent.push(stream.indentation() + config.indentUnit);
+          if (rule.data.dedent)
+            state.indent.pop();
+          if (matches.length > 2) {
+            state.pending = [];
+            for (var j = 2; j < matches.length; j++)
+              if (matches[j])
+                state.pending.push({text: matches[j], token: rule.token[j - 1]});
+            stream.backUp(matches[0].length - (matches[1] ? matches[1].length : 0));
+            return rule.token[0];
+          } else if (rule.token && rule.token.join) {
+            return rule.token[0];
+          } else {
+            return rule.token;
+          }
+        }
+      }
+      stream.next();
+      return null;
+    };
+  }
+
+  function cmp(a, b) {
+    if (a === b) return true;
+    if (!a || typeof a != "object" || !b || typeof b != "object") return false;
+    var props = 0;
+    for (var prop in a) if (a.hasOwnProperty(prop)) {
+      if (!b.hasOwnProperty(prop) || !cmp(a[prop], b[prop])) return false;
+      props++;
+    }
+    for (var prop in b) if (b.hasOwnProperty(prop)) props--;
+    return props == 0;
+  }
+
+  function enterLocalMode(config, state, spec, token) {
+    var pers;
+    if (spec.persistent) for (var p = state.persistentStates; p && !pers; p = p.next)
+      if (spec.spec ? cmp(spec.spec, p.spec) : spec.mode == p.mode) pers = p;
+    var mode = pers ? pers.mode : spec.mode || CodeMirror.getMode(config, spec.spec);
+    var lState = pers ? pers.state : CodeMirror.startState(mode);
+    if (spec.persistent && !pers)
+      state.persistentStates = {mode: mode, spec: spec.spec, state: lState, next: state.persistentStates};
+
+    state.localState = lState;
+    state.local = {mode: mode,
+                   end: spec.end && toRegex(spec.end),
+                   endScan: spec.end && spec.forceEnd !== false && toRegex(spec.end, false),
+                   endToken: token && token.join ? token[token.length - 1] : token};
+  }
+
+  function indexOf(val, arr) {
+    for (var i = 0; i < arr.length; i++) if (arr[i] === val) return true;
+  }
+
+  function indentFunction(states, meta) {
+    return function(state, textAfter, line) {
+      if (state.local && state.local.mode.indent)
+        return state.local.mode.indent(state.localState, textAfter, line);
+      if (state.indent == null || state.local || meta.dontIndentStates && indexOf(state.state, meta.dontIndentStates) > -1)
+        return CodeMirror.Pass;
+
+      var pos = state.indent.length - 1, rules = states[state.state];
+      scan: for (;;) {
+        for (var i = 0; i < rules.length; i++) {
+          var rule = rules[i];
+          if (rule.data.dedent && rule.data.dedentIfLineStart !== false) {
+            var m = rule.regex.exec(textAfter);
+            if (m && m[0]) {
+              pos--;
+              if (rule.next || rule.push) rules = states[rule.next || rule.push];
+              textAfter = textAfter.slice(m[0].length);
+              continue scan;
+            }
+          }
+        }
+        break;
+      }
+      return pos < 0 ? 0 : state.indent[pos];
+    };
+  }
+});

+ 40 - 0
public/theme/material/editor/lib/codemirror/addon/runmode/colorize.js

@@ -0,0 +1,40 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"), require("./runmode"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror", "./runmode"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  var isBlock = /^(p|li|div|h\\d|pre|blockquote|td)$/;
+
+  function textContent(node, out) {
+    if (node.nodeType == 3) return out.push(node.nodeValue);
+    for (var ch = node.firstChild; ch; ch = ch.nextSibling) {
+      textContent(ch, out);
+      if (isBlock.test(node.nodeType)) out.push("\n");
+    }
+  }
+
+  CodeMirror.colorize = function(collection, defaultMode) {
+    if (!collection) collection = document.body.getElementsByTagName("pre");
+
+    for (var i = 0; i < collection.length; ++i) {
+      var node = collection[i];
+      var mode = node.getAttribute("data-lang") || defaultMode;
+      if (!mode) continue;
+
+      var text = [];
+      textContent(node, text);
+      node.innerHTML = "";
+      CodeMirror.runMode(text.join(""), mode, node);
+
+      node.className += " cm-s-default";
+    }
+  };
+});

+ 157 - 0
public/theme/material/editor/lib/codemirror/addon/runmode/runmode-standalone.js

@@ -0,0 +1,157 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+window.CodeMirror = {};
+
+(function() {
+"use strict";
+
+function splitLines(string){ return string.split(/\r?\n|\r/); };
+
+function StringStream(string) {
+  this.pos = this.start = 0;
+  this.string = string;
+  this.lineStart = 0;
+}
+StringStream.prototype = {
+  eol: function() {return this.pos >= this.string.length;},
+  sol: function() {return this.pos == 0;},
+  peek: function() {return this.string.charAt(this.pos) || null;},
+  next: function() {
+    if (this.pos < this.string.length)
+      return this.string.charAt(this.pos++);
+  },
+  eat: function(match) {
+    var ch = this.string.charAt(this.pos);
+    if (typeof match == "string") var ok = ch == match;
+    else var ok = ch && (match.test ? match.test(ch) : match(ch));
+    if (ok) {++this.pos; return ch;}
+  },
+  eatWhile: function(match) {
+    var start = this.pos;
+    while (this.eat(match)){}
+    return this.pos > start;
+  },
+  eatSpace: function() {
+    var start = this.pos;
+    while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos;
+    return this.pos > start;
+  },
+  skipToEnd: function() {this.pos = this.string.length;},
+  skipTo: function(ch) {
+    var found = this.string.indexOf(ch, this.pos);
+    if (found > -1) {this.pos = found; return true;}
+  },
+  backUp: function(n) {this.pos -= n;},
+  column: function() {return this.start - this.lineStart;},
+  indentation: function() {return 0;},
+  match: function(pattern, consume, caseInsensitive) {
+    if (typeof pattern == "string") {
+      var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;};
+      var substr = this.string.substr(this.pos, pattern.length);
+      if (cased(substr) == cased(pattern)) {
+        if (consume !== false) this.pos += pattern.length;
+        return true;
+      }
+    } else {
+      var match = this.string.slice(this.pos).match(pattern);
+      if (match && match.index > 0) return null;
+      if (match && consume !== false) this.pos += match[0].length;
+      return match;
+    }
+  },
+  current: function(){return this.string.slice(this.start, this.pos);},
+  hideFirstChars: function(n, inner) {
+    this.lineStart += n;
+    try { return inner(); }
+    finally { this.lineStart -= n; }
+  }
+};
+CodeMirror.StringStream = StringStream;
+
+CodeMirror.startState = function (mode, a1, a2) {
+  return mode.startState ? mode.startState(a1, a2) : true;
+};
+
+var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {};
+CodeMirror.defineMode = function (name, mode) {
+  if (arguments.length > 2)
+    mode.dependencies = Array.prototype.slice.call(arguments, 2);
+  modes[name] = mode;
+};
+CodeMirror.defineMIME = function (mime, spec) { mimeModes[mime] = spec; };
+CodeMirror.resolveMode = function(spec) {
+  if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) {
+    spec = mimeModes[spec];
+  } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) {
+    spec = mimeModes[spec.name];
+  }
+  if (typeof spec == "string") return {name: spec};
+  else return spec || {name: "null"};
+};
+CodeMirror.getMode = function (options, spec) {
+  spec = CodeMirror.resolveMode(spec);
+  var mfactory = modes[spec.name];
+  if (!mfactory) throw new Error("Unknown mode: " + spec);
+  return mfactory(options, spec);
+};
+CodeMirror.registerHelper = CodeMirror.registerGlobalHelper = Math.min;
+CodeMirror.defineMode("null", function() {
+  return {token: function(stream) {stream.skipToEnd();}};
+});
+CodeMirror.defineMIME("text/plain", "null");
+
+CodeMirror.runMode = function (string, modespec, callback, options) {
+  var mode = CodeMirror.getMode({ indentUnit: 2 }, modespec);
+
+  if (callback.nodeType == 1) {
+    var tabSize = (options && options.tabSize) || 4;
+    var node = callback, col = 0;
+    node.innerHTML = "";
+    callback = function (text, style) {
+      if (text == "\n") {
+        node.appendChild(document.createElement("br"));
+        col = 0;
+        return;
+      }
+      var content = "";
+      // replace tabs
+      for (var pos = 0; ;) {
+        var idx = text.indexOf("\t", pos);
+        if (idx == -1) {
+          content += text.slice(pos);
+          col += text.length - pos;
+          break;
+        } else {
+          col += idx - pos;
+          content += text.slice(pos, idx);
+          var size = tabSize - col % tabSize;
+          col += size;
+          for (var i = 0; i < size; ++i) content += " ";
+          pos = idx + 1;
+        }
+      }
+
+      if (style) {
+        var sp = node.appendChild(document.createElement("span"));
+        sp.className = "cm-" + style.replace(/ +/g, " cm-");
+        sp.appendChild(document.createTextNode(content));
+      } else {
+        node.appendChild(document.createTextNode(content));
+      }
+    };
+  }
+
+  var lines = splitLines(string), state = (options && options.state) || CodeMirror.startState(mode);
+  for (var i = 0, e = lines.length; i < e; ++i) {
+    if (i) callback("\n");
+    var stream = new CodeMirror.StringStream(lines[i]);
+    if (!stream.string && mode.blankLine) mode.blankLine(state);
+    while (!stream.eol()) {
+      var style = mode.token(stream, state);
+      callback(stream.current(), style, i, stream.start, state);
+      stream.start = stream.pos;
+    }
+  }
+};
+})();

+ 72 - 0
public/theme/material/editor/lib/codemirror/addon/runmode/runmode.js

@@ -0,0 +1,72 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+"use strict";
+
+CodeMirror.runMode = function(string, modespec, callback, options) {
+  var mode = CodeMirror.getMode(CodeMirror.defaults, modespec);
+  var ie = /MSIE \d/.test(navigator.userAgent);
+  var ie_lt9 = ie && (document.documentMode == null || document.documentMode < 9);
+
+  if (callback.nodeType == 1) {
+    var tabSize = (options && options.tabSize) || CodeMirror.defaults.tabSize;
+    var node = callback, col = 0;
+    node.innerHTML = "";
+    callback = function(text, style) {
+      if (text == "\n") {
+        // Emitting LF or CRLF on IE8 or earlier results in an incorrect display.
+        // Emitting a carriage return makes everything ok.
+        node.appendChild(document.createTextNode(ie_lt9 ? '\r' : text));
+        col = 0;
+        return;
+      }
+      var content = "";
+      // replace tabs
+      for (var pos = 0;;) {
+        var idx = text.indexOf("\t", pos);
+        if (idx == -1) {
+          content += text.slice(pos);
+          col += text.length - pos;
+          break;
+        } else {
+          col += idx - pos;
+          content += text.slice(pos, idx);
+          var size = tabSize - col % tabSize;
+          col += size;
+          for (var i = 0; i < size; ++i) content += " ";
+          pos = idx + 1;
+        }
+      }
+
+      if (style) {
+        var sp = node.appendChild(document.createElement("span"));
+        sp.className = "cm-" + style.replace(/ +/g, " cm-");
+        sp.appendChild(document.createTextNode(content));
+      } else {
+        node.appendChild(document.createTextNode(content));
+      }
+    };
+  }
+
+  var lines = CodeMirror.splitLines(string), state = (options && options.state) || CodeMirror.startState(mode);
+  for (var i = 0, e = lines.length; i < e; ++i) {
+    if (i) callback("\n");
+    var stream = new CodeMirror.StringStream(lines[i]);
+    if (!stream.string && mode.blankLine) mode.blankLine(state);
+    while (!stream.eol()) {
+      var style = mode.token(stream, state);
+      callback(stream.current(), style, i, stream.start, state);
+      stream.start = stream.pos;
+    }
+  }
+};
+
+});

+ 120 - 0
public/theme/material/editor/lib/codemirror/addon/runmode/runmode.node.js

@@ -0,0 +1,120 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+/* Just enough of CodeMirror to run runMode under node.js */
+
+// declare global: StringStream
+
+function splitLines(string){ return string.split(/\r?\n|\r/); };
+
+function StringStream(string) {
+  this.pos = this.start = 0;
+  this.string = string;
+  this.lineStart = 0;
+}
+StringStream.prototype = {
+  eol: function() {return this.pos >= this.string.length;},
+  sol: function() {return this.pos == 0;},
+  peek: function() {return this.string.charAt(this.pos) || null;},
+  next: function() {
+    if (this.pos < this.string.length)
+      return this.string.charAt(this.pos++);
+  },
+  eat: function(match) {
+    var ch = this.string.charAt(this.pos);
+    if (typeof match == "string") var ok = ch == match;
+    else var ok = ch && (match.test ? match.test(ch) : match(ch));
+    if (ok) {++this.pos; return ch;}
+  },
+  eatWhile: function(match) {
+    var start = this.pos;
+    while (this.eat(match)){}
+    return this.pos > start;
+  },
+  eatSpace: function() {
+    var start = this.pos;
+    while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos;
+    return this.pos > start;
+  },
+  skipToEnd: function() {this.pos = this.string.length;},
+  skipTo: function(ch) {
+    var found = this.string.indexOf(ch, this.pos);
+    if (found > -1) {this.pos = found; return true;}
+  },
+  backUp: function(n) {this.pos -= n;},
+  column: function() {return this.start - this.lineStart;},
+  indentation: function() {return 0;},
+  match: function(pattern, consume, caseInsensitive) {
+    if (typeof pattern == "string") {
+      var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;};
+      var substr = this.string.substr(this.pos, pattern.length);
+      if (cased(substr) == cased(pattern)) {
+        if (consume !== false) this.pos += pattern.length;
+        return true;
+      }
+    } else {
+      var match = this.string.slice(this.pos).match(pattern);
+      if (match && match.index > 0) return null;
+      if (match && consume !== false) this.pos += match[0].length;
+      return match;
+    }
+  },
+  current: function(){return this.string.slice(this.start, this.pos);},
+  hideFirstChars: function(n, inner) {
+    this.lineStart += n;
+    try { return inner(); }
+    finally { this.lineStart -= n; }
+  }
+};
+exports.StringStream = StringStream;
+
+exports.startState = function(mode, a1, a2) {
+  return mode.startState ? mode.startState(a1, a2) : true;
+};
+
+var modes = exports.modes = {}, mimeModes = exports.mimeModes = {};
+exports.defineMode = function(name, mode) {
+  if (arguments.length > 2)
+    mode.dependencies = Array.prototype.slice.call(arguments, 2);
+  modes[name] = mode;
+};
+exports.defineMIME = function(mime, spec) { mimeModes[mime] = spec; };
+
+exports.defineMode("null", function() {
+  return {token: function(stream) {stream.skipToEnd();}};
+});
+exports.defineMIME("text/plain", "null");
+
+exports.resolveMode = function(spec) {
+  if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) {
+    spec = mimeModes[spec];
+  } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) {
+    spec = mimeModes[spec.name];
+  }
+  if (typeof spec == "string") return {name: spec};
+  else return spec || {name: "null"};
+};
+exports.getMode = function(options, spec) {
+  spec = exports.resolveMode(spec);
+  var mfactory = modes[spec.name];
+  if (!mfactory) throw new Error("Unknown mode: " + spec);
+  return mfactory(options, spec);
+};
+exports.registerHelper = exports.registerGlobalHelper = Math.min;
+
+exports.runMode = function(string, modespec, callback, options) {
+  var mode = exports.getMode({indentUnit: 2}, modespec);
+  var lines = splitLines(string), state = (options && options.state) || exports.startState(mode);
+  for (var i = 0, e = lines.length; i < e; ++i) {
+    if (i) callback("\n");
+    var stream = new exports.StringStream(lines[i]);
+    if (!stream.string && mode.blankLine) mode.blankLine(state);
+    while (!stream.eol()) {
+      var style = mode.token(stream, state);
+      callback(stream.current(), style, i, stream.start, state);
+      stream.start = stream.pos;
+    }
+  }
+};
+
+require.cache[require.resolve("../../lib/codemirror")] = require.cache[require.resolve("./runmode.node")];

+ 100 - 0
public/theme/material/editor/lib/codemirror/addon/scroll/annotatescrollbar.js

@@ -0,0 +1,100 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  CodeMirror.defineExtension("annotateScrollbar", function(options) {
+    if (typeof options == "string") options = {className: options};
+    return new Annotation(this, options);
+  });
+
+  CodeMirror.defineOption("scrollButtonHeight", 0);
+
+  function Annotation(cm, options) {
+    this.cm = cm;
+    this.options = options;
+    this.buttonHeight = options.scrollButtonHeight || cm.getOption("scrollButtonHeight");
+    this.annotations = [];
+    this.doRedraw = this.doUpdate = null;
+    this.div = cm.getWrapperElement().appendChild(document.createElement("div"));
+    this.div.style.cssText = "position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none";
+    this.computeScale();
+
+    function scheduleRedraw(delay) {
+      clearTimeout(self.doRedraw);
+      self.doRedraw = setTimeout(function() { self.redraw(); }, delay);
+    }
+
+    var self = this;
+    cm.on("refresh", this.resizeHandler = function() {
+      clearTimeout(self.doUpdate);
+      self.doUpdate = setTimeout(function() {
+        if (self.computeScale()) scheduleRedraw(20);
+      }, 100);
+    });
+    cm.on("markerAdded", this.resizeHandler);
+    cm.on("markerCleared", this.resizeHandler);
+    if (options.listenForChanges !== false)
+      cm.on("change", this.changeHandler = function() {
+        scheduleRedraw(250);
+      });
+  }
+
+  Annotation.prototype.computeScale = function() {
+    var cm = this.cm;
+    var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) /
+      cm.heightAtLine(cm.lastLine() + 1, "local");
+    if (hScale != this.hScale) {
+      this.hScale = hScale;
+      return true;
+    }
+  };
+
+  Annotation.prototype.update = function(annotations) {
+    this.annotations = annotations;
+    this.redraw();
+  };
+
+  Annotation.prototype.redraw = function(compute) {
+    if (compute !== false) this.computeScale();
+    var cm = this.cm, hScale = this.hScale;
+
+    var frag = document.createDocumentFragment(), anns = this.annotations;
+    if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) {
+      var ann = anns[i];
+      var top = nextTop || cm.charCoords(ann.from, "local").top * hScale;
+      var bottom = cm.charCoords(ann.to, "local").bottom * hScale;
+      while (i < anns.length - 1) {
+        nextTop = cm.charCoords(anns[i + 1].from, "local").top * hScale;
+        if (nextTop > bottom + .9) break;
+        ann = anns[++i];
+        bottom = cm.charCoords(ann.to, "local").bottom * hScale;
+      }
+      if (bottom == top) continue;
+      var height = Math.max(bottom - top, 3);
+
+      var elt = frag.appendChild(document.createElement("div"));
+      elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: "
+        + (top + this.buttonHeight) + "px; height: " + height + "px";
+      elt.className = this.options.className;
+    }
+    this.div.textContent = "";
+    this.div.appendChild(frag);
+  };
+
+  Annotation.prototype.clear = function() {
+    this.cm.off("refresh", this.resizeHandler);
+    this.cm.off("markerAdded", this.resizeHandler);
+    this.cm.off("markerCleared", this.resizeHandler);
+    if (this.changeHandler) this.cm.off("change", this.changeHandler);
+    this.div.parentNode.removeChild(this.div);
+  };
+});

+ 46 - 0
public/theme/material/editor/lib/codemirror/addon/scroll/scrollpastend.js

@@ -0,0 +1,46 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  CodeMirror.defineOption("scrollPastEnd", false, function(cm, val, old) {
+    if (old && old != CodeMirror.Init) {
+      cm.off("change", onChange);
+      cm.off("refresh", updateBottomMargin);
+      cm.display.lineSpace.parentNode.style.paddingBottom = "";
+      cm.state.scrollPastEndPadding = null;
+    }
+    if (val) {
+      cm.on("change", onChange);
+      cm.on("refresh", updateBottomMargin);
+      updateBottomMargin(cm);
+    }
+  });
+
+  function onChange(cm, change) {
+    if (CodeMirror.changeEnd(change).line == cm.lastLine())
+      updateBottomMargin(cm);
+  }
+
+  function updateBottomMargin(cm) {
+    var padding = "";
+    if (cm.lineCount() > 1) {
+      var totalH = cm.display.scroller.clientHeight - 30,
+          lastLineH = cm.getLineHandle(cm.lastLine()).height;
+      padding = (totalH - lastLineH) + "px";
+    }
+    if (cm.state.scrollPastEndPadding != padding) {
+      cm.state.scrollPastEndPadding = padding;
+      cm.display.lineSpace.parentNode.style.paddingBottom = padding;
+      cm.setSize();
+    }
+  }
+});

+ 66 - 0
public/theme/material/editor/lib/codemirror/addon/scroll/simplescrollbars.css

@@ -0,0 +1,66 @@
+.CodeMirror-simplescroll-horizontal div, .CodeMirror-simplescroll-vertical div {
+  position: absolute;
+  background: #ccc;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+  border: 1px solid #bbb;
+  border-radius: 2px;
+}
+
+.CodeMirror-simplescroll-horizontal, .CodeMirror-simplescroll-vertical {
+  position: absolute;
+  z-index: 6;
+  background: #eee;
+}
+
+.CodeMirror-simplescroll-horizontal {
+  bottom: 0; left: 0;
+  height: 8px;
+}
+.CodeMirror-simplescroll-horizontal div {
+  bottom: 0;
+  height: 100%;
+}
+
+.CodeMirror-simplescroll-vertical {
+  right: 0; top: 0;
+  width: 8px;
+}
+.CodeMirror-simplescroll-vertical div {
+  right: 0;
+  width: 100%;
+}
+
+
+.CodeMirror-overlayscroll .CodeMirror-scrollbar-filler, .CodeMirror-overlayscroll .CodeMirror-gutter-filler {
+  display: none;
+}
+
+.CodeMirror-overlayscroll-horizontal div, .CodeMirror-overlayscroll-vertical div {
+  position: absolute;
+  background: #bcd;
+  border-radius: 3px;
+}
+
+.CodeMirror-overlayscroll-horizontal, .CodeMirror-overlayscroll-vertical {
+  position: absolute;
+  z-index: 6;
+}
+
+.CodeMirror-overlayscroll-horizontal {
+  bottom: 0; left: 0;
+  height: 6px;
+}
+.CodeMirror-overlayscroll-horizontal div {
+  bottom: 0;
+  height: 100%;
+}
+
+.CodeMirror-overlayscroll-vertical {
+  right: 0; top: 0;
+  width: 6px;
+}
+.CodeMirror-overlayscroll-vertical div {
+  right: 0;
+  width: 100%;
+}

+ 141 - 0
public/theme/material/editor/lib/codemirror/addon/scroll/simplescrollbars.js

@@ -0,0 +1,141 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  function Bar(cls, orientation, scroll) {
+    this.orientation = orientation;
+    this.scroll = scroll;
+    this.screen = this.total = this.size = 1;
+    this.pos = 0;
+
+    this.node = document.createElement("div");
+    this.node.className = cls + "-" + orientation;
+    this.inner = this.node.appendChild(document.createElement("div"));
+
+    var self = this;
+    CodeMirror.on(this.inner, "mousedown", function(e) {
+      if (e.which != 1) return;
+      CodeMirror.e_preventDefault(e);
+      var axis = self.orientation == "horizontal" ? "pageX" : "pageY";
+      var start = e[axis], startpos = self.pos;
+      function done() {
+        CodeMirror.off(document, "mousemove", move);
+        CodeMirror.off(document, "mouseup", done);
+      }
+      function move(e) {
+        if (e.which != 1) return done();
+        self.moveTo(startpos + (e[axis] - start) * (self.total / self.size));
+      }
+      CodeMirror.on(document, "mousemove", move);
+      CodeMirror.on(document, "mouseup", done);
+    });
+
+    CodeMirror.on(this.node, "click", function(e) {
+      CodeMirror.e_preventDefault(e);
+      var innerBox = self.inner.getBoundingClientRect(), where;
+      if (self.orientation == "horizontal")
+        where = e.clientX < innerBox.left ? -1 : e.clientX > innerBox.right ? 1 : 0;
+      else
+        where = e.clientY < innerBox.top ? -1 : e.clientY > innerBox.bottom ? 1 : 0;
+      self.moveTo(self.pos + where * self.screen);
+    });
+
+    function onWheel(e) {
+      var moved = CodeMirror.wheelEventPixels(e)[self.orientation == "horizontal" ? "x" : "y"];
+      var oldPos = self.pos;
+      self.moveTo(self.pos + moved);
+      if (self.pos != oldPos) CodeMirror.e_preventDefault(e);
+    }
+    CodeMirror.on(this.node, "mousewheel", onWheel);
+    CodeMirror.on(this.node, "DOMMouseScroll", onWheel);
+  }
+
+  Bar.prototype.moveTo = function(pos, update) {
+    if (pos < 0) pos = 0;
+    if (pos > this.total - this.screen) pos = this.total - this.screen;
+    if (pos == this.pos) return;
+    this.pos = pos;
+    this.inner.style[this.orientation == "horizontal" ? "left" : "top"] =
+      (pos * (this.size / this.total)) + "px";
+    if (update !== false) this.scroll(pos, this.orientation);
+  };
+
+  Bar.prototype.update = function(scrollSize, clientSize, barSize) {
+    this.screen = clientSize;
+    this.total = scrollSize;
+    this.size = barSize;
+
+    // FIXME clip to min size?
+    this.inner.style[this.orientation == "horizontal" ? "width" : "height"] =
+      this.screen * (this.size / this.total) + "px";
+    this.inner.style[this.orientation == "horizontal" ? "left" : "top"] =
+      this.pos * (this.size / this.total) + "px";
+  };
+
+  function SimpleScrollbars(cls, place, scroll) {
+    this.addClass = cls;
+    this.horiz = new Bar(cls, "horizontal", scroll);
+    place(this.horiz.node);
+    this.vert = new Bar(cls, "vertical", scroll);
+    place(this.vert.node);
+    this.width = null;
+  }
+
+  SimpleScrollbars.prototype.update = function(measure) {
+    if (this.width == null) {
+      var style = window.getComputedStyle ? window.getComputedStyle(this.horiz.node) : this.horiz.node.currentStyle;
+      if (style) this.width = parseInt(style.height);
+    }
+    var width = this.width || 0;
+
+    var needsH = measure.scrollWidth > measure.clientWidth + 1;
+    var needsV = measure.scrollHeight > measure.clientHeight + 1;
+    this.vert.node.style.display = needsV ? "block" : "none";
+    this.horiz.node.style.display = needsH ? "block" : "none";
+
+    if (needsV) {
+      this.vert.update(measure.scrollHeight, measure.clientHeight,
+                       measure.viewHeight - (needsH ? width : 0));
+      this.vert.node.style.display = "block";
+      this.vert.node.style.bottom = needsH ? width + "px" : "0";
+    }
+    if (needsH) {
+      this.horiz.update(measure.scrollWidth, measure.clientWidth,
+                        measure.viewWidth - (needsV ? width : 0) - measure.barLeft);
+      this.horiz.node.style.right = needsV ? width + "px" : "0";
+      this.horiz.node.style.left = measure.barLeft + "px";
+    }
+
+    return {right: needsV ? width : 0, bottom: needsH ? width : 0};
+  };
+
+  SimpleScrollbars.prototype.setScrollTop = function(pos) {
+    this.vert.moveTo(pos, false);
+  };
+
+  SimpleScrollbars.prototype.setScrollLeft = function(pos) {
+    this.horiz.moveTo(pos, false);
+  };
+
+  SimpleScrollbars.prototype.clear = function() {
+    var parent = this.horiz.node.parentNode;
+    parent.removeChild(this.horiz.node);
+    parent.removeChild(this.vert.node);
+  };
+
+  CodeMirror.scrollbarModel.simple = function(place, scroll) {
+    return new SimpleScrollbars("CodeMirror-simplescroll", place, scroll);
+  };
+  CodeMirror.scrollbarModel.overlay = function(place, scroll) {
+    return new SimpleScrollbars("CodeMirror-overlayscroll", place, scroll);
+  };
+});

+ 128 - 0
public/theme/material/editor/lib/codemirror/addon/search/match-highlighter.js

@@ -0,0 +1,128 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Highlighting text that matches the selection
+//
+// Defines an option highlightSelectionMatches, which, when enabled,
+// will style strings that match the selection throughout the
+// document.
+//
+// The option can be set to true to simply enable it, or to a
+// {minChars, style, wordsOnly, showToken, delay} object to explicitly
+// configure it. minChars is the minimum amount of characters that should be
+// selected for the behavior to occur, and style is the token style to
+// apply to the matches. This will be prefixed by "cm-" to create an
+// actual CSS class name. If wordsOnly is enabled, the matches will be
+// highlighted only if the selected text is a word. showToken, when enabled,
+// will cause the current token to be highlighted when nothing is selected.
+// delay is used to specify how much time to wait, in milliseconds, before
+// highlighting the matches.
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  var DEFAULT_MIN_CHARS = 2;
+  var DEFAULT_TOKEN_STYLE = "matchhighlight";
+  var DEFAULT_DELAY = 100;
+  var DEFAULT_WORDS_ONLY = false;
+
+  function State(options) {
+    if (typeof options == "object") {
+      this.minChars = options.minChars;
+      this.style = options.style;
+      this.showToken = options.showToken;
+      this.delay = options.delay;
+      this.wordsOnly = options.wordsOnly;
+    }
+    if (this.style == null) this.style = DEFAULT_TOKEN_STYLE;
+    if (this.minChars == null) this.minChars = DEFAULT_MIN_CHARS;
+    if (this.delay == null) this.delay = DEFAULT_DELAY;
+    if (this.wordsOnly == null) this.wordsOnly = DEFAULT_WORDS_ONLY;
+    this.overlay = this.timeout = null;
+  }
+
+  CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
+    if (old && old != CodeMirror.Init) {
+      var over = cm.state.matchHighlighter.overlay;
+      if (over) cm.removeOverlay(over);
+      clearTimeout(cm.state.matchHighlighter.timeout);
+      cm.state.matchHighlighter = null;
+      cm.off("cursorActivity", cursorActivity);
+    }
+    if (val) {
+      cm.state.matchHighlighter = new State(val);
+      highlightMatches(cm);
+      cm.on("cursorActivity", cursorActivity);
+    }
+  });
+
+  function cursorActivity(cm) {
+    var state = cm.state.matchHighlighter;
+    clearTimeout(state.timeout);
+    state.timeout = setTimeout(function() {highlightMatches(cm);}, state.delay);
+  }
+
+  function highlightMatches(cm) {
+    cm.operation(function() {
+      var state = cm.state.matchHighlighter;
+      if (state.overlay) {
+        cm.removeOverlay(state.overlay);
+        state.overlay = null;
+      }
+      if (!cm.somethingSelected() && state.showToken) {
+        var re = state.showToken === true ? /[\w$]/ : state.showToken;
+        var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
+        while (start && re.test(line.charAt(start - 1))) --start;
+        while (end < line.length && re.test(line.charAt(end))) ++end;
+        if (start < end)
+          cm.addOverlay(state.overlay = makeOverlay(line.slice(start, end), re, state.style));
+        return;
+      }
+      var from = cm.getCursor("from"), to = cm.getCursor("to");
+      if (from.line != to.line) return;
+      if (state.wordsOnly && !isWord(cm, from, to)) return;
+      var selection = cm.getRange(from, to).replace(/^\s+|\s+$/g, "");
+      if (selection.length >= state.minChars)
+        cm.addOverlay(state.overlay = makeOverlay(selection, false, state.style));
+    });
+  }
+
+  function isWord(cm, from, to) {
+    var str = cm.getRange(from, to);
+    if (str.match(/^\w+$/) !== null) {
+        if (from.ch > 0) {
+            var pos = {line: from.line, ch: from.ch - 1};
+            var chr = cm.getRange(pos, from);
+            if (chr.match(/\W/) === null) return false;
+        }
+        if (to.ch < cm.getLine(from.line).length) {
+            var pos = {line: to.line, ch: to.ch + 1};
+            var chr = cm.getRange(to, pos);
+            if (chr.match(/\W/) === null) return false;
+        }
+        return true;
+    } else return false;
+  }
+
+  function boundariesAround(stream, re) {
+    return (!stream.start || !re.test(stream.string.charAt(stream.start - 1))) &&
+      (stream.pos == stream.string.length || !re.test(stream.string.charAt(stream.pos)));
+  }
+
+  function makeOverlay(query, hasBoundary, style) {
+    return {token: function(stream) {
+      if (stream.match(query) &&
+          (!hasBoundary || boundariesAround(stream, hasBoundary)))
+        return style;
+      stream.next();
+      stream.skipTo(query.charAt(0)) || stream.skipToEnd();
+    }};
+  }
+});

+ 8 - 0
public/theme/material/editor/lib/codemirror/addon/search/matchesonscrollbar.css

@@ -0,0 +1,8 @@
+.CodeMirror-search-match {
+  background: gold;
+  border-top: 1px solid orange;
+  border-bottom: 1px solid orange;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+  opacity: .5;
+}

+ 95 - 0
public/theme/material/editor/lib/codemirror/addon/search/matchesonscrollbar.js

@@ -0,0 +1,95 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"), require("./searchcursor"), require("../scroll/annotatescrollbar"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror", "./searchcursor", "../scroll/annotatescrollbar"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  CodeMirror.defineExtension("showMatchesOnScrollbar", function(query, caseFold, options) {
+    if (typeof options == "string") options = {className: options};
+    if (!options) options = {};
+    return new SearchAnnotation(this, query, caseFold, options);
+  });
+
+  function SearchAnnotation(cm, query, caseFold, options) {
+    this.cm = cm;
+    var annotateOptions = {listenForChanges: false};
+    for (var prop in options) annotateOptions[prop] = options[prop];
+    if (!annotateOptions.className) annotateOptions.className = "CodeMirror-search-match";
+    this.annotation = cm.annotateScrollbar(annotateOptions);
+    this.query = query;
+    this.caseFold = caseFold;
+    this.gap = {from: cm.firstLine(), to: cm.lastLine() + 1};
+    this.matches = [];
+    this.update = null;
+
+    this.findMatches();
+    this.annotation.update(this.matches);
+
+    var self = this;
+    cm.on("change", this.changeHandler = function(_cm, change) { self.onChange(change); });
+  }
+
+  var MAX_MATCHES = 1000;
+
+  SearchAnnotation.prototype.findMatches = function() {
+    if (!this.gap) return;
+    for (var i = 0; i < this.matches.length; i++) {
+      var match = this.matches[i];
+      if (match.from.line >= this.gap.to) break;
+      if (match.to.line >= this.gap.from) this.matches.splice(i--, 1);
+    }
+    var cursor = this.cm.getSearchCursor(this.query, CodeMirror.Pos(this.gap.from, 0), this.caseFold);
+    while (cursor.findNext()) {
+      var match = {from: cursor.from(), to: cursor.to()};
+      if (match.from.line >= this.gap.to) break;
+      this.matches.splice(i++, 0, match);
+      if (this.matches.length > MAX_MATCHES) break;
+    }
+    this.gap = null;
+  };
+
+  function offsetLine(line, changeStart, sizeChange) {
+    if (line <= changeStart) return line;
+    return Math.max(changeStart, line + sizeChange);
+  }
+
+  SearchAnnotation.prototype.onChange = function(change) {
+    var startLine = change.from.line;
+    var endLine = CodeMirror.changeEnd(change).line;
+    var sizeChange = endLine - change.to.line;
+    if (this.gap) {
+      this.gap.from = Math.min(offsetLine(this.gap.from, startLine, sizeChange), change.from.line);
+      this.gap.to = Math.max(offsetLine(this.gap.to, startLine, sizeChange), change.from.line);
+    } else {
+      this.gap = {from: change.from.line, to: endLine + 1};
+    }
+
+    if (sizeChange) for (var i = 0; i < this.matches.length; i++) {
+      var match = this.matches[i];
+      var newFrom = offsetLine(match.from.line, startLine, sizeChange);
+      if (newFrom != match.from.line) match.from = CodeMirror.Pos(newFrom, match.from.ch);
+      var newTo = offsetLine(match.to.line, startLine, sizeChange);
+      if (newTo != match.to.line) match.to = CodeMirror.Pos(newTo, match.to.ch);
+    }
+    clearTimeout(this.update);
+    var self = this;
+    this.update = setTimeout(function() { self.updateAfterChange(); }, 250);
+  };
+
+  SearchAnnotation.prototype.updateAfterChange = function() {
+    this.findMatches();
+    this.annotation.update(this.matches);
+  };
+
+  SearchAnnotation.prototype.clear = function() {
+    this.cm.off("change", this.changeHandler);
+    this.annotation.clear();
+  };
+});

+ 164 - 0
public/theme/material/editor/lib/codemirror/addon/search/search.js

@@ -0,0 +1,164 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Define search commands. Depends on dialog.js or another
+// implementation of the openDialog method.
+
+// Replace works a little oddly -- it will do the replace on the next
+// Ctrl-G (or whatever is bound to findNext) press. You prevent a
+// replace by making sure the match is no longer selected when hitting
+// Ctrl-G.
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+  function searchOverlay(query, caseInsensitive) {
+    if (typeof query == "string")
+      query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g");
+    else if (!query.global)
+      query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");
+
+    return {token: function(stream) {
+      query.lastIndex = stream.pos;
+      var match = query.exec(stream.string);
+      if (match && match.index == stream.pos) {
+        stream.pos += match[0].length;
+        return "searching";
+      } else if (match) {
+        stream.pos = match.index;
+      } else {
+        stream.skipToEnd();
+      }
+    }};
+  }
+
+  function SearchState() {
+    this.posFrom = this.posTo = this.query = null;
+    this.overlay = null;
+  }
+  function getSearchState(cm) {
+    return cm.state.search || (cm.state.search = new SearchState());
+  }
+  function queryCaseInsensitive(query) {
+    return typeof query == "string" && query == query.toLowerCase();
+  }
+  function getSearchCursor(cm, query, pos) {
+    // Heuristic: if the query string is all lowercase, do a case insensitive search.
+    return cm.getSearchCursor(query, pos, queryCaseInsensitive(query));
+  }
+  function dialog(cm, text, shortText, deflt, f) {
+    if (cm.openDialog) cm.openDialog(text, f, {value: deflt});
+    else f(prompt(shortText, deflt));
+  }
+  function confirmDialog(cm, text, shortText, fs) {
+    if (cm.openConfirm) cm.openConfirm(text, fs);
+    else if (confirm(shortText)) fs[0]();
+  }
+  function parseQuery(query) {
+    var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
+    if (isRE) {
+      try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); }
+      catch(e) {} // Not a regular expression after all, do a string search
+    }
+    if (typeof query == "string" ? query == "" : query.test(""))
+      query = /x^/;
+    return query;
+  }
+  var queryDialog =
+    'Search: <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
+  function doSearch(cm, rev) {
+    var state = getSearchState(cm);
+    if (state.query) return findNext(cm, rev);
+    dialog(cm, queryDialog, "Search for:", cm.getSelection(), function(query) {
+      cm.operation(function() {
+        if (!query || state.query) return;
+        state.query = parseQuery(query);
+        cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
+        state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
+        cm.addOverlay(state.overlay);
+        if (cm.showMatchesOnScrollbar) {
+          if (state.annotate) { state.annotate.clear(); state.annotate = null; }
+          state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
+        }
+        state.posFrom = state.posTo = cm.getCursor();
+        findNext(cm, rev);
+      });
+    });
+  }
+  function findNext(cm, rev) {cm.operation(function() {
+    var state = getSearchState(cm);
+    var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
+    if (!cursor.find(rev)) {
+      cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
+      if (!cursor.find(rev)) return;
+    }
+    cm.setSelection(cursor.from(), cursor.to());
+    cm.scrollIntoView({from: cursor.from(), to: cursor.to()});
+    state.posFrom = cursor.from(); state.posTo = cursor.to();
+  });}
+  function clearSearch(cm) {cm.operation(function() {
+    var state = getSearchState(cm);
+    if (!state.query) return;
+    state.query = null;
+    cm.removeOverlay(state.overlay);
+    if (state.annotate) { state.annotate.clear(); state.annotate = null; }
+  });}
+
+  var replaceQueryDialog =
+    'Replace: <input type="text" style="width: 10em" class="CodeMirror-search-field"/> <span style="color: #888" class="CodeMirror-search-hint">(Use /re/ syntax for regexp search)</span>';
+  var replacementQueryDialog = 'With: <input type="text" style="width: 10em" class="CodeMirror-search-field"/>';
+  var doReplaceConfirm = "Replace? <button>Yes</button> <button>No</button> <button>Stop</button>";
+  function replace(cm, all) {
+    if (cm.getOption("readOnly")) return;
+    dialog(cm, replaceQueryDialog, "Replace:", cm.getSelection(), function(query) {
+      if (!query) return;
+      query = parseQuery(query);
+      dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
+        if (all) {
+          cm.operation(function() {
+            for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
+              if (typeof query != "string") {
+                var match = cm.getRange(cursor.from(), cursor.to()).match(query);
+                cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
+              } else cursor.replace(text);
+            }
+          });
+        } else {
+          clearSearch(cm);
+          var cursor = getSearchCursor(cm, query, cm.getCursor());
+          var advance = function() {
+            var start = cursor.from(), match;
+            if (!(match = cursor.findNext())) {
+              cursor = getSearchCursor(cm, query);
+              if (!(match = cursor.findNext()) ||
+                  (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
+            }
+            cm.setSelection(cursor.from(), cursor.to());
+            cm.scrollIntoView({from: cursor.from(), to: cursor.to()});
+            confirmDialog(cm, doReplaceConfirm, "Replace?",
+                          [function() {doReplace(match);}, advance]);
+          };
+          var doReplace = function(match) {
+            cursor.replace(typeof query == "string" ? text :
+                           text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
+            advance();
+          };
+          advance();
+        }
+      });
+    });
+  }
+
+  CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
+  CodeMirror.commands.findNext = doSearch;
+  CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
+  CodeMirror.commands.clearSearch = clearSearch;
+  CodeMirror.commands.replace = replace;
+  CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
+});

+ 189 - 0
public/theme/material/editor/lib/codemirror/addon/search/searchcursor.js

@@ -0,0 +1,189 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+  var Pos = CodeMirror.Pos;
+
+  function SearchCursor(doc, query, pos, caseFold) {
+    this.atOccurrence = false; this.doc = doc;
+    if (caseFold == null && typeof query == "string") caseFold = false;
+
+    pos = pos ? doc.clipPos(pos) : Pos(0, 0);
+    this.pos = {from: pos, to: pos};
+
+    // The matches method is filled in based on the type of query.
+    // It takes a position and a direction, and returns an object
+    // describing the next occurrence of the query, or null if no
+    // more matches were found.
+    if (typeof query != "string") { // Regexp match
+      if (!query.global) query = new RegExp(query.source, query.ignoreCase ? "ig" : "g");
+      this.matches = function(reverse, pos) {
+        if (reverse) {
+          query.lastIndex = 0;
+          var line = doc.getLine(pos.line).slice(0, pos.ch), cutOff = 0, match, start;
+          for (;;) {
+            query.lastIndex = cutOff;
+            var newMatch = query.exec(line);
+            if (!newMatch) break;
+            match = newMatch;
+            start = match.index;
+            cutOff = match.index + (match[0].length || 1);
+            if (cutOff == line.length) break;
+          }
+          var matchLen = (match && match[0].length) || 0;
+          if (!matchLen) {
+            if (start == 0 && line.length == 0) {match = undefined;}
+            else if (start != doc.getLine(pos.line).length) {
+              matchLen++;
+            }
+          }
+        } else {
+          query.lastIndex = pos.ch;
+          var line = doc.getLine(pos.line), match = query.exec(line);
+          var matchLen = (match && match[0].length) || 0;
+          var start = match && match.index;
+          if (start + matchLen != line.length && !matchLen) matchLen = 1;
+        }
+        if (match && matchLen)
+          return {from: Pos(pos.line, start),
+                  to: Pos(pos.line, start + matchLen),
+                  match: match};
+      };
+    } else { // String query
+      var origQuery = query;
+      if (caseFold) query = query.toLowerCase();
+      var fold = caseFold ? function(str){return str.toLowerCase();} : function(str){return str;};
+      var target = query.split("\n");
+      // Different methods for single-line and multi-line queries
+      if (target.length == 1) {
+        if (!query.length) {
+          // Empty string would match anything and never progress, so
+          // we define it to match nothing instead.
+          this.matches = function() {};
+        } else {
+          this.matches = function(reverse, pos) {
+            if (reverse) {
+              var orig = doc.getLine(pos.line).slice(0, pos.ch), line = fold(orig);
+              var match = line.lastIndexOf(query);
+              if (match > -1) {
+                match = adjustPos(orig, line, match);
+                return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
+              }
+             } else {
+               var orig = doc.getLine(pos.line).slice(pos.ch), line = fold(orig);
+               var match = line.indexOf(query);
+               if (match > -1) {
+                 match = adjustPos(orig, line, match) + pos.ch;
+                 return {from: Pos(pos.line, match), to: Pos(pos.line, match + origQuery.length)};
+               }
+            }
+          };
+        }
+      } else {
+        var origTarget = origQuery.split("\n");
+        this.matches = function(reverse, pos) {
+          var last = target.length - 1;
+          if (reverse) {
+            if (pos.line - (target.length - 1) < doc.firstLine()) return;
+            if (fold(doc.getLine(pos.line).slice(0, origTarget[last].length)) != target[target.length - 1]) return;
+            var to = Pos(pos.line, origTarget[last].length);
+            for (var ln = pos.line - 1, i = last - 1; i >= 1; --i, --ln)
+              if (target[i] != fold(doc.getLine(ln))) return;
+            var line = doc.getLine(ln), cut = line.length - origTarget[0].length;
+            if (fold(line.slice(cut)) != target[0]) return;
+            return {from: Pos(ln, cut), to: to};
+          } else {
+            if (pos.line + (target.length - 1) > doc.lastLine()) return;
+            var line = doc.getLine(pos.line), cut = line.length - origTarget[0].length;
+            if (fold(line.slice(cut)) != target[0]) return;
+            var from = Pos(pos.line, cut);
+            for (var ln = pos.line + 1, i = 1; i < last; ++i, ++ln)
+              if (target[i] != fold(doc.getLine(ln))) return;
+            if (fold(doc.getLine(ln).slice(0, origTarget[last].length)) != target[last]) return;
+            return {from: from, to: Pos(ln, origTarget[last].length)};
+          }
+        };
+      }
+    }
+  }
+
+  SearchCursor.prototype = {
+    findNext: function() {return this.find(false);},
+    findPrevious: function() {return this.find(true);},
+
+    find: function(reverse) {
+      var self = this, pos = this.doc.clipPos(reverse ? this.pos.from : this.pos.to);
+      function savePosAndFail(line) {
+        var pos = Pos(line, 0);
+        self.pos = {from: pos, to: pos};
+        self.atOccurrence = false;
+        return false;
+      }
+
+      for (;;) {
+        if (this.pos = this.matches(reverse, pos)) {
+          this.atOccurrence = true;
+          return this.pos.match || true;
+        }
+        if (reverse) {
+          if (!pos.line) return savePosAndFail(0);
+          pos = Pos(pos.line-1, this.doc.getLine(pos.line-1).length);
+        }
+        else {
+          var maxLine = this.doc.lineCount();
+          if (pos.line == maxLine - 1) return savePosAndFail(maxLine);
+          pos = Pos(pos.line + 1, 0);
+        }
+      }
+    },
+
+    from: function() {if (this.atOccurrence) return this.pos.from;},
+    to: function() {if (this.atOccurrence) return this.pos.to;},
+
+    replace: function(newText) {
+      if (!this.atOccurrence) return;
+      var lines = CodeMirror.splitLines(newText);
+      this.doc.replaceRange(lines, this.pos.from, this.pos.to);
+      this.pos.to = Pos(this.pos.from.line + lines.length - 1,
+                        lines[lines.length - 1].length + (lines.length == 1 ? this.pos.from.ch : 0));
+    }
+  };
+
+  // Maps a position in a case-folded line back to a position in the original line
+  // (compensating for codepoints increasing in number during folding)
+  function adjustPos(orig, folded, pos) {
+    if (orig.length == folded.length) return pos;
+    for (var pos1 = Math.min(pos, orig.length);;) {
+      var len1 = orig.slice(0, pos1).toLowerCase().length;
+      if (len1 < pos) ++pos1;
+      else if (len1 > pos) --pos1;
+      else return pos1;
+    }
+  }
+
+  CodeMirror.defineExtension("getSearchCursor", function(query, pos, caseFold) {
+    return new SearchCursor(this.doc, query, pos, caseFold);
+  });
+  CodeMirror.defineDocExtension("getSearchCursor", function(query, pos, caseFold) {
+    return new SearchCursor(this, query, pos, caseFold);
+  });
+
+  CodeMirror.defineExtension("selectMatches", function(query, caseFold) {
+    var ranges = [], next;
+    var cur = this.getSearchCursor(query, this.getCursor("from"), caseFold);
+    while (next = cur.findNext()) {
+      if (CodeMirror.cmpPos(cur.to(), this.getCursor("to")) > 0) break;
+      ranges.push({anchor: cur.from(), head: cur.to()});
+    }
+    if (ranges.length)
+      this.setSelections(ranges, 0);
+  });
+});

+ 71 - 0
public/theme/material/editor/lib/codemirror/addon/selection/active-line.js

@@ -0,0 +1,71 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Because sometimes you need to style the cursor's line.
+//
+// Adds an option 'styleActiveLine' which, when enabled, gives the
+// active line's wrapping <div> the CSS class "CodeMirror-activeline",
+// and gives its background <div> the class "CodeMirror-activeline-background".
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+  var WRAP_CLASS = "CodeMirror-activeline";
+  var BACK_CLASS = "CodeMirror-activeline-background";
+
+  CodeMirror.defineOption("styleActiveLine", false, function(cm, val, old) {
+    var prev = old && old != CodeMirror.Init;
+    if (val && !prev) {
+      cm.state.activeLines = [];
+      updateActiveLines(cm, cm.listSelections());
+      cm.on("beforeSelectionChange", selectionChange);
+    } else if (!val && prev) {
+      cm.off("beforeSelectionChange", selectionChange);
+      clearActiveLines(cm);
+      delete cm.state.activeLines;
+    }
+  });
+
+  function clearActiveLines(cm) {
+    for (var i = 0; i < cm.state.activeLines.length; i++) {
+      cm.removeLineClass(cm.state.activeLines[i], "wrap", WRAP_CLASS);
+      cm.removeLineClass(cm.state.activeLines[i], "background", BACK_CLASS);
+    }
+  }
+
+  function sameArray(a, b) {
+    if (a.length != b.length) return false;
+    for (var i = 0; i < a.length; i++)
+      if (a[i] != b[i]) return false;
+    return true;
+  }
+
+  function updateActiveLines(cm, ranges) {
+    var active = [];
+    for (var i = 0; i < ranges.length; i++) {
+      var range = ranges[i];
+      if (!range.empty()) continue;
+      var line = cm.getLineHandleVisualStart(range.head.line);
+      if (active[active.length - 1] != line) active.push(line);
+    }
+    if (sameArray(cm.state.activeLines, active)) return;
+    cm.operation(function() {
+      clearActiveLines(cm);
+      for (var i = 0; i < active.length; i++) {
+        cm.addLineClass(active[i], "wrap", WRAP_CLASS);
+        cm.addLineClass(active[i], "background", BACK_CLASS);
+      }
+      cm.state.activeLines = active;
+    });
+  }
+
+  function selectionChange(cm, sel) {
+    updateActiveLines(cm, sel.ranges);
+  }
+});

+ 118 - 0
public/theme/material/editor/lib/codemirror/addon/selection/mark-selection.js

@@ -0,0 +1,118 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+// Because sometimes you need to mark the selected *text*.
+//
+// Adds an option 'styleSelectedText' which, when enabled, gives
+// selected text the CSS class given as option value, or
+// "CodeMirror-selectedtext" when the value is not a string.
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  CodeMirror.defineOption("styleSelectedText", false, function(cm, val, old) {
+    var prev = old && old != CodeMirror.Init;
+    if (val && !prev) {
+      cm.state.markedSelection = [];
+      cm.state.markedSelectionStyle = typeof val == "string" ? val : "CodeMirror-selectedtext";
+      reset(cm);
+      cm.on("cursorActivity", onCursorActivity);
+      cm.on("change", onChange);
+    } else if (!val && prev) {
+      cm.off("cursorActivity", onCursorActivity);
+      cm.off("change", onChange);
+      clear(cm);
+      cm.state.markedSelection = cm.state.markedSelectionStyle = null;
+    }
+  });
+
+  function onCursorActivity(cm) {
+    cm.operation(function() { update(cm); });
+  }
+
+  function onChange(cm) {
+    if (cm.state.markedSelection.length)
+      cm.operation(function() { clear(cm); });
+  }
+
+  var CHUNK_SIZE = 8;
+  var Pos = CodeMirror.Pos;
+  var cmp = CodeMirror.cmpPos;
+
+  function coverRange(cm, from, to, addAt) {
+    if (cmp(from, to) == 0) return;
+    var array = cm.state.markedSelection;
+    var cls = cm.state.markedSelectionStyle;
+    for (var line = from.line;;) {
+      var start = line == from.line ? from : Pos(line, 0);
+      var endLine = line + CHUNK_SIZE, atEnd = endLine >= to.line;
+      var end = atEnd ? to : Pos(endLine, 0);
+      var mark = cm.markText(start, end, {className: cls});
+      if (addAt == null) array.push(mark);
+      else array.splice(addAt++, 0, mark);
+      if (atEnd) break;
+      line = endLine;
+    }
+  }
+
+  function clear(cm) {
+    var array = cm.state.markedSelection;
+    for (var i = 0; i < array.length; ++i) array[i].clear();
+    array.length = 0;
+  }
+
+  function reset(cm) {
+    clear(cm);
+    var ranges = cm.listSelections();
+    for (var i = 0; i < ranges.length; i++)
+      coverRange(cm, ranges[i].from(), ranges[i].to());
+  }
+
+  function update(cm) {
+    if (!cm.somethingSelected()) return clear(cm);
+    if (cm.listSelections().length > 1) return reset(cm);
+
+    var from = cm.getCursor("start"), to = cm.getCursor("end");
+
+    var array = cm.state.markedSelection;
+    if (!array.length) return coverRange(cm, from, to);
+
+    var coverStart = array[0].find(), coverEnd = array[array.length - 1].find();
+    if (!coverStart || !coverEnd || to.line - from.line < CHUNK_SIZE ||
+        cmp(from, coverEnd.to) >= 0 || cmp(to, coverStart.from) <= 0)
+      return reset(cm);
+
+    while (cmp(from, coverStart.from) > 0) {
+      array.shift().clear();
+      coverStart = array[0].find();
+    }
+    if (cmp(from, coverStart.from) < 0) {
+      if (coverStart.to.line - from.line < CHUNK_SIZE) {
+        array.shift().clear();
+        coverRange(cm, from, coverStart.to, 0);
+      } else {
+        coverRange(cm, from, coverStart.from, 0);
+      }
+    }
+
+    while (cmp(to, coverEnd.to) < 0) {
+      array.pop().clear();
+      coverEnd = array[array.length - 1].find();
+    }
+    if (cmp(to, coverEnd.to) > 0) {
+      if (to.line - coverEnd.from.line < CHUNK_SIZE) {
+        array.pop().clear();
+        coverRange(cm, coverEnd.from, to);
+      } else {
+        coverRange(cm, coverEnd.to, to);
+      }
+    }
+  }
+});

+ 98 - 0
public/theme/material/editor/lib/codemirror/addon/selection/selection-pointer.js

@@ -0,0 +1,98 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+  if (typeof exports == "object" && typeof module == "object") // CommonJS
+    mod(require("../../lib/codemirror"));
+  else if (typeof define == "function" && define.amd) // AMD
+    define(["../../lib/codemirror"], mod);
+  else // Plain browser env
+    mod(CodeMirror);
+})(function(CodeMirror) {
+  "use strict";
+
+  CodeMirror.defineOption("selectionPointer", false, function(cm, val) {
+    var data = cm.state.selectionPointer;
+    if (data) {
+      CodeMirror.off(cm.getWrapperElement(), "mousemove", data.mousemove);
+      CodeMirror.off(cm.getWrapperElement(), "mouseout", data.mouseout);
+      CodeMirror.off(window, "scroll", data.windowScroll);
+      cm.off("cursorActivity", reset);
+      cm.off("scroll", reset);
+      cm.state.selectionPointer = null;
+      cm.display.lineDiv.style.cursor = "";
+    }
+    if (val) {
+      data = cm.state.selectionPointer = {
+        value: typeof val == "string" ? val : "default",
+        mousemove: function(event) { mousemove(cm, event); },
+        mouseout: function(event) { mouseout(cm, event); },
+        windowScroll: function() { reset(cm); },
+        rects: null,
+        mouseX: null, mouseY: null,
+        willUpdate: false
+      };
+      CodeMirror.on(cm.getWrapperElement(), "mousemove", data.mousemove);
+      CodeMirror.on(cm.getWrapperElement(), "mouseout", data.mouseout);
+      CodeMirror.on(window, "scroll", data.windowScroll);
+      cm.on("cursorActivity", reset);
+      cm.on("scroll", reset);
+    }
+  });
+
+  function mousemove(cm, event) {
+    var data = cm.state.selectionPointer;
+    if (event.buttons == null ? event.which : event.buttons) {
+      data.mouseX = data.mouseY = null;
+    } else {
+      data.mouseX = event.clientX;
+      data.mouseY = event.clientY;
+    }
+    scheduleUpdate(cm);
+  }
+
+  function mouseout(cm, event) {
+    if (!cm.getWrapperElement().contains(event.relatedTarget)) {
+      var data = cm.state.selectionPointer;
+      data.mouseX = data.mouseY = null;
+      scheduleUpdate(cm);
+    }
+  }
+
+  function reset(cm) {
+    cm.state.selectionPointer.rects = null;
+    scheduleUpdate(cm);
+  }
+
+  function scheduleUpdate(cm) {
+    if (!cm.state.selectionPointer.willUpdate) {
+      cm.state.selectionPointer.willUpdate = true;
+      setTimeout(function() {
+        update(cm);
+        cm.state.selectionPointer.willUpdate = false;
+      }, 50);
+    }
+  }
+
+  function update(cm) {
+    var data = cm.state.selectionPointer;
+    if (!data) return;
+    if (data.rects == null && data.mouseX != null) {
+      data.rects = [];
+      if (cm.somethingSelected()) {
+        for (var sel = cm.display.selectionDiv.firstChild; sel; sel = sel.nextSibling)
+          data.rects.push(sel.getBoundingClientRect());
+      }
+    }
+    var inside = false;
+    if (data.mouseX != null) for (var i = 0; i < data.rects.length; i++) {
+      var rect = data.rects[i];
+      if (rect.left <= data.mouseX && rect.right >= data.mouseX &&
+          rect.top <= data.mouseY && rect.bottom >= data.mouseY)
+        inside = true;
+    }
+    var cursor = inside ? data.value : "";
+    if (cm.display.lineDiv.style.cursor != cursor)
+      cm.display.lineDiv.style.cursor = cursor;
+  }
+});

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels