001: /*
002: * Copyright (c) 2003 The Visigoth Software Society. All rights
003: * reserved.
004: *
005: * Redistribution and use in source and binary forms, with or without
006: * modification, are permitted provided that the following conditions
007: * are met:
008: *
009: * 1. Redistributions of source code must retain the above copyright
010: * notice, this list of conditions and the following disclaimer.
011: *
012: * 2. Redistributions in binary form must reproduce the above copyright
013: * notice, this list of conditions and the following disclaimer in
014: * the documentation and/or other materials provided with the
015: * distribution.
016: *
017: * 3. The end-user documentation included with the redistribution, if
018: * any, must include the following acknowledgement:
019: * "This product includes software developed by the
020: * Visigoth Software Society (http://www.visigoths.org/)."
021: * Alternately, this acknowledgement may appear in the software itself,
022: * if and wherever such third-party acknowledgements normally appear.
023: *
024: * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
025: * project contributors may be used to endorse or promote products derived
026: * from this software without prior written permission. For written
027: * permission, please contact visigoths@visigoths.org.
028: *
029: * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
030: * nor may "FreeMarker" or "Visigoth" appear in their names
031: * without prior written permission of the Visigoth Software Society.
032: *
033: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
034: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
035: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
036: * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
037: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
038: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
039: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
040: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
041: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
042: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
043: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
044: * SUCH DAMAGE.
045: * ====================================================================
046: *
047: * This software consists of voluntary contributions made by many
048: * individuals on behalf of the Visigoth Software Society. For more
049: * information on the Visigoth Software Society, please see
050: * http://www.visigoths.org/
051: */
052:
053: package freemarker.core;
054:
055: import java.io.IOException;
056:
057: /**
058: * A TemplateElement representing a block of plain text.
059: * @version $Id: TextBlock.java,v 1.17 2004/01/06 17:06:42 szegedia Exp $
060: */
061: public final class TextBlock extends TemplateElement {
062: private static final char[] EMPTY_CHAR_ARRAY = new char[0];
063: static final TextBlock EMPTY_BLOCK = new TextBlock(
064: EMPTY_CHAR_ARRAY, false);
065: // We're using char[] instead of String for storing the text block because
066: // Writer.write(String) involves copying the String contents to a char[]
067: // using String.getChars(), and then calling Writer.write(char[]). By
068: // using Writer.write(char[]) directly, we avoid array copying on each
069: // write.
070: private char[] text;
071: private final boolean unparsed;
072:
073: public TextBlock(String text) {
074: this (text, false);
075: }
076:
077: public TextBlock(String text, boolean unparsed) {
078: this (text.toCharArray(), unparsed);
079: }
080:
081: private TextBlock(char[] text, boolean unparsed) {
082: this .text = text;
083: this .unparsed = unparsed;
084: }
085:
086: /**
087: * Simply outputs the text.
088: */
089: public void accept(Environment env) throws IOException {
090: env.getOut().write(text);
091: }
092:
093: public String getCanonicalForm() {
094: String text = new String(this .text);
095: if (unparsed) {
096: return "<#noparse>" + text + "</#noparse>";
097: }
098: return text;
099: }
100:
101: public String getDescription() {
102: String s = new String(text).trim();
103: if (s.length() == 0) {
104: return "whitespace";
105: }
106: if (s.length() > 20) {
107: s = s.substring(0, 20) + "...";
108: s = s.replace('\n', ' ');
109: s = s.replace('\r', ' ');
110: }
111: return "text block (" + s + ")";
112: }
113:
114: TemplateElement postParseCleanup(boolean stripWhitespace) {
115: if (text.length == 0)
116: return this ;
117: int openingCharsToStrip = 0, trailingCharsToStrip = 0;
118: boolean deliberateLeftTrim = deliberateLeftTrim();
119: boolean deliberateRightTrim = deliberateRightTrim();
120: if (!stripWhitespace || text.length == 0) {
121: return this ;
122: }
123: if (parent.parent == null && previousSibling() == null)
124: return this ;
125: if (!deliberateLeftTrim) {
126: trailingCharsToStrip = trailingCharsToStrip();
127: }
128: if (!deliberateRightTrim) {
129: openingCharsToStrip = openingCharsToStrip();
130: }
131: if (openingCharsToStrip == 0 && trailingCharsToStrip == 0) {
132: return this ;
133: }
134: this .text = substring(text, openingCharsToStrip, text.length
135: - trailingCharsToStrip);
136: if (openingCharsToStrip > 0) {
137: this .beginLine++;
138: this .beginColumn = 1;
139: }
140: if (trailingCharsToStrip > 0) {
141: this .endColumn = 0;
142: }
143: return this ;
144: }
145:
146: /**
147: * Scans forward the nodes on the same line to see whether there is a
148: * deliberate left trim in effect. Returns true if the left trim was present.
149: */
150: private boolean deliberateLeftTrim() {
151: boolean result = false;
152: for (TemplateElement elem = this .nextTerminalNode(); elem != null
153: && elem.beginLine == this .endLine; elem = elem
154: .nextTerminalNode()) {
155: if (elem instanceof TrimInstruction) {
156: TrimInstruction ti = (TrimInstruction) elem;
157: if (!ti.left && !ti.right) {
158: result = true;
159: }
160: if (ti.left) {
161: result = true;
162: int lastNewLineIndex = lastNewLineIndex();
163: if (lastNewLineIndex >= 0 || beginColumn == 1) {
164: char[] firstPart = substring(text, 0,
165: lastNewLineIndex + 1);
166: char[] lastLine = substring(text,
167: 1 + lastNewLineIndex);
168: if (trim(lastLine).length == 0) {
169: this .text = firstPart;
170: this .endColumn = 0;
171: } else {
172: int i = 0;
173: while (Character.isWhitespace(lastLine[i])) {
174: i++;
175: }
176: char[] printablePart = substring(lastLine,
177: i);
178: this .text = concat(firstPart, printablePart);
179: }
180: }
181: }
182: }
183: }
184: if (result) {
185: }
186: return result;
187: }
188:
189: /**
190: * Checks for the presence of a t or rt directive on the
191: * same line. Returns true if the right trim directive was present.
192: */
193: private boolean deliberateRightTrim() {
194: boolean result = false;
195: for (TemplateElement elem = this .prevTerminalNode(); elem != null
196: && elem.endLine == this .beginLine; elem = elem
197: .prevTerminalNode()) {
198: if (elem instanceof TrimInstruction) {
199: TrimInstruction ti = (TrimInstruction) elem;
200: if (!ti.left && !ti.right) {
201: result = true;
202: }
203: if (ti.right) {
204: result = true;
205: int firstLineIndex = firstNewLineIndex() + 1;
206: if (firstLineIndex == 0) {
207: return false;
208: }
209: if (text.length > firstLineIndex
210: && text[firstLineIndex - 1] == '\r'
211: && text[firstLineIndex] == '\n') {
212: firstLineIndex++;
213: }
214: char[] trailingPart = substring(text,
215: firstLineIndex);
216: char[] openingPart = substring(text, 0,
217: firstLineIndex);
218: if (trim(openingPart).length == 0) {
219: this .text = trailingPart;
220: this .beginLine++;
221: this .beginColumn = 1;
222: } else {
223: int lastNonWS = openingPart.length - 1;
224: while (Character.isWhitespace(text[lastNonWS])) {
225: lastNonWS--;
226: }
227: char[] printablePart = substring(text, 0,
228: lastNonWS + 1);
229: if (trim(trailingPart).length == 0) {
230: // THIS BLOCK IS HEINOUS! THERE MUST BE A BETTER WAY! REVISIT (JR)
231: boolean trimTrailingPart = true;
232: for (TemplateElement te = this
233: .nextTerminalNode(); te != null
234: && te.beginLine == this .endLine; te = te
235: .nextTerminalNode()) {
236: if (te.heedsOpeningWhitespace()) {
237: trimTrailingPart = false;
238: }
239: if (te instanceof TrimInstruction
240: && ((TrimInstruction) te).left) {
241: trimTrailingPart = true;
242: break;
243: }
244: }
245: if (trimTrailingPart)
246: trailingPart = EMPTY_CHAR_ARRAY;
247: }
248: this .text = concat(printablePart, trailingPart);
249: }
250: }
251: }
252: }
253: return result;
254: }
255:
256: /*
257: private String leftTrim(String s) {
258: int i =0;
259: while (i<s.length()) {
260: if (!Character.isWhitespace(s.charAt(i)))
261: break;
262: ++i;
263: }
264: return s.substring(i);
265: }
266: */
267: private int firstNewLineIndex() {
268: String content = new String(text);
269: int newlineIndex1 = content.indexOf('\n');
270: int newlineIndex2 = content.indexOf('\r');
271: int result = newlineIndex1 >= 0 ? newlineIndex1 : newlineIndex2;
272: if (newlineIndex1 >= 0 && newlineIndex2 >= 0) {
273: result = Math.min(newlineIndex1, newlineIndex2);
274: }
275: return result;
276: }
277:
278: private int lastNewLineIndex() {
279: String content = new String(text);
280: return Math.max(content.lastIndexOf('\r'), content
281: .lastIndexOf('\n'));
282: }
283:
284: /**
285: * figures out how many opening whitespace characters to strip
286: * in the post-parse cleanup phase.
287: */
288: private int openingCharsToStrip() {
289: int newlineIndex = firstNewLineIndex();
290: if (newlineIndex == -1 && beginColumn != 1) {
291: return 0;
292: }
293: ++newlineIndex;
294: if (text.length > newlineIndex) {
295: if (newlineIndex > 0 && text[newlineIndex - 1] == '\r'
296: && text[newlineIndex] == '\n') {
297: ++newlineIndex;
298: }
299: }
300: if (new String(text).substring(0, newlineIndex).trim().length() > 0) {
301: return 0;
302: }
303: // We look at the preceding elements on the line to see if we should
304: // strip the opening newline and any whitespace preceding it.
305: for (TemplateElement elem = this .prevTerminalNode(); elem != null
306: && elem.endLine == this .beginLine; elem = elem
307: .prevTerminalNode()) {
308: if (elem.heedsOpeningWhitespace()) {
309: return 0;
310: }
311: }
312: return newlineIndex;
313: }
314:
315: /**
316: * figures out how many trailing whitespace characters to strip
317: * in the post-parse cleanup phase.
318: */
319: private int trailingCharsToStrip() {
320: String content = new String(text);
321: int lastNewlineIndex = lastNewLineIndex();
322: if (lastNewlineIndex == -1 && beginColumn != 1) {
323: return 0;
324: }
325: String substring = content.substring(lastNewlineIndex + 1);
326: if (substring.trim().length() > 0) {
327: return 0;
328: }
329: // We look at the elements afterward on the same line to see if we should
330: // strip any whitespace after the last newline
331: for (TemplateElement elem = this .nextTerminalNode(); elem != null
332: && elem.beginLine == this .endLine; elem = elem
333: .nextTerminalNode()) {
334: if (elem.heedsTrailingWhitespace()) {
335: return 0;
336: }
337: }
338: return substring.length();
339: }
340:
341: boolean heedsTrailingWhitespace() {
342: if (isIgnorable()) {
343: return false;
344: }
345: for (int i = 0; i < text.length; i++) {
346: char c = text[i];
347: if (c == '\n' || c == '\r') {
348: return false;
349: }
350: if (!Character.isWhitespace(c)) {
351: return true;
352: }
353: }
354: return true;
355: }
356:
357: boolean heedsOpeningWhitespace() {
358: if (isIgnorable()) {
359: return false;
360: }
361: for (int i = text.length - 1; i >= 0; i--) {
362: char c = text[i];
363: if (c == '\n' || c == '\r') {
364: return false;
365: }
366: if (!Character.isWhitespace(c)) {
367: return true;
368: }
369: }
370: return true;
371: }
372:
373: boolean isIgnorable() {
374: if (text == null || text.length == 0) {
375: return true;
376: }
377: if (!isWhitespace()) {
378: return false;
379: }
380: boolean atTopLevel = (getParent().getParent() == null);
381: TemplateElement prevSibling = previousSibling();
382: TemplateElement nextSibling = nextSibling();
383: return ((prevSibling == null && atTopLevel) || nonOutputtingType(prevSibling))
384: && ((nextSibling == null && atTopLevel) || nonOutputtingType(nextSibling));
385: }
386:
387: private boolean nonOutputtingType(TemplateElement element) {
388: return (element instanceof Macro
389: || element instanceof Assignment
390: || element instanceof AssignmentInstruction
391: || element instanceof PropertySetting
392: || element instanceof LibraryLoad || element instanceof Comment);
393: }
394:
395: private static char[] substring(char[] c, int from, int to) {
396: char[] c2 = new char[to - from];
397: System.arraycopy(c, from, c2, 0, c2.length);
398: return c2;
399: }
400:
401: private static char[] substring(char[] c, int from) {
402: return substring(c, from, c.length);
403: }
404:
405: private static char[] trim(char[] c) {
406: if (c.length == 0) {
407: return c;
408: }
409: return new String(c).trim().toCharArray();
410: }
411:
412: private static char[] concat(char[] c1, char[] c2) {
413: char[] c = new char[c1.length + c2.length];
414: System.arraycopy(c1, 0, c, 0, c1.length);
415: System.arraycopy(c2, 0, c, c1.length, c2.length);
416: return c;
417: }
418:
419: boolean isWhitespace() {
420: return text == null || trim(text).length == 0;
421: }
422: }
|