1 CodeMirror.defineMode("markdown", function(cmCfg, modeCfg) {
3 var htmlFound = CodeMirror.modes.hasOwnProperty("xml");
4 var htmlMode = CodeMirror.getMode(cmCfg, htmlFound ? {name: "xml", htmlMode: true} : "text/plain");
8 json: "application/json",
10 "c++": "text/x-c++src",
12 csharp: "text/x-csharp",
13 "c#": "text/x-csharp",
17 var getMode = (function () {
18 var i, modes = {}, mimes = {}, mime;
21 for (var m in CodeMirror.modes)
22 if (CodeMirror.modes.propertyIsEnumerable(m)) list.push(m);
23 for (i = 0; i < list.length; i++) {
24 modes[list[i]] = list[i];
27 for (var m in CodeMirror.mimeModes)
28 if (CodeMirror.mimeModes.propertyIsEnumerable(m))
29 mimesList.push({mime: m, mode: CodeMirror.mimeModes[m]});
30 for (i = 0; i < mimesList.length; i++) {
31 mime = mimesList[i].mime;
32 mimes[mime] = mimesList[i].mime;
35 for (var a in aliases) {
36 if (aliases[a] in modes || aliases[a] in mimes)
37 modes[a] = aliases[a];
40 return function (lang) {
41 return modes[lang] ? CodeMirror.getMode(cmCfg, modes[lang]) : null;
45 // Should underscores in words open/close em/strong?
46 if (modeCfg.underscoresBreakWords === undefined)
47 modeCfg.underscoresBreakWords = true;
49 // Turn on fenced code blocks? ("```" to start/end)
50 if (modeCfg.fencedCodeBlocks === undefined) modeCfg.fencedCodeBlocks = false;
52 // Turn on task lists? ("- [ ] " and "- [x] ")
53 if (modeCfg.taskLists === undefined) modeCfg.taskLists = false;
61 , list1 = 'variable-2'
62 , list2 = 'variable-3'
73 var hrRE = /^([*\-=_])(?:\s*\1){2,}\s*$/
75 , olRE = /^[0-9]+\.\s+/
76 , taskListRE = /^\[(x| )\](?=\s)/ // Must follow ulRE or olRE
77 , headerRE = /^(?:\={1,}|-{1,})$/
78 , textRE = /^[^!\[\]*_\\<>` "'(]+/;
80 function switchInline(stream, state, f) {
81 state.f = state.inline = f;
82 return f(stream, state);
85 function switchBlock(stream, state, f) {
86 state.f = state.block = f;
87 return f(stream, state);
93 function blankLine(state) {
94 // Reset linkTitle state
95 state.linkTitle = false;
102 if (!htmlFound && state.f == htmlBlock) {
103 state.f = inlineNormal;
104 state.block = blockNormal;
106 // Reset state.trailingSpace
107 state.trailingSpace = 0;
108 state.trailingSpaceNewLine = false;
109 // Mark this line as blank
110 state.thisLineHasContent = false;
114 function blockNormal(stream, state) {
116 var prevLineIsList = (state.list !== false);
117 if (state.list !== false && state.indentationDiff >= 0) { // Continued list
118 if (state.indentationDiff < 4) { // Only adjust indentation if *not* a code block
119 state.indentation -= state.indentationDiff;
122 } else if (state.list !== false && state.indentation > 0) {
124 state.listDepth = Math.floor(state.indentation / 4);
125 } else if (state.list !== false) { // No longer a list
130 if (state.indentationDiff >= 4) {
131 state.indentation -= 4;
134 } else if (stream.eatSpace()) {
136 } else if (stream.peek() === '#' || (state.prevLineHasContent && stream.match(headerRE)) ) {
138 } else if (stream.eat('>')) {
142 while (stream.eat('>')) {
146 } else if (stream.peek() === '[') {
147 return switchInline(stream, state, footnoteLink);
148 } else if (stream.match(hrRE, true)) {
150 } else if ((!state.prevLineHasContent || prevLineIsList) && (stream.match(ulRE, true) || stream.match(olRE, true))) {
151 state.indentation += 4;
154 if (modeCfg.taskLists && stream.match(taskListRE, false)) {
155 state.taskList = true;
157 } else if (modeCfg.fencedCodeBlocks && stream.match(/^```([\w+#]*)/, true)) {
158 // try switching mode
159 state.localMode = getMode(RegExp.$1);
160 if (state.localMode) state.localState = state.localMode.startState();
161 switchBlock(stream, state, local);
165 return switchInline(stream, state, state.inline);
168 function htmlBlock(stream, state) {
169 var style = htmlMode.token(stream, state.htmlState);
170 if (htmlFound && style === 'tag' && state.htmlState.type !== 'openTag' && !state.htmlState.context) {
171 state.f = inlineNormal;
172 state.block = blockNormal;
174 if (state.md_inside && stream.current().indexOf(">")!=-1) {
175 state.f = inlineNormal;
176 state.block = blockNormal;
177 state.htmlState.context = undefined;
182 function local(stream, state) {
183 if (stream.sol() && stream.match(/^```/, true)) {
184 state.localMode = state.localState = null;
185 state.f = inlineNormal;
186 state.block = blockNormal;
188 } else if (state.localMode) {
189 return state.localMode.token(stream, state.localState);
197 function getType(state) {
200 if (state.taskOpen) { return "meta"; }
201 if (state.taskClosed) { return "property"; }
203 if (state.strong) { styles.push(strong); }
204 if (state.em) { styles.push(em); }
206 if (state.linkText) { styles.push(linktext); }
208 if (state.code) { styles.push(code); }
210 if (state.header) { styles.push(header); }
211 if (state.quote) { styles.push(state.quote % 2 ? quote1 : quote2); }
212 if (state.list !== false) {
213 var listMod = (state.listDepth - 1) % 3;
216 } else if (listMod === 1) {
223 if (state.trailingSpaceNewLine) {
224 styles.push("trailing-space-new-line");
225 } else if (state.trailingSpace) {
226 styles.push("trailing-space-" + (state.trailingSpace % 2 ? "a" : "b"));
229 return styles.length ? styles.join(' ') : null;
232 function handleText(stream, state) {
233 if (stream.match(textRE, true)) {
234 return getType(state);
239 function inlineNormal(stream, state) {
240 var style = state.text(stream, state);
241 if (typeof style !== 'undefined')
244 if (state.list) { // List marker (*, +, -, 1., etc)
246 return getType(state);
249 if (state.taskList) {
250 var taskOpen = stream.match(taskListRE, true)[1] !== "x";
251 if (taskOpen) state.taskOpen = true;
252 else state.taskClosed = true;
253 state.taskList = false;
254 return getType(state);
257 state.taskOpen = false;
258 state.taskClosed = false;
260 var ch = stream.next();
264 return getType(state);
267 // Matches link titles present on next line
268 if (state.linkTitle) {
269 state.linkTitle = false;
274 matchCh = (matchCh+'').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
275 var regex = '^\\s*(?:[^' + matchCh + '\\\\]+|\\\\\\\\|\\\\.)' + matchCh;
276 if (stream.match(new RegExp(regex), true)) {
281 // If this block is changed, it may need to be updated in GFM mode
283 var t = getType(state);
284 var before = stream.pos;
285 stream.eatWhile('`');
286 var difference = 1 + stream.pos - before;
288 codeDepth = difference;
290 return getType(state);
292 if (difference === codeDepth) { // Must be exact
296 return getType(state);
298 } else if (state.code) {
299 return getType(state);
302 if (ch === '!' && stream.match(/\[[^\]]*\] ?(?:\(|\[)/, false)) {
303 stream.match(/\[[^\]]*\]/);
304 state.inline = state.f = linkHref;
308 if (ch === '[' && stream.match(/.*\](\(| ?\[)/, false)) {
309 state.linkText = true;
310 return getType(state);
313 if (ch === ']' && state.linkText) {
314 var type = getType(state);
315 state.linkText = false;
316 state.inline = state.f = linkHref;
320 if (ch === '<' && stream.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/, false)) {
321 return switchInline(stream, state, inlineElement(linkinline, '>'));
324 if (ch === '<' && stream.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/, false)) {
325 return switchInline(stream, state, inlineElement(linkemail, '>'));
328 if (ch === '<' && stream.match(/^\w/, false)) {
329 if (stream.string.indexOf(">")!=-1) {
330 var atts = stream.string.substring(1,stream.string.indexOf(">"));
331 if (/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(atts)) {
332 state.md_inside = true;
336 return switchBlock(stream, state, htmlBlock);
339 if (ch === '<' && stream.match(/^\/\w*?>/)) {
340 state.md_inside = false;
344 var ignoreUnderscore = false;
345 if (!modeCfg.underscoresBreakWords) {
346 if (ch === '_' && stream.peek() !== '_' && stream.match(/(\w)/, false)) {
347 var prevPos = stream.pos - 2;
349 var prevCh = stream.string.charAt(prevPos);
350 if (prevCh !== '_' && prevCh.match(/(\w)/, false)) {
351 ignoreUnderscore = true;
356 var t = getType(state);
357 if (ch === '*' || (ch === '_' && !ignoreUnderscore)) {
358 if (state.strong === ch && stream.eat(ch)) { // Remove STRONG
359 state.strong = false;
361 } else if (!state.strong && stream.eat(ch)) { // Add STRONG
363 return getType(state);
364 } else if (state.em === ch) { // Remove EM
367 } else if (!state.em) { // Add EM
369 return getType(state);
371 } else if (ch === ' ') {
372 if (stream.eat('*') || stream.eat('_')) { // Probably surrounded by spaces
373 if (stream.peek() === ' ') { // Surrounded by spaces, ignore
374 return getType(state);
375 } else { // Not surrounded by spaces, back up pointer
382 if (stream.match(/ +$/, false)) {
383 state.trailingSpace++;
384 } else if (state.trailingSpace) {
385 state.trailingSpaceNewLine = true;
389 return getType(state);
392 function linkHref(stream, state) {
393 // Check if space, and return NULL if so (to avoid marking the space)
394 if(stream.eatSpace()){
397 var ch = stream.next();
398 if (ch === '(' || ch === '[') {
399 return switchInline(stream, state, inlineElement(linkhref, ch === '(' ? ')' : ']'));
404 function footnoteLink(stream, state) {
405 if (stream.match(/^[^\]]*\]:/, true)) {
406 state.f = footnoteUrl;
409 return switchInline(stream, state, inlineNormal);
412 function footnoteUrl(stream, state) {
413 // Check if space, and return NULL if so (to avoid marking the space)
414 if(stream.eatSpace()){
418 stream.match(/^[^\s]+/, true);
419 // Check for link title
420 if (stream.peek() === undefined) { // End of line, set flag to check next line
421 state.linkTitle = true;
422 } else { // More content on line, check if link title
423 stream.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/, true);
425 state.f = state.inline = inlineNormal;
429 var savedInlineRE = [];
430 function inlineRE(endChar) {
431 if (!savedInlineRE[endChar]) {
432 // Escape endChar for RegExp (taken from http://stackoverflow.com/a/494122/526741)
433 endChar = (endChar+'').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
434 // Match any non-endChar, escaped character, as well as the closing
436 savedInlineRE[endChar] = new RegExp('^(?:[^\\\\]|\\\\.)*?(' + endChar + ')');
438 return savedInlineRE[endChar];
441 function inlineElement(type, endChar, next) {
442 next = next || inlineNormal;
443 return function(stream, state) {
444 stream.match(inlineRE(endChar));
445 state.inline = state.f = next;
451 startState: function() {
455 prevLineHasContent: false,
456 thisLineHasContent: false,
459 htmlState: CodeMirror.startState(htmlMode),
462 inline: inlineNormal,
475 trailingSpaceNewLine: false
479 copyState: function(s) {
483 prevLineHasContent: s.prevLineHasContent,
484 thisLineHasContent: s.thisLineHasContent,
487 htmlState: CodeMirror.copyState(htmlMode, s.htmlState),
488 indentation: s.indentation,
490 localMode: s.localMode,
491 localState: s.localMode ? CodeMirror.copyState(s.localMode, s.localState) : null,
495 linkTitle: s.linkTitle,
499 taskList: s.taskList,
501 listDepth: s.listDepth,
503 trailingSpace: s.trailingSpace,
504 trailingSpaceNewLine: s.trailingSpaceNewLine,
505 md_inside: s.md_inside
509 token: function(stream, state) {
511 if (stream.match(/^\s*$/, true)) {
512 state.prevLineHasContent = false;
513 return blankLine(state);
515 state.prevLineHasContent = state.thisLineHasContent;
516 state.thisLineHasContent = true;
519 // Reset state.header
520 state.header = false;
522 // Reset state.taskList
523 state.taskList = false;
528 // Reset state.trailingSpace
529 state.trailingSpace = 0;
530 state.trailingSpaceNewLine = false;
532 state.f = state.block;
533 var indentation = stream.match(/^\s*/, true)[0].replace(/\t/g, ' ').length;
534 var difference = Math.floor((indentation - state.indentation) / 4) * 4;
535 if (difference > 4) difference = 4;
536 var adjustedIndentation = state.indentation + difference;
537 state.indentationDiff = adjustedIndentation - state.indentation;
538 state.indentation = adjustedIndentation;
539 if (indentation > 0) return null;
541 return state.f(stream, state);
544 blankLine: blankLine,
551 CodeMirror.defineMIME("text/x-markdown", "markdown");