93 changes: 37 additions & 56 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,14 @@ <h2>Real-world uses:</h2>
<li><a href="http://ql.io/">ql.io</a> (http API query helper)</li>
<li><a href="http://elm-lang.org/Examples.elm">Elm language examples</a></li>
<li><a href="https://thefiletree.com">The File Tree</a> (collab editor)</li>
<li><a href="http://bluegriffon.org/">BlueGriffon</a> (HTML editor)</li>
<li><a href="http://www.jshint.com/">JSHint</a> (JS linter)</li>
<li><a href="http://kl1p.com/cmtest/1">kl1p</a> (paste service)</li>
<li><a href="http://sqlfiddle.com">SQLFiddle</a> (SQL playground)</li>
<li><a href="http://try.haxe.org">Try Haxe</a> (Haxe Playground) </li>
<li><a href="http://cssdeck.com/">CSSDeck</a> (CSS showcase)</li>
<li><a href="http://www.ckwnc.com/">CKWNC</a> (UML editor)</li>
<li><a href="http://www.sketchpatch.net/labs/livecodelabIntro.html">sketchPatch Livecodelab</a></li>
<li><a href="https://notex.ch">NoTex</a> (rST authoring)</li>
<li><a href="doc/realworld.html">More...</a></li>
</ul>

</div></div>
Expand Down Expand Up @@ -257,9 +256,10 @@ <h2>Support CodeMirror</h2>
<ul>
<li>Donate
(<span onclick="document.getElementById('paypal').submit();"
class="quasilink">Paypal</span>
or <span onclick="document.getElementById('bankinfo').style.display = 'block';"
class="quasilink">bank</span>)</li>
class="quasilink">Paypal</span>,
<span onclick="document.getElementById('bankinfo').style.display = 'block';"
class="quasilink">bank</span>, or
<a href="https://www.gittip.com/marijnh">Gittip</a>)</li>
<li>Purchase <a href="#commercial">commercial support</a></li>
</ul>

Expand All @@ -281,6 +281,38 @@ <h2>Reading material</h2>

<h2 id=releases>Releases</h2>

<p class="rel">22-10-2012: <a href="http://codemirror.net/codemirror-2.35.zip">Version 2.35</a>:</p>

<ul class="rel-note">
<li>New (sub) mode: <a href="mode/javascript/typescript.html">TypeScript</a>.</li>
<li>Don't overwrite (insert key) when pasing.</li>
<li>Fix several bugs in <a href="doc/manual.html#markText"><code>markText</code></a>/undo interaction.</li>
<li>Better indentation of JavaScript code without semicolons.</li>
<li>Add <a href="doc/manual.html#defineInitHook"><code>defineInitHook</code></a> function.</li>
<li>Full <a href="https://github.com/marijnh/CodeMirror/compare/v2.34...v2.35">list of patches</a>.</li>
</ul>

<p class="rel">22-10-2012: <a href="http://codemirror.net/codemirror-3.0beta2.zip">Version 3.0, beta 2</a>:</p>

<p class="rel-note"><strong>BETA release, new major version</strong>. Only partially
backwards-compatible. See
the <a href="http://codemirror.net/3/doc/upgrade_v3.html">upgrading
guide</a> for more information. Changes since beta1:</p>

<ul class="rel-note">
<li>Fix page-based coordinate computation.</li>
<li>Fix firing of <a href="http://codemirror.net/3/doc/manual.html#event_gutterClick"><code>gutterClick</code></a> event.</li>
<li>Add <a href="http://codemirror.net/3/doc/manual.html#option_cursorHeight"><code>cursorHeight</code></a> option.</li>
<li>Fix bi-directional text regression.</li>
<li>Add <a href="http://codemirror.net/3/doc/manual.html#option_viewportMargin"><code>viewportMargin</code></a> option.</li>
<li>Directly handle mousewheel events (again, hopefully better).</li>
<li>Make vertical cursor movement more robust (through widgets, big line gaps).</li>
<li>Add <a href="http://codemirror.net/3/doc/manual.html#option_flattenSpans"><code>flattenSpans</code></a> option.</li>
<li>Initialization in hidden state works again.</li>
<li>Many optimizations. Poor responsiveness should be fixed.</li>
<li>Full <a href="https://github.com/marijnh/CodeMirror/compare/v3.0beta1...v3.0beta2">list of patches</a>.</li>
</ul>

<p class="rel">19-09-2012: <a href="http://codemirror.net/codemirror-2.34.zip">Version 2.34</a>:</p>

<ul class="rel-note">
Expand Down Expand Up @@ -406,57 +438,6 @@ <h2 id=releases>Releases</h2>
<li>Add <a href="doc/manual.html#findMarksAt"><code>findMarksAt</code></a> method.</li>
</ul>

<p class="rel">27-02-2012: <a href="http://codemirror.net/codemirror-2.22.zip">Version 2.22</a>:</p>

<ul class="rel-note">
<li>Allow <a href="doc/manual.html#keymaps">key handlers</a> to pass up events, allow binding characters.</li>
<li>Add <a href="doc/manual.html#option_autoClearEmptyLines"><code>autoClearEmptyLines</code></a> option.</li>
<li>Properly use tab stops when rendering tabs.</li>
<li>Make PHP mode more robust.</li>
<li>Support indentation blocks in <a href="doc/manual.html#util_foldcode">code folder</a>.</li>
<li>Add a script for <a href="doc/manual.html#util_match-highlighter">highlighting instances of the selection</a>.</li>
<li>New <a href="mode/properties/index.html">.properties</a> mode.</li>
<li>Fix many bugs.</li>
</ul>

<p class="rel">27-01-2012: <a href="http://codemirror.net/codemirror-2.21.zip">Version 2.21</a>:</p>

<ul class="rel-note">
<li>Added <a href="mode/less/index.html">LESS</a>, <a href="mode/mysql/index.html">MySQL</a>,
<a href="mode/go/index.html">Go</a>, and <a href="mode/verilog/index.html">Verilog</a> modes.</li>
<li>Add <a href="doc/manual.html#option_smartIndent"><code>smartIndent</code></a>
option.</li>
<li>Support a cursor in <a href="doc/manual.html#option_readOnly"><code>readOnly</code></a>-mode.</li>
<li>Support assigning multiple styles to a token.</li>
<li>Use a new approach to drawing the selection.</li>
<li>Add <a href="doc/manual.html#scrollTo"><code>scrollTo</code></a> method.</li>
<li>Allow undo/redo events to span non-adjacent lines.</li>
<li>Lots and lots of bugfixes.</li>
</ul>

<p class="rel">20-12-2011: <a href="http://codemirror.net/codemirror-2.2.zip">Version 2.2</a>:</p>

<ul class="rel-note">
<li>Slightly incompatible API changes. Read <a href="doc/upgrade_v2.2.html">this</a>.</li>
<li>New approach
to <a href="doc/manual.html#option_extraKeys">binding</a> keys,
support for <a href="doc/manual.html#option_keyMap">custom
bindings</a>.</li>
<li>Support for overwrite (insert).</li>
<li><a href="doc/manual.html#option_tabSize">Custom-width</a>
and <a href="demo/visibletabs.html">stylable</a> tabs.</li>
<li>Moved more code into <a href="doc/manual.html#addons">add-on scripts</a>.</li>
<li>Support for sane vertical cursor movement in wrapped lines.</li>
<li>More reliable handling of
editing <a href="doc/manual.html#markText">marked text</a>.</li>
<li>Add minimal <a href="demo/emacs.html">emacs</a>
and <a href="demo/vim.html">vim</a> bindings.</li>
<li>Rename <code>coordsFromIndex</code>
to <a href="doc/manual.html#posFromIndex"><code>posFromIndex</code></a>,
add <a href="doc/manual.html#indexFromPos"><code>indexFromPos</code></a>
method.</li>
</ul>

<p><a href="doc/oldrelease.html">Older releases...</a></p>

</div></div>
Expand Down
19 changes: 9 additions & 10 deletions keymap/vim.js
Original file line number Diff line number Diff line change
Expand Up @@ -318,13 +318,12 @@
};

// standard mode switching
iterList(["d", "t", "T", "f", "F", "c", "r"],
function (ch) {
CodeMirror.keyMap.vim[toCombo(ch)] = function (cm) {
cm.setOption("keyMap", "vim-prefix-" + ch);
emptyBuffer();
};
});
iterList(["d", "t", "T", "f", "F", "c", "r"], function (ch) {
CodeMirror.keyMap.vim[toCombo(ch)] = function (cm) {
cm.setOption("keyMap", "vim-prefix-" + ch);
emptyBuffer();
};
});

function addCountBindings(keyMap) {
// Add bindings for number keys
Expand Down Expand Up @@ -645,7 +644,7 @@
};

// Map our movement actions each operator and non-operational movement
motionList.forEach(function(key, index, array) {
iterList(motionList, function(key, index, array) {
CodeMirror.keyMap['vim-prefix-d'][key] = function(cm) {
// Get our selected range
var start = cm.getCursor();
Expand Down Expand Up @@ -695,7 +694,7 @@
});

var nums = [1,2,3,4,5,6,7,8,9];
nums.forEach(function(key, index, array) {
iterList(nums, function(key, index, array) {
CodeMirror.keyMap['vim'][key] = function (cm) {
reptTimes = (reptTimes * 10) + key;
};
Expand All @@ -713,7 +712,7 @@
// Create our keymaps for each operator and make xa and xi where x is an operator
// change to the corrosponding keymap
var operators = ['d', 'y', 'c'];
operators.forEach(function(key, index, array) {
iterList(operators, function(key, index, array) {
CodeMirror.keyMap['vim-prefix-'+key+'a'] = {
auto: 'vim', nofallthrough: true, style: "fat-cursor"
};
Expand Down
1 change: 1 addition & 0 deletions lib/codemirror.css
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
word-wrap: normal;
line-height: inherit;
color: inherit;
overflow: visible;
}

.CodeMirror-wrap pre {
Expand Down
80 changes: 51 additions & 29 deletions lib/codemirror.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ window.CodeMirror = (function() {
// Selection-related flags. shiftSelecting obviously tracks
// whether the user is holding shift.
var shiftSelecting, lastClick, lastDoubleClick, lastScrollTop = 0, draggingText,
overwrite = false, suppressEdits = false;
overwrite = false, suppressEdits = false, pasteIncoming = false;
// Variables used by startOperation/endOperation to track what
// happened during the operation.
var updateInput, userSelChange, changes, textChanged, selectionChanged,
Expand Down Expand Up @@ -128,7 +128,7 @@ window.CodeMirror = (function() {
connect(scroller, "drop", operation(onDrop));
}
connect(scroller, "paste", function(){focusInput(); fastPoll();});
connect(input, "paste", fastPoll);
connect(input, "paste", function(){pasteIncoming = true; fastPoll();});
connect(input, "cut", operation(function(){
if (!options.readOnly) replaceSelection("");
}));
Expand Down Expand Up @@ -167,6 +167,7 @@ window.CodeMirror = (function() {
else if (option == "lineWrapping" && oldVal != value) operation(wrappingChanged)();
else if (option == "tabSize") updateDisplay(true);
else if (option == "keyMap") keyMapChanged();
else if (option == "tabindex") input.tabIndex = value;
if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber" ||
option == "theme" || option == "lineNumberFormatter") {
gutterChanged();
Expand Down Expand Up @@ -954,12 +955,13 @@ window.CodeMirror = (function() {
while (same < l && prevInput[same] == text[same]) ++same;
if (same < prevInput.length)
sel.from = {line: sel.from.line, ch: sel.from.ch - (prevInput.length - same)};
else if (overwrite && posEq(sel.from, sel.to))
else if (overwrite && posEq(sel.from, sel.to) && !pasteIncoming)
sel.to = {line: sel.to.line, ch: Math.min(getLine(sel.to.line).text.length, sel.to.ch + (text.length - same))};
replaceSelection(text.slice(same), "end");
if (text.length > 1000) { input.value = prevInput = ""; }
else prevInput = text;
if (!nestedOperation) endOperation();
pasteIncoming = false;
return true;
}
function resetInput(user) {
Expand Down Expand Up @@ -1416,7 +1418,7 @@ window.CodeMirror = (function() {
var startChar = line.charAt(start);
var check = isWordChar(startChar) ? isWordChar :
/\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} :
function(ch) {return !/\s/.test(ch) && !isWordChar(ch);};
function(ch) {return !/\s/.test(ch) && isWordChar(ch);};
while (start > 0 && check(line.charAt(start - 1))) --start;
while (end < line.length && check(line.charAt(end))) ++end;
}
Expand Down Expand Up @@ -1460,6 +1462,7 @@ window.CodeMirror = (function() {

if (indentString != curSpaceString)
replaceRange(indentString, {line: n, ch: 0}, {line: n, ch: curSpaceString.length});
line.stateAfter = null;
}

function loadMode() {
Expand Down Expand Up @@ -1505,18 +1508,17 @@ window.CodeMirror = (function() {

function TextMarker(type, style) { this.lines = []; this.type = type; if (style) this.style = style; }
TextMarker.prototype.clear = operation(function() {
var min = Infinity, max = -Infinity;
var min, max;
for (var i = 0; i < this.lines.length; ++i) {
var line = this.lines[i];
var span = getMarkedSpanFor(line.markedSpans, this, true);
if (span.from != null || span.to != null) {
var lineN = lineNo(line);
min = Math.min(min, lineN); max = Math.max(max, lineN);
}
var span = getMarkedSpanFor(line.markedSpans, this);
if (span.from != null) min = lineNo(line);
if (span.to != null) max = lineNo(line);
line.markedSpans = removeMarkedSpan(line.markedSpans, span);
}
if (min != Infinity)
changes.push({from: min, to: max + 1});
if (min != null) changes.push({from: min, to: max + 1});
this.lines.length = 0;
this.explicitlyCleared = true;
});
TextMarker.prototype.find = function() {
var from, to;
Expand All @@ -1543,7 +1545,7 @@ window.CodeMirror = (function() {
var span = {from: curLine == from.line ? from.ch : null,
to: curLine == to.line ? to.ch : null,
marker: marker};
(line.markedSpans || (line.markedSpans = [])).push(span);
line.markedSpans = (line.markedSpans || []).concat([span]);
marker.lines.push(line);
++curLine;
});
Expand All @@ -1554,8 +1556,9 @@ window.CodeMirror = (function() {
function setBookmark(pos) {
pos = clipPos(pos);
var marker = new TextMarker("bookmark"), line = getLine(pos.line);
history.addChange(pos.line, 1, [newHL(line.text, line.markedSpans)], true);
var span = {from: pos.ch, to: pos.ch, marker: marker};
(line.markedSpans || (line.markedSpans = [])).push(span);
line.markedSpans = (line.markedSpans || []).concat([span]);
marker.lines.push(line);
return marker;
}
Expand Down Expand Up @@ -1644,8 +1647,6 @@ window.CodeMirror = (function() {

function measureLine(line, ch) {
if (ch == 0) return {top: 0, left: 0};
var wbr = options.lineWrapping && ch < line.text.length &&
spanAffectsWrapping.test(line.text.slice(ch - 1, ch + 1));
var pre = lineContent(line, ch);
removeChildrenAndAdd(measure, pre);
var anchor = pre.anchor;
Expand Down Expand Up @@ -1978,6 +1979,7 @@ window.CodeMirror = (function() {
if (extensions.propertyIsEnumerable(ext) &&
!instance.propertyIsEnumerable(ext))
instance[ext] = extensions[ext];
for (var i = 0; i < initHooks.length; ++i) initHooks[i](instance);
return instance;
} // (end of function CodeMirror)

Expand Down Expand Up @@ -2075,6 +2077,9 @@ window.CodeMirror = (function() {
extensions[name] = func;
};

var initHooks = [];
CodeMirror.defineInitHook = function(f) {initHooks.push(f);};

var modeExtensions = CodeMirror.modeExtensions = {};
CodeMirror.extendMode = function(mode, properties) {
var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {});
Expand Down Expand Up @@ -2204,6 +2209,7 @@ window.CodeMirror = (function() {
var name = keyNames[e_prop(event, "keyCode")];
return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod";
}
CodeMirror.isModifierKey = isModifierKey;

CodeMirror.fromTextArea = function(textarea, options) {
if (!options) options = {};
Expand Down Expand Up @@ -2353,16 +2359,20 @@ window.CodeMirror = (function() {
this.from = from; this.to = to; this.marker = marker;
}

function getMarkedSpanFor(spans, marker, del) {
function getMarkedSpanFor(spans, marker) {
if (spans) for (var i = 0; i < spans.length; ++i) {
var span = spans[i];
if (span.marker == marker) {
if (del) spans.splice(i, 1);
return span;
}
if (span.marker == marker) return span;
}
}

function removeMarkedSpan(spans, span) {
var r;
for (var i = 0; i < spans.length; ++i)
if (spans[i] != span) (r || (r = [])).push(spans[i]);
return r;
}

function markedSpansBefore(old, startCh, endCh) {
if (old) for (var i = 0, nw; i < old.length; ++i) {
var span = old[i], marker = span.marker;
Expand Down Expand Up @@ -2446,7 +2456,15 @@ window.CodeMirror = (function() {
// hl stands for history-line, a data structure that can be either a
// string (line without markers) or a {text, markedSpans} object.
function hlText(val) { return typeof val == "string" ? val : val.text; }
function hlSpans(val) { return typeof val == "string" ? null : val.markedSpans; }
function hlSpans(val) {
if (typeof val == "string") return null;
var spans = val.markedSpans, out = null;
for (var i = 0; i < spans.length; ++i) {
if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); }
else if (out) out.push(spans[i]);
}
return !out ? spans : out.length ? out : null;
}
function newHL(text, spans) { return spans ? {text: text, markedSpans: spans} : text; }

function detachMarkedSpans(line) {
Expand Down Expand Up @@ -2582,13 +2600,17 @@ window.CodeMirror = (function() {
span = function(html, text, style) {
var l = text.length;
if (wrapAt >= outPos && wrapAt < outPos + l) {
if (wrapAt > outPos) {
span_(html, text.slice(0, wrapAt - outPos), style);
var cut = wrapAt - outPos;
if (cut) {
span_(html, text.slice(0, cut), style);
// See comment at the definition of spanAffectsWrapping
if (compensateForWrapping) html.appendChild(elt("wbr"));
if (compensateForWrapping) {
var view = text.slice(cut - 1, cut + 1);
if (spanAffectsWrapping.test(view)) html.appendChild(elt("wbr"));
else if (!ie_lt8 && /\w\w/.test(view)) html.appendChild(document.createTextNode("\u200d"));
}
}
html.appendChild(anchor);
var cut = wrapAt - outPos;
span_(anchor, opera ? text.slice(cut, cut + 1) : text.slice(cut), style);
if (opera) span_(html, text.slice(cut + 1), style);
wrapAt--;
Expand Down Expand Up @@ -2872,7 +2894,7 @@ window.CodeMirror = (function() {
var time = +new Date, cur = lst(this.done), last = cur && lst(cur);
var dtime = time - this.time;

if (this.compound && cur && !this.closed) {
if (cur && !this.closed && this.compound) {
cur.push({start: start, added: added, old: old});
} else if (dtime > 400 || !last || this.closed ||
last.start > start + old.length || last.start + last.added < start) {
Expand Down Expand Up @@ -3079,7 +3101,7 @@ window.CodeMirror = (function() {
return -1;
}
function isWordChar(ch) {
return /\w/.test(ch) || ch.toUpperCase() != ch.toLowerCase();
return /\w/.test(ch) || ch.toUpperCase() != ch.toLowerCase() || /[\u4E00-\u9FA5]/.test(ch);
}

// See if "".split is the broken IE version, if so, provide an
Expand Down Expand Up @@ -3135,7 +3157,7 @@ window.CodeMirror = (function() {
for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i;
})();

CodeMirror.version = "2.34";
CodeMirror.version = "2.35";

return CodeMirror;
})();
7 changes: 6 additions & 1 deletion lib/util/overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ CodeMirror.overlayMode = CodeMirror.overlayParser = function(base, overlay, comb
},
electricChars: base.electricChars,

innerMode: function(state) { return {state: state.base, mode: base}; }
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);
}
};
};
2 changes: 1 addition & 1 deletion lib/util/runmode.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
CodeMirror.runMode = function(string, modespec, callback, options) {
function esc(str) {
return str.replace(/[<&]/, function(ch) { return ch == "<" ? "&lt;" : "&amp;"; });
return str.replace(/[<&]/g, function(ch) { return ch == "<" ? "&lt;" : "&amp;"; });
}

var mode = CodeMirror.getMode(CodeMirror.defaults, modespec);
Expand Down
6 changes: 3 additions & 3 deletions lib/util/searchcursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
query.lastIndex = 0;
var line = cm.getLine(pos.line).slice(0, pos.ch), match = query.exec(line), start = 0;
while (match) {
start += match.index;
line = line.slice(match.index);
start += match.index + 1;
line = line.slice(start);
query.lastIndex = 0;
var newmatch = query.exec(line);
if (newmatch) match = newmatch;
else break;
start++;
}
start--;
} else {
query.lastIndex = pos.ch;
var line = cm.getLine(pos.line), match = query.exec(line),
Expand Down
13 changes: 9 additions & 4 deletions lib/util/simple-hint.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
editor.replaceRange(str, result.from, result.to);
}
// When there is only one completion, use it directly.
if (completions.length == 1) {insert(completions[0]); return true;}
if (options.completeSingle && completions.length == 1) {
insert(completions[0]);
return true;
}

// Build the select widget
var complete = document.createElement("div");
Expand All @@ -41,7 +44,7 @@
}
sel.firstChild.selected = true;
sel.size = Math.min(10, completions.length);
var pos = editor.cursorCoords();
var pos = options.alignWithWord ? editor.charCoords(result.from) : editor.cursorCoords();
complete.style.left = pos.x + "px";
complete.style.top = pos.yBot + "px";
document.body.appendChild(complete);
Expand Down Expand Up @@ -71,7 +74,7 @@
if (code == 13) {CodeMirror.e_stop(event); pick();}
// Escape
else if (code == 27) {CodeMirror.e_stop(event); close(); editor.focus();}
else if (code != 38 && code != 40 && code != 33 && code != 34) {
else if (code != 38 && code != 40 && code != 33 && code != 34 && !CodeMirror.isModifierKey(event)) {
close(); editor.focus();
// Pass the event to the CodeMirror instance so that it can handle things like backspace properly.
editor.triggerOnKeyDown(event);
Expand All @@ -92,6 +95,8 @@
};
CodeMirror.simpleHint.defaults = {
closeOnBackspace: true,
closeOnTokenChange: false
closeOnTokenChange: false,
completeSingle: true,
alignWithWord: true
};
})();
1 change: 1 addition & 0 deletions mode/clike/clike.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) {
},

indent: function(state, textAfter) {
if (state.tokenize == tokenComment) return CodeMirror.Pass;
if (state.tokenize != tokenBase && state.tokenize != null) return 0;
var ctx = state.context, firstChar = textAfter && textAfter.charAt(0);
if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev;
Expand Down
2 changes: 1 addition & 1 deletion mode/css/css.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ CodeMirror.defineMode("css", function(config) {
else if (/[,+>*\/]/.test(ch)) {
return ret(null, "select-op");
}
else if (ch == "." && stream.match(/^\w+/)) {
else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) {
return ret("qualifier", type);
}
else if (ch == ":") {
Expand Down
4 changes: 2 additions & 2 deletions mode/css/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,9 @@ MT.testMode(

MT.testMode(
'classSelector',
'.foo { }',
'.foo-bar_hello { }',
[
'qualifier', '.foo',
'qualifier', '.foo-bar_hello',
null, ' { }'
]
);
Expand Down
222 changes: 83 additions & 139 deletions mode/gfm/gfm.js
Original file line number Diff line number Diff line change
@@ -1,150 +1,94 @@
CodeMirror.defineMode("gfm", function(config, parserConfig) {
var mdMode = CodeMirror.getMode(config, "markdown");
var aliases = {
html: "htmlmixed",
js: "javascript",
json: "application/json",
c: "text/x-csrc",
"c++": "text/x-c++src",
java: "text/x-java",
csharp: "text/x-csharp",
"c#": "text/x-csharp"
};

// make this lazy so that we don't need to load GFM last
var getMode = (function () {
var i, modes = {}, mimes = {}, mime;

var list = CodeMirror.listModes();
for (i = 0; i < list.length; i++) {
modes[list[i]] = list[i];
}
var mimesList = CodeMirror.listMIMEs();
for (i = 0; i < mimesList.length; i++) {
mime = mimesList[i].mime;
mimes[mime] = mimesList[i].mime;
}

for (var a in aliases) {
if (aliases[a] in modes || aliases[a] in mimes)
modes[a] = aliases[a];
}

return function (lang) {
return modes[lang] ? CodeMirror.getMode(config, modes[lang]) : null;
};
}());

function markdown(stream, state) {
// intercept fenced code blocks
if (stream.sol() && stream.match(/^```([\w+#]*)/)) {
// try switching mode
state.localMode = getMode(RegExp.$1);
if (state.localMode)
state.localState = state.localMode.startState();

state.token = local;
return 'code';
}

return mdMode.token(stream, state.mdState);
}

function local(stream, state) {
if (stream.sol() && stream.match(/^```/)) {
state.localMode = state.localState = null;
state.token = markdown;
return 'code';
}
else if (state.localMode) {
return state.localMode.token(stream, state.localState);
} else {
stream.skipToEnd();
return 'code';
}
}

// custom handleText to prevent emphasis in the middle of a word
// and add autolinking
function handleText(stream, mdState) {
var match;
if (stream.match(/^\w+:\/\/\S+/)) {
return 'link';
}
if (stream.match(/^[^\[*\\<>` _][^\[*\\<>` ]*[^\[*\\<>` _]/)) {
return mdMode.getType(mdState);
}
if (match = stream.match(/^[^\[*\\<>` ]+/)) {
var word = match[0];
if (word[0] === '_' && word[word.length-1] === '_') {
stream.backUp(word.length);
return undefined;
}
return mdMode.getType(mdState);
}
if (stream.eatSpace()) {
return null;
}
var codeDepth = 0;
function blankLine(state) {
state.code = false;
return null;
}

return {
var gfmOverlay = {
startState: function() {
var mdState = mdMode.startState();
mdState.text = handleText;
return {token: markdown, mode: "markdown", mdState: mdState,
localMode: null, localState: null};
return {
code: false,
codeBlock: false,
ateSpace: false
};
},

copyState: function(state) {
return {token: state.token, mdState: CodeMirror.copyState(mdMode, state.mdState),
localMode: state.localMode,
localState: state.localMode ? CodeMirror.copyState(state.localMode, state.localState) : null};
copyState: function(s) {
return {
code: s.code,
codeBlock: s.codeBlock,
ateSpace: s.ateSpace
};
},

token: function(stream, state) {
/* Parse GFM double bracket links */
var ch;
if ((ch = stream.peek()) != undefined && ch == '[') {
stream.next(); // Advance the stream

/* Only handle double bracket links */
if ((ch = stream.peek()) == undefined || ch != '[') {
stream.backUp(1);
return state.token(stream, state);
}

while ((ch = stream.next()) != undefined && ch != ']') {}

if (ch == ']' && (ch = stream.next()) != undefined && ch == ']')
return 'link';

/* If we did not find the second ']' */
stream.backUp(1);
}

/* Match GFM latex formulas, as well as latex formulas within '$' */
if (stream.match(/^\$[^\$]+\$/)) {
return "string";
// Hack to prevent formatting override inside code blocks (block and inline)
if (state.codeBlock) {
if (stream.match(/^```/)) {
state.codeBlock = false;
return null;
}

if (stream.match(/^\\\((.*?)\\\)/)) {
return "string";
}

if (stream.match(/^\$\$[^\$]+\$\$/)) {
return "string";
stream.skipToEnd();
return null;
}
if (stream.sol()) {
state.code = false;
}
if (stream.sol() && stream.match(/^```/)) {
stream.skipToEnd();
state.codeBlock = true;
return null;
}
// If this block is changed, it may need to be updated in Markdown mode
if (stream.peek() === '`') {
stream.next();
var before = stream.pos;
stream.eatWhile('`');
var difference = 1 + stream.pos - before;
if (!state.code) {
codeDepth = difference;
state.code = true;
} else {
if (difference === codeDepth) { // Must be exact
state.code = false;
}
}

if (stream.match(/^\\\[(.*?)\\\]/)) {
return "string";
return null;
} else if (state.code) {
stream.next();
return null;
}
// Check if space. If so, links can be formatted later on
if (stream.eatSpace()) {
state.ateSpace = true;
return null;
}
if (stream.sol() || state.ateSpace) {
state.ateSpace = false;
if(stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40}\b)/)) {
// User/Project@SHA
// User@SHA
// SHA
return "link";
} else if (stream.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/)) {
// User/Project#Num
// User#Num
// #Num
return "link";
}

return state.token(stream, state);
}
if (stream.match(/^((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»]))/i)) {
// URLs
// Taken from http://daringfireball.net/2010/07/improved_regex_for_matching_urls
return "link";
}
stream.next();
return null;
},

innerMode: function(state) {
if (state.token == markdown) return {state: state.mdState, mode: mdMode};
else return {state: state.localState, mode: state.localMode};
}
blankLine: blankLine
};
}, "markdown");
CodeMirror.defineMIME("gfmBase", {
name: "markdown",
underscoresBreakWords: false,
fencedCodeBlocks: true
});
return CodeMirror.overlayMode(CodeMirror.getMode(config, "gfmBase"), gfmOverlay);
});
31 changes: 27 additions & 4 deletions mode/gfm/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,52 @@
<title>CodeMirror: GFM mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
<script src="../../lib/util/overlay.js"></script>
<script src="../xml/xml.js"></script>
<script src="../markdown/markdown.js"></script>
<script src="gfm.js"></script>

<!-- Code block highlighting modes -->
<script src="../javascript/javascript.js"></script>
<script src="../css/css.js"></script>
<script src="../htmlmixed/htmlmixed.js"></script>
<script src="../clike/clike.js"></script>

<link rel="stylesheet" href="../markdown/markdown.css">
<style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
<link rel="stylesheet" href="../../doc/docs.css">
</head>
<body>
<h1>CodeMirror: GFM mode</h1>

<!-- source: http://daringfireball.net/projects/markdown/basics.text -->
<form><textarea id="code" name="code">
Github Flavored Markdown
GitHub Flavored Markdown
========================

Everything from markdown plus GFM features:

## Fenced code blocks
## URL autolinking

Underscores_are_allowed_between_words.

## Fenced code blocks (and syntax highlighting... someday)

```javascript
for (var i = 0; i &lt; items.length; i++) {
console.log(items[i], i); // log them
}
```

See http://github.github.com/github-flavored-markdown/
## A bit of GitHub spice

* SHA: be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* User@SHA ref: mojombo@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* User/Project@SHA: mojombo/god@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2
* \#Num: #1
* User/#Num: mojombo#1
* User/Project#Num: mojombo/god#1

See http://github.github.com/github-flavored-markdown/.

</textarea></form>

Expand All @@ -44,5 +63,9 @@ <h1>CodeMirror: GFM mode</h1>
});
</script>

<p>Optionally depends on other modes for properly highlighted code blocks.</p>

<p><strong>Parsing/Highlighting Tests:</strong> <a href="../../test/index.html#gfm_*">normal</a>, <a href="../../test/index.html#verbose,gfm_*">verbose</a>.</p>

</body>
</html>
225 changes: 225 additions & 0 deletions mode/gfm/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// Initiate ModeTest and set defaults
var MT = ModeTest;
MT.modeName = 'gfm';
MT.modeOptions = {};

// Emphasis characters within a word
MT.testMode(
'emInWordAsterisk',
'foo*bar*hello',
[
null, 'foo',
'em', '*bar*',
null, 'hello'
]
);
MT.testMode(
'emInWordUnderscore',
'foo_bar_hello',
[
null, 'foo_bar_hello'
]
);
MT.testMode(
'emStrongUnderscore',
'___foo___ bar',
[
'strong', '__',
'emstrong', '_foo__',
'em', '_',
null, ' bar'
]
);

// Fenced code blocks
MT.testMode(
'fencedCodeBlocks',
'```\nfoo\n\n```\nbar',
[
'comment', '```',
'comment', 'foo',
'comment', '```',
null, 'bar'
]
);
// Fenced code block mode switching
MT.testMode(
'fencedCodeBlockModeSwitching',
'```javascript\nfoo\n\n```\nbar',
[
'comment', '```javascript',
'variable', 'foo',
'comment', '```',
null, 'bar'
]
);

// SHA
MT.testMode(
'SHA',
'foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2 bar',
[
null, 'foo ',
'link', 'be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2',
null, ' bar'
]
);
// GitHub highlights hashes 7-40 chars in length
MT.testMode(
'shortSHA',
'foo be6a8cc bar',
[
null, 'foo ',
'link', 'be6a8cc',
null, ' bar'
]
);
// Invalid SHAs
//
// GitHub does not highlight hashes shorter than 7 chars
MT.testMode(
'tooShortSHA',
'foo be6a8c bar',
[
null, 'foo be6a8c bar'
]
);
// GitHub does not highlight hashes longer than 40 chars
MT.testMode(
'longSHA',
'foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd22 bar',
[
null, 'foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd22 bar'
]
);
MT.testMode(
'badSHA',
'foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cg2 bar',
[
null, 'foo be6a8cc1c1ecfe9489fb51e4869af15a13fc2cg2 bar'
]
);
// User@SHA
MT.testMode(
'userSHA',
'foo bar@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2 hello',
[
null, 'foo ',
'link', 'bar@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2',
null, ' hello'
]
);
// User/Project@SHA
MT.testMode(
'userProjectSHA',
'foo bar/hello@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2 world',
[
null, 'foo ',
'link', 'bar/hello@be6a8cc1c1ecfe9489fb51e4869af15a13fc2cd2',
null, ' world'
]
);

// #Num
MT.testMode(
'num',
'foo #1 bar',
[
null, 'foo ',
'link', '#1',
null, ' bar'
]
);
// bad #Num
MT.testMode(
'badNum',
'foo #1bar hello',
[
null, 'foo #1bar hello'
]
);
// User#Num
MT.testMode(
'userNum',
'foo bar#1 hello',
[
null, 'foo ',
'link', 'bar#1',
null, ' hello'
]
);
// User/Project#Num
MT.testMode(
'userProjectNum',
'foo bar/hello#1 world',
[
null, 'foo ',
'link', 'bar/hello#1',
null, ' world'
]
);

// Vanilla links
MT.testMode(
'vanillaLink',
'foo http://www.example.com/ bar',
[
null, 'foo ',
'link', 'http://www.example.com/',
null, ' bar'
]
);
MT.testMode(
'vanillaLinkPunctuation',
'foo http://www.example.com/. bar',
[
null, 'foo ',
'link', 'http://www.example.com/',
null, '. bar'
]
);
MT.testMode(
'vanillaLinkExtension',
'foo http://www.example.com/index.html bar',
[
null, 'foo ',
'link', 'http://www.example.com/index.html',
null, ' bar'
]
);
// Not a link
MT.testMode(
'notALink',
'```css\nfoo {color:black;}\n```http://www.example.com/',
[
'comment', '```css',
'tag', 'foo',
null, ' {',
'property', 'color',
'operator', ':',
'keyword', 'black',
null, ';}',
'comment', '```',
'link', 'http://www.example.com/'
]
);
// Not a link
MT.testMode(
'notALink',
'``foo `bar` http://www.example.com/`` hello',
[
'comment', '``foo `bar` http://www.example.com/``',
null, ' hello'
]
);
// Not a link
MT.testMode(
'notALink',
'`foo\nhttp://www.example.com/\n`foo\n\nhttp://www.example.com/',
[
'comment', '`foo',
'link', 'http://www.example.com/',
'comment', '`foo',
'link', 'http://www.example.com/'
]
);
1 change: 1 addition & 0 deletions mode/htmlembedded/htmlembedded.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,4 @@ CodeMirror.defineMode("htmlembedded", function(config, parserConfig) {
CodeMirror.defineMIME("application/x-ejs", { name: "htmlembedded", scriptingModeSpec:"javascript"});
CodeMirror.defineMIME("application/x-aspx", { name: "htmlembedded", scriptingModeSpec:"text/x-csharp"});
CodeMirror.defineMIME("application/x-jsp", { name: "htmlembedded", scriptingModeSpec:"text/x-java"});
CodeMirror.defineMIME("application/x-erb", { name: "htmlembedded", scriptingModeSpec:"ruby"});
15 changes: 11 additions & 4 deletions mode/javascript/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,17 @@ <h1>CodeMirror: JavaScript mode</h1>
});
</script>

<p>JavaScript mode supports a single configuration
option, <code>json</code>, which will set the mode to expect JSON
data rather than a JavaScript program.</p>
<p>
JavaScript mode supports a two configuration
options:
<ul>
<li><code>json</code> which will set the mode to expect JSON data rather than a JavaScript program.</li>
<li>
<code>typescript</code> which will activate additional syntax highlighting and some other things for TypeScript code (<a href="typescript.html">demo</a>).
</li>
</ul>
</p>

<p><strong>MIME types defined:</strong> <code>text/javascript</code>, <code>application/json</code>.</p>
<p><strong>MIME types defined:</strong> <code>text/javascript</code>, <code>application/json</code>, <code>text/typescript</code>, <code>application/typescript</code>.</p>
</body>
</html>
78 changes: 63 additions & 15 deletions mode/javascript/javascript.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
// TODO actually recognize syntax of TypeScript constructs

CodeMirror.defineMode("javascript", function(config, parserConfig) {
var indentUnit = config.indentUnit;
var jsonMode = parserConfig.json;
var isTS = parserConfig.typescript;

// Tokenizer

var keywords = function(){
function kw(type) {return {type: type, style: "keyword"};}
var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c");
var operator = kw("operator"), atom = {type: "atom", style: "atom"};
return {

var jsKeywords = {
"if": A, "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
"return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C,
"var": kw("var"), "const": kw("var"), "let": kw("var"),
Expand All @@ -17,6 +21,35 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
"in": operator, "typeof": operator, "instanceof": operator,
"true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom
};

// Extend the 'normal' keywords with the TypeScript language extensions
if (isTS) {
var type = {type: "variable", style: "variable-3"};
var tsKeywords = {
// object-like things
"interface": kw("interface"),
"class": kw("class"),
"extends": kw("extends"),
"constructor": kw("constructor"),

// scope modifiers
"public": kw("public"),
"private": kw("private"),
"protected": kw("protected"),
"static": kw("static"),

"super": kw("super"),

// types
"string": type, "number": type, "bool": type, "any": type
};

for (var attr in tsKeywords) {
jsKeywords[attr] = tsKeywords[attr];
}
}

return jsKeywords;
}();

var isOperatorChar = /[+\-*&%=<>!?|]/;
Expand Down Expand Up @@ -66,7 +99,8 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
stream.skipToEnd();
return ret("comment", "comment");
}
else if (state.reAllowed) {
else if (state.lastType == "operator" || state.lastType == "keyword c" ||
/^[\[{}\(,;:]$/.test(state.lastType)) {
nextUntilUnescaped(stream, "/");
stream.eatWhile(/[gimy]/); // 'y' is "sticky" option in Mozilla
return ret("regexp", "string-2");
Expand All @@ -87,7 +121,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
else {
stream.eatWhile(/[\w\$_]/);
var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word];
return (known && state.kwAllowed) ? ret(known.type, known.style, word) :
return (known && state.lastType != ".") ? ret(known.type, known.style, word) :
ret("variable", "variable", word);
}
}
Expand Down Expand Up @@ -275,19 +309,30 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == "}") return cont();
return pass(statement, block);
}
function maybetype(type) {
if (type == ":") return cont(typedef);
return pass();
}
function typedef(type) {
if (type == "variable"){cx.marked = "variable-3"; return cont();}
return pass();
}
function vardef1(type, value) {
if (type == "variable"){register(value); return cont(vardef2);}
return cont();
if (type == "variable") {
register(value);
return isTS ? cont(maybetype, vardef2) : cont(vardef2);
}
return pass();
}
function vardef2(type, value) {
if (value == "=") return cont(expression, vardef2);
if (type == ",") return cont(vardef1);
}
function forspec1(type) {
if (type == "var") return cont(vardef1, forspec2);
if (type == ";") return pass(forspec2);
if (type == "var") return cont(vardef1, expect(";"), forspec2);
if (type == ";") return cont(forspec2);
if (type == "variable") return cont(formaybein);
return pass(forspec2);
return cont(forspec2);
}
function formaybein(type, value) {
if (value == "in") return cont(expression);
Expand All @@ -306,7 +351,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (type == "(") return cont(pushlex(")"), pushcontext, commasep(funarg, ")"), poplex, statement, popcontext);
}
function funarg(type, value) {
if (type == "variable") {register(value); return cont();}
if (type == "variable") {register(value); return isTS ? cont(maybetype) : cont();}
}

// Interface
Expand All @@ -315,8 +360,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
startState: function(basecolumn) {
return {
tokenize: jsTokenBase,
reAllowed: true,
kwAllowed: true,
lastType: null,
cc: [],
lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
localVars: parserConfig.localVars,
Expand All @@ -334,19 +378,21 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {
if (stream.eatSpace()) return null;
var style = state.tokenize(stream, state);
if (type == "comment") return style;
state.reAllowed = !!(type == "operator" || type == "keyword c" || type.match(/^[\[{}\(,;:]$/));
state.kwAllowed = type != '.';
state.lastType = type;
return parseJS(state, style, type, content, stream);
},

indent: function(state, textAfter) {
if (state.tokenize == jsTokenComment) return CodeMirror.Pass;
if (state.tokenize != jsTokenBase) return 0;
var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical;
if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev;
var type = lexical.type, closing = firstChar == type;
if (type == "vardef") return lexical.indented + 4;
if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? 4 : 0);
else if (type == "form" && firstChar == "{") return lexical.indented;
else if (type == "stat" || type == "form") return lexical.indented + indentUnit;
else if (type == "form") return lexical.indented + indentUnit;
else if (type == "stat")
return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? indentUnit : 0);
else if (lexical.info == "switch" && !closing)
return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
else if (lexical.align) return lexical.column + (closing ? 0 : 1);
Expand All @@ -359,3 +405,5 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) {

CodeMirror.defineMIME("text/javascript", "javascript");
CodeMirror.defineMIME("application/json", {name: "javascript", json: true});
CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true });
CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true });
48 changes: 48 additions & 0 deletions mode/javascript/typescript.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: TypeScript mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
<script src="javascript.js"></script>
<link rel="stylesheet" href="../../doc/docs.css">
<style type="text/css">.CodeMirror {border-top: 1px solid black; border-bottom: 1px solid black;}</style>
</head>
<body>
<h1>CodeMirror: TypeScript mode</h1>

<div><textarea id="code" name="code">
class Greeter {
greeting: string;
constructor (message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}

var greeter = new Greeter("world");

var button = document.createElement('button')
button.innerText = "Say Hello"
button.onclick = function() {
alert(greeter.greet())
}

document.body.appendChild(button)

</textarea></div>

<script>
var editor = CodeMirror.fromTextArea(document.getElementById("code"), {
lineNumbers: true,
matchBrackets: true,
mode: "text/typescript"
});
</script>

<p>This is a specialization of the <a href="index.html">JavaScript mode</a>.</p>
</body>
</html>
2 changes: 1 addition & 1 deletion mode/lua/lua.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ CodeMirror.defineMode("lua", function(config, parserConfig) {
function normal(stream, state) {
var ch = stream.next();
if (ch == "-" && stream.eat("-")) {
if (stream.eat("["))
if (stream.eat("[") && stream.eat("["))
return (state.cur = bracketed(readBracket(stream), "comment"))(stream, state);
stream.skipToEnd();
return "comment";
Expand Down
145 changes: 122 additions & 23 deletions mode/markdown/markdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,46 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {

var htmlFound = CodeMirror.mimeModes.hasOwnProperty("text/html");
var htmlMode = CodeMirror.getMode(cmCfg, htmlFound ? "text/html" : "text/plain");
var aliases = {
html: "htmlmixed",
js: "javascript",
json: "application/json",
c: "text/x-csrc",
"c++": "text/x-c++src",
java: "text/x-java",
csharp: "text/x-csharp",
"c#": "text/x-csharp"
};

var getMode = (function () {
var i, modes = {}, mimes = {}, mime;

var list = CodeMirror.listModes();
for (i = 0; i < list.length; i++) {
modes[list[i]] = list[i];
}
var mimesList = CodeMirror.listMIMEs();
for (i = 0; i < mimesList.length; i++) {
mime = mimesList[i].mime;
mimes[mime] = mimesList[i].mime;
}

for (var a in aliases) {
if (aliases[a] in modes || aliases[a] in mimes)
modes[a] = aliases[a];
}

return function (lang) {
return modes[lang] ? CodeMirror.getMode(cmCfg, modes[lang]) : null;
};
}());

// Should underscores in words open/close em/strong?
if (modeCfg.underscoresBreakWords === undefined)
modeCfg.underscoresBreakWords = true;

// Turn on fenced code blocks? ("```" to start/end)
if (modeCfg.fencedCodeBlocks === undefined) modeCfg.fencedCodeBlocks = false;

var codeDepth = 0;
var prevLineHasContent = false
Expand All @@ -12,6 +52,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
, quote = 'quote'
, list = 'string'
, hr = 'hr'
, image = 'tag'
, linkinline = 'link'
, linkemail = 'link'
, linktext = 'link'
Expand All @@ -24,7 +65,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
, ulRE = /^[*\-+]\s+/
, olRE = /^[0-9]+\.\s+/
, headerRE = /^(?:\={1,}|-{1,})$/
, textRE = /^[^\[*_\\<>` "'(]+/;
, textRE = /^[^!\[\]*_\\<>` "'(]+/;

function switchInline(stream, state, f) {
state.f = state.inline = f;
Expand All @@ -42,8 +83,6 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
function blankLine(state) {
// Reset linkTitle state
state.linkTitle = false;
// Reset CODE state
state.code = false;
// Reset EM state
state.em = false;
// Reset STRONG state
Expand All @@ -58,7 +97,6 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
}

function blockNormal(stream, state) {
var match;

if (state.list !== false && state.indentationDiff >= 0) { // Continued list
if (state.indentationDiff < 4) { // Only adjust indentation if *not* a code block
Expand All @@ -84,9 +122,15 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
return switchInline(stream, state, footnoteLink);
} else if (stream.match(hrRE, true)) {
return hr;
} else if (match = stream.match(ulRE, true) || stream.match(olRE, true)) {
} else if (stream.match(ulRE, true) || stream.match(olRE, true)) {
state.indentation += 4;
state.list = true;
} else if (modeCfg.fencedCodeBlocks && stream.match(/^```([\w+#]*)/, true)) {
// try switching mode
state.localMode = getMode(RegExp.$1);
if (state.localMode) state.localState = state.localMode.startState();
switchBlock(stream, state, local);
return code;
}

return switchInline(stream, state, state.inline);
Expand All @@ -106,6 +150,30 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
return style;
}

function local(stream, state) {
if (stream.sol() && stream.match(/^```/, true)) {
state.localMode = state.localState = null;
state.f = inlineNormal;
state.block = blockNormal;
return code;
} else if (state.localMode) {
return state.localMode.token(stream, state.localState);
} else {
stream.skipToEnd();
return code;
}
}

function codeBlock(stream, state) {
if(stream.match(codeBlockRE, true)){
state.f = inlineNormal;
state.block = blockNormal;
switchInline(stream, state, state.inline);
return code;
}
stream.skipToEnd();
return code;
}

// Inline
function getType(state) {
Expand All @@ -114,6 +182,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
if (state.strong) { styles.push(state.em ? emstrong : strong); }
else if (state.em) { styles.push(em); }

if (state.linkText) { styles.push(linktext); }

if (state.code) { styles.push(code); }

if (state.header) { styles.push(header); }
Expand Down Expand Up @@ -161,6 +231,7 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
}
}

// If this block is changed, it may need to be updated in GFM mode
if (ch === '`') {
var t = getType(state);
var before = stream.pos;
Expand All @@ -181,8 +252,22 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
return getType(state);
}

if (ch === '[' && stream.match(/.*\] ?(?:\(|\[)/, false)) {
return switchInline(stream, state, linkText);
if (ch === '!' && stream.match(/\[.*\] ?(?:\(|\[)/, false)) {
stream.match(/\[.*\]/);
state.inline = state.f = linkHref;
return image;
}

if (ch === '[' && stream.match(/.*\](\(| ?\[)/, false)) {
state.linkText = true;
return getType(state);
}

if (ch === ']' && state.linkText) {
var type = getType(state);
state.linkText = false;
state.inline = state.f = linkHref;
return type;
}

if (ch === '<' && stream.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/, true)) {
Expand Down Expand Up @@ -210,8 +295,20 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
return "tag";
}

var ignoreUnderscore = false;
if (!modeCfg.underscoresBreakWords) {
if (ch === '_' && stream.peek() !== '_' && stream.match(/(\w)/, false)) {
var prevPos = stream.pos - 2;
if (prevPos >= 0) {
var prevCh = stream.string.charAt(prevPos);
if (prevCh !== '_' && prevCh.match(/(\w)/, false)) {
ignoreUnderscore = true;
}
}
}
}
var t = getType(state);
if (ch === '*' || ch === '_') {
if (ch === '*' || (ch === '_' && !ignoreUnderscore)) {
if (state.strong === ch && stream.eat(ch)) { // Remove STRONG
state.strong = false;
return t;
Expand All @@ -238,18 +335,6 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
return getType(state);
}

function linkText(stream, state) {
while (!stream.eol()) {
var ch = stream.next();
if (ch === '\\') stream.next();
if (ch === ']') {
state.inline = state.f = linkHref;
return linktext;
}
}
return linktext;
}

function linkHref(stream, state) {
// Check if space, and return NULL if so (to avoid marking the space)
if(stream.eatSpace()){
Expand Down Expand Up @@ -287,15 +372,16 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
return linkhref;
}

var savedInlineRE = [];
function inlineRE(endChar) {
if (!inlineRE[endChar]) {
if (!savedInlineRE[endChar]) {
// Escape endChar for RegExp (taken from http://stackoverflow.com/a/494122/526741)
endChar = (endChar+'').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
// Match any non-endChar, escaped character, as well as the closing
// endChar.
inlineRE[endChar] = new RegExp('^(?:[^\\\\]+?|\\\\.)*?(' + endChar + ')');
savedInlineRE[endChar] = new RegExp('^(?:[^\\\\]|\\\\.)*?(' + endChar + ')');
}
return inlineRE[endChar];
return savedInlineRE[endChar];
}

function inlineElement(type, endChar, next) {
Expand All @@ -309,6 +395,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {

return {
startState: function() {
prevLineHasContent = false;
thisLineHasContent = false;
return {
f: blockNormal,

Expand All @@ -318,6 +406,8 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {

inline: inlineNormal,
text: handleText,

linkText: false,
linkTitle: false,
em: false,
strong: false,
Expand All @@ -334,6 +424,9 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
block: s.block,
htmlState: CodeMirror.copyState(htmlMode, s.htmlState),
indentation: s.indentation,

localMode: s.localMode,
localState: s.localMode ? CodeMirror.copyState(s.localMode, s.localState) : null,

inline: s.inline,
text: s.text,
Expand Down Expand Up @@ -362,9 +455,15 @@ CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {

// Reset state.header
state.header = false;

// Reset state.code
state.code = false;

state.f = state.block;
var indentation = stream.match(/^\s*/, true)[0].replace(/\t/g, ' ').length;
var difference = Math.floor((indentation - state.indentation) / 4) * 4;
if (difference > 4) difference = 4;
indentation = state.indentation + difference;
state.indentationDiff = indentation - state.indentation;
state.indentation = indentation;
if (indentation > 0) { return null; }
Expand Down
184 changes: 183 additions & 1 deletion mode/markdown/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,36 @@ MT.testMode(
'comment', 'foo'
]
);
// Code blocks using 4 spaces with internal indentation
MT.testMode(
'codeBlocksUsing4SpacesIndentation',
' bar\n hello\n world\n foo\nbar',
[
null, ' ',
'comment', 'bar',
null, ' ',
'comment', 'hello',
null, ' ',
'comment', 'world',
null, ' ',
'comment', 'foo',
null, 'bar'
]
);
// Code blocks using 4 spaces with internal indentation
MT.testMode(
'codeBlocksUsing4SpacesIndentation',
' foo\n bar\n hello\n world',
[
null, ' foo',
null, ' ',
'comment', 'bar',
null, ' ',
'comment', 'hello',
null, ' ',
'comment', 'world'
]
);

// Code blocks using 1 tab (regardless of CodeMirror.indentWithTabs value)
MT.testMode(
Expand All @@ -41,6 +71,17 @@ MT.testMode(
]
);

// Block code using single backtick (shouldn't work)
MT.testMode(
'blockCodeSingleBacktick',
'`\nfoo\n`',
[
'comment', '`',
null, 'foo',
'comment', '`'
]
);

// Unclosed backticks
// Instead of simply marking as CODE, it would be nice to have an
// incomplete flag for CODE, that is styled slightly different.
Expand Down Expand Up @@ -584,6 +625,24 @@ MT.testMode(
'comment', 'hello'
]
);
// Code with internal indentation
MT.testMode(
'listCodeIndentation',
'* foo\n\n bar\n hello\n world\n foo\n bar',
[
'string', '* foo',
null, ' ',
'comment', 'bar',
null, ' ',
'comment', 'hello',
null, ' ',
'comment', 'world',
null, ' ',
'comment', 'foo',
null, ' ',
'string', 'bar'
]
);
// Code followed by text
MT.testMode(
'listCodeText',
Expand Down Expand Up @@ -660,6 +719,91 @@ MT.testMode(
]
);

// Inline link with Em
MT.testMode(
'linkEm',
'[*foo*](http://example.com/) bar',
[
'link', '[',
'link em', '*foo*',
'link', ']',
'string', '(http://example.com/)',
null, ' bar'
]
);

// Inline link with Strong
MT.testMode(
'linkStrong',
'[**foo**](http://example.com/) bar',
[
'link', '[',
'link strong', '**foo**',
'link', ']',
'string', '(http://example.com/)',
null, ' bar'
]
);

// Inline link with EmStrong
MT.testMode(
'linkEmStrong',
'[***foo***](http://example.com/) bar',
[
'link', '[',
'link strong', '**',
'link emstrong', '*foo**',
'link em', '*',
'link', ']',
'string', '(http://example.com/)',
null, ' bar'
]
);

// Image with title
MT.testMode(
'imageTitle',
'![foo](http://example.com/ "bar") hello',
[
'tag', '![foo]',
'string', '(http://example.com/ "bar")',
null, ' hello'
]
);

// Image without title
MT.testMode(
'imageNoTitle',
'![foo](http://example.com/) bar',
[
'tag', '![foo]',
'string', '(http://example.com/)',
null, ' bar'
]
);

// Image with asterisks
MT.testMode(
'imageAsterisks',
'![*foo*](http://example.com/) bar',
[
'tag', '![*foo*]',
'string', '(http://example.com/)',
null, ' bar'
]
);

// Not a link. Should be normal text due to square brackets being used
// regularly in text, especially in quoted material, and no space is allowed
// between square brackets and parentheses (per Dingus).
MT.testMode(
'notALink',
'[foo] (bar)',
[
null, '[foo] (bar)'
]
);

// Reference-style links
MT.testMode(
'linkReference',
Expand All @@ -670,6 +814,44 @@ MT.testMode(
null, ' hello'
]
);
// Reference-style links with Em
MT.testMode(
'linkReferenceEm',
'[*foo*][bar] hello',
[
'link', '[',
'link em', '*foo*',
'link', ']',
'string', '[bar]',
null, ' hello'
]
);
// Reference-style links with Strong
MT.testMode(
'linkReferenceStrong',
'[**foo**][bar] hello',
[
'link', '[',
'link strong', '**foo**',
'link', ']',
'string', '[bar]',
null, ' hello'
]
);
// Reference-style links with EmStrong
MT.testMode(
'linkReferenceEmStrong',
'[***foo***][bar] hello',
[
'link', '[',
'link strong', '**',
'link emstrong', '*foo**',
'link em', '*',
'link', ']',
'string', '[bar]',
null, ' hello'
]
);

// Reference-style links with optional space separator (per docuentation)
// "You can optionally use a space to separate the sets of brackets"
Expand Down Expand Up @@ -1081,4 +1263,4 @@ MT.testMode(
[
null, '\\\\# foo'
]
);
);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codemirror",
"version":"2.34.0",
"version":"2.35.0",
"main": "codemirror.js",
"description": "In-browser code editing made bearable",
"licenses": [{"type": "MIT",
Expand Down
3 changes: 3 additions & 0 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<link rel="stylesheet" href="../doc/docs.css">
<link rel="stylesheet" href="mode_test.css">
<script src="../lib/codemirror.js"></script>
<script src="../lib/util/overlay.js"></script>
<script src="../mode/javascript/javascript.js"></script>
<script src="../mode/xml/xml.js"></script>

Expand Down Expand Up @@ -44,6 +45,8 @@ <h1>CodeMirror: Test Suite</h1>
<script src="../mode/css/test.js"></script>
<script src="../mode/markdown/markdown.js"></script>
<script src="../mode/markdown/test.js"></script>
<script src="../mode/gfm/gfm.js"></script>
<script src="../mode/gfm/test.js"></script>
<script src="../mode/stex/stex.js"></script>
<script src="../mode/stex/test.js"></script>
<script>
Expand Down
17 changes: 11 additions & 6 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,16 +287,21 @@ testCM("markTextMultiLine", function(cm) {
});

testCM("markTextUndo", function(cm) {
var marker1 = cm.markText({line: 0, ch: 1}, {line: 0, ch: 3}, "CodeMirror-matchingbracket");
var marker2 = cm.markText({line: 0, ch: 0}, {line: 2, ch: 1}, "CodeMirror-matchingbracket");
var bookmark = cm.setBookmark({line: 1, ch: 5});
cm.replaceRange("foo", {line: 0, ch: 2});
cm.replaceRange("bar\baz\bug\n", {line: 2, ch: 0}, {line: 3, ch: 0});
var marker1, marker2, bookmark;
cm.compoundChange(function(){
marker1 = cm.markText({line: 0, ch: 1}, {line: 0, ch: 3}, "CodeMirror-matchingbracket");
marker2 = cm.markText({line: 0, ch: 0}, {line: 2, ch: 1}, "CodeMirror-matchingbracket");
bookmark = cm.setBookmark({line: 1, ch: 5});
});
cm.compoundChange(function(){
cm.replaceRange("foo", {line: 0, ch: 2});
cm.replaceRange("bar\baz\bug\n", {line: 2, ch: 0}, {line: 3, ch: 0});
});
cm.setValue("");
eq(marker1.find(), null); eq(marker2.find(), null); eq(bookmark.find(), null);
cm.undo();
eqPos(bookmark.find(), {line: 1, ch: 5});
cm.undo(); cm.undo();
cm.undo();
var m1Pos = marker1.find(), m2Pos = marker2.find();
eqPos(m1Pos.from, {line: 0, ch: 1}); eqPos(m1Pos.to, {line: 0, ch: 3});
eqPos(m2Pos.from, {line: 0, ch: 0}); eqPos(m2Pos.to, {line: 2, ch: 1});
Expand Down