"
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 |
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 |
--------------------------------------------------------------------------------