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}