PageSourceSearch

https://actionnetwork.org/includes/js/codemirror/liquid.js?v=1

js actionnetwork.org collected 2026-10-01 07:21:31 UTC 12,477 bytes, 378 lines download raw bytes

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.