scripts/CodeMirror/mode/textile/textile.js (view raw)
1// CodeMirror, copyright (c) by Marijn Haverbeke and others
2// Distributed under an MIT license: https://codemirror.net/LICENSE
3
4(function(mod) {
5 if (typeof exports == "object" && typeof module == "object") { // CommonJS
6 mod(require("../../lib/codemirror"));
7 } else if (typeof define == "function" && define.amd) { // AMD
8 define(["../../lib/codemirror"], mod);
9 } else { // Plain browser env
10 mod(CodeMirror);
11 }
12})(function(CodeMirror) {
13 "use strict";
14
15 var TOKEN_STYLES = {
16 addition: "positive",
17 attributes: "attribute",
18 bold: "strong",
19 cite: "keyword",
20 code: "atom",
21 definitionList: "number",
22 deletion: "negative",
23 div: "punctuation",
24 em: "em",
25 footnote: "variable",
26 footCite: "qualifier",
27 header: "header",
28 html: "comment",
29 image: "string",
30 italic: "em",
31 link: "link",
32 linkDefinition: "link",
33 list1: "variable-2",
34 list2: "variable-3",
35 list3: "keyword",
36 notextile: "string-2",
37 pre: "operator",
38 p: "property",
39 quote: "bracket",
40 span: "quote",
41 specialChar: "tag",
42 strong: "strong",
43 sub: "builtin",
44 sup: "builtin",
45 table: "variable-3",
46 tableHeading: "operator"
47 };
48
49 function startNewLine(stream, state) {
50 state.mode = Modes.newLayout;
51 state.tableHeading = false;
52
53 if (state.layoutType === "definitionList" && state.spanningLayout &&
54 stream.match(RE("definitionListEnd"), false))
55 state.spanningLayout = false;
56 }
57
58 function handlePhraseModifier(stream, state, ch) {
59 if (ch === "_") {
60 if (stream.eat("_"))
61 return togglePhraseModifier(stream, state, "italic", /__/, 2);
62 else
63 return togglePhraseModifier(stream, state, "em", /_/, 1);
64 }
65
66 if (ch === "*") {
67 if (stream.eat("*")) {
68 return togglePhraseModifier(stream, state, "bold", /\*\*/, 2);
69 }
70 return togglePhraseModifier(stream, state, "strong", /\*/, 1);
71 }
72
73 if (ch === "[") {
74 if (stream.match(/\d+\]/)) state.footCite = true;
75 return tokenStyles(state);
76 }
77
78 if (ch === "(") {
79 var spec = stream.match(/^(r|tm|c)\)/);
80 if (spec)
81 return tokenStylesWith(state, TOKEN_STYLES.specialChar);
82 }
83
84 if (ch === "<" && stream.match(/(\w+)[^>]+>[^<]+<\/\1>/))
85 return tokenStylesWith(state, TOKEN_STYLES.html);
86
87 if (ch === "?" && stream.eat("?"))
88 return togglePhraseModifier(stream, state, "cite", /\?\?/, 2);
89
90 if (ch === "=" && stream.eat("="))
91 return togglePhraseModifier(stream, state, "notextile", /==/, 2);
92
93 if (ch === "-" && !stream.eat("-"))
94 return togglePhraseModifier(stream, state, "deletion", /-/, 1);
95
96 if (ch === "+")
97 return togglePhraseModifier(stream, state, "addition", /\+/, 1);
98
99 if (ch === "~")
100 return togglePhraseModifier(stream, state, "sub", /~/, 1);
101
102 if (ch === "^")
103 return togglePhraseModifier(stream, state, "sup", /\^/, 1);
104
105 if (ch === "%")
106 return togglePhraseModifier(stream, state, "span", /%/, 1);
107
108 if (ch === "@")
109 return togglePhraseModifier(stream, state, "code", /@/, 1);
110
111 if (ch === "!") {
112 var type = togglePhraseModifier(stream, state, "image", /(?:\([^\)]+\))?!/, 1);
113 stream.match(/^:\S+/); // optional Url portion
114 return type;
115 }
116 return tokenStyles(state);
117 }
118
119 function togglePhraseModifier(stream, state, phraseModifier, closeRE, openSize) {
120 var charBefore = stream.pos > openSize ? stream.string.charAt(stream.pos - openSize - 1) : null;
121 var charAfter = stream.peek();
122 if (state[phraseModifier]) {
123 if ((!charAfter || /\W/.test(charAfter)) && charBefore && /\S/.test(charBefore)) {
124 var type = tokenStyles(state);
125 state[phraseModifier] = false;
126 return type;
127 }
128 } else if ((!charBefore || /\W/.test(charBefore)) && charAfter && /\S/.test(charAfter) &&
129 stream.match(new RegExp("^.*\\S" + closeRE.source + "(?:\\W|$)"), false)) {
130 state[phraseModifier] = true;
131 state.mode = Modes.attributes;
132 }
133 return tokenStyles(state);
134 };
135
136 function tokenStyles(state) {
137 var disabled = textileDisabled(state);
138 if (disabled) return disabled;
139
140 var styles = [];
141 if (state.layoutType) styles.push(TOKEN_STYLES[state.layoutType]);
142
143 styles = styles.concat(activeStyles(
144 state, "addition", "bold", "cite", "code", "deletion", "em", "footCite",
145 "image", "italic", "link", "span", "strong", "sub", "sup", "table", "tableHeading"));
146
147 if (state.layoutType === "header")
148 styles.push(TOKEN_STYLES.header + "-" + state.header);
149
150 return styles.length ? styles.join(" ") : null;
151 }
152
153 function textileDisabled(state) {
154 var type = state.layoutType;
155
156 switch(type) {
157 case "notextile":
158 case "code":
159 case "pre":
160 return TOKEN_STYLES[type];
161 default:
162 if (state.notextile)
163 return TOKEN_STYLES.notextile + (type ? (" " + TOKEN_STYLES[type]) : "");
164 return null;
165 }
166 }
167
168 function tokenStylesWith(state, extraStyles) {
169 var disabled = textileDisabled(state);
170 if (disabled) return disabled;
171
172 var type = tokenStyles(state);
173 if (extraStyles)
174 return type ? (type + " " + extraStyles) : extraStyles;
175 else
176 return type;
177 }
178
179 function activeStyles(state) {
180 var styles = [];
181 for (var i = 1; i < arguments.length; ++i) {
182 if (state[arguments[i]])
183 styles.push(TOKEN_STYLES[arguments[i]]);
184 }
185 return styles;
186 }
187
188 function blankLine(state) {
189 var spanningLayout = state.spanningLayout, type = state.layoutType;
190
191 for (var key in state) if (state.hasOwnProperty(key))
192 delete state[key];
193
194 state.mode = Modes.newLayout;
195 if (spanningLayout) {
196 state.layoutType = type;
197 state.spanningLayout = true;
198 }
199 }
200
201 var REs = {
202 cache: {},
203 single: {
204 bc: "bc",
205 bq: "bq",
206 definitionList: /- .*?:=+/,
207 definitionListEnd: /.*=:\s*$/,
208 div: "div",
209 drawTable: /\|.*\|/,
210 foot: /fn\d+/,
211 header: /h[1-6]/,
212 html: /\s*<(?:\/)?(\w+)(?:[^>]+)?>(?:[^<]+<\/\1>)?/,
213 link: /[^"]+":\S/,
214 linkDefinition: /\[[^\s\]]+\]\S+/,
215 list: /(?:#+|\*+)/,
216 notextile: "notextile",
217 para: "p",
218 pre: "pre",
219 table: "table",
220 tableCellAttributes: /[\/\\]\d+/,
221 tableHeading: /\|_\./,
222 tableText: /[^"_\*\[\(\?\+~\^%@|-]+/,
223 text: /[^!"_=\*\[\(<\?\+~\^%@-]+/
224 },
225 attributes: {
226 align: /(?:<>|<|>|=)/,
227 selector: /\([^\(][^\)]+\)/,
228 lang: /\[[^\[\]]+\]/,
229 pad: /(?:\(+|\)+){1,2}/,
230 css: /\{[^\}]+\}/
231 },
232 createRe: function(name) {
233 switch (name) {
234 case "drawTable":
235 return REs.makeRe("^", REs.single.drawTable, "$");
236 case "html":
237 return REs.makeRe("^", REs.single.html, "(?:", REs.single.html, ")*", "$");
238 case "linkDefinition":
239 return REs.makeRe("^", REs.single.linkDefinition, "$");
240 case "listLayout":
241 return REs.makeRe("^", REs.single.list, RE("allAttributes"), "*\\s+");
242 case "tableCellAttributes":
243 return REs.makeRe("^", REs.choiceRe(REs.single.tableCellAttributes,
244 RE("allAttributes")), "+\\.");
245 case "type":
246 return REs.makeRe("^", RE("allTypes"));
247 case "typeLayout":
248 return REs.makeRe("^", RE("allTypes"), RE("allAttributes"),
249 "*\\.\\.?", "(\\s+|$)");
250 case "attributes":
251 return REs.makeRe("^", RE("allAttributes"), "+");
252
253 case "allTypes":
254 return REs.choiceRe(REs.single.div, REs.single.foot,
255 REs.single.header, REs.single.bc, REs.single.bq,
256 REs.single.notextile, REs.single.pre, REs.single.table,
257 REs.single.para);
258
259 case "allAttributes":
260 return REs.choiceRe(REs.attributes.selector, REs.attributes.css,
261 REs.attributes.lang, REs.attributes.align, REs.attributes.pad);
262
263 default:
264 return REs.makeRe("^", REs.single[name]);
265 }
266 },
267 makeRe: function() {
268 var pattern = "";
269 for (var i = 0; i < arguments.length; ++i) {
270 var arg = arguments[i];
271 pattern += (typeof arg === "string") ? arg : arg.source;
272 }
273 return new RegExp(pattern);
274 },
275 choiceRe: function() {
276 var parts = [arguments[0]];
277 for (var i = 1; i < arguments.length; ++i) {
278 parts[i * 2 - 1] = "|";
279 parts[i * 2] = arguments[i];
280 }
281
282 parts.unshift("(?:");
283 parts.push(")");
284 return REs.makeRe.apply(null, parts);
285 }
286 };
287
288 function RE(name) {
289 return (REs.cache[name] || (REs.cache[name] = REs.createRe(name)));
290 }
291
292 var Modes = {
293 newLayout: function(stream, state) {
294 if (stream.match(RE("typeLayout"), false)) {
295 state.spanningLayout = false;
296 return (state.mode = Modes.blockType)(stream, state);
297 }
298 var newMode;
299 if (!textileDisabled(state)) {
300 if (stream.match(RE("listLayout"), false))
301 newMode = Modes.list;
302 else if (stream.match(RE("drawTable"), false))
303 newMode = Modes.table;
304 else if (stream.match(RE("linkDefinition"), false))
305 newMode = Modes.linkDefinition;
306 else if (stream.match(RE("definitionList")))
307 newMode = Modes.definitionList;
308 else if (stream.match(RE("html"), false))
309 newMode = Modes.html;
310 }
311 return (state.mode = (newMode || Modes.text))(stream, state);
312 },
313
314 blockType: function(stream, state) {
315 var match, type;
316 state.layoutType = null;
317
318 if (match = stream.match(RE("type")))
319 type = match[0];
320 else
321 return (state.mode = Modes.text)(stream, state);
322
323 if (match = type.match(RE("header"))) {
324 state.layoutType = "header";
325 state.header = parseInt(match[0][1]);
326 } else if (type.match(RE("bq"))) {
327 state.layoutType = "quote";
328 } else if (type.match(RE("bc"))) {
329 state.layoutType = "code";
330 } else if (type.match(RE("foot"))) {
331 state.layoutType = "footnote";
332 } else if (type.match(RE("notextile"))) {
333 state.layoutType = "notextile";
334 } else if (type.match(RE("pre"))) {
335 state.layoutType = "pre";
336 } else if (type.match(RE("div"))) {
337 state.layoutType = "div";
338 } else if (type.match(RE("table"))) {
339 state.layoutType = "table";
340 }
341
342 state.mode = Modes.attributes;
343 return tokenStyles(state);
344 },
345
346 text: function(stream, state) {
347 if (stream.match(RE("text"))) return tokenStyles(state);
348
349 var ch = stream.next();
350 if (ch === '"')
351 return (state.mode = Modes.link)(stream, state);
352 return handlePhraseModifier(stream, state, ch);
353 },
354
355 attributes: function(stream, state) {
356 state.mode = Modes.layoutLength;
357
358 if (stream.match(RE("attributes")))
359 return tokenStylesWith(state, TOKEN_STYLES.attributes);
360 else
361 return tokenStyles(state);
362 },
363
364 layoutLength: function(stream, state) {
365 if (stream.eat(".") && stream.eat("."))
366 state.spanningLayout = true;
367
368 state.mode = Modes.text;
369 return tokenStyles(state);
370 },
371
372 list: function(stream, state) {
373 var match = stream.match(RE("list"));
374 state.listDepth = match[0].length;
375 var listMod = (state.listDepth - 1) % 3;
376 if (!listMod)
377 state.layoutType = "list1";
378 else if (listMod === 1)
379 state.layoutType = "list2";
380 else
381 state.layoutType = "list3";
382
383 state.mode = Modes.attributes;
384 return tokenStyles(state);
385 },
386
387 link: function(stream, state) {
388 state.mode = Modes.text;
389 if (stream.match(RE("link"))) {
390 stream.match(/\S+/);
391 return tokenStylesWith(state, TOKEN_STYLES.link);
392 }
393 return tokenStyles(state);
394 },
395
396 linkDefinition: function(stream, state) {
397 stream.skipToEnd();
398 return tokenStylesWith(state, TOKEN_STYLES.linkDefinition);
399 },
400
401 definitionList: function(stream, state) {
402 stream.match(RE("definitionList"));
403
404 state.layoutType = "definitionList";
405
406 if (stream.match(/\s*$/))
407 state.spanningLayout = true;
408 else
409 state.mode = Modes.attributes;
410
411 return tokenStyles(state);
412 },
413
414 html: function(stream, state) {
415 stream.skipToEnd();
416 return tokenStylesWith(state, TOKEN_STYLES.html);
417 },
418
419 table: function(stream, state) {
420 state.layoutType = "table";
421 return (state.mode = Modes.tableCell)(stream, state);
422 },
423
424 tableCell: function(stream, state) {
425 if (stream.match(RE("tableHeading")))
426 state.tableHeading = true;
427 else
428 stream.eat("|");
429
430 state.mode = Modes.tableCellAttributes;
431 return tokenStyles(state);
432 },
433
434 tableCellAttributes: function(stream, state) {
435 state.mode = Modes.tableText;
436
437 if (stream.match(RE("tableCellAttributes")))
438 return tokenStylesWith(state, TOKEN_STYLES.attributes);
439 else
440 return tokenStyles(state);
441 },
442
443 tableText: function(stream, state) {
444 if (stream.match(RE("tableText")))
445 return tokenStyles(state);
446
447 if (stream.peek() === "|") { // end of cell
448 state.mode = Modes.tableCell;
449 return tokenStyles(state);
450 }
451 return handlePhraseModifier(stream, state, stream.next());
452 }
453 };
454
455 CodeMirror.defineMode("textile", function() {
456 return {
457 startState: function() {
458 return { mode: Modes.newLayout };
459 },
460 token: function(stream, state) {
461 if (stream.sol()) startNewLine(stream, state);
462 return state.mode(stream, state);
463 },
464 blankLine: blankLine
465 };
466 });
467
468 CodeMirror.defineMIME("text/x-textile", "textile");
469});