Fix bug #1385272: Improved version for "Parsing of short open tags not fully compatib...
[phpeclipse.git] / net.sourceforge.phpeclipse / src / net / sourceforge / phpeclipse / phpeditor / php / HTMLPartitionScanner.java
1 /**
2  * This program and the accompanying materials
3  * are made available under the terms of the Common Public License v1.0
4  * which accompanies this distribution, and is available at
5  * http://www.eclipse.org/legal/cpl-v10.html
6  * Created on 05.03.2003
7  *
8  * @author Stefan Langer (musk)
9  * @version $Revision: 1.2 $
10  */
11 package net.sourceforge.phpeclipse.phpeditor.php;
12
13 import java.util.ArrayList;
14 import java.util.HashMap;
15 import java.util.Map;
16
17 import net.sourceforge.phpdt.internal.ui.text.IPHPPartitions;
18
19 import org.eclipse.jface.text.Assert;
20 import org.eclipse.jface.text.BadLocationException;
21 import org.eclipse.jface.text.IDocument;
22 import org.eclipse.jface.text.ITypedRegion;
23 import org.eclipse.jface.text.rules.ICharacterScanner;
24 import org.eclipse.jface.text.rules.IPartitionTokenScanner;
25 import org.eclipse.jface.text.rules.IToken;
26 import org.eclipse.jface.text.rules.Token;
27
28 /**
29  * 
30  */
31 public class HTMLPartitionScanner implements IPartitionTokenScanner {
32   private static final boolean DEBUG = false;
33
34   private boolean fInString = false;
35   private boolean fInDoubString = false;
36   private IDocument fDocument = null;
37   private int fOffset = -1;
38   private String fContentType = IPHPPartitions.HTML;
39   private String fPrevContentType = IPHPPartitions.HTML;
40   private boolean partitionBorder = false;
41   private int fTokenOffset;
42   private int fEnd = -1;
43   private int fLength;
44   private int fCurrentLength;
45   private int fFileType;
46   private Map tokens = new HashMap();
47
48   public HTMLPartitionScanner() {
49         this(IPHPPartitions.PHP_FILE);
50   }
51   
52   public HTMLPartitionScanner(int fileType) {
53     this.tokens.put(IPHPPartitions.HTML, new Token(IPHPPartitions.HTML));
54     this.tokens.put(
55       IPHPPartitions.HTML_MULTILINE_COMMENT,
56       new Token(IPHPPartitions.HTML_MULTILINE_COMMENT));
57
58     this.tokens.put(IPHPPartitions.SMARTY, new Token(IPHPPartitions.SMARTY));
59     this.tokens.put(
60       IPHPPartitions.SMARTY_MULTILINE_COMMENT,
61       new Token(IPHPPartitions.SMARTY_MULTILINE_COMMENT));
62
63     this.tokens.put(IDocument.DEFAULT_CONTENT_TYPE, new Token(IDocument.DEFAULT_CONTENT_TYPE));
64     fFileType = fileType;
65   }
66
67   private IToken getToken(String type) {
68     fLength = fCurrentLength;
69     if (DEBUG) {
70
71       try {
72         if (fLength <= 0) {
73           int line = fDocument.getLineOfOffset(fOffset);
74           System.err.println("Error at " + line + " offset:" + String.valueOf(fOffset - fDocument.getLineOffset(line)));
75         }
76       } catch (BadLocationException e) { // should never happen
77         // TODO Write stacktrace to log
78         e.printStackTrace();
79       }
80     }
81     Assert.isTrue(fLength > 0, "Partition length <= 0!");
82     fCurrentLength = 0;
83     // String can never cross partition borders so reset string detection
84     fInString = false;
85     fInDoubString = false;
86     IToken token = (IToken) this.tokens.get(type);
87     Assert.isNotNull(token, "Token for type \"" + type + "\" not found!");
88     if (DEBUG) {
89       System.out.println("Partition: fTokenOffset=" + fTokenOffset + " fContentType=" + type + " fLength=" + fLength);
90     }
91     return token;
92   }
93
94   /* (non-Javadoc)
95    * @see org.eclipse.jface.text.rules.IPartitionTokenScanner#setPartialRange(org.eclipse.jface.text.IDocument, int, int, java.lang.String, int)
96    */
97   public void setPartialRange(IDocument document, int offset, int length, String contentType, int partitionOffset) {
98     if (DEBUG) {
99       System.out.println("*****");
100       System.out.println("PartialRange: contentType=" + contentType + " partitionOffset=" + partitionOffset);
101     }
102
103     try {
104       if (partitionOffset > -1) {
105         partitionBorder = false;
106         // because of strings we have to parse the whole partition
107         this.setRange(document, partitionOffset, offset - partitionOffset + length);
108         // sometimes we get a wrong partition so we retrieve the partition
109         // directly from the document
110         fContentType = fDocument.getContentType(partitionOffset);
111       } else
112         this.setRange(document, offset, length);
113
114     } catch (BadLocationException e) {
115       // should never happen
116       // TODO print stack trace to log
117       // fall back just scan the whole document again
118       this.setRange(document, 0, fDocument.getLength());
119     }
120
121   }
122
123   /* (non-Javadoc)
124    * @see org.eclipse.jface.text.rules.ITokenScanner#getTokenLength()
125    */
126   public int getTokenLength() {
127     return fLength;
128   }
129
130   /* (non-Javadoc)
131    * @see org.eclipse.jface.text.rules.ITokenScanner#getTokenOffset()
132    */
133   public int getTokenOffset() {
134     return fTokenOffset;
135   }
136
137   /* (non-Javadoc)
138    * @see org.eclipse.jface.text.rules.ITokenScanner#nextToken()
139    */
140   public IToken nextToken() {
141     int c;
142
143     // check if we are not allready at the end of the
144     // file
145     if ((c = read()) == ICharacterScanner.EOF) {
146       partitionBorder = false;
147       return Token.EOF;
148     } else
149       unread();
150
151     if (partitionBorder) {
152       fTokenOffset = fOffset;
153       partitionBorder = false;
154     }
155
156     while ((c = read()) != ICharacterScanner.EOF) {
157       switch (c) {
158         case '<' :
159           if ( checkPattern(new char[] { '!', '-', '-' })) { // return previouse partition
160             if (fContentType != IPHPPartitions.HTML_MULTILINE_COMMENT && fCurrentLength > 4) {
161               unread(4);
162               IToken token = getToken(fContentType);
163               fContentType = IPHPPartitions.HTML_MULTILINE_COMMENT;
164               return token;
165             } else
166               fContentType = IPHPPartitions.HTML_MULTILINE_COMMENT;
167
168             fTokenOffset = fOffset - 4;
169             fCurrentLength = 4;
170           }
171           break;
172         case '-' :
173           if (fContentType == IPHPPartitions.HTML_MULTILINE_COMMENT
174             && checkPattern(new char[] { '-', '>' })) {
175             fContentType = IPHPPartitions.HTML;
176             partitionBorder = true;
177             return getToken(IPHPPartitions.HTML_MULTILINE_COMMENT);
178           }
179           break;
180         case '{' : // SMARTY code starts here ?
181           if (fFileType == IPHPPartitions.SMARTY_FILE) {
182             if ((c = read()) == '*') {
183               if (DEBUG) {
184                 System.out.println(
185                   "SMARTYDOC_TOKEN start "
186                     + fTokenOffset
187                     + " fContentType="
188                     + fContentType
189                     + " fLength="
190                     + fLength
191                     + " fOffset="
192                     + fOffset
193                     + " fCurrentLength="
194                     + fCurrentLength);
195               }
196               if (fContentType != IPHPPartitions.SMARTY_MULTILINE_COMMENT && fCurrentLength > 2) {
197                 // SMARTY doc code starts here 
198                 unread(2);
199                 IToken token = getToken(fContentType);
200                 fContentType = IPHPPartitions.SMARTY_MULTILINE_COMMENT;
201                 return token;
202                 //              } else if (fContentType == IPHPPartitionScannerConstants.HTML && fOffset == 2) {
203                 //                fContentType = IPHPPartitionScannerConstants.SMARTY_MULTILINE_COMMENT;
204               } else { // if (fContentType == IPHPPartitionScannerConstants.SMARTY_MULTILINE_COMMENT) {
205                 fContentType = IPHPPartitions.SMARTY_MULTILINE_COMMENT;
206                 fTokenOffset = fOffset - 2;
207                 fCurrentLength = 2;
208               }
209               break;
210             }
211             if (DEBUG) {
212               System.out.println(
213                 "SMARTY_TOKEN start "
214                   + fTokenOffset
215                   + " fContentType="
216                   + fContentType
217                   + " fLength="
218                   + fLength
219                   + " fOffset="
220                   + fOffset);
221             }
222             if (c != ICharacterScanner.EOF) {
223                 unread();
224             }
225             if (fContentType != IPHPPartitions.SMARTY && fCurrentLength > 1) {
226               unread(1);
227               IToken token = getToken(fContentType);
228               fContentType = IPHPPartitions.SMARTY;
229               return token;
230               //            } else if (fContentType == IPHPPartitionScannerConstants.HTML && fOffset==1) {
231               //              fContentType = IPHPPartitionScannerConstants.SMARTY;
232             } else {
233               fContentType = IPHPPartitions.SMARTY;
234               fTokenOffset = fOffset - 1;
235               fCurrentLength = 1;
236             }
237           }
238           break;
239         case '}' : // SMARTY code ends here ?
240           if (fFileType == IPHPPartitions.SMARTY_FILE && fContentType == IPHPPartitions.SMARTY) {
241             if (DEBUG) {
242               System.out.println(
243                 "SMARTY_TOKEN end "
244                   + fTokenOffset
245                   + " fContentType="
246                   + fContentType
247                   + " fLength="
248                   + fLength
249                   + " fOffset="
250                   + fOffset);
251             }
252             fContentType = IPHPPartitions.HTML;
253             partitionBorder = true;
254             return getToken(IPHPPartitions.SMARTY);
255           }
256           break;
257 //        case '/' :
258 //          if (!isInString(IPHPPartitions.PHP_PARTITIONING) && (c = read()) == '*') { // MULTINE COMMENT JAVASCRIPT, CSS, PHP
259 //            if (fContentType == IPHPPartitions.PHP_PARTITIONING && fCurrentLength > 2) {
260 //              unread(2);
261 //              IToken token = getToken(fContentType);
262 //              fContentType = IPHPPartitions.PHP_PHPDOC_COMMENT;
263 //              return token;
264 //            } else if (fContentType == IPHPPartitions.PHP_PHPDOC_COMMENT) {
265 //              fTokenOffset = fOffset - 2;
266 //              fCurrentLength = 2;
267 //            }
268 //
269 //          } else if (!isInString(IPHPPartitions.PHP_PARTITIONING) && c != ICharacterScanner.EOF)
270 //            unread();
271 //          break;
272         case '*' :
273           if (fFileType == IPHPPartitions.SMARTY_FILE && (c = read()) == '}') {
274             if (DEBUG) {
275               System.out.println(
276                 "SMARTYDOC_TOKEN end "
277                   + fTokenOffset
278                   + " fContentType="
279                   + fContentType
280                   + " fLength="
281                   + fLength
282                   + " fOffset="
283                   + fOffset);
284             }
285             if (fContentType == IPHPPartitions.SMARTY_MULTILINE_COMMENT) {
286               fContentType = IPHPPartitions.HTML;
287               partitionBorder = true;
288               return getToken(IPHPPartitions.SMARTY_MULTILINE_COMMENT);
289             }
290           } 
291           break;
292         case '\'' :
293           if (!fInDoubString)
294             fInString = !fInString;
295           break;
296         case '"' :
297           // toggle String mode
298           if (!fInString)
299             fInDoubString = !fInDoubString;
300           break;
301       }
302     } // end of file reached but we have to return the
303     // last partition.
304     return getToken(fContentType);
305   }
306   /* (non-Javadoc)
307    * @see org.eclipse.jface.text.rules.ITokenScanner#setRange(org.eclipse.jface.text.IDocument, int, int)
308    */
309   public void setRange(IDocument document, int offset, int length) {
310     if (DEBUG) {
311       System.out.println("SET RANGE: offset=" + offset + " length=" + length);
312     }
313
314     fDocument = document;
315     fOffset = offset;
316     fTokenOffset = offset;
317     fCurrentLength = 0;
318     fLength = 0;
319     fEnd = fOffset + length;
320     fInString = false;
321     fInDoubString = false;
322     fContentType = IPHPPartitions.HTML;
323     //        String[] prev = getPartitionStack(offset);
324   }
325
326   private int read() {
327     try {
328       if (fOffset < fEnd) {
329         fCurrentLength++;
330         return fDocument.getChar(fOffset++);
331       }
332       return ICharacterScanner.EOF;
333     } catch (BadLocationException e) {
334       // should never happen
335       // TODO write stacktrace to log
336       fOffset = fEnd;
337       return ICharacterScanner.EOF;
338     }
339   }
340
341   private void unread() {
342     --fOffset;
343     --fCurrentLength;
344   }
345
346   private void unread(int num) {
347     fOffset -= num;
348     fCurrentLength -= num;
349   }
350
351   private boolean checkPattern(char[] pattern) {
352     return checkPattern(pattern, false);
353   }
354
355   /**
356    * Check if next character sequence read from document is equals to 
357    * the provided pattern. Pattern is read from left to right until the 
358    * first character read doesn't match. If this happens all read characters are
359    * unread.
360    * @param pattern The pattern to check.
361    * @return <code>true</code> if pattern is equals else returns <code>false</code>.
362    */
363   private boolean checkPattern(char[] pattern, boolean ignoreCase) {
364     int prevOffset = fOffset;
365     int prevLength = fCurrentLength;
366     for (int i = 0; i < pattern.length; i++) {
367       int c = read();
368
369       if (c == ICharacterScanner.EOF || !letterEquals(c, pattern[i], ignoreCase)) {
370         fOffset = prevOffset;
371         fCurrentLength = prevLength;
372         return false;
373       }
374     }
375
376     return true;
377   }
378
379   private boolean letterEquals(int test, char letter, boolean ignoreCase) {
380     if (test == letter)
381       return true;
382     else if (ignoreCase && Character.isLowerCase(letter) && test == Character.toUpperCase(letter))
383       return true;
384     else if (ignoreCase && Character.isUpperCase(letter) && test == Character.toLowerCase(letter))
385       return true;
386
387     return false;
388   }
389
390   /**
391    * Checks wether the offset is in a <code>String</code> and the specified 
392    * contenttype is the current content type.
393    * Strings are delimited, mutual exclusive, by a " or by a '.
394    * 
395    * @param contentType The contenttype to check.
396    * @return <code>true</code> if the current offset is in a string else 
397    *                    returns false.
398    */
399   private boolean isInString(String contentType) {
400     if (fContentType == contentType)
401       return (fInString || fInDoubString);
402     else
403       return false;
404   }
405
406   /**
407    * Returns the previouse partition stack for the given offset.
408    * 
409    * @param offset The offset to return the previouse partitionstack for.
410    * 
411    * @return The stack as a string array.
412    */
413   private String[] getPartitionStack(int offset) {
414     ArrayList types = new ArrayList();
415     int tmpOffset = 0;
416     try {
417       ITypedRegion region = fDocument.getPartition(offset);
418       tmpOffset = region.getOffset();
419       while (tmpOffset - 1 > 0) {
420         region = fDocument.getPartition(tmpOffset - 1);
421         tmpOffset = region.getOffset();
422         types.add(0, region.getType());
423       }
424     } catch (BadLocationException e) {
425       if (DEBUG) {
426         e.printStackTrace();
427       }
428     }
429
430     String[] retVal = new String[types.size()];
431
432     retVal = (String[]) types.toArray(retVal);
433     return retVal;
434   }
435
436 }