• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.streamhtmlparser.util;
18 
19 import com.google.common.base.Preconditions;
20 
21 import java.util.Arrays;
22 
23 /**
24  * Implements a circular (ring) buffer of characters with specialized
25  * application logic in order to determine the context of some
26  * Javascript content that is being parsed.
27  *
28  * This is a specialized class - of no use to external code -
29  * which aims to be 100% compatible with the corresponding logic
30  * in the C-version of the HtmlParser, specifically
31  * <code>jsparser.c</code>. In particular:
32  * <ul>
33  *   <li> The API is odd, using negative indexes to access content in
34  *        the buffer. Changing the API would mean changing the test
35  *        cases and have more difficulty determining whether we are
36  *        remaining compatible with the C-version. It is left as an
37  *        exercise for once the code is very stable and proven.
38  *   <li> Repeated whitespace is folded into just one character to
39  *        use the space available efficiently.
40  *   <li> The buffer size is fixed. There is currently no need to
41  *        make it variable so we avoid the need for constructors.
42  * </ul>
43  */
44 public class JavascriptTokenBuffer {
45 
46   /**
47    * Size of the ring buffer used to lookup the last token in the javascript
48    * stream. The size is somewhat arbitrary but must be larger than
49    * the biggest token we want to lookup plus three: Two delimiters plus
50    * an empty ring buffer slot.
51    */
52   private static final int BUFFER_SIZE = 18;
53 
54   /** Storage implementing the circular buffer. */
55   private final char[] buffer;
56 
57   /** Index of the first item in our circular buffer. */
58   private int startIndex;
59 
60   /** Index of the last item in our circular buffer. */
61   private int endIndex;
62 
63   /**
64    * Constructs an empty javascript token buffer. The size is fixed,
65    * see {@link #BUFFER_SIZE}.
66    */
JavascriptTokenBuffer()67   public JavascriptTokenBuffer() {
68     buffer = new char[BUFFER_SIZE];
69     startIndex = 0;
70     endIndex = 0;
71   }
72 
73   /**
74    * Constructs a javascript token buffer that is identical to
75    * the one given. In particular, it has the same size and contents.
76    *
77    * @param aJavascriptTokenBuffer the {@code JavascriptTokenBuffer} to copy
78    */
JavascriptTokenBuffer(JavascriptTokenBuffer aJavascriptTokenBuffer)79   public JavascriptTokenBuffer(JavascriptTokenBuffer aJavascriptTokenBuffer) {
80     buffer = Arrays.copyOf(aJavascriptTokenBuffer.buffer,
81                            aJavascriptTokenBuffer.buffer.length);
82     startIndex = aJavascriptTokenBuffer.startIndex;
83     endIndex = aJavascriptTokenBuffer.endIndex;
84   }
85 
86   /**
87    * A simple wrapper over <code>appendChar</code>, it appends a string
88    * to the buffer. Sequences of whitespace and newlines
89    * are folded into one character to save space. Null strings are
90    * not allowed.
91    *
92    * @param input the {@code String} to append, cannot be {@code null}
93    */
94   // TODO: Move to testing since not used in code.
appendString(String input)95   public void appendString(String input) {
96     if (input == null) {
97       throw new NullPointerException("input == null is not allowed");
98     }
99     for (int i = 0; i < input.length(); i++) {
100       appendChar(input.charAt(i));
101     }
102   }
103 
104   /**
105    * Appends a character to the buffer. We fold sequences of whitespace and
106    * newlines into one to save space.
107    *
108    * @param input the {@code char} to append
109    */
appendChar(char input)110   public void appendChar(char input) {
111     if (HtmlUtils.isJavascriptWhitespace(input) &&
112         HtmlUtils.isJavascriptWhitespace(getChar(-1))) {
113       return;
114     }
115     buffer[endIndex] = input;
116     endIndex = (endIndex + 1) % buffer.length;
117     if (endIndex == startIndex) {
118       startIndex = (endIndex + 1) % buffer.length;
119     }
120   }
121 
122   /**
123    * Returns the last character in the buffer and removes it from the buffer
124    * or the NUL character '\0' if the buffer is empty.
125    *
126    * @return last character in the buffer or '\0' if the buffer is empty
127    */
popChar()128   public char popChar() {
129     if (startIndex == endIndex) {
130       return '\0';
131     }
132     endIndex--;
133     if (endIndex < 0) {
134       endIndex += buffer.length;
135     }
136     return buffer[endIndex];
137   }
138 
139   /**
140    * Returns the character at a given index in the buffer or nul ('\0')
141    * if the index is outside the range of the buffer. Such could happen
142    * if the buffer is not filled enough or the index is larger than the
143    * size of the buffer.
144    *
145    * <p>Position must be negative where -1 is the index of the last
146    * character in the buffer.
147    *
148    * @param position The index into the buffer
149    *
150    * @return character at the requested index
151    */
getChar(int position)152   public char getChar(int position) {
153     assert(position < 0);   // Developer error if it triggers.
154 
155     int absolutePosition = getAbsolutePosition(position);
156     if (absolutePosition < 0) {
157       return '\0';
158     }
159 
160     return buffer[absolutePosition];
161   }
162 
163   /**
164    * Sets the given {@code input} at the given {@code position} of the buffer.
165    * Returns {@code true} if we succeeded or {@code false} if we
166    * failed (i.e. the write was beyond the buffer boundary).
167    *
168    * <p>Index positions are negative where -1 is the index of the
169    * last character in the buffer.
170    *
171    * @param position The index at which to set the character
172    * @param input The character to set in the buffer
173    * @return {@code true} if we succeeded, {@code false} otherwise
174    */
175   public boolean setChar(int position, char input) {
176     assert(position < 0);   // Developer error if it triggers.
177 
178     int absolutePosition = getAbsolutePosition(position);
179     if (absolutePosition < 0) {
180       return false;
181     }
182 
183     buffer[absolutePosition] = input;
184     return true;
185   }
186 
187 
188   /**
189    * Returns the last javascript identifier/keyword in the buffer.
190    *
191    * @return the last identifier or {@code null} if none was found
192    */
193   public String getLastIdentifier() {
194     int end = -1;
195 
196     if (HtmlUtils.isJavascriptWhitespace(getChar(-1))) {
197       end--;
198     }
199     int position;
200     for (position = end; HtmlUtils.isJavascriptIdentifier(getChar(position));
201          position--) {
202     }
203     if ((position + 1) >= end) {
204       return null;
205     }
206     return slice(position + 1, end);
207   }
208 
209   /**
210    * Returns a slice of the buffer delimited by the given indices.
211    *
212    * The start and end indexes represent the start and end of the
213    * slice to copy. If the start argument extends beyond the beginning
214    * of the buffer, the slice will only contain characters
215    * starting from the beginning of the buffer.
216    *
217    * @param start The index of the first character the copy
218    * @param end the index of the last character to copy
219    *
220    * @return {@code String} between the given indices
221    */
222   public String slice(int start, int end) {
223     // Developer error if any of the asserts below fail.
224     Preconditions.checkArgument(start <= end);
225     Preconditions.checkArgument(start < 0);
226     Preconditions.checkArgument(end < 0);
227 
228     StringBuffer output = new StringBuffer();
229     for (int position = start; position <= end; position++) {
230       char c = getChar(position);
231       if (c != '\0') {
232         output.append(c);
233       }
234     }
235     return new String(output);
236   }
237 
238   /**
239    * Returns the position relative to the start of the buffer or -1
240    * if the position is past the size of the buffer.
241    *
242    * @param position the index to be translated
243    * @return the position relative to the start of the buffer
244    */
245   private int getAbsolutePosition(int position) {
246     assert (position < 0);   // Developer error if it triggers.
247     if (position <= -buffer.length) {
248       return -1;
249     }
250     int len = endIndex - startIndex;
251     if (len < 0) {
252       len += buffer.length;
253     }
254     if (position < -len) {
255       return -1;
256     }
257     int absolutePosition = (position + endIndex) % buffer.length;
258     if (absolutePosition < 0) {
259       absolutePosition += buffer.length;
260     }
261     return absolutePosition;
262   }
263 }
264