├── .bowerrc ├── .gitignore ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── bower.json ├── demo.html ├── lib └── simditor-livemd.js ├── package.json ├── src └── simditor-livemd.coffee └── umd.hbs /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "vendor/bower" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | 17 | .DS_Store 18 | .sass-cache 19 | .grunt 20 | vendor 21 | .bundle 22 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | grunt.initConfig 4 | 5 | pkg: grunt.file.readJSON 'package.json' 6 | 7 | coffee: 8 | src: 9 | options: 10 | bare: true 11 | files: 12 | 'lib/simditor-livemd.js': 'src/simditor-livemd.coffee' 13 | watch: 14 | src: 15 | files: ['src/*.coffee'] 16 | tasks: ['coffee:src', 'umd'] 17 | 18 | umd: 19 | all: 20 | src: 'lib/simditor-livemd.js' 21 | template: 'umd.hbs' 22 | amdModuleId: 'simditor-livemd' 23 | objectToExport: 'SimditorLivemd' 24 | globalAlias: 'SimditorLivemd' 25 | deps: 26 | 'default': ['$', 'SimpleModule', 'Simditor'] 27 | amd: ['jquery', 'simple-module', 'simditor'] 28 | cjs: ['jquery', 'simple-module', 'simditor'] 29 | global: 30 | items: ['jQuery', 'SimpleModule', 'Simditor'] 31 | prefix: '' 32 | 33 | grunt.loadNpmTasks 'grunt-contrib-coffee' 34 | grunt.loadNpmTasks 'grunt-contrib-watch' 35 | grunt.loadNpmTasks 'grunt-umd' 36 | 37 | grunt.registerTask 'default', ['coffee', 'umd', 'watch'] 38 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 彩程设计 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | simditor-livemd 2 | =============== 3 | 4 | [Simditor](http://simditor.tower.im/) 的官方扩展,通过 Markdown 语法快速便捷地输入内容。 5 | 6 | ### 如何使用 7 | 8 | 在 Simditor 的基础上额外引用 simditor-livemd 的脚本。 9 | 10 | ```html 11 | 12 | ``` 13 | 14 | 配置 15 | 16 | ```javascript 17 | new Simditor({ 18 | textarea: textareaElement, 19 | ..., 20 | livemd: true 21 | }) 22 | ``` 23 | 24 | 如果需要禁用某些语法,可以在配置里这样写: 25 | 26 | ```javascript 27 | new Simditor({ 28 | textarea: textareaElement, 29 | ..., 30 | livemd: { 31 | title: false, // 禁用标题 32 | hr: false // 禁用分割线 33 | } 34 | }) 35 | ``` 36 | 37 | ### 语法 38 | 39 | 支持以下 Markdown 语法,在 Simditor 中输入后通过空格或回车触发: 40 | 41 | ```markdown 42 | 标题:##这里是标题 43 | 44 | 引用:>这是一行引用 45 | 46 | 代码:```这是一行代码 47 | 48 | 分割线:*** 或 --- 49 | 50 | 粗体:**粗体文字** 或 __粗体文字__ 51 | 52 | 斜体:*斜体文字* 53 | 54 | 无序列表:*第一行内容 或 +第一行内容 或 -第一行内容 55 | 56 | 有序列表:1.第一行内容 57 | 58 | 图片:![图片](image path) 59 | 60 | 链接:[链接文字](url) 或 61 | ``` 62 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simditor-livemd", 3 | "version": "2.1.1", 4 | "homepage": "https://github.com/mycolorway/simditor-livemd", 5 | "authors": [ 6 | "ruochenlyu " 7 | ], 8 | "description": "a Markdown plugin Simditor", 9 | "main": "lib/simditor-livemd.js", 10 | "keywords": [ 11 | "simditor", 12 | "livemd" 13 | ], 14 | "license": "MIT", 15 | "ignore": [ 16 | "**/.*", 17 | "node_modules", 18 | "vendor", 19 | "Gruntfile.coffee", 20 | "package.json" 21 | ], 22 | "dependencies": { 23 | "jquery": "2.x", 24 | "simditor": "2.3.x" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Simditor 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 41 | 42 |
43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /lib/simditor-livemd.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module unless amdModuleId is set 4 | define('simditor-livemd', ["jquery","simple-module","simditor"], function ($, SimpleModule, Simditor) { 5 | return (root['SimditorLivemd'] = factory($, SimpleModule, Simditor)); 6 | }); 7 | } else if (typeof exports === 'object') { 8 | // Node. Does not work with strict CommonJS, but 9 | // only CommonJS-like environments that support module.exports, 10 | // like Node. 11 | module.exports = factory(require("jquery"),require("simple-module"),require("simditor")); 12 | } else { 13 | root['SimditorLivemd'] = factory(jQuery,SimpleModule,Simditor); 14 | } 15 | }(this, function ($, SimpleModule, Simditor) { 16 | 17 | var SimditorLivemd, 18 | extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, 19 | hasProp = {}.hasOwnProperty; 20 | 21 | SimditorLivemd = (function(superClass) { 22 | extend(SimditorLivemd, superClass); 23 | 24 | function SimditorLivemd() { 25 | return SimditorLivemd.__super__.constructor.apply(this, arguments); 26 | } 27 | 28 | SimditorLivemd.pluginName = 'Livemd'; 29 | 30 | SimditorLivemd.prototype.opts = { 31 | livemd: false 32 | }; 33 | 34 | SimditorLivemd.prototype._init = function() { 35 | var hooks; 36 | if (!this.opts.livemd) { 37 | return; 38 | } 39 | this.editor = this._module; 40 | if (typeof this.opts.livemd === "object") { 41 | hooks = $.extend({}, this.hooks, this.opts.livemd); 42 | } else { 43 | hooks = $.extend({}, this.hooks); 44 | } 45 | return this.editor.on("keypress", (function(_this) { 46 | return function(e) { 47 | var $blockEl, button, cmdEnd, cmdStart, container, content, hook, match, name, range, result, testRange; 48 | if (!(e.which === 32 || e.which === 13)) { 49 | return; 50 | } 51 | range = _this.editor.selection.range(); 52 | container = range != null ? range.commonAncestorContainer : void 0; 53 | if (!(range && range.collapsed && container && container.nodeType === 3 && !$(container).parent("pre").length)) { 54 | return; 55 | } 56 | content = container.textContent; 57 | for (name in hooks) { 58 | hook = hooks[name]; 59 | if (e.which === 13 && !hook.enterKey) { 60 | return; 61 | } 62 | if (!(hook && hook.cmd instanceof RegExp)) { 63 | continue; 64 | } 65 | match = content.match(hook.cmd); 66 | if (!match) { 67 | continue; 68 | } 69 | button = _this.editor.toolbar.findButton(name); 70 | if (button === null || button.disabled) { 71 | continue; 72 | } 73 | if (hook.block) { 74 | $blockEl = _this.editor.selection.blockNodes().last(); 75 | testRange = document.createRange(); 76 | testRange.setStart(container, 0); 77 | testRange.collapse(true); 78 | if (!_this.editor.selection.rangeAtStartOf($blockEl, testRange)) { 79 | continue; 80 | } 81 | } 82 | cmdStart = match.index; 83 | cmdEnd = match[0].length + match.index; 84 | range.setStart(container, cmdStart); 85 | range.setEnd(container, cmdEnd); 86 | if (hook.block) { 87 | range.deleteContents(); 88 | if (_this.editor.util.isEmptyNode($blockEl)) { 89 | $blockEl.append(_this.editor.util.phBr); 90 | } 91 | _this.editor.selection.setRangeAtEndOf($blockEl); 92 | } 93 | result = hook.callback.call(_this, button, hook, range, match, $blockEl); 94 | if ((e.which === 32 || name === "code") && result) { 95 | e.preventDefault(); 96 | } 97 | break; 98 | } 99 | }; 100 | })(this)); 101 | }; 102 | 103 | SimditorLivemd.prototype.hooks = { 104 | title: { 105 | cmd: /^#+/, 106 | block: true, 107 | enterKey: true, 108 | callback: function(button, hook, range, match, $blockEl) { 109 | var level; 110 | level = Math.min(match[0].length, 3); 111 | return button.command("h" + level); 112 | } 113 | }, 114 | blockquote: { 115 | cmd: /^>{1}/, 116 | block: true, 117 | enterKey: true, 118 | callback: function(button, hook, range, match, $blockEl) { 119 | return button.command(); 120 | } 121 | }, 122 | code: { 123 | cmd: /^`{3}/, 124 | block: true, 125 | enterKey: true, 126 | callback: function(button, hook, range, match, $blockEl) { 127 | return button.command(); 128 | } 129 | }, 130 | hr: { 131 | cmd: /^\*{3,}$|^\-{3,}$/, 132 | block: true, 133 | enterKey: true, 134 | callback: function(button, hook, range, match, $blockEl) { 135 | return button.command(); 136 | } 137 | }, 138 | bold: { 139 | cmd: /\*{2}([^\*]+)\*{2}$|_{2}([^_]+)_{2}$/, 140 | block: false, 141 | callback: function(button, hook, range, match) { 142 | var text, textNode; 143 | text = match[1] || match[2]; 144 | textNode = document.createTextNode(text); 145 | this.editor.selection.range(range); 146 | range.deleteContents(); 147 | range.insertNode(textNode); 148 | range.selectNode(textNode); 149 | this.editor.selection.range(range); 150 | document.execCommand("bold"); 151 | this.editor.selection.setRangeAfter(textNode); 152 | document.execCommand("bold"); 153 | this.editor.trigger("valuechanged"); 154 | return this.editor.trigger("selectionchanged"); 155 | } 156 | }, 157 | italic: { 158 | cmd: /\*([^\*]+)\*$/, 159 | block: false, 160 | callback: function(button, hook, range, match) { 161 | var text, textNode; 162 | text = match[1] || match[2]; 163 | textNode = document.createTextNode(text); 164 | this.editor.selection.range(range); 165 | range.deleteContents(); 166 | range.insertNode(textNode); 167 | range.selectNode(textNode); 168 | this.editor.selection.range(range); 169 | document.execCommand("italic"); 170 | this.editor.selection.setRangeAfter(textNode); 171 | document.execCommand("italic"); 172 | this.editor.trigger("valuechanged"); 173 | return this.editor.trigger("selectionchanged"); 174 | } 175 | }, 176 | ul: { 177 | cmd: /^\*{1}$|^\+{1}$|^\-{1}$/, 178 | block: true, 179 | callback: function(button, hook, range, match, $blockEl) { 180 | return button.command(); 181 | } 182 | }, 183 | ol: { 184 | cmd: /^[0-9][\.\u3002]{1}$/, 185 | block: true, 186 | callback: function(button, hook, range, match, $blockEl) { 187 | return button.command(); 188 | } 189 | }, 190 | image: { 191 | cmd: /!\[(.+)\]\((.+)\)$/, 192 | block: true, 193 | callback: function(button, hook, range, match) { 194 | return button.command(match[2]); 195 | } 196 | }, 197 | link: { 198 | cmd: /\[(.+)\]\((.+)\)$|\<((.[^\[\]\(\)]+))\>$/, 199 | block: false, 200 | callback: function(hook, range, match) { 201 | var $link, url; 202 | url = match[2] || match[4]; 203 | if (!/[a-zA-z]+:\/\/[^\s]*/.test(url)) { 204 | return false; 205 | } 206 | $link = $("", { 207 | text: match[1] || match[3], 208 | href: url, 209 | target: "_blank" 210 | }); 211 | this.editor.selection.range(range); 212 | range.deleteContents(); 213 | range.insertNode($link[0]); 214 | this.editor.selection.setRangeAfter($link); 215 | this.editor.trigger("valuechanged"); 216 | return this.editor.trigger("selectionchanged"); 217 | } 218 | } 219 | }; 220 | 221 | return SimditorLivemd; 222 | 223 | })(SimpleModule); 224 | 225 | Simditor.connect(SimditorLivemd); 226 | 227 | return SimditorLivemd; 228 | 229 | })); 230 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simditor-livemd", 3 | "version": "2.1.2", 4 | "description": "A Markdown plugin for Simditor", 5 | "main": "lib/simditor-livemd.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/mycolorway/simditor-livemd.git" 9 | }, 10 | "author": "ruochenlyu", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/mycolorway/simditor-livemd/issues" 14 | }, 15 | "homepage": "https://github.com/mycolorway/simditor-livemd", 16 | "devDependencies": { 17 | "grunt": "0.x", 18 | "grunt-contrib-watch": "0.x", 19 | "grunt-contrib-coffee": "0.x", 20 | "grunt-umd": ">=2.3.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/simditor-livemd.coffee: -------------------------------------------------------------------------------- 1 | class SimditorLivemd extends SimpleModule 2 | 3 | @pluginName: 'Livemd' 4 | 5 | opts: 6 | livemd: false 7 | 8 | _init: -> 9 | return unless @opts.livemd 10 | 11 | @editor = @_module 12 | if typeof @opts.livemd is "object" 13 | hooks = $.extend({}, @hooks, @opts.livemd) 14 | else 15 | hooks = $.extend({}, @hooks) 16 | 17 | @editor.on "keypress", (e) => 18 | return unless e.which is 32 or e.which is 13 19 | 20 | range = @editor.selection.range() 21 | container = range?.commonAncestorContainer 22 | return unless range and range.collapsed and container and container.nodeType is 3 \ 23 | and not $(container).parent("pre").length 24 | 25 | content = container.textContent 26 | for name, hook of hooks 27 | return if e.which is 13 and not hook.enterKey 28 | continue unless hook and hook.cmd instanceof RegExp 29 | match = content.match hook.cmd 30 | continue unless match 31 | button = @editor.toolbar.findButton name 32 | continue if button is null or button.disabled 33 | 34 | if hook.block 35 | $blockEl = @editor.selection.blockNodes().last() 36 | testRange = document.createRange() 37 | testRange.setStart container, 0 38 | testRange.collapse true 39 | continue unless @editor.selection.rangeAtStartOf($blockEl, testRange) 40 | 41 | cmdStart = match.index 42 | cmdEnd = match[0].length + match.index 43 | range.setStart container, cmdStart 44 | range.setEnd container, cmdEnd 45 | 46 | if hook.block 47 | range.deleteContents() 48 | $blockEl.append(@editor.util.phBr) if @editor.util.isEmptyNode($blockEl) 49 | @editor.selection.setRangeAtEndOf $blockEl 50 | 51 | result = hook.callback.call(@, button, hook, range, match, $blockEl) 52 | e.preventDefault() if (e.which is 32 or name is "code") and result 53 | break 54 | 55 | 56 | hooks: 57 | # Header 58 | title: 59 | cmd: /^#+/ 60 | block: true 61 | enterKey: true 62 | callback: (button, hook, range, match, $blockEl) -> 63 | level = Math.min match[0].length, 3 64 | button.command "h#{level}" 65 | 66 | 67 | # Blockquote 68 | blockquote: 69 | cmd: /^>{1}/ 70 | block: true 71 | enterKey: true 72 | callback: (button, hook, range, match, $blockEl) -> 73 | button.command() 74 | 75 | 76 | # Code 77 | code: 78 | cmd: /^`{3}/ 79 | block: true 80 | enterKey: true 81 | callback: (button, hook, range, match, $blockEl) -> 82 | button.command() 83 | 84 | 85 | # Horizontal rule 86 | hr: 87 | cmd: /^\*{3,}$|^\-{3,}$/ 88 | block: true 89 | enterKey: true 90 | callback: (button, hook, range, match, $blockEl) -> 91 | button.command() 92 | 93 | 94 | # Emphasis: bold 95 | bold: 96 | cmd: /\*{2}([^\*]+)\*{2}$|_{2}([^_]+)_{2}$/ 97 | block: false 98 | callback: (button, hook, range, match) -> 99 | text = match[1] or match[2] 100 | textNode = document.createTextNode text 101 | @editor.selection.range range 102 | range.deleteContents() 103 | range.insertNode textNode 104 | range.selectNode textNode 105 | @editor.selection.range range 106 | document.execCommand "bold" 107 | @editor.selection.setRangeAfter textNode 108 | document.execCommand "bold" 109 | @editor.trigger "valuechanged" 110 | @editor.trigger "selectionchanged" 111 | 112 | 113 | # Emphasis: italic 114 | italic: 115 | cmd: /\*([^\*]+)\*$/ 116 | block: false 117 | callback: (button, hook, range, match) -> 118 | text = match[1] or match[2] 119 | textNode = document.createTextNode text 120 | @editor.selection.range range 121 | range.deleteContents() 122 | range.insertNode textNode 123 | range.selectNode textNode 124 | @editor.selection.range range 125 | document.execCommand "italic" 126 | @editor.selection.setRangeAfter textNode 127 | document.execCommand "italic" 128 | @editor.trigger "valuechanged" 129 | @editor.trigger "selectionchanged" 130 | 131 | 132 | # Unordered list 133 | ul: 134 | cmd: /^\*{1}$|^\+{1}$|^\-{1}$/ 135 | block: true 136 | callback: (button, hook, range, match, $blockEl) -> 137 | button.command() 138 | 139 | 140 | # Ordered list 141 | ol: 142 | cmd: /^[0-9][\.\u3002]{1}$/ 143 | block: true 144 | callback: (button, hook, range, match, $blockEl) -> 145 | button.command() 146 | 147 | 148 | # Image 149 | image: 150 | cmd: /!\[(.+)\]\((.+)\)$/ 151 | block: true 152 | callback: (button, hook, range, match) -> 153 | button.command match[2] 154 | 155 | 156 | # Link 157 | link: 158 | cmd: /\[(.+)\]\((.+)\)$|\<((.[^\[\]\(\)]+))\>$/ 159 | block: false 160 | callback: (hook, range, match) -> 161 | url = match[2] or match[4] 162 | return false unless /[a-zA-z]+:\/\/[^\s]*/.test url 163 | 164 | $link = $("", { 165 | text: match[1] or match[3] 166 | href: url 167 | target: "_blank" 168 | }) 169 | @editor.selection.range range 170 | range.deleteContents() 171 | range.insertNode $link[0] 172 | @editor.selection.setRangeAfter $link 173 | @editor.trigger "valuechanged" 174 | @editor.trigger "selectionchanged" 175 | 176 | 177 | Simditor.connect SimditorLivemd 178 | -------------------------------------------------------------------------------- /umd.hbs: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | // AMD. Register as an anonymous module unless amdModuleId is set 4 | define({{#if amdModuleId}}'{{amdModuleId}}', {{/if}}[{{{amdDependencies.wrapped}}}], function ({{{dependencies}}}) { 5 | return ({{#if objectToExport}}root['{{{objectToExport}}}'] = {{/if}}factory({{dependencies}})); 6 | }); 7 | } else if (typeof exports === 'object') { 8 | // Node. Does not work with strict CommonJS, but 9 | // only CommonJS-like environments that support module.exports, 10 | // like Node. 11 | module.exports = factory({{{cjsDependencies.wrapped}}}); 12 | } else { 13 | {{#if globalAlias}}root['{{{globalAlias}}}'] = {{else}}{{#if objectToExport}}root['{{{objectToExport}}}'] = {{/if}}{{/if}}factory({{{globalDependencies.normal}}}); 14 | } 15 | }(this, function ({{dependencies}}) { 16 | 17 | {{{code}}} 18 | {{#if objectToExport}} 19 | return {{{objectToExport}}}; 20 | {{/if}} 21 | 22 | })); 23 | --------------------------------------------------------------------------------