The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
/***
|''Name''|tiddlywiki.js|
|''Description''|Enables TiddlyWikiy syntax highlighting using CodeMirror|
|''Author''|PMario|
|''Version''|0.1.7|
|''Status''|''stable''|
|''Source''|[[GitHub|https://github.com/pmario/CodeMirror2/blob/tw-syntax/mode/tiddlywiki]]|
|''Documentation''|http://codemirror.tiddlyspace.com/|
|''License''|[[MIT License|http://www.opensource.org/licenses/mit-license.php]]|
|''CoreVersion''|2.5.0|
|''Requires''|codemirror.js|
|''Keywords''|syntax highlighting color code mirror codemirror|
! Info
CoreVersion parameter is needed for TiddlyWiki only!
***/
//{{{
CodeMirror.defineMode("tiddlywiki", function () {
	// Tokenizer
	var textwords = {};

	var keywords = function () {
		function kw(type) {
			return { type: type, style: "macro"};
		}
		return {
			"allTags": kw('allTags'), "closeAll": kw('closeAll'), "list": kw('list'),
			"newJournal": kw('newJournal'), "newTiddler": kw('newTiddler'),
			"permaview": kw('permaview'), "saveChanges": kw('saveChanges'),
			"search": kw('search'), "slider": kw('slider'),	"tabs": kw('tabs'),
			"tag": kw('tag'), "tagging": kw('tagging'),	"tags": kw('tags'),
			"tiddler": kw('tiddler'), "timeline": kw('timeline'),
			"today": kw('today'), "version": kw('version'),	"option": kw('option'),

			"with": kw('with'),
			"filter": kw('filter')
		};
	}();

	var isSpaceName = /[\w_\-]/i,
		reHR = /^\-\-\-\-+$/,					// <hr>
		reWikiCommentStart = /^\/\*\*\*$/,		// /***
		reWikiCommentStop = /^\*\*\*\/$/,		// ***/
		reBlockQuote = /^<<<$/,

		reJsCodeStart = /^\/\/\{\{\{$/,			// //{{{ js block start
		reJsCodeStop = /^\/\/\}\}\}$/,			// //}}} js stop
		reXmlCodeStart = /^<!--\{\{\{-->$/,		// xml block start
		reXmlCodeStop = /^<!--\}\}\}-->$/,		// xml stop

		reCodeBlockStart = /^\{\{\{$/,			// {{{ TW text div block start
		reCodeBlockStop = /^\}\}\}$/,			// }}} TW text stop

		reUntilCodeStop = /.*?\}\}\}/;

	function chain(stream, state, f) {
		state.tokenize = f;
		return f(stream, state);
	}

	// Used as scratch variables to communicate multiple values without
	// consing up tons of objects.
	var type, content;

	function ret(tp, style, cont) {
		type = tp;
		content = cont;
		return style;
	}

	function jsTokenBase(stream, state) {
		var sol = stream.sol(), ch;
			
		state.block = false;	// indicates the start of a code block.

		ch = stream.peek(); 	// don't eat, to make matching simpler
		
		// check start of  blocks
		if (sol && /[<\/\*{}\-]/.test(ch)) {
			if (stream.match(reCodeBlockStart)) {
				state.block = true;
				return chain(stream, state, twTokenCode);
			}
			if (stream.match(reBlockQuote)) {
				return ret('quote', 'quote');
			}
			if (stream.match(reWikiCommentStart) || stream.match(reWikiCommentStop)) {
				return ret('code', 'comment');
			}
			if (stream.match(reJsCodeStart) || stream.match(reJsCodeStop) || stream.match(reXmlCodeStart) || stream.match(reXmlCodeStop)) {
				return ret('code', 'comment');
			}
			if (stream.match(reHR)) {
				return ret('hr', 'hr');
			}
		} // sol
		ch = stream.next();

		if (sol && /[\/\*!#;:>|]/.test(ch)) {
			if (ch == "!") { // tw header
				stream.skipToEnd();
				return ret("header", "header");
			}
			if (ch == "*") { // tw list
				stream.eatWhile('*');
				return ret("list", "comment");
			}
			if (ch == "#") { // tw numbered list
				stream.eatWhile('#');
				return ret("list", "comment");
			}
			if (ch == ";") { // definition list, term
				stream.eatWhile(';');
				return ret("list", "comment");
			}
			if (ch == ":") { // definition list, description
				stream.eatWhile(':');
				return ret("list", "comment");
			}
			if (ch == ">") { // single line quote
				stream.eatWhile(">");
				return ret("quote", "quote");
			}
			if (ch == '|') {
				return ret('table', 'header');
			}
		}

		if (ch == '{' && stream.match(/\{\{/)) {
			return chain(stream, state, twTokenCode);
		}

		// rudimentary html:// file:// link matching. TW knows much more ...
		if (/[hf]/i.test(ch)) {
			if (/[ti]/i.test(stream.peek()) && stream.match(/\b(ttps?|tp|ile):\/\/[\-A-Z0-9+&@#\/%?=~_|$!:,.;]*[A-Z0-9+&@#\/%=~_|$]/i)) {
				return ret("link", "link");
			}
		}
		// just a little string indicator, don't want to have the whole string covered
		if (ch == '"') {
			return ret('string', 'string');
		}
		if (ch == '~') {	// _no_ CamelCase indicator should be bold
			return ret('text', 'brace');
		}
		if (/[\[\]]/.test(ch)) { // check for [[..]]
			if (stream.peek() == ch) {
				stream.next();
				return ret('brace', 'brace');
			}
		}
		if (ch == "@") {	// check for space link. TODO fix @@...@@ highlighting
			stream.eatWhile(isSpaceName);
			return ret("link", "link");
		}
		if (/\d/.test(ch)) {	// numbers
			stream.eatWhile(/\d/);
			return ret("number", "number");
		}
		if (ch == "/") { // tw invisible comment
			if (stream.eat("%")) {
				return chain(stream, state, twTokenComment);
			}
			else if (stream.eat("/")) { // 
				return chain(stream, state, twTokenEm);
			}
		}
		if (ch == "_") { // tw underline
			if (stream.eat("_")) {
				return chain(stream, state, twTokenUnderline);
			}
		}
		// strikethrough and mdash handling
		if (ch == "-") {
			if (stream.eat("-")) {
				// if strikethrough looks ugly, change CSS.
				if (stream.peek() != ' ')
					return chain(stream, state, twTokenStrike);
				// mdash
				if (stream.peek() == ' ')
					return ret('text', 'brace');
			}
		}
		if (ch == "'") { // tw bold
			if (stream.eat("'")) {
				return chain(stream, state, twTokenStrong);
			}
		}
		if (ch == "<") { // tw macro
			if (stream.eat("<")) {
				return chain(stream, state, twTokenMacro);
			}
		}
		else {
			return ret(ch);
		}

		// core macro handling
		stream.eatWhile(/[\w\$_]/);
		var word = stream.current(),
			known = textwords.propertyIsEnumerable(word) && textwords[word];

		return known ? ret(known.type, known.style, word) : ret("text", null, word);

	} // jsTokenBase()

	// tw invisible comment
	function twTokenComment(stream, state) {
		var maybeEnd = false,
			ch;
		while (ch = stream.next()) {
			if (ch == "/" && maybeEnd) {
				state.tokenize = jsTokenBase;
				break;
			}
			maybeEnd = (ch == "%");
		}
		return ret("comment", "comment");
	}

	// tw strong / bold
	function twTokenStrong(stream, state) {
		var maybeEnd = false,
			ch;
		while (ch = stream.next()) {
			if (ch == "'" && maybeEnd) {
				state.tokenize = jsTokenBase;
				break;
			}
			maybeEnd = (ch == "'");
		}
		return ret("text", "strong");
	}

	// tw code
	function twTokenCode(stream, state) {
		var ch, sb = state.block;
		
		if (sb && stream.current()) {
			return ret("code", "comment");
		}

		if (!sb && stream.match(reUntilCodeStop)) {
			state.tokenize = jsTokenBase;
			return ret("code", "comment");
		}

		if (sb && stream.sol() && stream.match(reCodeBlockStop)) {
			state.tokenize = jsTokenBase;
			return ret("code", "comment");
		}

		ch = stream.next();
		return (sb) ? ret("code", "comment") : ret("code", "comment");
	}

	// tw em / italic
	function twTokenEm(stream, state) {
		var maybeEnd = false,
			ch;
		while (ch = stream.next()) {
			if (ch == "/" && maybeEnd) {
				state.tokenize = jsTokenBase;
				break;
			}
			maybeEnd = (ch == "/");
		}
		return ret("text", "em");
	}

	// tw underlined text
	function twTokenUnderline(stream, state) {
		var maybeEnd = false,
			ch;
		while (ch = stream.next()) {
			if (ch == "_" && maybeEnd) {
				state.tokenize = jsTokenBase;
				break;
			}
			maybeEnd = (ch == "_");
		}
		return ret("text", "underlined");
	}

	// tw strike through text looks ugly
	// change CSS if needed
	function twTokenStrike(stream, state) {
		var maybeEnd = false, ch;
			
		while (ch = stream.next()) {
			if (ch == "-" && maybeEnd) {
				state.tokenize = jsTokenBase;
				break;
			}
			maybeEnd = (ch == "-");
		}
		return ret("text", "strikethrough");
	}

	// macro
	function twTokenMacro(stream, state) {
		var ch, word, known;

		if (stream.current() == '<<') {
			return ret('brace', 'macro');
		}

		ch = stream.next();
		if (!ch) {
			state.tokenize = jsTokenBase;
			return ret(ch);
		}
		if (ch == ">") {
			if (stream.peek() == '>') {
				stream.next();
				state.tokenize = jsTokenBase;
				return ret("brace", "macro");
			}
		}

		stream.eatWhile(/[\w\$_]/);
		word = stream.current();
		known = keywords.propertyIsEnumerable(word) && keywords[word];

		if (known) {
			return ret(known.type, known.style, word);
		}
		else {
			return ret("macro", null, word);
		}
	}

	// Interface
	return {
		startState: function () {
			return {
				tokenize: jsTokenBase,
				indented: 0,
				level: 0
			};
		},

		token: function (stream, state) {
			if (stream.eatSpace()) return null;
			var style = state.tokenize(stream, state);
			return style;
		},

		electricChars: ""
	};
});

CodeMirror.defineMIME("text/x-tiddlywiki", "tiddlywiki");
//}}}