define([ 'cm/lib/codemirror', 'cm/addon/mode/simple' ], function (CodeMirror) { CodeMirror.__mode = 'orgmode'; var isEmpty = function (el, idx) { if (idx < 2) { return true; } return !Boolean(el); }; var onLevelOne = function (matches) { // If all the elements starting from index 2 are empty, remove them // because it means it's an empty header for now and it may break codemirror if (matches && matches.length > 2 && matches.every(isEmpty)) { matches.splice(2, (matches.length-2)); } return ["header level1 org-level-star","header level1 org-todo","header level1 org-done", "header level1 org-priority", "header level1", "header level1 void", "header level1 comment"]; }; // Dirty hack to make the function also work as an array onLevelOne().forEach(function (str, i) { onLevelOne[i] = str; }); var onLevelStar = function (matches) { // If all the elements starting from index 2 are empty, remove them // because it means it's an empty header for now and it may break codemirror if (matches && matches.length > 2 && matches.every(isEmpty)) { matches.splice(2, (matches.length-2)); } return ["header org-level-star","header org-todo","header org-done", "header org-priority", "header", "header void", "header comment"]; }; // Dirty hack to make the function also work as an array onLevelStar().forEach(function (str, i) { onLevelStar[i] = str; }); CodeMirror.defineSimpleMode("orgmode", { start: [ {regex: /(\*\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: onLevelOne}, {regex: /(\*{1,}\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/g, sol: true, token: onLevelStar}, /* {regex: /(\*\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header level1 org-level-star","header level1 org-todo","header level1 org-done", "header level1 org-priority", "header level1", "header level1 void", "header level1 comment"]}, {regex: /(\*{1,}\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/g, sol: true, token: ["header org-level-star","header org-todo","header org-done", "header org-priority", "header", "header void", "header comment"]}, */ {regex: /(\+[^\+]+\+)/, token: ["strikethrough"]}, {regex: /(\*[^\*]+\*)/, token: ["strong"]}, {regex: /(\/[^\/]+\/)/, token: ["em"]}, {regex: /(\_[^\_]+\_)/, token: ["link"]}, {regex: /(\~[^\~]+\~)/, token: ["comment"]}, {regex: /(\=[^\=]+\=)/, token: ["comment"]}, {regex: /\[\[[^\[\]]+\]\[[^\[\]]+\]\]/, token: "org-url"}, // links {regex: /\[\[[^\[\]]+\]\]/, token: "org-image"}, // image {regex: /\[[xX\s\-\_]\]/, token: 'qualifier org-toggle'}, // checkbox {regex: /\#\+(?:(BEGIN|begin))_[a-zA-Z]*/, token: "comment", next: "env", sol: true}, // comments {regex: /:?[A-Z_]+\:.*/, token: "comment", sol: true}, // property drawers {regex: /(\#\+[a-zA-Z_]*)(\:.*)/, token: ["keyword", 'qualifier'], sol: true}, // environments {regex: /(CLOCK\:|SHEDULED\:|DEADLINE\:)(\s.+)/, token: ["comment", "keyword"]} ], env: [ {regex: /\#\+(?:(END|end))_[a-zA-Z]*/, token: "comment", next: "start", sol: true}, {regex: /.*/, token: "comment"} ] }); CodeMirror.registerHelper("fold", "orgmode", function(cm, start) { function headerLevel (lineNo) { var line = cm.getLine(lineNo); var match = /^\*+/.exec(line); if (match && match.length === 1 && /header/.test(cm.getTokenTypeAt(CodeMirror.Pos(lineNo, 0)))) { return match[0].length; } return null; } // init var levelToMatch = headerLevel(start.line); // no folding needed if(levelToMatch === null) { return; } // find folding limits var lastLine = cm.lastLine(); var end = start.line; while (end < lastLine){ end += 1; var level = headerLevel(end); if (level && level <= levelToMatch) { end = end - 1; break; } } return { from: CodeMirror.Pos(start.line, cm.getLine(start.line).length), to: CodeMirror.Pos(end, cm.getLine(end).length) }; }); CodeMirror.registerGlobalHelper("fold", "drawer", function(mode) { return mode.name === 'orgmode' ? true : false; }, function(cm, start) { function isBeginningOfADrawer(lineNo) { var line = cm.getLine(lineNo); var match = /^\:.*\:$/.exec(line); if(match && match.length === 1 && match[0] !== ':END:'){ return true; } return false; } function isEndOfADrawer(lineNo){ var line = cm.getLine(lineNo); return line.trim() === ':END:' ? true : false; } var drawer = isBeginningOfADrawer(start.line); if (drawer === false) { return; } // find folding limits var lastLine = cm.lastLine(); var end = start.line; while(end < lastLine){ end += 1; if (isEndOfADrawer(end)) { break; } } return { from: CodeMirror.Pos(start.line, cm.getLine(start.line).length), to: CodeMirror.Pos(end, cm.getLine(end).length) }; }); CodeMirror.registerHelper("orgmode", "init", function (editor) { editor.setOption("extraKeys", { "Tab": function(cm) { org_cycle(cm); }, "Shift-Tab": function(cm){ org_shifttab(cm); }, "Alt-Left": function(cm){ org_metaleft(cm); }, "Alt-Right": function(cm){ org_metaright(cm); }, "Alt-Enter": function(cm){ org_meta_return(cm); }, "Alt-Up": function(cm){ org_metaup(cm); }, "Alt-Down": function(cm){ org_metadown(cm); }, "Shift-Alt-Left": function(cm){ org_shiftmetaleft(cm); }, "Shift-Alt-Right": function(cm){ org_shiftmetaright(cm); }, "Shift-Alt-Enter": function(cm){ org_insert_todo_heading(cm); }, "Shift-Left": function(cm){ org_shiftleft(cm); }, "Shift-Right": function(cm){ org_shiftright(cm); } }); editor.on('mousedown', toggleHandler); editor.on('touchstart', toggleHandler); editor.on('gutterClick', foldLine); // fold everything except headers by default editor.operation(function() { for (var i = 0; i < editor.lineCount() ; i++) { if(/header/.test(editor.getTokenTypeAt(CodeMirror.Pos(i, 0))) === false){ fold(editor, CodeMirror.Pos(i, 0)); } } }); return CodeMirror.orgmode.destroy.bind(this, editor); }); CodeMirror.registerHelper("orgmode", "destroy", function (editor) { editor.off('mousedown', toggleHandler); editor.off('touchstart', toggleHandler); editor.off('gutterClick', foldLine); // Restore CryptPad shortcuts if (typeof (editor.updateSettings) === "function") { editor.updateSettings(); } }); function foldLine (cm, line){ var cursor = {line: line, ch: 0}; isFold(cm, cursor) ? unfold(cm, cursor) : fold(cm, cursor); } var widgets = []; function toggleHandler (cm, e){ var position = cm.coordsChar({ left: e.clientX || (e.targetTouches && e.targetTouches[0].clientX), top: e.clientY || (e.targetTouches && e.targetTouches[0].clientY) }, "page"), token = cm.getTokenAt(position); _disableSelection(); if(/org-level-star/.test(token.type)){ _preventIfShould(); _foldHeadline(); _disableSelection(); }else if(/org-toggle/.test(token.type)){ _preventIfShould(); _toggleCheckbox(); _disableSelection(); }else if(/org-todo/.test(token.type)){ _preventIfShould(); _toggleTodo(); _disableSelection(); }else if(/org-done/.test(token.type)){ _preventIfShould(); _toggleDone(); _disableSelection(); }else if(/org-priority/.test(token.type)){ _preventIfShould(); _togglePriority(); _disableSelection(); }else if(/org-url/.test(token.type)){ _disableSelection(); _navigateLink(); }else if(/org-image/.test(token.type)){ _disableSelection(); _toggleImageWidget(); } function _preventIfShould(){ if('ontouchstart' in window) e.preventDefault(); } function _disableSelection(){ cm.on('beforeSelectionChange', _onSelectionChangeHandler); function _onSelectionChangeHandler(cm, obj){ obj.update([{ anchor: position, head: position }]); cm.off('beforeSelectionChange', _onSelectionChangeHandler); } } function _foldHeadline(){ var line = position.line; if(line >= 0){ var cursor = {line: line, ch: 0}; isFold(cm, cursor) ? unfold(cm, cursor) : fold(cm, cursor); } } function _toggleCheckbox(){ var line = position.line; var content = cm.getRange({line: line, ch: token.start}, {line: line, ch: token.end}); var new_content = content === "[X]" || content === "[x]" ? "[ ]" : "[X]"; cm.replaceRange(new_content, {line: line, ch: token.start}, {line: line, ch: token.end}); } function _toggleTodo(){ var line = position.line; cm.replaceRange("DONE", {line: line, ch: token.start}, {line: line, ch: token.end}); } function _toggleDone(){ var line = position.line; cm.replaceRange("TODO", {line: line, ch: token.start}, {line: line, ch: token.end}); } function _togglePriority(){ var PRIORITIES = [" [#A] ", " [#B] ", " [#C] ", " [#A] "]; var line = position.line; var content = cm.getRange({line: line, ch: token.start}, {line: line, ch: token.end}); var new_content = PRIORITIES[PRIORITIES.indexOf(content) + 1]; cm.replaceRange(new_content, {line: line, ch: token.start}, {line: line, ch: token.end}); } function _toggleImageWidget(){ var exist = !!widgets .filter(function (line) { return line === position.line; })[0]; if(exist === false){ if(!token.string.match(/\[\[(.*)\]\]/)) return null; var $node = _buildImage(RegExp.$1); var widget = cm.addLineWidget(position.line, $node, {coverGutter: false}); widgets.push(position.line); $node.addEventListener('click', closeWidget); function closeWidget(){ widget.clear(); $node.removeEventListener('click', closeWidget); widgets = widgets.filter(function (line) { return line !== position.line; }); } } function _buildImage(src){ var $el = document.createElement("div"); var $img = document.createElement("img"); if(/^https?\:\/\//.test(src)){ $img.src = src; }else{ var root_path = dirname(window.location.pathname.replace(/^\/view/, '')); var img_path = src; $img.src = "/api/files/cat?path="+encodeURIComponent(pathBuilder(root_path, img_path)); } $el.appendChild($img); return $el; } return null; } function _navigateLink(){ token.string.match(/\[\[(.*?)\]\[/); var link = RegExp.$1; if(!link) return; if(/^https?\:\/\//.test(link)){ window.open(link); }else{ var root_path = dirname(window.location.pathname.replace(/^\/view/, '')); var link_path = link; window.open("/view"+pathBuilder(root_path, link_path)); } } } CodeMirror.defineMIME("text/org", "org"); function fold(cm, start){ cm.foldCode(start, null, "fold"); } function unfold(cm, start){ cm.foldCode(start, null, "unfold"); } function isFold(cm, start){ var line = start.line; var marks = cm.findMarks(CodeMirror.Pos(line, 0), CodeMirror.Pos(line + 1, 0)); for (var i = 0; i < marks.length; ++i) { if (marks[i].__isFold && marks[i].find().from.line === line) { return marks[i]; } } return false; } /* CodeMirror.afterInit = function(editor){ function fold(cm, start){ cm.foldCode(start, null, "fold"); } function unfold(cm, start){ cm.foldCode(start, null, "unfold"); } function isFold(cm, start){ var line = start.line; var marks = cm.findMarks(CodeMirror.Pos(line, 0), CodeMirror.Pos(line + 1, 0)); for (var i = 0; i < marks.length; ++i) { if (marks[i].__isFold && marks[i].find().from.line === line) { return marks[i]; } } return false; } var state = { stab: 'OVERVIEW' }; editor.setOption("extraKeys", { "Tab": function(cm) { var pos = cm.getCursor(); return isFold(cm, pos) ? unfold(cm, pos) : fold(cm, pos); }, "Shift-Tab": function(cm){ if(state.stab === "SHOW_ALL"){ // fold everything that can be fold state.stab = 'OVERVIEW'; cm.operation(function() { for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++){ fold(cm, CodeMirror.Pos(i, 0)); } }); }else{ // unfold all headers state.stab = 'SHOW_ALL'; cm.operation(function() { for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++){ if(/header/.test(cm.getTokenTypeAt(CodeMirror.Pos(i, 0))) === true){ unfold(cm, CodeMirror.Pos(i, 0)); } } }); } } }); editor.on('touchstart', function(cm){ setTimeout(function () { return isFold(cm, cm.getCursor()) ? unfold(cm, cm.getCursor()) : fold(cm, cm.getCursor()); }, 150); }); // fold everything except headers by default editor.operation(function() { for (var i = 0; i < editor.lineCount() ; i++) { if(/header/.test(editor.getTokenTypeAt(CodeMirror.Pos(i, 0))) === false){ fold(editor, CodeMirror.Pos(i, 0)); } } }); }; */ var org_cycle = function (cm) { var pos = cm.getCursor(); isFold(cm, pos) ? unfold(cm, pos) : fold(cm, pos); }; var state = { stab: 'CONTENT' }; var org_set_fold = function (cm) { var cursor = cm.getCursor(); set_folding_mode(cm, state.stab); cm.setCursor(cursor); return state.stab; }; /* * DONE: Global visibility cycling * TODO: or move to previous table field. */ var org_shifttab = function (cm) { if(state.stab === "SHOW_ALL"){ state.stab = 'OVERVIEW'; }else if(state.stab === "OVERVIEW"){ state.stab = 'CONTENT'; }else if(state.stab === "CONTENT"){ state.stab = 'SHOW_ALL'; } set_folding_mode(cm, state.stab); return state.stab; }; function set_folding_mode(cm, mode){ if(mode === "OVERVIEW"){ folding_mode_overview(cm); }else if(mode === "SHOW_ALL"){ folding_mode_all(cm); }else if(mode === "CONTENT"){ folding_mode_content(cm); } cm.refresh(); function folding_mode_overview(cm){ cm.operation(function() { for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++){ fold(cm, CodeMirror.Pos(i, 0)); } }); } function folding_mode_content(cm){ cm.operation(function() { var previous_header = null; for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++){ fold(cm, CodeMirror.Pos(i, 0)); if(/header/.test(cm.getTokenTypeAt(CodeMirror.Pos(i, 0))) === true){ var level = cm.getLine(i).replace(/^(\*+).*/, "$1").length; if(previous_header && level > previous_header.level){ unfold(cm, CodeMirror.Pos(previous_header.line, 0)); } previous_header = { line: i, level: level }; } } }); } function folding_mode_all(cm){ cm.operation(function() { for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++){ if(/header/.test(cm.getTokenTypeAt(CodeMirror.Pos(i, 0))) === true){ unfold(cm, CodeMirror.Pos(i, 0)); } } }); } } /* * Promote heading or move table column to left. */ var org_metaleft = function (cm) { var line = cm.getCursor().line; _metaleft(cm, line); }; function _metaleft(cm, line){ var p = null; if(p = isTitle(cm, line)){ if(p['level'] > 1) cm.replaceRange('', {line: p.start, ch: 0}, {line: p.start, ch: 1}); }else if(p = isItemList(cm, line)){ for(var i=p.start; i<=p.end; i++){ if(p['level'] > 0) cm.replaceRange('', {line: i, ch: 0}, {line: i, ch: 2}); } }else if(p = isNumberedList(cm, line)){ for(var i=p.start; i<=p.end; i++){ if(p['level'] > 0) cm.replaceRange('', {line: i, ch: 0}, {line: i, ch: 3}); } rearrange_list(cm, line); } } /* * Demote a subtree, a list item or move table column to right. * In front of a drawer or a block keyword, indent it correctly. */ var org_metaright = function (cm){ var line = cm.getCursor().line; _metaright(cm, line); }; function _metaright(cm, line) { var p = null, tmp = null; if(p = isTitle(cm, line)){ cm.replaceRange('*', {line: p.start, ch: 0}); }else if(p = isItemList(cm, line)){ if(tmp = isItemList(cm, p.start - 1)){ if(p.level < tmp.level + 1){ for(var i=p.start; i<=p.end; i++){ cm.replaceRange(' ', {line: i, ch: 0}); } } } }else if(p = isNumberedList(cm, line)){ if(tmp = isNumberedList(cm, p.start - 1)){ if(p.level < tmp.level + 1){ for(var i=p.start; i<=p.end; i++){ cm.replaceRange(' ', {line: i, ch: 0}); } rearrange_list(cm, p.start); } } } } /* * Insert a new heading or wrap a region in a table */ var org_meta_return = function (cm) { var line = cm.getCursor().line, content = cm.getLine(line); var p = null; if(p = isItemList(cm, line)){ var level = p.level; cm.replaceRange('\n'+" ".repeat(level*2)+'- ', {line: p.end, ch: cm.getLine(p.end).length}); cm.setCursor({line: p.end+1, ch: level*2+2}); }else if(p = isNumberedList(cm, line)){ var level = p.level; cm.replaceRange('\n'+" ".repeat(level*3)+(p.n+1)+'. ', {line: p.end, ch: cm.getLine(p.end).length}); cm.setCursor({line: p.end+1, ch: level*3+3}); rearrange_list(cm, line); }else if(p = isTitle(cm, line)){ var tmp = previousOfType(cm, 'title', line); var level = tmp && tmp.level || 1; cm.replaceRange('\n'+'*'.repeat(level)+' ', {line: line, ch: content.length}); cm.setCursor({line: line+1, ch: level+1}); }else if(content.trim() === ""){ cm.replaceRange('* ', {line: line, ch: 0}); cm.setCursor({line: line, ch: 2}); }else{ cm.replaceRange('\n\n* ', {line: line, ch: content.length}); cm.setCursor({line: line + 2, ch: 2}); } }; var TODO_CYCLES = ["TODO", "DONE", ""]; /* * Cycle the thing at point or in the current line, depending on context. * Depending on context, this does one of the following: * - TODO: switch a timestamp at point one day into the past * - DONE: on a headline, switch to the previous TODO keyword. * - TODO: on an item, switch entire list to the previous bulvar type * - TODO: on a property line, switch to the previous allowed value * - TODO: on a clocktable definition line, move time block into the past */ var org_shiftleft = function (cm) { var cycles = [].concat(TODO_CYCLES.slice(0).reverse(), TODO_CYCLES.slice(-1)), line = cm.getCursor().line, content = cm.getLine(line), params = isTitle(cm, line); if(params === null) return; params['status'] = cycles[cycles.indexOf(params['status']) + 1]; cm.replaceRange(makeTitle(params), {line: line, ch: 0}, {line: line, ch: content.length}); }; /* * Cycle the thing at point or in the current line, depending on context. * Depending on context, this does one of the following: * - TODO: switch a timestamp at point one day into the future * - DONE: on a headline, switch to the next TODO keyword. * - TODO: on an item, switch entire list to the next bulvar type * - TODO: on a property line, switch to the next allowed value * - TODO: on a clocktable definition line, move time block into the future */ var org_shiftright = function (cm) { cm.operation(function () { var cycles = [].concat(TODO_CYCLES, [TODO_CYCLES[0]]), line = cm.getCursor().line, content = cm.getLine(line), params = isTitle(cm, line); if(params === null) return; params['status'] = cycles[cycles.indexOf(params['status']) + 1]; cm.replaceRange(makeTitle(params), {line: line, ch: 0}, {line: line, ch: content.length}); }); }; var org_insert_todo_heading = function (cm) { cm.operation(function () { var line = cm.getCursor().line, content = cm.getLine(line); var p = null; if(p = isItemList(cm, line)){ var level = p.level; cm.replaceRange('\n'+" ".repeat(level*2)+'- [ ] ', {line: p.end, ch: cm.getLine(p.end).length}); cm.setCursor({line: line+1, ch: 6+level*2}); }else if(p = isNumberedList(cm, line)){ var level = p.level; cm.replaceRange('\n'+" ".repeat(level*3)+(p.n+1)+'. [ ] ', {line: p.end, ch: cm.getLine(p.end).length}); cm.setCursor({line: p.end+1, ch: level*3+7}); rearrange_list(cm, line); }else if(p = isTitle(cm, line)){ var level = p && p.level || 1; cm.replaceRange('\n'+"*".repeat(level)+' TODO ', {line: line, ch: content.length}); cm.setCursor({line: line+1, ch: level+6}); }else if(content.trim() === ""){ cm.replaceRange('* TODO ', {line: line, ch: 0}); cm.setCursor({line: line, ch: 7}); }else{ cm.replaceRange('\n\n* TODO ', {line: line, ch: content.length}); cm.setCursor({line: line + 2, ch: 7}); } }); } /* * Move subtree up or move table row up. * Calls ‘org-move-subtree-up’ or ‘org-table-move-row’ or * ‘org-move-item-up’, depending on context */ var org_metaup = function (cm) { cm.operation(function () { var line = cm.getCursor().line; var p = null; if(p = isItemList(cm, line)){ var a = isItemList(cm, p.start - 1); if(a){ swap(cm, [p.start, p.end], [a.start, a.end]); rearrange_list(cm, line); } }else if(p = isNumberedList(cm, line)){ var a = isNumberedList(cm, p.start - 1); if(a){ swap(cm, [p.start, p.end], [a.start, a.end]); rearrange_list(cm, line); } }else if(p = isTitle(cm, line)){ var _line = line, a; do{ _line -= 1; if(a = isTitle(cm, _line, p.level)){ break; } }while(_line > 0); if(a){ swap(cm, [p.start, p.end], [a.start, a.end]); org_set_fold(cm); } } }); } /* * Move subtree down or move table row down. * Calls ‘org-move-subtree-down’ or ‘org-table-move-row’ or * ‘org-move-item-down’, depending on context */ var org_metadown = function (cm) { cm.operation(function () { var line = cm.getCursor().line; var p = null; if(p = isItemList(cm, line)){ var a = isItemList(cm, p.end + 1); if(a){ swap(cm, [p.start, p.end], [a.start, a.end]); } }else if(p = isNumberedList(cm, line)){ var a = isNumberedList(cm, p.end + 1); if(a){ swap(cm, [p.start, p.end], [a.start, a.end]); } rearrange_list(cm, line); }else if(p = isTitle(cm, line)){ var a = isTitle(cm, p.end + 1, p.level); if(a){ swap(cm, [p.start, p.end], [a.start, a.end]); org_set_fold(cm); } } }); } var org_shiftmetaright = function(cm) { cm.operation(function () { var line = cm.getCursor().line; var p = null; if(p = isTitle(cm, line)){ _metaright(cm, line); for(var i=p.start + 1; i<=p.end; i++){ if(isTitle(cm, i)){ _metaright(cm, i); } } } }); }; var org_shiftmetaleft = function(cm){ cm.operation(function () { var line = cm.getCursor().line; var p = null; if(p = isTitle(cm, line)){ if(p.level === 1) return; _metaleft(cm, line); for(var i=p.start + 1; i<=p.end; i++){ if(isTitle(cm, i)){ _metaleft(cm, i); } } } }); }; function makeTitle(p){ var content = "*".repeat(p['level'])+" "; if(p['status']){ content += p['status']+" "; } content += p['content']; return content; } function previousOfType(cm, type, line){ var content, tmp, i; for(i=line - 1; i>0; i--){ if(type === 'list' || type === null){ tmp = isItemList(cm, line); }else if(type === 'numbered' || type === null){ tmp = isNumberedList(cm, line); }else if(type === 'title' || type === null){ tmp = isTitle(cm, line); } if(tmp !== null){ return tmp; } } return null; } function isItemList(cm, line){ var rootLineItem = findRootLine(cm, line); if(rootLineItem === null) return null; line = rootLineItem; var content = cm.getLine(line); if(content && (content.trimLeft()[0] !== "-" || content.trimLeft()[1] !== " ")) return null; var padding = content.replace(/^(\s*).*$/, "$1").length; if(padding % 2 !== 0) return null; return { type: 'list', level: padding / 2, content: content.trimLeft().replace(/^\s*\-\s(.*)$/, '$1'), start: line, end: function(_cm, _line){ var line_candidate = _line, content = null; do{ _line += 1; content = _cm.getLine(_line); if(content === undefined || content.trimLeft()[0] === "-"){ break; }else if(/^\s+/.test(content)){ line_candidate = _line; continue; }else{ break; } }while(_line <= _cm.lineCount()) return line_candidate; }(cm, line) }; function findRootLine(_cm, _line){ var content; do{ content = _cm.getLine(_line); if(/^\s*\-/.test(content)) return _line; else if(/^\s+/.test(content) === false){ break; } _line -= 1; }while(_line >= 0); return null; } } function isNumberedList(cm, line){ var rootLineItem = findRootLine(cm, line); if(rootLineItem === null) return null; line = rootLineItem; var content = cm.getLine(line); if(/^[0-9]+[\.\)]\s.*$/.test(content && content.trimLeft()) === false) return null; var padding = content.replace(/^(\s*)[0-9]+.*$/, "$1").length; if(padding % 3 !== 0) return null; return { type: 'numbered', level: padding / 3, content: content.trimLeft().replace(/^[0-9]+[\.\)]\s(.*)$/, '$1'), start: line, end: function(_cm, _line){ var line_candidate = _line, content = null; do{ _line += 1; content = _cm.getLine(_line); if(content === undefined || /^[0-9]+[\.\)]/.test(content.trimLeft())){ break; }else if(/^\s+/.test(content)){ line_candidate = _line; continue; }else{ break; } }while(_line <= _cm.lineCount()) return line_candidate; }(cm, line), // specific n: parseInt(content.trimLeft().replace(/^([0-9]+).*$/, "$1")), separator: content.trimLeft().replace(/^[0-9]+([\.\)]).*$/, '$1') }; function findRootLine(_cm, _line){ var content; do{ content = _cm.getLine(_line); if(/^\s*[0-9]+[\.\)]\s/.test(content)) return _line; else if(/^\s+/.test(content) === false){ break; } _line -= 1; }while(_line >= 0); return null; } } function isTitle(cm, line, level){ var content = cm.getLine(line); if(/^\*+\s/.test(content) === false) return null; var match = content.match(/^(\*+)([\sA-Z]*)\s(.*)$/); var reference_level = match[1].length; if(level !== undefined && level !== reference_level){ return null; } if(match === null) return null; return { type: 'title', level: reference_level, content: match[3], start: line, end: function(_cm, _line){ var line_candidate = _line, content = null; do{ _line += 1; content = _cm.getLine(_line); if(content === undefined) break; var match = content.match(/^(\*+)\s.*/); if(match && match[1] && ( match[1].length === reference_level || match[1].length < reference_level)){ break; }else{ line_candidate = _line; continue; } }while(_line <= _cm.lineCount()) return line_candidate; }(cm, line), // specific status: match[2].trim() }; } function rearrange_list(cm, line){ var line_inferior = find_limit_inferior(cm, line); var line_superior = find_limit_superior(cm, line); var last_p = null, p; for(var i=line_inferior; i<=line_superior; i++){ if(p = isNumberedList(cm, i)){ // rearrange numbers on the numbered list if(last_p){ if(p.level === last_p.level){ var tmp = findLastAtLevel(cm, p.start, line_inferior, p.level); if(tmp && p.n !== tmp.n + 1) setNumber(cm, p.start, tmp.n + 1); }else if(p.level > last_p.level){ if(p.n !== 1){ setNumber(cm, p.start, 1); } }else if(p.level < last_p.level){ var tmp = findLastAtLevel(cm, p.start, line_inferior, p.level); if(tmp && p.n !== tmp.n + 1) setNumber(cm, p.start, tmp.n + 1); } }else{ if(p.n !== 1){ setNumber(cm, p.start, 1); } } } if(p = (isNumberedList(cm, i) || isItemList(cm, i))){ // rearrange spacing levels in list if(last_p){ if(p.level > last_p.level){ if(p.level !== last_p.level + 1){ setLevel(cm, [p.start, p.end], last_p.level + 1, p.type); } } }else{ if(p.level !== 0){ setLevel(cm, [p.start, p.end], 0, p.type); } } } last_p = p; // we can process content block instead of line if(p){ i += (p.end - p.start); } } function findLastAtLevel(_cm, line, line_limit_inf, level){ var p; do{ line -= 1; if((p = isNumberedList(_cm, line)) && p.level === level) return p; }while(line > line_limit_inf); return null; } function setLevel(_cm, range, level, type){ var content, i; for(i=range[0]; i<=range[1]; i++){ content = cm.getLine(i).trimLeft(); var n_spaces = function(_level, _line, _type){ var spaces = _level * 3; if(_line > 0){ spaces += _type === 'numbered' ? 3 : 2; } return spaces; }(level, i - range[0], type) content = " ".repeat(n_spaces) + content; cm.replaceRange(content, {line: i, ch: 0}, {line: i, ch: _cm.getLine(i).length}); } } function setNumber(_cm, line, level){ var content = _cm.getLine(line); var new_content = content.replace(/[0-9]+\./, level+"."); cm.replaceRange(new_content, {line: line, ch: 0}, {line: line, ch: content.length}); } function find_limit_inferior(_cm, _line){ var content, p, match, line_candidate = _line; do{ content = _cm.getLine(_line); p = isNumberedList(_cm, _line); match = /(\s+).*$/.exec(content); if(p){ line_candidate = _line;} if(!p || !match) break; _line -= 1; }while(_line >= 0); return line_candidate; } function find_limit_superior(_cm, _line){ var content, p, match, line_candidate = _line; do{ content = _cm.getLine(_line); p = isNumberedList(_cm, _line); match = /(\s+).*$/.exec(content); if(p){ line_candidate = _line;} if(!p || !match) break; _line += 1; }while(_line < _cm.lineCount()); return line_candidate; } } function swap(cm, from, to){ var from_content = cm.getRange({line: from[0], ch: 0}, {line: from[1], ch: cm.getLine(from[1]).length}), to_content = cm.getRange({line: to[0], ch: 0}, {line: to[1], ch: cm.getLine(to[1]).length}), cursor = cm.getCursor(); if(to[0] > from[0]){ // moving down cm.replaceRange( from_content, {line: to[0], ch:0}, {line: to[1], ch: cm.getLine(to[1]).length} ); cm.replaceRange( to_content, {line: from[0], ch:0}, {line: from[1], ch: cm.getLine(from[1]).length} ); cm.setCursor({ line: cursor.line + (to[1] - to[0] + 1), ch: cursor.ch }); }else{ // moving up cm.replaceRange( to_content, {line: from[0], ch:0}, {line: from[1], ch: cm.getLine(from[1]).length} ); cm.replaceRange( from_content, {line: to[0], ch:0}, {line: to[1], ch: cm.getLine(to[1]).length} ); cm.setCursor({ line: cursor.line - (to[1] - to[0] + 1), ch: cursor.ch }); } } });