/******************************************************************************* * Copyright (c) 2005, 2016 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * *******************************************************************************/ package org.eclipse.dltk.ruby.internal.ui.text.rules; import java.util.Arrays; import java.util.Comparator; import org.eclipse.dltk.ruby.core.utils.RubySyntaxUtils; import org.eclipse.core.runtime.Assert; import org.eclipse.jface.text.rules.ICharacterScanner; import org.eclipse.jface.text.rules.IPredicateRule; import org.eclipse.jface.text.rules.IToken; import org.eclipse.jface.text.rules.Token; public class RubyPercentStringRule implements IPredicateRule { private static final char ESCAPE = '\\'; /** * Comparator that orders <code>char[]</code> in decreasing array lengths. * * @since 3.1 */ private static class DecreasingCharArrayLengthComparator implements Comparator<char[]> { @Override public int compare(char[] o1, char[] o2) { return o2.length - o1.length; } } /** Internal setting for the un-initialized column constraint */ protected static final int UNDEFINED= -1; /** The token to be returned on success */ protected IToken fToken; /** The pattern's column constrain */ protected int fColumn= UNDEFINED; /** * Indicates whether the escape character continues a line * @since 3.0 */ protected boolean fEscapeContinuesLine; /** Indicates whether end of line terminates the pattern */ protected boolean fBreaksOnEOL; /** Indicates whether end of file terminates the pattern */ protected boolean fBreaksOnEOF; /** * Line delimiter comparator which orders according to decreasing delimiter length. * @since 3.1 */ private Comparator<char[]> fLineDelimiterComparator= new DecreasingCharArrayLengthComparator(); /** * Cached line delimiters. * @since 3.1 */ private char[][] fLineDelimiters; /** * Cached sorted {@linkplain #fLineDelimiters}. * @since 3.1 */ private char[][] fSortedLineDelimiters; /** * Creates a rule for the given starting and ending sequence. * When these sequences are detected the rule will return the specified token. * Alternatively, the sequence can also be ended by the end of the line. * Any character which follows the given escapeCharacter will be ignored. * * @param token the token which will be returned on success * @param breaksOnEOL indicates whether the end of the line also terminates the pattern */ public RubyPercentStringRule(IToken token, boolean breaksOnEOL) { Assert.isNotNull(token); fToken= token; fBreaksOnEOL= breaksOnEOL; fBreaksOnEOF= false; } /** * Sets a column constraint for this rule. If set, the rule's token * will only be returned if the pattern is detected starting at the * specified column. If the column is smaller then 0, the column * constraint is considered removed. * * @param column the column in which the pattern starts */ public void setColumnConstraint(int column) { if (column < 0) column= UNDEFINED; fColumn= column; } /** * Evaluates this rules without considering any column constraints. * * @param scanner the character scanner to be used * @return the token resulting from this evaluation */ protected IToken doEvaluate(ICharacterScanner scanner) { return doEvaluate(scanner, false); } /** * Evaluates this rules without considering any column constraints. Resumes * detection, i.e. look sonly for the end sequence required by this rule if the * <code>resume</code> flag is set. * * @param scanner the character scanner to be used * @param resume <code>true</code> if detection should be resumed, <code>false</code> otherwise * @return the token resulting from this evaluation * @since 2.0 */ protected IToken doEvaluate(ICharacterScanner scanner, boolean resume) { if (resume) { // if (endSequenceDetected(scanner, (char), ']')) // return fToken; } else { int c= scanner.read(); if (c == '%') { if (scanner.getColumn() > 1) { scanner.unread(); scanner.unread(); int b = scanner.read(); scanner.read(); if (b == '<') return Token.UNDEFINED; } char leader = startSequenceDetected(scanner); char term = RubySyntaxUtils.getPercentStringTerminator(leader); if (term != (char) 0) { if (endSequenceDetected(scanner, leader, term)) return fToken; } } } scanner.unread(); return Token.UNDEFINED; } @Override public IToken evaluate(ICharacterScanner scanner) { return evaluate(scanner, false); } protected boolean endSequenceDetected(ICharacterScanner scanner, char lead, char term) { char[][] originalDelimiters= scanner.getLegalLineDelimiters(); int count= originalDelimiters.length; if (fLineDelimiters == null || originalDelimiters.length != count) { fSortedLineDelimiters= new char[count][]; } else { while (count > 0 && fLineDelimiters[count-1] == originalDelimiters[count-1]) count--; } if (count != 0) { fLineDelimiters= originalDelimiters; System.arraycopy(fLineDelimiters, 0, fSortedLineDelimiters, 0, fLineDelimiters.length); Arrays.sort(fSortedLineDelimiters, fLineDelimiterComparator); } int c; int nestCount = 1; int d = 0; while ((c= scanner.read()) != ICharacterScanner.EOF) { d++; if (c == ESCAPE) { // System.out.println("ESCAPE " + (char) c); // Skip escaped character(s) if (fEscapeContinuesLine) { c= scanner.read(); for (int i= 0; i < fSortedLineDelimiters.length; i++) { if (c == fSortedLineDelimiters[i][0] && sequenceDetected(scanner, fSortedLineDelimiters[i], true)) break; } } else scanner.read(); } else if (lead != term && c == lead) { nestCount++; } else if (c == term) { if (--nestCount <= 0) return true; } else if (c == '%') { while (d-- > 0) scanner.unread(); return false; } else if (fBreaksOnEOL) { // System.out.println((char) c); // Check for end of line since it can be used to terminate the pattern. for (int i= 0; i < fSortedLineDelimiters.length; i++) { if (c == fSortedLineDelimiters[i][0] && sequenceDetected(scanner, fSortedLineDelimiters[i], true)) return true; } } } if (fBreaksOnEOF) return true; scanner.unread(); return false; } protected char startSequenceDetected(ICharacterScanner scanner) { int c = scanner.read(); if (c == ICharacterScanner.EOF) { scanner.unread(); return (char) 0; } boolean letterFound = false; if (RubySyntaxUtils.isValidPercentStringStarter((char) c)) { letterFound = true; c = scanner.read(); if (c == ICharacterScanner.EOF) { scanner.unread(); scanner.unread(); return (char) 0; } } char term = RubySyntaxUtils.getPercentStringTerminator((char) c); if (term == (char) 0) { scanner.unread(); if (letterFound) scanner.unread(); return (char) 0; } return (char) c; } protected boolean sequenceDetected(ICharacterScanner scanner, char[] sequence, boolean eofAllowed) { for (int i= 1; i < sequence.length; i++) { int c= scanner.read(); if (c == ICharacterScanner.EOF && eofAllowed) { return true; } else if (c != sequence[i]) { // Non-matching character detected, rewind the scanner back to the start. // Do not unread the first character. scanner.unread(); for (int j= i-1; j > 0; j--) scanner.unread(); return false; } } return true; } @Override public IToken evaluate(ICharacterScanner scanner, boolean resume) { int c= scanner.read(); scanner.unread(); if (c == '%') { // RubyPartitionScanner s = (RubyPartitionScanner) scanner; // if (s.getCurrentContext() == RubyContext.AFTER_EXPRESSION) // return Token.UNDEFINED; return doEvaluate(scanner, resume); } return Token.UNDEFINED; } @Override public IToken getSuccessToken() { return fToken; } }