mirror of
				https://github.com/mickael-kerjean/filestash.git
				synced 2025-10-31 18:16:00 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			283 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			283 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import 'codemirror/addon/mode/simple';
 | |
| import {
 | |
|     org_cycle, org_shifttab, org_metaleft, org_metaright, org_meta_return, org_metaup,
 | |
|     org_metadown, org_insert_todo_heading, org_shiftleft, org_shiftright, fold, unfold,
 | |
|     isFold, org_set_fold, org_shiftmetaleft, org_shiftmetaright
 | |
| } from './emacs-org';
 | |
| import { pathBuilder, dirname } from '../../../helpers/';
 | |
| let CodeMirror = window.CodeMirror;
 | |
| 
 | |
| CodeMirror.__mode = 'orgmode';
 | |
| 
 | |
| 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]+\:|)$/, 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]+\:|)$/, 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_[A-Z]*/, token: "comment", next: "env"}, // comments
 | |
|         {regex: /:?[A-Z_]+\:.*/, token: "comment"}, // property drawers
 | |
|         {regex: /(\#\+[a-zA-Z_]*)(\:.*)/, token: ["keyword", 'qualifier']}, // environments
 | |
|         {regex: /(CLOCK\:|SHEDULED\:|DEADLINE\:)(\s.+)/, token: ["comment", "keyword"]}
 | |
|     ],
 | |
|     env: [
 | |
|         {regex: /.*?\#\+END_[A-Z]*/, token: "comment", next: "start"},
 | |
|         {regex: /.*/, token: "comment"}
 | |
|     ]
 | |
| });
 | |
| CodeMirror.registerHelper("fold", "orgmode", function(cm, start) {
 | |
|     // init
 | |
|     const levelToMatch = headerLevel(start.line);
 | |
| 
 | |
|     // no folding needed
 | |
|     if(levelToMatch === null) return;
 | |
| 
 | |
|     // find folding limits
 | |
|     const lastLine = cm.lastLine();
 | |
|     let end = start.line;
 | |
|     while(end < lastLine){
 | |
|         end += 1;
 | |
|         let 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)
 | |
|     };
 | |
| 
 | |
|     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;
 | |
|     }
 | |
| });
 | |
| CodeMirror.registerGlobalHelper("fold", "drawer", function(mode) {
 | |
|     return mode.name === 'orgmode' ? true : false;
 | |
| }, function(cm, start) {
 | |
|     const drawer = isBeginningOfADrawer(start.line);
 | |
|     if(drawer === false) return;
 | |
| 
 | |
|     // find folding limits
 | |
|     const lastLine = cm.lastLine();
 | |
|     let 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)
 | |
|     };
 | |
| 
 | |
|     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;
 | |
|     }
 | |
| });
 | |
| 
 | |
| 
 | |
| CodeMirror.registerHelper("orgmode", "init", (editor, fn) => {
 | |
|     editor.setOption("extraKeys", {
 | |
|         "Tab": function(cm) { org_cycle(cm); },
 | |
|         "Shift-Tab": function(cm){ fn('shifttab', 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); }
 | |
|     });
 | |
|     fn('shifttab', org_set_fold(editor));
 | |
| 
 | |
|     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", (editor) => {
 | |
|     editor.off('mousedown', toggleHandler);
 | |
|     editor.off('touchstart', toggleHandler);
 | |
|     editor.off('gutterClick', foldLine);
 | |
| });
 | |
| 
 | |
| function foldLine(cm, line){
 | |
|     const cursor = {line: line, ch: 0};
 | |
|     isFold(cm, cursor) ? unfold(cm, cursor) : fold(cm, cursor);
 | |
| }
 | |
| 
 | |
| 
 | |
| let widgets = [];
 | |
| function toggleHandler(cm, e){
 | |
|     const 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(){
 | |
|         const line = position.line;
 | |
|         if(line >= 0){
 | |
|             const cursor = {line: line, ch: 0};
 | |
|             isFold(cm, cursor) ? unfold(cm, cursor) : fold(cm, cursor);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     function _toggleCheckbox(){
 | |
|         const line = position.line;
 | |
|         const content = cm.getRange({line: line, ch: token.start}, {line: line, ch: token.end});
 | |
|         let new_content = content === "[X]" || content === "[x]" ? "[ ]" : "[X]";
 | |
|         cm.replaceRange(new_content, {line: line, ch: token.start}, {line: line, ch: token.end});
 | |
|     }
 | |
| 
 | |
|     function _toggleTodo(){
 | |
|         const line = position.line;
 | |
|         cm.replaceRange("DONE", {line: line, ch: token.start}, {line: line, ch: token.end});
 | |
|     }
 | |
| 
 | |
|     function _toggleDone(){
 | |
|         const line = position.line;
 | |
|         cm.replaceRange("TODO", {line: line, ch: token.start}, {line: line, ch: token.end});
 | |
|     }
 | |
| 
 | |
|     function _togglePriority(){
 | |
|         const PRIORITIES = [" [#A] ", " [#B] ", " [#C] ", " [#A] "];
 | |
|         const line = position.line;
 | |
|         const content = cm.getRange({line: line, ch: token.start}, {line: line, ch: token.end});
 | |
|         let new_content = PRIORITIES[PRIORITIES.indexOf(content) + 1];
 | |
|         cm.replaceRange(new_content, {line: line, ch: token.start}, {line: line, ch: token.end});
 | |
|     }
 | |
| 
 | |
|     function _toggleImageWidget(){
 | |
|         let exist = !!widgets
 | |
|             .filter((line) => line === position.line)[0];
 | |
| 
 | |
|         if(exist === false){
 | |
|             if(!token.string.match(/\[\[(.*)\]\]/)) return null;
 | |
|             let $node = _buildImage(RegExp.$1);
 | |
|             const 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((line) => line !== position.line);
 | |
|             }
 | |
|         }
 | |
|         function _buildImage(src){
 | |
|             let $el = document.createElement("div");
 | |
|             let $img = document.createElement("img");
 | |
| 
 | |
|             if(/^https?\:\/\//.test(src)){
 | |
|                 $img.src = src;
 | |
|             }else{
 | |
|                 const root_path = dirname(window.location.pathname.replace(/^\/view/, ''));
 | |
|                 const 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(/\[\[(.*?)\]\[/);
 | |
|         const link = RegExp.$1;
 | |
|         if(!link) return;
 | |
| 
 | |
|         if(/^https?\:\/\//.test(link)){
 | |
|             window.open(link);
 | |
|         }else{
 | |
|             const root_path = dirname(window.location.pathname.replace(/^\/view/, ''));
 | |
|             const link_path = link;
 | |
|             window.open("/view"+pathBuilder(root_path, link_path));
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| 
 | |
| export default CodeMirror;
 | 
