package com.laytonsmith.tools; import com.laytonsmith.PureUtilities.ClassLoading.ClassDiscovery; import com.laytonsmith.PureUtilities.Color; import com.laytonsmith.PureUtilities.Common.HTMLUtils; import com.laytonsmith.PureUtilities.Common.StreamUtils; import com.laytonsmith.core.compiler.KeywordList; import java.util.Collections; import java.util.EnumMap; import java.util.Set; import java.util.regex.Pattern; /** * The SimpleSyntaxHighlighter class contains a method to do HTML syntax highlighting on * a given block of plain text code. */ public class SimpleSyntaxHighlighter { public static void main(String[] args){ StreamUtils.GetSystemOut().println(SimpleSyntaxHighlighter.Highlight("a\na\na\na\na\na\na\na\na\na\na\na")); } /** * A list of keywords in the MethodScript language. */ public static final Set<String> KEYWORDS; static { ClassDiscovery.getDefaultInstance().addDiscoveryLocation(ClassDiscovery.GetClassContainer(SimpleSyntaxHighlighter.class)); KEYWORDS = Collections.unmodifiableSet(KeywordList.getKeywordNames()); } private static final EnumMap<ElementTypes, Color> CLASSES = new EnumMap<>(ElementTypes.class); static{ CLASSES.put(ElementTypes.COMMENT, new Color(0x88, 0x88, 0x88)); CLASSES.put(ElementTypes.SINGLE_STRING, new Color(0xFF, 0x99, 0x00)); CLASSES.put(ElementTypes.DOUBLE_STRING, new Color(0xCC, 0x99, 0x00)); CLASSES.put(ElementTypes.VAR, new Color(0x00, 0x99, 0x33)); CLASSES.put(ElementTypes.DVAR, new Color(0x00, 0xCC, 0xFF)); CLASSES.put(ElementTypes.BACKGROUND_COLOR, new Color(0xF9, 0xF9, 0xF9)); CLASSES.put(ElementTypes.BORDER_COLOR, new Color(0xA7, 0xD7, 0xF9)); CLASSES.put(ElementTypes.KEYWORD, Color.BLUE); CLASSES.put(ElementTypes.LINE_NUMBER, new Color(0xBD, 0xC4, 0xB1)); CLASSES.put(ElementTypes.FUNCTION, new Color(0x00, 0x00, 0x00)); } private final EnumMap<ElementTypes, Color> classes; private final String code; private SimpleSyntaxHighlighter(EnumMap<ElementTypes, Color> classes, String code){ this.classes = classes; this.code = code; } private String getColor(ElementTypes type){ Color c = classes.get(type); if(c == null){ c = CLASSES.get(type); } return "color: #" + getRGB(c) + ";"; } private String getRGB(Color c){ return String.format("%02X%02X%02X", c.getRed(), c.getGreen(), c.getBlue()); } private String highlight(){ String[] lines = code.split("\r\n|\n\r|\n"); StringBuilder out = new StringBuilder(); boolean blankLine = false; //We're gonna write a mini parser here, so here's our state variables. boolean inDoubleString = false; boolean inSingleString = false; boolean inLineComment = false; boolean inBlockComment = false; boolean inDollarVar = false; boolean inVar = false; for (int i = 0; i < lines.length; i++) { if (i == 0 && "".equals(lines[0].trim())) { blankLine = true; continue; } StringBuilder lout = new StringBuilder(); if (inBlockComment) { lout.append("<span style=\"").append(getColor(ElementTypes.COMMENT)).append("\">"); } if (inDoubleString) { lout.append("<span style=\"").append(getColor(ElementTypes.DOUBLE_STRING)).append("\">"); } if (inSingleString) { lout.append("<span style=\"").append(getColor(ElementTypes.SINGLE_STRING)).append("\">"); } String buffer = ""; for (int j = 0; j < lines[i].length(); j++) { char c = lines[i].charAt(j); char c2 = (j + 1 < lines[i].length() ? lines[i].charAt(j + 1) : '\0'); if(inSingleString || inDoubleString){ if(c == '\\' && c2 == '\''){ buffer += "\\'"; j++; continue; } if (c == '\\' && c2 == '"') { buffer += "\\""; j++; continue; } if(c == '\\' && c2 == '\\'){ buffer += "\\\\"; j++; continue; } } if (inSingleString && c == '\'') { inSingleString = false; lout.append("<span style=\"").append(getColor(ElementTypes.SINGLE_STRING)).append("\">'").append(HTMLUtils.escapeHTML(buffer)) .append("'</span>"); buffer = ""; continue; } if (inDoubleString && c == '"') { inDoubleString = false; //This also escapes html internally. buffer = processDoubleString(buffer); lout.append("<span style=\"").append(getColor(ElementTypes.DOUBLE_STRING)).append("\">"").append(buffer) .append(""</span>"); buffer = ""; continue; } if (inVar) { if (!Character.toString(c).matches("[a-zA-Z0-9_]")) { lout.append(buffer).append("</span>"); buffer = ""; inVar = false; } } if (inDollarVar) { if (!Character.toString(c).matches("[a-zA-Z0-9_]")) { lout.append(buffer).append("</span>"); buffer = ""; inDollarVar = false; } } if (!inDoubleString && !inSingleString && !inBlockComment && (c == '#' || (c == '/' && c2 == '/'))) { lout.append(processBuffer(buffer)).append("<span style=\"").append(getColor(ElementTypes.COMMENT)).append("\">"); if(c == '#'){ lout.append("#"); } else { lout.append("//"); j++; } buffer = ""; inLineComment = true; continue; } if (!inDoubleString && !inSingleString && !inLineComment && c == '/' && c2 == '*') { lout.append(processBuffer(buffer)); buffer = ""; lout.append("<span style=\"").append(getColor(ElementTypes.COMMENT)).append("\">/*"); j++; inBlockComment = true; continue; } if (c == '\'' && !inDoubleString && !inLineComment && !inBlockComment) { lout.append(processBuffer(buffer)); buffer = ""; inSingleString = true; continue; } if (c == '"' && !inSingleString && !inLineComment && !inBlockComment) { lout.append(processBuffer(buffer)); buffer = ""; inDoubleString = true; continue; } if (c == '*' && c2 == '/') { lout.append(buffer).append("*/</span>"); buffer = ""; j++; inBlockComment = false; continue; } if (!inDoubleString && !inSingleString && !inLineComment && !inBlockComment && c == '@') { lout.append(processBuffer(buffer)).append("<span style=\"").append(getColor(ElementTypes.VAR)).append("\">"); buffer = ""; inVar = true; } if (!inDoubleString && !inSingleString && !inLineComment && !inBlockComment && c == '$') { lout.append(processBuffer(buffer)).append("<span style=\"").append(getColor(ElementTypes.DVAR)).append("\">$"); buffer = ""; if (!Character.toString(c2).matches("[a-zA-Z0-9_]")) { //Done, it's final var lout.append("</span>"); } else { inDollarVar = true; } continue; } buffer += c; } if(inLineComment){ lout.append(buffer); } else { lout.append(processBuffer(buffer)); } if (inBlockComment || inVar || inLineComment || inDollarVar || inSingleString || inDoubleString) { inVar = false; inLineComment = false; inDollarVar = false; lout.append("</span>"); } int lineToOutput = blankLine ? i : i + 1; int lineBufferSize = Integer.toString(lines.length - 1).length(); out.append("<span style=\"font-style: italic; ").append(getColor(ElementTypes.LINE_NUMBER)) .append("\">").append(String.format("%0" + lineBufferSize + "d", lineToOutput)).append("</span>   ").append(lout.toString()).append("<br />\n"); } return "<div style=\"font-family: 'Consolas','DejaVu Sans','Lucida Console',monospace; background-color: #" + getRGB(classes.get(ElementTypes.BACKGROUND_COLOR)) + ";" + " border-color: #" + getRGB(classes.get(ElementTypes.BORDER_COLOR)) + "; border-style: solid; border-width: 1px 0px 1px 0px; margin: 1em 2em;" + " padding: 0 0 0 1em;\">\n" + out.toString().replace("\t", "   ") + "</div>\n"; } private static final String FUNCTION_PATTERN = "([^a-zA-Z0-9_])?([a-zA-Z_]*[a-zA-Z0-9_]+)((?: )*\\()"; /** * Unknown buffer text should be sent here for processing for keywords. * @param buffer * @return */ private String processBuffer(String buffer){ buffer = HTMLUtils.escapeHTML(buffer).replace(" ", " "); for(String keyword : KEYWORDS){ buffer = buffer.replaceAll("([^a-zA-Z0-9_]|^)(" + Pattern.quote(keyword) + ")([^a-zA-Z0-9_]|$)", "$1<span style=\"" + getColor(ElementTypes.KEYWORD) + "\">$2</span>$3"); } buffer = buffer.replaceAll(FUNCTION_PATTERN, "$1<span style=\"font-style: italic; " + getColor(ElementTypes.FUNCTION) + "\">$2</span>$3"); return buffer; } /** * Processes and highlights double strings * @param buffer * @return */ private String processDoubleString(String value){ StringBuilder b = new StringBuilder(); StringBuilder brace = new StringBuilder(); boolean inSimpleVar = false; boolean inBrace = false; for(int i = 0; i < value.length(); i++){ char c = value.charAt(i); char c2 = (i + 1 < value.length() ? value.charAt(i + 1) : '\0'); if(c == '\\' && c2 == '@'){ b.append("\\@"); i++; continue; } if(c == '@'){ if(Character.isLetterOrDigit(c2) || c2 == '_'){ inSimpleVar = true; b.append("<span style=\"").append(getColor(ElementTypes.VAR)).append("\">@"); continue; } else if(c2 == '{'){ inBrace = true; b.append("<span style=\"").append(getColor(ElementTypes.VAR)).append("\">@{</span>"); i++; continue; } } if(inSimpleVar && !(Character.isLetterOrDigit(c) || c == '_')){ inSimpleVar = false; b.append("</span>"); } if(inBrace && c == '}'){ inBrace = false; b.append(processBraceString(brace.toString())); brace = new StringBuilder(); b.append("<span style=\"").append(getColor(ElementTypes.VAR)).append("\">}</span>"); continue; } if(!inBrace){ b.append(HTMLUtils.escapeHTML(Character.toString(c))); } else { brace.append(HTMLUtils.escapeHTML(Character.toString(c))); } } if(inSimpleVar || inBrace){ b.append("</span>"); } return b.toString(); } /** * Brace strings are more complicated, so do this processing separately. * @param v * @return */ private String processBraceString(String value){ StringBuilder b = new StringBuilder(); boolean inVarName = true; boolean inString = false; b.append("<span style=\"").append(getColor(ElementTypes.VAR)).append("\">"); for(int i = 0; i < value.length(); i++){ char c = value.charAt(i); char c2 = (i + 1 < value.length() ? value.charAt(i + 1) : '\0'); if(c == '[' && inVarName){ inVarName = false; b.append("</span>"); } if(c == '[' && !inString){ b.append("<span style=\"").append(getColor(ElementTypes.VAR)).append("\">[</span>"); continue; } if(c == ']' && !inString){ b.append("<span style=\"").append(getColor(ElementTypes.VAR)).append("\">]</span>"); continue; } if(c == '\\' && c2 == '\'' && inString){ b.append("\\'"); continue; } if(c == '\''){ inString = !inString; } b.append(c); } if(inVarName){ b.append("</span>"); } return b.toString(); } /** * Highlights the given code, using the default color scheme. * @param code The plain text code * @return The HTML highlighted code */ public static String Highlight(String code) { return new SimpleSyntaxHighlighter(CLASSES, code).highlight(); } /** * Highlights the given code, using the specified color scheme. If any * elements are missing from the EnumMap, the default color is used. * @param colors A list of colors for each element type * @param code The plain text code * @return The HTML highlighted code */ public static String Highlight(EnumMap<ElementTypes, Color> colors, String code){ return new SimpleSyntaxHighlighter(colors, code).highlight(); } public enum ElementTypes { /** * Denotes a comment, either line or block */ COMMENT, /** * Denotes a string with single quotes */ SINGLE_STRING, /** * Denotes a string with double quotes */ DOUBLE_STRING, /** * Denotes a @var */ VAR, /** * Denotes a $var */ DVAR, /** * The background color of the text field */ BACKGROUND_COLOR, /** * The border color of the text field */ BORDER_COLOR, /** * Denotes a keyword, "true", "false", "null", etc. */ KEYWORD, /** * The color of the line numbers in the left margin */ LINE_NUMBER, /** * The color of function/proc names */ FUNCTION, ; } }