001package com.randomnoun.common.jessop.lang; 002 003/* (c) 2016 randomnoun. All Rights Reserved. This work is licensed under a 004 * BSD Simplified License. ( http://www.randomnoun.com/bsd-simplified.html ) 005 */ 006 007import org.apache.log4j.Logger; 008 009import com.randomnoun.common.jessop.AbstractJessopScriptBuilder; 010import com.randomnoun.common.jessop.JessopScriptBuilder; 011 012public class Python2JessopScriptBuilder extends AbstractJessopScriptBuilder implements JessopScriptBuilder { 013 014 Logger logger = Logger.getLogger(Python2JessopScriptBuilder.class); 015 int outputLine = 1; // current line number in the target script; 016 int outputCol = 1; // current output column 017 int lastScriptletLine = 1; // the last line number of the last scriptlet (used for suppressEol) 018 int indent = 0; // current number of spaces at start of line (we use 4-space indents) 019 public Python2JessopScriptBuilder() { 020 } 021 private void skipToLine(int line, int indent) { 022 if (outputLine > line) { 023 // could allow, but then that'll open another can of worms 024 // throw new IllegalArgumentException("cannot generate output on same line as starting new python block"); 025 logger.warn("can't go back to line " + line + " (outputLine=" + outputLine + "); line numbers may be inaccurate"); 026 } 027 while (outputLine < line) { print("\n"); } 028 while (outputCol < indent) { print(" "); } 029 // for (int i=0; i<indent; i++) { print(" "); } 030 } 031 private void print(String s) { 032 // logger.info("** PRINT " + s); 033 pw.print(s); 034 for (int i=0; i<s.length(); i++) { 035 if (s.charAt(i)=='\n') { outputLine++; outputCol = 1; } 036 else { outputCol++; } 037 } 038 } 039 private static String escapePython(String string) { 040 /* valid escapes ( https://docs.python.org/2.0/ref/strings.html ) 041\a bell 042\b back space 043\f form feed 044\n newline 045\r carriage return 046\t horizontal tab 047\v vertical tab 048\\ backslash 049\" double quote 050\' single quote 051 */ 052 053 StringBuilder sb = new StringBuilder(string.length()); 054 String escapeChars = "\u0007" + "\u0008" + "\u000f" + "\n" + "\r" + "\u0009" + "\u000b" + "\\" + "\"" + "'"; 055 String backslashChars = "abfnrtv\\\"'"; 056 for (int i = 0; i<string.length(); i++) { 057 char ch = string.charAt(i); 058 int pos = escapeChars.indexOf(ch); 059 if (pos !=- 1) { 060 sb.append("\\" + backslashChars.charAt(pos)); 061 062 } else if (ch<32 || (ch>126 && ch <= 255)) { 063 String hex = Integer.toString(ch, 16); 064 sb.append("\\x" + "00".substring(0, 2-hex.length()) + hex); 065 sb.append(ch); 066 067 } else if (ch<=255) { 068 sb.append(ch); 069 070 } else { 071 throw new IllegalArgumentException("Cannot escape characters > 0xFF in python2 (found char '" + ch + "'; code=" + ((int) ch) + ")"); 072 } 073 } 074 return sb.toString(); 075 } 076 077 @Override 078 public void emitText(int line, String s) { 079 skipToLine(line, indent); 080 s = suppressEol(s, declarations.isSuppressEol() && lastScriptletLine == line); 081 print("out.write(\"" + escapePython(s) + "\");"); 082 lastScriptletLine = 0; // don't suppress eols on this line 083 } 084 @Override 085 public void emitExpression(int line, String s) { 086 skipToLine(line, indent); 087 print("out.write((str) (" + s + "));"); // coerce to String 088 lastScriptletLine = 0; // don't suppress eols on this line 089 } 090 @Override 091 public void emitScriptlet(int line, String s) { 092 if (outputLine > line) { 093 throw new IllegalArgumentException("cannot generate scriptlet output on same line as starting new python block"); 094 } 095 096 skipToLine(line, indent); 097 // if there's content before the first newline, remove whitespace from the beginning 098 // so that we maintain our line/indentation position 099 int pos = s.indexOf("\n"); if (pos==-1) { pos = s.length(); } 100 int i=0; 101 while (i < s.length() && s.charAt(i)==' ') { i++; } 102 if (s.charAt(i)=='\t') { throw new IllegalArgumentException("tab indentation for python scriplets not supported"); } 103 s = s.substring(i); 104 105 logger.debug("scriptlet is '" + s + "'"); 106 print(s); 107 /* 108 <% 109 for i=1..10: 110 something 111 somethingElse 112 %>no idea whether this is in the for loop or not 113 114 <% 115 for i=1..10: 116 %>presumably this is in the for loop. not sure how to terminate it though. 117 <% 118 pass; # empty statement on a line could indicate end of block 119 %> 120 121 122 * This sort of thing is why whitespace-significant languages should burn in hellfire, 123 * (I'm looking at you, yaml), and why things like jinja are apparently necessary 124 */ 125 126 // get the amount of indentation on the last non-blank line 127 int endLinePos = s.length(); 128 int startLinePos = s.lastIndexOf("\n"); 129 while (s.substring(startLinePos+1).trim().equals("")) { 130 endLinePos = startLinePos; 131 startLinePos = s.lastIndexOf("\n", startLinePos-1); 132 } 133 startLinePos++; // don't include the '\n' 134 135 i = 0; 136 while (startLinePos + i < endLinePos && s.charAt(i)==' ') { i++; } 137 logger.debug("python last newline pos=" + startLinePos + ", indent on last line=" + i); 138 if (s.charAt(startLinePos + i)=='\t') { 139 // could support this later, perhaps 140 throw new IllegalArgumentException("tab indentation for python scriplets not supported"); 141 } 142 indent = i; 143 if (s.substring(startLinePos, endLinePos).trim().endsWith(":")) { 144 logger.debug("last non-blank line endsWith ':', indenting by 4"); 145 // this may mean some loop constructs will now have inaccurate line numbers 146 // e.g. <% for i in range (0,10): %><%= something %> 147 print("\n"); 148 indent += 4; 149 } 150 151 // this should probably go before the indent processing above. maybe. 152 lastScriptletLine = line; 153 for (i=0; i<s.length(); i++) { if (s.charAt(i)=='\n') { lastScriptletLine++; } } 154 } 155 @Override 156 public String getLanguage() { 157 return "python2"; 158 } 159 @Override 160 public String getDefaultScriptEngineName() { 161 return "jython"; 162 } 163 164}