// ==UserScript==
// @name maxscriptSyntaxHighlighter
// @namespace mxs
// @description Maxscript syntax highlighter
// @include http://forums.cgsociety.org/showthread.php?*
// @version 1
// @grant none
// ==/UserScript==
window.document.body.onload = function(){
var useMxsHighlighter = getCookie( 'useMxsHighlighter' ),
code_window_width = 950,
code_window_height = 400,
code_window_line_height = 20,
code_window_lines = 15,
post_right_padding = 20,
posts = [].slice.call(window.document.getElementsByTagName('pre')); // <-- this is html elements collection that contain maxscript code
// Maxscript functions, keywords etc...
var maxscript_keywords = "pi by selection tick with undo off on for in to where while do as string int ineger float rollout if not else then try catch polyop meshop pos width height pressed changed local global using",
maxscript_values = "false true null unsupplied undefined (\#\\\(\\\))",
maxscript_functions = "gc heapfree setproperty getproperty getpropnames hasproperty isproperty show messagebox free superclassof classof iskindof snapshotasmesh snapshot instance copy getFiles clearListener cos sin atan atan2 degtorad radtodeg normalize cross globalvars dotnetclass dotnetobject dotnet\.addEventHandler dot updateshape getSourceFileName update addmodifier addnewspline join makedir collect matchpattern substring delete open close filterstring trimright trimleft trim print getdir sort random format timestamp appendifunique uniquename append tolower getnodebyname finditem execute destroydialog createdialog",
maxscript_controls = "angle slider spinner group button checkbutton mapbutton materialbutton pickbutton checkbox colorpicker listbox multilistbox dropdownlist combobox edittext hyperlink label groupBox progressbar radiobuttons bitmap imgTag SubRollout curvecontrol";
// css style classes used to colorize maxscript syntax
var maxscript_styles = [".tmp",".mxsFunction",".mxsFunctionName",".mxsFunctionArgs",".mxsFunctions",".mxsComments",".mxsKeywords",".mxsString",".mxsValue",".mxsNumber",".mxsSelection",".mxsControls"];
var default_styles = {
"tmp":{},
"mxsFunction": {"color":"steelblue;","font-weight":"bolder;" },
"mxsFunctionName": {"color":"lightblue;","font-weight":"bolder;" },
"mxsFunctionArgs": {"color":"sandybrown;" },
"mxsFunctions": {"color":"skyblue;" },
"mxsComments": {"color":"green;","font-style":"italic;" },
"mxsKeywords": {"color":"Tan;" },
"mxsString": {"color":"pink;" },
"mxsValue": {"color":"skyblue;" },
"mxsNumber": {"color":"palegreen;" },
"mxsSelection": {"color":"MediumSpringGreen;" },
"mxsControls": {"color":"MediumPurple;" }
};
// set empty user style cookie at first run
for ( var mxsClass in default_styles ){
if ( getCookie( mxsClass ) == null ) setCookie( mxsClass, "" );
}
// style constructor
function Style(){
var _this = this;
var openTag = "<style class='mxsStyle'>\n";
var closeTag = "</style>";
this.classes = {};
this.initStyle = function(){
for ( var mxsClass in default_styles ) {
this.addClass( mxsClass );
for ( var prop in default_styles[ mxsClass ] ){
this.setClassCssValue( mxsClass, prop, default_styles[ mxsClass ][ prop ] );
}
}
};
this.modifyStyle = function(){
for ( var mxsClass in default_styles ){
var userStyle = getCookie( mxsClass );
if ( userStyle != "" ){
var props = userStyle.split(';');
props.forEach( function( prop ){
var line = prop.split(':');
_this.setClassCssValue( mxsClass, line[0], line[1] );
});
}
}
};
this.addClass = function( className ){
this.classes[ className ] = {};
};
this.setClassCssValue = function( className, property, value ){
this.classes[ className ][ property ] = value;
};
this.removeClassCssValue = function( className, property ){
delete this.classes[ className ][ property ];
};
this.buildStyle = function(){
var style = openTag;
for ( var mxsclass in this.classes ){
style += "." + mxsclass + " {";
for ( var prop in this.classes[ mxsclass ] ){
style += prop +": " + this.classes[ mxsclass ][ prop ] + ";";
}
style += "}\n";
}
style = style.replace(/\;\;/g, ";");
style += closeTag;
return style;
};
this.initStyle();
this.modifyStyle();
}
// this function taken from here url:https://books.google.ru/books?id=6k7IfACN_P8C&pg=PA206&lpg=PA206&dq=regex+find+double+quote+outside+tags&source=bl&ots=CpKf4Lql4M&sig=eqL2-We3xTnI6Y0G8bos36sOUKg&hl=ru&sa=X&ved=0ahUKEwiPo9jTmMvNAhWIBZoKHcuTBzwQ6AEIPjAE#v=onepage&q=regex%20find%20double%20quote%20outside%20tags&f=false
function regexOuterQuotes( str ){
str = str.replace( /(\@\".*?\")/gmi, "<font class='mxsString'>$1</font>");
var result = "",
outerRegex = /<[^<>]*>/g,
innerRegex = /"([^"]*)"/g,
outerMatch = null,
lastIndex = 0;
while (outerMatch = outerRegex.exec(str)){
if ( outerMatch.index == outerRegex.lastIndex ) outerRegex.lastIndex++;
var textBetween = str.slice(lastIndex, outerMatch.index);
result += textBetween.replace( innerRegex, "<font class='mxsString'>\"$1\"</font>");
lastIndex = outerMatch.index + outerMatch[0].length;
result += outerMatch[0];
}
var textAfter = str.slice(lastIndex);
result += textAfter.replace( innerRegex, "<font class='mxsString'>\"$1\"</font>");
return result;
}
function regexOuter( str, regex, replacement ){
var result = "",
outerRegex = /<[^<>]*>/g,
innerRegex = regex, // /"([^"]*)"/g; <-- regex for what we need
outerMatch = null,
lastIndex = 0;
while (outerMatch = outerRegex.exec(str)){
if ( outerMatch.index == outerRegex.lastIndex ) outerRegex.lastIndex++;
var textBetween = str.slice(lastIndex, outerMatch.index);
result += textBetween.replace( innerRegex, replacement );
lastIndex = outerMatch.index + outerMatch[0].length;
result += outerMatch[0];
}
var textAfter = str.slice(lastIndex);
result += textAfter.replace( innerRegex, replacement );
return result;
}
function codeWindowDimensions( codeElement, maxWidth, maxHeight, lineHeight, linesCount ){
var magicNumber = 2,
totalLines = codeElement.textContent.replace("\n\n","\n").split('\n').length,
totalHeight = (lineHeight + magicNumber) * Math.max( 2.5, totalLines),
maxHeight = Math.min( maxHeight, totalHeight);
codeElement.style.lineHeight = lineHeight + "px";
codeElement.style.height = maxHeight + "px";
codeElement.style.width = maxWidth + "px";
codeElement.style.color = "#ccc";
// move text slightly from right border
// $("td.alt1:not(td.alt1:contains('share'))").css("paddingRight", post_right_padding + "px");
}
function showColorChangeMenu( className, y_coord ){
var colorMenu = document.getElementById('mxsColorChange');
if ( typeof colorMenu == undefined || colorMenu == null ){
colorMenu = document.createElement("div");
colorMenu.id = "mxsColorChange";
colorMenu.style.display = "block";
colorMenu.style.position = "absolute";
colorMenu.style.width = "150px";
colorMenu.style.height = "150px";
colorMenu.style.backgroundColor = "#222";
colorMenu.style.padding = "10px 10px";
colorMenu.style.border = "Solid 1px #888";
colorMenu.style.borderRadius = "10px";
document.body.appendChild( colorMenu );
} else { $( colorMenu ).toggle(); }
var classColor = $( "." + className ).eq(0).css('color');
var menuHTML = "";
menuHTML += "<div style='padding-bottom:10px; text-align:center;'><b>" + className + "</b></div>";
menuHTML += "<div id='ccColor' style='padding:10px 10px;width:130px;height:90px;background-color:" + classColor + ";'></div>";
menuHTML += "<div onclick='$(this).parent().toggle();' style='text-align:center; padding-top:5px;'><b>click to close</b></div>";
colorMenu.innerHTML = menuHTML;
colorMenu.style.left = "150px";
colorMenu.style.top = y_coord + "px";
$('#ccColor').colorPicker({ renderCallback: function( $elm, toggled ){
$( "." + className ).css('color', "#" + this.color.colors.HEX ); // colorize on change
var cookie = getCookie( className ); // redo
cookie = cookie.replace(/(color\:.*?)/gi, "color: " + "#" + this.color.colors.HEX); // redo
cookie = "color: " + "#" + this.color.colors.HEX; // redo
setCookie( className, cookie );
}
});
}
// Cookie get/set
function setCookie( key, value ){
var expires = new Date();
expires.setTime(expires.getTime() + (365 * 24 * 60 * 60 * 1000));
document.cookie = key + '=' + value + ';expires=' + expires.toUTCString();
}
function getCookie( key ){
var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
return keyValue ? keyValue[2] : null;
}
/* // expose functions to browser DEBUG
window.getCookie = getCookie;
window.setCookie = setCookie;
window.Style = Style;
*/
if ( useMxsHighlighter == null ){
setCookie( "useMxsHighlighter", "true" );
useMxsHighlighter = getCookie( 'useMxsHighlighter' );
}
if ( useMxsHighlighter == "true" && posts.length > 0 ){ // main party goes here
function wrap( mxsClass, value ){
return "<font class='" + mxsClass + "'>" + value + "</font>";
}
// string regex
var re_keywords = new RegExp( "\\b(" + maxscript_keywords.replace(/\s+/gmi, "|") + ")\\b" , "gmi" ),
re_values = new RegExp( "(" + maxscript_values.replace(/\s+/gmi, "|") + ")" , "gmi" ),
re_functions = new RegExp( "\\b(" + maxscript_functions.replace(/\s+/gmi, "|") + ")\\b" , "gmi" ),
re_controls = new RegExp( "\\b(" + maxscript_controls.replace(/\s+/gmi, "|") + ")\\b" , "gmi" );
var re_assignment = new RegExp( /(\w|\d|\))(\s*(\>*|\<*)\=+)(\w|\d|\#|\()/gmi ),
re_quotes = new RegExp( /(\@*'([^']*)')/gmi ),
re_numbers = new RegExp( /([^\w])(\-*\d*\.*\d+f*)/gmi ),
re_selections = new RegExp( /(\$(\w+\**)*)/gmi ),
re_names = new RegExp( /(\#\w+\d*\w*)/gmi ),
re_globalVars = new RegExp( /(\:{2}\w+)/gmi ),
re_funcArgs = new RegExp( /(\w+\d*\:+)/gmi ),
re_comments = new RegExp( /((--|^\s--|^--)+.*(?!\n\r))|(\/\*(?:(?!\*\/).|[\n\r])*\*\/)/gmi ),
re_funcDefine = new RegExp( /(mapped\s+fn|mapped\s+function|fn|function)\s+(\w+)((\s\w+\:*)*)(?!\r)/gmi);
posts.forEach( function( post ){
codeWindowDimensions( post, code_window_width, code_window_height, code_window_line_height, code_window_lines );
var html = post.innerHTML;
html = regexOuterQuotes( html ); // coloring strings doublequotes
html = regexOuter( html, re_assignment, "$1 $2 $4" ); // beautify value assignment
html = regexOuter( html, re_quotes, wrap( "mxsString", "$1" ) ); // coloring strings quotes
html = regexOuter( html, re_numbers, "$1"+wrap( "mxsNumber", "$2" ) ); // coloring numbers
html = regexOuter( html, re_selections, wrap( "mxsSelection", "$1" ) ); // coloring selections
html = regexOuter( html, re_functions, wrap( "mxsFunctions", "$1" ) ); // coloring native functions
html = regexOuter( html, re_names, wrap( "mxsString", "$1" ) ); // coloring names
html = regexOuter( html, re_globalVars, wrap( "mxsFunctionArgs", "$1" ) ); // coloring global vars ::var
html = regexOuter( html, re_funcArgs, wrap( "mxsFunctionArgs", "$1" ) ); // coloring function arguments
html = regexOuter( html, re_controls, wrap( "mxsControls", "$1" ) ); // coloring maxscript ui controls
html = html.replace( re_values, wrap( "mxsValue", "$1" ) ); // coloring values, operators
html = html.replace(re_comments, wrap( "mxsComments", " $1 " ) ); // coloring comments
// coloring functions
html = regexOuter( html, re_funcDefine, wrap('mxsFunction','$1 ')+wrap('mxsFunctionName','$2')+wrap('mxsFunctionArgs','$3'));
html = html.replace( re_keywords, wrap('mxsKeywords','$1')); // coloring keywords
post.innerHTML = html;
$(post).find(".mxsComments").find("font").each(function(){$(this).contents().unwrap();}); // remove all mxsClass tags inside
$(post).find(".mxsString").find("font").each(function(){$(this).contents().unwrap();}); // remove all mxsClass tags inside
});
// generate style for processed document
var st = document.createElement( 'style' );
st.innerHTML = (new Style( maxscript_styles )).buildStyle();
document.body.appendChild( st );
// use user backround color for code elements
if ( getCookie( "mxsCodeBackgroundColor" ) != null ){
$( posts ).css( 'background-color', getCookie( "mxsCodeBackgroundColor" ) );
}
// Here goes handler and a menu for changing default color style
$("body").bind( 'click', function(e){
if ( e.altKey && e.target.tagName == "FONT" ) {
e.stopPropagation();
showColorChangeMenu( e.target.className, e.pageY );
} else if ( e.altKey && e.target.tagName == "DIV" ){
$( e.target ).colorPicker({
renderCallback: function( $elm, toggled ){
setCookie( 'mxsCodeBackgroundColor', "#" + this.color.colors.HEX );
$( posts ).css( 'background-color', "#" + this.color.colors.HEX );
},
positionCallback: function($elm) {
var $UI = this.$UI, // this is the instance; this.$UI is the colorPicker DOMElement
position = $elm.offset(), // $elm is the current trigger that opened the UI
gap = this.color.options.gap, // this.color.options stores all options
top = position.top + $elm.height()/2,
left = position.left + $elm.width()/2;
// $UI.appendTo('#somwhereElse');
// do here your calculations with top and left and then...
return { // the object will be used as in $('.something').css({...});
left: left,
top: top
}
}
});
}
});
// Third party colorpicker
var colorPickerScript = document.createElement("script");
colorPickerScript.src = "https://cdnjs.cloudflare.com/ajax/libs/tinyColorPicker/1.1.0/jqColorPicker.min.js";
document.body.appendChild( colorPickerScript );
} else {
if ( useMxsHighlighter != "true" ){ // if highlighter is disabled add handler for enabling
$("body").bind( 'click', function(e){
if ( e.altKey ) {
if ( confirm("enable maxscript highlighter?") ) setCookie( "useMxsHighlighter", "true");
}
});
}
}
}