package de.neuland.jade4j.lexer; import de.neuland.jade4j.exceptions.ExpressionException; import de.neuland.jade4j.exceptions.JadeLexerException; import de.neuland.jade4j.expression.ExpressionHandler; import de.neuland.jade4j.lexer.token.*; import de.neuland.jade4j.template.TemplateLoader; import de.neuland.jade4j.util.CharacterParser; import de.neuland.jade4j.util.Options; import de.neuland.jade4j.util.StringReplacer; import de.neuland.jade4j.util.StringReplacerCallback; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import java.io.IOException; import java.io.Reader; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public class Lexer { private static final Pattern cleanRe = Pattern.compile("^['\"]|['\"]$"); private static final Pattern doubleQuotedRe = Pattern.compile("^\"[^\"]*\"$"); private static final Pattern quotedRe = Pattern.compile("^'[^']*'$"); @SuppressWarnings("unused") private LinkedList<String> options; Scanner scanner; private LinkedList<Token> deferredTokens; private int lastIndents = -1; private int lineno; private LinkedList<Token> stash; private LinkedList<Integer> indentStack; private String indentRe = null; private boolean pipeless = false; @SuppressWarnings("unused") private boolean attributeMode; private final String filename; private final TemplateLoader templateLoader; private String indentType; private CharacterParser characterParser; private ExpressionHandler expressionHandler; public Lexer(String filename, TemplateLoader templateLoader,ExpressionHandler expressionHandler) throws IOException { this.expressionHandler = expressionHandler; this.filename = ensureJadeExtension(filename); this.templateLoader = templateLoader; Reader reader = templateLoader.getReader(this.filename); options = new LinkedList<String>(); scanner = new Scanner(reader); deferredTokens = new LinkedList<Token>(); stash = new LinkedList<Token>(); indentStack = new LinkedList<Integer>(); lastIndents = 0; lineno = 1; characterParser = new CharacterParser(); } public Lexer(String input,String filename, TemplateLoader templateLoader,ExpressionHandler expressionHandler) throws IOException { this.expressionHandler = expressionHandler; this.filename = ensureJadeExtension(filename); this.templateLoader = templateLoader; Reader reader = templateLoader.getReader(this.filename); options = new LinkedList<String>(); scanner = new Scanner(input); deferredTokens = new LinkedList<Token>(); stash = new LinkedList<Token>(); indentStack = new LinkedList<Integer>(); lastIndents = 0; lineno = 1; characterParser = new CharacterParser(); } public Token next() { Token token; if ((token = deferred()) != null) { return token; } if ((token = blank()) != null) { return token; } if ((token = eos()) != null) { return token; } if ((token = pipelessText()) != null) { return token; } if ((token = yield()) != null) { return token; } if ((token = doctype()) != null) { return token; } if ((token = interpolation()) != null) { return token; } if ((token = caseToken()) != null) { return token; } if ((token = when()) != null) { return token; } if ((token = defaultToken()) != null) { return token; } if ((token = extendsToken()) != null) { return token; } if ((token = append()) != null) { return token; } if ((token = prepend()) != null) { return token; } if ((token = block()) != null) { return token; } if ((token = mixinBlock()) != null) { return token; } if ((token = include()) != null) { return token; } if ((token = includeFiltered()) != null) { return token; } if ((token = mixin()) != null) { return token; } if ((token = call()) != null) { return token; } if ((token = conditional()) != null) { return token; } if ((token = each()) != null) { return token; } if ((token = whileToken()) != null) { return token; } if ((token = tag()) != null) { return token; } if ((token = filter()) != null) { return token; } if ((token = blockCode()) != null) { return token; } if ((token = code()) != null) { return token; } if ((token = id()) != null) { return token; } if ((token = className()) != null) { return token; } if ((token = attrs()) != null) { return token; } if ((token = attributesBlock()) != null) { return token; } if ((token = indent()) != null) { return token; } if ((token = text()) != null) { return token; } if ((token = comment()) != null) { return token; } if ((token = colon()) != null) { return token; } if ((token = dot()) != null) { return token; } if ((token = assignment()) != null) { return token; } if ((token = textFail()) != null) { return token; } if ((token = fail()) != null) { return token; } return null; } public void consume(int len) { scanner.consume(len); } public void defer(Token tok) { deferredTokens.add(tok); } public Token lookahead(int n) { int fetch = n - stash.size(); while (fetch > 0) { stash.add(next()); fetch = fetch - 1; } n = n - 1; return this.stash.get(n); } // /** // * Return the indexOf `(` or `{` or `[` / `)` or `}` or `]` delimiters. // * // * @return {Number} // * @api private // */ // // bracketExpression: function(skip){ // skip = skip || 0; // var start = this.input[skip]; // if (start != '(' && start != '{' && start != '[') throw new Error('unrecognized start character'); // var end = ({'(': ')', '{': '}', '[': ']'})[start]; // var range = characterParser.parseMax(this.input, {start: skip + 1}); // if (this.input[range.end] !== end) throw new Error('start character ' + start + ' does not match end character ' + this.input[range.end]); // return range; // }, private CharacterParser.Match bracketExpression(){ return bracketExpression(0); } private CharacterParser.Match bracketExpression(int skip){ char start = scanner.getInput().charAt(skip); if(start != '(' && start != '{' && start != '[') { throw new JadeLexerException("unrecognized start character", filename, getLineno(), templateLoader); } Map<Character,Character> closingBrackets = new HashMap<Character,Character>(); closingBrackets.put('(',')'); closingBrackets.put('{','}'); closingBrackets.put('[',']'); char end = closingBrackets.get(start); Options options = new Options(); options.setStart(skip+1); CharacterParser.Match range; try { range = characterParser.parseMax(scanner.getInput(), options); }catch(CharacterParser.SyntaxError exception){ throw new JadeLexerException(exception.getMessage() + " See "+ StringUtils.substring(scanner.getInput(),0,5), filename, getLineno(), templateLoader); } if(scanner.getInput().charAt(range.getEnd()) != end) throw new JadeLexerException("start character " + start + " does not match end character " + scanner.getInput().charAt(range.getEnd()), filename, getLineno(), templateLoader); return range; } public int getLineno() { return lineno; } public void setPipeless(boolean pipeless) { this.pipeless = pipeless; } public Token advance() { Token t = this.stashed(); return t != null ? t : next(); } // TODO: use multiscan?! private String scan(String regexp) { String result = null; Matcher matcher = scanner.getMatcherForPattern(regexp); if (matcher.find(0) && matcher.group(0)!=null) { int end = matcher.end(); consume(end); return matcher.group(0); } return result; } private String scan1(String regexp) { String result = null; Matcher matcher = scanner.getMatcherForPattern(regexp); if (matcher.find(0) && matcher.groupCount()>0) { int end = matcher.end(); consume(end); return matcher.group(1); } return result; } // private int indexOfDelimiters(char start, char end) { // String str = scanner.getInput(); // int nstart = 0; // int nend = 0; // int pos = 0; // for (int i = 0, len = str.length(); i < len; ++i) { // if (start == str.charAt(i)) { // nstart++; // } else if (end == str.charAt(i)) { // nend = nend + 1; // if (nend == nstart) { // pos = i; // break; // } // } // } // return pos; // } private Token stashed() { if (stash.size() > 0) { return stash.poll(); } return null; } private Token deferred() { if (deferredTokens.size() > 0) { return deferredTokens.poll(); } return null; } /** * Blank line. */ // blank: function() { // var captures; // if (captures = /^\n *\n/.exec(this.input)) { // this.consume(captures[0].length - 1); // ++this.lineno; // if (this.pipeless) return this.tok('text', ''); // return this.next(); // } // }, private Token blank(){ Matcher matcher = scanner.getMatcherForPattern("^\\n *\\n"); if (matcher.find(0)) { consume(matcher.end()-1); ++this.lineno; if(this.pipeless) return new Text("",lineno); return this.next(); } return null; } private Token eos() { if (scanner.getInput().length() > 0) { return null; } if (indentStack.size() > 0) { indentStack.poll(); return new Outdent(lineno); } else { return new Eos("eos", lineno); } } private Token comment() { Matcher matcher = scanner.getMatcherForPattern("^\\/\\/(-)?([^\\n]*)"); if (matcher.find(0) && matcher.groupCount() > 1) { boolean buffer = !"-".equals(matcher.group(1)); Comment comment = new Comment(matcher.group(2), lineno, buffer); consume(matcher.end()); this.pipeless = true; return comment; } return null; } private Token code() { Matcher matcher = scanner.getMatcherForPattern("^(!?=|-)[ \\t]*([^\\n]+)"); if (matcher.find(0) && matcher.groupCount() > 1) { consume(matcher.end()); String flags = matcher.group(1); Expression code = new Expression(matcher.group(2), lineno); code.setEscape(flags.charAt(0) == '='); code.setBuffer(flags.charAt(0) == '=' || flags.length()>1 && flags.charAt(1) == '='); if(code.isBuffer()) try { expressionHandler.assertExpression(matcher.group(2)); } catch (ExpressionException e) { throw new JadeLexerException(e.getMessage(), filename, lineno, templateLoader); } return code; } return null; } // /** // * Interpolated tag. // */ // // interpolation: function() { // if (/^#\{/.test(this.input)) { // var match; // try { // match = this.bracketExpression(1); // } catch (ex) { // return;//not an interpolation expression, just an unmatched open interpolation // } // // this.consume(match.end + 1); // return this.tok('interpolation', match.src); // } // } private Token interpolation(){ Matcher matcher = scanner.getMatcherForPattern("^#\\{"); if (matcher.find(0)) { try { CharacterParser.Match match = this.bracketExpression(1); this.scanner.consume(match.getEnd()+1); return new Interpolation(match.getSrc(),lineno); } catch(Exception ex){ return null; //not an interpolation expression, just an unmatched open interpolation } } return null; } // code: function() { // var captures; // if (captures = /^(!?=|-)([^\n]+)/.exec(this.input)) { // this.consume(captures[0].length); // var flags = captures[1]; // captures[1] = captures[2]; // var tok = this.tok('code', captures[1]); // tok.escape = flags[0] === '='; // tok.buffer = flags[0] === '=' || flags[1] === '='; // return tok; // } // }, private Token tag() { Matcher matcher = scanner.getMatcherForPattern("^(\\w[-:\\w]*)(\\/?)"); if (matcher.find(0) && matcher.groupCount() > 1) { consume(matcher.end()); Tag tok; String name = matcher.group(1); if (':' == name.charAt(name.length() - 1)) { name = name.substring(0, name.length() - 1); tok = new Tag(name, lineno); this.defer(new Colon(lineno)); while (' ' == scanner.getInput().charAt(0)) scanner.consume(1); } else { tok = new Tag(name, lineno); } if (!matcher.group(2).isEmpty()) { tok.setSelfClosing(true); } return tok; } return null; } private Token yield() { Matcher matcher = scanner.getMatcherForPattern("^yield *"); if (matcher.find(0)) { matcher.group(0); int end = matcher.end(); consume(end); return new Yield(lineno); } return null; } private Token filter() { String val = scan1("^:([\\w\\-]+)"); if (StringUtils.isNotBlank(val)) { this.pipeless = true; return new Filter(val, lineno); } return null; } private Token each() { Matcher matcher = scanner.getMatcherForPattern("^(?:- *)?(?:each|for) +([a-zA-Z_$][\\w$]*)(?: *, *([a-zA-Z_$][\\w$]*))? * in *([^\\n]+)"); if (matcher.find(0) && matcher.groupCount() > 2) { consume(matcher.end()); String value = matcher.group(1); String key = matcher.group(2); String code = matcher.group(3); Each each = new Each(value, lineno); each.setCode(code); each.setKey(key); return each; } return null; /* * if (captures = /^(?:- *)?(?:each|for) +(\w+)(?: *, *(\w+))? * in * *([^\n]+)/.exec(this.input)) { this.consume(captures[0].length); var * tok = this.tok('each', captures[1]); tok.key = captures[2] || * '$index'; tok.code = captures[3]; return tok; } */ } private Token whileToken() { String val = scan1("^while +([^\\n]+)"); if (StringUtils.isNotBlank(val)) { return new While(val, lineno); } return null; } private Token conditional() { Matcher matcher = scanner.getMatcherForPattern("^(if|unless|else if|else)\\b([^\\n]*)"); if (matcher.find(0) && matcher.groupCount() > 1) { String type = matcher.group(1); String condition = matcher.group(2); consume(matcher.end()); if ("else".equals(type)) { return new Else(null, lineno); } else if ("else if".equals(type)) { return new ElseIf(condition, lineno); } else { If ifToken = new If(condition, lineno); ifToken.setInverseCondition("unless".equals(type)); return ifToken; } } return null; } /* * private Token conditionalElse() { String val = scan("^(else)"); if * (StringUtils.isNotBlank(val)) { return new Filter(val, lineno); } return * null; } */ /** * Doctype. */ // doctype: function() { // if (this.scan(/^!!! *([^\n]+)?/, 'doctype')) { // throw new Error('`!!!` is deprecated, you must now use `doctype`'); // } // var node = this.scan(/^(?:doctype) *([^\n]+)?/, 'doctype'); // if (node && node.val && node.val.trim() === '5') { // throw new Error('`doctype 5` is deprecated, you must now use `doctype html`'); // } // return node; // }, private Token doctype() { String val = scan1("^!!! *([^\\n]+)?"); if (StringUtils.isNotBlank(val)) { throw new JadeLexerException("`!!!` is deprecated, you must now use `doctype`", filename, getLineno(), templateLoader); } Matcher matcher = scanner.getMatcherForPattern("^(?:doctype) *([^\\n]+)?"); if (matcher.find(0) && matcher.groupCount()>0) { int end = matcher.end(); consume(end); String name = matcher.group(1); if(name != null && "5".equals(name.trim())) throw new JadeLexerException("`doctype 5` is deprecated, you must now use `doctype html`", filename, getLineno(), templateLoader); return new Doctype(name, lineno); } return null; } private Token id() { String val = scan1("^#([\\w-]+)"); if (StringUtils.isNotBlank(val)) { return new CssId(val, lineno); } return null; } private Token className() { String val = scan1("^\\.([\\w-]+)"); if (StringUtils.isNotBlank(val)) { return new CssClass(val, lineno); } return null; } // text: function() { // return this.scan(/^(?:\| ?| )([^\n]+)/, 'text') || // this.scan(/^\|?( )/, 'text') || // this.scan(/^(<[^\n]*)/, 'text'); // }, private Token text() { String val = scan1("^(?:\\| ?| )([^\\n]+)"); if (StringUtils.isEmpty(val)) { val = scan1("^\\|?( )"); if (StringUtils.isEmpty(val)) { val = scan1("^(<[^\\n]*)"); } } if (StringUtils.isNotEmpty(val)) { return new Text(val, lineno); } return null; } private Token textFail() { String val = scan1("^([^\\.\\n][^\\n]+)"); if (StringUtils.isNotEmpty(val)) { return new Text(val, lineno); } return null; } private Token fail() { throw new JadeLexerException("unexpected text " + StringUtils.substring(scanner.getInput(),0,5), filename, getLineno(), templateLoader); } private Token extendsToken() { String val = scan1("^extends? +([^\\n]+)"); if (StringUtils.isNotBlank(val)) { return new ExtendsToken(val, lineno); } return null; } private Token prepend() { String name = scan1("^prepend +([^\\n]+)"); if (StringUtils.isNotBlank(name)) { Block tok = new Block(name, lineno); tok.setMode("prepend"); return tok; } return null; } private Token append() { String name = scan1("^append +([^\\n]+)"); if (StringUtils.isNotBlank(name)) { Block tok = new Block(name, lineno); tok.setMode("append"); return tok; } return null; } private Token block() { Matcher matcher = scanner.getMatcherForPattern("^block\\b *(?:(prepend|append) +)?([^\\n]+)"); if (matcher.find(0) && matcher.groupCount() > 1) { String val = matcher.group(1); String mode = StringUtils.isNotBlank(val) ? val : "replace"; String name = matcher.group(2); Block tok = new Block(name, lineno); tok.setMode(mode); consume(matcher.end()); return tok; } return null; } private Token mixinBlock() { Matcher matcher = scanner.getMatcherForPattern("^block[ \\t]*(\\n|$)"); if (matcher.find(0) && matcher.groupCount() > 0) { consume(matcher.end()-matcher.group(1).length()); return new MixinBlock(lineno); } return null; } private Token blockCode() { Matcher matcher = scanner.getMatcherForPattern("^-\\n"); if (matcher.find(0)) { consume(matcher.end()-1); BlockCode blockCode = new BlockCode(lineno); this.pipeless = true; return blockCode; } return null; } private Token include() { String val = scan1("^include +([^\\n]+)"); if (StringUtils.isNotBlank(val)) { return new Include(val, lineno); } return null; } private Token includeFiltered() { Matcher matcher = Pattern.compile("^include:([\\w\\-]+)([\\( ])").matcher(scanner.getInput()); if(matcher.find(0) && matcher.groupCount()>1){ this.consume(matcher.end()-1); String filter = matcher.group(1); Token attrs = matcher.group(2).equals("(") ? this.attrs():null; if(!(matcher.group(2).equals(" ") || scanner.getInput().charAt(0) == ' ')){ throw new JadeLexerException("expected space after include:filter but got " + String.valueOf(scanner.getInput().charAt(0)), filename, getLineno(), templateLoader); } matcher = Pattern.compile("^ *([^\\n]+)").matcher(scanner.getInput()); if(!(matcher.find(0)&&matcher.groupCount()>0) || matcher.group(1).trim().equals("")){ throw new JadeLexerException("missing path for include:filter", filename, getLineno(), templateLoader); } this.consume(matcher.end()); String path = matcher.group(1); Include tok = new Include(path, lineno); tok.setFilter(filter); tok.setAttrs(attrs); return tok; } return null; } private Token caseToken() { String val = scan1("^case +([^\\n]+)"); if (StringUtils.isNotBlank(val)) { return new CaseToken(val, lineno); } return null; } private Token when() { String val = scan1("^when +([^:\\n]+)"); if (StringUtils.isNotBlank(val)) { return new When(val, lineno); } return null; } private Token defaultToken() { String val = scan1("^(default *)"); if (StringUtils.isNotBlank(val)) { return new Default(val, lineno); } return null; } private Token assignment() { Matcher matcher = scanner.getMatcherForPattern("^(\\w+) += *([^;\\n]+)( *;? *)"); if (matcher.find(0) && matcher.groupCount() > 1) { String name = matcher.group(1); String val = matcher.group(2); consume(matcher.end()); Assignment assign = new Assignment(val, lineno); assign.setName(name); return assign; } return null; } private Token dot() { this.pipeless = true; Matcher matcher = scanner.getMatcherForPattern("^\\."); if (matcher.find(0)) { Dot tok = new Dot(lineno); consume(matcher.end()); return tok; } return null; } private Token mixin() { Matcher matcher = scanner.getMatcherForPattern("^mixin +([-\\w]+)(?: *\\((.*)\\))? *"); if (matcher.find(0) && matcher.groupCount() > 1) { Mixin tok = new Mixin(matcher.group(1), lineno); tok.setArguments(matcher.group(2)); consume(matcher.end()); return tok; } return null; } private Token call() { Call tok; Matcher matcher = scanner.getMatcherForPattern("^\\+(\\s*)(([-\\w]+)|(#\\{))"); if (matcher.find(0) && matcher.groupCount() > 3) { // try to consume simple or interpolated call if(matcher.group(3)!=null) { // simple call consume(matcher.end()); tok = new Call(matcher.group(3), lineno); }else{ // interpolated call CharacterParser.Match match = this.bracketExpression(2 + matcher.group(1).length()); this.consume(match.getEnd() + 1); try { expressionHandler.assertExpression(match.getSrc()); } catch (ExpressionException e) { e.printStackTrace(); } tok = new Call("#{"+match.getSrc()+"}", lineno); } matcher = scanner.getMatcherForPattern("^ *\\("); if (matcher.find(0)) { CharacterParser.Match range = this.bracketExpression(matcher.group(0).length() - 1); matcher = Pattern.compile("^\\s*[-\\w]+ *=").matcher(range.getSrc()); if (!matcher.find(0)) { // not attributes this.consume(range.getEnd() + 1); tok.setArguments(range.getSrc()); } if (tok.getArguments()!=null) { try { expressionHandler.assertExpression("[" + tok.getArguments() + "]"); } catch (ExpressionException e) { e.printStackTrace(); } } } return tok; } return null; } public boolean isEndOfAttribute(int i, String str, String key, String val, Loc loc, CharacterParser.State state) { if (key.trim().isEmpty()) return false; if (i == str.length()) return true; if (Loc.KEY.equals(loc)) { if (str.charAt(i) == ' ' || str.charAt(i) == '\n') { for (int x = i; x < str.length(); x++) { if (str.charAt(x) != ' ' && str.charAt(x) != '\n') { if (str.charAt(x) == '=' || str.charAt(x) == '!' || str.charAt(x) == ',') return false; else return true; } } } return str.charAt(i) == ','; } else if (Loc.VALUE.equals(loc) && !state.isNesting()) { try { expressionHandler.assertExpression(val); if (str.charAt(i) == ' ' || str.charAt(i) == '\n') { for (int x = i; x < str.length(); x++) { if (str.charAt(x) != ' ' && str.charAt(x) != '\n') { if (characterParser.isPunctuator(str.charAt(x)) && str.charAt(x) != '"' && str.charAt(x) != '\'') return false; else return true; } } } return str.charAt(i) == ','; } catch (Exception ex) { return false; } } return false; } private String interpolate(String attr, final String quote) { Pattern regex = Pattern.compile("(\\\\)?#\\{(.+)"); return StringReplacer.replace(attr, regex, new StringReplacerCallback() { @Override public String replace(Matcher m) { String match = m.group(0); String escape = m.group(1); String expr = m.group(2); if (escape != null) return match; try { try { CharacterParser.Match range = characterParser.parseMax(expr); if (expr.charAt(range.getEnd()) != '}') return substr(match, 0, 2) + interpolate(match.substring(2), quote); expressionHandler.assertExpression(range.getSrc()); return quote + " + (" + range.getSrc() + ") + " + quote + interpolate(expr.substring(range.getEnd() + 1), quote); } catch (ExpressionException ex) { return substr(match, 0, 2) + interpolate(match.substring(2), quote); } }catch(CharacterParser.SyntaxError e){ throw new JadeLexerException(e.getMessage()+ " See " + match, filename, getLineno(), templateLoader); } } }); } private String substr(String str, int start, int length) { return str.substring(start, start + length); } private boolean assertNestingCorrect(String exp) { //this verifies that code is properly nested, but allows //invalid JavaScript such as the contents of `attributes` try { CharacterParser.State res = characterParser.parse(exp); if (res.isNesting()) { throw new JadeLexerException("Nesting must match on expression `" + exp + "`", filename, getLineno(), templateLoader); } } catch (CharacterParser.SyntaxError syntaxError) { throw new JadeLexerException("Nesting must match on expression `" + exp + "`", filename, getLineno(), templateLoader); } return true; } private enum Loc { KEY, KEY_CHAR, VALUE, STRING } /** * Attributes. */ private Token attrs() { if ('(' == scanner.getInput().charAt(0)) { int index = this.bracketExpression().getEnd(); String str = scanner.getInput().substring(1, index); AttributeList tok = new AttributeList(getLineno()); assertNestingCorrect(str); String quote = ""; scanner.consume(index + 1); boolean escapedAttr = true; String key = ""; String val = ""; String interpolatable = ""; CharacterParser.State state = characterParser.defaultState(); Loc loc = Loc.KEY; this.lineno += str.split("\n").length - 1; for (int i = 0; i <= str.length(); i++) { if (isEndOfAttribute(i, str, key, val, loc, state)) { val = val.trim(); val = val.replaceAll("\\n",""); if (!val.isEmpty()) try { expressionHandler.assertExpression(val); } catch (ExpressionException e) { throw new JadeLexerException(e.getMessage(), filename, lineno, templateLoader); } val = StringEscapeUtils.unescapeJson(val); key = key.trim(); key = key.replaceAll("^['\"]|['\"]$", ""); if ("".equals(val)) { tok.addBooleanAttribute(key, Boolean.TRUE); } else if (doubleQuotedRe.matcher(val).matches() || quotedRe.matcher(val).matches()) { tok.addAttribute(key, cleanRe.matcher(val).replaceAll(""),escapedAttr); } else { tok.addExpressionAttribute(key, val,escapedAttr); } key = val = ""; loc = Loc.KEY; escapedAttr = false; } else { switch (loc) { case KEY_CHAR: if (String.valueOf(str.charAt(i)).equals(quote)) { loc = Loc.KEY; List<Character> expectedCharacter = Arrays.asList(' ', ',', '!', '=', '\n'); if (i + 1 < str.length() && expectedCharacter.indexOf(str.charAt(i + 1)) == -1) throw new JadeLexerException("Unexpected character " + str.charAt(i + 1) + " expected ` `, `\\n`, `,`, `!` or `=`", filename, getLineno(), templateLoader); } else { key += str.charAt(i); } break; case KEY: if (key.isEmpty() && !str.isEmpty() && (str.charAt(i) == '"' || str.charAt(i) == '\'')) { loc = Loc.KEY_CHAR; quote = String.valueOf(str.charAt(i)); } else if (!str.isEmpty() &&(str.charAt(i) == '!' || str.charAt(i) == '=')) { escapedAttr = str.charAt(i) != '!'; if (str.charAt(i) == '!') i++; if (str.charAt(i) != '=') throw new JadeLexerException("Unexpected character " + str.charAt(i) + " expected `=`", filename, getLineno(), templateLoader); loc = Loc.VALUE; state = characterParser.defaultState(); } else if(!str.isEmpty()){ key += str.charAt(i); } break; case VALUE: state = characterParser.parseChar(str.charAt(i), state); if (state.isString()) { loc = Loc.STRING; quote = String.valueOf(str.charAt(i)); interpolatable = String.valueOf(str.charAt(i)); } else { val += str.charAt(i); } break; case STRING: state = characterParser.parseChar(str.charAt(i), state); interpolatable += str.charAt(i); if (!state.isString()) { loc = Loc.VALUE; val += interpolate(interpolatable, quote); } break; } } } if (scanner.getInput().length()>0 && '/' == scanner.getInput().charAt(0)) { this.consume(1); tok.setSelfClosing(true); } return tok; } return null; } // var captures; // if (/^&attributes\b/.test(this.input)) { // this.consume(11); // var args = this.bracketExpression(); // this.consume(args.end + 1); // return this.tok('&attributes', args.src); // } // }, /** * &attributes block */ private Token attributesBlock() { Matcher matcher = scanner.getMatcherForPattern("^&attributes\\b"); if (matcher.find(0) && matcher.group(0) != null) { this.scanner.consume(11); CharacterParser.Match match = this.bracketExpression(); this.scanner.consume(match.getEnd()+1); return new AttributesBlock(match.getSrc(),lineno); } return null; } private int indexOfDelimiters(char start, char end) { String str = scanner.getInput(); int nstart = 0; int nend = 0; int pos = 0; for (int i = 0, len = str.length(); i < len; i++) { if (start == str.charAt(i)) { nstart++; } else if (end == str.charAt(i)) { if (++nend == nstart) { pos = i; break; } } } return pos; } /* * private Token attributes() { Attribute tok = new Attribute(null, lineno); * Matcher matcher = scanner.getMatcherForPattern("^\\("); if * (matcher.find(0)) { consume(matcher.end()); attributeMode = true; } else * { return null; } * * StringBuilder sb = new StringBuilder(); String regexp = * "^[, ]*?([-_\\w]+)? *?= *?(\"[^\"]*?\"|'[^']*?'|[.-_\\w]+)"; matcher = * scanner.getMatcherForPattern(regexp); if (matcher.find(0)) { while * (matcher.find(0)) { String name = matcher.group(1); String value = * matcher.group(2); tok.addAttribute(name, value); * sb.append(matcher.group(0)); consume(matcher.end()); matcher = * scanner.getMatcherForPattern(regexp); } tok.setValue(sb.toString()); } * else { return null; } * * matcher = scanner.getMatcherForPattern("^ *?\\)"); if (matcher.find(0)) { * consume(matcher.end()); attributeMode = false; } else { throw new * JadeLexerException * ("Error while parsing attribute. Missing closing bracket ", filename, * getLineno(), scanner.getInput()); } return tok; } */ private Token indent() { Matcher matcher; String re; if (indentRe != null) { matcher = scanner.getMatcherForPattern(indentRe); } else { // tabs re = "^\\n(\\t*) *"; String indentType = "tabs"; matcher = scanner.getMatcherForPattern(re); // spaces if (matcher.find(0) && matcher.group(1).length() == 0) { re = "^\\n( *)"; indentType = "spaces"; matcher = scanner.getMatcherForPattern(re); } // established if (matcher.find(0) && matcher.group(1).length() > 0) this.indentRe = re; this.indentType = indentType; } if (matcher.find(0) && matcher.groupCount() > 0) { Token tok; int indents = matcher.group(1).length(); lineno++; consume(indents + 1); if(scanner.getInput().length() > 0 && (scanner.getInput().charAt(0) == ' ' || scanner.getInput().charAt(0) == '\t')){ throw new JadeLexerException("Invalid indentation, you can use tabs or spaces but not both", filename, getLineno(), templateLoader); } // if (lastIndents <= 0 && indents > 0) // lastIndents = indents; // if ((indents > 0 && lastIndents > 0 && indents % lastIndents != 0) || scanner.isIntendantionViolated()) { // throw new JadeLexerException("invalid indentation; expecting " + indents + " " + indentType, filename, getLineno(), templateLoader); // } // blank line if (scanner.isBlankLine()) { this.pipeless = false; return new Newline(lineno); } // outdent if (indentStack.size() > 0 && indents < indentStack.get(0)) { while (indentStack.size() > 0 && indentStack.get(0) > indents) { stash.add(new Outdent(lineno)); indentStack.poll(); } tok = this.stash.pollLast(); // indent } else if (indents > 0 && (indentStack.size() == 0 || indents != indentStack.get(0))) { indentStack.push(indents); tok = new Indent(String.valueOf(indents), lineno); tok.setIndents(indents); // newline } else { tok = new Newline(lineno); } this.pipeless = false; return tok; } return null; } private Token pipelessText(){ if (!this.pipeless) return null; Matcher matcher; String re; // established regexp if (this.indentRe != null) { matcher = scanner.getMatcherForPattern(indentRe); // determine regexp } else { // tabs re = "^\\n(\\t*) *"; matcher = scanner.getMatcherForPattern(re); // spaces if (matcher.find(0) && matcher.group(1).length() == 0) { re = "^\\n( *)"; matcher = scanner.getMatcherForPattern(re); } // established if (matcher.find(0) && matcher.group(1).length() > 0) this.indentRe = re; } if (matcher.find(0) && matcher.group(1).length() > 0) { int indents = matcher.group(1).length(); if (indents > 0 && (this.indentStack.size() == 0 || indents > this.indentStack.get(0))) { String indent = matcher.group(1); ArrayList<String> tokens = new ArrayList<String>(); boolean isMatch = false; do { // text has `\n` as a prefix int i = scanner.getInput().substring(1).indexOf('\n'); if (-1 == i) i = scanner.getInput().length() - 1; String str; str = scanner.getInput().substring(1,i+1); int indentLength = indent.length(); if(str.length()<=indentLength) indentLength = str.length(); isMatch = str.substring(0, indentLength).equals(indent) || !(str.trim().length() > 0); if (isMatch) { // consume test along with `\n` prefix if match this.consume(str.length() + 1); lineno++; tokens.add(str.substring(indentLength)); } } while (scanner.getInput().length() > 0 && isMatch); while (scanner.getInput().length() == 0 && tokens.get(tokens.size() - 1).equals("")) tokens.remove(tokens.size() - 1); PipelessText pipelessText = new PipelessText(lineno); pipelessText.setValues(tokens); return pipelessText; } } return null; } private Token colon() { String val = scan("^: *"); if (StringUtils.isNotBlank(val)) { return new Colon(lineno); } return null; } private String ensureJadeExtension(String templateName) { if ( StringUtils.isBlank(FilenameUtils.getExtension(templateName))) { return templateName + ".jade"; } return templateName; } public boolean getPipeless() { return pipeless; } public LinkedList<Token> getTokens(){ Token t = null; LinkedList<Token> list = new LinkedList<Token>(); while((t = this.advance()) != null){ list.add(t); if(t instanceof Eos) break; } return list; } }