mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-27 01:51:34 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			576 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			576 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| // CodeMirror, copyright (c) by Marijn Haverbeke and others
 | |
| // Distributed under an MIT license: https://codemirror.net/5/LICENSE
 | |
| 
 | |
| // Slim Highlighting for CodeMirror copyright (c) HicknHack Software Gmbh
 | |
| 
 | |
| (function(mod) {
 | |
|   if (typeof exports == "object" && typeof module == "object") // CommonJS
 | |
|     mod(require("../../lib/codemirror"), require("../htmlmixed/htmlmixed"), require("../ruby/ruby"));
 | |
|   else if (typeof define == "function" && define.amd) // AMD
 | |
|     define(["../../lib/codemirror", "../htmlmixed/htmlmixed", "../ruby/ruby"], mod);
 | |
|   else // Plain browser env
 | |
|     mod(CodeMirror);
 | |
| })(function(CodeMirror) {
 | |
| "use strict";
 | |
| 
 | |
|   CodeMirror.defineMode("slim", function(config) {
 | |
|     var htmlMode = CodeMirror.getMode(config, {name: "htmlmixed"});
 | |
|     var rubyMode = CodeMirror.getMode(config, "ruby");
 | |
|     var modes = { html: htmlMode, ruby: rubyMode };
 | |
|     var embedded = {
 | |
|       ruby: "ruby",
 | |
|       javascript: "javascript",
 | |
|       css: "text/css",
 | |
|       sass: "text/x-sass",
 | |
|       scss: "text/x-scss",
 | |
|       less: "text/x-less",
 | |
|       styl: "text/x-styl", // no highlighting so far
 | |
|       coffee: "coffeescript",
 | |
|       asciidoc: "text/x-asciidoc",
 | |
|       markdown: "text/x-markdown",
 | |
|       textile: "text/x-textile", // no highlighting so far
 | |
|       creole: "text/x-creole", // no highlighting so far
 | |
|       wiki: "text/x-wiki", // no highlighting so far
 | |
|       mediawiki: "text/x-mediawiki", // no highlighting so far
 | |
|       rdoc: "text/x-rdoc", // no highlighting so far
 | |
|       builder: "text/x-builder", // no highlighting so far
 | |
|       nokogiri: "text/x-nokogiri", // no highlighting so far
 | |
|       erb: "application/x-erb"
 | |
|     };
 | |
|     var embeddedRegexp = function(map){
 | |
|       var arr = [];
 | |
|       for(var key in map) arr.push(key);
 | |
|       return new RegExp("^("+arr.join('|')+"):");
 | |
|     }(embedded);
 | |
| 
 | |
|     var styleMap = {
 | |
|       "commentLine": "comment",
 | |
|       "slimSwitch": "operator special",
 | |
|       "slimTag": "tag",
 | |
|       "slimId": "attribute def",
 | |
|       "slimClass": "attribute qualifier",
 | |
|       "slimAttribute": "attribute",
 | |
|       "slimSubmode": "keyword special",
 | |
|       "closeAttributeTag": null,
 | |
|       "slimDoctype": null,
 | |
|       "lineContinuation": null
 | |
|     };
 | |
|     var closing = {
 | |
|       "{": "}",
 | |
|       "[": "]",
 | |
|       "(": ")"
 | |
|     };
 | |
| 
 | |
|     var nameStartChar = "_a-zA-Z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD";
 | |
|     var nameChar = nameStartChar + "\\-0-9\xB7\u0300-\u036F\u203F-\u2040";
 | |
|     var nameRegexp = new RegExp("^[:"+nameStartChar+"](?::["+nameChar+"]|["+nameChar+"]*)");
 | |
|     var attributeNameRegexp = new RegExp("^[:"+nameStartChar+"][:\\."+nameChar+"]*(?=\\s*=)");
 | |
|     var wrappedAttributeNameRegexp = new RegExp("^[:"+nameStartChar+"][:\\."+nameChar+"]*");
 | |
|     var classNameRegexp = /^\.-?[_a-zA-Z]+[\w\-]*/;
 | |
|     var classIdRegexp = /^#[_a-zA-Z]+[\w\-]*/;
 | |
| 
 | |
|     function backup(pos, tokenize, style) {
 | |
|       var restore = function(stream, state) {
 | |
|         state.tokenize = tokenize;
 | |
|         if (stream.pos < pos) {
 | |
|           stream.pos = pos;
 | |
|           return style;
 | |
|         }
 | |
|         return state.tokenize(stream, state);
 | |
|       };
 | |
|       return function(stream, state) {
 | |
|         state.tokenize = restore;
 | |
|         return tokenize(stream, state);
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     function maybeBackup(stream, state, pat, offset, style) {
 | |
|       var cur = stream.current();
 | |
|       var idx = cur.search(pat);
 | |
|       if (idx > -1) {
 | |
|         state.tokenize = backup(stream.pos, state.tokenize, style);
 | |
|         stream.backUp(cur.length - idx - offset);
 | |
|       }
 | |
|       return style;
 | |
|     }
 | |
| 
 | |
|     function continueLine(state, column) {
 | |
|       state.stack = {
 | |
|         parent: state.stack,
 | |
|         style: "continuation",
 | |
|         indented: column,
 | |
|         tokenize: state.line
 | |
|       };
 | |
|       state.line = state.tokenize;
 | |
|     }
 | |
|     function finishContinue(state) {
 | |
|       if (state.line == state.tokenize) {
 | |
|         state.line = state.stack.tokenize;
 | |
|         state.stack = state.stack.parent;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     function lineContinuable(column, tokenize) {
 | |
|       return function(stream, state) {
 | |
|         finishContinue(state);
 | |
|         if (stream.match(/^\\$/)) {
 | |
|           continueLine(state, column);
 | |
|           return "lineContinuation";
 | |
|         }
 | |
|         var style = tokenize(stream, state);
 | |
|         if (stream.eol() && stream.current().match(/(?:^|[^\\])(?:\\\\)*\\$/)) {
 | |
|           stream.backUp(1);
 | |
|         }
 | |
|         return style;
 | |
|       };
 | |
|     }
 | |
|     function commaContinuable(column, tokenize) {
 | |
|       return function(stream, state) {
 | |
|         finishContinue(state);
 | |
|         var style = tokenize(stream, state);
 | |
|         if (stream.eol() && stream.current().match(/,$/)) {
 | |
|           continueLine(state, column);
 | |
|         }
 | |
|         return style;
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     function rubyInQuote(endQuote, tokenize) {
 | |
|       // TODO: add multi line support
 | |
|       return function(stream, state) {
 | |
|         var ch = stream.peek();
 | |
|         if (ch == endQuote && state.rubyState.tokenize.length == 1) {
 | |
|           // step out of ruby context as it seems to complete processing all the braces
 | |
|           stream.next();
 | |
|           state.tokenize = tokenize;
 | |
|           return "closeAttributeTag";
 | |
|         } else {
 | |
|           return ruby(stream, state);
 | |
|         }
 | |
|       };
 | |
|     }
 | |
|     function startRubySplat(tokenize) {
 | |
|       var rubyState;
 | |
|       var runSplat = function(stream, state) {
 | |
|         if (state.rubyState.tokenize.length == 1 && !state.rubyState.context.prev) {
 | |
|           stream.backUp(1);
 | |
|           if (stream.eatSpace()) {
 | |
|             state.rubyState = rubyState;
 | |
|             state.tokenize = tokenize;
 | |
|             return tokenize(stream, state);
 | |
|           }
 | |
|           stream.next();
 | |
|         }
 | |
|         return ruby(stream, state);
 | |
|       };
 | |
|       return function(stream, state) {
 | |
|         rubyState = state.rubyState;
 | |
|         state.rubyState = CodeMirror.startState(rubyMode);
 | |
|         state.tokenize = runSplat;
 | |
|         return ruby(stream, state);
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     function ruby(stream, state) {
 | |
|       return rubyMode.token(stream, state.rubyState);
 | |
|     }
 | |
| 
 | |
|     function htmlLine(stream, state) {
 | |
|       if (stream.match(/^\\$/)) {
 | |
|         return "lineContinuation";
 | |
|       }
 | |
|       return html(stream, state);
 | |
|     }
 | |
|     function html(stream, state) {
 | |
|       if (stream.match(/^#\{/)) {
 | |
|         state.tokenize = rubyInQuote("}", state.tokenize);
 | |
|         return null;
 | |
|       }
 | |
|       return maybeBackup(stream, state, /[^\\]#\{/, 1, htmlMode.token(stream, state.htmlState));
 | |
|     }
 | |
| 
 | |
|     function startHtmlLine(lastTokenize) {
 | |
|       return function(stream, state) {
 | |
|         var style = htmlLine(stream, state);
 | |
|         if (stream.eol()) state.tokenize = lastTokenize;
 | |
|         return style;
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     function startHtmlMode(stream, state, offset) {
 | |
|       state.stack = {
 | |
|         parent: state.stack,
 | |
|         style: "html",
 | |
|         indented: stream.column() + offset, // pipe + space
 | |
|         tokenize: state.line
 | |
|       };
 | |
|       state.line = state.tokenize = html;
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     function comment(stream, state) {
 | |
|       stream.skipToEnd();
 | |
|       return state.stack.style;
 | |
|     }
 | |
| 
 | |
|     function commentMode(stream, state) {
 | |
|       state.stack = {
 | |
|         parent: state.stack,
 | |
|         style: "comment",
 | |
|         indented: state.indented + 1,
 | |
|         tokenize: state.line
 | |
|       };
 | |
|       state.line = comment;
 | |
|       return comment(stream, state);
 | |
|     }
 | |
| 
 | |
|     function attributeWrapper(stream, state) {
 | |
|       if (stream.eat(state.stack.endQuote)) {
 | |
|         state.line = state.stack.line;
 | |
|         state.tokenize = state.stack.tokenize;
 | |
|         state.stack = state.stack.parent;
 | |
|         return null;
 | |
|       }
 | |
|       if (stream.match(wrappedAttributeNameRegexp)) {
 | |
|         state.tokenize = attributeWrapperAssign;
 | |
|         return "slimAttribute";
 | |
|       }
 | |
|       stream.next();
 | |
|       return null;
 | |
|     }
 | |
|     function attributeWrapperAssign(stream, state) {
 | |
|       if (stream.match(/^==?/)) {
 | |
|         state.tokenize = attributeWrapperValue;
 | |
|         return null;
 | |
|       }
 | |
|       return attributeWrapper(stream, state);
 | |
|     }
 | |
|     function attributeWrapperValue(stream, state) {
 | |
|       var ch = stream.peek();
 | |
|       if (ch == '"' || ch == "\'") {
 | |
|         state.tokenize = readQuoted(ch, "string", true, false, attributeWrapper);
 | |
|         stream.next();
 | |
|         return state.tokenize(stream, state);
 | |
|       }
 | |
|       if (ch == '[') {
 | |
|         return startRubySplat(attributeWrapper)(stream, state);
 | |
|       }
 | |
|       if (stream.match(/^(true|false|nil)\b/)) {
 | |
|         state.tokenize = attributeWrapper;
 | |
|         return "keyword";
 | |
|       }
 | |
|       return startRubySplat(attributeWrapper)(stream, state);
 | |
|     }
 | |
| 
 | |
|     function startAttributeWrapperMode(state, endQuote, tokenize) {
 | |
|       state.stack = {
 | |
|         parent: state.stack,
 | |
|         style: "wrapper",
 | |
|         indented: state.indented + 1,
 | |
|         tokenize: tokenize,
 | |
|         line: state.line,
 | |
|         endQuote: endQuote
 | |
|       };
 | |
|       state.line = state.tokenize = attributeWrapper;
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     function sub(stream, state) {
 | |
|       if (stream.match(/^#\{/)) {
 | |
|         state.tokenize = rubyInQuote("}", state.tokenize);
 | |
|         return null;
 | |
|       }
 | |
|       var subStream = new CodeMirror.StringStream(stream.string.slice(state.stack.indented), stream.tabSize);
 | |
|       subStream.pos = stream.pos - state.stack.indented;
 | |
|       subStream.start = stream.start - state.stack.indented;
 | |
|       subStream.lastColumnPos = stream.lastColumnPos - state.stack.indented;
 | |
|       subStream.lastColumnValue = stream.lastColumnValue - state.stack.indented;
 | |
|       var style = state.subMode.token(subStream, state.subState);
 | |
|       stream.pos = subStream.pos + state.stack.indented;
 | |
|       return style;
 | |
|     }
 | |
|     function firstSub(stream, state) {
 | |
|       state.stack.indented = stream.column();
 | |
|       state.line = state.tokenize = sub;
 | |
|       return state.tokenize(stream, state);
 | |
|     }
 | |
| 
 | |
|     function createMode(mode) {
 | |
|       var query = embedded[mode];
 | |
|       var spec = CodeMirror.mimeModes[query];
 | |
|       if (spec) {
 | |
|         return CodeMirror.getMode(config, spec);
 | |
|       }
 | |
|       var factory = CodeMirror.modes[query];
 | |
|       if (factory) {
 | |
|         return factory(config, {name: query});
 | |
|       }
 | |
|       return CodeMirror.getMode(config, "null");
 | |
|     }
 | |
| 
 | |
|     function getMode(mode) {
 | |
|       if (!modes.hasOwnProperty(mode)) {
 | |
|         return modes[mode] = createMode(mode);
 | |
|       }
 | |
|       return modes[mode];
 | |
|     }
 | |
| 
 | |
|     function startSubMode(mode, state) {
 | |
|       var subMode = getMode(mode);
 | |
|       var subState = CodeMirror.startState(subMode);
 | |
| 
 | |
|       state.subMode = subMode;
 | |
|       state.subState = subState;
 | |
| 
 | |
|       state.stack = {
 | |
|         parent: state.stack,
 | |
|         style: "sub",
 | |
|         indented: state.indented + 1,
 | |
|         tokenize: state.line
 | |
|       };
 | |
|       state.line = state.tokenize = firstSub;
 | |
|       return "slimSubmode";
 | |
|     }
 | |
| 
 | |
|     function doctypeLine(stream, _state) {
 | |
|       stream.skipToEnd();
 | |
|       return "slimDoctype";
 | |
|     }
 | |
| 
 | |
|     function startLine(stream, state) {
 | |
|       var ch = stream.peek();
 | |
|       if (ch == '<') {
 | |
|         return (state.tokenize = startHtmlLine(state.tokenize))(stream, state);
 | |
|       }
 | |
|       if (stream.match(/^[|']/)) {
 | |
|         return startHtmlMode(stream, state, 1);
 | |
|       }
 | |
|       if (stream.match(/^\/(!|\[\w+])?/)) {
 | |
|         return commentMode(stream, state);
 | |
|       }
 | |
|       if (stream.match(/^(-|==?[<>]?)/)) {
 | |
|         state.tokenize = lineContinuable(stream.column(), commaContinuable(stream.column(), ruby));
 | |
|         return "slimSwitch";
 | |
|       }
 | |
|       if (stream.match(/^doctype\b/)) {
 | |
|         state.tokenize = doctypeLine;
 | |
|         return "keyword";
 | |
|       }
 | |
| 
 | |
|       var m = stream.match(embeddedRegexp);
 | |
|       if (m) {
 | |
|         return startSubMode(m[1], state);
 | |
|       }
 | |
| 
 | |
|       return slimTag(stream, state);
 | |
|     }
 | |
| 
 | |
|     function slim(stream, state) {
 | |
|       if (state.startOfLine) {
 | |
|         return startLine(stream, state);
 | |
|       }
 | |
|       return slimTag(stream, state);
 | |
|     }
 | |
| 
 | |
|     function slimTag(stream, state) {
 | |
|       if (stream.eat('*')) {
 | |
|         state.tokenize = startRubySplat(slimTagExtras);
 | |
|         return null;
 | |
|       }
 | |
|       if (stream.match(nameRegexp)) {
 | |
|         state.tokenize = slimTagExtras;
 | |
|         return "slimTag";
 | |
|       }
 | |
|       return slimClass(stream, state);
 | |
|     }
 | |
|     function slimTagExtras(stream, state) {
 | |
|       if (stream.match(/^(<>?|><?)/)) {
 | |
|         state.tokenize = slimClass;
 | |
|         return null;
 | |
|       }
 | |
|       return slimClass(stream, state);
 | |
|     }
 | |
|     function slimClass(stream, state) {
 | |
|       if (stream.match(classIdRegexp)) {
 | |
|         state.tokenize = slimClass;
 | |
|         return "slimId";
 | |
|       }
 | |
|       if (stream.match(classNameRegexp)) {
 | |
|         state.tokenize = slimClass;
 | |
|         return "slimClass";
 | |
|       }
 | |
|       return slimAttribute(stream, state);
 | |
|     }
 | |
|     function slimAttribute(stream, state) {
 | |
|       if (stream.match(/^([\[\{\(])/)) {
 | |
|         return startAttributeWrapperMode(state, closing[RegExp.$1], slimAttribute);
 | |
|       }
 | |
|       if (stream.match(attributeNameRegexp)) {
 | |
|         state.tokenize = slimAttributeAssign;
 | |
|         return "slimAttribute";
 | |
|       }
 | |
|       if (stream.peek() == '*') {
 | |
|         stream.next();
 | |
|         state.tokenize = startRubySplat(slimContent);
 | |
|         return null;
 | |
|       }
 | |
|       return slimContent(stream, state);
 | |
|     }
 | |
|     function slimAttributeAssign(stream, state) {
 | |
|       if (stream.match(/^==?/)) {
 | |
|         state.tokenize = slimAttributeValue;
 | |
|         return null;
 | |
|       }
 | |
|       // should never happen, because of forward lookup
 | |
|       return slimAttribute(stream, state);
 | |
|     }
 | |
| 
 | |
|     function slimAttributeValue(stream, state) {
 | |
|       var ch = stream.peek();
 | |
|       if (ch == '"' || ch == "\'") {
 | |
|         state.tokenize = readQuoted(ch, "string", true, false, slimAttribute);
 | |
|         stream.next();
 | |
|         return state.tokenize(stream, state);
 | |
|       }
 | |
|       if (ch == '[') {
 | |
|         return startRubySplat(slimAttribute)(stream, state);
 | |
|       }
 | |
|       if (ch == ':') {
 | |
|         return startRubySplat(slimAttributeSymbols)(stream, state);
 | |
|       }
 | |
|       if (stream.match(/^(true|false|nil)\b/)) {
 | |
|         state.tokenize = slimAttribute;
 | |
|         return "keyword";
 | |
|       }
 | |
|       return startRubySplat(slimAttribute)(stream, state);
 | |
|     }
 | |
|     function slimAttributeSymbols(stream, state) {
 | |
|       stream.backUp(1);
 | |
|       if (stream.match(/^[^\s],(?=:)/)) {
 | |
|         state.tokenize = startRubySplat(slimAttributeSymbols);
 | |
|         return null;
 | |
|       }
 | |
|       stream.next();
 | |
|       return slimAttribute(stream, state);
 | |
|     }
 | |
|     function readQuoted(quote, style, embed, unescaped, nextTokenize) {
 | |
|       return function(stream, state) {
 | |
|         finishContinue(state);
 | |
|         var fresh = stream.current().length == 0;
 | |
|         if (stream.match(/^\\$/, fresh)) {
 | |
|           if (!fresh) return style;
 | |
|           continueLine(state, state.indented);
 | |
|           return "lineContinuation";
 | |
|         }
 | |
|         if (stream.match(/^#\{/, fresh)) {
 | |
|           if (!fresh) return style;
 | |
|           state.tokenize = rubyInQuote("}", state.tokenize);
 | |
|           return null;
 | |
|         }
 | |
|         var escaped = false, ch;
 | |
|         while ((ch = stream.next()) != null) {
 | |
|           if (ch == quote && (unescaped || !escaped)) {
 | |
|             state.tokenize = nextTokenize;
 | |
|             break;
 | |
|           }
 | |
|           if (embed && ch == "#" && !escaped) {
 | |
|             if (stream.eat("{")) {
 | |
|               stream.backUp(2);
 | |
|               break;
 | |
|             }
 | |
|           }
 | |
|           escaped = !escaped && ch == "\\";
 | |
|         }
 | |
|         if (stream.eol() && escaped) {
 | |
|           stream.backUp(1);
 | |
|         }
 | |
|         return style;
 | |
|       };
 | |
|     }
 | |
|     function slimContent(stream, state) {
 | |
|       if (stream.match(/^==?/)) {
 | |
|         state.tokenize = ruby;
 | |
|         return "slimSwitch";
 | |
|       }
 | |
|       if (stream.match(/^\/$/)) { // tag close hint
 | |
|         state.tokenize = slim;
 | |
|         return null;
 | |
|       }
 | |
|       if (stream.match(/^:/)) { // inline tag
 | |
|         state.tokenize = slimTag;
 | |
|         return "slimSwitch";
 | |
|       }
 | |
|       startHtmlMode(stream, state, 0);
 | |
|       return state.tokenize(stream, state);
 | |
|     }
 | |
| 
 | |
|     var mode = {
 | |
|       // default to html mode
 | |
|       startState: function() {
 | |
|         var htmlState = CodeMirror.startState(htmlMode);
 | |
|         var rubyState = CodeMirror.startState(rubyMode);
 | |
|         return {
 | |
|           htmlState: htmlState,
 | |
|           rubyState: rubyState,
 | |
|           stack: null,
 | |
|           last: null,
 | |
|           tokenize: slim,
 | |
|           line: slim,
 | |
|           indented: 0
 | |
|         };
 | |
|       },
 | |
| 
 | |
|       copyState: function(state) {
 | |
|         return {
 | |
|           htmlState : CodeMirror.copyState(htmlMode, state.htmlState),
 | |
|           rubyState: CodeMirror.copyState(rubyMode, state.rubyState),
 | |
|           subMode: state.subMode,
 | |
|           subState: state.subMode && CodeMirror.copyState(state.subMode, state.subState),
 | |
|           stack: state.stack,
 | |
|           last: state.last,
 | |
|           tokenize: state.tokenize,
 | |
|           line: state.line
 | |
|         };
 | |
|       },
 | |
| 
 | |
|       token: function(stream, state) {
 | |
|         if (stream.sol()) {
 | |
|           state.indented = stream.indentation();
 | |
|           state.startOfLine = true;
 | |
|           state.tokenize = state.line;
 | |
|           while (state.stack && state.stack.indented > state.indented && state.last != "slimSubmode") {
 | |
|             state.line = state.tokenize = state.stack.tokenize;
 | |
|             state.stack = state.stack.parent;
 | |
|             state.subMode = null;
 | |
|             state.subState = null;
 | |
|           }
 | |
|         }
 | |
|         if (stream.eatSpace()) return null;
 | |
|         var style = state.tokenize(stream, state);
 | |
|         state.startOfLine = false;
 | |
|         if (style) state.last = style;
 | |
|         return styleMap.hasOwnProperty(style) ? styleMap[style] : style;
 | |
|       },
 | |
| 
 | |
|       blankLine: function(state) {
 | |
|         if (state.subMode && state.subMode.blankLine) {
 | |
|           return state.subMode.blankLine(state.subState);
 | |
|         }
 | |
|       },
 | |
| 
 | |
|       innerMode: function(state) {
 | |
|         if (state.subMode) return {state: state.subState, mode: state.subMode};
 | |
|         return {state: state, mode: mode};
 | |
|       }
 | |
| 
 | |
|       //indent: function(state) {
 | |
|       //  return state.indented;
 | |
|       //}
 | |
|     };
 | |
|     return mode;
 | |
|   }, "htmlmixed", "ruby");
 | |
| 
 | |
|   CodeMirror.defineMIME("text/x-slim", "slim");
 | |
|   CodeMirror.defineMIME("application/x-slim", "slim");
 | |
| });
 | 
