cryptpad/www/code/orgmode.js

1063 lines
37 KiB
JavaScript
Raw Normal View History

define([
'cm/lib/codemirror',
'cm/addon/mode/simple'
], function (CodeMirror) {
// Hack to avoid breaking a document with ```__proto__ codeblock
CodeMirror.defineSimpleMode("__proto__", {
start: [ {regex: /.*/, token: "comment"} ],
env: [ {regex: /.*/, token: "comment"} ]
});
CodeMirror.registerHelper("fold", "__proto__", function(cm, start) {
return {
from: CodeMirror.Pos(0,0),
to: CodeMirror.Pos(0,0)
};
});
CodeMirror.__mode = 'orgmode';
2020-07-10 14:31:42 +00:00
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: [
2020-07-10 14:31:42 +00:00
{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"]},
2020-07-10 14:31:42 +00:00
{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)
};
});
var init = false;
2020-07-10 14:31:42 +00:00
CodeMirror.registerHelper("orgmode", "init", function (editor) {
if (init) { return; }
2020-07-10 14:31:42 +00:00
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); }
});
init = true;
2020-07-10 14:31:42 +00:00
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) {
if (!init) { return; }
init = false;
2020-07-10 14:31:42 +00:00
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));
}
}
});
};
2020-07-10 14:31:42 +00:00
*/
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
});
}
}
});