1// Copyright (c) 2012 Henning Kiel 2// 3// Permission is hereby granted, free of charge, to any person obtaining a copy of 4// this software and associated documentation files (the "Software"), to deal in 5// the Software without restriction, including without limitation the rights to 6// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7// the Software, and to permit persons to whom the Software is furnished to do so, 8// subject to the following conditions: 9 10// The above copyright notice and this permission notice shall be included in all 11// copies or substantial portions of the Software. 12 13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 20CodeMirror.defineMode("liquid", function(config, parserConfig) { 21 function chain(stream, state, f) { 22 state.tokenize = f; 23 return f(stream, state); 24 } 25 26 // Used as scratch variables to communicate multiple values without 27 // consing up tons of objects. 28 var type, content; 29 function ret(tp, style, cont) { 30 type = tp; content = cont; 31 return style; 32 } 33 34 function liquidTokenBase(stream, state) { 35 var ch = stream.next(); 36 if (/[\s]/.test(ch)) { 37 return ret("whitespace", "liquid-whitespace"); 38 } 39 else if (state.liquidMode == "output-markup" && ch == '}' && stream.eat('}')) { 40 state.tokenize = nonLiquid; 41 return ret("}}"); 42 } 43 else if (state.liquidMode == "tag-markup" && ch == '%' && stream.eat('}')) { 44 state.tokenize = nonLiquid; 45 return ret("%}"); 46 } 47 else if (ch == '"' || ch == "'") 48 return chain(stream, state, liquidTokenString(ch)); 49 else if (/[\.,:\|\[\]]/.test(ch)) 50 return ret(ch); 51 else if (/\d/.test(ch)) { 52 stream.match(/^\d*(?:\.\d*)?/); 53 return ret("number", "liquid-atom"); 54 } 55 else { 56 stream.eatWhile(/[\w_?<>=]/); 57 var word = stream.current(); 58 return ret("word", "liquid-word", word); 59 } 60 61 } 62 63 function liquidTokenString(quote) { 64 return function(stream, state) { 65 if (!nextUntilUnescaped(stream, quote)) 66 state.tokenize = liquidTokenBase; 67 return ret("string", "liquid-string"); 68 }; 69 } 70 71 function nextUntilUnescaped(stream, end) { 72 var escaped = false, next; 73 while ((next = stream.next()) != null) { 74 if (next == end && !escaped) 75 return false; 76 // Liquid does not seem to have string quote escaping 77 escaped = !escaped && next == "abc"; 78 } 79 return escaped; 80 } 81 82 function nonLiquid(stream, state) { 83 var ch = stream.next(); 84 state.liquidMode = null; 85 if (ch == '{') { 86 if (stream.eat('%')) { 87 state.tokenize = liquidTokenBase; 88 state.liquidMode = "tag-markup"; 89 return ret("{%", "liquid-markup-delimiter"); 90 } 91 else if (stream.eat('{')) { 92 state.tokenize = liquidTokenBase; 93 state.liquidMode = "output-markup"; 94 return ret("{{", "liquid-markup-delimiter"); 95 } 96 } 97 stream.eatWhile(/[^{]/); 98 return null; 99 } 100 101 102 function parseLiquid(state, type, content, stream) { 103 var cc = state.cc; 104 // Communicate our context to the combinators. 105 // (Less wasteful than consing up a hundred closures on every call.) 106 cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; 107 108 while(true) { 109 var combinator = cc.length ? cc.pop() : liquidMarkup; 110 if (combinator(type, content)) { 111 // Immediately work off any autocompletion objects on the stack 112 while(cc.length && cc[cc.length - 1].ac) 113 cc.pop()(); 114 115 if (cx.marked) return cx.marked; 116 return null; 117 } 118 } 119 } 120 121 // Combinator utils 122 123 var cx = {state: null, column: null, marked: null, cc: null}; 124 function pass() { 125 for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); 126 } 127 function cont() { 128 pass.apply(null, arguments); 129 return true; 130 } 131 132 // Combinators 133 134 function expect(wanted, marker, wantedValue) { 135 return function expecting(type, value) { 136 if (type == wanted && (typeof(value) == "undefined" || value == wantedValue)) { 137 if (typeof(marker) == 'string') cx.marked = marker; 138 return cont(); 139 } 140 else return cont(arguments.callee); 141 }; 142 } 143 144 function markAc(type) { 145 var result = function() { 146 var state = cx.state; 147 state.ac = type; 148 pass(unmarkAc); 149 }; 150 result.ac = true; 151 152 return result; 153 } 154 155 function unmarkAc() { 156 cx.state.ac = null; 157 } 158 159 function liquidMarkup(type) { 160 if (type == "{{") return cont(markAc('variable'), expression, maybeFilters, expect("}}", "liquid-markup-delimiter")); 161 if (type == "{%") return cont(markAc('tag'), tagName, expect("%}", "liquid-markup-delimiter")); 162 return cont(); 163 } 164 165 var endTag = /^end[\w]+$/; 166 var zeroArgTags = {"live": true, "footer": true, "comment": true}; 167 168 function tagName(type, value) { 169 if (type == "word") { 170 if (endTag.test(value)) { 171 cx.marked = "liquid-endtag-name"; 172 } 173 else { 174 cx.marked = "liquid-tag-name"; 175 if (value == "include") return cont(expression, maybeIncludeArgs); 176 if (value == "cycle") return cont(maybeCycleName, commasep(expression, "%}", true)); 177 if (value == "if" || value == "unless") return cont(markAc('variable'), expression, maybeOperator); 178 if (value == "capture") return cont(expression); 179 if (value == "for") return cont(expression, expect("word", "liquid-keyword", "in"), markAc('variable'), expression, maybeForArgs); 180 if (value == "assign") return cont(expression, expect('word', "liquid-operator", "="), markAc('variable'), expression); 181 if (zeroArgTags.hasOwnProperty(value)) return cont(); 182 } 183 } 184 return cont(); 185 } 186 187 function maybeForArgs(type, value) { 188 if (type == 'word' && value == 'reversed') { 189 cx.marked = "liquid-keyword"; 190 return cont(maybeTagAttributes); 191 } 192 return pass(maybeTagAttributes); 193 } 194 195 function maybeTagAttributes(type) { 196 if (type == "word") { 197 cx.marked = "liquid-tag-attribute-name"; 198 return cont(expect(":"), expression, maybeTagAttributes); 199 } 200 return pass(); 201 } 202 203 function expression(type) { 204 if (type == 'word') { 205 cx.marked = "liquid-variable"; 206 return cont(maybeMethod); 207 } 208 return cont(); 209 } 210 211 function maybeMethod(type) { 212 if (type == '.') return cont(markAc('method'), method, maybeMethod); 213 if (type == '[') return cont(expression, expect(']'), maybeMethod); 214 return pass(); 215 } 216 217 function method(type) { 218 if (type == 'word') { 219 cx.marked = "liquid-method"; 220 } 221 return cont(); 222 } 223 224 var operators = {"<": true, ">": true, "<=": true, ">=": true, "==": true, "contains": true, "and": true, "or": true}; 225 226 function maybeOperator(type, value) { 227 if (operators.hasOwnProperty(value)) { 228 cx.marked = "liquid-operator"; 229 return cont(markAc('variable'), expression, maybeOperator); 230 } 231 return pass(); 232 } 233 234 function maybeCycleName() { 235 // Liquid's cycle interprets its first argument as the cycle name if it is follow by a :. 236 // In order to separate between a non-name variable argument and the name argument, we 237 // do a look-ahead here. 238 // If we find a : after our current position, the current expression is the cycle name. 239 if (cx.stream.match(/\s*:/, false)) { 240 cx.stream.match(/\s*/); 241 cx.marked = "liquid-tag-cycle-name"; 242 return cont(expect(':')); 243 } 244 return pass(); 245 } 246 247 function maybeIncludeArgs(type, value) { 248 if (type == 'word') { 249 if (value == 'for' || value == 'with') { 250 cx.marked = "liquid-keyword"; 251 return cont(expression); 252 } 253 } 254 return pass(); 255 } 256 257 function maybeFilters(type) { 258 if (type == '|') return cont(markAc('filter'), filterName, maybeFilterArgs, maybeFilters); 259 return pass(); 260 } 261 262 function filterName() { 263 cx.marked = "liquid-filter"; 264 return cont(); 265 } 266 267 function maybeFilterArgs(type) { 268 if (type == ':') return cont(commasep(expression, {"}}": true, "|": true}, true)); 269 return pass(maybeFilters); 270 } 271 272 function commasep(what, end, doNotEat) { 273 function checkForEnd(type) { 274 return (typeof(end) == "string" && type == end) || (typeof(end) == "object" && end.hasOwnProperty(type)); 275 } 276 function proceed(type) { 277 if (type == ",") return cont(what, proceed); 278 if (checkForEnd(type)) return doNotEat ? pass() : cont(); 279 return cont(expect(end)); 280 } 281 return function commaSeparated(type) { 282 if (checkForEnd(type)) return doNotEat ? pass() : cont(); 283 else return pass(what, proceed); 284 }; 285 } 286 287 return { 288 startState: function() { 289 return { 290 tokenize: nonLiquid, 291 cc: [],
292 liquidMode: null, 293 ac: null 294 } 295 }, 296 297 resetState: function(state) { 298 state.tokenize = nonLiquid; 299 state.cc = []; 300 state.liquidMode = null; 301 state.ac = null; 302 return state; 303 }, 304 305 token: function(stream, state) { 306 type = null; 307 308 // Liquid markup cannot span multiple lines 309 if (stream.sol()) this.resetState(state); 310 311 var style = state.tokenize(stream, state); 312 313 // If we have found liquid markup, we always set some common CSS classes, so we can make use 314 // of the style cascade. 315 if (state.liquidMode != null) { 316 style = (style || "") + " liquid liquid-" + state.liquidMode; 317 } 318 319 // No parsing necessary for white space inside liquid markup or when did not detect any liquid markup. 320 if (type == "whitespace" || type == null) return style; 321 322 var parseType = parseLiquid(state, type, content, stream); 323 324 return (style || "") + " " + (parseType || ""); 325 } 326 }; 327}); 328CodeMirror.defineMIME("application/x-liquid-template", "liquid"); 329 330CodeMirror.defineMode("htmlmixedliquid", function(config, parserConfig) { 331 return CodeMirror.overlayMode(CodeMirror.getMode(config, parserConfig.backdrop || "text/html"), CodeMirror.getMode(config, parserConfig.overlay || "application/x-liquid-template")); 332}); 333 334// Compatibility with CodeMirror's formatting addon 335if (CodeMirror.modeExtensions){ 336 CodeMirror.modeExtensions["htmlmixedliquid"] = CodeMirror.modeExtensions["htmlmixed"]; 337 338 // If the current mode is 'htmlmixed', returns the extension of a mode located at 339 // the specified position (can be htmlmixed, css or javascript). Otherwise, simply 340 // returns the extension of the editor's current mode. 341 CodeMirror.defineExtension("getModeExtAtPos", function (pos) { 342 var token = this.getTokenAt(pos); 343 if (token && token.state && token.state.mode) 344 return CodeMirror.modeExtensions[token.state.mode == "html" ? "htmlmixed" : token.state.mode]; 345 else 346 if (token && token.state && token.state.base && token.state.base.mode) 347 return CodeMirror.modeExtensions[token.state.base.mode == "html" ? "htmlmixed" : token.state.base.mode]; 348 else 349 return this.getModeExt(); 350 }); 351 352} 353 354CodeMirror.defineExtension("commentRangeLiquid", function (isComment, from, to) { 355 var curMode = this.getModeExtAtPos(this.getCursor()); 356 if (isComment) { // Comment range 357 var commentedText = this.getRange(from, to); 358 this.replaceRange("{% comment %}" + this.getRange(from, to) + "{% endcomment %}" 359 , from, to); 360 if (from.line == to.line && from.ch == to.ch) { // An empty comment inserted - put cursor inside 361 this.setCursor(from.line, from.ch + "{% comment %}".length); 362 } 363 } 364 else { // Uncomment range 365 var selText = this.getRange(from, to); 366 var startIndex = selText.indexOf("{% comment %}"); 367 var endIndex = selText.lastIndexOf("{% endcomment %}"); 368 if (startIndex > -1 && endIndex > -1 && endIndex > startIndex) { 369 // Take string till comment start 370 selText = selText.substr(0, startIndex) 371 // From comment start till comment end 372 + selText.substring(startIndex + "{% comment %}".length, endIndex) 373 // From comment end till string end 374 + selText.substr(endIndex + "{% endcomment %}".length); 375 } 376 this.replaceRange(selText, from, to); 377 } 378});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.